异构 Agent 集成:CLI 工具的平台化封装
异构 Agent 集成:CLI 工具的平台化封装
1. 问题定义
市场上已有成熟的 CLI Agent 工具(Claude Code、Codex、OpenCode 等),它们各自有独特的能力和优势。一个 Agent 平台如果只支持自研 Agent,就无法利用这些外部工具的能力。异构 Agent 集成的目标是:将不同来源、不同运行方式的 Agent 统一封装为平台可控的 Agent,使用统一的事件模型进行交互和管理。
2. 两种运行模式
本地 CLI 模式
CLI Agent 作为 Desktop 子进程运行,通过 Electron IPC 与主进程通信。典型代表:
| Agent | 命令 | 运行方式 |
|---|---|---|
| Claude Code | claude | Node.js 子进程 |
| Codex | codex | Node.js 子进程 |
远程平台模式
远程 Agent 通过通知机制(如 agentNotify.notify)回传事件,不运行在本地。典型代表:
| Agent | 类型 | 通信方式 |
|---|---|---|
| OpenClaw | remote | agentNotify |
| Hermes | remote | agentNotify |
| Amp | remote | agentNotify |
| OpenCode | remote | agentNotify |
3. 注册表模式扩展
异构 Agent 通过注册表模式管理,新增 Agent 只需注册一个 entry,无需修改其他代码:
// 伪代码:注册表const registry: Record<string, AgentRegistryEntry> = { "claude-code": { createAdapter: () => new ClaudeCodeAdapter() }, "codex": { createAdapter: () => new CodexAdapter() }, // 新增 Agent 只需添加一行 // "kimi-cli": { createAdapter: () => new KimiCLIAdapter() },};优势:每个 Agent 的适配器完全独立,互不影响。
4. CLI 子进程事件流
本地 CLI Agent 的核心挑战是解析其终端输出并转换为结构化事件流:
事件类型
| 事件类型 | 说明 | 数据 |
|---|---|---|
stream_start | Agent 开始输出 | 无 |
stream_chunk | 流式文本块 | text: string |
tool_call | 工具调用开始 | name, arguments |
tool_result | 工具调用结果 | name, result |
subagent_spawn | 子 Agent 生成 | name, type |
agent_end | Agent 完成 | exitCode |
terminal_error | 终端错误 | error |
输出解析策略
CLI 工具的输出格式各异:
- JSON 行模式:每行一个 JSON 对象(如 Codex 的
--output-format json) - ANSI 文本模式:解析终端 ANSI 转义序列和文本模式
- 混合模式:部分结构化 + 部分纯文本
陷阱:CLI 工具更新可能改变输出格式,导致解析器失效。对策:
- 使用版本化的解析器(如
claude-code-v1、claude-code-v2) - 在解析器中设置 fallback 逻辑
- 通过 E2E 测试验证解析器对 CLI 新版本的兼容性
5. AskUserQuestion 桥接
CLI Agent 在执行过程中可能需要用户交互(如选择方案、确认操作)。在桌面应用中,需要将 CLI 的交互式提示桥接为 UI 弹窗:
陷阱:某些 CLI Agent(如 Claude Code)在无人交互模式下会 decline AskUserQuestion。需要特殊处理:在 Desktop 环境中将 AskUserQuestion 桥接为外部工具调用而非内部交互。
6. 事件适配器模式
每个 CLI Agent 实现一个 AgentEventAdapter,负责双向转换:
// 伪代码:适配器接口interface AgentEventAdapter { // CLI 输出 → 平台事件 parseOutput(line: string): HeterogeneousAgentEvent[];
// 平台指令 → CLI 输入 formatInput(event: PlatformEvent): string;
// 获取 Agent 显示名称 getDisplayName(): string;}优势:
- 新增 Agent 只需实现适配器,不修改核心逻辑
- 每个适配器独立测试
- CLI Agent 与平台 Agent 共享同一事件处理代码路径
7. 文件变更追踪
对于编码类 CLI Agent(如 Codex),需要追踪其对文件系统的修改:
// 伪代码:文件变更追踪class CodexFileChangeTracker { private baseline: Map<string, FileHash>;
// 启动时记录文件快照 recordBaseline(dir: string): Promise<void>;
// 周期性检查变更 getChangedFiles(): FileChange[];}用途:
- 在 Agent 任务完成后展示代码 diff
- 检测意外修改(如删除重要文件)
- 支持撤销 Agent 的更改
8. 陷阱与对策
| 陷阱 | 后果 | 对策 |
|---|---|---|
| CLI 输出缓冲导致事件延迟 | UI 显示延迟 | 使用 --streaming 标志或禁用输出缓冲 |
| 子进程异常退出 | 任务静默失败 | 监控 exitCode 和 stderr,发送错误事件 |
| CLI 版本不兼容 | 解析器失效 | 版本化解析器 + E2E 测试 |
Windows .cmd shim | 子进程启动失败 | 检测平台并使用 npm.cmd 而非 npm |
| 长时间运行无输出 | 超时误判 | 使用心跳检测而非固定超时 |
9. 服务端 Adapter 合约模式(Paperclip 贡献)
Paperclip 在异构 Agent 集成的基础上,提出了更正式的服务端 Adapter 合约,将 Adapter 的核心能力分离为三个独立维度:
9.1 三分离架构
| 维度 | 职责 | 接口 |
|---|---|---|
| 执行(Execute) | 启动/恢复 Agent 会话,返回执行结果 | execute(agent, config, context) → ExecutionResult |
| 会话编解码(Session Codec) | 序列化/反序列化 Agent 会话状态 | serialize(session) / restore(serialized) |
| UI 解析器(UI Parser) | 解析 Agent 输出为 UI 可渲染格式 | ./ui-parser export from adapter package |
这种三分离使得:
- 执行引擎只关心如何启动和监控 Agent
- 会话状态可以跨心跳恢复(Agent 不丢失上下文)
- UI 渲染独立于执行逻辑,不同 Adapter 的输出可以统一展示
9.2 外部 Adapter Plugin 加载
Paperclip 的 Adapter 以独立 npm 包形式存在,通过动态 ESM import 加载:
// 伪代码:外部 Adapter 加载async function loadExternalAdapter(packageName: string) { const modulePath = resolvePackageEntry(packageName); const mod = await import(modulePath); const adapterModule = mod.createServerAdapter();
// 验证最小合同 if (!adapterModule.type) { throw new Error("Adapter module missing 'type' field"); } return adapterModule;}关键设计:
- Server 核心零硬编码 Adapter 导入(plugin loader 中无
import claude等) - ESM cache busting 支持热重载(开发时不需要重启 server)
- UI Parser 通过 package.json
exports["./ui-parser"]声明,server 提取源码并在运行时注入前端
9.3 模型配置文件(Model Profiles)
每个 Adapter 声明自己的模型列表和配置文件:
// 伪代码:Adapter 模型声明const models = [ { id: "claude-sonnet-4-6", provider: "anthropic", contextWindow: 200000 }, { id: "claude-opus-4-7", provider: "anthropic", contextWindow: 200000 },];
const modelProfiles = { "fast": { model: "claude-sonnet-4-6", maxTokens: 4096 }, "smart": { model: "claude-opus-4-7", maxTokens: 8192 }, "default": { model: "claude-sonnet-4-6", maxTokens: 4096 },};这使得同一 Agent 可以在不同任务中使用不同模型,通过 profile key 切换。
9.4 环境检测
每个 Adapter 提供 testEnvironment() 方法,在安装时检测运行环境是否满足:
| 检测项 | 说明 |
|---|---|
| CLI 是否安装 | 如 claude 命令是否在 PATH 中 |
| API Key 是否配置 | 环境变量或配置文件中是否存在 Key |
| 版本是否兼容 | CLI 版本是否满足最低要求 |
10. 参考来源
- LobeHub
HeterogeneousAgents—packages/heterogeneous-agents/src/完整实现 - OpenAI Codex CLI — CLI Agent 的输出格式和事件模型
- Anthropic Claude Code — 工具调用和 AskUserQuestion 机制
- Paperclip Adapter Registry —
server/src/adapters/registry.ts(27KB) - Paperclip Adapter Plugin Loader —
server/src/adapters/plugin-loader.ts - Paperclip Adapter Utils —
packages/adapter-utils/src/types.ts