外观
Harness 设计原则
在模型与 Harness 之争中我们说过:同一个模型,套上不同的 harness,表现可以天差地别。那么问题来了——设计 harness 时,到底该遵循什么?
坏消息是:这个领域还没有像「数据库范式」或「REST 约束」那样成熟的设计定律。好消息是:从 2024 年底开始,几篇工程实践文章迅速收敛出了一批被反复验证的原则。本页把这些原则提炼成八条可操作的规则,每条都给出陈述、理由、正例与反例。
本页的性质
这不是学术论文式的分类学,而是一份工程检查清单(checklist)。每条原则你都可以拿来自问:「我的 harness 违反它了吗?如果违反了,我是否有充分的理由?」
原则从哪来:三个主要来源
在展开原则之前,先交代它们的出处。理解来源的语境,比背诵条目更重要。
来源一:Anthropic《Building Effective Agents》(2024 年 12 月)
2024 年 12 月 19 日,Anthropic 的 Erik Schluntz 和 Barry Zhang 发表了《Building Effective Agents》。这篇文章的核心贡献有三点:
- 明确区分了 workflow 与 agent:workflow 是「LLM 和工具被预定义代码路径编排」的系统;agent 是「LLM 动态指挥自身流程和工具使用」的系统。这个区分后来被整个行业沿用。
- 给出了五种基础 workflow 模式:提示链(prompt chaining)、路由(routing)、并行化(parallelization)、编排者-工作者(orchestrator-workers)、评估者-优化者(evaluator-optimizer)。
- 反复强调简单优先:原话的大意是「找到能满足需求的最简方案,只在确有必要时才增加复杂度」,并坦承「agent 用延迟和成本换取更好的任务表现,这个交换何时划算需要你自己掂量」。
值得注意的是,这篇文章出自做了 Claude 和 Claude Code 的团队——他们自己大量生产 agent 产品,却写了一份「劝你先别急着上 agent」的指南。这种克制本身就是原则。
来源二:Cognition《Don't Build Multi-Agents》(2025 年 6 月)
Cognition(Devin 的开发方)的 Walden Yan 在 2025 年 6 月发表了这篇影响力很大的文章,提出了两条上下文工程(context engineering)原则:
- 共享上下文(Share context):传递完整的 agent 轨迹(full agent trace),而不只是单条消息。
- 动作携带隐式决策(Actions carry implicit decisions):一个 agent 的每个动作都隐含了它对任务的理解和决策;如果系统的不同部分基于不同假设行动,结果必然冲突。
文章的结论相当激进:这两条原则重要到「默认应该排除所有违反它们的架构」——包括当时流行的多 agent 并行架构。文中举的 Flappy Bird 例子很经典:把「克隆 Flappy Bird」拆给两个子 agent,一个做出了马里奥风格的背景,一个做出了不像游戏素材的小鸟,最后负责合并的 agent 面对两份互不相容的产出无从下手。
来源三:12-Factor Agents(HumanLayer,2025 年)
HumanLayer 的 Dex Horthy 仿照著名的「十二要素应用(12-Factor App)」提出了 12 条构建可靠 LLM 应用的要素,在工程圈流传很广。完整十二条是:
- 自然语言转工具调用(Natural Language to Tool Calls)
- 拥有自己的提示词(Own your prompts)
- 拥有自己的上下文窗口(Own your context window)
- 工具即结构化输出(Tools are just structured outputs)
- 统一执行状态与业务状态(Unify execution state and business state)
- 用简单 API 实现启动/暂停/恢复(Launch/Pause/Resume with simple APIs)
- 用工具调用联系人类(Contact humans with tool calls)
- 拥有自己的控制流(Own your control flow)
- 把错误压缩进上下文窗口(Compact Errors into Context Window)
- 小而专注的 agent(Small, Focused Agents)
- 从任何地方触发(Trigger from anywhere)
- 让 agent 成为无状态归约器(Make your agent a stateless reducer)
这份清单的基调是反框架:它主张你自己掌握提示词、上下文、控制流,而不是把这些交给黑盒框架。
三份文献的共识
三份文献立场不同、详略不同,但有一个惊人的共识:harness 工程的核心是上下文工程。模型已经不是系统里最聪明的部分之外唯一的瓶颈——喂给模型什么、藏起什么、何时打断它,才是。
原则一:简单优先,能 workflow 不 agent
陈述:从最简单的可行方案开始——单次 LLM 调用 → 固定 workflow → 单 agent 循环 → 多 agent 系统,每一级复杂度都必须用「上一级解决不了的具体问题」来换取。
理由:复杂度不是免费的。Anthropic 明确指出 agent 用延迟和成本换取任务表现;Cognition 则展示了多 agent 架构在上下文共享上的结构性缺陷。每一层复杂度都带来新的失败模式:workflow 的失败是可预测的代码 bug,agent 的失败是不可预测的模型行为,多 agent 的失败是系统性的决策冲突。调试成本沿这个梯度指数上升。
一个常见的复杂度阶梯:
复杂度 ↑ 可预测性 ↓ 调试难度 ↑
单次 LLM 调用 翻译、分类、抽取
│
固定 workflow RAG、提示链、路由
│ (步骤可预知,控制流在代码里)
单 agent 循环 编码助手、深度研究
│ (步骤不可预知,控制流在模型里)
多 agent 系统 并行研究、长时任务
(决策分散,上下文碎片化——慎用)正例:SWE-agent 用的是一个相当朴素的单 agent 循环——模型在一个专为它设计的命令行界面里逐步操作。它的论文关注点不是架构多精巧,而是 agent-计算机接口(ACI)的设计如何让简单循环发挥出强性能。
反例:一个内部工单系统,用「分类 agent + 摘要 agent + 回复 agent + 审核 agent」四个 agent 串联,实际上每个步骤输入输出完全确定,一个提示链 workflow 就能解决——多出的三层 agent 循环只贡献了延迟、token 成本和不可预测的串联失败。
判断标准
问自己:「如果我把这个 agent 换成一段确定性代码 + 一次 LLM 调用,会丢失什么?」如果答案是「什么都不会丢」,那你需要的是 workflow,不是 agent。
原则二:让模型做决策,让 harness 做约束
陈述:模型负责需要判断力的部分——下一步做什么、答案是否足够好;harness 负责需要确定性的部分——能调什么工具、能碰什么文件、花多少钱、什么时候必须停下来问人。
理由:这是职责分离(separation of concerns)在 agent 时代的版本。模型的长处是开放世界中的模糊判断,短处是确定性执行;代码正好相反。把约束写进提示词(「请不要删除任何文件」)是把确定性工作推给了最不适合做它的组件——模型会以某个概率违反它,而这个概率在上下文变长、任务变复杂时会升高。把约束写进 harness(工具层直接没有 delete 权限)才是工程意义上的保证。
┌─────────────────────────────────────────────┐
│ 用户意图 │
└──────────────────┬──────────────────────────┘
▼
┌─────────────────────────────────────────────┐
│ 模型层:决策 │
│ 「下一步做什么?」「这个结果够好吗?」 │
│ 「需要问用户吗?」 │
└──────────────────┬──────────────────────────┘
▼ 提出动作(tool call)
┌─────────────────────────────────────────────┐
│ Harness 层:约束 │
│ · 权限:这个工具允许调吗? │
│ · 预算:步数/token/时间超了吗? │
│ · 护栏:这个动作需要人批准吗? │
│ · 状态:把结果和错误组织好喂回去 │
└──────────────────┬──────────────────────────┘
▼ 执行或拒绝
外部环境正例:Claude Code 的权限体系把「这个 Bash 命令能不能跑」的裁决放在 harness 的权限层,配合允许列表、目录边界和人工确认钩子——模型可以自由决策,但越界的动作在执行前被确定性拦截。详见权限与人机协同。
反例:在系统提示词里写「你只能修改 src/ 目录下的文件」,然后给模型一个裸的 write_file(path, content) 工具。第一次遇到模型「好心」地修复 node_modules 里的类型声明时,你就明白提示词约束和执行约束的区别了。
原则三:工具少而精
陈述:工具集应该小而正交,每个工具都有清晰的用途、良好的错误信息和确定的副作用;宁可给模型五个设计精良的工具,不给它三十个随手封装的 API。
理由:工具是模型的「手」,但每只多余的手都在消耗上下文窗口里的注意力预算。工具描述本身占 token;工具越多,模型选错工具、用错参数的概率越高。SWE-agent 论文的核心发现之一就是:接口设计显著影响 agent 表现——他们为模型定制了带行号、一次只显示一个窗口的文件查看器,而不是让模型直接面对原始的 cat/sed,这个 ACI 设计让它在 SWE-bench 上以 12.5% 的 pass@1 达到当时的最佳水平。工具是为模型这个「新类别的终端用户」设计的界面,不是给人看的 API 文档。
正例:一个好的 read_file 工具:带行号输出、超长时截断并说明「共 N 行,已显示 1–200」、读不存在的文件时返回「文件不存在,是否需要先 list_dir?」——错误信息本身在教模型如何恢复。
反例:把内部 OpenAPI 的 80 个端点原样注册成 80 个工具,每个端点的描述是从 Swagger 自动生成的英文摘要。模型在 get_user_by_id 和 fetch_user_details_v2 之间犹豫不决,猜参数格式,收到 422 错误码后茫然重试。
一个「工具设计」的微例子
差的错误返回:
json
{ "error": "exit code 1" }好的错误返回:
json
{
"error": "grep: pattern 'handleRequest(' not found in any file.",
"hint": "Searched 1,247 files under /repo. Did you mean 'handle_request'? Python 代码通常用 snake_case。"
}后者把「失败」变成了「可供模型下一步决策的信息」。这就是 Cognition 所说的:动作携带信息,错误信息也是上下文。
原则四:上下文即一切(garbage in, garbage out)
陈述:模型在每一步的输出质量,几乎完全由它此刻看到的上下文决定;harness 的第一要务是决定每一步把什么放进上下文、把什么拿出去。
理由:Cognition 把上下文工程称为「构建 AI agent 的工程师的头号工作」,这不是修辞。同一个模型,给它完整的错误堆栈和相关文件,它能修复 bug;给它一句「构建失败了」,它只能瞎猜。上下文工程涵盖:系统提示词、工具结果的组织方式、历史消息的压缩、检索注入的内容、子 agent 之间传递的信息——详见上下文工程。
这条原则有两个推论,正好对应 Cognition 的两条上下文工程原则:
- 给子 agent 传完整轨迹,而不是传结论。「主 agent 已经搜索过 X、排除了 Y 假设」这类过程信息,决定了子 agent 会不会重复劳动或做出矛盾假设。
- 警惕上下文中相互矛盾的指令。模型的注意力对冲突很敏感:系统提示词说「保持简洁」,用户消息里粘了一段「详细解释每一步」,中间还有历史轮次的第三种风格——输出质量会在这种拉扯中下降。
正例:Claude Code 的子 agent(subagent)设计符合 Cognition 的观察:子 agent 通常只被派去「回答一个定义清晰的问题」(比如「这个函数在哪里被调用」),而不是并行地写代码——因为并行写代码的子 agent 无法共享彼此的决策上下文,产出必然冲突。子 agent 的探索过程不污染主上下文,返回的是提炼后的答案。详见子代理。
反例:一个客服 agent,把用户三个月内的全部历史工单原文塞进每轮上下文——里面有过期的政策、已解决的抱怨、其他客服的错误承诺。模型被这些噪声「带偏」,开始引用两年前的退款政策。垃圾进,垃圾出,与模型多强无关。
原则五:失败要可见、可恢复
陈述:harness 必须把每一次失败——工具报错、解析失败、模型输出不合法——变成模型能读懂、能行动的上下文,并且让整个系统可以从失败点恢复,而不是从头再来。
理由:agent 循环天然会失败:模型会幻觉出不存在的参数,外部世界会超时,测试会挂。问题不在于失败,而在于失败之后系统是什么状态。12-Factor Agents 的第 9 条说得很直白:把错误压缩进上下文窗口(compact errors into context window)——错误不是需要被捕获吞掉的异常,而是喂回给模型的信息。再叠加第 5、6 条(统一执行状态与业务状态、简单 API 实现暂停/恢复):agent 的执行状态应该是可序列化的,循环中断后能从断点继续。
一个最小可用的失败处理骨架:
python
result = run_tool(call)
if result.ok:
ctx.append(tool_message(result.output))
else:
# 错误不抛出、不吞掉,而是压缩成一条模型能用的上下文
ctx.append(tool_message(
f"ERROR: {result.summary}\n" # 一句话说清发生了什么
f"stderr (last 20 lines): {result.tail}" # 只留关键尾部,防止撑爆窗口
))
# 执行状态可随时落盘:中断 → 恢复 = 重新加载 ctx 继续循环
checkpoint.save(ctx, step=i)正例:Aider 在模型输出无法应用的编辑(edit format 匹配失败)时,会把匹配失败的具体信息反馈给模型让它自我修正,并且整个编辑-测试-反馈循环可被用户随时中断、回退到任意 git 提交——失败既是可见的,也是有出路的。详见 Aider 案例。
反例:一个长任务 agent 跑到第 40 步时工具超时,异常一路抛到顶层,进程退出,四十步的中间成果——写了一半的文件、已经查清的结论——全部丢失,只能从第 1 步重跑。更糟的版本:超时错误被 except: pass 吞掉,模型以为命令成功了,基于错误前提继续推进了二十步。
原则六:为不确定性设计
陈述:把模型的每一步输出当作「一个概率分布的采样」而非「一段程序的返回值」来设计——关键路径上要有校验、重试、降级路径,而非假设模型一次做对。
理由:这是 harness 与传统软件工程最本质的区别。传统系统中,函数调用要么返回正确结果要么抛异常;agent 系统中,模型可能返回一个「看起来对但实际上错」的结果,而且同样的输入明天可能给出不同的输出。因此:结构化输出要用 schema 校验而不是正则硬解析;高风险动作要二次确认;评估者-优化者(evaluator-optimizer)这类模式本质上是承认「第一次生成大概率不够好」,把校验显式地建进循环里。
正例:生成代码 → 运行测试 → 失败信息喂回 → 修正,这个循环在 SWE-agent、OpenHands、Aider 中都以不同形式存在。它不假设模型一次写对代码,而是把「写对」变成一个收敛过程,把确定性最强的裁判(编译器、测试套件)放进环路。
反例:让模型生成 SQL 并直接在生产库执行,中间没有任何 dry-run、没有只读副本验证、没有行数上限保护。第一次模型把 DELETE FROM users WHERE id = 5 写成 DELETE FROM users WHERE id > 5 时,设计上的侥幸就变成了事故。
原则七:evals 驱动迭代
陈述:harness 的每一处改动——换提示词、加工具、调整上下文组织——都应该由评测(evals)来证明是改进,而不是靠感觉。
理由:harness 是典型的「牵一发而动全身」系统:改一句系统提示词,可能让 A 类任务通过率上升 5 个点、B 类任务下降 10 个点,而你只会注意到 A。没有 evals,迭代就是布朗运动。公开案例反复印证这一点:SWE-bench 之于 SWE-agent 和 OpenHands,Aider 维护的公开编码基准之于它的编辑格式演进——Aider 的多种编辑格式(whole、diff、udiff、edit-fenced 等)的取舍正是基准跑出来的结论,不是设计者的品味。这也解释了为什么可观测性不是锦上添花:没有 trace,你连评测失败的原因都无从定位。
正例:改进流程是:收集真实失败案例 → 把它们变成 eval 集 → 修改 harness → eval 集上验证 → 上线后把新失败再加进 eval 集。eval 集随系统一起成长,是 harness 团队最重要的资产之一。
反例:「我感觉新提示词更专业了」——在三个手测样例上看了看输出,就全量上线。两周后用户报障量上升,回滚,然后再也没人能说清那句「更专业」的提示词到底改了什么行为。
从小做起
evals 不需要一步到位建成平台。二十个你亲手标注过的真实失败案例 + 一个能自动跑 harness 并统计通过率的脚本,就已经超过大多数团队的起点。
原则八:自主性需要配比的护栏
陈述:给 agent 多大的自主权,就要配多强的护栏(guardrails);护栏的强度应该随动作的不可逆性和爆炸半径递增,而不是平均分布。
理由:自主性与风险不是线性关系。让 agent 自由探索一个只读代码库,风险接近零;让它自由执行数据库迁移,一次失误就是灾难。合理的护栏设计是分级的:
风险低 ◄────────────────────────────────► 风险高
只读操作 可逆写操作 不可逆/高爆炸半径
(read/grep) (edit/test) (rm/deploy/发消息)
│ │ │
完全自治 自治+事后审计 人工批准后才能执行这与 12-Factor Agents 第 7 条(用工具调用联系人类)的设计一脉相承:「请求人类批准」本身被建模成一个工具调用,agent 可以在任何它判断需要的时候发起,harness 把暂停、通知、等待、恢复的状态管理接住——人不是循环里被动的瓶颈,而是被显式设计进去的一环。
正例:Claude Code 的默认姿态:读操作自由,写文件提示确认,Bash 命令按规则匹配决定放行还是询问,用户可以逐级调整——把自主权当作一个旋钮,而不是开关。
反例:两种极端都是反例。一端是「为了安全,每步都人工确认」——用户在第 50 次点击「允许」后已经不看内容了,护栏沦为形式;另一端是「为了流畅,全程 --dangerously-skip-permissions」——直到 agent 在一次清理任务里删掉了它认为「多余」的目录。护栏设计的目标是把人的注意力花在真正需要判断的少数动作上。
原则之间的张力
八条原则并不总是和谐的。成熟的 harness 设计很大程度上是在这些张力之间找平衡:
| 张力 | 一端 | 另一端 | 典型取舍点 |
|---|---|---|---|
| 简单 vs 能力 | 原则一(简单优先) | 长任务、开放任务确实需要 agent | 先用 workflow 覆盖 80%,剩余 20% 才上 agent |
| 自治 vs 安全 | 原则八(护栏) | 每加一道确认都在损耗 agent 的连续作业能力 | 按风险分级,不一刀切 |
| 上下文完整 vs 上下文干净 | 原则四(共享完整轨迹) | 窗口有限,噪声有害 | 子 agent 返回提炼结论;历史做压缩而非全量保留 |
| 速度 vs 可靠 | 原则六(校验重试) | 每个校验环节都在加延迟和成本 | 低风险路径快通,高风险路径才上 evaluator-optimizer |
没有普适的最优点
这些张力的最优解取决于你的任务分布、失败成本和用户预期。编码 agent 可以容忍较长的校验循环(测试跑得动就行),客服 agent 不能。原则是地图,你的 evals 才是罗盘。
一张速查表
| # | 原则 | 一句话自检 |
|---|---|---|
| 1 | 简单优先 | 这层复杂度换来的是什么具体问题被解决? |
| 2 | 模型决策、harness 约束 | 这条约束是写在提示词里,还是写在代码里? |
| 3 | 工具少而精 | 删掉这个工具,任务成功率真的会降吗? |
| 4 | 上下文即一切 | 如果我是模型,只看到这些上下文,我能做对这步吗? |
| 5 | 失败可见可恢复 | 这个错误现在去了哪里?模型知道吗?能重来吗? |
| 6 | 为不确定性设计 | 这一步如果模型答错了,谁会第一个发现? |
| 7 | evals 驱动迭代 | 上次改动有没有 eval 数据支持? |
| 8 | 自主性配护栏 | 这个动作不可逆吗?谁批准的? |
延伸阅读
- 什么是 Agent Harness —— 回到定义,理解这些原则作用于哪个层面
- Agent 循环 —— 原则五、六的落点:循环如何组织失败与校验
- 上下文工程 —— 原则四的完整展开
- 工具设计 —— 原则三的完整展开
- 权限与人机协同 —— 原则八的完整展开
- 可观测性与 Evals —— 原则七的基础设施
- 自己动手写一个最小 Agent —— 把八条原则压缩进两百行代码
- 常见陷阱与反模式 —— 本文反例的扩充版
参考资料
- Anthropic: Building Effective Agents(Erik Schluntz & Barry Zhang,2024-12-19)
- Cognition: Don't Build Multi-Agents(Walden Yan,2025 年 6 月)
- HumanLayer: 12-Factor Agents(Dex Horthy,2025 年)
- SWE-agent: Agent-Computer Interfaces Enable Automated Software Engineering(Yang et al.,2024 年 5 月)