HagiCode多AI Provider架构实践深度评测
HagiCode 多AI供应商架构实践:统一接口适配WebSocket与HTTP
本文详解在Orleans Grain架构下,通过标准化的IAIProvider接口集成iflow与OpenCode两款AI工具的技术方案,并深入对比WebSocket与HTTP两种通信模式在实现层面的关键差异。
背景与动机
开发HagiCode时遇到了一个典型难题:用户群体习惯迥异——有人偏好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供应商所需实现的能力收敛为可互换的契约:
public interface IAIProvider
{
string Name { get; }
bool SupportsStreaming { get; }
ProviderCapabilities Capabilities { get; }
Task ExecuteAsync(AIRequest request, CancellationToken cancellationToken = default);
IAsyncEnumerable StreamAsync(AIRequest request, CancellationToken cancellationToken = default);
Task PingAsync(CancellationToken cancellationToken = default);
IAsyncEnumerable SendMessageAsync(AIRequest request, string? embeddedCommandPrompt = null, CancellationToken cancellationToken = default);
} 接口包含四个核心方法:
ExecuteAsync:执行一次性AI请求,返回完整响应StreamAsync:流式获取响应,支持逐块实时输出PingAsync:健康检查,快速验证供应商可用性SendMessageAsync:发送消息并支持嵌入式命令(如文件引用)
IFlowCliProvider:基于WebSocket的实现
iflow通过WebSocket实现ACP通信,架构分层如下:
IFlowCliProvider → ACPSessionManager → WebSocketAcpTransport → iflow CLI
↓
动态端口分配 + 进程管理核心流程简洁:
ACPSessionManager负责创建与管理ACP会话WebSocketAcpTransport处理WebSocket连接的建立与消息收发- 动态分配空闲端口,以
iflow --experimental-acp --port {port}启动iflow进程 - 通过
IAIRequestToAcpMapper和IAcpToAIResponseMapper完成请求/响应格式转换
核心代码实现:
private async IAsyncEnumerable 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中的
IsComplete标记用于判断响应是否完整,保证数据完整性
OpenCodeCliProvider:基于HTTP API的实现
OpenCode对外暴露HTTP API,架构略有不同:
OpenCodeCliProvider → OpenCodeRuntimeManager → OpenCodeClient → OpenCode HTTP API
↓
OpenCodeProcessManager → opencode 进程管理OpenCode的一大特色是使用SQLite数据库持久化会话绑定关系,支持会话恢复与提示词响应恢复:
private async Task 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持久化:应用重启后绑定关系依然有效,提升可靠性
两种实现方式对比
| 维度 | IFlowCliProvider | OpenCodeCliProvider |
|---|---|---|
| 通信方式 | WebSocket (ACP) | HTTP API |
| 进程管理 | ACPSessionManager | OpenCodeProcessManager |
| 端口分配 | 动态端口 | 无端口(HTTP端口配置) |
| 会话管理 | ACPSession | OpenCodeSession |
| 持久化 | 内存缓存 | SQLite数据库 |
| 启动命令 | iflow --experimental-acp --port {port} | opencode |
| 延迟 | 更低(长连接) | 相对较高(HTTP请求) |
选择依据很简单:WebSocket适合实时性要求高的交互场景;HTTP API则胜在简单易调试。两者无优劣之分,匹配业务需求即可。
实践指南
配置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
// 通过 Factory 获取 provider
var 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
// 通过 Factory 获取 provider
var 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);健康检查
在正式使用前,先通过PingAsync确认供应商是否可用:
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;
}嵌入式命令支持
两个供应商均支持嵌入式命令(如/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);
}注意事项与最佳实践
资源管理
IFlow基于WebSocket长连接,资源管理需格外严谨:
- 务必使用
await using确保会话释放 - 取消操作自动触发进程清理
ACPSessionManager支持配置最大会话数,防止资源耗尽
OpenCode的进程管理相对轻量,OpenCodeRuntimeManager自动处理生命周期,省心不少。
错误处理
两种供应商均具备完善的错误传播机制:
- IFlow通过ACP会话更新通道传递错误
- OpenCode以
OpenCodeApiException形式抛出 - 建议在调用层统一捕获并处理,实现优雅降级
性能优化
- WebSocket长连接使IFlow延迟明显低于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时的技术方案。通过统一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
