Essay

Agent Harness 工程:从 Agent Loop 到完整运行时

By XiaoLeiJun

Agent Harness 工程:从 Agent Loop 到完整运行时

想象一下,你在下班前交给 Agent 一个任务:

搜索接口偶尔会返回 500。请找出原因,修好它,补上测试,最后给我一个可以合并的结果。

几秒钟后,模型已经写好了一份像模像样的计划。它说要阅读代码、复现问题、修改实现、运行测试。

听起来,事情似乎已经完成了一半。

可真正棘手的部分才刚刚开始:

  • 它应该在哪个仓库、哪个分支里修改?
  • 读取哪些文件,执行哪些命令,权限由谁决定?
  • 测试要跑十几分钟,模型难道一直等着吗?
  • 上下文装不下完整日志时,哪些信息可以丢,哪些必须留下?
  • 运行中断之后,新的进程怎么知道上一次做到哪里?
  • 如果两个 Agent 同时修改同一个文件,谁来处理冲突?
  • 模型说“已经完成”,系统凭什么相信?

这些问题,几乎都不能靠“再写一段更聪明的提示词”解决。

模型擅长理解目标、分析信息、提出下一步;让下一步真正落到现实里,并且可执行、可约束、可恢复,则是 Agent Harness 的工作。

这个系列从一个几十行的 while 循环出发。走到最后,它已经不再只是“模型调用工具”的小程序,而是一套有状态、有边界、有生命周期的运行时。

这一章,我们不再添加新功能。我们沿着一个任务完整走一遍,看看前 20 章究竟拼出了什么。

真正的问题,不是模型会不会回答

最初的 Agent Loop 很简单:

用户消息
-> 模型决定下一步
-> Harness 执行工具
-> 工具结果写回 messages
-> 模型继续判断
-> 输出最终回答

它之所以像一个 Agent,不是因为模型突然获得了行动能力,而是因为 Harness 把模型输出的:

tool_name + arguments

变成了一次真实操作。

这里有一道很重要的分界线:

模型提出行动,Harness 决定这次行动能不能发生,以及发生之后留下什么事实。

模型可以提出“修改 search.py”,但 Harness 仍然要回答:

  • write_file 是否存在;
  • 参数是否符合 schema;
  • 目标路径是否属于当前任务;
  • 这次写入是否需要审批;
  • 文件应该写进哪个 Worktree;
  • 执行失败后能否安全重试;
  • 修改完成后如何验证;
  • 结果应该写入消息、Task、Artifact,还是审计记录。

因此,模型和 Harness 并不是两个可以互相替代的部分。

模型负责在不确定的信息里寻找下一步,Harness 负责给这一步装上边界、状态和后果。

我们是怎样走到这里的

回看前 20 章,会发现它们并不是二十个孤立的功能。每当 Agent 开始承担更长、更真实的任务,旧结构就会暴露一个新的缺口,于是下一块能力自然出现。

第一程:先让模型真的动起来

第 1 章先区分了模型与 Harness,第 2 章则从一次普通的 LLM 调用出发,接上工具调用与结果回传,得到最小 Agent Loop。

但工具一多,大段 if/elif 很快就会失控。因此,第 3 章加入 Tool Registry,让工具定义、查找和执行有了统一入口。

紧接着出现的是更现实的问题:模型能调用工具,不等于它应该调用所有工具。第 4 章把允许、拒绝和人工审批放到副作用之前;第 5 章再用 Hook 承接权限、审计和结果处理,让主循环不用为每一种横切逻辑反复改造。

到这里,Agent 第一次拥有了受控的行动能力。

第二程:让它做长一点的事

会行动之后,Agent 很快会遇到另一个麻烦:步骤一多,它就容易忘记当前做到哪里。

第 6 章加入 TodoWrite,让模型先列步骤,再逐项推进;第 7 章引入 Subagent,把局部工作交给一段干净的上下文;第 8 章把规范、流程和示例整理成 Skill,在任务匹配后再按需加载。

可是,Todo、工具结果和 Skill 都会挤进上下文窗口。第 9 章开始压缩历史,第 10 章把真正值得跨会话保留的事实放进 Memory Store,第 11 章则把固定的 System Prompt 拆开,根据当前任务、权限、工具和记忆在运行时组装。

这一阶段解决的不是“知道得更多”,而是 在有限上下文里,恰好知道此刻需要知道的事

第三程:让工作穿过时间

只要任务持续得足够久,失败就不再是例外。

第 12 章区分了模型临时故障、上下文超限、输出截断与工具执行失败。因为不同失败有不同含义,也必须采用不同的恢复策略。

随后,第 13 章把会话里的 Todo 升级成持久化 Task:任务有依赖、有状态、可以被认领,也可以从 checkpoint 恢复。第 14 章再把一次耗时执行抽成后台 Job,让 Agent 不必守着测试进程空等。第 15 章加入 Scheduler,使“每天早上八点检查服务器”也能可靠地产生一次可追踪的任务。

从这一刻开始,Agent 的工作不再依附于某一次对话或某一个进程。

第四程:让多个 Agent 一起工作

单个 Agent 能恢复,不代表多个 Agent 放在一起就能合作。

第 16 章让稳定身份的团队成员共享 Task Graph、Message Bus 和结构化结果;第 17 章进一步区分“消息送达”和“协议完成”,为请求、审批与优雅退出补上明确的状态约束。

最后,第 18 章为每个 Task 创建独立 Worktree。每个 Agent 在自己的工作目录中修改、测试、提交,再由 Integration Worker 串行集成结果。

协作不再依靠“大家小心一点”,而是依靠共享事实、明确所有权和物理隔离。

第五程:把外部世界接进来

工具不可能永远和 Agent 写在同一个进程里。

第 19 章通过 MCP 接入本地 Server,完成初始化、工具发现、名称映射和调用转发;第 20 章把连接扩展到远程,继续处理认证、Session、断线恢复、并发和多租户隔离。

MCP 让能力接入有了共同语言,但它没有替 Harness 做权限判断,也没有替系统解决沙箱、副作用和重试问题。

走完这五程,我们得到的不是一个堆满功能的 Agent,而是一条越来越清楚的边界:

模型负责思考下一步
Harness 负责让下一步安全地成为事实

一张图看清完整架构

现在再看这张总图,先不用急着记住每个方框。

完整 Agent Harness 架构

一次运行从上方的事件开始,穿过任务编排、模型决策与行动控制,最终把结果写入可持久化的事实来源。右侧的策略、恢复、预算与观测贯穿整个过程。

沿着箭头从上往下看,它讲的其实是一件很朴素的事。

最上方是 为什么现在要运行。用户消息、API 请求、定时触发、Job 完成、团队消息和审批结果,都可能唤醒 Runtime Event Loop。

再往下是 现在应该推进什么。Task Graph 保存目标、依赖和所有权;Scheduler 负责制造一次运行;Team Protocol 负责成员之间的请求、回复和交接。

中间才轮到模型。Prompt Assembler 从 Task、Todo、Skill、Memory、工具目录和压缩后的历史里挑出当前需要的信息,组成一次有限的上下文。模型看见这份快照,提出下一步行动。

行动不会直接落到系统上。它还要经过 schema 校验、权限策略、审批、Hook 和能力路由,随后才会进入本地工具、后台 Job、Subagent 或 MCP Server。

最下方的 Store 保存最终事实:Task 到了什么状态,Job 是否结束,审批是否通过,Worktree 对应哪个提交,Memory 里有哪些长期知识,Artifact 在哪里。

右侧那些贯穿全图的能力,则不断追问:

这次操作允许吗?
预算还够吗?
失败能恢复吗?
重复执行会怎样?
谁拥有这个资源?
什么时候可以安全结束?

这就是 Harness 的完整形状。它不是围在模型外面的一圈工具函数,而是一条从事件到结果的可信路径。

跟着一个任务走一遍

我们回到开头的任务:

修复搜索接口偶发 500,补上测试,并交付一个可以合并的结果。

下面这张图画出了它可能走过的主路径,以及几条常见的恢复支路。

Agent Harness 端到端运行流程

绿色主线表示正常推进,红色支线表示故障后的恢复。无论走哪条路,关键状态都会先写入 Store,再进入下一步。

1. 一句话先变成一项可以恢复的工作

用户消息只负责表达意图。Harness 接到请求后,先创建一个稳定的 task_id,把目标、依赖、验收条件和当前状态写入 Task Store。

如果任务只需要几分钟,TodoWrite 足以帮助模型安排当前步骤;如果它可能跨会话、跨进程,甚至交给别的 Agent,真正的进度就必须进入 Task 和 checkpoint。

两者看起来都像“待办事项”,但生命长度完全不同:

Todo:这一次思考接下来做什么
Task:整个系统正在推进什么

即使进程在下一秒退出,新的 Worker 仍然可以根据 Task Store 找回工作。

2. 给任务一间独立的工作室

Harness 从固定的 base commit 创建 Worktree,并把当前 Task 绑定到这个目录。

之后,读文件、写文件、安装依赖和运行测试都必须在这里发生。另一个任务即使修改同名文件,也会待在自己的 Worktree 中,不会把尚未完成的改动覆盖过来。

Worktree 解决的是协作干扰,不是安全问题。网络能否访问、进程能看到哪些目录、命令是否允许执行,仍然要由权限策略和沙箱控制。

3. 只把这一程需要的东西带进上下文

模型不需要看到整个仓库、全部记忆和二十个任务的聊天记录。

Prompt Assembler 会在调用前组装一份快照:

稳定的系统规则
+ 当前 Task 与验收条件
+ 当前 Worktree 和权限模式
+ 本轮 Todo
+ 与故障排查相关的 Skill
+ 检索出的少量 Memory
+ 压缩后的历史
+ 当前可用工具

这里的关键词是“少量”。

Context 是模型此刻摆在桌面上的材料;Memory 是从过去筛选后保存的资料;Skill 是完成某类任务时才翻开的操作手册。三者都能给模型信息,却有不同的来源、寿命和加载时机。

上下文管理做得好,不是让模型什么都看见,而是让关键证据恰好在需要时出现。

4. 模型提出行动,Harness 逐层把关

模型阅读代码后,可能提出:

{
  "name": "write_file",
  "arguments": {
    "path": "src/search.py",
    "content": "..."
  }
}

这还不是一次写文件,只是一份行动提议。

Harness 要先确认工具绑定存在、参数通过 schema 校验、路径位于当前 Worktree、调用者拥有权限,并判断是否需要人工审批。只有这些检查全部通过,工具才会真正执行。

如果工具来自 MCP,边界也不会改变。Harness 仍然要在发出 MCP 请求之前完成权限判断;远程 Server 的成功响应也要经过本地结果处理,才能进入 Task 或上下文。

工具协议可以统一,责任不能外包。

5. 慢工作进入后台,意外必须留下痕迹

修改完成后,Agent 启动完整测试。这个过程可能持续十几分钟,因此 Harness 创建一个 Job,立刻返回 job_id,让模型先去检查其他问题。

测试结束事件会唤醒 Runtime Event Loop,但事件本身只是一声“门铃”。运行时仍然要重新读取 Job Store,确认退出码、输出位置和结束时间。

如果中途发生故障,也不能只写一句“重试一下”:

  • 模型服务短暂不可用,可以在预算内退避重试;
  • 上下文超限,应先压缩,再重新调用模型;
  • 测试进程失败,需要把退出码和日志交还给模型分析;
  • 写操作超时,只能说明没有及时收到结果,不能证明远端没有执行;
  • 进程崩溃后,应从 checkpoint 和外部状态重新对账。

恢复的关键不是假装故障没有发生,而是让系统知道:已经确认什么,仍然不确定什么,下一步怎样最安全。

6. 需要协作时,工作通过任务交接

假如排查发现问题同时涉及查询服务和缓存组件,Lead Agent 可以把工作拆成两个有依赖关系的 Task,交给不同成员。

每个成员拥有自己的上下文和 Worktree,通过 Message Bus 交换通知,通过 Task Store 共享进度。真正解锁下游工作的,不是某条“我做完了”的消息,而是上游 Task 的状态与经过验证的结果。

Subagent 更像一次短暂委派:用干净上下文完成局部问题,然后返回结果。Agent Team 则有稳定身份、长期职责和共享任务图。任务是否需要升级为团队协作,应由复杂度决定,而不是由“多几个模型可能更聪明”决定。

7. “完成”必须由证据决定

最后,测试通过,Agent 在自己的 Worktree 中生成一个 Commit。Integration Worker 检查 base commit、验证结果并串行集成;发生冲突时,问题回到原 Task 的工作区解决,而不是在主分支上临时拼接。

只有当验收条件、测试、提交和集成状态都满足时,Harness 才把 Task 标记为 completed,随后释放租约、关闭 MCP Connection、回收 Job 和 Worktree。

模型当然可以说“任务已经完成”,但那只是一句话。

Task Store 中的终态、可定位的 Artifact、通过的测试和已经集成的 Commit,才是系统能够相信的完成。

它早已不只是一个 while True

最初的 Agent 只有一个循环:模型调用工具,再读取结果。

完整运行时里,至少有四种不同节奏的循环:

Event Loop
  唤醒系统:有新消息、定时触发或后台结果吗?

Task Loop
  选择工作:哪些任务已经就绪,由谁认领?

Agent Loop
  推进当前回合:下一步调用什么工具?

Reconciliation Loop
  修复现场:持久化记录与真实外部状态一致吗?

它们不能揉成一个巨大的循环。

Agent Loop 可以很快,Scheduler 可能一分钟检查一次,Job 在操作系统里独立运行,Reconciliation 则专门处理崩溃后留下的半完成状态。把不同节奏分开,系统才知道该等待谁、重试什么、在哪里恢复。

几组最容易混淆的概念

这个系列后半程出现了不少相似名词。真正理解它们,不需要背定义,只要问三个问题:

它描述的是目标、过程,还是知识?
它应该活多久?
谁是它的最终事实来源?

Todo、Task、Job 与 Schedule

Todo 是当前 Agent 的步骤清单,帮助它在一次工作过程中不迷路。

Task 是持久化目标,带有依赖、状态、所有者、租约和 checkpoint。

Job 是某一次具体执行,比如一条测试命令或一个构建进程。

Schedule 是时间规则,负责在未来创建一次 ScheduleRun,再由它产生 Task。

把它们串起来,就是:

Schedule 到点
-> 创建 Task
-> Agent 用 Todo 规划当前步骤
-> 某一步启动 Job
-> Job 结果推动 Task 继续

Context、Memory 与 Skill

Context 是本次模型调用能够看到的全部输入,容量有限,用完即变。

Memory 是经过筛选、可以跨会话保存的事实。它需要来源、更新和遗忘机制,不能把所有历史原样塞进去。

Skill 是针对某类任务准备的知识与流程。系统启动时只暴露元信息,匹配到任务后才加载正文。

可以把它们理解为桌面、资料柜和操作手册:当前桌面要保持清爽,资料柜不能什么都收,操作手册也不必每次全部摊开。

Subagent 与 Agent Team

Subagent 为一个局部问题临时创建,完成后把结果交回主 Agent。它最重要的价值是上下文隔离。

Agent Team 的成员拥有稳定身份和能力边界,通过持久化任务图协作。它适合真正可以并行、需要长期交接的工作。

前者是一次调用,后者是一套组织关系。

Message 与 Protocol

Message 只证明一段内容被投递。

Protocol 还要证明请求与回复是否匹配、审批适用于哪次调用、状态能否继续转换,以及双方是否完成了退出握手。

一封“同意”消息并不天然等于有效审批。Harness 必须知道是谁、在什么时刻、批准了哪一个带参数的操作。

Worktree 与 Sandbox

Worktree 隔离代码工作现场,避免不同任务互相覆盖文件和 Git 索引。

Sandbox 限制进程能力,例如可访问目录、网络、系统调用和资源额度。

一个负责“不互相干扰”,另一个负责“不能越过边界”。它们经常一起出现,但解决的是两类问题。

Token、MCP Session 与业务身份

Token 证明调用者获得了某种访问授权;MCP Session 保存一次协议连接的状态;租户和用户身份则回答“这次调用究竟属于谁”。

它们不能混用。Session 失效不代表 Token 必须失效,拿到 Token 也不代表可以访问另一个租户的工具与数据。

最重要的变化:把事实搬出对话

如果只能从这个系列带走一个结论,我希望是这一句:

messages 适合承载思考过程,不适合充当整个系统的数据库。

Agent 刚开始很容易把一切都塞进对话:计划、权限、进度、工具输出、团队消息、记忆,甚至“测试已经通过”的结论。

这样做在演示里很方便,在真实运行中却很脆弱。消息可能被压缩,进程可能退出,模型可能误读,多个 Agent 也可能各自拥有不同版本的“事实”。

所以,系统要逐渐把不同事实搬到各自的位置:

工具能力与 schema       -> Tool Registry / MCP Catalog
权限与审批结果          -> Policy / Approval Store
当前会话步骤            -> Todo
长期目标与依赖          -> Task Store
一次异步执行            -> Job Store
定时触发记录            -> ScheduleRun
团队投递与协议状态      -> Message Bus / Protocol Store
代码工作现场与结果      -> Worktree / Commit
跨会话知识              -> Memory Store
大日志与生成文件        -> Artifact Store

模型不需要直接维护这些事实。每次运行前,Harness 从可信来源读取状态,制作一份适合模型理解的快照;模型做出决定后,Harness 再把结果写回对应的 Store。

对话因此变轻了,系统反而变得更可靠。

七条值得长期保留的底线

框架会变化,模型会更新,工具协议也会继续演进。但下面七条原则,很难随着技术栈过时。

1. 提议不等于执行

模型输出的工具调用永远是一份请求。参数校验、权限判断、资源绑定和实际执行都属于 Harness。

2. 权限必须早于副作用

审批发生在写文件、执行命令或发送远程请求之前。事后记录一条“已拒绝”,无法撤销已经发生的操作。

3. 重试必须有预算,超时代表结果未知

所有重试都要限制次数、总时间和成本。尤其对有副作用的调用,超时后应先查询状态或使用幂等键,不能把“没有收到响应”当成“肯定没有执行”。

4. 事件负责唤醒,Store 负责证明

通知可能重复、乱序或丢失。收到 Job 完成、Task 更新或团队消息后,运行时要回到持久化 Store 重新读取事实。

5. 上下文必须有限,记忆必须经过选择

更多信息不必然带来更好判断。上下文要压缩,工具结果要截断,Memory 要有来源、置信度、更新和遗忘策略。

6. 每个资源都要有所有者和生命周期

Task 租约、Job 进程、Worktree、MCP Connection、Approval 和 Artifact 都应该回答:谁创建、谁使用、何时过期、失败后谁清理。

7. 完成与停止都必须可验证

完成要看测试、Artifact、Commit 和 Task 状态;停止要先排空新工作、保存 checkpoint、处理后台 Job,再释放资源。一个只会启动、不会安全结束的 Agent,还不是完整运行时。

不必从终点开始

看到总图之后,很容易产生一种压力:是不是实现 Agent,就必须一次写完这些模块?

当然不是。

如果你只是在验证想法,一个最小 Agent Loop、几个只读工具和清晰日志已经足够。

当它开始修改真实数据,再加入 Tool Registry、权限、审批和 Hook。

当任务超过一次会话,再加入上下文压缩、Memory、Task、Job 和 checkpoint。

当工作需要定时发生,再加入 Scheduler;当多个执行者真的可以并行,再加入 Team、Protocol 与 Worktree;当能力跨越进程和网络,再接入 MCP、认证、并发与连接恢复。

合理的建设顺序,不是照着架构图从上到下抄一遍,而是让每个模块回答一个已经出现的失败模式:

它解决了什么具体问题?
不用它,系统会在哪种情况下失效?
它引入的新状态由谁维护?
我们怎样测试它真的有效?

架构图是一张地图,不是一份必须一次买齐的清单。

Harness 也不是万能答案

一套边界清晰的 Harness 仍然不能保证模型每次都推理正确,也不能替代可靠的测试、准确的 Skill、经过治理的 Memory 和审慎配置的权限策略。

它所做的,是把模型的不确定性限制在可以观察、可以阻止、可以恢复的范围里。

模型可能选错下一步,但系统不必因此越权写入;模型可能误判完成,但 Task 不会在缺少证据时进入终态;进程可能崩溃,但已经保存的任务和工作现场不会跟着消失。

这不是消灭错误,而是让错误不再轻易变成失控。

结语

这个系列开始时,我们只有:

messages -> model -> response

后来,路径逐渐变成:

event
-> durable task
-> isolated workspace
-> bounded context
-> model proposal
-> policy and execution
-> durable evidence
-> verified result

看起来复杂了许多,但它只是补上了现实世界原本就存在的东西:时间会流逝,进程会退出,权限有边界,信息会遗失,多个人会冲突,远程调用也可能没有答案。

模型让系统能够理解目标、寻找路径;Harness 则让这条路径有路标、有护栏,也有在半途停下后重新出发的地方。

下一次再看到一个“会调用工具的 Agent”,不妨多问几句:

  • 它把什么当作最终事实?
  • 它如何约束副作用?
  • 它在故障后从哪里恢复?
  • 它怎样证明任务真的完成?
  • 它又怎样安全地停下来?

这些问题的答案,往往比模型名称更能决定一个 Agent 最终能走多远。

至此,Agent Harness 工程系列告一段落。

模型给出了下一步,Harness 让下一步经得起现实。