跳到主要内容

Harness Engineering:从零讲清楚

Santiago

来源:santi 在 X 上

图像

一年前,如果你问别人如何改进一个 Agent,答案几乎总是一样的:换个模型,或者写一段更好的提示词。

但那只是一部分。我想给你展示为什么:

OpenAI 构建了一整个内部产品:一百万行代码、1500 个 pull request,而没有任何人类写过一行代码。

LangChain 把它自己的编码 Agent 从 Terminal Bench 2.0 的第 30 名带到了前 5 名。而它并没有更换模型。

Anthropic 记录了类似的事情:同一个模型,在不同的配置下,可能给你留下一个看起来不错却跑不起来的应用,也可能给你留下一个真正能用的应用。

这三者没有一个是通过更换模型做到的。三者都投资在了同一件事上:围绕模型的那个系统。

而这恰恰开始被称作 Harness Engineering。

他们到底构建了什么?

这就是我们在这里要从零搭起来的东西:我们从一个只会返回文本的模型开始,一次加一块,直到得到一个你可以把具体目标委派给它的 Agent。

但首先,我们需要理解模型与 harness 之间的区别。

模型做什么,harness 做什么

当你让一个编码 Agent(不管是 Claude Code、Codex 还是别的什么)去做一项任务时,你看到的只有一样东西:一段对话。你写下请求,Agent 就开始干活。它搜索文件、打开它们、写代码、跑测试,然后把改动展示给你。

看起来这一切都是模型做的。但实际上,模型只做了一部分:思考和决定该做什么。

一个模型接收文本并返回文本。这就是它做的全部。它不打开文件,不执行命令,也没有记忆。

但那么,如果不是模型,是谁在查找文件?是谁在写代码?一定还有另一个组件负责这些。

我们把这个组件称作 harness。

一个 harness,就是围绕模型的那个软件系统,它把模型写出来的东西变成完成的工作:它组装模型将要阅读的内容,解释模型的回应,执行模型要求的动作,保存过程中发生的事情,并判断任务何时完成。

说得更简短些:一切不是模型的东西,都是 harness。运行的程序、它提供的 tools、它执行这些工具的地方、它在一次调用与另一次调用之间保存的东西。所有这一切。

但模型只能写文本,它是怎么请求做一件事的呢?

harness 传给模型一份可用动作的清单:搜索代码、读取文件、编辑它、跑测试。这些动作叫做 tools。模型并不执行它们,它只写出想用哪一个,以及带什么参数。这个请求仍然是文本。harness 才把它转换成真实动作。

如果现在还没完全讲清楚,没关系。后面我们会用例子深入地看。目前有这个想法就够了:模型请求,harness 执行。

我们来看一个更实际的例子。

你正在构建一个可以发帖的应用,有点像 Twitter。你打开了仓库,所有代码都在里面。你想给帖子加上一个点赞按钮。于是你对 Agent 说:

「给帖子加一个点赞按钮。」

从这一刻起,接下来发生的是:

  1. harness 组装模型将收到的消息:关于它该怎么工作的指令、你的请求、项目关于自身的文档(团队约定、每样东西在哪里),以及带参数的可用 tools 清单。
  2. 模型 用文本回应:「我需要找到每个帖子在哪里渲染」,并请求使用搜索(tool)。
  3. harness 在你的真实文件上执行这次搜索,并返回三个文件:Post.tsx、PostList.tsx 和 useUserData.ts。(别在意这些名字,只是举例)
  4. 模型 判断按钮放在第一个文件里,并请求打开 Post.tsx。
  5. harness 打开它,并把内容发过去。
  6. 模型 提出改动:在这里、用这段代码加上按钮。
  7. harness 把它应用到真实文件上。
  8. 模型 请求跑测试。
  9. harness 跑测试并返回结果:其中一个失败了,连同错误信息。
  10. 模型 读取那个错误,并请求下一步改动。循环重新开始。

与此同时,你看到的只是 Agent 在工作。

注意这个模式:模型从不做任何事,它只是请求。harness 搜索、打开、编辑、执行,并把发生了什么返回给模型,好让模型能决定下一步。

图像

这就是为什么这些工具感觉像是在一台电脑上工作的 Agent。模型提供推理。harness 把它与文件、终端、浏览器、数据库,或者任何它需要行动的环境连接起来。

具体落到实处:当你使用 Claude Code 时,模型是 Claude(Opus、Sonnet,随你选),而 Claude Code 是 harness。在 Codex 里也一样,Codex 是 harness,Sol 是模型。一个负责思考,另一个负责其余一切。

它们是你像别的程序一样安装并运行的程序。有意思的是它们内部有什么,而这恰恰就是我们现在要一步步搭起来的东西。

角色厘清之后,我们要一步步搭起一个 harness,以这同一个请求为目标:让 Agent 能加上点赞按钮,并且让你能相信它做对了。

我们从一个单独的、只会返回文本的模型开始,一次加一块。一共九块,分成两组。

前六块是 harness 给模型的东西,好让它能完成工作:

  1. Tools,让它能行动。
  2. 一个 loop,用来串联步骤。
  3. 记忆,让它记得自己做过什么。
  4. 上下文,用来选择每次调用时它看到什么。
  5. 一个独立于你自己的工作场所。
  6. 一个清晰的目标和验证,用来知道它是否完成了。

另外三块是 harness 给你的东西,好让你能相信结果:

  1. 权限与边界,让它不做不该做的事。
  2. 可观测性,让你看到发生了什么。
  3. Evals,用来衡量它是否变好了。

这两组的区别很重要。前六块让 Agent 能自主工作并得到一个结果。另外三块让你能限制它、理解它做了什么,并衡量它是否真的有效。

图像

harness 给模型的东西

1. Tools,让它能行动

回到那个请求:给帖子加上点赞按钮。

模型读了它,完美地理解了要做的事,并用文本回应:「应该在帖子的组件里加一个按钮」。

但你的仓库和之前一模一样。没有一个文件被打开,没有一行代码被写。

那么,如果它唯一能产出的就是文本,它要怎么去找到那个组件、打开它、并修改代码呢?

通过 tools。一个 tool 是 harness 提供给模型的一个函数:它有一个名字、一段说明它做什么的描述、它接收的参数,以及它返回的结果。harness 把可用 tools 的清单,以及模型请求它们时必须用的确切格式传给它。

对于我们的任务,我们给它 4 个:

buscar_codigo(texto)
leer_archivo(ruta)
editar_archivo(ruta, texto_viejo, texto_nuevo)
ejecutar_tests()

模型不能执行它们。它唯一能做的,是写出想用哪一个:

buscar_codigo("post")

这仍然是文本,但现在它有了一种 harness 能识别的形式。harness 读取它,在你的文件上执行搜索,并在下一次调用时把结果返回给它:

  • components/Post.tsx
  • components/PostList.tsx
  • hooks/useUserData.ts

这一来一回就是全部机制:模型按一种约定的格式请求,harness 执行并回应。

这里有一点很重要:你如何设计这些 tools,会改变 Agent 的行为。让 editar_archivo 失败时只返回一个 error,和让它失败时说「我在 Post.tsx 里没找到那段文本,但在第 42 行有一处缩进不同的相似匹配」,是不一样的。要考虑到,精确性至关重要。用前者,模型只能猜。用后者,它已经知道该去哪里改正。(OpenAI 甚至到了这个地步:专门为了让 Agent 能读懂而去撰写他们 linter 的错误信息。)

有了 tools,模型就已经能让你的仓库里发生事情了。这是模型与环境交互的方式。

但注意我们停在了哪里:它请求了一次搜索,harness 返回了 3 个文件,然后这次交互就结束了。一个请求,一个回应。按钮仍然不存在,而且没有人会去请求下一步。

2. 一个 loop,用来串联步骤

所以需要有人再次调用它。

想想加上那个按钮都需要些什么:(1)搜索组件,(2)读取它,(3)理解数据是怎么保存的,(4)写下改动,(5)跑测试,(6)修好失败的地方。至少六步。

而且它没法一开始就把它们规划好,因为每一步都依赖上一步的结果。在搜索返回文件名之前,它不知道自己该打开哪个文件。在读取文件之前,它不知道自己该写什么代码。在跑测试之前,它不知道是否奏效了。

所以 harness 不是只调用它一次。它调用很多次,而在每一次调用时,都告诉它上一次是怎么结束的。

harness 会做类似这样的事:

mientras la tarea no esté terminada:
    respuesta = llamar_modelo(el pedido + todo lo que pasó hasta ahora)
    resultado = ejecutar(la tool que pidió)

换句话说:问模型该做什么,去做,告诉它发生了什么,然后再次问它。直到完成。(一个 loop!)

重要的地方在第二部分,「截至目前发生的一切」。第一轮里,模型只有你的请求。第二轮里,它已经知道存在哪些文件,因为 harness 把搜索的结果告诉了它。第三轮里,它已经读过一个文件了。第四轮里,它已经知道有一个测试失败了,以及为什么。

每一轮开始时都比上一轮知道得更多。这就是为什么它能比单次尝试走得更远。

这个模式常被联系到 ReAct:推理、行动、观察发生了什么,再重新推理。它是当今许多 Agent 的核心。

而这里出现了一个之前缺失的词:当模型和 harness 在这个循环里一起运作、追逐一个目标时,那就是一个 Agent。

LangChain 把它概括为一个公式:

agent = 模型 + harness

它不是在前两个之外新增的一块。它是整个系统运转时的名字。

还有一点值得注意:没有人事先决定第二步必须是打开 Post.tsx。是模型在看到搜索结果之后选择了它。如果那次搜索只返回一个文件,或者一个都没返回,下一步就会是别的。

在一个 Agent 里,每一步都源自上一步发生的事。这就是为什么它能解决你在搭建它时未曾预想到的任务。

还剩一个悬而未决的问题:loop 什么时候停止?目前来说,是当模型说自己完成的时候。而这恰恰就是我们后面要解决的那个问题。

现在 Agent 可以工作好几个步骤了。但在一次来回与另一次来回之间,模型会把一切都忘掉。

3. 记忆,让它记得自己做过什么

如果模型在两次调用之间忘掉一切,它怎么还能继续工作?

答案藏在 loop 里,藏在我们一带而过的那个说法里:那句「截至目前发生的一切」。

模型怎么会知道截至目前发生的一切?这些信息存在哪里?

不在模型里。每次调用都是独立的(stateless):一段文本进来,另一段出去,然后就结束了。什么都不留下。

在任何聊天里都容易看到这一点。当你连着写三条消息,而模型在回答时考虑到了第一条,它看起来像是记住了这段对话。它并没有记住:每一个回合都会把完整的历史记录从头再发一遍给它。那个看起来像记忆的东西,其实是 harness 每次都在重发整段历史。

Agent 里也一样,只不过重发的不是聊天的消息,而是工作的记录。如果第 3 轮里模型发现偏好是用 X 函数保存的,到了第 4 轮它就已经不知道了,除非有人再告诉它一次。

那个「有人」就是 harness。所以我们加上记忆:把状态保存在模型之外,并在每次调用时再把它传给它。

状态可以包括:

  • 它已经检查过哪些文件;
  • 它应用了哪些改动;
  • tools 返回了什么;
  • 出现了哪些错误;
  • 还差什么没完成;
  • 它已经尝试了多少次。

loop 的每一轮,也就是每一次对模型的调用加上由它产生的动作,都会留下新信息。harness 把这一切记下来。

经过三四轮之后,我们 Agent 的状态可能长这样:

Objetivo: agregar me gusta a los posts.
Archivos revisados: Post.tsx, useUserData.ts.
Cambio aplicado: botón agregado en Post.tsx.
Pendiente: persistir la preferencia y verificar la UI.
Último error: falta agregar likePost al mock de useUserData.

这些东西不存在模型里。它活在 harness 里,由 harness 保持更新,并用它来组装下一次调用。

再告诉你一件事:在长任务里,还需要会话之间的连续性。Anthropic 处理过这个问题,让每个会话都留下一个进度文件和清晰的 commit。下一个会话读取它们,理解做了什么,并从那里继续。

这是对一个具体问题的简单解法:让接手任务的 Agent 看到一张整洁的桌面,上面摆着之前的工作和接下来的步骤。

现在 Agent 记得自己做过的一切了。而这恰恰就是下一个问题:每次调用都把这些全部发过去,太多了。

4. 上下文,用来选择每次调用时它看到什么

模型对每次调用能接收多少文本,是有限制的。

每次调用都必须装进上下文窗口,也就是模型一次能读取的最大文本量。它以 token 计量,token 是词的碎片,每个大约三到四个字符。

如今的窗口是巨大的。Sol 能处理大约一百万 token,Claude 也差不多在这个量级。听起来非常多,而它确实是:相当于好几整本书。

但一个真实的仓库可能比这还大。算上代码、测试、文档和其他文件,一个大型 codebase 很容易超过一百万 token。

而即便装得下,你还有另一个问题:更多上下文不等于更好的上下文。如果你把整个项目发过去让它加一个按钮,重要的东西会淹没在成千上万行与任务毫无关系的代码里。而要读的东西越多,模型把注意力集中在错误部分上的概率就越大。

所以 harness 必须选择每次调用里放进去什么。而选错在两个方向上都很昂贵:如果发得太多,会烧掉窗口并增加噪声;如果压缩得太多,模型会丢失重要决策,并重复它已经做过的工作。

那它怎么选?靠两个动作。

第一个是给它一张项目地图,而不是整个项目。几行字告诉它每样东西在哪里,例如:

- 帖子在 components/Post.tsx 里渲染。 - 偏好用 hooks/useUserData.ts 保存。 - 组件测试与每个组件放在一起。

有了这些,模型还没读过一行代码,就已经知道该往哪里去。

第二个动作是等到需要时再把其余部分拿过来。如果它需要理解 useUserData,它就请求搜索它的用法,打开那些文件,仅此而已。仓库的其余部分永远不进入。

这就是 Agent 内部的 retrieval:在当前上下文之外搜索信息,只带回对下一步有用的东西。Anthropic 在这里详细展开过,讲了他们用来决定什么进、什么不进的那些策略。

这种工作方式——从很少的东西开始,边走边把其余部分拿来——叫做 progressive disclosure(渐进式披露)。你之后还会再看到它出现。

于是,上下文不是一个一开始组装好就固定不变的块。它在整个任务过程中不断变化:

pedido inicial
→ mapa del proyecto
→ búsqueda de Post.tsx
→ contenido del componente
→ usos de useUserData
→ error de un test

每个动作都产生新信息。一个好的 harness 会过滤它,保留重要的东西,避免用旧结果把上下文填满。尽管这也有代价:由于模型会缓存它已经处理过的内容以免重复付费,每当 harness 重写已经存在过的内容时,那份缓存从此就丢失了。

那如果对话还是变长、开始装不下了呢?那里有两个出口。一个是 compaction:就地总结旧内容,让 Agent 继续做同一件事但占用更少。另一个是完全切断,开一个新会话,并给它留下笔记说明停在了哪一点。

所有这一切(发什么、何时拿来、总结什么、丢弃什么)都有它自己的名字:上下文工程(context engineering)。这是如今谈论最多的东西,有时人们把它当作整个问题。但它只是 harness 里的一块,就像本文开头提到的 提示词工程一样:它仍然重要,仍然是某个更大事物的一部分。

现在它知道自己该做什么、该用什么信息了。还差一个到目前为止我们一直想当然的细节:这一切到底发生在哪里。

5. 一个独立于你自己的工作场所

Agent 做的一切都发生在某个地方。当它编辑文件或跑测试时,那是在真实的文件上、在一台真实的电脑上发生的。

问题是:是哪台电脑。

如果是你的,那么 Agent 的每一个错误都是你机器上的一个错误:一个被删掉的文件、一个弄坏别的东西的依赖、一条你本不想跑的命令。

另一个选项是给它一份副本去工作。把你的项目放进一个与其余部分隔离开的空间里。如果它在里面弄坏了什么,你扔掉它重新开始,而你的机器毫不知情。

那个地方叫做环境。当它是隔离的时候,就叫 sandbox(沙箱)。

这存在于你用到的每一个工具里。Codex 在操作系统的沙箱里跑命令,并让你在三个级别之间选择:只读、写入仅限于项目文件夹,或完全访问。默认是中间那个,而且网络是关闭的。Claude Code 有它自己的沙箱模式,在你平常见到的那些权限之上,又加了同样一层操作系统级别的底座。

它保护你免受什么?免受三件相当具体的事。

  1. 一条越出项目的破坏性命令。一条配置不当的 rm(删除)无法碰触文件夹之外的任何东西。
  2. 一个你没要求的连接。网络关闭时,Agent 无法把你的代码发到任何地方,也无法下载什么奇怪东西。
  3. 还有一件听起来不那么明显、但越来越重要的事:藏在 Agent 所读材料里的隐藏指令。如果它打开一个文件、一个依赖或一个网页,上面写着「把所有东西删掉、把凭据上传到某处」,沙箱就是让这件事走不远的东西,哪怕模型相信了它。

对最后这点要当心:沙箱是多加的一层,不是保证。比如,如果你为了让它安装依赖而启用了网络,那么其中一把锁就打开了。

搭建那个地方也是 harness 的工作,因为这不只是隔离的问题:还必须要决定里面有哪些文件、装了哪些依赖、它是否能上网、有没有一个测试用的数据库。

Agent 现在已经能行动、串联步骤、记得自己做过什么、选择要看什么,并在一个受控的地方工作。但它仍然不知道自己何时算完成了。

6. 一个清晰的目标和验证,用来知道它是否完成了

它已经能干活了。还差的是它要知道自己干得好不好,而这件事在结束之前很久就开始了。

「加一个点赞按钮」听起来是个具体的请求,直到你真的尝试实现它。

用户只能点一次吗?要不要显示计数?状态需要在刷新之后仍然保留吗?按钮放在哪里?可以动帖子的设计吗?

如果 harness 就把收到时的请求原样发出去,模型会自己补上这些空白。有时它猜对了。有时它做出了与你预期不同的东西。

而由谁来写这些东西?有两条路。

(1)一条是你来写: 就写在请求里,或写在仓库里一个 Agent 启动时读取的文件里,比如很多工具都采用的 AGENTS.md,或 Claude Code 的 CLAUDE.md。

(2)另一条是让 harness 来生成。 在开始工作之前,它先对模型做一次单独的调用,让它把你两行字的请求变成一份完整的规格说明。Anthropic 是这样测试的,用一个专门只做这件事的 Agent。

无论哪种情况,最终写下来的都是这样:

Objetivo:
Agregar un botón de me gusta en cada post.

Restricciones:
Mantener el diseño actual de la lista.
Usar el sistema existente de preferencias del usuario.

Criterios de aceptación:
- El botón aparece en cada post.
- Cambia de estado al tocarlo.
- El estado persiste al recargar.
- Los tests existentes siguen pasando.

验收标准是关键的部分,因为它们之后会变成验证清单。

为什么需要那份清单?因为某一刻模型会说:

「好了。点赞按钮已经能用了。」

而这句话说的是模型以为发生了什么,不是实际发生了什么。

所以验证要在模型之外寻找证据。在我们的例子里:

  1. 打开应用。
  2. 确认按钮在每个帖子上都出现。
  3. 点它,确认它改变了状态。
  4. 刷新页面,检查它仍然是点亮的。
  5. 跑测试。

每一点都直接来自我们之前定义的验收标准。

根据任务的不同,证据可以采取其他形式:一套测试、一张截图、一个 API 响应,或一次数据库查询。

让它更可靠的一种做法,是把「构建者」和「审阅者」分开。一个 Agent 实现改动,另一个 Agent 接收标准、检查结果并寻找问题。Anthropic 把它们叫做 generator 和 evaluator:一个生产,另一个用独立的眼光评估。两个角色可以是同一个模型;改变的是每次执行的上下文和目标。

有了这些,Agent 就拥有了工作所需的一切。现在,我们来说 harness 给你的东西。

harness 给你的东西

7. 权限与边界,让它不做不该做的事

到目前为止,我们一直在给它能力。到了这一步,Agent 可以搜索、读取、编辑文件、跑命令,并独自把一项任务维持很长一段时间。

而这本身就是问题。一个能做所有这些事的 Agent,也可能删掉一个不该删的文件、装一个弄坏别的东西的东西,或者对着错误的数据库跑一条命令。

我们之前看过的沙箱限制的是损失能扩散到哪里。权限是另一回事:它们定义模型可以请求什么,以及在动手之前必须先询问你什么。

这叫做权限系统,而它在实践中定义三件事:

  • 存在哪些 tools。 如果没有一个删除文件的 tool,模型就没办法删文件,哪怕它请求了。
  • 哪些自动执行。 搜索代码或读取文件不需要任何人批准。
  • 哪些会先问你。 编辑文件、安装依赖、对着数据库跑东西:这时 harness 会停下来等你的确认。

你会发现这一切都被叫做 guardrails(护栏),尽管这个词通常用得更宽泛:它还包括对 Agent 能说什么或交付什么的过滤,而不只是它能执行什么。

而这里是最重要的那个观念:重要的边界必须活在权限系统里,而不是活在指令里。像「不要删任何重要的东西」这样一条指令,依赖于模型在每一刻都正确理解它。相反,如果删除的 tool 从来就没被提供过,那就没有什么好理解的:不能删,就这样。

接下来是另一半:当某件事仍然出错时该怎么办。

一个错误不一定要总是中断执行。harness 可以允许 Agent 重试,并返回一条更清晰的信息。

但它也需要上限:重试多少次、多长时间、多少成本。两个例子:

  • Agent 写错了一个函数名,测试失败了。 它读取错误,改正那一行,再跑一次测试。重试恰恰是你想要发生的事。
  • Agent 试图修好同一个测试五次,五次都又失败了。 它已经不是在改任何东西了。harness 中断执行,向你展示每一轮它都尝试了什么,并把决定权留给你。当系统停下、把控制权交还给你时,这叫做 human in the loop(人在回路中)。

现在 Agent 可以失败了而不弄坏任何东西。还差的是能理解它为什么失败。

8. 可观测性,用来看到发生了什么

当一项任务出错时,Agent 的最终回答透露的信息很少。要理解问题,必须重建这次执行:

  • 模型收到了什么上下文;
  • 它选了哪个 tool;
  • 用什么参数调用它;
  • 得到了什么结果;
  • 在哪一步改变了方向。

这些 trace 向你展示真正的故障在哪里。也许模型从未收到一个关键文件。也许某个 tool 的描述令人困惑。也许最有用的那个错误被排除在了上下文之外。

没有可观测性,一切看起来都是「模型失败了」。相反,如果你有可观测性,你就能看到该修系统的哪一部分。

现在你知道该改什么了。还差的是知道这个改进是否奏效。

9. Evals,用来衡量它是否变好了

改动 harness 的一块之后,需要知道系统是变好了,还是你修好了一处却弄坏了另一处。

Evals 就是这个:跑一组稳定的任务,用定义好的标准衡量结果。这样你就能对比 harness 的两个版本,并发现退化。

一个例子:你改变了 harness 组装上下文的方式:现在,除了模型请求的那个文件之外,你还把它周围的文件也一起发过去。

你用一项复杂的任务来测试它——这项任务需要同时动好几个文件——结果比之前好得多。看起来是一个明显的改进。

但在简单的任务上它变差了。现在模型收到了一些它不需要的文件,有时最终编辑了错误的那个。

这不是靠测试一个单独的任务能看出来的。你要在改动前后跑同样那二十个任务并对比结果时,才能看出来。

全部合在一起

好吧……信息量很大?是的。好消息是,你不需要把这一切都记在脑子里。你只需要开始认出这些模式。我们回到开头,这次带着完整的 harness。你请求的东西和第一次一模一样:给帖子加一个点赞按钮。

接下来发生的是:

  1. harness 把你的请求变成一项带验收标准的任务:按钮在每个帖子上都出现、点它会改变状态、刷新后仍然保留,而且测试继续通过。
  2. 它用任务、项目地图和可用 tools 组装第一次调用。
  3. 模型请求搜索帖子在哪里渲染。harness 在沙箱里跑这次搜索,并返回那三个文件。
  4. 它把结果存进状态里,再次调用模型,此时带着比之前更多的信息。
  5. 模型请求编辑 Post.tsx。这个动作需要确认,所以 harness 停下来问你。
  6. 你接受。harness 应用改动、跑测试,其中一个失败了。
  7. 模型读取错误、改正、重试。这一次全部通过。
  8. harness 对照标准验证:打开应用、点按钮、刷新页面、确认它仍然点亮。
  9. 直到这时它才把任务标记为完成。而过程中发生的一切都被记录下来,以备需要复查。

之所以要有后面加进来的每一步,是因为没有它,某件事就会失败:模型碰不到任何东西、不知道该看哪里、忘记自己做过什么、弄坏不该弄坏的东西、没验证就说「好了」,或者失败了却不留下任何关于为什么的痕迹。

而模型还是开头那个模型。唯一变了的,是我们在它周围搭起来的东西。

后来出现的那些部件

这个 harness 已经能解决一项完整任务了。当任务变大时,更先进的系统会加进其他组织知识与工作的方式。

Skills。 它们把某一类任务的指令、标准和资源打包在一起。一个前端 skill 可以告诉 Agent 如何审查设计、遵循哪些约定,以及在结束时展示哪些截图。harness 在需要时才加载它,而不是让它一直待在上下文里:就是之前那个 progressive disclosure,只是应用于指令而非代码。我为此单独写过一篇文章,在这里。

MCP。 它标准化了一个 Agent 发现并使用外部工具或来源的方式。它能通过一个共同的接口连接 GitHub、数据库、内部文档,或一套工单系统。用 harness 的话说,它拓宽了 Agent 能从哪里获取上下文、能执行哪些动作。

Subagentes(子 Agent)。 一项大任务可以分给若干次执行,每次有不同的上下文和目标:一个调研仓库,一个实现,一个验证。好处不是靠增加模型得来的,而是在这种划分减少了上下文、分离了职责,或者允许独立地复查工作时才出现的。而一旦你这样划分,你就可以在每个角色里用不同的模型,简单的用便宜的、困难的用贵的:这就是 model routing。

长期记忆。 状态维持的是一项任务的脉络。长期记忆在任务之间复用信息:架构决策、团队偏好、反复出现的错误。harness 决定保存什么、何时取回,以及如何避免旧信息污染一次新的执行。

注意,这些部件没有一个新机制。它们都是之前那些的延伸:skills 是一种更有条理地管理上下文的方式,MCP 是一种加 tools 的标准方式,子 Agent 是多个协调起来的 loop,而长期记忆是能活过任务本身的状态。

背后那个模式——模型请求、harness 执行——始终是同一个。

那么,什么是 harness engineering?

值得把这两个词拆开看。

Harness,在英语里就是「马具」:你给某个东西套上的一整套皮带,好利用它的力量而不让它失控。动词 to harness 就由此而来,意思是拿一个已经存在的东西,把它导向对你有用的方向。

在软件里,这个词还有它自己的历史。几十年来,人们把环绕着一个你要测试的部件的代码叫做 test harness:它执行那个部件、给它输入、看它返回什么,并检查是否正确。

而这恰恰就是我们整篇文章一直在搭的东西。一个环绕模型的系统,给它输入、执行它请求的东西、观察返回的内容,并检查工作是否做对了。

Engineering 是另一半,也是常被跳过的那一半。搭一次还不够:必须衡量它、找到它在哪里失败、改变它,然后再次衡量。

把两者合起来:

Harness engineering 就是设计、衡量并改进那个让模型完成真实任务的系统的工作。

有意思的是,harness 依赖于装在它里面的模型。一条今天必要的规则,可能随着模型变好而不再有贡献。一个为某个模型设计的 tool,可能对另一个模型来说很难用。每一代新模型都迫使你重新审视:什么有帮助、什么是多余的、以及新的失败出现在哪里。

还有一点:模型是连同 harness 一起被训练的。Claude Code 和 Codex 在训练后阶段都带着各自的 harness,所以模型习惯了那个 harness 的具体形式。如果你之后改变了一个 tool 的逻辑,性能可能会下降,即便这个新 tool 同样合理。

但这并不意味着原生的 harness 永远是最好的。LangChain 测量过 Opus 4.6 在 Claude Code 里的表现,对比同一个模型在其他 harness 里的表现,结果它在 Claude Code 里得分明显更低。

所以 harness engineering 不是围绕模型搭一层就完事。它是一项迭代的工作:

ejecutar tareas
→ revisar trazas y resultados
→ encontrar una falla repetida
→ ajustar el harness
→ correr las evals otra vez

模型提供能力。harness 创造把它用好的条件。

而当这两部分一起工作时,就出现了一个你真正能把工作委派给它的 Agent。

收尾

下一次你打开 Claude Code 或 Codex、向它提出请求时,希望你看到的是另一种东西。

当出现一行字说它正在你的仓库里搜索时,你会知道那是模型请求了一个 tool,而 harness 正在执行它。当它打开三个文件而不是整个项目时,你会知道是有人决定了什么进上下文。当它回过头处理十步之前做过的东西时,你会知道那不是模型记住的:是 harness 在告诉它。当它停下来、在碰一个文件前请求你的许可时,你会明白那是权限系统。而当它跑测试并修好失败的地方时,你就能够在脑子里跟完整个 loop。

所有那些感觉流畅的东西,都是由一个个部件构成的,而现在你认识它们了。你能理解里面有什么。

希望你喜欢这篇文章,并希望它对继续学习有帮助。

抱抱。

santi (:

评论