找回密码
 立即注册
首页 业界区 业界 HagiCode 平台的多 AI Provider 架构实践

HagiCode 平台的多 AI Provider 架构实践

郏琼芳 7 小时前
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

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个基于 Orleans Grain 架构的 AI 辅助开发平台,通过统一的 IAIProvider 接口与不同的 AI 提供者集成,让用户可以灵活选择自己喜欢的 AI 工具。
架构设计

统一的接口抽象

首先,定义了 IAIProvider 接口,把所有 AI 提供者需要实现的能力都抽象出来:
  1. public interface IAIProvider
  2. {
  3.     string Name { get; }
  4.     bool SupportsStreaming { get; }
  5.     ProviderCapabilities Capabilities { get; }
  6.     Task ExecuteAsync(AIRequest request, CancellationToken cancellationToken = default);
  7.     IAsyncEnumerable StreamAsync(AIRequest request, CancellationToken cancellationToken = default);
  8.     Task<ProviderTestResult> PingAsync(CancellationToken cancellationToken = default);
  9.     IAsyncEnumerable SendMessageAsync(AIRequest request, string? embeddedCommandPrompt = null, CancellationToken cancellationToken = default);
  10. }
复制代码
这个接口有几个关键方法:

  • ExecuteAsync:执行一次性的 AI 请求
  • StreamAsync:流式获取响应,支持实时展示
  • PingAsync:健康检查,验证 provider 是否可用
  • SendMessageAsync:发送消息,支持嵌入式命令
IFlowCliProvider:基于 WebSocket 的实现

iflow 使用 WebSocket 进行 ACP 通信,整体架构是这样的:
  1. IFlowCliProvider → ACPSessionManager → WebSocketAcpTransport → iflow CLI
  2.                 ↓
  3.          动态端口分配 + 进程管理
复制代码
核心流程也挺简单:

  • ACPSessionManager 负责创建和管理 ACP 会话
  • WebSocketAcpTransport 处理 WebSocket 通信
  • 动态分配一个端口,用 iflow --experimental-acp --port {port} 启动 iflow 进程
  • 通过 IAIRequestToAcpMapper 和 IAcpToAIResponseMapper 做请求/响应的转换
来看看核心代码:
  1. private async IAsyncEnumerable StreamCoreAsync(
  2.     AIRequest request,
  3.     string? embeddedCommandPrompt,
  4.     [EnumeratorCancellation] CancellationToken cancellationToken)
  5. {
  6.     // 解析工作目录
  7.     var resolvedWorkingDirectory = ResolveWorkingDirectory(request);
  8.     var effectiveRequest = ApplyEmbeddedCommandPrompt(request, embeddedCommandPrompt);
  9.     // 创建 ACP 会话
  10.     await using var session = await _sessionManager.CreateSessionAsync(
  11.         Name,
  12.         resolvedWorkingDirectory,
  13.         cancellationToken,
  14.         request.SessionId);
  15.     // 发送提示词
  16.     var prompt = _requestMapper.ToPromptString(effectiveRequest);
  17.     var promptResponse = await session.SendPromptAsync(prompt, cancellationToken);
  18.     // 接收流式响应
  19.     await foreach (var notification in session.ReceiveUpdatesAsync(cancellationToken))
  20.     {
  21.         if (_responseMapper.TryConvertToStreamingChunk(notification, out var chunk))
  22.         {
  23.             if (chunk.Type == StreamingChunkType.Metadata && chunk.IsComplete)
  24.             {
  25.                 yield return chunk;
  26.                 yield break;
  27.             }
  28.             yield return chunk;
  29.         }
  30.     }
  31. }
复制代码
这里有几个设计上的注意点,也算是一些小心得:

  • 用 await using 确保会话正确释放,避免资源泄漏,毕竟资源这东西,不用了就该放归自然
  • 流式响应通过 IAsyncEnumerable 返回,天然支持异步流
  • Metadata 类型的 chunk 判断是否完成,确保完整接收响应
OpenCodeCliProvider:基于 HTTP API 的实现

OpenCode 用 HTTP API 方式提供服务,架构略有不同:
  1. OpenCodeCliProvider → OpenCodeRuntimeManager → OpenCodeClient → OpenCode HTTP API
  2.                       ↓
  3.                 OpenCodeProcessManager → opencode 进程管理
复制代码
OpenCode 的特点是用 SQLite 数据库持久化会话绑定关系,这样可以支持会话恢复和提示词响应恢复,这倒是挺贴心的设计:
  1. private async Task<OpenCodePromptExecutionResult> ExecutePromptAsync(
  2.     AIRequest request,
  3.     string? embeddedCommandPrompt,
  4.     CancellationToken cancellationToken)
  5. {
  6.     var prompt = BuildPrompt(request, embeddedCommandPrompt);
  7.     var resolvedWorkingDirectory = ResolveWorkingDirectory(request.WorkingDirectory);
  8.     var client = await _runtimeManager.GetClientAsync(resolvedWorkingDirectory, cancellationToken);
  9.     var bindingSessionId = request.SessionId;
  10.     var boundSession = TryGetBinding(bindingSessionId, resolvedWorkingDirectory);
  11.     // 尝试使用已绑定的会话
  12.     if (boundSession is not null)
  13.     {
  14.         try
  15.         {
  16.             return await PromptSessionAsync(
  17.                 client,
  18.                 boundSession,
  19.                 BuildPromptRequest(request, prompt, CreatePromptMessageId()),
  20.                 request.Model ?? _settings.Model,
  21.                 cancellationToken);
  22.         }
  23.         catch (OpenCodeApiException ex) when (IsStaleBinding(ex))
  24.         {
  25.             // 会话已过期,移除绑定
  26.             RemoveBinding(bindingSessionId);
  27.         }
  28.     }
  29.     // 创建新会话
  30.     var session = await client.Session.CreateAsync(new OpenCodeSessionCreateRequest
  31.     {
  32.         Title = BuildSessionTitle(request)
  33.     }, cancellationToken);
  34.     BindSession(bindingSessionId, session.Id, resolvedWorkingDirectory);
  35.     return await PromptSessionAsync(client, session.Id, ...);
  36. }
复制代码
这个实现有几个亮点,或者说几个有趣的地方:

  • 会话绑定机制:同一个 SessionId 会复用 OpenCode 会话,避免重复创建,省得浪费资源
  • 过期处理:检测到会话过期时自动清理绑定,旧的不去,新的不来
  • 数据库持久化:通过 SQLite 存储绑定关系,重启后仍然有效,有些东西记住了就是记住了
两种方式的对比

方面IFlowCliProviderOpenCodeCliProvider通信方式WebSocket (ACP)HTTP API进程管理ACPSessionManagerOpenCodeProcessManager端口分配动态端口无端口(使用 HTTP)会话管理ACPSessionOpenCodeSession持久化内存缓存SQLite 数据库启动命令iflow --experimental-acp --portopencode延迟更低(长连接)相对较高(HTTP 请求)选择哪种方式主要看你的需求:WebSocket 适合实时性要求高的场景,HTTP API 则更简单、更容易调试。这就像选路一样,有的路快一点,有的路好走一点罢了。
实践指南

配置 Provider

先在配置文件里启用这两个 provider:
  1. AI:
  2.   Providers:
  3.     IFlowCli:
  4.       Type: "IFlowCli"
  5.       Enabled: true
  6.       ExecutablePath: "iflow"
  7.       Model: null
  8.       WorkingDirectory: null
  9.     OpenCodeCli:
  10.       Type: "OpenCodeCli"
  11.       Enabled: true
  12.       ExecutablePath: "opencode"
  13.       Model: "anthropic/claude-sonnet-4"
  14.       WorkingDirectory: null
  15. OpenCode:
  16.   Enabled: true
  17.   BaseUrl: "http://localhost:38376"
  18.   ExecutablePath: "opencode"
  19.   StartupTimeoutSeconds: 30
  20.   RequestTimeoutSeconds: 120
复制代码
使用 IFlowCliProvider
  1. // 通过 Factory 获取 provider
  2. var provider = await _providerFactory.GetProviderAsync(AIProviderType.IFlowCli);
  3. // 执行 AI 请求
  4. var request = new AIRequest
  5. {
  6.     Prompt = "请帮我重构这个函数",
  7.     WorkingDirectory = "/path/to/project",
  8.     Model = "claude-sonnet-4"
  9. };
  10. // 获取完整响应
  11. var response = await provider.ExecuteAsync(request, cancellationToken);
  12. Console.WriteLine(response.Content);
  13. // 或者用流式响应
  14. await foreach (var chunk in provider.StreamAsync(request, cancellationToken))
  15. {
  16.     if (chunk.Type == StreamingChunkType.ContentDelta)
  17.     {
  18.         Console.Write(chunk.Content);
  19.     }
  20. }
复制代码
使用 OpenCodeCliProvider
  1. // 通过 Factory 获取 provider
  2. var provider = await _providerFactory.GetProviderAsync(AIProviderType.OpenCodeCli);
  3. var request = new AIRequest
  4. {
  5.     Prompt = "请帮我分析这个错误",
  6.     WorkingDirectory = "/path/to/project",
  7.     Model = "anthropic/claude-sonnet-4"
  8. };
  9. var response = await provider.ExecuteAsync(request, cancellationToken);
  10. Console.WriteLine(response.Content);
复制代码
健康检查

在启动或使用前,可以先检查 provider 是否可用:
  1. var iflowResult = await iflowProvider.PingAsync(cancellationToken);
  2. if (!iflowResult.Success)
  3. {
  4.     Console.WriteLine($"IFlow 不可用: {iflowResult.ErrorMessage}");
  5.     return;
  6. }
  7. var openCodeResult = await openCodeProvider.PingAsync(cancellationToken);
  8. if (!openCodeResult.Success)
  9. {
  10.     Console.WriteLine($"OpenCode 不可用: {openCodeResult.ErrorMessage}");
  11.     return;
  12. }
复制代码
嵌入式命令支持

两个 provider 都支持嵌入式命令,比如 /file:xxx 这样的命令:
  1. var request = new AIRequest
  2. {
  3.     Prompt = "分析这个文件的问题",
  4.     SystemMessage = "你是一个代码分析专家"
  5. };
  6. await foreach (var chunk in provider.SendMessageAsync(
  7.     request,
  8.     embeddedCommandPrompt: "/file:src/main.cs",
  9.     cancellationToken))
  10. {
  11.     Console.Write(chunk.Content);
  12. }
复制代码
注意事项和最佳实践

资源管理

IFlow 用 WebSocket 长连接,所以资源管理要特别注意:

  • 用 await using 确保会话正确释放,不用了就放手
  • 取消操作会触发进程清理
  • ACPSessionManager 支持最大会话数限制
OpenCode 的进程管理相对简单,OpenCodeRuntimeManager 会自动处理,省心不少。
错误处理

两个 provider 都有完善的错误处理:

  • IFlow 的错误通过 ACP 会话更新传播
  • OpenCode 的错误通过 OpenCodeApiException 抛出
  • 建议在调用方捕获并处理这些异常,毕竟错误总会发生的
性能考虑


  • IFlow 的 WebSocket 通信比 HTTP 有更低的延迟
  • OpenCode 的会话复用可以减少 HTTP 请求开销
  • Factory 的缓存机制可以避免重复创建 provider
  • 高并发场景下,要关注进程数和连接数的限制,别到时候撑不住了
配置验证

启动时会验证可执行文件路径,但运行时也可能出问题。PingAsync 是个好工具,可以验证配置是否正确:
  1. // 启动时检查
  2. var provider = await _providerFactory.GetProviderAsync(providerType);
  3. var result = await provider.PingAsync(cancellationToken);
  4. if (!result.Success)
  5. {
  6.     _logger.LogError("Provider {ProviderType} 不可用: {Error}", providerType, result.ErrorMessage);
  7. }
复制代码
总结

本文分享了 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/
  • 公测已开始,欢迎安装体验

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册