Agent Team 实践(一): 如何构建跨 Harness 的统一 Runtime
如今通用大语言模型越来越多,已经很难再说是哪一家“一家独大”。与此同时,围绕模型构建的通用 Agent Harness 也越来越多。
一个很自然的问题是:为什么不把所有模型都接入同一个 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 的才能发挥出模型的最大的能力。

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 | 进程内 SDK | AgentSession | 控制能力最完整 |
| Codex | app-server + JSON-RPC/stdin stdout | threadId | 标准协议式集成 |
| Claude Code | 直接启动 CLI + stream-json | sessionId | 自己解析事件流 |
| Qoder CLI | 官方 Agent SDK + ProcessTransport | sessionId | SDK 包装 CLI |
| Antigravity | 直接启动 agy CLI + stream-json | conversationId | 兼容和防御性逻辑最多 |
这里就要不得不提到 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 验证”三层一起证明出来的。
- Feature Implementation 证明“代码确实实现了这个能力”。比如 MCP 不是简单写 supportsMcp: true,而是要有真正的 runtimeFeature.session({ prepare() {} });像 cancellation 这类能力,如果声明启用,就必须实现 cancelTurn(),否则 defineRuntimeDriver() 注册时就会报错。也就是说先保证“代码形状完整”。
- Runtime Conformance 定义“什么叫行为正确”。例如声明支持 Streaming,不是最后能返回完整文本就算,而是事件必须满足条件。
- 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。

免费分享,随意打赏




发表评论