--- 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 代码块围栏、**没有** `` 标签、**没有**"好的我来调用工具"这种旁白。 ### 单步调用 ``` 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 标签 `` | | 部分国产模型 | `` 自定义标签包 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** `` 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. ---

LycheeAI —— 小的,但能干活。
LycheeAI — small, but it gets the job done.