Development

什么是 Harness

模型只会生成文本。Harness 是把其中一部分文本变成工具调用、真正执行,再把结果送回模型的那一层运行时。

·15 min read·Lios09
#AI#Agent#Harness#LLM#MCP

这个词已经用得很随便。打开一个编码产品,界面、模型、工具、循环经常被叫成同一个名字。

先把它们拆开。后面几件是实际接 agent 时绕不开的:协议长什么样、循环在哪停、权限和上下文归谁、MCP 装上之后为什么有时像没装。


先给一句能用的定义

Harness 是包在模型外面的运行时。

它做五件事:

1. 准备一组工具,每个工具有名字、说明、参数
2. 用模型认得出的格式,把这组工具放进本次请求
3. 认出模型回复里的工具调用,并真正执行
4. 把执行结果写回对话
5. 决定要不要再叫一次模型,直到这次任务停下来

模型负责下一个 token。Harness 负责让某些 token 变成副作用,再把副作用的结果变成下一批 token。

你在终端里看到的 TUI、在桌面端看到的聊天窗,是界面。界面把这场循环展示给人。


人用的是界面

人很少靠「为每件事现写一段程序」来使用计算机。人用的是已经包好的入口:按钮、快捷键、命令、网页。

Chrome 是浏览器的界面。Claude Code 的终端界面、ChatGPT 的桌面应用,也是界面。人通常看不到底下那层怎么读文件、怎么改 diff、怎么发请求。人看到的是输入框、批准按钮、终端输出。

理论上,人可以为每个任务写一段新的 Python 或 JavaScript。产品存在的理由,就是不必这么做。


模型默认只会写字

语言模型的本职是生成文本。读文件、改文件、跑命令、搜网页,都是文本之外的事。

这些能力要变成模型能用的东西,就得先变成工具。现在的编码模型,训练时就见过这类工具:读文件、改文件、执行命令、搜索。名字各家不同,形状差不多。

模型仍然发不出一次真正的系统调用。它能做的,是在回复里交出一份「请帮我跑这段」的结构。教学上可以把它想成这样一段文本:

read_file
  path: README.md

生产里往往更进一步。提供商的 API 通常直接给出结构化的工具调用,例如 Anthropic 的 tool_use 内容块,或 OpenAI 风格的 function call。模型权重吐出的仍是 token,API 已经帮你把其中一段收成了名字加参数。

于是会出现两种 harness:

文本协议
  模型把调用写在正文里
  Harness 自己解析

原生工具协议
  API 返回结构化的 tool call
  Harness 按这块结构执行

写成正文的那种调用属于第一种。它说明一件事:调用首先是模型能写出的东西。自己写 harness 时,优先走提供商的原生工具协议。解析正文里的伪 XML,是没有原生工具、或要走自定义通道时的退路。

无论哪一种,解析完都还只是数据。计算机没开的时候,敲键盘也不会有反应。必须有一个程序接住这份数据,跑对应的代码,再把 stdout、报错、文件 diff 送回去。

少任何一步,模型都只是在空转。


一次调用里,Harness 在做什么

把上面五件事展开,就是大家说的 agent loop:

组装本次请求
  系统提示、历史、工具清单、相关文件
        │
        ▼
调用模型
        │
        ├── 只有正文 ──────────────► 结束,把正文交给界面
        │
        └── 带有工具调用
                │
                ▼
           权限 / 沙箱检查
                │
                ▼
           执行工具
                │
                ▼
           把结果追加进对话
                │
                ▼
           再次调用模型

循环在哪里停,也是 harness 的事,工具自己决定不了:

  • 模型明确表示这一轮结束
  • 达到最大轮数
  • 人中断
  • 工具失败,且当前策略选择停下来,而不是把错误塞回去让模型重试

编码产品里的 harness 通常还拥有这些。定义里没写,一动手就会碰到:

职责它决定什么
工作目录和沙箱命令能碰到哪些文件
权限哪次写入、哪次联网要人点头
会话记录关掉窗口之后还能否续上
上下文预算对话变长后,压缩什么、丢掉什么
钩子工具执行前后再插一层检查
子代理再开一个循环,用另一份工具和提示

所以「有 read_file」只说明工具存在。两个产品可以都叫这个名字,一个直接覆写整文件,一个只应用补丁;一个默认执行,一个每次弹批准。用起来完全是两个东西。差异在 harness。


界面和 Harness 是两层

Claude Code 经常被直接叫成 harness。这个说法只对了一半。

你在终端运行 claude,打开的是 TUI。你在 Claude 桌面端打开 Code,打开的是 GUI。这两层都是界面。

界面底下,Claude Code 通过 Anthropic 的 Agent SDK 跟模型交互。ChatGPT 那边是同一结构:聊天应用对着 Codex App Server,这层做在 CLI 里。

人容易混,是因为这两款产品把 harness 烤进了应用里,打开就能用,看不见接缝。

同一套 harness 可以被别的应用装进去。已经看得到的几种接法:

  • Cursor 的 harness 被 Grok Bot 使用
  • Anthropic Agents SDK 和 Codex App Server 被 T3 Code 使用
  • Pi 被 OpenClaw 使用

换一款「编码 agent」,有时换的是模型,更多时候换的是这层运行时:工具表、权限默认值、上下文怎么塞、编辑怎么落地。模型可以很接近,产品手感可以差很远。


工具归谁

把 harness 想成一间厨房。锅、刀、锅铲是工具。没有一间厨房能备齐每个厨师要的全部厨具,所以好的 harness 从一开始就假定:常用的自己带,其余的允许人加进来。

Codex 和 Claude Code 自带的工具很多。Pi 这类 harness 故意很瘦,把选择权留在外面。

这里有一条容易忽略的边界:

        人                    模型
         │                     │
         │    只看见界面        │    只看见名字、说明、参数、返回文本
         │                     │
         └──────────┬──────────┘
                    │
                Harness
                    │
         真正持有工具实现的一层
                    │
        read_file / bash / 你自己加的插件

人和模型都摸不到工具的实现。

对模型,工具是一份 schema,加上后来塞回来的一段结果。对你,工具是 diff、终端、一次批准提示。所以两个人都可以觉得自己「在用 Claude」,一个用的是官方工具表,一个还挂了邮箱、数据库、浏览器。权重可能一样,模型能碰到的世界不一样。这个世界是 harness 暴露出来的。


MCP 是工具的另一种住法

MCP(Model Context Protocol)解决的是:工具的代码不必住在 harness 里面。

可以分成两种托管:

内置工具
  实现跟着 harness 走
  权限、沙箱、生命周期由 harness 管

MCP
  实现住在另一个进程,或另一台机器
  Harness 负责发现、转发、把结果写回上下文

可以压成一句:工具是「在这里跑一段代码」,MCP 是「到别处发一次请求,让那边做事」。常见用途是把外部数据接进来,比如邮箱、站点分析、工单。

对模型来说,两边长得一样:名字、描述、参数 schema、一段返回。模型分不出这段结果来自进程内函数,还是来自一次 MCP 调用。这个区别是给人看的,用来决定代码放哪、谁来升级、挂了算谁的故障。

一个贴切的类比是浏览器:扩展像内置工具,代码跑在你这台机器上的浏览器里,能改页面怎么工作。网站像 MCP,你不把实现装进浏览器,你只需要知道地址,按它公布的方式去用。

装上服务器,不等于模型就会用。Harness 还得做完这四步:

连上 MCP server
    │
    ▼
把 tool schema 放进这一次请求
    │
    ▼
模型吐出调用时,转发到那个 server
    │
    ▼
用模型认得出的格式,把结果写回去

少一步,它就是一本没人翻开的菜谱。


为什么有些能力做成 MCP,而不是做成插件

有些能力不属于任何一间厨房。比如让一条 Claude Code 线程再去拉起 Codex 里的子代理,编排逻辑得活在 harness 外面。T3 Code、Herdr、Orca、Superset 都在做这类事。

这些代码可以重写成 Claude Code、Codex、Antigravity、Pi、Cursor 各自的插件。那样每接一个新 harness,就要移植一次。

另一条路是把代码留在编排这层,对外只暴露一个 MCP server。各个 harness 只要会说 MCP,就能调用,不必各自内置这套编排。

MCP 在这里像一本菜谱。它不改造厨房的灶台。厨师会不会翻开,取决于你有没有把这本书放上案板,也取决于这间厨房认不认这本书的格式。


Agent 是模型和 Harness 叠在一起

单独的模型只会生成文本。单独的 harness 只是一间空厨房,没有人下刀。

两者叠在同一次循环里,才成为 agent。

日常口语里的「一个 agent」,经常只是这份配置:

哪个模型
哪份系统提示
哪组工具
哪些 MCP server
默认权限松还是紧

配置和运行时要分开记。换一份提示词,还是同一个 harness。换一套压缩上下文、派生子代理、失败后重试的策略,那是换了运行时。产品页面上它们都可能被叫成「换一个 agent」。


有些产品是餐厅,不是厨房

坐在 harness 上面的产品,自己不是 harness。不另外带一套 harness,它跑不起来。T3 Code 就是这种:厨房可以换,它负责把客人和菜单接进去。

对应关系是:

厨师    =  模型
厨房    =  harness
餐厅    =  坐在上面的那层产品
菜谱    =  它通过 MCP 交给每间厨房的能力

餐厅允许你换厨房。它有自己的做法,会把菜谱递进去。厨师不会自动去翻。MCP 得接上,模型也得在某一步真的发出调用。


一张可以带走的分层

从下往上:

模型
  权重。根据上下文生成下一批 token

提供商 API
  消息格式、工具 schema、流式输出
  经常已经把工具调用收成结构化块

Harness
  循环、工具执行、权限、沙箱
  上下文组装、会话、压缩、子代理

界面
  终端、桌面、网页
  人在这里打字、看 diff、点批准

产品
  上面几层的一种打包方式
  有的把 harness 包在里面,有的要求你自己带一套进来

判断一个新名词该放哪一层,可以问三个问题:

  1. 拿走模型,它还能不能自己把任务做完?不能,说明它在靠模型产生下一步。
  2. 关掉界面,循环还能不能跑?还能,说明界面只是观察口,干活的是 harness。
  3. 这个能力的代码住在哪?住在进程里,就是内置工具。住在别处、靠协议进来,就是 MCP,或别的远程工具。

三个问题都指向同一件事:模型提出下一步,harness 让下一步发生,再把发生过的事告诉模型。人通过界面观看,并在该停的时候插手。

Related Posts