OpenLaoKe 彻底重构-从 7.8 万行砍到 1.8 万行,试图成为python版本的Pi

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 文档:

写 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 语言翻译版。依赖里也顺手扔了 fastapiuvicornwebsocketsjiebawatchfiles

留下的: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:2bLiquidAI-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
  • 请求体字段 modelmessagesmax_tokenstemperaturesystem
  • 响应解析:content[0].text == "PONG",用量 input=11, output=2

本地端点不再要求 API key:当 base URL 是本地地址(localhost127.0.0.10.0.0.0::1host.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-textsnowflake-arctic-embed、两个 all-minilmgranite-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.pyaverage() 的偏差,并运行 去掉 + 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

失败到底错在哪,值得单独说,因为全在模型那边,跟智能体工具没关系:

  1. \n 写成字面量。 模型在工具参数里放了两个字符 \n,而不是真的换行。LiquidAI-dev 多次如此——fib.py 变成单行 print("Fibonacci test")\n,报 SyntaxError: unexpected character after line continuation character。智能体工具只是把模型要求的内容原样写进去。
  2. 定义了却不调用。 openlaoke + qwen3.5:2b + 英 + T3 生成了 fib(n) 函数却没调用,跑起来没输出。
  3. 复述指令。 pi + qwen3.5:2b + 中 + T2 答的是提示词原文,而不是算出来的和。

另外还有一次 600 秒超时(pi + LiquidAI-dev + 中 + T3)和一次 Python 报错(openlaoke + LiquidAI-dev + 中 + T3)。

按任务看,最惨的是“写脚本并运行”,只有 3/8。这里既要模型产出合法代码、又要它真的去执行,是小模型翻车的重灾区。相对可靠的是精确写文件(7/8)和读文件汇报。

五、本地推理速度

这一部分通过 Ollama 原生 API 测量,它给出纳秒级的 prompt_eval_durationeval_durationprefill = 提示 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 更好(没有思考延迟),代价是输出里带思考标记。

这些数字说明什么

  1. 瓶颈不在智能体工具。 OpenLaoKe(42/60)和 pi(41/60)基本打平,差异在模型噪声范围内。同一个模型在两个智能体工具里表现几乎一致。
  2. 模型选择才是决定性的。 qwen3.5:2b 在两个智能体工具拿到 10/10 和 9/10;granite4:350m-hlfm2.5-thinking 都只有 2–5/10。
  3. 智能体工具影响的是“身份”,不是语言。 同一个模型在 OpenLaoKe 里答“我是 OpenLaoKe……”,在 pi 里答“我是 Pi Coding Agent……”,说明系统提示词确实生效,但没有改变它回什么语言。
  4. 过小的模型不适合 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:2blfm2.5-2.6b 这两个,更小的我试过,有时候可能会连工具调用都带不动。