其实一切都要从那个问题说起:在 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, |
// ... 其他 provider |
PiCli = 13, |
} |
这个枚举值是 provider 身份的根源,会影响 OpenAPI 生成的PCode_Models_AIProviderType.ts前端枚举、AIProviderFactory的创建分支,以及 Provider 解析和活跃 provider 判断逻辑。在AIProviderFactory中注册新的创建分支:
case AIProviderType.PiCli: |
provider = new PiCliProvider( |
logger, |
configuration, |
providerFactory.GetRequiredService<ICliProvider<PiOptions>>(), |
providerFactory.GetService<IAgentCliRuntimeEnvironmentResolver>()); |
break; |
其实这些代码也没什么特别的,就是一个枚举加一个 switch case 而已。只是它们很重要,就像乐谱上的音符,一个个加起来才能演奏出完整的曲子。
Thin Adapter 实现
PiCliProvider.cs是核心的 thin adapter,它实现了IAIProvider、IVersionedAIProvider和IAsyncDisposable接口。通过构造函数接收ICliProvider<PiOptions>(来自HagiCode.Libs),将AIRequest/ProviderConfiguration映射到PiOptions,然后委托执行、ping、版本查询等操作给底层 provider。
关键是要处理 Pi 特有的 JSON 事件流,包括assistant.thought、assistant、terminal.completed等事件。这些事件在流式输出过程中需要被正确解析和转换为系统的标准格式。这有点像翻译,把一种语言翻译成另一种语言,意思要传达到位才行。
主职业预设
在main-professions.yaml中增加了profession-pi主职业条目:
- Id: "profession-pi" |
Name: "Pi" |
Family: "pi" |
Summary: "hero.professionCopy.primary.pi.summary" |
Icon: "executor-avatar:Pi" |
SourceLabel: "hero.professionCopy.sources.aiProvidersPiCli" |
ProviderType: "PiCli" |
SortOrder: 59 |
DefaultEnabled: true |
DefaultParameters: |
binary: "pi" |
provider: "omniroute" |
thinking: "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 Set<string>([ |
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/pi |
Provider: omniroute |
Thinking: balanced |
SessionDirectory: ~/.pi/sessions |
NoSession: false |
DisableAllTools: false |
DisableBuiltinTools: 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 pi |
pi --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 枚举用
PiCli,CLI 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 交流。毕竟,技术这东西,多交流才有进步。