多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

HagiCode 是怎么把 13 个 Agent CLI 接到一套系统里的:从 AIProviderType 到 AIProviderFactory 的接入拆解

HagiCode 是怎么把 13 个 Agent CLI 接到一套系统里的:从 AIProviderType 到 AIProviderFactory 的接入拆解 1. 为什么 13 个 Agent CLI 需要一套统一接入层如果你最近在折腾 AI 编程工具大概率会有一种追不上的感觉Claude Code 刚用顺手Codex CLI 又更新了Copilot CLI 还没摸透Gemini CLI 和 Kimi CLI 又冒出来了。每个 CLI 都有自己的安装方式、自己的参数风格、自己的流式输出格式。作为一个想让用户装一个平台、用全套 Agent的项目HagiCode 面对的第一个问题就是怎么把这些脾气各异的 CLI 塞进同一套系统里而不是为每个 CLI 写一套从安装、健康检查到调度的完整逻辑。答案藏在三个核心抽象里AIProviderType身份枚举、IAIProvider业务契约、AIProviderFactory工厂路由。这三个东西构成了 HagiCode 多 Agent CLI 统一接入架构的骨架。本文会从架构视角拆解这套设计给出可复制的 Provider 注册配置片段并带你走一遍新增一个 Agent CLI的完整验证步骤。如果你正在做类似的多 Provider 整合系统这套分层思路可以直接搬到你的工程里。先说清楚13 个这个数字是怎么来的。它不是一个营销数字而是代码里真真切切数出来的。在AIProviderType枚举里一共定义了 14 个值但IFlowCli 5这条路已经走不通了——在AIProviderFactory里它被显式挡在门外配合IsActivelySupportedProviderType()做一次过滤真正在系统里活着的就是 13 个Claude Code、Codex、GitHub Copilot、CodeBuddy、OpenCode、Hermes、Qoder、Kiro、Kimi、Gemini、DeepAgents、Reasonix、Pi。这 13 个 CLI 的差异有多大有的走 stdio、有的走 gRPC、有的只给你一个 shell 入口流式输出的格式各说各话有的返回 JSON Lines有的返回带 ANSI 转义的自定义文本。如果直接在业务代码里写if (provider ClaudeCode)这种判断没过半年就会变成一坨谁都不敢动的祖传代码。所以 HagiCode 做了一个决定在业务层和具体 CLI 之间加一套薄薄的抽象层和共享运行时。这套抽象层的目标很纯粹——让业务代码不关心它调的到底是哪一个 CLI。2. AIProviderType、IAIProvider、AIProviderFactory 三层抽象怎么分工接 13 个 CLI 的核心思路其实就一句话让业务代码不关心它调的到底是哪一个。为了做到这一点HagiCode 把系统拆成了六层从上往下看每一层都有明确的职责边界。身份层——AIProviderType 枚举。这是每个 CLI 的身份证号。任何地方提到一个 CLI都用这个枚举值标识字符串和枚举之间用ToStringValue()/ToAIProviderType()互转。简单却不可或缺。枚举的原始定义长这样public enum AIProviderType { ClaudeCodeCli 0, CodexCli 1, GitHubCopilot 2, CodebuddyCli 3, OpenCodeCli 4, IFlowCli 5, // 已废弃 HermesCli 6, QoderCli 7, KiroCli 8, KimiCli 9, GeminiCli 10, DeepAgentsCli 11, ReasonixCli 12, PiCli 13, }业务契约层——IAIProvider / IAIProviderFactory。业务侧只认IAIProvider这个接口里面定义的是发一个 prompt、拿到流式回复这种通用动作。至于底下是 Claude 还是 Codex业务不关心。就像你写信只管把信交出去至于邮差姓什么谁在乎呢适配器层——*CliProvider。每个 CLI 对应一个薄适配器比如PiCliProvider、ReasonixCliProvider、ClaudeCodeCliProvider。这些适配器要做的事情很少把通用的业务请求翻译成具体 CLI 能懂的参数再把具体 CLI 的输出翻译回来。它们故意写得很薄新加一个 CLI基本就是抄一个现成的改改而已。共享运行时层——ICliProvider。这一层在HagiCode.Libs里是真正干脏活累活的地方跨平台拉起进程、处理 stdio 传输、解析流式输出、处理超时和重试。所有适配器都复用同一套运行时所以对接一个新 CLI 时进程管理这块基本不用重写。打个比方适配器层是翻译官共享运行时层是快递公司。翻译官只管把话说清楚包裹怎么送、路上堵不堵车那是快递公司的事。工厂路由层——AIProviderFactory。CreateProvider里一个 switch按AIProviderType实例化对应适配器顺带校验IsConfigured。这是唯一一处知道具体类型的地方被严格隔离在工厂里。变化只允许在一个角落里发生其余地方都干干净净。目录 / UI 投影层——main-professions.yaml。这一层有意思它不是代码是数据。主职业清单我是个前端、我是个后端、我是个全栈这种角色画像由main-professions.yaml这个预设文件驱动通过HeroPrimaryProfessionPresetProvider读出来再投影到前端 UI。新增一个主职业不需要改一行代码改 YAML 就行。顺便说一句这块是 HagiCode 重构最大的地方。早期版本里有个叫AgentCliInstallRegistry的代码内注册表后来发现维护成本太高整套被推倒换成了数据驱动 健康监测的方案。这也是为什么 HagiCode 现在能快速扩展职业类型的原因。3. 可复制的 Provider 注册配置与工厂路由片段理解了分层接下来看具体怎么落地。这一节给出可以直接抄进你工程的配置片段和工厂路由代码。注意HagiCode 的 Provider 注册分两部分一部分是代码里的工厂路由一部分是数据驱动的 YAML 预设。先看工厂路由的核心逻辑。AIProviderFactory.CreateProvider是唯一知道具体类型的地方它的结构大概是这样public IAIProvider CreateProvider(AIProviderType providerType, IServiceProvider sp) { if (providerType AIProviderType.IFlowCli) { throw new NotSupportedException(IFlowCli is no longer supported); } if (!IsActivelySupportedProviderType(providerType)) { throw new NotSupportedException($Provider {providerType} is not actively supported); } return providerType switch { AIProviderType.ClaudeCodeCli sp.GetRequiredServiceClaudeCodeCliProvider(), AIProviderType.CodexCli sp.GetRequiredServiceCodexCliProvider(), AIProviderType.GitHubCopilot sp.GetRequiredServiceGitHubCopilotProvider(), AIProviderType.CodebuddyCli sp.GetRequiredServiceCodebuddyCliProvider(), AIProviderType.OpenCodeCli sp.GetRequiredServiceOpenCodeCliProvider(), AIProviderType.HermesCli sp.GetRequiredServiceHermesCliProvider(), AIProviderType.QoderCli sp.GetRequiredServiceQoderCliProvider(), AIProviderType.KiroCli sp.GetRequiredServiceKiroCliProvider(), AIProviderType.KimiCli sp.GetRequiredServiceKimiCliProvider(), AIProviderType.GeminiCli sp.GetRequiredServiceGeminiCliProvider(), AIProviderType.DeepAgentsCli sp.GetRequiredServiceDeepAgentsCliProvider(), AIProviderType.ReasonixCli sp.GetRequiredServiceReasonixCliProvider(), AIProviderType.PiCli sp.GetRequiredServicePiCliProvider(), _ throw new ArgumentOutOfRangeException(nameof(providerType)) }; }再看数据驱动的部分。main-professions.yaml驱动主职业目录和 UI 投影结构大致如下professions: - id: frontend name: 前端工程师 description: 专注 Web 界面与交互实现 providers: - ClaudeCodeCli - CodexCli - GeminiCli - id: backend name: 后端工程师 description: 专注服务端逻辑与数据层 providers: - ClaudeCodeCli - CodexCli - KimiCli - id: fullstack name: 全栈工程师 description: 前后端通吃 providers: - ClaudeCodeCli - CodexCli - GitHubCopilot - OpenCodeCli如果你要把这套模式接到自己的工程里还需要一个 Provider 的注册配置。以 .NET 的依赖注入为例注册片段大概是这样services.AddSingletonClaudeCodeCliProvider(); services.AddSingletonCodexCliProvider(); services.AddSingletonGitHubCopilotProvider(); services.AddSingletonCodebuddyCliProvider(); services.AddSingletonOpenCodeCliProvider(); services.AddSingletonHermesCliProvider(); services.AddSingletonQoderCliProvider(); services.AddSingletonKiroCliProvider(); services.AddSingletonKimiCliProvider(); services.AddSingletonGeminiCliProvider(); services.AddSingletonDeepAgentsCliProvider(); services.AddSingletonReasonixCliProvider(); services.AddSingletonPiCliProvider(); services.AddSingletonIAIProviderFactory, AIProviderFactory();这里有个关键点每个*CliProvider都实现了IAIProvider而它们内部都依赖同一个ICliProvider共享运行时。所以你在注册时只需要把共享运行时注册一次所有适配器都能复用。这就是薄适配器 共享运行时的价值——新增一个 CLI进程管理、stdio 传输、流式解析这些脏活累活都不用重写。如果你用的是 TaoToken 这类统一接入服务来管理多个模型的 API Key可以把 Base URL 指向https://taotoken.net/api然后在 Provider 配置里统一填 Key 和 Model ID。这样即使底层 CLI 换了业务代码也不用动。需要生成或管理 Key 的话可以到 TaoToken API Keys 页面操作接入文档在 TaoToken 文档。4. 新增一个 Agent CLI 的完整验证步骤理论讲完了落到实操。在 HagiCode 里加一个新 CLI大概也就这么几步。我把它拆成一个可跟做的清单你可以照着在自己的工程里复现。第一步在 AIProviderType 里加一个枚举值。假设你要接入一个叫NewAgentCli的 CLI先在枚举末尾加一行NewAgentCli 14,注意不要复用已废弃的IFlowCli 5那个值已经被显式挡在门外了。新增值从 14 开始保持向后兼容。第二步抄一个现成的 *CliProvider。找一个参数风格最接近的适配器比如PiCliProvider复制一份改成NewAgentCliProvider。核心改动是三个地方CLI 可执行文件路径、参数拼装逻辑、流式输出解析。适配器本身很薄通常不超过 100 行。public class NewAgentCliProvider : IAIProvider { private readonly ICliProvider _cliProvider; public NewAgentCliProvider(ICliProvider cliProvider) { _cliProvider cliProvider; } public async IAsyncEnumerablestring StreamAsync( string prompt, [EnumeratorCancellation] CancellationToken ct) { var args new[] { --prompt, prompt, --stream }; await foreach (var chunk in _cliProvider.RunStreamAsync(new-agent, args, ct)) { yield return chunk; } } }第三步在 AIProviderFactory 的 switch 里加一行路由。这一步不能忘否则工厂找不到新 ProviderAIProviderType.NewAgentCli sp.GetRequiredServiceNewAgentCliProvider(),第四步如果要进主职业目录在 main-professions.yaml 里配一下。比如把NewAgentCli加到fullstack职业的 providers 列表里。这一步是数据改动不需要动代码。第五步镜像里加一条安装命令或者走外部管理兜底。HagiCode 的做法是 Docker Compose 预装 外部管理兜底。镜像里把主流 CLI 都预装好用户拉镜像就能用。对于需要在本地环境单独装的安装命令矩阵大概是这样CLI官方安装方式Claude Codenpm install -g anthropic-ai/claude-codeCodexnpm install -g openai/codexGitHub Copilotnpm install -g github/copilotCodeBuddynpm install -g tencent-ai/codebuddy-codeOpenCodenpm i -g opencode-ailatestQodernpm install -g qoder-ai/qodercliKirocurl -fsSL https://cli.kiro.dev/install | bashKimicurl -LsSf https://code.kimi.com/install.sh | bashGemininpm install -g google/gemini-cliHermes官方脚本保留 docs-only 兜底DeepAgents / Reasonix见各自官方文档第六步验证请求。启动服务后调用一次CreateProvider确认返回的是NewAgentCliProvider实例然后发一个测试 prompt看流式输出是否正常。如果一切顺利你会在日志里看到类似这样的输出[ProviderFactory] Resolved NewAgentCli - NewAgentCliProvider [NewAgentCliProvider] Streaming prompt: hello [NewAgentCliProvider] Chunk: hello [NewAgentCliProvider] Chunk: world [NewAgentCliProvider] Stream completed in 1.2s整套流程下来核心改动不超过两百行代码。这就是这套抽象真正的价值——每多接一个 CLI边际成本都很低业务代码一行都不用改。5. 接入过程中最容易踩的 5 个坑与排查方法即使有了这套抽象实际接入时还是会遇到一些坑。这一节把 HagiCode 开发过程中踩过的典型问题列出来对照真实报错给出排查方法。坑一401 UnauthorizedKey 没配对。这是最常见的报错。现象是 Provider 能实例化但一发请求就返回 401。排查步骤先确认环境变量里的 Key 是否正确加载再确认 Base URL 是否指向了正确的端点。如果你用的是 TaoToken 统一接入Base URL 应该是https://taotoken.net/apiKey 从 TaoToken API Keys 页面获取。注意Key 和 Base URL 必须配套混用不同服务的 Key 会直接 401。坑二local proxy failed进程拉不起来。这个报错通常出现在共享运行时层。现象是ICliProvider.RunStreamAsync抛异常提示进程启动失败。排查步骤先确认 CLI 可执行文件在 PATH 里用which new-agent或where new-agent检查再确认参数拼装没有多余的空格或引号最后检查跨平台差异Windows 下可能需要.cmd后缀。坑三reading choices 报错流式解析对不上。这个报错说明适配器的输出解析逻辑和 CLI 实际返回的格式不匹配。有的 CLI 返回 JSON Lines有的返回纯文本有的带 ANSI 转义。排查步骤先用命令行手动跑一次 CLI把原始输出抓下来对照适配器里的解析逻辑逐行核对。如果 CLI 返回的是{choices: [...]}这种结构但适配器按纯文本解析就会报reading choices相关的错误。坑四OAuth 认证失败CLI 需要交互式登录。有些 CLI比如 GitHub Copilot需要 OAuth 登录不能纯靠 API Key。现象是请求返回OAuth token expired或authentication required。排查步骤确认 CLI 是否支持非交互式认证如果不支持需要在镜像构建阶段预置 token或者走外部管理兜底让用户在宿主机上先登录好。坑五工厂路由漏配Provider 找不到。这个坑最隐蔽因为编译能过运行时才报错。现象是CreateProvider抛ArgumentOutOfRangeException。排查步骤检查AIProviderFactory的 switch 里是否加了新枚举值的路由检查IsActivelySupportedProviderType()是否把新值过滤掉了检查依赖注入注册里是否漏了services.AddSingletonNewAgentCliProvider()。如果你在接入 Claude Code 或 Codex 这类 CLI 时遇到认证问题可以先用 TaoToken 模型对话 验证一下 Key 和模型是否可用排除是 Key 本身的问题还是 CLI 配置的问题。对于长期跑编码任务或 Agent 的场景可以考虑 TaoToken Coding Plan把多个 CLI 的调用统一到一套配额下管理。6. 把这套接入模式搬到你的工程里回头看接 13 个 CLI听着吓人可拆开看其实也就两层功夫。一层是把变化隔离——通过AIProviderType枚举 IAIProvider契约 薄适配器 共享运行时让业务代码和具体 CLI 解耦另一层是把配置数据化——用main-professions.yaml这种 YAML 预设驱动目录和 UI避免每加一个东西都要动代码。这套方案是 HagiCode 在实际开发里踩过坑、迭代过几轮才稳定下来的。如果你正在做类似的多 Provider 整合系统建议从三个地方入手先把身份枚举和业务契约定下来再写一个共享运行时把进程管理和流式解析收口最后用工厂路由把类型判断隔离在一个角落里。做完这三步你会发现新增一个 CLI 的成本从重写一套逻辑降到抄一个适配器改改。Agent CLI 这两年还会继续冒出来一个能快速接入新 CLI 的架构比现在支持了几个重要得多。如果你在接入过程中需要统一管理多个模型的 Key 和配额可以到 TaoToken 控制台 看看接入文档在 TaoToken 文档。
返回列表