OpenLaoKe 之前写过一篇,那时候它是个功能堆得特别满但并不太好用的命令行 AI 助手。 最开始那会儿是参考 opencode 来做的,想的是不用 npm、不用 Node,直接拿 Python 把这事儿实现出来。因为我自己平时以 Python 为主,也希望所有工具的生态都落在 PyPI 上。 OpenLaoKe 最早脱胎于 2024 年 Clap 项目的一个分支,2026 年 4 月才独立出来。当时就是按 opencode 那种思路来的:工具要多、功能要全。到后面堆了三十多个工具,还塞进了 MCP、子智能体、supervisor、计划模式、权限系统、记忆、双模型协作、一个 Web UI,外加一个 FastAPI 后端。
结果就弄得特别臃肿、特别复杂,调试起来都很麻烦。286 个文件,77955 行——自己想改都得先找半天,改完还常常不知道会不会碰到别的活儿。
最近崔总建议试试 pi。试了之后发现确实非常轻巧简洁,效率更高,尤其是省 token。
我在写 pi 那篇里量过:同样一个任务,opencode 那边每一轮比 pi 多背 5800 多个 token,4 轮下来就多两万多。而且这个差距跟用什么模型没关系——换 deepseek-reasoner 再跑一遍,差的还是那个数。差距是智能体工具带来的常数。
按原来的设计,用 OpenLaoKe 跟模型说话的时候,每一轮请求都要重新发一遍系统提示词、工具定义、技能的元信息。这部分东西跟你的任务没关系,但每一次都要花钱、花时间。功能越全,这份“固定开销”就越厚。
用了 pi 几天之后,我就决定把 OpenLaoKe 整个推倒重写,改成按 pi 的设计来。背后的逻辑很简单:把每一轮都要重复付的那份成本压下去。这不是微优化,是设计上的重构。
这个的代码也依然是开源的,GPLv3 协议,地址在 https://github.com/cycleuser/OpenLaoKe。
安装方法如下所示:
pip install openlaoke
openlaoke
需要 Python 3.12+。
运行效果如下所示:

能够来写 markdown 文档:

也能完成文档翻译的任务:

但如果要做复杂的任务,本地的 2b 小模型就不够用了,建议用更强模型。
改成什么样
全面修改完之后,行数从 7.8 万掉到 1.8 万。
| 改造前 | 现在(0.1.44) | |
|---|---|---|
| Python 文件 | 286 | 68 |
| 行数 | 77955 | 18253 |
| 运行时依赖 | 一堆 | 7 |
| 工具 | 30+ | 8(Windows 上 9) |
| 提供商 | 一堆 | 24 |
砍掉的东西:MCP、子智能体、supervisor、计划模式、权限弹窗、记忆层、insomnia、双模型、自适应路由、防检测层、Web UI、FastAPI 服务、cron、消息总线,还有那个 C 语言翻译版。依赖里也顺手扔了 fastapi、uvicorn、websockets、jieba、watchfiles。
留下的:8 个工具、pi 那 23 个内置命令、树状会话(能 fork / clone / rewind)、prompt 模板、按需加载的技能。还有原来那个内置的 GGUF 推理运行时也删了——本地模型现在统一走 OpenAI 兼容端点,Ollama、LM Studio、vLLM 都行,反而更简单。
对比测试:和 pi 到底差多少
说了这么多改动,总得有点数字。下面这些测试是为这次重构做的,跑的时候是 OpenLaoKe 0.1.40(那会儿还有几个问题没修完)、pi 0.85.1,都在同一台机器上。小模型的数字噪声很大,请当方向性参考,不要当权威结论。
测试环境
| 项目 | 值 |
|---|---|
| 机器 | Apple M4(10 核 CPU),16 GB 内存,macOS |
| Python | 3.12(conda 环境 dev) |
| OpenLaoKe | 0.1.40 |
| pi | 0.85.1 |
| Ollama | 0.34.2,http://127.0.0.1:11434 |
| 本地端点 | http://127.0.0.1:11434/v1(OpenAI 兼容) |
这个版本号得说清楚:测试是真的在 0.1.40 上跑的,表里所有数字都出自那个版本。后面那些修补是在 0.1.40 到 0.1.44 之间做的,所以现在再去跑,结果大概会比表里好看一些。本文写完时最新版是 0.1.44。
参测的对话模型(都来自本机 Ollama,参数与量化取自 ollama show 原样输出):
| 模型 | 参数 | 量化 | 能力 |
|---|---|---|---|
qwen3.5:2b |
2.3B | Q8_0 | completion, vision, tools, thinking |
qwen3.5:0.8b |
873.44M | Q8_0 | completion, vision, tools, thinking |
LiquidAI-dev/lfm2.5-2.6b |
2.7B | Q4_K_M | completion, tools, thinking |
LiquidAI/lfm2.5-350m |
354.48M | Q8_0 | completion, tools, thinking |
lfm2.5-thinking |
1.2B | Q4_K_M | completion, tools, thinking |
granite4:350m-h |
340.33M | Q8_0 | completion, tools |
日常真正好用的就是两个 ≥2B 的(qwen3.5:2b、LiquidAI-dev/lfm2.5-2.6b),更小的几个我都删了,表里留的是当时的数据。
一、提供商格式兼容性
这一部分验证的是承载大多数提供商的那两种线协议:Anthropic Messages 和 OpenAI Chat Completions,都做了端到端测试。OpenLaoKe 还原生支持 Google generateContent、AWS Bedrock 和 Cohere,那三种不在这一部分的范围内。
| 测试 | 端点 | 结果 |
|---|---|---|
| OpenAI 格式,非流式 | POST https://api.deepseek.com/v1/chat/completions |
✅ 返回 PONG |
| OpenAI 格式,流式 | 同上 | ✅ 流式返回 PONG |
| Anthropic 格式,真实端点 | POST https://api.anthropic.com/v1/messages(无效 key) |
✅ 请求正确;标准 401 authentication_error |
| Anthropic 格式,完整请求/响应 | 本地 mock | ✅ PASS |
| 本地 OpenAI 兼容 | POST http://127.0.0.1:11434/v1/chat/completions |
✅ 返回 PONG |
Anthropic 那一项用的是本地 mock,校验的是精确契约,而不只是连通性:
- 路径必须是
/v1/messages - 请求头
anthropic-version: 2023-06-01 - 请求头
x-api-key - 请求体字段
model、messages、max_tokens、temperature、system - 响应解析:
content[0].text == "PONG",用量input=11, output=2
本地端点不再要求 API key:当 base URL 是本地地址(localhost、127.0.0.1、0.0.0.0、::1、host.docker.internal)且没配 key 时,OpenLaoKe 会发一个占位的 bearer token。
二、本地模型能力
每个模型先用一句简单提示走 OpenLaoKe 的 OpenAI 兼容客户端,再走完整的 agent 循环做一个文件任务(“创建 out.txt 内容为 hello pi,然后运行 cat out.txt”)。
| 模型 | 非流式 | 流式 | 工具调用 | 备注 |
|---|---|---|---|---|
qwen3.5:2b |
✅ | ✅ | ✅ | 综合最好 |
qwen3.5:0.8b |
✅ | ✅ | ✅ | 写成 hello pi.(多一个句号) |
LiquidAI-dev/lfm2.5-2.6b |
✅ | ✅ | ✅ | 写成 hello pi.(多一个句号) |
LiquidAI/lfm2.5-350m |
✅ | ✅ | ✅ | 文件内容正确 |
lfm2.5-thinking |
✅ | ✅ | ❌ | 只思考不行动 |
granite4:350m-h |
✅ | ✅ | ❌ | 工具调用幻觉报错 |
纯 embedding 模型(nomic-embed-text、snowflake-arctic-embed、两个 all-minilm、granite-embedding)在 /chat/completions 上被正确拒绝(HTTP 400)——它们本来就不是对话模型,所以不列进来。
这一部分还顺手抓到一个 bug。 Ollama 的 OpenAI 兼容流把思考放在 delta.reasoning,而 DeepSeek 用的是 delta.reasoning_content。OpenLaoKe 原本只读后者,导致思考型模型流式输出全空。现在两个字段都读了。
三、多语言一致性
让两个智能体工具各用 10 种语言回一句“介绍一下你自己”,再对回复做语言分类:按字符脚本判定(Hangul → 韩语,假名 → 日语,CJK → 中文,西里尔 → 俄语),拉丁字母则按停用词特征判断。✅ 表示检测到的语言和请求一致,❌ 表示对不上。
先看 OpenLaoKe 这边:
| 语言 | qwen3.5:2b |
qwen3.5:0.8b |
lfm2.5-2.6b |
lfm2.5-350m |
lfm2.5-thinking |
granite4:350m-h |
|---|---|---|---|---|---|---|
| 中 | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| 英 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 日 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| 法 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| 俄 | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ |
| 德 | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| 西 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| 葡 | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ |
| 意 | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
| 韩 | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ |
| 得分 | 10/10 | 8/10 | 9/10 | 8/10 | 5/10 | 2/10 |
再看 pi:
| 语言 | qwen3.5:2b |
qwen3.5:0.8b |
lfm2.5-2.6b |
lfm2.5-350m |
lfm2.5-thinking |
granite4:350m-h |
|---|---|---|---|---|---|---|
| 中 | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| 英 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 日 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| 法 | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ |
| 俄 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| 德 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| 西 | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| 葡 | ✅ | ✅ | ❌ | ❌ | ❌ | ✅ |
| 意 | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
| 韩 | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| 得分 | 9/10 | 9/10 | 8/10 | 9/10 | 3/10 | 3/10 |
那些 ❌ 大致分三种:回了别的语言(比如要求日语却答成中文,或者用英文作答)、干脆空回复、分类器判不出来。
合计:
| 智能体工具 | 通过 |
|---|---|
| OpenLaoKe | 42/60 |
| pi | 41/60 |
按语言拆开(两个智能体工具合并,满分 12):
| 语言 | 通过 | 语言 | 通过 |
|---|---|---|---|
| 英 | 12/12 | 葡 | 8/12 |
| 法 | 10/12 | 韩 | 8/12 |
| 西 | 10/12 | 德 | 7/12 |
| 日 | 8/12 | 中 | 6/12 |
| 俄 | 8/12 | 意 | 6/12 |
四、具体任务
在两个 ≥2B 的模型上,用两个智能体工具、中英双语跑 5 个具体任务:8 种(智能体工具、模型、语言)组合 × 5 任务 = 40 次。每次运行单独一个目录,输出在 run.log,生成的文件保留原处供人工检查。
| 任务 | 指令(中文) | 通过条件 |
|---|---|---|
| T1 精确写文件 | 在当前目录创建 hello.txt,内容正好是 hello small model |
文件含该文本 |
| T2 读并求和 | 读 data.txt(每行一个整数),求和 |
答案含 224 |
| T3 写+运行 | 写 fib.py 打印前 10 个斐波那契数,并运行 |
脚本可运行并输出 0 1 1 2 3 5 8 13 21 34 |
| T4 生成产物 | 写 report.md,含标题和三条要点 |
有 # 标题且 ≥3 条要点 |
| T5 修 bug | 修复 buggy.py 中 average() 的偏差,并运行 |
去掉 + 1,输出 4.0 |
结果(✅ = 通过,❌ = 失败):
| 智能体工具 | 模型 | 语言 | T1 | T2 | T3 | T4 | T5 | 得分 |
|---|---|---|---|---|---|---|---|---|
| openlaoke | qwen3.5:2b | 中 | ✅ | ✅ | ✅ | ❌ | ✅ | 4/5 |
| openlaoke | qwen3.5:2b | 英 | ✅ | ✅ | ❌ | ✅ | ❌ | 3/5 |
| openlaoke | LiquidAI-dev/lfm2.5-2.6b | 中 | ✅ | ✅ | ❌ | ✅ | ✅ | 4/5 |
| openlaoke | LiquidAI-dev/lfm2.5-2.6b | 英 | ✅ | ❌ | ❌ | ✅ | ✅ | 3/5 |
| pi | qwen3.5:2b | 中 | ✅ | ❌ | ✅ | ✅ | ✅ | 4/5 |
| pi | qwen3.5:2b | 英 | ❌ | ✅ | ✅ | ✅ | ✅ | 4/5 |
| pi | LiquidAI-dev/lfm2.5-2.6b | 中 | ✅ | ✅ | ❌ | ✅ | ✅ | 4/5 |
| pi | LiquidAI-dev/lfm2.5-2.6b | 英 | ✅ | ✅ | ❌ | ✅ | ✅ | 4/5 |
按任务、按模型、按智能体工具、按语言四个角度看:
| 按任务 | 通过 | 按模型 | 通过 | 按智能体工具 | 通过 | 按语言 | 通过 |
|---|---|---|---|---|---|---|---|
| T1 精确写文件 | 7/8 | qwen3.5:2b | 15/20 | openlaoke | 14/20 | 中 | 16/20 |
| T2 读并求和 | 6/8 | LiquidAI-dev/lfm2.5-2.6b | 15/20 | pi | 16/20 | 英 | 14/20 |
| T3 写+运行 | 3/8 | ||||||
| T4 生成产物 | 7/8 | ||||||
| T5 修 bug | 7/8 |
失败到底错在哪,值得单独说,因为全在模型那边,跟智能体工具没关系:
- 把
\n写成字面量。 模型在工具参数里放了两个字符\和n,而不是真的换行。LiquidAI-dev 多次如此——fib.py变成单行print("Fibonacci test")\n,报SyntaxError: unexpected character after line continuation character。智能体工具只是把模型要求的内容原样写进去。 - 定义了却不调用。
openlaoke + qwen3.5:2b + 英 + T3生成了fib(n)函数却没调用,跑起来没输出。 - 复述指令。
pi + qwen3.5:2b + 中 + T2答的是提示词原文,而不是算出来的和。
另外还有一次 600 秒超时(pi + LiquidAI-dev + 中 + T3)和一次 Python 报错(openlaoke + LiquidAI-dev + 中 + T3)。
按任务看,最惨的是“写脚本并运行”,只有 3/8。这里既要模型产出合法代码、又要它真的去执行,是小模型翻车的重灾区。相对可靠的是精确写文件(7/8)和读文件汇报。
五、本地推理速度
这一部分通过 Ollama 原生 API 测量,它给出纳秒级的 prompt_eval_duration 和 eval_duration。prefill = 提示 token 数 / 预填充耗时,decode = 生成 token 数 / 解码耗时。3 次取中位数,输出上限 128 token。
| 模型 | 提示规模 | 提示 token | Prefill (t/s) | Decode (t/s) | 首包 (s) | 总时长 (s) |
|---|---|---|---|---|---|---|
| qwen3.5:2b | 短 | 15 | 142 | 17.2 | 0.11 | 7.72 |
| qwen3.5:2b | 中 | 560 | 5608 | 17.5 | — | 7.41 |
| qwen3.5:2b | 长 | 2210 | 25188 | 17.1 | — | 7.62 |
| LiquidAI-dev/lfm2.5-2.6b | 短 | 17 | 109 | 19.1 | 0.32 | 6.88 |
| LiquidAI-dev/lfm2.5-2.6b | 中 | 560 | 3438 | 18.8 | — | 6.99 |
| LiquidAI-dev/lfm2.5-2.6b | 长 | 2210 | 12675 | 18.8 | — | 7.03 |
“首包”指任何类型的第一个流式分片耗时,两个模型都在 0.35 秒内。对思考型模型来说,更该看的是首个可见答案 token 的时间:qwen3.5:2b 把推理放进 thinking 字段,思考结束前 response 一直是空的,所以答案大约在上表那个预算结束时才出现(128 token 上限时约 7.5 秒);LiquidAI-dev 则把 <think> 标记直接写进 response 流,文字立刻出现,代价是输出里带着思考标记。
几点小结:
- Decode 差不多:约 17 t/s(qwen)对约 19 t/s(LiquidAI)。
- 长上下文下 prefill,qwen 更快:2210 token 时约 25k 对约 13k t/s。短提示的 prefill 数字被固定开销主导,只有中/长才有意义。
- 响应感上,短提示时 LiquidAI 更好(没有思考延迟),代价是输出里带思考标记。
这些数字说明什么
- 瓶颈不在智能体工具。 OpenLaoKe(42/60)和 pi(41/60)基本打平,差异在模型噪声范围内。同一个模型在两个智能体工具里表现几乎一致。
- 模型选择才是决定性的。
qwen3.5:2b在两个智能体工具拿到 10/10 和 9/10;granite4:350m-h和lfm2.5-thinking都只有 2–5/10。 - 智能体工具影响的是“身份”,不是语言。 同一个模型在 OpenLaoKe 里答“我是 OpenLaoKe……”,在 pi 里答“我是 Pi Coding Agent……”,说明系统提示词确实生效,但没有改变它回什么语言。
- 过小的模型不适合 agent 场景。
granite4:350m-h在 OpenLaoKe 里产生工具幻觉、乱建文件;管道是对的,是模型不行。
也得把局限说清楚:
- 语言分类器是启发式的。有些 ❌ 其实是混语言回复或者分类边界,不一定是硬失败。
- 每格只跑了 1 次。小模型本身随机性大,重跑会移动个别格子。
- 两处异常:
pi + qwen3.5:0.8b + 中文超时 420 秒(空回复);openlaoke + granite4:350m-h + 中文空回复。 - 对所有模型,英文都是最强的,中文和意大利语最弱。这是模型属性,跟两个智能体工具无关。
额外的一些废话
这轮折腾下来,我最大的感受是智能体工具的“聪明”要用对地方。
重构本身其实不算难,难的是改完之后跟本地小模型磨合的那几天,一路上碰到一堆乱七八糟的麻烦:有的命令明明只是看看系统信息,却被当成危险命令拦下来;工具列表不看平台就一股脑全给模型,结果在 macOS 上塞了个 PowerShell 进去;模型名里带个斜杠也能被解析吃掉;小模型明明乖乖回了工具调用,反倒被判成“没有输出”。还有几处是我自己给自己挖的坑,比如上下文预算从官方目录里取了个理论值,对着本地服务根本对不上;又比如默认放开让思考型模型去想,结果一轮就卡在那儿不动。
这些毛病有个共同点:在云端和“乖”的模型上不显山不露水,一旦换成小模型、换成本地,全都会变成绊脚石。 而其中一部分,纯粹是我自作聪明的结果——以为从官网目录读窗口比硬编码强,却没分清“模型能支持多大”和“服务器实际给多大”根本不是一回事。改进之前,得先弄清楚你改的是不是同一个东西。
至于和 pi 的差距,我觉得很正常。pi 是 Mario 写了很久的原版,我这是照着重写一遍。多语言打平、具体任务差 2 分,这个成绩我觉得可以接受,剩下的差距主要在细节打磨上。
说句实在的,本地小模型这条路,智能体工具能帮你的有限。工具调用链路我可以修得干干净净,但“读一周的代码然后总结”这种活,2B 模型是真干不了;翻译一整篇文章,它会直接照抄原文。想让本地模型干正事,起码上到能用的量级——我这里就是 qwen3.5:2b 和 lfm2.5-2.6b 这两个,更小的我试过,有时候可能会连工具调用都带不动。