whcl412's picture
README: 新增 MLX 无损版 bf16 入口,明确 4bit 为有损、再加工需用无损版
996c47a verified
|
Raw History Blame Contribute Delete
26.5 kB
---
license: apache-2.0
tags:
- code
- minicpm
- chinese
- conversational
- tool-calling
- mlx
- quantization
base_model: openbmb/MiniCPM5-2B
pipeline_tag: text-generation
library_name: mlx
---
# LycheeAI-coder-2b-II-pro · MLX 4bit
> **v7** —— 用「精选 1000 条 × 1 epoch」替代「全量 5341 条 × 0.22 epoch」,
> 验收 26/27 通过(v6.1 是 25/27)。
[中文](#中文) | [English](#english)
> 基于 [MiniCPM5-2B](https://huggingface.co/openbmb/MiniCPM5-2B) 微调的轻量编程助手 · Apache 2.0
> **2B 参数,主打本地工具调用**
**这是个人开发者的项目,2B 参数注定它只是个"本地小助手"。** 它比 1B 版更能干,但请别用 GPT / Claude / DeepSeek 的标准要求它。
它最大的用处是:**跑在你自己的机器上,不用联网,能调工具,响应飞快。**
**本仓库是 MLX 4bit 量化版本**,1.4 GB,Apple Silicon 上开箱即用。
**四个版本,按需要选:**
| 版本 | 体积 | 用在什么场景 |
|---|---|---|
| **MLX 4bit**(本仓库) | 1.42 GB | Mac 上日常跑,最省内存、最快 |
| [MLX 无损 bf16](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-MLX-bf16) | 5.03 GB | **要质量基准 / 二次量化 / 继续微调** |
| [-f16](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-f16) | 5.03 GB | 同上(**同一份权重**,方便 transformers 用户找) |
| [GGUF](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-GGUF) | 1.45 / 2.50 / 4.69 GB | Ollama、llama.cpp |
> **MLX 无损版(bf16)**是给"要在它身上再加工"的人用的:4bit 是有损的,不能作为二次量化或续训的基础。
> 它和 `-f16` 仓库是**同一份权重**(字节相同),两个名字只是为了让 MLX 用户和 transformers 用户都能找到。
> 格式是标准 safetensors,**MLX 和 transformers 都能直接加载**。
---
# 中文
## 这一版做了什么
上一版(1b-II)最大的短板是**工具调用完全不会**——训练数据里一条工具样本都没有。这一版专门补上了这块:
| 能力 | 1b-II | **2b-II-pro** |
|---|---|---|
| 工具调用格式 | ❌ 全错、字段编造 | ✅ 裸 JSON,格式稳定 |
| 单步调用 | ❌ | ✅ |
| 多步调用 | ❌ | ✅ |
| 拒绝不该做的事 | ⚠️ 时好时坏 | ✅ |
| 结果解读 | ❌ | ✅ |
**基座换了**:从 MiniCPM5-1B 换成 MiniCPM5-2B(面壁智能,Apache 2.0,512K 上下文)。
---
## 快速开始
### MLX(Apple Silicon,本仓库)
```bash
pip install mlx-lm
# 直接加载(4bit,1.4 GB)
python -m mlx_lm.generate \
--model whcl412/LycheeAI-coder-2b-II-pro \
--prompt "帮我写个快速排序"
# 起个 OpenAI 兼容的服务
python -m mlx_lm.server --model whcl412/LycheeAI-coder-2b-II-pro --port 8080
```
> **要在 MLX 上用无损版**(二次量化、继续微调、要质量基准),把模型名换成
> `whcl412/LycheeAI-coder-2b-II-pro-MLX-bf16` 即可(5.03 GB,MLX 直接加载)。
> 手工拼 prompt 时记得套 chat template 并传 `enable_thinking=False`,否则会复读乱码。
### transformers
本仓库是 **MLX 4bit 格式**,transformers 不能直接加载。
需要 HF safetensors 请用无损版 → [`whcl412/LycheeAI-coder-2b-II-pro-MLX-bf16`](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-MLX-bf16)(或等价的 [`-f16`](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-f16))。
### Ollama(GGUF)
已发布 → [`whcl412/LycheeAI-coder-2b-II-pro-GGUF`](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-GGUF)
三档可选:`q4_k_m`(1.45 GiB)/ `q8_0`(2.50 GiB,推荐)/ `f16`(4.69 GiB 无损)。
仓库里带 `Modelfile`,下载后 `ollama create` 即可用。国内可用 [ModelScope 镜像](https://modelscope.cn/models/whcl412/LycheeAI-coder-2b-II-pro-GGUF)。
⚠️ 注意:Ollama 的**自动 `tool_calls` 解析对这个小模型不可靠**——它是按「直接输出 JSON」训练的,不带运行时期望的包裹标签。生产环境请从 `content` 里自己解析 JSON,详见 GGUF 仓库 README。
---
## 工具调用
### 输出格式
它输出的是**裸 JSON**(OpenAI function calling 风格),不带任何包裹:
```json
{"name": "get_weather", "arguments": {"city": "北京"}}
```
**没有** ```json 代码块围栏、**没有** `<tool_call>` 标签、**没有**"好的我来调用工具"这种旁白。
### 单步调用
```
system: 你是 LycheeAI-coder-2b-II-pro。可以使用工具来帮助用户。
可用工具:
- get_weather(city: string): 查询指定城市的天气
user: 北京今天天气怎么样?
```
输出:
```json
{"name": "get_weather", "arguments": {"city": "北京"}}
```
### 多步调用(有依赖时只输出第一个)
问"北京今天适合出门吗",它需要先查天气再判断,此时**只输出第一步**:
```json
{"name": "get_weather", "arguments": {"city": "北京"}}
```
你把工具结果喂回去,它再继续。**宿主需要循环驱动**:解析 → 执行 → 喂回结果 → 再解析。
### 完全独立的调用会并列输出
同时查北京和上海,它会一次给两行:
```json
{"name": "get_weather", "arguments": {"city": "北京"}}
{"name": "get_weather", "arguments": {"city": "上海"}}
```
### 该拒绝时会明确拒绝
不是所有请求都调工具。超出能力或不合规的,它会说明原因并给替代方案,不会硬编一个工具调用糊弄过去。
---
## 宿主接入注意
### 格式不是通用的
**这一点很重要**:不同 agent 框架的工具调用格式不一样。
| 框架/模型 | 格式 |
|---|---|
| OpenAI / 大多数本地框架 | 裸 JSON `{"name":..., "arguments":{...}}` |
| Anthropic / Claude Tools | XML 标签 `<function_calls><invoke name="...">` |
| 部分国产模型 | `<tool_call>` 自定义标签包 JSON |
| MCP | 传输层协议,内部通常还是 JSON-RPC |
**它默认输出裸 JSON**。如果你的宿主需要别的格式,在 system 里明确告诉它,它会按你说的来。
### 什么时候给工具,什么时候别给
它训练时见过四类场景,行为是分开的:
| 类别 | 例子 | 它的行为 |
|---|---|---|
| 工具能算出确定结果 | `789*123`、`1000 美元换人民币` | 调工具 |
| 能力/伦理不允许 | 订机票、破解 WiFi、伪造证明 | 明确拒绝 + 给替代方案 |
| 工具帮不上忙 | 什么是递归、写二分查找 | 直接回答 |
| **宿主没有工具** | 纯聊天软件里的"帮我搜一下" | 说明没有这个能力,不硬调 |
**最后一行很关键**:如果你的宿主是纯聊天(微信、客服机器人),**不要给它任何工具定义**,它会自然地说"我这边没有联网能力",而不是吐一坨宿主接不住的 JSON。
**如果你给它 `calculate` 却不希望它算简单加减**,可以在 system 里说明。默认行为是"算术优先用工具"。
---
## 训练信息
| 项 | 值 |
|---|---|
| 基座 | `openbmb/MiniCPM5-2B` |
| 方法 | LoRA(rank 8,dropout 0,scale 20,16 层) |
| 数据量 | 980 条(精选,v7) |
| 轮次 | 1 epoch(490 步) |
| 学习率 | 5e-5(adam) |
| batch / seq | 2 / 2048 |
| 框架 | MLX-LM 0.31.3 |
| 本版本量化 | 4bit affine(group_size 64,4.501 bits/weight) |
### 数据构成(v6 系列)
v6 是在 v5 基础上做的**定向修复**,共四批:
| 批次 | 条数 | 解决什么 |
|---|---|---|
| v5 基础 | 4,526 | v6.1 起点(v5 + OOD 初版) |
| v6.1 | 4,526 | 修 v6 的 ⑥⑨ 退步项,OOD 补强 |
| v6.2 | 4,778 | 补算术泛化(K6,252 条程序化生成) |
| v6.3 | 5,093 | 补能力边界 + 结果收尾(K7,315 条) |
| **v6.4** | 5,341 | 补运算类型均衡(K8,248 条) |
**发布的是 v6.1**(不是 v6.4)。原因见下面"关于迭代"。
### 三类行为严格区分
这是训练中最容易搞混的地方:
| 类别 | 例子 | 正确行为 |
|---|---|---|
| 工具调用 | `789*123` | 调 `calculate` |
| **真拒绝**(能力/伦理不允许) | 订机票、破解 WiFi | 说不做 + 给替代方案 |
| **无需工具** | 什么是递归、写二分查找 | 直接回答,不提工具 |
混为一谈会导致模型把正常提问也当成要拒绝的事——踩过这个坑。
---
## 能力边界
**能做:**
- 写代码、调 bug、解释技术概念
- 单步 / 多步工具调用(裸 JSON)
- 拒绝能力范围外或不合规的请求
- 解读工具返回结果
- 中文 / 英文 / 粤语
**不擅长:**
- 复杂多步推理(2B 容量限制)
- 超长上下文(虽然基座支持 512K,但实际长文表现没测)
- **无工具时的算术**(会算错,请给 `calculate`——见下面"已知问题")
- 自称身份的 OOD 场景(见下面"已知问题")
---
## ⚠️ 已知问题
### 1. 无工具时会心算,而且**算错**
这是当前最实际的短板。实测:
```
1234 + 5678 = 2000 (正确 6912)
35 ** 2 = 70 (正确 1225)
13.5 × 4 = 7.8 (正确 54)
199 × 257 = 40043 (正确 51143)
```
**根因**(做过对照实验确认):模型是按**运算类型**决定调不调工具的,而这跟训练数据里各类型的样本量完全对应。
| 运算类型 | 训练样本量 | 行为 |
|---|---|---|
| 乘法 `*` | 432 | ✅ 调工具 |
| 加法 `+` | 86 | ❌ 心算 |
| 幂 `**` | 61 | ❌ 心算 |
| 小数 | 61 | ❌ 心算且算错 |
| 减法 `-` | 49 | ❌ 心算 |
不是"字符串记忆"(见过 38% vs 没见过 50%,差不多),也不是"难度"(`1234+5678` 也在心算)。
**规避方法**:**只要涉及算术,就在 system 里明确要求"任何计算都必须用 calculate"**:
```
需要任何数值计算时,一律调用 calculate,不要自己心算。
```
这条指令实测有效。v6.4 试图用数据修正(K8,按运算类型均衡),但引入了更严重的副作用(工具调用率从 60%+ 崩到 12%),所以**发布了 v6.1 而没有发布 v6.4**。
### 2. 身份问答在「宿主提供的通用 system」下会出错
如果你的 system prompt 是这样(很多 agent 框架的默认写法):
```
你是一个AI助手,可以使用工具来帮助用户。
可用工具:
- get_weather(city: str): 查询指定城市的天气
...
需要调用工具时,请直接输出 JSON:{"name": "工具名", "arguments": {...}}
```
那么问"你叫什么名字"时,它可能乱答(实测出现过"我叫 Qwen")。
**但如果 system 是它训练时的格式,就完全正常:**
```
你是 LycheeAI-coder-2b-II-pro,由 MiniCPM5-2B 通过 LoRA 微调而来的编程助手。
```
→ 答"我叫 LycheeAI-coder-2b-II-pro,由 MiniCPM5-2B 微调而来的编程助手。"
**原因**:训练数据的身份样本里,system 全部是上面第二种。模型没见过"通用 system + 工具列表 + 问身份"这个组合,没有训练信号。
**规避方法**:接入时把身份写进 system。比如:
```
你是 LycheeAI-coder-2b-II-pro。可以使用工具来帮助用户。
可用工具:
...
```
### 3. 两种 system 结构的一致性
训练数据混了两种结构:工具定义放 `system` / 放 `user`。实测两种都能用,但**放 system 更稳**(这也是主流 agent 框架的做法)。
### 4. 工具结果有两种结构,宿主需要知道
模型见过两种上下文,**两种都认**:
**结构 A** —— 有明确的调用轮(主流 agent 框架的样子):
```
user: 789*123 等于多少
assistant: {"name": "calculate", "arguments": {"expression": "789*123"}}
user: [工具调用结果] {"name": "calculate", "result": {"value": 97047}}
assistant: 789 × 123 = **97047**。
```
**结构 B** —— 宿主已代为执行,直接把结果给模型(67 条早期样本用这个):
```
user: 跑一下这段代码
[工具调用结果] {"exit_code": 0, "stdout": "45\n"}
assistant: 执行成功,输出 `45`。
```
**区别**:结构 B 的结果**不带 `name`**,因为上下文里本来就没有调用轮,工具身份从 system 的工具列表和问题本身就能推断。
**建议**:正常接入用**结构 A**(多轮循环)。只有在你已经知道该调什么、只想让模型解读结果时,才用结构 B。
---
## v7 做了什么:用「精选」代替「全量」
### 起因:一个 500 步的硬约束
新版训练被限制在 **500 步以内**。而 `步数 = ceil(条数 / batch_size)`,batch=2 → **最多 1000 条**。
原来的数据集有 5,341 条,两条路:
| 方案 | 效果 |
|---|---|
| 5,341 条 × 250 步 | 每条样本只被见到 **0.22 次**(22% epoch) |
| **980 条 × 490 步** | 每条都被完整学到 **1 次**(100% epoch)✅ |
**为什么第二条更好**——不只是"学得够",而是**行为惯性按样本条数加权**:
原数据集里 **51.4% 是单轮纯文本**(普通聊天),它们把梯度拉向"纯聊天";而"多轮完整链路"(调用→拿结果→收尾,宿主真实交互形态)只占 10.7%。在 22% epoch 下,每条多轮样本平均只贡献 0.22 次更新——**根本学不到**。
### 重新配比:按行为重要性,而不是按原始比例
| 类别 | 原数据集 | v7 精选 | 说明 |
|---|---|---|---|
| 多轮链路(调用→结果→收尾) | 569 | **201(20.5%)** | 最值钱,宿主真实路径 |
| 工具在 user + 调用 | 780 | 243(24.8%) | 占原始 66%,结构不能丢 |
| 有工具但不调 | 819 | 216(22.0%) | 压住"手痒调工具"的反面样本 |
| 工具在 system + 调用 | 397 | 179(18.3%) | 主结构 |
| 单轮纯文本 | 2,746 | **130(13.3%)** | 从 51.4% 砍到 13.3% |
### 算术问题的真正解法:两头夹
之前的分析已经找到根因——**乘法独占 50.5% 的样本**,所以模型学到的是"乘法 → 调工具"这个**特例**,而不是"算术 → 调工具"这个**通则**。
v6.4 试过只做"补"(补 248 条稀缺类型),结果**配比反而更差**,还把工具调用率从 60%+ 砸到 12%。
v7 用**两头夹**:
```
一头补稀缺:K9 生成 90 条,按稀缺度加权(小数加法最缺 → 补最多)
一头削过剩:对基线里超量的乘法样本下采样,删掉 48 条
```
结果——乘法占比 **50.5% → 20.0%**,七个运算类型的最热/最冷差距从 **30.5 倍降到 2.4 倍**:
| 运算类型 | v6.1 | v7 |
|---|---|---|
| 乘法 | 244(50.5%) | 81(20.0%) |
| 幂 | 44(9.1%) | 66(16.3%) |
| 除法 | 53(11.0%) | 62(15.3%) |
| 加法 | 71(14.7%) | 54(13.3%) |
| 小数乘法 | 31(6.4%) | 47(11.6%) |
| 减法 | 31(6.4%) | 42(10.4%) |
| 小数加法 | 8(1.7%) | 35(8.6%) |
**直接效果**:v7 修好了两个长期存在的验收失败项——①b 第二道算术题、⑧b 多轮追问。
### 修掉一个源数据缺陷:工具名幻觉
质检时发现 **9 处自相矛盾的样本**:system 里只定义了 `calculate`,模型却去调 `read_file` / `get_weather`。
根因在数据生成脚本:「多轮追问」那批轨迹里有些是查天气、读文件,但 system 被写死只定义 `calculate`。**这等于在教模型"调用没定义的工具"**。
修法:先扫轨迹实际调用了哪些工具,只定义那些。
> 但修完发现新问题:「见过 ≥3 种工具」的样本从 56 条掉到 0 条——模型失去了「在多个候选里挑对的」训练信号。于是又加了**干扰工具**(2/3 的样本掺入 1~3 个无关工具,但不掺 `calculate`),恢复到 24 条。
>
> **教训:修一个问题很容易带出另一个。每次修改后必须重新做全量质检。**
### 一个影响深远的方法论修正:稳定采样
第一次尝试修验收 ⑥ 时,我调高了某类配额、砍了另一类,结果 **26/1 变成 23/4**——修一个坏三个。
查数据才发现真因:**改配额会让 40% 的样本被重洗**(两版只有 585 条共有)。等于每做一次实验就换了个数据集,任何结论都不可归因。**这正是 v6 系列"修一个坏一个"的机制。**
修法:把采样改成**按内容哈希排序**,改配额变成"单调扩展"——原来选中的样本仍然全部在,只多补差额。
验证:只改一类配额(260→220),**重叠率 99.9%**。
### 验收结果
| 版本 | 验收 | 说明 |
|---|---|---|
| v6.1(上一版) | 25 / 2 | ❌ ①b 第二道算术、❌ ⑧b 多轮追问 |
| **v7(本版)** | **26 / 1** | ✅ ①b、⑧b 全修好;❌ ⑥ 工具放 user |
**26/1 已经过复现验证**:同一份数据独立重跑一次训练,结果完全一致(含失败项相同)。
### 为什么没继续修 ⑥
⑥ 的考点是"工具定义放在 user 消息里"(而不是 system)。v7 在这一项退化成心算。
后续试了两个方向:
- **v7b**:调高 user 结构配额 + 砍多轮样本 → **23/4**,修一个坏三个
- **v7c**:不动配额,纯追加 90 条该考点样本 → **22/5**,工具调用倾向被整体稀释
三次尝试指向同一个结论:
> **在 1000 条预算内,任何"定向补强"都会挤掉其他能力。26/1 已经是这个预算下的最优解。**
⑥ 属于**兼容性场景**,不是核心能力。如果你的宿主把工具定义放在 user 消息里,请在 system 中补一句身份/工具说明(见上面「已知问题 3」),或改用把工具定义放 system 的标准写法。
### 五轮迭代的完整复盘
| 版本 | 验收 | 结果 |
|---|---|---|
| v6 | 24 / 3 | ⑥⑨ 坏,①b 好 |
| v6.1 | 25 / 2 | ✅ 曾发布 |
| v6.2 | 24 / 3 | 修好 ①b,但 ⑧a ⑨g 退步 |
| v6.3 | 25 / 2 | 修好 ⑧a ⑨g,①b 又退;**逐项与 v6.1 相同(等于白跑)** |
| v6.4 | 25/2 + ④坏 | 调用率崩到 12% |
| **v7** | **26 / 1** | ✅ 本版发布 |
| v7b | 23 / 4 | 尝试修 ⑥ 失败 |
| v7c | 22 / 5 | 尝试修 ⑥ 失败(另一方向) |
**核心教训**:v6 系列每轮都是「发现问题 → 补 5% 数据 → 全量重训」,把整个能力分布重洗一遍,结果**修一个坏一个**。**补丁数据的效力取决于它占新数据集的比例,不是它的绝对条数**——v6.4 补了 248 条去对冲 244 条乘法基线,占比只有 4.6%,压不住已有惯性。
v7 换了方法:**改配比,不改增量**。不往大数据集里塞补丁,而是重新精选整个数据集。
---
## 许可
Apache 2.0,跟基座 MiniCPM5-2B 一致。
训练数据、脚本都在项目里,欢迎 issue 交流。
---
# English
## What's new
The biggest gap in the previous release (1b-II) was that it **couldn't call tools at all** — the training data contained zero tool-use samples. This release is built specifically to fix that:
| Capability | 1b-II | **2b-II-pro** |
|---|---|---|
| Tool-call format | ❌ malformed, invented fields | ✅ bare JSON, stable |
| Single-step calls | ❌ | ✅ |
| Multi-step calls | ❌ | ✅ |
| Refusing what it shouldn't do | ⚠️ inconsistent | ✅ |
| Interpreting tool results | ❌ | ✅ |
**New base model**: switched from MiniCPM5-1B to MiniCPM5-2B (OpenBMB, Apache 2.0, 512K context).
**This repo is the MLX 4-bit quantized build**, 1.4 GB, ready to run on Apple Silicon.
**Four builds, pick by use case:**
| Build | Size | Use case |
|---|---|---|
| **MLX 4-bit** (this repo) | 1.42 GB | Everyday use on a Mac — fastest, smallest footprint |
| [MLX lossless bf16](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-MLX-bf16) | 5.03 GB | **Quality baseline / re-quantization / further fine-tuning** |
| [-f16](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-f16) | 5.03 GB | Same as above (**identical weights**, named for transformers users) |
| [GGUF](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-GGUF) | 1.45 / 2.50 / 4.69 GB | Ollama, llama.cpp |
> **The MLX lossless (bf16) build** is for anyone who needs to *build on top of* this model —
> 4-bit is lossy and unsuitable as a base for re-quantization or continued training.
> It holds **byte-identical weights** to the `-f16` repo; the two names exist so both MLX and
> transformers users can find it. It's a standard safetensors checkpoint — **loadable by MLX and transformers alike**.
---
## Quick start
### MLX (Apple Silicon, this repo)
```bash
pip install mlx-lm
python -m mlx_lm.generate \
--model whcl412/LycheeAI-coder-2b-II-pro \
--prompt "write a quicksort"
python -m mlx_lm.server --model whcl412/LycheeAI-coder-2b-II-pro --port 8080
```
### transformers
This repo is in **MLX 4-bit format** and cannot be loaded by transformers.
Use the lossless build for HF safetensors → [`whcl412/LycheeAI-coder-2b-II-pro-MLX-bf16`](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-MLX-bf16) (or the equivalent [`-f16`](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-f16)).
### Ollama (GGUF)
Published → [`whcl412/LycheeAI-coder-2b-II-pro-GGUF`](https://huggingface.co/whcl412/LycheeAI-coder-2b-II-pro-GGUF)
Three variants: `q4_k_m` (1.45 GiB) / `q8_0` (2.50 GiB, recommended) / `f16` (4.69 GiB, lossless).
The repo ships a `Modelfile` — download, then `ollama create`.
⚠️ Note: Ollama's **automatic `tool_calls` parsing is unreliable for this small model** — it was trained to emit bare JSON, without the wrapper tags runtimes expect. For production, parse the JSON from `content` yourself. See the GGUF repo README.
---
## Tool calling
### Output format
It emits **bare JSON** (OpenAI function-calling style), with no wrapper at all:
```json
{"name": "get_weather", "arguments": {"city": "Beijing"}}
```
**No** ```json fences, **no** `<tool_call>` tags, **no** "Sure, let me call the tool" narration.
### Multi-step calls (only the first step is emitted when dependent)
For "Is it a good day to go out in Beijing?", it needs to check the weather first. In that case it **emits only the first step**:
```json
{"name": "get_weather", "arguments": {"city": "Beijing"}}
```
Feed the tool result back and it continues. **Your host must drive the loop**: parse → execute → feed result back → parse again.
### Independent calls are emitted in parallel
```json
{"name": "get_weather", "arguments": {"city": "Beijing"}}
{"name": "get_weather", "arguments": {"city": "Shanghai"}}
```
### It refuses clearly when it should
Not every request becomes a tool call. When something is out of scope or disallowed, it says so and offers alternatives instead of fabricating a tool call.
---
## When to give it tools, and when not to
| Category | Example | Behavior |
|---|---|---|
| Tool gives a definite answer | `789*123`, `1000 USD to CNY` | calls the tool |
| Out of scope / disallowed | booking flights, cracking WiFi | refuses + offers alternatives |
| Tool doesn't help | "what is recursion", "write binary search" | answers directly |
| **Host has no tools** | "search for me" in a plain chat app | says it can't, doesn't emit JSON |
**The last row matters**: if your host is a plain chat app, **give it no tool definitions at all**. It will say "I have no web access" instead of emitting JSON your host can't handle.
---
## Training
| Item | Value |
|---|---|
| Base | `openbmb/MiniCPM5-2B` |
| Method | LoRA (rank 8, dropout 0, scale 20, 16 layers) |
| Data | 5,341 samples |
| Epochs | 1 (2,671 steps) |
| LR | 5e-5 (adam) |
| batch / seq | 2 / 2048 |
| Framework | MLX-LM 0.31.3 |
| Quantization | 4-bit affine (group_size 64) |
---
## ⚠️ Known issues
### 1. It does mental arithmetic without tools — and gets it wrong
```
1234 + 5678 = 2000 (should be 6912)
35 ** 2 = 70 (should be 1225)
13.5 × 4 = 7.8 (should be 54)
199 × 257 = 40043 (should be 51143)
```
**Root cause** (confirmed by controlled experiment): the model decides based on **operation type**, and this maps exactly onto per-type sample counts in the training data — multiplication 432 samples → calls the tool; addition 86 / power 61 / subtraction 49 → does it in its head.
It is **not** string memorization (38% vs 50%, no real difference), and **not** difficulty (`1234+5678` also gets mental math).
**Workaround**: state it explicitly in the system prompt:
```
需要任何数值计算时,一律调用 calculate,不要自己心算。
```
### 2. Identity questions fail under a generic system prompt
If your system prompt is the generic "You are an AI assistant with tools..." style, asking "what's your name" may produce nonsense (observed: "I'm Qwen").
Put the identity in the system prompt:
```
You are LycheeAI-coder-2b-II-pro. You have access to tools.
```
Cause: every identity sample in the training data used the specific system prompt above; the model never saw the generic-tools-plus-identity combination.
### 3. Tool results come in two shapes — your host should know which it's sending
Both are understood:
**Shape A** — an explicit call turn (what mainstream agent frameworks do):
```
user: 789*123 等于多少
assistant: {"name": "calculate", "arguments": {"expression": "789*123"}}
user: [工具调用结果] {"name": "calculate", "result": {"value": 97047}}
assistant: 789 × 123 = **97047**。
```
**Shape B** — the host already executed it and hands the result straight over (used by 67 early samples):
```
user: 跑一下这段代码
[工具调用结果] {"exit_code": 0, "stdout": "45\n"}
assistant: 执行成功,输出 `45`。
```
**Difference**: shape B has no `name` in the result, because there is no call turn in context — the tool identity is inferable from the system tool list and the question itself.
**Recommendation**: use **shape A** for normal integration (multi-turn loop). Use shape B only when you already know which tool to run and just want the model to interpret the result.
### 4. Why v6.1 and not v6.4
Five iterations were trained. v6.2 fixed one item and broke two; v6.3 was item-for-item identical to v6.1; v6.4 collapsed the tool-call rate to 12%. **v6.1 is the only version with a real improvement over v6** (25/2 vs 24/3 on a 27-item eval).
Lesson: each round patched ~5% of the data and retrained on everything, reshuffling the whole behavior distribution. The next version will validate on small-scale runs first.
---
## License
Apache 2.0, same as the MiniCPM5-2B base.
---
<p align="center">
<sub>LycheeAI —— 小的,但能干活。</sub><br>
<sub>LycheeAI — small, but it gets the job done.</sub>
</p>