HagiCode 平台的多 AI Provider 架构实践
HagiCode 平台的多 AI Provider 架构实践
Section titled “HagiCode 平台的多 AI Provider 架构实践”本文分享了在 Orleans Grain 架构下,如何通过统一的 IAIProvider 接口集成 iflow 和 OpenCode 两个 AI 工具的技术方案,并详细对比了 WebSocket 和 HTTP 两种通信方式的实现差异。
其实也没什么特别的,就是做 HagiCode 的时候遇到了个挺实际的问题——用户想用不同的 AI 工具,这倒也不奇怪,毕竟每个人都有自己的习惯。有的喜欢 Claude Code,有的钟爱 GitHub Copilot,还有些团队用自己开发的工具。
最开始的方案也挺简单粗暴的,就是给每个 AI 工具写专门的对接代码。可后来问题就来了——代码里全是 if-else,改一个地方要到处测试,新工具接入还得重新写一堆逻辑,想想都觉得累。
后来我想明白了,不如做一个统一的 IAIProvider 接口,把所有 AI 提供者的能力都抽象出来。这样,不管底层用的是哪个工具,对上层来说都是一样的调用方式,岂不美哉?
最近项目要接入两个新工具:iflow 和 OpenCode。这两个都支持 ACP 协议,但通信方式不太一样——iflow 用 WebSocket,OpenCode 用 HTTP API。这也算是种考验吧,要在统一的接口下适配两种不同的通信模式,不过想想也挺有意思的。
关于 HagiCode
Section titled “关于 HagiCode”本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个基于 Orleans Grain 架构的 AI 辅助开发平台,通过统一的 IAIProvider 接口与不同的 AI 提供者集成,让用户可以灵活选择自己喜欢的 AI 工具。
统一的接口抽象
Section titled “统一的接口抽象”首先,定义了 IAIProvider 接口,把所有 AI 提供者需要实现的能力都抽象出来:
public interface IAIProvider{ string Name { get; } bool SupportsStreaming { get; } ProviderCapabilities Capabilities { get; }
Task<AIResponse> ExecuteAsync(AIRequest request, CancellationToken cancellationToken = default); IAsyncEnumerable<AIStreamingChunk> StreamAsync(AIRequest request, CancellationToken cancellationToken = default); Task<ProviderTestResult> PingAsync(CancellationToken cancellationToken = default); IAsyncEnumerable<AIStreamingChunk> SendMessageAsync(AIRequest request, string? embeddedCommandPrompt = null, CancellationToken cancellationToken = default);}这个接口有几个关键方法:
- ExecuteAsync:执行一次性的 AI 请求
- StreamAsync:流式获取响应,支持实时展示
- PingAsync:健康检查,验证 provider 是否可用
- SendMessageAsync:发送消息,支持嵌入式命令
IFlowCliProvider:基于 WebSocket 的实现
Section titled “IFlowCliProvider:基于 WebSocket 的实现”iflow 使用 WebSocket 进行 ACP 通信,整体架构是这样的:
IFlowCliProvider → ACPSessionManager → WebSocketAcpTransport → iflow CLI ↓ 动态端口分配 + 进程管理核心流程也挺简单:
- ACPSessionManager 负责创建和管理 ACP 会话
- WebSocketAcpTransport 处理 WebSocket 通信
- 动态分配一个端口,用 iflow —experimental-acp —port 启动 iflow 进程
- 通过 IAIRequestToAcpMapper 和 IAcpToAIResponseMapper 做请求/响应的转换
来看看核心代码:
private async IAsyncEnumerable<AIStreamingChunk> StreamCoreAsync( AIRequest request, string? embeddedCommandPrompt, [EnumeratorCancellation] CancellationToken cancellationToken){ // 解析工作目录 var resolvedWorkingDirectory = ResolveWorkingDirectory(request); var effectiveRequest = ApplyEmbeddedCommandPrompt(request, embeddedCommandPrompt);
// 创建 ACP 会话 await using var session = await _sessionManager.CreateSessionAsync( Name, resolvedWorkingDirectory, cancellationToken, request.SessionId);
// 发送提示词 var prompt = _requestMapper.ToPromptString(effectiveRequest); var promptResponse = await session.SendPromptAsync(prompt, cancellationToken);
// 接收流式响应 await foreach (var notification in session.ReceiveUpdatesAsync(cancellationToken)) { if (_responseMapper.TryConvertToStreamingChunk(notification, out var chunk)) { if (chunk.Type == StreamingChunkType.Metadata && chunk.IsComplete) { yield return chunk; yield break; } yield return chunk; } }}这里有几个设计上的注意点,也算是一些小心得:
- 用 await using 确保会话正确释放,避免资源泄漏,毕竟资源这东西,不用了就该放归自然
- 流式响应通过 IAsyncEnumerable 返回,天然支持异步流
- Metadata 类型的 chunk 判断是否完成,确保完整接收响应
OpenCodeCliProvider:基于 HTTP API 的实现
Section titled “OpenCodeCliProvider:基于 HTTP API 的实现”OpenCode 用 HTTP API 方式提供服务,架构略有不同:
OpenCodeCliProvider → OpenCodeRuntimeManager → OpenCodeClient → OpenCode HTTP API ↓ OpenCodeProcessManager → opencode 进程管理OpenCode 的特点是用 SQLite 数据库持久化会话绑定关系,这样可以支持会话恢复和提示词响应恢复,这倒是挺贴心的设计:
private async Task<OpenCodePromptExecutionResult> ExecutePromptAsync( AIRequest request, string? embeddedCommandPrompt, CancellationToken cancellationToken){ var prompt = BuildPrompt(request, embeddedCommandPrompt); var resolvedWorkingDirectory = ResolveWorkingDirectory(request.WorkingDirectory); var client = await _runtimeManager.GetClientAsync(resolvedWorkingDirectory, cancellationToken); var bindingSessionId = request.SessionId; var boundSession = TryGetBinding(bindingSessionId, resolvedWorkingDirectory);
// 尝试使用已绑定的会话 if (boundSession is not null) { try { return await PromptSessionAsync( client, boundSession, BuildPromptRequest(request, prompt, CreatePromptMessageId()), request.Model ?? _settings.Model, cancellationToken); } catch (OpenCodeApiException ex) when (IsStaleBinding(ex)) { // 会话已过期,移除绑定 RemoveBinding(bindingSessionId); } }
// 创建新会话 var session = await client.Session.CreateAsync(new OpenCodeSessionCreateRequest { Title = BuildSessionTitle(request) }, cancellationToken);
BindSession(bindingSessionId, session.Id, resolvedWorkingDirectory); return await PromptSessionAsync(client, session.Id, ...);}这个实现有几个亮点,或者说几个有趣的地方:
- 会话绑定机制:同一个 SessionId 会复用 OpenCode 会话,避免重复创建,省得浪费资源
- 过期处理:检测到会话过期时自动清理绑定,旧的不去,新的不来
- 数据库持久化:通过 SQLite 存储绑定关系,重启后仍然有效,有些东西记住了就是记住了
两种方式的对比
Section titled “两种方式的对比”| 方面 | IFlowCliProvider | OpenCodeCliProvider |
|---|---|---|
| 通信方式 | WebSocket (ACP) | HTTP API |
| 进程管理 | ACPSessionManager | OpenCodeProcessManager |
| 端口分配 | 动态端口 | 无端口(使用 HTTP) |
| 会话管理 | ACPSession | OpenCodeSession |
| 持久化 | 内存缓存 | SQLite 数据库 |
| 启动命令 | iflow —experimental-acp —port | opencode |
| 延迟 | 更低(长连接) | 相对较高(HTTP 请求) |
选择哪种方式主要看你的需求:WebSocket 适合实时性要求高的场景,HTTP API 则更简单、更容易调试。这就像选路一样,有的路快一点,有的路好走一点罢了。
配置 Provider
Section titled “配置 Provider”先在配置文件里启用这两个 provider:
AI: Providers: IFlowCli: Type: "IFlowCli" Enabled: true ExecutablePath: "iflow" Model: null WorkingDirectory: null OpenCodeCli: Type: "OpenCodeCli" Enabled: true ExecutablePath: "opencode" Model: "anthropic/claude-sonnet-4" WorkingDirectory: null
OpenCode: Enabled: true BaseUrl: "http://localhost:38376" ExecutablePath: "opencode" StartupTimeoutSeconds: 30 RequestTimeoutSeconds: 120使用 IFlowCliProvider
Section titled “使用 IFlowCliProvider”// 通过 Factory 获取 providervar provider = await _providerFactory.GetProviderAsync(AIProviderType.IFlowCli);
// 执行 AI 请求var request = new AIRequest{ Prompt = "请帮我重构这个函数", WorkingDirectory = "/path/to/project", Model = "claude-sonnet-4"};
// 获取完整响应var response = await provider.ExecuteAsync(request, cancellationToken);Console.WriteLine(response.Content);
// 或者用流式响应await foreach (var chunk in provider.StreamAsync(request, cancellationToken)){ if (chunk.Type == StreamingChunkType.ContentDelta) { Console.Write(chunk.Content); }}使用 OpenCodeCliProvider
Section titled “使用 OpenCodeCliProvider”// 通过 Factory 获取 providervar provider = await _providerFactory.GetProviderAsync(AIProviderType.OpenCodeCli);
var request = new AIRequest{ Prompt = "请帮我分析这个错误", WorkingDirectory = "/path/to/project", Model = "anthropic/claude-sonnet-4"};
var response = await provider.ExecuteAsync(request, cancellationToken);Console.WriteLine(response.Content);在启动或使用前,可以先检查 provider 是否可用:
var iflowResult = await iflowProvider.PingAsync(cancellationToken);if (!iflowResult.Success){ Console.WriteLine($"IFlow 不可用: {iflowResult.ErrorMessage}"); return;}
var openCodeResult = await openCodeProvider.PingAsync(cancellationToken);if (!openCodeResult.Success){ Console.WriteLine($"OpenCode 不可用: {openCodeResult.ErrorMessage}"); return;}嵌入式命令支持
Section titled “嵌入式命令支持”两个 provider 都支持嵌入式命令,比如 /file:xxx 这样的命令:
var request = new AIRequest{ Prompt = "分析这个文件的问题", SystemMessage = "你是一个代码分析专家"};
await foreach (var chunk in provider.SendMessageAsync( request, embeddedCommandPrompt: "/file:src/main.cs", cancellationToken)){ Console.Write(chunk.Content);}注意事项和最佳实践
Section titled “注意事项和最佳实践”IFlow 用 WebSocket 长连接,所以资源管理要特别注意:
- 用 await using 确保会话正确释放,不用了就放手
- 取消操作会触发进程清理
- ACPSessionManager 支持最大会话数限制
OpenCode 的进程管理相对简单,OpenCodeRuntimeManager 会自动处理,省心不少。
两个 provider 都有完善的错误处理:
- IFlow 的错误通过 ACP 会话更新传播
- OpenCode 的错误通过 OpenCodeApiException 抛出
- 建议在调用方捕获并处理这些异常,毕竟错误总会发生的
- IFlow 的 WebSocket 通信比 HTTP 有更低的延迟
- OpenCode 的会话复用可以减少 HTTP 请求开销
- Factory 的缓存机制可以避免重复创建 provider
- 高并发场景下,要关注进程数和连接数的限制,别到时候撑不住了
启动时会验证可执行文件路径,但运行时也可能出问题。PingAsync 是个好工具,可以验证配置是否正确:
// 启动时检查var provider = await _providerFactory.GetProviderAsync(providerType);var result = await provider.PingAsync(cancellationToken);if (!result.Success){ _logger.LogError("Provider {ProviderType} 不可用: {Error}", providerType, result.ErrorMessage);}本文分享了 HagiCode 平台在集成 iflow 和 OpenCode 两个 AI 工具时的技术方案。通过统一的 IAIProvider 接口,实现了对不同通信方式(WebSocket 和 HTTP)的适配,同时保持了上层调用的一致性。
核心思路其实挺简单的:
- 定义统一的接口抽象
- 对不同实现做适配层
- 通过工厂模式统一管理
这样扩展性就很好,以后有新的 AI 工具要接入,只需要实现 IAIProvider 接口就行,不用改动太多现有代码。想想也挺合理的,就像搭积木一样,有统一的接口,想怎么拼都行。
如果你也在做多 AI 工具的集成,希望本文对你有帮助。不过话说回来,技术这东西,能帮到人就好,其他的也不必太在意…
- HagiCode GitHub: github.com/HagiCode-org/site
- HagiCode 官网: hagicode.com
- HagiCode 安装指南: docs.hagicode.com/installation
- ACP 协议规范: github.com/modelcontextprotocol/specification
- Orleans 文档: learn.microsoft.com/dotnet/orleans
如果本文对你有帮助:
- 点个赞让更多人看到
- 来 GitHub 给个 Star:github.com/HagiCode-org/site
- 访问官网了解更多:hagicode.com
- 观看 30 分钟实战演示:www.bilibili.com/video/BV1pirZBuEzq/
- 一键安装体验:docs.hagicode.com/installation/docker-compose
- Desktop 桌面端快速安装:hagicode.com/desktop/
- 公测已开始,欢迎安装体验