Agent Harness:把一切装起来
从第 2 篇到第 9 篇,我们一块一块地拆
、工具契约、提示词与缓存、短期记忆、长期记忆、状态持久化、规划、多智能体委派。每一篇都是一个独立的部件。这一篇要做的事很简单,也很难——把它们装成一个能跑的程序。这个”装起来的东西”有个名字:Agent Harness(智能体运行时框架)。专题二里我们已经从 Claude Code 的角度拆过它;这一篇换个视角,讲清楚它的通用骨架——不绑任何具体实现。
一句话定位:模型负责判断,Harness 负责结构。
一、什么是 Harness,什么不是
这是理解 Harness 的第一道坎,也是最容易糊涂的地方。
Harness 拥有的
、提示词构建器、工具注册表与分发器、记忆管理器、持久化层、钩子系统、事件总线、模型路由器,以及把这些接起来的生命周期。Harness 不拥有的
相信什么、它积累了哪些具体技能、某个工具的具体提示词、“该解决哪些任务”的业务逻辑。这些是应用代码。判断一个东西属于哪边,有个好用的准则:
如果移除某个功能会破坏”这个系统能解决什么任务”,它是应用代码。 如果移除它会破坏”这个系统还能不能跑”,它是 Harness。
同一套 Harness,今天能托管一个代码探索 Agent,明天能托管一个客服 Agent,后天能托管一个数据分析 Agent——而 Harness 本身一行都不用改。这就是 Harness 与应用代码分离的价值。
在 agent开发系列横评的 16 个项目里,Paperclip 是这个分离做得最干净的参考
本身根本不调用模型,它只负责派生适配器进程(应用)并编排它们。OpenCode 也是同样的切法——server/services(Harness)与 agent 定义(应用)分开。
二、组件清单
一个生产级 Harness 通常有十个左右的核心服务,而它们几乎逐一对应我们前面讲过的章节:
| 组件 | 职责 | 对应篇目 |
|---|---|---|
| 循环控制器 Loop Controller | 驱动 observe→plan→act→reflect→stop | 第 2 篇 |
| 工具注册表 + 分发器 | 工具的单一事实来源 + 校验管线 | 第 3 篇 |
| 提示词构建器 Prompt Builder | 稳定前缀 + 易变尾部的装配 | 第 4 篇 |
| 记忆管理器 + 压缩器 | 三视图、裁剪、去重、摘要 | 第 5 篇 |
| 记忆存储 + 写入器 | 长期记忆的检索与写入 | 第 6 篇 |
| 持久化 + 运行状态机 + 检查点 | 步边界提交、崩溃恢复 | 第 7 篇 |
| 规划器 Planner | 循环之上的规划层 | 第 8 篇 |
| 委派 Delegation | supervisor 在循环里,specialist 由它派生 | 第 9 篇 |
| 钩子 / 事件总线 / 模型路由 / 追踪 / 配置 | 横切的”管道层” | 本篇 + 后续 |
Harness 就是这张组件图,前面每一篇是图里的一块。 把它们摆到一起,Agent 运行时的全貌就出来了。
三、组合
跨项目看,把这些服务接起来的方式主要有三种,按正式程度递增:
- 闭包工厂(Closure Factories),接收依赖、返回一组方法。接线在
main/app.ts里一次性完成。Paperclip 用这个——小、显式、传假对象就能测试。 - 服务注册表(Service Registry),消费者按名字查。当有很多同类东西(工具、agent、provider)时好用。
- 分层依赖注入(Layered DI),运行时按序解析。OpenCode 用 Effect 的
Layer.effect正是干这个。
反模式警告
Harness 是把三种混着用的——有的服务注入、有的注册、有的当单例 import。选一种,坚持到底。
一个带类型的 Harness 长这样——所有服务作为字段,所有依赖显式声明:
type Harness = {
config: Config;
bus: EventBus;
hooks: HookRunner;
tracer: TraceSink;
prompt: PromptBuilder; // 第 4 篇
memory: MemoryManager; // 第 5-6 篇
tools: ToolRegistry; // 第 3 篇
loop: LoopController; // 第 2 篇
state: RunStateStore; // 第 7 篇
checkpoints: CheckpointStore; // 第 7 篇
router: ModelRouter; // 后续:成本与模型策略
};
四、生命周期 → tick → shutdown
Harness 有三个阶段,每个都有自己的规则。大多数 Harness 的 bug 都藏在阶段之间的边界上——服务在 boot 没完成时就被用了、drain 开始后还在接新请求、shutdown 没等运行状态机写完检查点就退了。
stateDiagram-v2
[*] --> Boot
Boot --> Ready : 服务初始化完成 + 健康检查通过
Ready --> Tick : 用户消息 / 定时触发
Tick --> Tick : 下一个请求
Tick --> Draining : SIGINT / SIGTERM
Ready --> Draining : SIGINT / SIGTERM
Draining --> Shutdown : 在途任务排空或到达截止时间
Shutdown --> [*]
Bootstrap 顺序不是随意的
启动序列的每一步都依赖前一步。跨项目验证过的可行顺序:
- 加载并解析配置文件(带环境变量覆盖)。
- 校验配置 schema;出错立即失败,并一次性列出所有错误。
- 替换环境变量,解析
$secret:引用。 - 打开数据库;跑完待执行的 migration。
- 初始化各服务(按依赖顺序)。
- 加载插件/技能/钩子。
- 启动后台任务(心跳、定时器、孤儿回收)。
- 健康检查全绿,才开始接请求。
顺序错了就是那类”开发时不出现、上线才炸”的 bug
、插件加载器在第一次模型调用后才跑、心跳在 migration 完成前就启动了。Tick
Ready 之后,每来一个用户消息或定时触发,就是一次 tick。tick 内部就是第 2 篇的对话循环——只不过现在它跑在一个有配置、有持久化、有钩子、有追踪的完整环境里。第 7 篇的步边界提交,正是发生在每个 tick 里的每一步之后。
Shutdown
收到 SIGINT/SIGTERM,不能直接退。要进入 draining 状态
,等在途任务跑到步边界并写完检查点,或到达截止时间强制退出。没有优雅 shutdown,崩溃恢复(第 7 篇)就形同虚设——因为你根本没给状态机写盘的机会。五、Harness 是脚手架,不是纪念碑
Anthropic 在长时运行应用的文章里有一句话点破了 Harness 的本质:
Harness 里的每一个组件,都编码了一个”模型自己做不到什么”的假设。
这句话的分量在于它的推论
,一些当初”模型做不到、必须由 Harness 兜底”的假设会失效。如果不带着这个框架去看,Harness 会在模型早就不需要某个功能之后,还长年累月地堆着它。所以 Harness 不是永久的纪念碑,而是应该随模型演进的脚手架。今天你为”模型不会自己停”写的步数上限、为”模型会重复读文件”写的去重逻辑、为”模型分不清事实和流程”写的记忆分层——其中一部分,在下一代模型上可能就是多余的。
这也呼应了 agent开发系列横评 16 个项目后得出的一条核心结论:
协议比实现重要。 MCP、Plugin SDK、Runnable 接口这些协议,才是长期价值的锚点。具体实现会随模型迭代而变,但把”变化的部分”和”稳定的部分”隔离开的那条接口线,会一直在。
小结
- Harness = 模型的运行时;模型带来判断,Harness 带来结构。
- 判断准则”能解决什么任务”的是应用代码,破坏”还能不能跑”的是 Harness。
- 组件清单基本就是前面每一篇的部件,加上钩子/总线/路由/追踪/配置这些横切管道。
- 生命周期 bootstrap→tick→shutdown,bug 多在阶段边界。
- Harness 是脚手架”模型做不到什么”的假设,随模型变强要敢于拆掉多余的部件。
前十篇搭完了 Agent 的”内部骨架”。接下来三篇转向对外与生产
、连接器与 MCP、Skills 与子代理,以及把 Agent 推上生产环境要面对的后端、可观测、成本与安全。主要参考
ch11(The Agent Harness)· claude-code-book ch15(构建你自己的 Agent Harness)· agent开发/9执行层 · agent开发/项目差异对比总览(Paperclip/OpenCode 的 Harness 与应用分离)· Anthropic「Building effective agents / long-running apps」