将 Pi Agent 接入 HagiCode 的实践之路
其实一切都要从那个问题说起在 HagiCode Mono 项目中虽然repos/Hagicode.Libs已经实现了可复用的PiProvider可是repos/hagicode-core和repos/web还没有将 Pi 提升为项目级一等 Agent CLI。这就像你有了一双好鞋却还没系好鞋带虽然能走路但总觉得差点什么。现有的PiProvider位于HagiCode.Libs/src/HagiCode.Libs.Providers/Pi/PiProvider.cs它实现了基于行分隔 JSON--mode json --print的 CLI 通信协议支持会话管理、工具调用和流式输出。这部分代码写得很好只是它还躺在那里没有被唤醒。这种状况造成了一个断层Pi 的底层能力是完整的但项目级的接入链路从用户配置到执行监控是缺失的。这就像画了一幅好画却没挂在墙上让人欣赏。所以需要将 Pi 作为新的活跃 providerPiCli接入整个系统使其成为与其他 Agent CLI 一等的工作流入口。毕竟我们想要的是完整的体验而不是零散的片段。关于 HagiCode本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个智能代码助手项目在开发过程中我们需要整合多种 AI 能力提供商。本文所述的 Pi agent 接入方案正是我们在实践中总结出来的一套可复用的集成模式对于类似的多 provider 集成场景具有参考价值。项目的 GitHub 地址是 github.com/HagiCode-org/site。架构设计Pi agent 对接采用了thin adapter 模式而不是在 core 层重新实现 Pi 进程协议。这个设计决策其实也没什么复杂的就是想了想何必重复造轮子呢毕竟避免重复实现PiProvider中的参数构建、进程启动、JSON 解析和错误处理逻辑已经很完整重复实现会制造两套行为源和两套测试矩阵。这也罢了但维护起来会很麻烦。保持一致性与 Kimi、Gemini、Reasonix、DeepAgents 等现有 CLI 的接入方式保持一致都是通过 thin adapter 委托给 libs 层的实现。大家都这么做跟着走就是了。关注点分离hagicode-core专注于运行时契约和业务逻辑进程细节交给HagiCode.Libs处理。各司其职岂不美哉这种设计让 HagiCode 在保持核心层简洁的同时能够快速集成新的 AI 能力提供商。毕竟简洁是美效率是钱。核心组件实现Provider 枚举和 Factory在hagicode-core/src/PCode.Models/AIProviderType.cs中新增了PiCli 13枚举值public enum AIProviderType{ClaudeCodeCli 0,// ... 其他 providerPiCli 13,}这个枚举值是 provider 身份的根源会影响 OpenAPI 生成的PCode_Models_AIProviderType.ts前端枚举、AIProviderFactory的创建分支以及 Provider 解析和活跃 provider 判断逻辑。在AIProviderFactory中注册新的创建分支case AIProviderType.PiCli:provider new PiCliProvider(logger,configuration,providerFactory.GetRequiredServiceICliProviderPiOptions(),providerFactory.GetServiceIAgentCliRuntimeEnvironmentResolver());break;其实这些代码也没什么特别的就是一个枚举加一个 switch case 而已。只是它们很重要就像乐谱上的音符一个个加起来才能演奏出完整的曲子。Thin Adapter 实现PiCliProvider.cs是核心的 thin adapter它实现了IAIProvider、IVersionedAIProvider和IAsyncDisposable接口。通过构造函数接收ICliProviderPiOptions来自HagiCode.Libs将AIRequest/ProviderConfiguration映射到PiOptions然后委托执行、ping、版本查询等操作给底层 provider。关键是要处理 Pi 特有的 JSON 事件流包括assistant.thought、assistant、terminal.completed等事件。这些事件在流式输出过程中需要被正确解析和转换为系统的标准格式。这有点像翻译把一种语言翻译成另一种语言意思要传达到位才行。主职业预设在main-professions.yaml中增加了profession-pi主职业条目- Id: profession-piName: PiFamily: piSummary: hero.professionCopy.primary.pi.summaryIcon: executor-avatar:PiSourceLabel: hero.professionCopy.sources.aiProvidersPiCliProviderType: PiCliSortOrder: 59DefaultEnabled: trueDefaultParameters:binary: piprovider: omniroutethinking: balanced这确保了后端快照与前端 fallback 目录有一致的 Pi 身份可以通过现有 Hero 编辑器管理 Pi 配置而且 Pi 的加入不会破坏现有主职业条目的可消费性。毕竟我们不想因为新增一个功能就把原来的东西弄乱了那样就因小失大了。监控注册表AgentCliMonitoringRegistry需要增加 Pi 的监控 descriptor使系统能够解析可执行路径、展示品牌名、进行健康探测并在状态栏和健康详情中显示 Pi 状态new AgentCliMonitoringDescriptor(CliId: pi,DisplayName: Pi,ProviderType: AIProviderType.PiCli,DisplayOrder: 13,Strategy: AgentCliMonitoringStrategy.Grain,NotConfiguredMessage: Pi CLI is not configured or executable not found.,EnabledPaths: [],ExecutablePathConfigPaths: [Hero:PrimaryProfessions:Pi:ExecutablePath],DefaultExecutablePath: pi)监控系统就像仪表盘告诉你车子跑得怎么样。Pi 的状态一目了然这样用户就能知道一切是否正常。前端适配执行器类型适配前端需要更新executorTypeAdapter.ts添加 Pi 的类型识别逻辑const PI_IDS new Setstring([PCode_Models_AIProviderType.PI_CLI,Pi,PiCli,pi,picli,pi-cli,]);export function isPi(value: string): boolean {const normalized normalize(value);const normalizedLower normalized.toLowerCase();return PI_IDS.has(normalized)|| normalizedLower.includes(pi-cli)|| normalizedLower.includes(picli)|| normalizedLower pi;}这就像给 Pi 起了好几个名字不管你怎么叫它它都知道你是在叫它。毕竟叫什么名字不重要重要的是知道你是谁。Fallback Hero 目录在hero.ts中添加 Pi 的 fallback 条目确保即使后端数据未加载前端也能正常显示 Pi 配置{id: profession-pi,name: Pi,family: pi,summary: hero.professionCopy.primary.pi.summary,icon: executor-avatar:Pi,sourceLabel: hero.professionCopy.sources.aiProvidersPiCli,providerType: AIProviderType.PI_CLI,sortOrder: 59,isReadOnly: true,managedParameterKeys: {// 首版支持的参数 key},defaultParameters: {binary: pi,provider: omniroute,thinking: balanced,},}Fallback 就像备用计划万一后端挂了或者数据没加载出来前端也能正常工作。毕竟谁也不想到时候手忙脚乱。本地化文案和表单在locales/*/common/{hero,settings}.yml中增加 Pi 相关的翻译并在HeroCliEquipmentForm.tsx中为 Pi 新增配置字段区块支持 binary、provider、thinking、sessionDirectory 和工具/会话开关字段。首版 Pi 只暴露最小必要字段复杂功能如工具 allowlist/denylist 和环境变量编辑器被明确延后到后续变更。毕竟一口吃不成胖子慢慢来比较快。配置示例后端配置在appsettings.yml中配置 Pi 参数Hero:PrimaryProfessions:Pi:ExecutablePath: /usr/local/bin/piProvider: omnirouteThinking: balancedSessionDirectory: ~/.pi/sessionsNoSession: falseDisableAllTools: falseDisableBuiltinTools: false前端配置在 Hero 编辑器中配置 Pi profession{id: my-pi-profession,name: My Pi Profession,family: pi,providerType: AIProviderType.PI_CLI,primaryModel: {provider: PiCli,model: glm-4.7,providerSettings: {provider: omniroute,thinking: balanced,sessionDirectory: /Users/username/.pi/sessions,noSession: false,disableAllTools: false,disableBuiltinTools: false,},},}配置文件就像菜谱照着做就能做出好菜。只是有时候就算照着菜谱也会把菜做糊......不过这次的配置倒是很清晰应该没什么问题。验证和测试后端验证在测试中验证主职业预设var snapshot await presetProvider.GetSnapshotAsync();var piProfession snapshot.FindById(profession-pi);piProfession.ShouldNotBeNull();piProfession.ProviderType.ShouldBe(AIProviderType.PiCli);piProfession.Family.ShouldBe(pi);还需要验证 Pi 可用性和健康检查# 检查 Pi 可执行文件which pipi --version# 验证后端 provider 注册curl http://localhost:35168/api/health/agent-cli/pi测试就像考试考过了才能说明真的会了。只是有时候考试过了也未必真的懂了不过至少说明你能做对题。前端验证// 验证类型解析和显示名称expect(resolveExecutorVisualType(pi-cli)).toBe(Pi);expect(resolveExecutorVisualType(PCode_Models_AIProviderType.PI_CLI)).toBe(Pi);expect(resolveExecutorDisplayName(PiCli)).toBe(Pi);// 验证 fallback 目录const piFallback findFallbackProfessionById(profession-pi);expect(piFallback?.providerType).toBe(AIProviderType.PI_CLI);这些测试用例覆盖了主要的逻辑路径确保 Pi 能被正确识别和显示。毕竟展示给用户的东西不能出差错。常见问题排查Pi 可执行文件找不到如果健康检查返回 Pi executable was not found.需要检查 PATH 中是否有 pi或确认配置的路径是否正确。解决方法是确保pi已安装并在 PATH 中或在appsettings.yml中配置正确的ExecutablePath。这就像找不到家门钥匙得想想是不是放错地方了。其实解决办法也挺简单的要么把钥匙放回原来的地方要么换把新锁。配置字段不识别如果启动时抛出 PiCli runtime settings [...] are not supported 错误检查是否只使用了首版支持的配置字段。首版支持的字段包括provider、thinking、sessionDirectory、noSession、disableAllTools、disableBuiltinTools。有时候就是贪心想要的功能太多结果系统不支持。其实首版的功能已经够用了贪多嚼不烂。前端无法选择 Pi如果 Hero 编辑器中没有 Pi 选项检查是否已运行npm run generate:api重新生成前端枚举hero.ts中是否有profession-pi条目以及本地化文案是否正确添加。排查问题就像找丢失的物品得一步步来。毕竟瞎找是找不到的得有逻辑地找。最佳实践使用 thin adapter 模式不要在 core 层重新实现进程协议委托给 libs 层的 provider。这样可以避免重复实现保持代码一致性。毕竟重复造轮子不仅累还容易出问题。保持命名一致性前后端使用统一的命名约定避免混淆。Provider 枚举用PiCliCLI ID 用pi显示名称用Pi。名字取得好沟通成本就低。优先使用预设首版应基于profession-pi预设而不是要求用户手工配置。这样可以让用户快速上手减少配置复杂度。用户喜欢简单的事情复杂的让他们来找我。关注错误信息确保错误信息清晰、可操作帮助用户快速定位问题。错误信息写得清楚用户就不会因为一个错误就抓狂。版本兼容性考虑到AIProviderType枚举值的序列化稳定性变更需要谨慎处理。AIProviderType.PiCli 13的枚举值不能轻易修改。毕竟改了这个值可能会破坏向后兼容性那就麻烦了。总结通过 thin adapter 模式我们成功将 Pi agent 接入 HagiCode 系统使其成为与其他 Agent CLI 一等的工作流入口。这套方案的核心优势在于避免了重复实现复用了 libs 层已有的PiProvider与现有 provider 保持一致的接入方式降低了维护成本实现了从用户配置到执行监控的完整链路HagiCode 的这次实践证明thin adapter 模式是集成 AI 能力提供商的有效方案。它让我们能够快速支持新的 agent同时保持系统的稳定性和可维护性。其实做技术就是这样找到一个好的模式然后复用它。这样既能快速前进又不会迷失方向。就像走路找到了一条好路就一直走下去只是偶尔也会停下来看看风景......如果你也在做多 provider 的 AI 能力集成希望这套方案能给你一些启发。如果你对 HagiCode 项目感兴趣欢迎来 GitHub 交流。毕竟技术这东西多交流才有进步。参考