Agent Harness 源码拆解(一):六个主流开源实现的整体架构
如果你今天要从零写一个 coding agent,第一个要做的决定不是选哪个模型——模型换起来太容易了。真正的第一个决定是:你的 agent 内核,写多大?
Anthropic 在一篇谈 harness 设计的文章里给过定义:agent harness 是模型外围的软件脚手架——循环、工具、上下文管理和护栏,把原始智能变成能干活的 agent。他们还有一句更锋利的话:harness 里写死的每一条规则,本质上都是”模型做不到什么”的假设,而模型在持续变强,这些假设会不断过期。
我把六个主流开源 harness 的源码并排放在一起后发现,它们对这个”内核写多大”的问题给出的答案,差异大得近乎吵架。最极端的两家差了两个数量级:pi 的内核只有两千多行,CodeWhale 光 TUI 一个 crate 就上百万行。更有意思的是,吵完之后它们又在另一件事上达成了罕见的一致——这件事我们放到最后讲。
先把六位选手介绍清楚,后文不再重复背景:
- pi:earendil-works 出品,TypeScript monorepo,切成几个可独立复用的 npm 包,主打”极简核心 + 扩展”。
- opencode:TypeScript 写的终端 coding agent,整个运行时压在 Effect 这个函数式库上。
- Grok Build:xAI 官方开源,Rust workspace 下 101 个 crate,TUI / 无界面 / 编辑器协议三种形态共用一个 runtime。
- ZCode:智谱(zai-org)开源的 coding agent,一个内核喂饱终端、桌面、Web 多个前端。
- Maka:Apache 孵化器项目,事件溯源内核,”什么算事实”被焊死在包依赖上。
- CodeWhale:Rust 写的,六家里工程纪律最重的一家。
这一篇是整个系列的地基:先看六家的整体骨架怎么搭。后面各章再按模块逐层深入——LLM 抽象层、Agent Loop、上下文工程、工具系统、权限与沙箱、持久化与恢复、多 Agent、Memory 与扩展——每章回答同一个问题:这个模块,几家是怎么设计的,代价是什么,我该抄谁的。
下面逐个看六家的答案,以及它们为答案付出了什么代价。
pi:两千五百行,连权限系统都不肯要
先说最偏执的一家。
pi 的作者是 Mario Zechner——就是写 libGDX 的那位,他把 pi 做成了一套可以独立复用的 npm 包。代码切成三个包,依赖严格单向:pi-ai 只管把几十家模型 API 的方言统一成一种事件流;pi-agent-core 只有六个文件、约两千五百行,装着 agent 的主循环和状态,没有持久化、没有产品逻辑;最外面的 pi-coding-agent 才是产品,系统提示、工具实现、压缩调度都堆在这一层。
产品层也不是个垃圾抽屉,它有自己的正经职责:管理输入语义和会话生命周期。举一个例子就能看出颗粒度——agent 正在干活时用户又发了一条消息,这条消息该什么时候生效?pi 拒绝靠”消息到达的时机”来猜,而是要求显式声明:是要打断引导方向(steer),还是追加为下一个任务(follow-up)。连”一次运行结束”都拆成了两个事件——agent_end 表示这轮循环跑完了,agent_settled 才表示不会再因为重试、压缩或排队任务自动继续。这些设计留到 Agent Loop 那章展开,这里只需要记住:产品层的复杂度是被认真组织过的,不是核心塞不下才漏下来的。
这个切法最狠的地方在中间那道缝。模型层和循环层之间的全部契约,是一个函数类型:给定模型和上下文,返回一条事件流。就一个函数签名。而且 pi 给它加了一条奇怪的纪律——这个函数永不 throw,失败也要编码成流内的事件。
为什么这么轴?设想一个场景:agent 循环里如果靠异常传递错误,代码里会到处是 try/catch,每一层都要考虑”上游会不会炸”。而把失败变成数据之后,错误只是事件流里的一种事件,循环不需要任何特殊处理,崩溃面一下子就收窄了。
但 pi 真正的立场不在分层,在一句话:核心零策略。它没有内置权限系统——README 里明说了。你想拦危险操作?内核提供一个 beforeToolCall 钩子,权限门自己写,官方仓库里的示例扩展几十行就能实现。系统提示、plan mode、沙箱,全是扩展。
初看这像是不负责任:一个 coding agent 不带权限控制?读完才反过来:内核里每塞进一条策略,这个内核就少一种被复用的可能。权限规则这东西,公司 A 要求弹窗审批,公司 B 要求全部静默放行,塞进内核就永远讨好不了所有人。pi 选择让内核只保留”机制”(一个钩子点),把”策略”全部推到外面。
而且它恰好是前面 Anthropic 那句话的最佳执行者:既然 harness 里每条规则都是关于模型能力的、注定过期的假设,那核心里的规则越少,需要跟着模型换代一起返工的东西就越少。零策略不是偷懒,是把”过期”这个问题从架构里摘了出去。
代价也很实在:用 pi 做产品,你得自己把安全这块拼回去。它省下的是内核的干净,付出的是每个使用者的装配成本。
ZCode:一个内核,喂饱四个前端
第二个问题紧跟着就来:终端 TUI、桌面 App、Web、IDE 插件——难道每个前端都养一份 agent 逻辑?
ZCode(智谱开源的 coding agent)的答案是:内核只准有一份,一个叫 createZCodeApp 的工厂函数。TUI 在自己进程里直接调用这个工厂;桌面端和 Web 端则各自 fork 一个子进程,子进程里跑同一个工厂,父子之间用逐行 JSON(NDJSON)通信。
这个结构本身不算稀奇,稀奇的是它对通信通道的洁癖程度。读它的代码会发现一个专门的文件,职责只有一个:拦截任何试图打到 stdout 的杂散输出。为什么?因为 stdout 是父子进程之间的数据通道,宿主是按”每一行都是一个合法 JSON 消息”来解析的,一条随手打的 warning 日志就能让整个桌面端解析崩溃。
这个细节的价值在于,它暴露了”协议解耦”这个方案的隐藏成本:你一旦决定用进程边界换隔离性,通道纯净度就从”卫生问题”升级成了”正确性问题”。日志、进度条、第三方库的调试输出,全都要有明确的去处。这是很多做 Electron + 后台进程架构的团队都摔过的坑,ZCode 直接用代码把坑填了。
Maka:把”什么算事实”焊死在包结构里
Maka(Apache 孵化器项目)最有意思的地方,不是它用了事件溯源——event sourcing 在这个领域已经是主流做法了——而是它把一个容易变成口头约定的架构语义,焊死在了包依赖上。
它有一个叫 packages/core 的契约包,里面只有类型定义:事件长什么样、会话长什么样、权限决策长什么样。关键在包的注释里写明了一条铁律:这个包不允许出现任何存储、投影逻辑。
这里需要解释一下”投影”这个词,因为它是理解 Maka(以及 opencode、pi)的钥匙。类比 Git:commit 历史是事实,checkout 出来的工作区是投影——工作区删了随时可以重新 checkout,历史删了才真正失去真相。放到 agent 里:agent 干的每一步(用户说了什么、模型回了什么、工具结果是什么、权限批没批)都被记成一条只增不改的事件,这条流水账是”事实”;而发给模型的上下文、终端上你看到的画面、崩溃后恢复的会话,都是从这条流水账现算出来的”投影”,不是独立存储的东西。
最容易犯的错是什么?是图省事把 UI 事件直接落盘当真相,或者把投影(比如压缩过的对话历史)当成事实存起来——从此再也无法回放审计,崩溃后恢复出来的也是一份被压缩过的、失真的历史。Maka 的解法是用包依赖把这条路堵死:运行时只生产事实,桌面和 CLI 只消费事实,中间没有任何一层”可以顺手把投影写成事实”。
这里还有个容易被忽略的推论:正因为发给模型的上下文只是有损投影(被压缩、被筛选过),压缩这个动作才可以放心大胆地做,甚至做错了也不要紧——真相永远在日志里,大不了重新投影一遍。这个心理安全感,是事件溯源架构给 agent 开发者的最大红利。
CodeWhale:它杀掉了自己的第二个循环
CodeWhale 是六家里工程纪律最重的一家,而它最有意思的故事,是一次自我否定。
在某个历史版本里,它的 crates/core 里存在一个占位的 engine 模块,会发出”循环完成”的事件——但从不联系模型。v0.9.11,团队把它整体删掉了,删的理由写在文档里,就一句话:workspace 里必须只有一个 turn 循环。
什么意思?agent 的主循环(发对话给模型 → 模型请求调工具 → 执行工具回填结果 → 再发,直到模型给出终止答复,这样一轮叫一个 turn)是这个系统的语义核心。如果仓库里存在两个循环——哪怕一个是”假的”——两套行为语义就会慢慢分叉,最后没人说得清哪个是准的。所以 CodeWhale 不但删了,还写了一个守护测试把这条规则锁死:这类测试不检查业务对错,只检查”你是不是又搞出了第二个循环”。现在四个入口(终端界面、无界面执行、HTTP 服务、编辑器协议)全部收敛到唯一的 Engine::run_turn。
它家的另一套手法叫”棘轮”:CI 里有个检查,统计跨层引用的数量,规则是只许下降,不许上升。今天的数字是基线,任何人想往错误方向加一笔,CI 直接红。这招的狠处在于它不要求你一步到位清理干净技术债,只要求方向永远正确。
pi 在同一个问题上走了条耐人寻味的中间路线:它其实有两个循环——内存版是参考实现,两千五百行,读它就是读 agent 语义本身;durable 版是生产实现,带崩溃一致性保证。两个循环通常是大忌,pi 敢这么做是因为它配套维护了一份规范性文档,并且让生产版持续对着参考版做一致性测试。**没有这套方法论就养两个循环,是大忌;有这套方法论,两个循环反而是”语义演进有 ground truth”的保障。**照抄结论之前,先看人家的配套。
opencode:把全部身家押在一个函数式库上
opencode 的架构故事一句话就能说完:整个运行时压在 Effect 这个 TypeScript 函数式库上——服务、生命周期、并发、权限等待,全部用 Effect 的原语表达。
这个选择买到的东西很具体:“半路取消”这类资源管理问题的确定性。agent 运行时最难调的 bug 往往出在取消和清理上——用户中途打断,工具进程杀没杀干净?权限等待挂起的会话,超时后资源释放了吗?Effect 的结构化并发和作用域语义让这些问题有确定的答案,而不是靠程序员记得在每个分支里写 cleanup。
代价同样具体:整个团队和所有潜在贡献者被绑进了 Effect 的心智模型。不熟悉函数式编程的人看它的类型签名像看天书,社区对这个选型的评价两极。而且这是扇单向门——一旦运行时建立在 Effect 上,想迁出去等于重写整个系统。架构选型里最贵的从来不是学习成本,是不可逆性。
Grok Build:不惊艳,但把确定性做到位
Grok Build(xAI 官方)展示的是另一个极端:不追求架构上的巧思,101 个 Rust crate 按大厂方式严格分层,终端、无界面、编辑器协议三种产品形态共用一个 runtime,把工程确定性做到位。
读它的收获不在某个巧妙设计,而在一个大规模维护的 harness 是怎么划分模块边界的——哪些职责值得独立成 crate,哪些合并在一起反而更好,它给出了一份经过实战的参考答案。
但要提醒一句:101 个 crate 的认知成本和编译成本,只有”多人长期维护”这个前提才撑得住。小团队照抄这种拆分粒度,大概率会把自己淹死在边界里——每改一个功能要动三个 crate,重构时 import 链调到怀疑人生。架构的颗粒度要跟团队的规模走,这是 Grok Build 没写在代码里、但读代码时应该读出来的潜台词。
它们唯一达成一致的事
前面说六家吵得很凶,但有一件事它们罕见地全体站队——可以叫它**”架构纪律的工程化”**:
- ZCode 把架构规则写进 policy 文件,配了专门的检查器去校验;
- CodeWhale 给每条架构不变量配守护测试,CI 里跑棘轮;
- pi 的洁癖更彻底,连 SQLite、OpenTelemetry exporter 这种”顺手内置”的库都拆出核心包,npm 依赖变更按 code review 对待。
三家动机相同:分层画在 README 里,三个月就会烂掉。第二个月来了个新人,图方便在错误的层里引了个依赖,没有任何东西拦他——直到半年后分层名存实亡。靠自觉维持架构是靠不住的,六家用三种不同方式选择了同一个答案:不信任自觉,信任机制。
顺带一提,ZCode 的 policy 检查器虽然写好了,但当前真正接管的模块只有一个——工具建好了不接入 CI,和没建是一样的。
如果是你,怎么选
把六家的答案折算成选型建议:
- 个人工具、或者想借项目学透 harness 原理:学 pi,三包切片,内核压到千行级,策略全走扩展。你会被迫想清楚每一层的边界在哪。
- 一个内核要喂 TUI + 桌面 + Web 多个前端:学 ZCode,单内核工厂 + 子进程协议解耦,并且从第一天就把通道纯净度当正确性问题管起来。
- 产品要求完整审计、崩溃恢复、会话回放:学 Maka,事件溯源,并且像它一样用包依赖把”事实与投影”焊死,而不是写进 wiki。
- 多人协作的开源项目:学 CodeWhale,守护测试 + 棘轮,先把架构不变量变成机器可执行的约束再扩张团队。
最后是四个最容易踩的翻车点,比选型表更值钱:
一是 core 变垃圾抽屉。”这个工具函数放哪?放 core 吧。”每说一次,内核就脏一分。CodeWhale 的防御方式是给 core 写明禁区:”runs no turns”,并且靠守护测试执行。
二是 长出第二个循环。新前端来了,复制一份循环改改最快——快是快,语义分叉从这一天开始。
三是 投影写成事实。把压缩后的对话历史当真相落盘,短期毫无问题,直到你有一天需要回放审计,发现原始数据早就不存在了。
四是 策略渗入内核。”先 hardcode 一个权限判断,以后再抽。”——那一天不会来的。pi 用整个架构告诉你:内核只留钩子,策略从第一天就放外面。
下一篇讲 LLM 抽象层:pi 要同时接几十家模型 API,方言各不相同,它是怎么把这一切收敛成一个统一事件协议的?以及为什么那个”永不 throw”的约定,值得每个多模型项目抄走。
本篇术语
- 事件溯源(event sourcing):类比会计记账——只增不改的流水账是唯一事实,任何报表都是从账本算出来的。agent 里指每一步(消息、工具调用、权限决策)都落为 append-only 事件
- 投影(projection,又译读模型):类比 Git——commit 历史是事实,checkout 的工作区是投影。指从事件日志现算出来的视图。注意:发给模型的上下文是有损投影,不是事实本身
- turn 循环:agent 主循环的一轮:发对话 → 模型要工具 → 执行回填 → 再发,直到模型终止
- NDJSON:逐行一个 JSON 对象的通信格式,ZCode 用它做父子进程间的协议帧
- 守护测试(guard test):不查业务对错、只查架构约束有没有被破坏的测试
- 棘轮(ratchet):只许变好不许变坏的 CI 检查——指标当前值记为基线,此后只允许朝好的方向变
参考资料
参照系仓库(结论基于 2026-09 下旬至 10 月上旬各仓库 HEAD):
- earendil-works/pi —— 作者 Mario Zechner(libGDX 之父),MIT 协议 TypeScript monorepo
- anomalyco/opencode
- xai-org/grok-build
- zai-org/ZCode
- apache/maka
- Hmbown/CodeWhale
Pi 相关源码分析(本文 pi 部分的事实校对与细节补充):
- 梁典典,《Pi Agent 为什么设计得好:从源码拆解一个极简 Agent Harness》,基于 pi-coding-agent 0.84.3
- iceyao,《Pi Agent Harness 源码深度技术解析:一个可自扩展编码 Agent 的架构全貌》,基于 v0.84.1
- 掘金,《Pi 源码拆解:当一个极简主义的 agent harness 只有 4 个 tool》
Harness 设计方法论文献:
- Anthropic,《Agent Harness Design: 3 Patterns for Harnessing Claude’s Intelligence》——harness 定义与”假设会过期”论述出处
- Anthropic,《Building agents with the Claude Agent SDK》