11|如果你要自己做一个 Harness,先抄这五层
先别从循环抄起
读完前面十章,最危险的冲动是打开编辑器,从"不断调模型、跑工具"这个最简单的循环开始写,再把这个项目的包结构一一对照过去。
那会抄到最便宜的一层。
第 00 章说过:玩具 Agent 死掉,不是因为循环不够优雅。贵的是三件事同时成立——换执行世界时模型契约不裂,进程挂了能重建模型当时看见的上下文,加审批和命令不准改循环。这个项目值得偷的不是它的默认循环实现,是它为了让这三件事成立而拒绝的那些结构。
这一章按"从零搭一台"的顺序收五层。顺序不是章节号,是你缺哪一层会最先把另外几层污染掉。抄完这五层,前面第 04、07、08、10 章那些具体机制几乎都是推论。抄具体包名、抄某个具体的编排功能,是在抄家具。
这个项目本身还处于早期预览阶段,明确声明会有破坏性变更,磁盘格式也可能不向后兼容。所以你要偷的是判断,不是某个具体的函数签名——那些明年可能就变了。
1. 先抄事件日志:只准有一份真相
判断:没有只增不改的会话事件流,后面四层都会各自养一份自己的消息数组,对账从第一天开始输。
第 05 章的钉子就这一句:会话不是对话的审计记录,它就是对话。投影是模型被允许使用的读法。这条纪律被写成一句话:模型看见的,等于已经被记下的。 一条运行时检查会把它焊成运行时错误——请求里的消息数组必须等于当时的投影结果。
从零搭的时候,这一层比循环还早。你甚至可以先不写司机:能追加一条事件,能投影出模型要的消息数组,能在恢复会话时得到同一份数组,这一层就站住了。新的模型可见输入 = 一种新事件。在请求组装处偷偷往数组里塞一个额外元素,是在犯罪。
表面层和原始日志要分开。模型只读三种能变成消息的事件(用户消息、助手消息、工具结果)。返回片段、turn 边界、队列裁剪记录、审批审计,都进原始日志,不进表面层。人看见的记录走追加起源那条路,压缩用替换标记盖住投影,不改已经写下的字节(第 05 章)。
如果先写循环再补日志会怎样?循环里那份数组会变成"真历史",日志变成"有空再抄"的附属品。恢复、分叉、界面、压缩各抄各的。三个月后你说不清模型当时看见了什么。先有事件流,循环只是一个往流上写、再从流上读的插件而已。
2. 再抄可逆注册:每笔贡献自带析构,每条拦截自带否决
判断:没有可卸载的贡献,所谓插件就是启动脚本里的一份 import 列表。没有可否决的链,政策只能改循环。
第 02 章:登记一个效果会立刻执行,卸载时按反序拆除;监听事件走的也是同一条带标签的效果登记路径;拦截链不调用"继续",后面的监听器包括默认行为都看不见。这两条被写成不可讨论的仓库纪律。
你不必抄这个项目底层框架的具体品牌。你必须抄这两条语义。任何"插件系统"如果卸不干净,或者拦截只能通知不能否决,就还是一个普通的依赖注入容器。这个项目甚至把底层框架本身也按"卸载必须干净"的标准重新打磨过——他们知道循环一旦也变成插件,卸载是唯一还能让系统保持可预测的办法。
第 07 章的工具管道、第 10 章的审批请求,都是这条语义的产品级用法:政策是可协商链,所有者闸门是不能翻案的否决,审批默认拒绝。特殊情况不是"针对每种拦截类型写一个 if",是"谁拥有这次决定,谁不把球传下去"。
如果先堆功能再补效果登记会怎样?每个新能力都在启动脚本里占一个位置,热更新变成重启进程,Agent 死了工具还挂在全局表里。政策想拦一次命令执行,只能去改循环内部的判断逻辑。可逆注册让"旁边再挂一个同级贡献者"成为唯一合法的扩展动作。
3. 把循环降成司机:产品打接口,组装才点名实现
判断:创建、投递、拦截如果都认识循环内部细节,循环就再也不能换,核心就会长回来。
第 03 章:接口是合同,默认循环实现是默认司机。接口对外只交出身份、待办投影、发送方法及其三个固定别名、取消和空闲等待。默认循环实现满足这份合同,但包本身不对外导出内部对象——创建走一个可插拔的"工厂槽位",界面和自动化协议桥这些消费者的运行时依赖打的是接口本身,不是具体循环实现。
第 04 章证明司机可以穷:心跳是待办队列的领走动作,不是"调模型"这个动作本身。追加下一轮、插入下一步、注入上下文,是"队列格子 × 是否唤醒"这两个维度的组合。先开 turn,再领走,再走准入检查。否决也留下一对"turn 开始/结束"的记录。这些是合同的履行方式,不是合同本身。你自己写司机时可以换调度细节,不能把内部实现泄漏给界面代码。
依赖方向必须钉死:扩展依赖能力定义(或接口),组装层才有权点名某个具体实现。抄错方向,第一层的日志纪律和第二层的效果登记都会被司机内部的私有逻辑吸进去,变成没人管的暗道。
如果产品代码依赖了循环内部细节会怎样?换司机等于重写所有消费方。每个人都会在循环内部加一个很小的条件判断。三个月后循环就是产品本身。接口文件还在,没人理它。
4. 能力必须成套:门、房间、敲门的手分开演化
判断:只登记一个工具函数,你换不了执行世界,只能再复制一份参数格式。
第 06 章:能力是定义 + 实现者 + 消费者,单独一个角色不算一个完整能力。能力定义拥有该长什么样的词表和调用方式;实现者拥有怎么跑,消费者的运行时依赖里不包含任何具体实现者;消费者只打能力定义这一个键,包信息里没有任何具体实现者的痕迹。"归一化成完整规格"是包边界上的显式默认,不是执行方法里的一个隐式兜底。
第 07 章补上门上的锁:政策、超时、呈现挂在工具管道上,不进某个具体实现者,也不进循环。第 08 章补上"这扇门属于谁":经某个 Agent 专属上下文登记,可见性和寿命是同一件事;父子关系是血缘数据,不是一棵往下传工具的树。预设的常驻作用域才是那条共享构图的父链。
不要预防性拆包。只有一个想得出来的实现者时,先放在一个包里。模型接入这个能力,把定义和消费者折进同一个包,因为消费面就是循环本身。该拆的判据是变化速率:模型契约、执行世界、呈现意图,三件事不要捆死。
如果三件事写在一个函数里会怎样?沙箱版、其它平台版、远端版各一份参数格式,提示词和权限矩阵一起炸。文件工具还在本机,命令执行已经在远端,Agent 开始在两个世界上自相矛盾。"指到远端,相关能力全部跟着走",是这一层值不值得受拆包代价的试金石。
5. 组合发生在启动时:产品形态是叠层,不是产品条件分支
判断:不同产品形态若靠条件判断分叉,第三层的司机和第四层的能力会立刻长出各自的产品线。
第 09 章:一个运行实例是空列表上按顺序叠起来的补丁。命名配置是有名字的目录,分发补丁是带补丁声明的包,覆盖补丁按编号整行替换。基础分发层没有运行时 API。从空列表开始组装的函数,和"只打印当前配置"走的是同一条函数。层序是分发补丁 → 命名配置补丁 → 运行环境补丁 → 命令行覆盖。坏的用户层留下上一份好树,不影响正在跑的会话。
第 10 章是同一判断用在人身上。斜杠命令是适配器在"追加下一轮"这个动作之前截下的指令;审批是工具管道上默认拒绝的一次性许可;提问是等一个承诺结果的普通工具。循环里没有"正在等人"这种状态。人机平面整体缺席时,循环照样是循环——只是有些门敲不开。
从零搭的时候,这一层可以很土:一份配置列出要挂的插件,再允许用户文件盖一行。不要一上来就做热更新和命名配置模板。要抄的是:产品线是配置清单的差分,不是代码的差分;"打印当前配置"打不出来的那个"产品",其实并不存在。
如果先做两套应用再抽公共库会怎样?底座分叉是时间问题。循环内部出现"如果是无服务器模式"这样的判断,宿主端代码被焊进循环,持续集成矩阵按产品线裂开。把产品线收成一份配置清单,循环继续保持穷。穷,是因为组合发生在启动时。
6. 五层站住之后,其余是推论——以及你现在不必抄的
待办队列的三种投递方式(第 04 章)是第三层"司机变穷"之后的调度细节。工具管道(第 07 章)是第二层的可协商拦截链加上第四层的消费者入口。扁平的作用域(第 08 章)是第二层"一个上下文决定可见性和寿命"这条规则,打在多 Agent 场景上的具体应用。人机三条平面(第 10 章)是第三层拒绝为"等人"长出内部状态、第五层拒绝为人机交互分叉产品线的共同结果。
按这个顺序核对,你会发现自己几乎不会去改循环。新行为挂事件,新输入加事件类型,新能力交三人组,新产品叠一层补丁。架构层面"新行为该挂哪里"的那张对照表,会变成你自己的操作手册。
现在不必抄的,列出来以免手痒:
- 具体的底层插件框架品牌。 抄的是"贡献可撤走"和"拦截可否决"这两条语义。你已经有等价的运行时机制,就用你自己的。
- 几十个能力包、特殊的代码执行模式、固定脚本编排、类型生成工具、复杂的界面卡片体系。 那是家具。五层房子没立住,家具会逼你在循环里打钉子。
- 界面本身。 第 01 章的走法仍然对:界面是事件流的消费者。先让一个无头进程能从日志重建一次请求,再画窗口。
- 子代理树。 第 08 章已经说明血缘关系不是继承关系。没有扁平作用域就去做委托,你会先发明一棵权限树,再花一年时间去拆掉它。
- 兼容性垫片。 这个项目在第一个正式版本之前明确拒绝垫片。如果你的项目已经有真实用户,"不要破坏已有用户的使用体验"这条原则对你比对他们更硬——抄的是判断力,不是"磁盘格式永远可以推倒重来"这个具体决定。
附录候选还留在目录里:压缩与请求重建的算法细节、持久化格式与表面层的关系、子代理与委托机制、界面如何只消费会话事件、运行时不变量如何把纪律焊成检查。它们值得写,因为各自要跨好几个概念才能讲清楚,放进正文会打断这五层的节奏。主线到此结束。
回看第 00 章那三件贵的事。事件日志让第三件(重建上下文)成立。可逆注册和拦截链让第一件、第二件能挂在循环外面。接口 / 司机让循环保持可换。成套能力让第一件(换执行世界)不裂契约。启动时组合让你不必为第二件、第三件去复制产品代码。五层是同一句话的五个面:把贵的东西做成边界,把循环降成边界里面最不特殊的那一块。
7. 结语:带走一句话
自己做 harness,先偷这五层——一份可投影的事件日志、可卸载可否决的注册机制、穷到只履行合同的司机、成套可换的能力、启动时叠出来的产品——循环是最后才该碰的那一块,也是最不该被谁直接依赖的那一块。