Skip to content

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》。这篇文章的核心贡献有三点:

  1. 明确区分了 workflow 与 agent:workflow 是「LLM 和工具被预定义代码路径编排」的系统;agent 是「LLM 动态指挥自身流程和工具使用」的系统。这个区分后来被整个行业沿用。
  2. 给出了五种基础 workflow 模式:提示链(prompt chaining)、路由(routing)、并行化(parallelization)、编排者-工作者(orchestrator-workers)、评估者-优化者(evaluator-optimizer)。
  3. 反复强调简单优先:原话的大意是「找到能满足需求的最简方案,只在确有必要时才增加复杂度」,并坦承「agent 用延迟和成本换取更好的任务表现,这个交换何时划算需要你自己掂量」。

值得注意的是,这篇文章出自做了 Claude 和 Claude Code 的团队——他们自己大量生产 agent 产品,却写了一份「劝你先别急着上 agent」的指南。这种克制本身就是原则。

来源二:Cognition《Don't Build Multi-Agents》(2025 年 6 月)

Cognition(Devin 的开发方)的 Walden Yan 在 2025 年 6 月发表了这篇影响力很大的文章,提出了两条上下文工程(context engineering)原则:

  1. 共享上下文(Share context):传递完整的 agent 轨迹(full agent trace),而不只是单条消息。
  2. 动作携带隐式决策(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 应用的要素,在工程圈流传很广。完整十二条是:

  1. 自然语言转工具调用(Natural Language to Tool Calls)
  2. 拥有自己的提示词(Own your prompts)
  3. 拥有自己的上下文窗口(Own your context window)
  4. 工具即结构化输出(Tools are just structured outputs)
  5. 统一执行状态与业务状态(Unify execution state and business state)
  6. 用简单 API 实现启动/暂停/恢复(Launch/Pause/Resume with simple APIs)
  7. 用工具调用联系人类(Contact humans with tool calls)
  8. 拥有自己的控制流(Own your control flow)
  9. 把错误压缩进上下文窗口(Compact Errors into Context Window)
  10. 小而专注的 agent(Small, Focused Agents)
  11. 从任何地方触发(Trigger from anywhere)
  12. 让 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_idfetch_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为不确定性设计这一步如果模型答错了,谁会第一个发现?
7evals 驱动迭代上次改动有没有 eval 数据支持?
8自主性配护栏这个动作不可逆吗?谁批准的?

延伸阅读

参考资料