Harness 架构的通用设计模式及特点
1. 概览
研究目的:研究生产级 agent harness 架构的通用设计模式,弄清楚三个问题:
a. 哪些基础能力是必备的?
b. 这些基础能力有哪些可行实现逻辑,可以怎么进一步强化这些能力?
c. 如果我们要自研 agent harness,应该怎么借鉴?
研究对象:Codex、Hermes Agent、Generic Agent、MUSE、Claw Code(改编自被泄露的 Claude Code)、OpenCode。
研究方法:阅读相关源码/论文/文档。
研究维度:工具系统、上下文管理、记忆、skill、runtime 执行机制、反馈与自我进化。
未重点研究维度:沙箱/权限控制、安全审查、prompt/schema 设计等。
2. 通用 agent 架构:ReAct Loop + Hook Runtime
2.1 ReAct Loop
目前,几乎所有的 agent loop 都采用 ReAct 范式:思考(推理 + 决策)→ 行动 → 观察,构成循环。
prompt messages (system msg + history msg + user msg)
→ [LLM request]
→ 思考(推理:message.content + 决策:message.tool_calls)
→ 行动:执行 tool_calls
→ 观察:将工具执行结果 observation 拼接回 prompt messages
因此,一个 agent loop 开始于用户发送一段消息,结束于模型给出最终的回复,整个过程可能包含多个 ReAct 循环,而在一个完整的 session 中,一次完整的 agent loop 也称为一个 trace/turn。
注意:为了限制每个 agent loop 中 ReAct 的循环次数,可能会定义一个 max_turns 变量,和这里的 trace/turn 意义不一样,需要区分。在一个 session 中,是不设置 agent loop 的上限的,即 while True: agent_loop.run(),说明一个会话理论上可以产生无限个 trace/turn。
2.2 Hook Runtime
如果说 ReAct 是 agent 的执行骨架,那么 hook 就是将各种能力接入 agent 的方式。直白地说,在现代 agent 中,hook 承担的是传统 workflow graph 中节点的角色,用来执行特定的逻辑,并且可灵活插拔,不必写死在 loop 中。
举个例子:
当用户发送 user message 后,可以插入一行代码 compress_context():检查当前会话是否需要被压缩 → 生成压缩 plan → 执行压缩逻辑。然后,再进入到 ReAct 循环中。
在这个例子中,compress_context() 就是一个上下文压缩 hook。如果没有它,agent 也能跑,但是在长会话中可能会出现上下文爆炸、信息密度过低等问题。
此外,上下文的构造、事件派发都属于 agent 运行时的 hook。与被灵活调用的工具不同,这些 hook 通常都是根据实际需要,以硬编码规则的方式在固定的时机触发。
3. 通用 agent 能力维度
3.1 工具系统
- what:工具是 LLM 与外部世界交互的接口,相当于 agent 的手。模型本身只能生成文本,而工具以函数的形式提供了输入、输出以及特定的执行逻辑。模型可以通过生成已有工具的名称和输入参数,然后执行工具,拿到对应的结果。
- why:没有工具,agent 只能说,不能做,例如无法主动读写文件系统,无法执行系统命令等,退化成普通的聊天模型,且会因为无法通过检索/检验来保证事实依据。
- how:在请求 LLM 时可以传入
tool_schemas字段,描述工具名称、用途、输入参数结构与类型。模型会根据 prompt 推理并返回工具调用决策结果字段tool_calls。然后通过解析tool_calls,可以根据工具名找到对应 handler,校验并传入参数,执行工具,再把工具结果 observation 回填到 messages 中。
3.2 上下文管理
- what:上下文管理器决定每次请求 LLM 时应该给模型看什么。它会把原始信息(system message、历史消息、当前用户请求、工具结果、记忆、skill、项目说明、任务状态等内容)组织成 prompt messages,并在适当的时机进行压缩/裁剪。
- why:LLM 的上下文窗口有限,真实任务中的历史消息和工具结果会越来越多。如果没有上下文管理,agent 要么很快超过窗口限制,要么被大量无关信息干扰,要么在压缩/裁剪时丢掉关键任务信息。
- how:通常会维护一份完整 raw messages,再由 Context Manager 在每轮请求前构造实际发送给 LLM 的 api messages。构造时会保留当前任务、最近对话、关键工具结果、active plan/state checkpoint,并按需注入 memory、skill 或项目上下文。过长历史则通过压缩/裁剪处理。
3.3 记忆系统
- what:从系统工程的角度来说,记忆(狭义)一般指 agent 跨 session 保留的持久化信息,例如用户长期偏好、情景经历、语义事实、长期约定、历史踩坑和可复用经验等,是被筛选后的长期知识。但从概念上说,session 内信息(如最近 5 轮对话)也可以称为记忆(广义)。在工程上,memory 系统主要关注对前者的管理逻辑的实现。
- why:如果没有记忆,每新开一个会话时,agent 都像第一次见到用户和项目,无法迎合用户偏好,会重复询问已知信息、重复探索项目结构、重复犯过去犯过的错误,无法随着使用逐步变得更懂用户和项目。
- how:一般会提供 Memory Store 和 memory 工具,用来读取、写入、修改或删除长期记忆。在构造上下文时,Context Manager 会把相关记忆作为 context block 注入。在任务结束后,也可以通过 review hook 机制从 trace 中提取新的 memory candidate。
3.4 Skill
- what:skill 是一种过程性记忆,用来描述某类特定的任务应该怎么做,通常包含适用场景、操作步骤、注意事项、输入输出约定,甚至脚本和测试。
- why:没有 skill,agent 遇到相似任务时每次都要重新探索和规划,无法复用过去已经验证过的做法。工具或脚本可以给 agent 提供做某些事的能力,skill 则告诉 agent 具体应该怎么做。
- how:通常把一个具体的 skill 存成独立目录,目录下放
SKILL.md、scripts/、tests/等,SKILL.md中带有 name、description 等元信息和正文。请求 LLM 时可以先注入 skill metadata,让模型判断是否相关,如果相关,再通过 skill 工具读取完整内容,并按其中步骤执行任务。 - 重要补充:有一种观点认为,skill 可以看作是一种外置的模型参数,可以像训练模型参数那样,通过优化 skill 的内容来提升模型表现,并且,优化 skill 内容这件事本身也可以让 agent 自主完成。
3.5 Runtime 执行机制与状态管理
- what:Runtime 可以理解成为 agent loop 设计的一套运行机制,负责管理 session/turn 的完整执行生命周期。一方面,它主要通过 LLM request 和 tool calls 来驱动 loop 持续运行。另一方面,它在执行流程的各个关键节点接入不同 hook,实现 SSE、持久化结果等功能,并维护各种运行状态和任务状态。
- why:Runtime 的核心价值在于“流程控制”,是 harness 思想的全局体现。第一,如果不实现 ReAct 循环骨架,agent 就无法做事。第二,如果不在恰当的执行时机接入 context/memory/skill 管理等 hook,agent 的基础能力就会非常有限。第三,如果没有日志记录/权限校验/状态维护等功能,agent 的执行过程就会不受控制,也无法进行追踪/恢复。
- how:ReAct Loop + Hook Runtime + State Maintenance。
- 重要补充:如果和使用 LangChain/LangGraph 实现的 workflow 式 agent 进行类比,Claw 式 agent 的 runtime 机制就是 graph 的节点和边。这两种 agent 都可以看作是一个控制系统,执行任务的过程就是一个控制工程。
3.6 反馈与自我进化
- what:反馈与自我进化是指将“agent 执行轨迹”沉淀为“长期能力资产”的机制。它可以将稳定有效的处理链路整理成 skill,将反复出现的事实、偏好或失败经验写入 memory,并根据用户反馈或任务结果触发 skill/memory 整理 hook。
- why:若 agent 只完成当前任务,不沉淀经验,就很难在重复任务中提升效率和稳定性。反馈机制让系统能识别哪些做法有效、哪些错误需要避免、哪些知识值得长期保留,同时也能降低完全依赖人工维护 skill/memory 的成本。
- how:可以将自进化能力封装为系统工具,例如 skill/memory 的候选生成、增删改查、去重和校验,让 agent 在执行任务时按需调用;也可以将自进化机制设计为一系列 hook,在任务结束、用户反馈、错误发生或人工确认后触发。实际整理长期资产前,通常也需要经过规则校验或人工确认。
4. 一些成熟 agent harness 产品的能力特性
4.1 Codex 的工具系统
(1)构建
流程:built_tools() → 构建本轮 tool plan → 生成 model_visible_specs(暴露给 model 的 tool_schemas)→ 把计划中的所有可执行工具注册到 registry → ToolRouter(model_visible_specs + registry)。
Codex 的基础工具(如 exec_command、tool_search)在满足基本运行要求时默认暴露,条件工具则根据运行环境、实际配置筛选。每个工具都有一个 exposure 标签,agent 据此构建 model_visible_specs 和 registry:
- Direct:直接暴露给模型
- Deferred:tool_search 渐进式披露
- Hidden:只注册不暴露
为什么要区分暴露给模型的和实际注册的工具?
兼容性旧工具调用 + 支持内部路由(上层工具调用底层工具)+ 支持渐进式披露
(2)执行
流程:model 返回 tool_calls → ToolRouter 根据 tool_name 找到对应工具 → ToolRegistry 构造 ToolInvocation → 执行 pre hook → 调用 handler → 执行 post hook → 返回 ToolOutput。
Codex 的工具执行是带调度规则的异步执行:
- 并发:每个 tool call 会被放进异步任务中执行。工具会声明自己是否支持并发:支持并发的工具走共享锁,不支持并发的工具走独占锁,因此读类工具可以并行,写文件、执行命令这类工具会被更严格地串行化。
- 隔离:每次工具调用都会包装成一次独立的 ToolInvocation,里面包含 session、turn、call_id、工具参数、取消信号等运行上下文,handler 只处理这次调用。
- hook:pre hook 可以阻止工具执行或改写输入,post hook 可以阻止结果回填,或把工具结果替换成给模型看的反馈。
(3)结果处理
流程:handler 返回 ToolOutput → 转成 function_call_output → 写回 conversation history → 下一轮 LLM 把它当 observation 继续推理。
Codex 的工具结果处理重点是把执行结果变成模型可继续使用的 observation:
- 结构化:exec_command 的结果不只是 stdout,还会带 wall time、exit code、process_id、original token count 等信息,方便模型判断命令是否结束、是否需要继续轮询。
- 长运行:exec_command 只等待 yield_time_ms,如果进程还没结束,会返回 process_id,后续通过 write_stdin 继续读取输出或写入 stdin。
- 截断:工具层会根据 max_output_tokens 和 truncation_policy 截断模型可见输出,原始输出仍保留在 runtime 侧。这里是截断,不是语义压缩,和上下文压缩逻辑不同。
- 错误:参数错误、未知工具、权限拒绝、sandbox deny、进程启动失败、用户取消等,会被转换成错误 observation 或 runtime error,让模型或上层 runtime 继续处理。
- 取消:工具调用带 cancellation_token。如果 turn 被中断,runtime 会取消工具任务,并对正在运行的 exec 进程做清理。
4.2 MUSE 的 skill 生命周期管理
(1)skill 的创建:通过 skill_create 工具实现,工具内部流程:SkillCreator.create() → SkillEvaluator.evaluate() → SkillBank.register()。
(2)skill 的读取:skill catalog(name + description)注入 system prompt,agent 可以自主调用 read_skill 工具,读取对应的 skill 正文以及 skill-level memory,作为 observation 回传到上下文。
(3)skill 的记忆:通过 append_memory 工具实现,可以写入全局长期 memory。
(4)skill 的维护:实现了 SkillRefiner.refine_all_degraded()、SkillManager.merge_all_candidates()、SkillManager.prune(),源码实际上既没有被接入工具,也没有作为 hook 在 agent loop runtime 中触发。
MUSE 在创建 skill 时,可能会创建 scripts/、tests/。如果存在 tests/,在 SkillEvaluator.evaluate() 过程中,会创建 Docker sandbox 跑测试,通过测试就注册 skill,失败则返回 error trace 给 SkillCreator。
问题一:MUSE 的原生工具并不包括 shell 执行工具,因此即使加载了 skill 正文,agent 也不能运行 scripts。
问题二:MUSE 创建 skill 是通过自主调用工具实现,这里并不是凭空创造能力,让以前不会做的事变得会做,而是将概率化的模型参数固化成一个流程说明(收紧概率分布),并通过评估的方式检验有效性。
问题三:MUSE 并没有把 skill 记忆存储、优化、合并、剪枝能力接入 agent loop runtime,只是定义了相应的类和基本逻辑。
4.3 Hermes 的自进化机制
(1)前台:tool
- memory 工具:用于维护长期记忆,支持 add/replace/remove,目标分成 user(用户画像、偏好、沟通风格等)、memory(环境事实、工作约定、工作经验等),写到
USER.md/MEMORY.md。 skill_manage工具:用于管理过程性经验,支持 create/patch/edit/write_file/delete。
Hermes 默认会把这两种工具暴露给前台 agent,供其在执行任务时灵活调用。
(2)后台:review hook
review hook 是 Hermes 自进化机制的核心。流程:前台 agent 返回 final response → turn end 判断是否触发 review → 创建后台 review agent → 注入 turn 轨迹,只开放 memory/skill 工具 → 让 review agent 自主调用工具维护 memory/skill。
特点:
- 环境隔离:review 过程不写入 session,避免污染用户会话。
- 职责单一:独立 system prompt,只允许调用 memory/skill 相关工具,其他工具被 deny。
(3)长期 skill 整理:Curator
- 调用时机:不在 agent loop 中触发,而是在应用层定期开后台执行,默认每 7 天最多运行一次,并要求系统处于空闲状态。
- 总体流程:curator 的处理分两段:第一段是规则整理,不调用 LLM,根据 skill_usage 把长期未活动的 skill 标记为 stale 或移动到 archive。第二段是可选的 LLM consolidation,默认关闭,开启后会 fork 一个 curator agent,把窄 skill 合并进 class-level skill。curator 不做不可恢复删除,所有归档都可恢复,并通过一系列规则避免误伤。
4.4 Hermes 的 memory
(1)存储结构
Hermes 内置 memory 主要分为两个文件:
MEMORY.md:agent 自己的长期笔记,例如环境事实、项目约定、工具经验、踩坑记录等。USER.md:用户画像,例如用户偏好、沟通风格、工作习惯、长期期望等。
特点:小容量、高密度。每条 memory 用 § 分隔,并有字符上限,默认 MEMORY.md 约 2200 chars,USER.md 约 1375 chars。
(2)注入机制
Hermes 新建 session 时,会读取 memory snapshot,构造 system prompt 时注入,并持久化到 session DB。后续 turn 或 resume session 时会优先读取内存或 session DB。因此,即使 memory 文件发生了修改,也不会立即体现在已有 session 的 system prompt。当 system prompt 异常缺失或触发上下文压缩/重建时,会重新构造,此时新的 memory snapshot 就有机会进入新的 system prompt 了。
4.5 Hermes 的 Task Runtime(todo 工具与 TodoStore)
(1)解决什么问题?→ 长任务模型容易丢失三类信息:任务被拆成了哪些步骤,正在做什么,哪些已经做了/还没做。
(2)TodoStore 对象持有 todo items 列表,每个 item 有 id、content(task description)、status(pending | in_progress | completed | cancelled)三个字段,tool schema 约束 only one in_progress at a time。
(3)生命周期:
- 初始化:agent 实例初始化时,创建一个 store 对象,是内存状态。
- 工具注册:注册一个 todo 工具,行为规范放在 schema 中而不是 prompt。
- 读取逻辑:工具可以直接返回 todo list + summary,作为工具结果被 agent 看到。触发上下文压缩时,会从 store 生成 todo snapshot,追加成一个 user msg,使得模型在压缩上下文后也可以知道正在做什么和哪些还没做。
- 写入/更新逻辑:工具实现,参数里直接带 todos 列表,还有个 merge 参数可以控制是覆盖还是更新。
- 持久化/恢复逻辑:通过 tool message 本身在上下文里间接持久,某些场景下创建 agent 时会从上下文构造 store 内存对象。
4.6 Generic Agent 的 memory
(1)分层架构
- L1:索引层记忆
- 文件:
global_mem_insight.txt - 职责:固定注入 prompt,只保存极简索引、触发词、规则和边界,不保存具体步骤。目的是让 agent 知道有哪些长期记忆/能力,以及该去哪里读。
- L2:全局事实记忆
- 文件:
global_mem.txt - 职责:记录跨会话稳定事实,例如环境路径、配置、账号/工具约定、项目常量等。原则上不存临时状态和未经验证的信息。
- L3:任务级经验与工具脚本
- 文件:
memory/下的*_sop.md、*.py等 - 职责:保存特定任务类型的可复用经验,类似 skill。SOP 记录关键前置条件、典型坑点和操作流程,脚本封装高复用、复杂但稳定的操作逻辑。
- L4:历史会话归档
- 文件:
memory/L4_raw_sessions/ - 职责:保存原始或压缩后的历史会话,用于后续回溯、挖掘和再提炼,不直接作为每轮 prompt 的主要内容。
(2)memory runtime
- 初始化:agent 启动时,确保
memory/目录存在,并初始化 L1/L2 记忆文件。 - 读取:system prompt 默认注入 L1 内容,L1 路由到 L2/L3,需要通过工具进一步读取。
- 维护:
update_working_checkpoint工具负责维护结构化的工作记忆(任务状态、SOP 等),start_long_term_update工具负责生成记忆更新指导,后续 agent 据此调用file_write等工具对记忆文件进行实际操作。 - 兜底:单个 turn 循环次数过多会额外注入提示词,防止模型偏离 L1/任务状态。
- 归档:每 12 小时将原始会话日志压缩归档到
memory/L4_raw_sessions/。
4.7 Generic Agent 的 Task Runtime
(1)update_working_checkpoint 工具维护结构化的工作记忆,记录关键信息、相关 SOP,每轮注入上下文。
(2)复杂任务:plan_sop.md、plan.md
- 初始化:
plan_sop.md是源码自带文件,plan.md在实际工作中被创建。模型按plan_sop.md里的模板,用file_write写出来。 - 读取逻辑:
plan_sop.md通过 L1 索引触发读取,plan_sop.md要求模型每步都file_read(plan.md)。 - 维护逻辑:
plan_sop.md无需维护,plan_sop.md描述了维护规则。
总结:plan_sop.md 是源码自带,由 L1 记忆路由,模型自主触发读取。plan_sop.md 描述了怎么创建、读取、修改 plan.md,实际也是通过工具实现。
特点:plan_sop.md 和 plan.md 都是全局文件,不被 session 占有。
4.8 Claw Code 的工具系统
(1)基本设计
- 内置工具:50+ 个,大致分为文件工具、shell 工具、web 工具、状态工具、skill/agent 编排工具、git 工具等。
- 注册与 schema 组装:注册表把可用工具一次性汇总,转成 LLM API 的
ToolDefinition { name, description, input_schema }。
(2)执行链路
CliToolExecutor.execute():allowed_tools 检查 → 特判 ToolSearch / runtime tool → GlobalToolRegistry.execute() → execute_tool_with_enforcer() → match 工具名 → 解析参数 → 跑 pre_tool_use hook → handler 执行 → 跑 post_tool_use hook → 返回结果。
(3)结果处理
没有统一的“所有工具结果压缩器”。主要是各工具自己处理,例如:bash 工具会按文本大小截断,并标注 marker。
4.9 Claw Code 的 memory
三层结构:
- 索引层,常驻。
- 文档层,按需加载。
- 会话层,
.jsonl,grep。
4.10 Claw Code 的上下文压缩
5 级 pipeline:
裁剪旧的对话 → 工具结果卸载到磁盘 → 中间对话折叠摘要 → 全量压缩 → 413 应急压缩(规则策略)。