Agent Harness 架构设计
Agent Harness 架构设计
提示词是建议,Harness 代码是法律。
过去两年,很多团队把 Agent 开发的全部精力押在提示词上:换措辞、调温度、拼 Few-shot,仿佛话术足够精巧,模型就会自己长出工程素养。直到三次线上事故摆在面前——长任务跑到一半开始幻觉,进程重启后中间状态全部蒸发,一次越权命令删掉了测试库——才被迫承认一个事实:
模型回答什么,和系统允不允许它回答错,是两个问题。 前者归模型管,后者归我们管。
2025 年底,Anthropic 把 Claude Agent SDK 正式称为 Agent Harness;OpenAI 的 Codex 团队仅靠 3-7 名工程师,在 5 个月内让 AI 自主产出超过一百万行生产代码;HashiCorp 的 Mitchell Hashimoto 为这门手艺命名 Harness Engineering,留下一句被广泛引用的话:Prompts are suggestions. Harness code is law.
本文想回答的是架构问题:一套能进生产的 Agent 系统,运行时骨架长什么样,每一层在解决什么,设计权衡在哪里。
一、范式转移:为什么提示词工程不够了
2023 年,OpenAI 的 Lilian Weng 用 Agent = Model + Planning + Memory + Tool 定义了 Agent 的能力构成;ReAct 论文确立的 Thought → Action → Observation 循环,至今仍是智能体的推理底色。这两个成果的价值在于划定了能力边界——但它们回答不了运行保障的问题。
原因很朴素:模型是概率系统,工程是确定性的。 幻觉、遗忘、越权、状态丢失,这些事故没有一个是"缺了某个要素",全部是"少了一层约束"。提示词只能调整模型"愿不愿意配合",管不住系统"能不能兜底"。当模型能力趋于同质化,架构师真正能抓住的确定性杠杆,恰恰在系统层。
于是概念发生迁移:
Agent = Model + Harness
Model 提供智能,Harness 提供纪律。如果用一个比喻:一个能力很强但不太可靠的实习生,和一个给他配上流程手册、权限边界与质检环节的团队制度——两者的差距,就是裸模型和 Harness 化 Agent 的差距。
翻开 Claude Code、OpenCode、OpenClaw、Hermes、AutoGen AG2 这些代表性项目,产品形态差异很大——编程执行、本地生态、长期记忆、自我进化、多智能体编排——但剥掉外壳,它们都在做同一件事:围绕一个主循环,铺设一层又一层确定性约束。 产品千差万别,工程骨架殊途同归。这就是 Harness 值得从架构视角研究的原因。
二、骨架:一个主循环,五个职责面
最直接的诱惑是把所有逻辑塞进一个循环:模型调用、上下文拼接、工具执行、权限检查、日志记录,几百行代码,演示毫无问题。
演示没问题,排障是灾难。Agent 的执行链路同时牵涉推理、输入、行动、安全和状态恢复,它们的失效模式与扩展节奏完全不同:上下文的压力来自 token 膨胀,工具的压力来自越权风险,调度的压力来自死循环。混在一起,任何一次线上故障都会变成考古现场。生产级 Harness 的第一原则因此是按职责切面,围绕闭环咬合:
- 调度面是主循环本身,回答"下一步做什么、什么时候停下来";
- 上下文面是模型入口前的信息管道,回答"什么信息以什么形态进去";
- 记忆面负责跨会话的状态沉淀,回答"什么该记住、什么该忘掉";
- 工具与安全面监管对外部世界的每一次触碰,回答"能不能做、怎么安全地做";
- 观测面是贯穿所有面的证据链,回答"刚才到底发生了什么"。
五个面各自独立演进——上下文压力大时只改上下文面,安全要求变高时只改工具面。分层解耦的真实收益是按风险暴露顺序逐面加固,而不是一次建齐。
顺带厘清一个常见混淆:Harness 和 LangChain 这类框架的区别不在功能,在姿态。框架提供能力——模型接入、工具封装、链式调用,怎么组装是你的事;Harness 提供约束——权限校验、状态机、预算熔断,这些关卡不是可选功能,而是运行时不可绕过的路径。框架是"要不要用"的问题,Harness 是"用不用得过"的问题。
三、主循环:把概率流装进状态机
最流行的 Agent 循环写法,也是最危险的:
// 反模式:隐式状态、无边界、无中断
while (true) {
const response = await llm(messages);
if (response.tool_calls) {
const result = await execute(response.tool_calls);
messages.push(result);
} else {
return response.content;
}
}
问题不在"能跑",在"不可治理":当前处于什么阶段只有 messages 数组自己知道;没有轮次上限,死循环只能杀进程;没有 token 预算,账单烧穿没有熔断;用户中断无人响应;工具失败没有重试策略;崩溃后状态全丢。线上出事,只能翻几万行日志猜逻辑。
生产级做法是把隐式流转压成显式状态。一个最简实现:
// 显式状态枚举:每一步可追踪
type LoopState =
| 'INIT' // 装载上下文与记忆
| 'THINKING' // LLM 推理中
| 'PARSING' // 解析输出:工具调用 or 终答
| 'EXECUTING' // 执行工具
| 'OBSERVING' // 收集结果
| 'COMPRESSING' // token 预算触发,压缩上下文
| 'TERMINATED';
type LoopEvent =
| { type: 'LLM_DONE'; payload: LLMResponse }
| { type: 'TOOLS_DONE'; payload: ToolResult[] }
| { type: 'USER_INTERRUPT' }
| { type: 'BUDGET_EXCEEDED' }
| { type: 'MAX_ROUNDS' };
// 事件驱动流转:switch + 事件总线
function transition(ev: LoopEvent) {
switch (state) {
case 'INIT':
state = 'THINKING'; break;
case 'THINKING':
state = ev.type === 'LLM_DONE' ? 'PARSING' : 'TERMINATED'; break;
case 'PARSING':
state = ev.payload.hasTools ? 'EXECUTING' : 'TERMINATED'; break;
case 'EXECUTING':
state = 'OBSERVING'; break;
case 'OBSERVING':
state = budgetNearLimit() ? 'COMPRESSING' : 'THINKING'; break;
case 'COMPRESSING':
state = 'THINKING'; break;
}
EventBus.emit(`state:${state}`, ev);
}
状态机 + 事件总线换来的是可追踪、可干预、可断点:死循环时查 state: 事件序列,卡在哪个节点一目了然;任何状态都可以作为恢复锚点。
骨架之上,有四个决策值得单独讨论。
第一,单线程串行执行。 Claude Code 与 AG2 的核心调度都是单线程的,初看反直觉——为什么不让工具并行?答案是确定性优先:并发抢占引入的状态竞态,排查成本远高于串行省下的时间。真正的并行放在单线程的调度之下做,比如 DAG 拓扑内的工具批处理,而不是放在循环层。
第二,规划与执行分离。 复杂任务先进入规划模式,产出可审阅的计划清单,人点头之后才进入执行模式。这一步把"模型想做什么"和"系统允许做什么"在流程上切开,是最便宜的一道人工闸门。
第三,中断传播。 用户按停止,信号要能穿透正在进行的 LLM 调用和工具执行。AbortController(或等价的 CancellationToken)从循环层一路传到工具层,停止按钮才是真的,否则只是装饰。
第四,快照与恢复。 每轮结束把状态序列化——轮次、消息、待处理工具。崩溃后从最近的快照续跑,而不是从头烧钱。这也是"时间旅行调试"的基础:回放任意快照,复现问题现场。
四、委派:多智能体不是群聊
多智能体系统的流行叙事是"群聊式涌现"——几个 Agent 自由讨论、互相启发,智慧自然产生。生产环境的答案是四个字:受限并行。
主 Agent 扛不住时,把任务拆出去,让 SubAgent 在独立上下文里完成,再把干净的结果收回来。真正难的不是"派活",而是派出去之后四件事:不污染主上下文、不无限递归、不抢资源、不把失败扩散成全局事故。
最稳妥的落地方式是任务委派:SubAgent 不直接暴露给模型,而是包成标准工具,主循环调用它和读写文件没有任何区别:
// SubAgent 封装为 Task Tool:主循环无感知
const taskTool = {
name: 'task',
description: '把子任务委派给专门的 SubAgent 执行',
parameters: {
type: 'object',
properties: {
agentType: { type: 'string', enum: ['explorer', 'tester', 'analyst'] },
prompt: { type: 'string' },
context: { type: 'object' } // 按需裁剪的上下文
}
}
};
async function executeTask(args: TaskArgs): Promise<TaskResult> {
const sub = createSubAgent(args.agentType); // 工厂创建独立实例
sub.setContext(args.context);
return { output: (await sub.run(args.prompt)).finalAnswer };
}
这是依赖倒置的经典应用:主循环只依赖 task 这个抽象,不关心底层是沙箱、子进程还是远端 API。委派的另一面是故障隔离——每个 SubAgent 拥有独立的上下文、工具集、token 预算和退出条件,一个子任务爆炸不会污染主循环。分布式系统里的隔离舱(Bulkhead)思想,在这里原样生效。
至于 DAG 编排,我的建议是克制:只有任务存在明确拓扑依赖时才值得引入;没有依赖关系的"并行",常常只是把复杂度从主循环转移到了编排层。先让委派模式在生产环境跑稳,再谈 DAG;对等网状和自由群聊式协作,留给研究原型。
五、上下文:带宽不是仓库
上下文窗口是带宽,不是仓库。把 System Prompt、历史消息、工具结果、项目说明、记忆文件一股脑塞进 messages,是 Demo 阶段最自然的写法,也是长任务 Agent 最常见的死法:窗口爆表、关键信息被挤掉、账单失控。
生产级上下文系统的核心工作是治理输入,分三层。
装配层:缓存友好的动静分离。 稳定不变的部分——系统提示、规则、项目规范——前置为静态前缀;动态变化的部分——本轮消息、工具结果——后置。前缀稳定,服务端的 prompt cache 命中率才有保障;动态内容后置,才不会破坏缓存键。一个字节顺序的差别,可能就是几倍的推理成本。
预算层:输出截断与背压。 工具返回可能是一次十万 token 的文件读取。预算层负责在结果进入上下文前做截断、落盘或摘要,原则是"先存后读":完整结果落到外部存储,模型只拿需要看到的部分。
压缩层:分级降损。 压缩不是单一动作,而是一条成本阶梯——先折叠(合并冗余,零成本无损),再剪枝(按规则淘汰低价值块),最后才摘要(LLM 重写,有损且最贵)。这条阶梯的意义在于把昂贵操作推迟到不得不做的那一刻:能折叠就不剪枝,能剪枝就不摘要。
三层合起来,上下文系统就不再是"把资料塞给模型",而是模型入口前的一条有车道、有限速、有分流的信息管道。
六、记忆:分层沉淀与索引加载
没有记忆的 Agent,每次会话都是"第一次认识你"。但记忆系统的架构价值不在于存了多少,而在于存进去的东西能不能被治理、被检索、被修正。
先分层。按生命周期切:
| 层级 | 载体 | 内容 | 生命周期 |
|---|---|---|---|
| 工作记忆 | 消息数组 / SQLite | 当前会话历史、工具中间态 | 会话结束即清理 |
| 项目记忆 | Markdown 文件 | 技术栈、架构决策、常用脚本 | 跨会话,随项目演进 |
| 全局记忆 | 用户级目录 | 偏好、习惯、长期知识 | 持久化,持续沉淀 |
分层的本质是关注点分离:高频热数据走上下文,低频冷数据走检索——别让冷数据挤占热数据的带宽。
再谈加载。长记忆最反直觉的实践是:启动时不全量注入,只加载索引。 Claude Code 的做法很有代表性——记忆入口是一个严格限量的小索引文件,启动时只读目录,判断某条可能相关再用 Read 工具按需拉取详情。AG2 的上下文中间件同理:先路由意图,再拉记忆载荷,最后拼装 System Prompt。这种"索引 + 按需加载"带来两个收益:token 账单的数量级下降,以及上下文腐烂概率的大幅降低——后者意义更大,因为记忆越多,无差别注入带来的噪音衰减越快。
最后是写入。什么记忆值得晋升到长期层?要有评分和淘汰,不能来者不拒。主流项目的做法各异——有的克制,有的激进到让 Agent 把经验自动固化成可复用的 Skill 并迭代 patch——但底线一致:记忆必须分层,索引必须轻,写入必须可治理,文本必须人类可审计。 人类可读的 Markdown 作为真相源,意味着记忆系统天然支持 diff、回滚和人工修正。
七、工具与安全:最后一道闸门
工具是 Agent 触碰真实世界的唯一出口。模型可以想,工具负责做——而"做"一旦连到文件系统、Shell、数据库和企业 API,风险就从"答错一句话"升级为"删掉一个库"。
最常见的写法是 API 胶水:把 OpenAPI Spec 喂给模型,模型吐 JSON,程序照单执行。这条链路省掉了生产环境最重要的五样东西——入参校验、权限控制、超时熔断、错误清洗、执行隔离。一次越权 rm -rf 就能搞崩整个环境。
生产级工具系统是一条从注册到审计的安全管道,五个环节缺一不可:
契约先行。 每个工具必须有结构化 schema(参数类型、取值范围),参数幻觉在执行前就被拦截——相当于把 gRPC IDL 的思想复刻到工具层。
注册即治理。 工具统一注册进注册中心,新工具接入不侵入主循环;同时能力分级披露——全量工具描述会吃掉上下文带宽,按需披露才可控。
权限决策。 高风险操作必须经过确定性的规则引擎,allow / ask / deny 三态是经典做法:常规操作放行,边界操作询问,危险操作拒绝。权限规则是代码,不是提示词。
静态分析。 对 Bash 命令做 AST 层面的解析,不执行也能判断风险——检测到递归删除、系统级安装等危险模式就在执行前拦截。这是编译器前端技术向安全场景的直接迁移。
执行隔离。 高风险操作跑在隔离环境里(Git worktree、容器、临时沙箱),失败了不污染宿主,成功了再合并。
一句话总结:模型负责"想做什么",Harness 负责"能不能做、怎么安全地做、做完怎么审计"。
八、可观测性:让概率系统可以被复盘
确定性系统出故障,看日志能定位;概率系统出故障,没有证据链就只能靠猜。可观测性是 Harness 从黑盒执行走向可治理系统的分水岭。
一次 Agent 执行留下的证据至少分四个账本:Trace/Span 记录发生了什么、耗时多少;事件日志记录顺序;Checkpoint 记录恢复到哪;Token 成本记录钱花在哪。最容易踩的坑是各记各的:工具失败了,日志里有 error,Trace 里只有一个 500,Checkpoint 里没有待处理工具,成本面板只知道这轮烧了多少 token——四套数据拼不起来,复盘还是靠猜。
解法是统一的 ID 链:sessionId 挂住一次长任务的所有运行时事实,traceId 串起一次端到端执行,spanId 定位到单个可观测步骤,toolCallId 支撑外部副作用的幂等与回滚,checkpointId 支撑恢复、Fork 与回放。四账合账之后,事故复盘从"翻日志猜"变成"查账本"。
可观测性的另一半是回归验证。模型升级、提示词修改之后,行为有没有静默退化?答案不在上线之后,而在上线之前:把历史执行留痕做成黄金样本(Golden Trace),放进 CI 做回归比对,退化在合并前就被发现。没有这一步,"模型升级"在工程上就等同于玄学。
九、落地路线:先收敛,再扩展
Harness 的架构思想说起来宏大,落地时反而要克制。别一上来就搞多智能体、DAG、向量数据库——从最小的确定性约束开始,按风险暴露顺序逐级加固:
| 阶段 | 先做 | 先别做 | 通过标准 |
|---|---|---|---|
| 一:可控循环 | 显式状态机、轮次上限、中断传播、基础日志 | 多智能体、自动规划 | 每一步可追踪,随时能安全停下 |
| 二:工具契约 | Schema 校验、allow/ask/deny、只读优先 | 自动执行高危命令 | "想做什么"与"允许做什么"分开 |
| 三:预算与快照 | 静态前缀、token 预算、输出截断、快照续跑 | 全量记忆注入、无限历史拼接 | 长任务可恢复,账单不失控 |
| 四:观测与回放 | Trace/Span、统一 ID、Eval 回归 | 大规模插件生态 | 事故可复盘,升级可验证 |
| 五:委派与编排 | SubAgent 隔离、DAG、黄金样本进 CI | 自由群聊式 Swarm | 并行有边界,成本可归因 |
这条路线背后的原则只有一句:先收敛行为,再扩展智能。 单 Agent 还没办法中断和恢复,就别急着加十个 SubAgent;工具调用还没有权限闸门,就别急着接生产 API;上下文预算还算不清,就别急着上长期记忆;Trace 和 Replay 还没有,模型升级就是玄学。
一个能进生产灰度的 Harness,最终要看五个最小闭环是否闭合:行为闭环(状态机 + 退出条件 + 中断)、输入闭环(预算 + 压缩 + 缓存命中监控)、行动闭环(契约 + 权限 + 沙箱)、状态闭环(会话 + 快照 + 分支回滚)、质量闭环(评测回放 + 黄金样本 + 退化告警)。闭环的意思是:系统不只会"做一次",还知道自己做到了哪一步、为什么失败、怎么恢复、下次如何避免。
最后回到开头。
Agent 的上限由模型决定,没有争议。但 Agent 能不能进生产、能不能跑长任务、能不能被审计、能不能从事故里恢复——这些由 Harness 决定,而不是由模型决定。
模型的军备竞赛会有天花板,系统工程的护城河没有。当模型越来越便宜、越来越快、越来越同质化,"谁更会写提示词"的差距会迅速抹平,而"谁的运行时更能兜底"的差距会逐年拉大。
Harness 这门手艺,本质上是一次认知迁移:从"如何让模型更聪明",到"如何让一个概率系统变得可靠"。前者是模型厂商的事,后者,是每一个架构师手里现在就能握住的事。
更多推荐
所有评论(0)