HagiCode 平台的多 AI Provider 架构实践
This content is not available in your language yet.
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/
- 公测已开始,欢迎安装体验