看到“多 Agent 引擎可替换架构实现”这道赛题时,我想到的是自己已经遇到过的一个问题:底层 Agent 迭代得这么快,我正在做的应用应该怎样接住这些变化,而不是每换一个引擎就重写一遍?
此前我用 Open Design 给 AI DevFlow Studio 做过界面设计。它把设计方法、设计系统和产物组织放在上层,让已有的编码 Agent 去执行。这种分工给了我一个判断:应用应该长期打磨自己的工作方法,同时通过一份清楚的契约接入外部引擎。Open Design Agent Gateway 就是我把这个判断放到比赛里的一次实践。
这个判断后来经受了一次真实的检验:比赛后期,项目退役了一个引擎、又接入了五个,引擎契约的方法一个都没有改。这篇文章从赛题讲起,一直讲到这次检验。
从赛题出发
我们参加的是华为内部的质量与流程 IT 首届 AI 争霸赛,选择“多 Agent 引擎可替换架构实现”这道题,最终获得二等奖,赛题排名 2/46,总榜排名 5/307。我担任项目主程,负责方案、交互设计、开发、测试和交付;组员协助多台 Windows 设备的测试、方案评估和参考项目讨论。
赛题的要求很具体:应用对外实现统一的网关接口,底层至少接入两种不同的 Harness,支持 Windows,并能通过配置选择引擎。评测任务面向办公场景,包括文档修改、数据分析、演示文稿、文件操作、信息检索,以及通过 WeLink 发送消息。项目总纲与赛题约束(在新标签页打开来源)
这里要先分清两个概念。Harness 是承载 Agent 执行过程的软件:它组织模型调用、工具使用、上下文和任务运行,比如 OpenCode、Pi。模型 Provider 只提供推理服务。开发验证中各个 Harness 都调用 DeepSeek 模型,这并不等于多接了一个“DeepSeek 引擎”。项目里确实另有一个实验性的 DSH(DeepSeek Harness),但它没有进入这一阶段的三引擎基线。
我把这道题理解为两种演进速度之间的协调:业务在不断增加自己的流程和交付要求,Agent 生态在不断改进执行能力。目标是让业务接口、任务工具和验收方式保持稳定,同时让底层引擎可以替换。
比赛之前,我已经用过 Open Design
6 月,我把 DevFlow 的产品背景交给 Open Design:一个本地 Electron 工作台,要承接需求澄清、方案设计、编码、测试、PR 交付与验收,界面还要表达 Run、Gate、Artifact 和 Trace 之间的关系。
那段原型迭代的重点不是换配色。我反复追问工作台、Workflow Board 和 Inspector 应该怎样分工,Team Project 和 Local Project 会不会被混淆,Gate、知识、产物和执行记录之间的关系有没有真正画出来。这些问题决定了使用者能不能一眼看出“现在在哪里、为什么不能继续、下一步是什么”。
这段经历留下了可检查的产物。DevFlow 仓库在 6 月 23 日加入了原型参考,两天后归档了完整设计包。设计说明保留了 Local Project + Runs、Workflow Board、Inspector 三个区域,并写明它是视觉与交互参考,不是要直接嵌入的静态 HTML;移植到 React/Electron 时必须保留业务概念和交互关系,尤其要让 Team Project 与 Local Project 在视觉上分开。设计参考与移植规则(在新标签页打开来源) · 原型参考提交(在新标签页打开来源)
对我最有启发的,是设计约束可以和执行它的 Agent 分开。Open Design 负责工作台、指导和产物组织,底层 Agent 负责读项目、改文件、返回结果。上游的 Agent Adapters 文档写得很清楚:Agent 循环委托给外部 CLI,每种 Runtime 只是一份声明式定义,描述可执行文件、参数、能力和输出格式,本身不带 run() 或 cancel()。上游 Agent Adapters(在新标签页打开来源) OpenDesign 在 5 月的官方文章(在新标签页打开来源)里也讲过这种用 skill 和适配层复用现有 Agent 的思路。
后来做 Gateway 时,我在这里做了一个关键的改变:Gateway 的引擎契约不能只是声明式的数据。比赛接口要把 Agent 的反问和权限请求转给客户端,还要为每一轮任务给出唯一的终态,所以驱动必须是“可以调用的对象”。这一点在后面的 EngineDriver 里会看到。
方法留在应用层
这和我做 DevFlow 时关心的事情一致:真正值得长期积累的,是一套从需求走到交付的方法,包括每个阶段产出什么、怎样检查、由谁决定继续。执行引擎通过契约参与这套方法,而不是反过来。
DevFlow 的六个阶段是 Clarify → Design → Build → Test → PR → Accept,展开是 8 个节点:需求确认 Gate 要求团队成员批准,方案评审 Gate 和最终验收要求负责人批准。工作流模板源码(在新标签页打开来源) 2026 年 8 月 12 日通过的 ADR 0015 进一步规定:OpenCode 和 DevFlow 自己的受限 Native 执行器实现同一套 Coding Executor 契约,共用能力声明、事件、取消和结果结构;执行器不能批准 Gate,也不能发布;取消或终态之后到达的事件不能再提交。ADR 0015(在新标签页打开来源)
后来我读到 Anthropic 于 2026 年 8 月 21 日发布的《The AI-Native SDLC playbook》,里面有相近的关注点:代码生成加速之后,要重新组织编码前后的流程,让阶段产物接续,并保留人的判断。它的六个阶段是 Plan、Design、Build、Test、Deploy、Maintain,和 DevFlow 并不相同:DevFlow 单独分出了 PR 和业务验收,Anthropic 则覆盖部署与维护。Anthropic 原文(在新标签页打开来源) DevFlow 六月的原型早于这篇文章,所以这只是事后看到的呼应,不是设计来源。
带着这个思路进入比赛,我给 Gateway 的分工是:网关负责稳定的业务契约和运行边界,Harness 继续发展自己的执行能力,共享的任务工具承担可以确定性处理的文件和计算工作。
一份契约,三种协议
比赛实现固定在 Open Design v0.21.1 上,通过保留历史的 Git subtree 导入。这样上游原有的内容和项目后来的改造可以分开追溯。上游导入记录(在新标签页打开来源)
下面这张图按实际调用关系重画。上方是比赛请求的主链,下方分别是直接复用的基础能力和仓库里保留、但不在主链上的产品代码。
启动入口读取 --engine,没有时再读 AGENT_ENGINE;两者都没有就直接报错,没有默认引擎。它据此创建一个 Driver,再启动比赛用的 HTTP/SSE 服务。一个 Gateway 进程从启动到退出只绑定一个引擎,切换引擎需要重启;它不会在任务中途自动挑选引擎,也不会把会话迁移到另一种 Harness。启动与装配源码(在新标签页打开来源)
HTTP 层之后,GatewaySessionService 管理会话、消息、交互和忙闲状态;原生协议的差异全部收在三个 Driver 里。它们共同实现的契约是这样的:EngineDriver 定义(在新标签页打开来源)
export interface EngineDriver {
readonly capabilities: EngineCapabilities;
start(): Promise<void>;
createSession(input: EngineSessionInput): Promise<EngineHandle>;
runTurn(handle: EngineHandle, input: EngineTurnInput,
sink: EngineEventSink): Promise<EngineTerminal>;
replyInteraction(handle: EngineHandle, reply: InteractionReply): Promise<void>;
abort(handle: EngineHandle): Promise<void>;
closeSession(handle: EngineHandle): Promise<void>;
dispose(): Promise<void>;
}
每一轮任务只会以四种终态之一结束:completed、aborted、failed 或 timed_out。引擎的原生事件被翻译成统一的六类事件:助手文本、工具调用、工具结果、步骤结束、反问和权限请求。
| 引擎 | 实际接入方式 | 保留下来的差异 |
|---|---|---|
| OpenCode 1.18.18 | 每个 Driver 启动一个本机 opencode serve,通过 SDK 调用并消费事件流 |
一个服务承载多个会话;反问和权限都能转发 |
| Pi 0.84.4 | 每个会话启动一个 pi --mode rpc 子进程,通过 stdio 上的 JSONL 通信 |
项目自带的扩展注册 ask_user 工具,并在只读工具之外的调用前请求一次、始终或拒绝 |
| Hermes 0.21.0 | 每个会话启动一个 ACP 进程,依次 initialize、session/new、session/set_model |
权限可以转发;当前 Adapter 不支持反问 |
这些差异直接写在各自的启动参数、初始化流程和 capabilities 里。OpenCode Driver(在新标签页打开来源) · Pi Driver(在新标签页打开来源) · Pi 扩展(在新标签页打开来源) · Hermes Driver(在新标签页打开来源)
执行期间,真正调用模型、选择工具的仍然是 Harness。项目把同一套 Office/WeLink Skill 和一个共享 CLI 显式交给每个 Driver:Skill 说明处理顺序和约束,CLI 负责读文件、改文件、做统计。但“同一套”落到三个引擎上是三种形式:OpenCode 在配置里接收 Skill 目录,Pi 用 --skill 接收文件,Hermes 则把 Skill 目录复制到自己的 home 下,并在提示词前加上对应的斜杠命令。共享工具装配(在新标签页打开来源) 通用工具修好一次,三个引擎都能用上。
一次任务的完整回合
一个容易误读的接口是 prompt_async。名字听起来像“收到请求就返回”,实际上路由会等会话服务把这一轮跑完,才返回 HTTP 204:HTTP 实现(在新标签页打开来源)
if (method === "POST" && prompt?.[1]) {
const body = await readJson(request);
await this.service.prompt(decodePathId(prompt[1]), validatePrompt(body), directory);
writeNoContent(response);
return;
}
执行中的文本、工具调用和交互请求通过 SSE 实时推送。正常完成和取消都返回 204,但取消不会被伪装成一条正常的助手回复;失败和超时返回 502,同时推送一条 session.error。每轮默认有 30 分钟的时限,超时后会同时中止底层引擎。会话服务还给每一轮发放一个令牌,已经结束的那一轮不会再被迟到的事件改写。会话服务(在新标签页打开来源)
所以 204 只说明这一轮处理结束了。客户端还要检查最终消息、终态和实际文件。项目自己的评测器就是这样做的:它从公开的 HTTP/SSE 接口进入,等到 session.idle 之后再读取消息并检查产物。把这些行为收敛在会话层,是接入第二、第三种引擎之后,客户端看到的行为仍然一致的关键。
接口之外的两次故障
Git 历史能还原主要的实施顺序:9 月 3 日导入上游并完成第一个多引擎 Gateway,当时接入的是 OpenCode 和 Pi;9 月 4 日到 5 日补上 V1.3 评测流水线、只读的 Judge,并接入 Hermes ACP;之后持续修复工具、上下文隔离、Windows 兼容和交付依赖。首个多引擎实现(在新标签页打开来源) · Hermes 接入(在新标签页打开来源) 真正让我改变认识的,是其中两次故障。
第一次:参数传了,Skill 却没有加载。 共享接口传给引擎的是 SKILL.md 的文件路径,而 OpenCode 的配置要求的是 Skill 所在的目录。用真实的 opencode debug skill --pure 检查,给文件路径时共享 Skill 不在列表里,给目录时才出现。V1.5 的第一批全量评测在确认这一点后被停止,已完成的记录保留、标记为不计入比较。修复(在新标签页打开来源)把转换放进了 OpenCode Driver:
// Our shared interface names SKILL.md files; OpenCode scans directories instead.
skills: { paths: [...new Set(skillPaths.map(skill =>
path.resolve(path.basename(skill) === "SKILL.md" ? path.dirname(skill) : skill)))] }
同时补上真实的 Skill 发现和模型加载检查。这正是 Adapter 存在的意义:契约统一了“要给引擎什么”,每个 Driver 负责把它翻译成引擎真正能理解的形式。
第二次:Pi 读到了旧答案。 第二批全量评测运行中,从公开的会话消息接口可以看到,Pi 在处理一道演示文稿任务时读取了旧批次的产物记录、复查汇总、旧的 Judge 输出和旧的 PPT。原因是 Pi 会自动加载上层目录里的 AGENTS/CLAUDE 上下文文件,而评测工作区当时就放在仓库内部。那一批从未进入评分。
修复分三处:Pi Driver 加上 --no-context-files,所以它对所有 Gateway 中的 Pi 会话都生效,而不只是评测;每次执行改在仓库之外的独立临时目录进行;执行结束后把工作区复制归档,并比对前后的文件哈希清单。执行目录与归档(在新标签页打开来源) 隔离后重跑同一道题,Pi 调用了 38 次工具,没有读取任何历史材料。这里控制的是评测上下文和产物来源,并不是操作系统级的沙箱。V1.5 记录(在新标签页打开来源)
处理这次问题时,项目没有用加大输出预算的办法去掩盖同批出现的 length 错误,而是先解决隔离。第三批冻结后,三个引擎共完成 27 次真实 DeepSeek 执行,评测细节写在另一篇笔记里。后续范围扩展到包含 WeLink 在内的 30 项任务,我在公司的 Windows 环境上确认跑通并写入了现场记录(在新标签页打开来源);对应的脱敏运行目录和最终包的独立复验还没有补齐。
这两件事让我看清:引擎可替换只是第一步。同一个输入交给不同的 Harness 之后,还要确认它们拿到了正确的指导、只用了允许的上下文、留下了真实的产物。
复用、保留与没有做的
打开 Open Design 仓库,很容易被它的功能数量吸引。解释这个项目时,我更愿意沿调用关系说明每一部分的归属:
| 范围 | 在 Gateway 中的实际关系 |
|---|---|
| 跨平台命令、进程树处理 | 直接复用上游 platform 包,RuntimeHost 调用它的命令封装、进程枚举和停止逻辑 |
| 引擎发现、可执行文件解析 | 部分复用:先用比赛固定的安装位置或显式配置,再用上游的注册表和解析逻辑兜底 |
| HTTP/SSE、会话服务、EngineDriver | 本项目新增,比赛协议和引擎适配放在独立的包里 |
| Office/WeLink 工具、开发评测 | 本项目新增,三个引擎共用任务工具,评测单独观察和检查结果 |
| Web/Electron、产品 daemon、SQLite、插件与 sidecar | 保留的上游产品能力,不在比赛请求的链路上 |
这些关系可以从 RuntimeHost(在新标签页打开来源) 的实际引用和 CLI 的装配顺序交叉核对;上游的 platform、runtime 和插件目录与导入基线的 Git 对象完全一致,不能算作本项目的原创。架构归属说明(在新标签页打开来源)
这些取舍也留下了清楚的边界:比赛会话只保存在内存里,没有接入产品 daemon 的数据库;没有进程内热切换、跨引擎会话同步或自动故障转移;WeLink 的发送记录和回执另行持久化,但这不代表整个会话已经持久化。评测器遇到反问时自动选第一个选项、遇到权限请求时自动批准一次,所以批量评测只证明交互链路能走通,不代表这些决定经过了人的判断。比赛服务默认只监听本机,没有 HTTP 认证和 TLS;而权限回复接口实际上能批准工具执行,如果要变成共享服务,认证、TLS 和网络访问控制都必须补上并单独验证。当前实现与边界(在新标签页打开来源)
后来:退役一个引擎,又接入五个
9 月中旬之后,项目转入决赛准备,这份契约迎来了一次真实的检验。
9 月 17 日,Hermes 被退役,它的 Adapter、补丁和 Python 运行环境一并删除,ACP 传输层移到了中立的目录;记录里写下的直接好处是,一台新的 Windows 机器不再需要安装 Python 3.12 和专门的补丁。同一天接入了 Qwen Code 和 Kimi Code,之后又陆续接入 Cline、DSH、Kilo 和 MiMo,基线变成了八个引擎。退役与迁移提交(在新标签页打开来源) · 八引擎清单(在新标签页打开来源)
它们的原生协议各不相同:OpenCode、Kilo、MiMo 走 HTTP 加 SDK,Qwen、Kimi、Cline、DSH 走 stdio 上的 ACP,Pi 仍然是 JSONL RPC。但从 9 月 9 日的固定提交到 9 月 20 日的主干,EngineDriver 的方法一个都没有变。对比两个版本的 types.ts(在新标签页打开来源),唯一的改动在运行时宿主上:解析可执行文件时接受的引擎名称,从四个写死的值换成了八个引擎的联合类型。
并发也是在这份契约之上加的:最多四个不同的 Harness 同时运行,每个 Harness 同一时间只处理一个任务,Judge 最多两路并发,WeLink 保持单通道;一个 run 仍然只绑定一个引擎。并发实现(在新标签页打开来源) 这些是代码层面的事实,不能说明各个引擎的结果质量。决赛的 32 项组合在记录里明确标注为“计划 ≠ 通过”,仓库中也没有任何决赛成绩。
回头看,从 Open Design 到 DevFlow,再到这次比赛,我愿意长期维护的是这几件事:任务怎样描述,流程怎样推进,交互怎样表达,结果依据什么被接受。Gateway 把这些问题变成了一份可以检查的接口。换一种 Harness,仍然要安装、配置、声明能力、映射协议、用真实任务验证,适配成本并没有消失;但这些工作有了固定的落点,应用本身的方法和交付质量,也就不必跟着每一次引擎更替重新来过。
技术说明对应 Gateway 比赛阶段的固定提交 4296525bd、Open Design v0.21.1,以及文中列出的 DevFlow 固定版本文件;最后一章对应 9 月 20 日的主干版本。两张图根据比赛阶段的源码绘制。历史运行结果来自当时保存的报告和记录,本文写作时没有重新运行模型任务。比赛名称、奖项、排名、分工和个人动机依据我的参赛记录;六月的使用经历结合了本地设计对话和仓库里的原型归档。本文不公开内部聊天、企业地址、测试原始文件或凭据。
延伸阅读:
- Open Design 的固定版本架构说明(在新标签页打开来源):产品工作台、daemon 与 Runtime 原有的分工。
- Gateway 架构与亮点说明(在新标签页打开来源):更完整的复用范围、交互和交付边界。
- DevFlow 的 Coding Executor 决策(在新标签页打开来源):上层工作流怎样约束原生与外部执行器。
- The AI-Native SDLC playbook(在新标签页打开来源):软件生命周期各阶段的方法与产物衔接。