HagiCode多AI Provider架构实践深度评测

2026-06-11阅读 0热度 0
人工智能

HagiCode 多AI供应商架构实践:统一接口适配WebSocket与HTTP

本文详解在Orleans Grain架构下,通过标准化的IAIProvider接口集成iflow与OpenCode两款AI工具的技术方案,并深入对比WebSocket与HTTP两种通信模式在实现层面的关键差异。

HagiCode 平台的多 AI Provider 架构实践

背景与动机

开发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
                ↓
         动态端口分配 + 进程管理

核心流程简洁:

  1. ACPSessionManager 负责创建与管理ACP会话
  2. WebSocketAcpTransport 处理WebSocket连接的建立与消息收发
  3. 动态分配空闲端口,以 iflow --experimental-acp --port {port} 启动iflow进程
  4. 通过 IAIRequestToAcpMapperIAcpToAIResponseMapper 完成请求/响应格式转换

核心代码实现:

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持久化:应用重启后绑定关系依然有效,提升可靠性

两种实现方式对比

维度IFlowCliProviderOpenCodeCliProvider
通信方式WebSocket (ACP)HTTP API
进程管理ACPSessionManagerOpenCodeProcessManager
端口分配动态端口无端口(HTTP端口配置)
会话管理ACPSessionOpenCodeSession
持久化内存缓存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两种通信模式的无缝适配,同时向上层业务提供一致的调用体验。

核心设计理念可以概括为:

  1. 定义标准接口抽象
  2. 针对不同实现构建适配层
  3. 通过工厂模式统一管理生命周期

这套架构的扩展性非常出色——未来接入新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
免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

相关阅读

更多
欢迎回来 登录或注册后,可保存提示词和历史记录
登录后可同步收藏、历史记录和常用模板
注册即表示同意服务条款与隐私政策