Agent Team 实践(一): 如何构建跨 Harness 的统一 Runtime

  • 2026-08-16
  • 39
  • 0

如今通用大语言模型越来越多,已经很难再说是哪一家“一家独大”。与此同时,围绕模型构建的通用 Agent Harness 也越来越多。

我自己订阅了 ChatGPT Pro、Gemini Pro,充值了 DeepSeek API,之前也订阅过 Claude;为了尽可能发挥不同模型的能力,本地也安装了多个 Harness,主力是 Codex,除此之外还有 Claude Code、PI、Antigravity。

一个很自然的问题是:为什么不把所有模型都接入同一个 Harness?

当然可以,而且很多 Agent 产品正是这么做的。但我越来越倾向于认为,只有最适配的 harness 才能发挥出模型的最大的能力。

模型提供核心的推理、生成和工具选择能力,而 Harness 决定模型以什么方式运行:Agent Loop 如何设计、有哪些工具、如何访问工作区、如何管理上下文、如何执行 Shell、如何请求权限、能不能启动 Sub Agent、如何恢复 Session……

两者共同决定了最终 Agent 的能力。未来也许会出现这种组合:垂类的 model + 垂类的 harness = 垂类 Agent。某个模型并不需要在所有 Harness 上都最强,只需要找到最适合它的执行环境。

为什么只做“多模型”远远不够

很多 AI 应用把 “模型” 视为最重要的运行时选择:给模型名称加一个下拉框,再把请求发给不同供应商,就完成了所谓的多模型架构。这对普通聊天应用或许足够,但对 Agent 系统远远不够。同一个模型运行在普通 API、Codex、Claude Code 或其他 Agent Harness 中,得到的不是同一种能力。模型只负责推理和生成,真正决定 Agent 能做什么的,是模型之外的整套执行环境:Agent Loop、工具系统、工作区访问、Session 管理、权限机制、上下文压缩、子 Agent 能力,以及取消和恢复协议。

Joel Niklaus 在 Hugging Face 上公开过一组很有意思的实验,固定两套模型:GLM-5.2 744B-A40B 和 Gemma 4 26B-A4B,然后让 10 个不同 Agent Harness 跑同一批 250 个 SWE-bench Pro 任务,每个任务一次 rollout,总共:10 harness × 2 models × 250 tasks = 5,000 rollouts。

结果是模型完全不变,只换 Harness,GLM-5.2 的 Pass@1 从 23% 直接变成 52%,不存在一个“普遍最好的 Harness”,两个模型之间 Harness 排名的相关系数只有:-0.05,基本等于毫无相关性。

最终结论就是:Harness 性能高度依赖具体模型,不能脱离模型谈“哪个 Harness 最强”。也就是回到上面说的,只有最适配 harness 的才能发挥出模型的最大的能力。

来源:https://x.com/joelniklaus/status/2085725862142623875

Agent Team 真正需要统一的是什么

如果要构建一个 Agent Team 系统,让每一个 teammate 都可以绑定不同的:Harness + Model

例如:

Product Expert      → Claude Code + Claude
Architecture Expert → Codex + GPT
Research Expert     → Gemini / Antigravity
Coding Expert       → PI + 某个 Coding Model

那么系统首先必须解决一个问题:

上层 Agent Team 如何统一调度这些完全不同的 Harness?

这就是本篇文章主要想讨论的内容。它可以是一个 Team,可以是 Expert 调用的 Sub Agent,也可以是固定流程编排的 Flow。它们之间还可以继续组合,形成更加复杂的 Agent 系统。这里统称为 Agent Team。Agent Team 不应该知道底下运行的是 Codex、Claude Code 还是 PI。所以中间需要一层统一 Runtime:统一的 Session 管理,统一的 Streaming Event 实现,统一的 Feature 协议(mcp, skills, etc.),定义 Runtime Driver SPI,每个不同的 harness 实现各自的 Driver。

我们核心要做的其实就是定义我们需要的 API 接口,统一上层运行语义,但允许每个 Harness 保留自己的原生实现方式和能力。

Driver、Adapter、Session 分别是什么

不同 Harness 的集成方式差别很大。

大体可以分成几类:进程内 SDK、常驻协议进程,例如 JSON-RPC / stdin / stdout、CLI 子进程 + stream-json / NDJSON、SDK 包装 CLI、远程 Runtime,有些 SDK 底层确实仍然在启动 CLI,但不能假设所有 SDK 都只是 CLI Wrapper。

因此 Core 不应该关心它到底怎么启动。

可以把 Runtime 拆成以下几个核心概念:

RuntimeDriver       = Harness 的具体实现
RuntimeAdapter      = Runtime 注册后的公开句柄
RuntimeAgentSession = Core 实际操作的逻辑 Session
NativeSession       = Harness 自己的原生 Session
RuntimeSubmitHandle = 一次 Run / Turn 的执行句柄

一句话概括:

Driver 是实现面,Adapter 是公开面,Session 是运行面,Run 是一次执行面。

Driver SPI 通过 defineRuntimeDriver()​ 定义。通过这个定义完成从原生实现到系统实现的转化,包括各种 Feature 的实现,session 的创建,流式事件的映射等等。

const adapter = defineRuntimeDriver({
  descriptor,
  features,
  createSession() {},
  startTurn() {},
  mapEvent() {},
  cancelTurn() {},
  steerTurn() {},
  closeSession() {},
})

其中内部定义就是 RuntimeDriver,也就是各个 harness 得各自实现的部分,返回值 adapter 即是这个运行时的句柄,后续使用这个句柄即可完成所有的 harness 调用。对于 core 层真正感知的只有 RuntimeAgentSession,并不关心底层是 Codex、Claude Code 或 PI, 甚至是云端的某个远程 harness。然后不同的 harness 返回的流式数据也在 driver 层被归一化层了统一变量。

interface RuntimeAgentSession {
  info()
  messages()
  contextWindow?: {
    inspect()
    canCompact()
    compact()
  }
  submit(...)
  steer(...)
  close()
}

这里有一个很容易被忽略的问题,就是不同 harness 对应的 session,Session 并不是一个 ID,一个跨 Harness Runtime 至少需要区分三层身份。

1. System Session

这是 Agent Team 系统自己的 Session:systemSessionId。它属于系统定义,而不是某个 Harness。

它负责连接:

Expert
Execution
Workspace
Persistence
Event Log
Runtime Session

即使底层 Runtime 将来发生变化,系统仍然需要自己的 Session Identity。

2. Native Runtime Session

这是 Harness 自己的 Session,例如:

PI          → AgentSession
Codex       → threadId
Claude Code → sessionId
Qoder CLI   → sessionId
Antigravity → conversationId

在系统中可以抽象成:

RuntimeSessionRef {
  type
  id
}

恢复 Session 时,Core 需要保证:systemSessionId <-> RuntimeSessionRef, 仍然能够正确对应。不能拿一个 Claude Code 的 sessionId​ 去恢复 Codex Runtime,也不能只保存原生 Session ID 而丢掉应用层 Session。

3. Run / Turn

一个 Session 内又可能执行很多轮任务。所以:

const run = session.submit(...)

应该返回一个独立的执行句柄:

interface RuntimeSubmitHandle {
  runId
  events
  result
  cancel()
}

于是 close() 关闭的是 Session。而 cancel() 取消的是某一次正在运行的 Run。Steering 也应该明确作用于某个 runId​。这样才能真正处理 Agent Team 中的并发、取消、恢复和父子任务关系。

不同 Harness 实际是怎么接入的

不同的 Harness Runtime 的实现方式并不相同:

Harness接入方式Native Session特点
PI进程内 SDKAgentSession控制能力最完整
Codexapp-server + JSON-RPC/stdin stdoutthreadId标准协议式集成
Claude Code直接启动 CLI + stream-jsonsessionId自己解析事件流
Qoder CLI官方 Agent SDK + ProcessTransportsessionIdSDK 包装 CLI
Antigravity直接启动 agy CLI + stream-jsonconversationId兼容和防御性逻辑最多

这里就要不得不提到 ACP(Agent Client Protocol) 协议,这是由 JetBrains 和 Zed 等联合推出的一项开源协议。它的定位类似于编程领域的 LSP(语言服务器协议),专门为 AI 编码智能体设计。基于 JSON-RPC 2.0 规范,方便宿主环境调用多个不同的 Agent。这里只需要实现一套 ACP runtime driver 就可以快速对接支持 ACP 协议的所有 harness,目前有些 harness 虽然已经宣称支持 ACP 协议,但是相比较 cli 或者 SDK 很多工具实现不完整,所以还是使用自己对接的方式。例如 cursor-agent acp ,grok agent stdio 对 ACP 协议支持的比较好就可以快速接入试使用。ACP 可以看成:一种标准化程度很高的 Runtime Driver Protocol。

未来还可以实现 RemoteRuntimeDriver,把真正的 Harness 放到远程 Sandbox、容器或者 Worker 中运行。对 Core 来说,Local Runtime 和 Remote Runtime 最终仍然只是不同的 Driver。

如何适配不同的 Harness Feature

跨 Harness 还有另一个很麻烦的问题:不同 Harness 支持的能力完全不一样。

例如: mcp、skills、thinking、steering、Image Attachment、Permissions、Resume,所以需要定义一个 Runtime Feature Framework,定义出所有需要的特性,driver 去实现这些特性或者标注为 unsupported。每个 Harness 都必须明确回答:

supported
degraded(reason)
unsupported(reason)
notApplicable(reason)

这里 degraded​ 非常重要。现实世界里的 Harness 很少只有:支持、不正常很多时候其实是:

可以工作,但只支持某种模式
可以工作,但不能 steering
可以工作,但 context window 无法得到精确 denominator
可以工作,但图片只能退化成本地文件引用

Feature 不只是 metadata,而可以包含真正的实现,对于需要准备资源的 Feature,它本身就是一段实现。比如:

const mcp = runtimeFeature.session({ 
	async prepare(ctx) { ... } 
}); 
const permissions = runtimeFeature.session({ 
	needs: { mcp }, 
	async prepare(ctx, { mcp }) { ... } 
}); 
const skills = runtimeFeature.session({ 
	needs: { mcp, permissions }, 
	async prepare(ctx, { mcp, permissions }) { ... }
});

于是 Driver 不需要自己手写:先启动 MCP、再启动 permission relay、然后物化 Skill、最后创建 Session。Feature 之间直接声明 dependency。Core 根据依赖关系构建 Preparation Graph。

Core 会构建 preparation dependency graph,管理 Feature 的初始化顺序和资源释放。并且这些 Feature 获取的 socket、relay、MCP registry 等资源,都进入 RuntimeResourceScope​,Session 结束时统一回收。

Core 负责执行图,但不硬编码具体 Harness 的准备顺序。

Session Scope 和 Turn Scope

Feature 还应该有生命周期。有些资源属于整个 Session:MCP Server、Permission Relay、Managed HOME、Skill Directory、Native Process

有些只属于某一次 Turn:临时 Attachment、Turn-specific Model Selection、某次运行的临时资源

所以 Runtime Feature 可以区分:driver、session、turn

Session Feature 在 Session 创建时准备,在 Session 关闭时释放。Turn Feature 则在每次 submit()​ 时准备,Run 结束后释放。

所有动态创建的资源,例如:

socket
relay
MCP registry lease
temporary directory
listener
subprocess

都进入 RuntimeResourceScope​。

这解决了一个非常实际的问题:初始化到一半失败怎么办?

比如:1. MCP 创建成功,2. Permission Relay 创建成功,3. Skill 物化失败

如果资源完全由各 Driver 自己管理,很容易出现泄漏。有统一的 Resource Scope 后,无论是:正常结束、初始化失败、取消、异常、Session close

都可以沿着同一条生命周期路径释放资源。

具体需要适配哪些内容

实现一个 Driver 最难的部分,其实并不是:spawn(“claude”)​,真正难适配的六类 Harness 差异主要包括:Session、Streaming Event、Tools / MCP、Permissions、 Context / Compaction、Process Environment。当然实际 Feature 远不止六个,还包括:Model Discovery、 Model Selection、 Thinking、 Attachments、 Usage、 Cancellation、 Steering … 但上面六类通常是最容易出现 Harness-specific 逻辑的地方。

Streaming Event 是其中最典型的例子,以 Event 为例,不同 Harness 返回的东西完全不同:

PI       → AgentEvent
Codex    → app-server notification
Claude   → stream-json
Qoder    → SDKMessage
Agy      → stream-json / NDJSON

所以每个 Driver 都需要:mapEvent(nativeEvent)​ 映射进 Core。但这里真正要统一的并不只是事件名字,如果 Core 只定义:

message.delta
thought.delta
tool.started
tool.completed
usage
progress

还远远不够,真正需要统一的是:事件的生命周期、顺序和因果关系。Runtime Event 大致包括:

run.started、run.completed、run.failed、run.cancelled

message.delta、message.completed

thought.delta

tool.started、tool.delta、tool.approval_requested、tool.completed、tool.failed

usage.updated
context-window.updated

progress
artifact.created
agent.command

同时每个事件还应该携带类似:eventId、sequence、runId、parentRunId、source、sessionId、parentSessionId、path 这样的信息。

为什么需要这么复杂?因为单 Agent Chat 只需要知道:“模型输出了什么?”

而 Agent Team 需要知道:是谁产生的?属于哪一个 Run?是谁启动了它?属于哪个 Session?哪个 Tool Call?顺序是什么?是不是已经进入 Terminal State?

所以 Event Normalization 本质上并不是:JSON 字段格式转换,而是:把不同 Harness 的原生生命周期转换成系统统一的执行语义。

Supported 不是“代码写了”

Feature Framework 还有一个我认为非常重要的设计:声明支持某项能力必须有证据。

例如一个 Runtime Adapter 写了:supportsMcp = true​它到底意味着什么?

可能只是:配置文件写进去了。但 Harness 根本没发现这个 MCP Server。

也可能 Harness 发现了:MCP Server,但模型从来没有成功调用过。

例如 MCP:

Materialized
→ MCP 配置已经生成

Discovered
→ Harness 确实发现了这个 MCP Server

Executed
→ 真正完成过一次无副作用 Tool Call

只有最后一种,才能比较有把握地说:supported

同样,Streaming 不能因为最后拿到了完整答案,就声称:supportsStreaming

至少应该真实观察到:delta -> delta -> message.completed -> run.completed

​supported​ 不是 Driver 自己声明出来的,而是“实现 + 契约验证 + 真实 Harness 验证”三层一起证明出来的。

  1. Feature Implementation 证明“代码确实实现了这个能力”。比如 MCP 不是简单写 supportsMcp: true​,而是要有真正的 runtimeFeature.session({ prepare() {} })​;像 cancellation 这类能力,如果声明启用,就必须实现 cancelTurn()​,否则 defineRuntimeDriver()​ 注册时就会报错。也就是说先保证“代码形状完整”。
  2. Runtime Conformance 定义“什么叫行为正确”。例如声明支持 Streaming,不是最后能返回完整文本就算,而是事件必须满足条件。
  3. Runtime Probe 用真实 Harness 跑这些能力, Probe Evidence 把这次真实验证结果保存下来

​supported​ 的真实含义应该是:这个 Feature 不仅在代码里实现了,而且符合统一 Runtime 行为规范,并且已经在真实 Harness 上实际执行验证通过。

​degraded(EVIDENCE_PENDING)​——不是没实现,而是实现已经存在,但还没有足够的真实 Probe 证据把它提升成 supported​。

写在最后

一个好的 Runtime 层,不应该消灭 Harness 的差异,如何构建跨 Harness 的统一 Runtime,这里统一的是语义,而不是统一实现,允许运行时存在不同的 Harness 差异。

这是一个系列文章,大概六篇,讨论 Pragma 在构建多 Agent 系统过程中遇到的六个核心问题。它不从 Prompt 技巧出发,而是从一个长期运行的 Agent 系统需要具备的工程能力出发:执行环境如何替换,上下文如何组织,多个专家如何协作,经验如何积累,复杂任务如何组合,以及整套工作方式如何成为可以版本化和分享的资产。

开源地址:https://github.com/pqpo/pragma (欢迎下载体验、 star、fork 和提交 PR)

Pragma 介绍

Pragma 是一个开源的多 Agent 桌面应用,用来把不同的 Agent Harness、模型、工具、上下文来源和人工决策组织成可复用的 AI 系统。一项 Mission 可以在不同专家之间持续推进,同时保留已经形成的决策、产物和经验。

Pragma 不替代 Claude Code、Codex、PI、Qoder CLI、Antigravity CLI 或下一个优秀的 Agent,而是让它们协同工作。

用得越多,沉淀得越多。不同任务、Harness 与模型产生的事件会形成跨 Harness 的动态记忆;其中有价值的经验与事实,可以在策略和审阅机制控制下自动升级为稳定的知识库或可复用 Skill。

欢迎下载体验、star 、fork 和 pr。

>> 转载请注明来源:Agent Team 实践(一): 如何构建跨 Harness 的统一 Runtime

免费分享,随意打赏

感谢打赏!
微信
支付宝

评论

还没有任何评论,你来说两句吧

发表评论