Featured image of post UHP 深度拆解:继 MCP 之后,Agent Harness 也要有自己的协议了!

UHP 深度拆解:继 MCP 之后,Agent Harness 也要有自己的协议了!

把 UHP(Unified Harness Protocol)的规范仓库整个读了一遍:三个角色、三档一致性、七个对象、一整套任务生命周期,还有它刻意踩的坑。这是一层想给 Agent Harness 当 HTTP 的公共契约。

先说结论

我把 HarnessRouter 那个 UHP 规范仓库整个扒了下来——protocol/versions/ 下四个版本的完整章节、docs/ 里的实测数据、conformance/ 的检查项,一路读到 CHANGELOG 里同一天打了七个补丁的段落。读完的结论只有一句:

Agent Harness 这一层,终于有人把它当成一个需要协议的层来对待了。

如果你今年一直在跟 Agent 基础设施,应该已经感觉到那股暗流。2026 年上半年,六家主要的 AI 和云厂商几乎前后脚把"自己的 agent harness"做成了产品——一个原本属于工程实现的零件,几个月内变成了采购清单上的一行。可每一家的 harness 接口都不一样,结果就是:任何一个想把 agent 接进自己产品的团队,都要为每个 harness 重写一遍集成。

UHP 想解决的,就是这一层的接口问题。它不是一个新模型,也不是又一个 Agent 框架。它是一份 HTTP 契约,规定"客户端怎么驱动一个 harness"。

我把规范里的关键设计挑出来拆了一遍,顺手把它的安全条款整理成了一张对照清单。下面就是这些。

它到底是什么:任务,不是一次 completion

先把它和其他东西划清界限。

UHP 的定义很短:An open standard for running complete agent harnesses as shared infrastructure.(把完整的 agent harness 当作共享基础设施来运行的开放标准。)

这里的 harness 指一个完整的 agent 运行时——自己会规划、调工具、改文件、把结果报回来。Codex、Claude Code、Hermes 都是 harness。它们各自都会干活,问题在于:没有两家在"产品该怎么驱动它"这件事上达成过一致。 怎么起一个任务、怎么跟进进度、怎么接着聊、怎么取消、怎么拿回它产出的文件、失败了怎么知道为什么——今天的现实是,每个产品都为每个 harness 把这些问题重新回答一遍。

UHP 的回答是:这些问题只回答一次。

它跟模型 API 的关系,规范里有一句话我觉得是全文最精准的:

Model APIs give you a turn: messages in, tokens out, tools you have to run yourself. UHP gives you a task: work in, and a running agent that uses its own tools, keeps its own session, and hands back results and files. The unit of exchange is a job, not a completion.

模型 API 给你的是一个 turn(一轮对话),UHP 给你的是一个 task(一个任务)。交换的单位是 job,不是 completion。这句话把定位钉死了——它不在模型层,也不在工具层,它在"怎么跑一个 agent"这一层。

三个角色

规范里只有三个角色,画成一条链:

┌──────────┐   UHP over HTTP   ┌──────────┐   implementation-defined   ┌──────────┐
│  Client  │ ────────────────▶ │  Server  │ ─────────────────────────▶ │ Harness  │
└──────────┘                   └──────────┘                            └──────────┘
  • Client:想要把活干完的应用(产品后端、CLI、CI、甚至另一个 agent)。它只说 UHP,而且规范明确要求它不应该知道背后跑的是哪个 harness。
  • Server:实现这份规范的一方。它接收任务、驱动一个或多个 harness、用规范定义的词汇回报进度。怎么跑 harness(容器、子进程、队列、远端 worker)全部属于实现细节,不许漏进 wire format。
  • Harness:一个完整的 agent 运行时,用一个稳定的 base 字符串标识,比如 codex、claude-code、hermes。

这里有个我觉得设计得很对的概念:configured harness(已配置 harness)。

它不是"某个 base",而是"某个 base + 一套配置"——默认模型、system prompt、工具限制、skills、MCP servers、步数与时间预算。它是一等、可寻址的对象,客户端要干活时选的就是它。

Why a configured harness, and not just a base? The same base behaves very differently with a different system prompt, a different model, or a different tool set.

同一个 base,换了 system prompt、换了模型、换了工具集,行为可以差到不是同一个东西。把配置提升成一等对象,产品就能"不重新部署后端就改掉 agent 的行为"。

拆解一:三档一致性 + 能力发现

UHP 没有"要么全支持要么别来"这种粗暴划分。它定义了三个累进的一致性等级(Core ⊂ Extended ⊂ Full):

等级 必须实现
Core 能力发现、harness 发现、任务的流式与非流式执行、会话续接、取消、错误模型
Extended Core + 文件输入、artifact 取回、会话列表/检视
Full Extended + harness 生命周期管理(增改删)+ 会话分享

配套的是能力发现端点 GET /v1/uhp。注意它有两个不寻常的要求:

  1. 这个端点必须免鉴权——客户端得先知道自己在跟一个 UHP server 说话,才谈得上要不要出示凭证。
  2. 服务器必须显式报 false,而不是省略能力字段——这样客户端才能区分"不支持"和"这服务器比这个字段还老"。客户端则必须把缺席的 key 当作 false。

发现文档里 conformance_class 和 capabilities 还必须自洽:声称 extended 的服务器,必须把 files_input、files_output、session_listing 报成 true。规范的原话很硬:

advertising is a promise, and a client that trusts it and receives a 404 has been lied to in a way it cannot recover from.

能力声明是承诺,不是愿望清单。这一条我喜欢。

拆解二:七个对象,每个 id 都带前缀

对象模型是七个类型,每一个都带 object 字段标明类型,id 带类型前缀——好处是一个标识符永远不会让你猜它指向什么:

对象 object 值 id 前缀 生命周期
Harness harness chrn_ 直到删除
Response response resp_ 按服务器策略保留
Session session hsess 直到删除
File file file_ 随其 container
Container container cntr_ 随其 session
Environment environment henv_ 直到删除;可选
Memory memory hmem_ 直到删除;可选

它们的关系是这样:

Harness ──┐
          ├──▶ Session ──┬──▶ Response ──▶ Response ──▶ …    (每个任务一个,链式)
Model  ───┘              └──▶ Container ──▶ File, File, …    (artifacts)

两个设计取舍值得单独说。

Session 是隐式创建的。 第一个任务就自动建好 session,不需要先 POST /sessions。理由写在规范里:要求先建会话会凭空多一次往返、一个失败点、一个要清理的对象——而大多数第一次发来的任务根本用不上"会话"这个概念。只发一次性任务的客户端,可以一辈子不知道 “session” 这个词;需要连续性的客户端,引用它已经拿到的 id 就行。

Environment 和 Container 是分开的。 Environment 是"一个项目的文件和装好的依赖",构建一次、只读地挂进每个引用它的会话;Container 是会话的文件命名空间,agent 写出的文件变成里面的 artifact。规范里一句话概括:artifact 来自工作目录,environment 是工作目录读取的东西。把"只读底座"和"可写产出"拆开,这个边界划得干净。

拆解三:任务状态机 + 一条不能缓冲的流

任务从提交到结果,走这几个状态:

                    ┌──────────────┐
   POST /responses  │              │
   ────────────────▶│ in_progress  │
                    └──────┬───────┘
                           │
        ┌──────────────────┼──────────────────┬───────────────────┐
        ▼                  ▼                  ▼                   ▼
   ┌──────────┐      ┌──────────┐      ┌────────────┐      ┌───────────┐
   │completed │      │  failed  │      │ incomplete │      │ cancelled │
   └──────────┘      └──────────┘      └────────────┘      └───────────┘

这里最容易被忽略、但我觉得最实用的是 incomplete 和 failed 的区别。因为步数或时间预算停下来的工作,标 incomplete;出错才标 failed。规范特意解释了这个区分的意义:对客户端来说,incomplete 通常值得接着跑,failed 通常不值得。而且一旦进入终态就不许再变,连部分产出也必须保留:

Discarding partial work because a task later failed destroys the only evidence of what went wrong.

任务跑挂了就把之前的产出丢掉,等于把"到底哪里出问题"的唯一证据也一起丢了。这条规则很老练。

流式部分,协议直接定了事件词汇(response.created / response.output_text.delta / response.function_call_arguments.done …),并要求 sequence_number 从 0 开始、每个事件精确 +1——客户端因此能发现掉包,而不是悄悄渲染出一个缺口。有一条部署陷阱被专门点名:

Servers behind a proxy MUST disable response buffering. This is the single most common deployment error for UHP servers; it looks exactly like the harness being slow.

代理没关响应缓冲,会看起来和"harness 很慢"一模一样。这行提醒说明写规范的人自己踩过。

还有一个哲学性的选择:流是优化,存储的 response 才是真相源(source of truth)。 断线不会中止任务,服务器继续跑;客户端回来用 GET /v1/responses/{id} 重新读就行。规范直接点名了反模式——“把 stream 当成结果唯一存在的地方"的产品,每次负载均衡回收连接都会丢工作。

拆解四:故意长得像 OpenAI Responses API

这一点是被明说的,不是巧合:

UHP’s task surface is deliberately shaped like the OpenAI Responses API, and a conformant server MUST accept the subset of that request body … This is a compatibility decision, not an accident.

产品里已经有大量代码在说 Responses 协议了;现成的 SDK、流式解析器、UI 组件,不用改就能对着 UHP server 跑。UHP 在此之上加的是 harness 才需要、而模型端点根本没有概念的东西:跑的是哪个 harness、它的工具和 skills、跨任务存活的会话、回来的文件、以及"对已经在跑的活取消”。

扩展只发生在文档化的增量位置(metadata、少量额外请求字段、额外对象类型),绝不改变已有字段的含义。一个把所有 UHP 扩展都忽略的客户端,仍然能拿到一个可用的任务。这个"向后兼容到能摆烂"的取向,对一个想被大范围实现的标准来说是对的。

拆解五:packaging 不自己造,直接复用三层

这是我读下来觉得最聪明的一处:UHP 不定义 package 格式。

一个 UHP plugin,本身就是一个 Agent Plugins 1.0.0 的包——一个根目录有 plugin.json 的文件夹,MCP servers 声明在 mcp.json,skills 放在 skills/<name>/SKILL.md,每个 skill 又是一个带 SKILL.md 的 Agent Skills 文件夹。三层套得干干净净:

Agent Skills     SKILL.md and its folder               what one skill is
Agent Plugins    plugin.json + mcp.json + skills/      how skills and tools are packaged
UHP Plugins      this chapter                          how a package is installed into a hosted harness

为什么要"采用一个格式"而不是"定义一个格式"?规范给了个特别实在的理由:

A package format is worth exactly the number of places it installs.

一个包格式的价值,等于它能装进去的地方数量。因为 Agent Plugins 是由来自主要 agent 客户端的指导委员会维护的,为一个客户端写的 plugin 不用改就能装进 UHP harness,反之亦然。如果 UHP 自己造一个格式,每个边界都要转换,而"转换"正是内容会丢的地方。

同样的思路也体现在它和 MCP 的关系上。命名文档里说得很清楚:MCP 标准化"能力怎么暴露",Agent Plugins 标准化"skills 和 MCP server 定义怎么打包",UHP 把前两者收进一个配置。生态里的开放标准,在一层接口上汇合,而不是碎成一堆。

我看到的三个"坑",以及它们为什么是对的

规范里有两处,初读像"没做完",细看才发现是刻意的决定。这也是我认为这份规范值得认真对待的原因。

坑一:tools 和 include 被保留,但永远不起作用

这两个字段跟着 Responses 的 wire shape 一起进来的。UHP 的处理是:服务器接受它们、从不据此行动、并在 metadata.ignored_fields 里报出来。它直接说这是"a decision rather than a gap left open"——是个决定,不是留着没补的洞。

tools 为什么不能按 Responses 的意思用?因为在 Responses 里客户端执行工具:模型吐 function_call,客户端跑它,再把结果作为下一轮的输入发回去。而 UHP 把这两段都放进了 output——harness 自己调用、自己执行、把过程当作可观测性报出来。没有一条"工具结果"的输入路径,所以 tools 暗示的那个循环,在 UHP 的对象模型里根本没地方完成。

那 tools 能不能当"这个任务用哪些 MCP server"?也不行,而且理由更重:这会变成一个越权原语。 如果一次请求能挂一个 MCP server,那么任何拿到 API key 的人都能把 agent 指向自己选的端点,agent 就用 harness 的凭证和工作区权限在服务端执行工具。而 harness 的所有者和 API 调用者通常是不同的两拨人。请求级的声明,等于把一个能力决策从"负责任的业主"手里悄悄挪给"任意调用者"。

规范把底下的规则总结成一句我很赞同的:

narrowing is safe, widening is escalation.(收窄是安全的,放宽是越权。)

未来版本可以合理地加一个"按请求禁用某工具"的列表,但永远不该加"按请求授予"。

坑二:模型替换必须上报

如果请求的模型无法在该 harness 上服务,服务器只有两条路:要么 422 model_unavailable 失败,要么替换成该 harness 的授权默认模型、并在 response 里记下来(requested_model / model_fallback / model_fallback_reason)。客户端永远能通过比对 model 和 metadata.requested_model 来回答"我点的模型到底跑了没有"。

这条规则的理由,戳到了所有做过评测和成本归因的人:

A server that substitutes silently makes every measurement downstream wrong — benchmarks, cost attribution, quality comparisons — and the client has no way to detect it.

静默替换,会让下游所有的度量全错——benchmark、成本归因、质量对比——而客户端还无从察觉。上报一个替换,成本就两个字段。

我自己在做 Agent 观测和网关时最怕的就是这个:你以为在 A 模型上跑出来的数据,其实是 B 模型跑出来的,而且没有任何地方会报错。UHP 把"模型身份"提到协议层强制可审计,这一步走得很正。

坑三:安全章节是一张"给实现者打勾的清单"

UHP 的 security 章节有个少见的写法:凡是别处提过的安全要求,这里重复一遍,而不是只给交叉引用。理由也写明了——一条要读者从六个章节里拼起来的安全规则,就是一条会被漏掉的规则。

我把它整理成了一张对照清单:

  • 凭证:除 GET /v1/uhp 外每个端点都要鉴权;不得区分"没这个 token"和"这个 token 不配用",否则可被枚举;任何响应体、错误、事件、日志、artifact 里都不许回显凭证;provider 凭证是系统里最值钱的秘密,而 agent 沙箱是最不可信的地方——agent 能被诱导读自己的环境再打印出来。
  • 对象作用域:越权访问必须返回 404 而不是 403。403 等于确认这个 id 存在,正是枚举攻击者想要的;而且作用域要覆盖每个操作,不只读——取消、删除、续接、下载都算对象访问。
  • artifact 是敌意内容:agent 可以被说服写出带指定内容、指定文件名的文件,所以服务 artifact 的一方必须把它当敌意内容处理。下载必须带 X-Content-Type-Options: nosniff(否则一个叫 x.html 的 artifact 就变成打客户端自己源站的存储型 XSS);应该从与主站不同的 origin 提供;通过 artifact id 做路径穿越,被点名是"UHP 实现里最可能的严重漏洞",一致性套件专门有 X-08 探针,但规范自己说"通过探针只是地板,不是证明"。
  • prompt injection 是客户端的责任:规范承认 UHP 防不了,并且说"假装能防的规范比明说的更糟"。它能提供的工具是:disabledTools(最小的工具集就是最小的注入面)、max_step 和 timeout_seconds(有界任务才不会被诱导成无界)、按信任级别拆不同的 harness(读不可信输入的 harness,不该同时持有高权限工具)、以及把工具调用做成可实时看见、可门控的事件流。
  • 资源耗尽:必须限制任务时长,并且预算停下时报 incomplete 而不是 completed;同一 session 的第二个并发任务必须拒绝(session_busy)——“两个 agent 在同一个工作目录里不是一个有定义的状态”;上传超限必须报 file_too_large,不许截断——被静默截断的输入会产生一个自信而错误的答案。
  • plugin 是第三方代码:plugin 里声明的 stdio MCP server 必须跑在跑 agent 的那个沙箱里、用 agent 的权限、且不得经 shell 启动;服务器只许展开 ${PLUGIN_ROOT} 和 ${PLUGIN_DATA} 两个占位符,且只在 args、env、cwd 里——“一个能把沙箱环境读进自己进程参数的 plugin,就会读到沙箱持有的任何凭证”。

这份清单我几乎是逐条点头读完的。它跟这两年 Agent 安全领域摔过的跤,一条条对得上。

一份实测数据:harness 不只是接口差异,是可度量的差异

规范仓库里还带了一份实测(docs/benchmark.md)。同一模型 deepseek-v4.1-flash,同一个包 SpreadsheetBench Verified 的前 50 个任务,web 工具全部关闭,WORKERS=2、TASK_CAP_S=900,四个 harness 跑出来的结果:

Harness Tasks Resolved Reward Wall(sum) Median wall Tool calls Calls failed Fresh in Cached in Output
cline 47 36 (77%) 0.77 6882s 108s 736 121 (16%) 1.20M 19.74M 1.21M
dsh 46 36 (78%) 0.78 6506s 100s 721 113 (16%) 761k 12.32M 752k
opencode 48 39 (81%) 0.81 5515s 75s 563 67 (12%) 666k 11.64M 709k
pi 47 40 (85%) 0.85 3605s 48s 526 89 (17%) 484k 7.53M 546k

同一个模型,决定解出率、耗时、token 消耗的不是模型本身,是 harness。pi 用 3605 秒墙钟跑出 85%,cline 用 6882 秒跑出 77%——几乎两倍的时间、更低的分数、更多的 token。这份表本身就是 UHP 存在的理由:如果这一层差异这么大,而每个产品都要为每个 harness 重造集成,那浪费是结构性的。

数据也保持了诚实:有两列被"靠边放"(cline 在 0.18.0 的 36/50、opencode 在某个补丁之前的 37/48),另有若干次运行因为"访问了网络"或"跑到工作区外面找任务"被单列不计分,比如有一条记录显示 harness 执行了 find / -name '*.xlsx' 2>/dev/null。把"不合规的运行"单列而不是偷偷抹掉,这个细节让我对这份数据的可信度评价高了一档。

版本节奏读出来的东西

UHP 的版本号是日期。我把 CHANGELOG 的版本线拉出来:

  • 2026-08-11 — 首次发布
  • 2026-09-12 — 增加 Plugins 章节(含 plugin 内的 stdio MCP server)、plugins 能力、五个错误码
  • 2026-09-28 — 一次修订
  • 2026-10-04 — 当前版本,同一天打了七个补丁

八个星期,四个版本,最后一天连打七个补丁。这个节奏说明两件事:一,这层确实有人真的在写实现、写的时候把规范里的洞一个个顶出来;二,它还没定型。规范自己把状态标成 “Draft standard — stable enough to build on, versioned so it can change safely”,是诚实的措辞。

补丁的内容也很有意思——大多集中在 Memories 章节的一条条细节上(记录内容必须是 parts 数组、memory id 不透明、recall 搜子树、统一"谁在操作 memory"的词表……)。这些不是功能膨胀,是把语义反复拧紧。一个标准愿不愿意在细处拧,往往比它喊的愿景更能说明它会活多久。

我的判断:这层会不会被大厂吸走?

得说实话,这里有个真实的张力。

UHP 由 HarnessRouter 发起并主导,参考实现是它的 Community Edition(Apache-2.0,对规范套件的 Full 级报告,当前 52 项检查)。而 2026 年上半年把 harness 做成产品的,是六家大厂。一个由创业公司发起的标准,能不能管住大厂自己的 harness,是这份规范最大的问号。

但 UHP 有几处设计,客观上把"被采纳"的门槛压得很低,这是我愿意给它正面评价的原因:

  • 不要求托管服务。规范原话:conformant server 可以完全跑在你自己机器上、用你自己的 provider key、把一切存在你拥有的卷上。没有账号、没有 license key、不打电话回总部。
  • 一致性本地可验证。pip install -e protocol/conformance 然后跑 uhp-conformance 就行,一致性是一个"可独立验证的属性",不是承诺。
  • 复用而非另造。package 复用 Agent Plugins,能力暴露复用 MCP,peer 协作留给 A2A(命名文档明确说 UHP 与 A2A 互补而非竞争)。它把自己定位成"让这些标准在一层接口上汇合",而不是再开一个战场。

这套打法的风险也清楚:它的价值完全取决于有多少东西真的实现了它。 一份再优雅的规范,如果大厂 harness 各自为政、只在自家控制面里互通,UHP 就会沦为"参考实现很好、生态没跟上"的又一份 RFC。规范套件和实测数据的存在说明发起方明白这个道理,但明白和赢是两回事。

我个人的判断偏谨慎乐观:它更可能先赢在"第三方 harness 的可移植性"上,而不是赢在大厂互认上。 只要有一批中小 harness 和产品愿意用 UHP 当统一入口——尤其是那些本来就要同时支持好几个 harness 的产品——它就能先成为一个真实可用的翻译层。至于能不能再往上长成 harness 生态的公共边界,取决于 HarnessRouter 之外的第二个、第三个独立实现来得有多快。命名文档里那句自我要求,其实也是它自己的考题:

A sentence here that no test enforces is a wish, not a standard.

一句没有被测试约束的话,是愿望,不是标准。

写在最后

如果你在做 Agent 产品、Agent 网关、或者任何一个要"同时接好几个 harness"的东西,UHP 值得花一个下午读一遍——不是因为它现在一定用得上,而是因为它把这一层该长什么样,用很具体的条款讲了一遍。哪些该是协议层的、哪些该留在实现里、哪些字段是越权原语、模型身份为什么必须可审计,这些问题它都给了明确的答案。

我这篇拆得还算细,但规范本身比我讲的更值得读。仓库在 github.com/HarnessRouter/harnessrouter,站点在 unifiedharnessprotocol.org。我最想看到的下一步,是 docs/IMPLEMENTATIONS.md 里那张实现列表,在接下来一两周里多出几个和大厂无关的名字。

如果你也在琢磨"控制平面该画在哪一层",之前写过两篇可以一起看:一篇是 《Dots vs Muse:永远在线 Agent 的架构之战》,讲的是 agent 底层运行架构的分歧;还有 《OpenCode vs Claude Code vs Cursor》,正好是 UHP 想抹平的那种"每种 harness 一套驱动方式"。工具与能力的供应链那一侧,可以接上 《Agent 供应链安全》 里对 MCP 生态威胁的梳理——UHP 的 plugin 章节,本质上就是在给这条链补上"包怎么安全落地"的那一环。

By AI博士 万戈