找回密码
 立即注册
首页 业界区 业界 Hagicode 多 AI 提供者切换与互操作实现方案 ...

Hagicode 多 AI 提供者切换与互操作实现方案

端木茵茵 昨天 10:55
Hagicode 多 AI 提供者切换与互操作实现方案

在现代开发工具生态中,开发者经常需要使用不同的 AI 编码助手来辅助开发工作。Anthropic 的 Claude Code CLI 和 OpenAI 的 Codex CLI 各有其优势:Claude 以出色的代码理解和长上下文处理能力著称,而 Codex 在代码生成和工具使用方面表现优异。
本文将深入分析 hagicode 项目如何实现多个 AI 提供者的无缝切换与互操作,包括核心架构设计、关键实现细节以及实践中的注意事项。
背景

问题域

hagicode 项目面临的核心挑战是在同一平台中支持多种 AI CLI,让用户能够:

  • 根据需求灵活切换不同的 AI 提供者
  • 在切换过程中保持会话状态的连续性
  • 统一抽象不同 CLI 的 API 差异
  • 为未来添加新的 AI 提供者预留扩展空间
技术挑战


  • 接口差异统一:Claude Code CLI 通过命令行调用,Codex CLI 使用 JSON 事件流
  • 流式响应处理:两种提供者都支持流式响应,但数据格式不同
  • 工具调用语义:Claude 和 Codex 对工具调用的表示和生命周期管理不同
  • 会话生命周期:需要正确管理每个提供者的会话创建、恢复和终止
分析

架构设计思路

hagicode 采用了提供者模式(Provider Pattern)结合工厂模式来抽象 AI 服务的调用。这种设计的核心思想是:

  • 统一接口抽象:定义 IAIProvider 接口作为所有 AI 提供者的统一抽象
  • 工厂创建实例:通过 AIProviderFactory 根据类型动态创建对应的提供者实例
  • 智能选择逻辑:使用 AIProviderSelector 根据场景和配置自动选择最合适的提供者
  • 会话状态管理:通过数据库持久化会话与 CLI 线程的绑定关系
关键组件

组件职责语言IAIProvider统一提供者接口C#AIProviderFactory创建和管理提供者实例C#AIProviderSelector智能选择提供者C#ClaudeCodeCliProviderClaude Code CLI 实现C#CodexCliProviderCodex CLI 实现C#AgentCliManager桌面端 CLI 管理TypeScript解决

1. 核心接口设计

IAIProvider 接口 定义了统一的提供者抽象:
  1. public interface IAIProvider
  2. {
  3.     /// <summary>
  4.     /// 提供者显示名称
  5.     /// </summary>
  6.     string Name { get; }
  7.     /// <summary>
  8.     /// 是否支持流式响应
  9.     /// </summary>
  10.     bool SupportsStreaming { get; }
  11.     /// <summary>
  12.     /// 提供者能力描述
  13.     /// </summary>
  14.     ProviderCapabilities Capabilities { get; }
  15.     /// <summary>
  16.     /// 执行单个 AI 请求
  17.     /// </summary>
  18.     Task ExecuteAsync(AIRequest request, CancellationToken cancellationToken = default);
  19.     /// <summary>
  20.     /// 执行流式 AI 请求
  21.     /// </summary>
  22.     IAsyncEnumerable StreamAsync(AIRequest request, CancellationToken cancellationToken = default);
  23.     /// <summary>
  24.     /// 检查提供者连接性和响应速度
  25.     /// </summary>
  26.     Task<ProviderTestResult> PingAsync(CancellationToken cancellationToken = default);
  27.     /// <summary>
  28.     /// 发送带嵌入式命令的消息
  29.     /// </summary>
  30.     IAsyncEnumerable SendMessageAsync(
  31.         AIRequest request,
  32.         string? embeddedCommandPrompt = null,
  33.         CancellationToken cancellationToken = default);
  34. }
复制代码
接口设计的关键特性:

  • 统一的请求/响应模型:所有提供者使用相同的 AIRequest 和 AIResponse 类型
  • 流式支持:通过 IAsyncEnumerable 统一流式输出
  • 能力描述:ProviderCapabilities 描述提供者支持的功能(流式、工具、最大 token 等)
  • 嵌入式命令:SendMessageAsync 支持将 OpenSpec 命令嵌入到提示中
2. 提供者类型枚举
  1. public enum AIProviderType
  2. {
  3.     ClaudeCodeCli,   // Anthropic Claude Code
  4.     OpenCodeCli,     // 其他 CLI(可扩展)
  5.     GitHubCopilot,    // GitHub Copilot
  6.     CodebuddyCli,    // Codebuddy
  7.     CodexCli         // OpenAI Codex
  8. }
复制代码
这个枚举为系统支持的所有提供者提供了类型安全的表示。
3. 工厂模式实现

AIProviderFactory 负责创建和管理提供者实例:
  1. public class AIProviderFactory : IAIProviderFactory
  2. {
  3.     private readonly ConcurrentDictionary _cache;
  4.     private readonly IOptions _options;
  5.     private readonly IServiceProvider _serviceProvider;
  6.     public Task<IAIProvider?> GetProviderAsync(AIProviderType providerType)
  7.     {
  8.         // 使用缓存避免重复创建
  9.         if (_cache.TryGetValue(providerType, out var cached))
  10.             return Task.FromResult<IAIProvider?>(cached);
  11.         // 从配置中获取提供者配置
  12.         var aiOptions = _options.Value;
  13.         if (!aiOptions.Providers.TryGetValue(providerType, out var config))
  14.         {
  15.             _logger.LogWarning("Provider '{ProviderType}' not found in configuration", providerType);
  16.             return Task.FromResult<IAIProvider?>(null);
  17.         }
  18.         // 根据类型创建提供者
  19.         var provider = providerType switch
  20.         {
  21.             AIProviderType.ClaudeCodeCli =>
  22.                 _serviceProvider.GetService(typeof(ClaudeCodeCliProvider)) as IAIProvider,
  23.             AIProviderType.CodexCli =>
  24.                 _serviceProvider.GetService(typeof(CodexCliProvider)) as IAIProvider,
  25.             AIProviderType.GitHubCopilot =>
  26.                 _serviceProvider.GetService(typeof(CopilotAIProvider)) as IAIProvider,
  27.             _ => null
  28.         };
  29.         if (provider != null)
  30.         {
  31.             _cache[providerType] = provider;
  32.         }
  33.         return Task.FromResult<IAIProvider?>(provider);
  34.     }
  35. }
复制代码
工厂模式的优势:

  • 实例缓存:避免重复创建相同类型的提供者
  • 依赖注入:通过 IServiceProvider 创建实例,支持依赖注入
  • 配置驱动:从配置文件读取提供者配置
  • 异常处理:创建失败时返回 null,便于上层处理
4. 智能选择器

AIProviderSelector 实现提供者选择策略:
  1. public class AIProviderSelector : IAIProviderSelector
  2. {
  3.     private readonly BusinessLayerConfiguration _configuration;
  4.     private readonly IAIProviderFactory _providerFactory;
  5.     private readonly IMemoryCache _cache;
  6.     public async Task SelectProviderAsync(
  7.         BusinessScenario scenario,
  8.         CancellationToken cancellationToken = default)
  9.     {
  10.         // 1. 尝试从场景映射获取提供者
  11.         if (_configuration.ScenarioProviderMapping.TryGetValue(scenario, out var providerType))
  12.         {
  13.             if (await IsProviderAvailableAsync(providerType, cancellationToken))
  14.             {
  15.                 _logger.LogDebug("Selected provider '{Provider}' for scenario '{Scenario}'",
  16.                     providerType, scenario);
  17.                 return providerType;
  18.             }
  19.             _logger.LogWarning("Configured provider '{Provider}' for scenario '{Scenario}' is not available",
  20.                 providerType, scenario);
  21.         }
  22.         // 2. 尝试使用默认提供者
  23.         if (await IsProviderAvailableAsync(_configuration.DefaultProvider, cancellationToken))
  24.         {
  25.             _logger.LogDebug("Using default provider '{Provider}' for scenario '{Scenario}'",
  26.                 _configuration.DefaultProvider, scenario);
  27.             return _configuration.DefaultProvider;
  28.         }
  29.         // 3. 尝试回退链
  30.         foreach (var fallbackProvider in _configuration.FallbackChain)
  31.         {
  32.             if (await IsProviderAvailableAsync(fallbackProvider, cancellationToken))
  33.             {
  34.                 _logger.LogInformation("Using fallback provider '{Provider}' for scenario '{Scenario}'",
  35.                     fallbackProvider, scenario);
  36.                 return fallbackProvider;
  37.             }
  38.         }
  39.         // 4. 无法找到可用提供者
  40.         throw new InvalidOperationException(
  41.             $"No available AI provider found for scenario '{scenario}'");
  42.     }
  43.     public async Task<bool> IsProviderAvailableAsync(
  44.         AIProviderType providerType,
  45.         CancellationToken cancellationToken = default)
  46.     {
  47.         var cacheKey = $"provider_available_{providerType}";
  48.         // 使用缓存减少 Ping 调用
  49.         if (_configuration.EnableCache &&
  50.             _cache.TryGetValue<bool>(cacheKey, out var cached))
  51.         {
  52.             return cached;
  53.         }
  54.         var provider = await _providerFactory.GetProviderAsync(providerType);
  55.         var isAvailable = provider != null;
  56.         if (_configuration.EnableCache && isAvailable)
  57.         {
  58.             _cache.Set(cacheKey, isAvailable,
  59.                 TimeSpan.FromSeconds(_configuration.CacheExpirationSeconds));
  60.         }
  61.         return isAvailable;
  62.     }
  63. }
复制代码
选择器策略:

  • 场景映射优先:首先检查业务场景是否有特定的提供者映射
  • 默认提供者回退:场景映射失败时使用默认提供者
  • 回退链兜底:逐个尝试回退链中的提供者
  • 可用性缓存:缓存提供者可用性检查结果,减少 Ping 调用
5. Claude Code CLI 提供者实现
  1. public class ClaudeCodeCliProvider : IAIProvider
  2. {
  3.     private readonly ILogger<ClaudeCodeCliProvider> _logger;
  4.     private readonly IClaudeStreamManager _streamManager;
  5.     private readonly ProviderConfiguration _config;
  6.     public string Name => "ClaudeCodeCli";
  7.     public bool SupportsStreaming => true;
  8.     public ProviderCapabilities Capabilities { get; }
  9.     public async Task ExecuteAsync(AIRequest request, CancellationToken cancellationToken = default)
  10.     {
  11.         _logger.LogInformation("Executing AI request with provider: {Provider}", Name);
  12.         var sessionOptions = ClaudeRequestMapper.MapToSessionOptions(request, _config);
  13.         var messages = _streamManager.SendMessageAsync(request.Prompt, sessionOptions, cancellationToken);
  14.         var responseBuilder = new StringBuilder();
  15.         ResultMessage? finalResult = null;
  16.         await foreach (var streamMessage in messages)
  17.         {
  18.             switch (streamMessage.Message)
  19.             {
  20.                 case ResultMessage result:
  21.                     finalResult = result;
  22.                     responseBuilder.Append(result.Result);
  23.                     break;
  24.             }
  25.         }
  26.         if (finalResult != null)
  27.         {
  28.             return ClaudeResponseMapper.MapToAIResponse(finalResult, Name);
  29.         }
  30.         return new AIResponse
  31.         {
  32.             Content = responseBuilder.ToString(),
  33.             FinishReason = FinishReason.Unknown,
  34.             Provider = Name
  35.         };
  36.     }
  37. }
复制代码
Claude Code CLI 提供者的特点:

  • 流式管理器集成:使用 IClaudeStreamManager 与 Claude CLI 通信
  • CessionId 会话隔离:使用 CessionId 作为会话唯一标识,与系统 sessionId 区分
  • 工作目录配置:支持配置工作目录、权限模式等
  • 工具支持:支持 AllowedTools、DisallowedTools 等工具权限配置
6. Codex CLI 提供者实现
  1. public class CodexCliProvider : IAIProvider
  2. {
  3.     private readonly ILogger _logger;
  4.     private readonly CodexSettings _settings;
  5.     private readonly ConcurrentDictionary<string, string> _sessionThreadBindings;
  6.     public string Name => "CodexCli";
  7.     public bool SupportsStreaming => true;
  8.     public ProviderCapabilities Capabilities { get; }
  9.     public async IAsyncEnumerable StreamAsync(
  10.         AIRequest request,
  11.         [EnumeratorCancellation] CancellationToken cancellationToken = default)
  12.     {
  13.         _logger.LogInformation("Executing streaming AI request with provider: {Provider}", Name);
  14.         var codex = CreateCodexClient();
  15.         var thread = ResolveThread(codex, request);
  16.         var currentTurn = 0;
  17.         var activeToolCalls = new Dictionary<string, AIToolCallDelta>();
  18.         await foreach (var threadEvent in thread.RunStreamedAsync(BuildPrompt(request), cancellationToken))
  19.         {
  20.             if (threadEvent is TurnStartedEvent)
  21.             {
  22.                 currentTurn++;
  23.             }
  24.             switch (threadEvent)
  25.             {
  26.                 case ItemCompletedEvent { Item: AgentMessageItem message }:
  27.                     var messageText = message.Text ?? string.Empty;
  28.                     yield return new AIStreamingChunk
  29.                     {
  30.                         Content = messageText,
  31.                         Type = StreamingChunkType.ContentDelta,
  32.                         IsComplete = false
  33.                     };
  34.                     break;
  35.                 case ItemStartedEvent or ItemUpdatedEvent or ItemCompletedEvent:
  36.                     var toolChunk = BuildToolChunk(threadEvent, currentTurn);
  37.                     if (toolChunk?.ToolCallDelta != null)
  38.                     {
  39.                         yield return toolChunk;
  40.                     }
  41.                     break;
  42.                 case TurnCompletedEvent turnCompleted:
  43.                     activeToolCalls.Clear();
  44.                     yield return new AIStreamingChunk
  45.                     {
  46.                         Content = string.Empty,
  47.                         Type = StreamingChunkType.Metadata,
  48.                         IsComplete = true,
  49.                         Usage = MapUsage(turnCompleted.Usage)
  50.                     };
  51.                     break;
  52.             }
  53.         }
  54.         BindSessionThread(request.SessionId, thread.Id);
  55.     }
  56.     private CodexThread ResolveThread(Codex codex, AIRequest request)
  57.     {
  58.         var sessionId = request.SessionId;
  59.         // 检查是否已有绑定的线程
  60.         if (!string.IsNullOrWhiteSpace(sessionId) &&
  61.             _sessionThreadBindings.TryGetValue(sessionId, out var threadId) &&
  62.             !string.IsNullOrWhiteSpace(threadId))
  63.         {
  64.             _logger.LogInformation("Resuming Codex thread {ThreadId} for session {SessionId}", threadId, sessionId);
  65.             return codex.ResumeThread(threadId, threadOptions);
  66.         }
  67.         _logger.LogInformation("Starting new Codex thread for session {SessionId}", sessionId ?? "(none)");
  68.         return codex.StartThread(threadOptions);
  69.     }
  70. }
复制代码
Codex CLI 提供者的特点:

  • JSON 事件流处理:解析 Codex 的 JSON 事件流(TurnStarted、ItemStarted、TurnCompleted 等)
  • 会话线程绑定:使用 SQLite 数据库持久化会话与线程的绑定关系
  • 线程复用:支持恢复已有线程,保持会话连续性
  • 工具调用追踪:追踪活动工具调用状态,正确处理工具生命周期
7. 会话线程绑定机制

Codex CLI 使用 SQLite 数据库持久化会话与线程的绑定:
  1. public class CodexCliProvider : IAIProvider
  2. {
  3.     private const int SessionThreadBindingRetentionDays = 30;
  4.     private readonly ConcurrentDictionary<string, string> _sessionThreadBindings;
  5.     private readonly string _sessionThreadBindingDatabaseConnectionString;
  6.     private readonly string _sessionThreadBindingDatabasePath;
  7.     private void BindSessionThread(string? sessionId, string? threadId)
  8.     {
  9.         if (string.IsNullOrWhiteSpace(sessionId) || string.IsNullOrWhiteSpace(threadId))
  10.         {
  11.             return;
  12.         }
  13.         // 内存缓存
  14.         _sessionThreadBindings.AddOrUpdate(sessionId, threadId, (_, _) => threadId);
  15.         // 持久化到 SQLite
  16.         PersistSessionThreadBinding(sessionId, threadId);
  17.     }
  18.     private void PersistSessionThreadBinding(string sessionId, string threadId)
  19.     {
  20.         try
  21.         {
  22.             using var connection = new SqliteConnection(_sessionThreadBindingDatabaseConnectionString);
  23.             connection.Open();
  24.             using var upsertCommand = connection.CreateCommand();
  25.             upsertCommand.CommandText =
  26.                 """
  27.                 INSERT INTO SessionThreadBindings (SessionId, ThreadId, CreatedAtUtc, UpdatedAtUtc)
  28.                 VALUES ($sessionId, $threadId, $createdAtUtc, $updatedAtUtc)
  29.                 ON CONFLICT(SessionId) DO UPDATE SET
  30.                     ThreadId = excluded.ThreadId,
  31.                     UpdatedAtUtc = excluded.UpdatedAtUtc;
  32.                 """;
  33.             var nowUtc = DateTimeOffset.UtcNow.ToString("O");
  34.             upsertCommand.Parameters.AddWithValue("$sessionId", sessionId);
  35.             upsertCommand.Parameters.AddWithValue("$threadId", threadId);
  36.             upsertCommand.Parameters.AddWithValue("$createdAtUtc", nowUtc);
  37.             upsertCommand.Parameters.AddWithValue("$updatedAtUtc", nowUtc);
  38.             upsertCommand.ExecuteNonQuery();
  39.         }
  40.         catch (Exception ex)
  41.         {
  42.             _logger.LogWarning(
  43.                 ex,
  44.                 "Failed to persist Codex session-thread binding for session {SessionId} to {DatabasePath}",
  45.                 sessionId,
  46.                 _sessionThreadBindingDatabasePath);
  47.         }
  48.     }
  49.     private void LoadPersistedSessionThreadBindings()
  50.     {
  51.         using var connection = new SqliteConnection(_sessionThreadBindingDatabaseConnectionString);
  52.         connection.Open();
  53.         using var loadCommand = connection.CreateCommand();
  54.         loadCommand.CommandText = "SELECT SessionId, ThreadId FROM SessionThreadBindings;";
  55.         using var reader = loadCommand.ExecuteReader();
  56.         while (reader.Read())
  57.         {
  58.             var sessionId = reader.GetString(0);
  59.             var threadId = reader.GetString(1);
  60.             _sessionThreadBindings[sessionId] = threadId;
  61.         }
  62.     }
  63. }
复制代码
会话线程绑定的优势:

  • 会话恢复:系统重启后可以恢复之前的会话
  • 线程复用:同一会话可以复用已有的 Codex 线程
  • 自动清理:超过 30 天的绑定会被自动清理
8. 桌面端 CLI 管理

hagicode-desktop 通过 AgentCliManager 管理 CLI 选择:
  1. export enum AgentCliType {
  2.   ClaudeCode = 'claude-code',
  3.   Codex = 'codex',
  4.   // 未来可扩展: Aider, Cursor 等其他 CLI
  5. }
  6. export class AgentCliManager {
  7.   private static readonly STORE_KEY = 'agentCliSelection';
  8.   private static readonly EXECUTOR_TYPE_MAP: Record = {
  9.     [AgentCliType.ClaudeCode]: 'ClaudeCodeCli',
  10.     [AgentCliType.Codex]: 'CodexCli',
  11.   };
  12.   constructor(private store: any) {}
  13.   async saveSelection(cliType: AgentCliType): Promise<void> {
  14.     const selection: StoredAgentCliSelection = {
  15.       cliType,
  16.       isSkipped: false,
  17.       selectedAt: new Date().toISOString(),
  18.     };
  19.     this.store.set(AgentCliManager.STORE_KEY, selection);
  20.   }
  21.   loadSelection(): StoredAgentCliSelection {
  22.     return this.store.get(AgentCliManager.STORE_KEY, {
  23.       cliType: null,
  24.       isSkipped: false,
  25.       selectedAt: null,
  26.     });
  27.   }
  28.   getCommandName(cliType: AgentCliType): string {
  29.     switch (cliType) {
  30.       case AgentCliType.ClaudeCode:
  31.         return 'claude';
  32.       case AgentCliType.Codex:
  33.         return 'codex';
  34.       default:
  35.         return 'claude';
  36.     }
  37.   }
  38.   getExecutorType(cliType: AgentCliType | null): string {
  39.     if (!cliType) return 'ClaudeCodeCli';
  40.     return this.EXECUTOR_TYPE_MAP[cliType] || 'ClaudeCodeCli';
  41.   }
  42. }
复制代码
桌面端 IPC 处理器示例:
  1. ipcMain.handle('llm:call-api', async (event, manifestPath, region) => {
  2.   if (!state.llmInstallationManager) {
  3.     return { success: false, error: 'LLM Installation Manager not initialized' };
  4.   }
  5.   try {
  6.     const prompt = await state.llmInstallationManager.loadPrompt(manifestPath, region);
  7.     // 根据用户选择确定 CLI 命令
  8.     let commandName = 'claude';
  9.     if (state.agentCliManager) {
  10.       const selectedCliType = state.agentCliManager.getSelectedCliType();
  11.       if (selectedCliType) {
  12.         commandName = state.agentCliManager.getCommandName(selectedCliType);
  13.       }
  14.     }
  15.     // 使用对应的 CLI 执行
  16.     const result = await state.llmInstallationManager.callApi(
  17.       prompt.filePath,
  18.       event.sender,
  19.       commandName
  20.     );
  21.     return result;
  22.   } catch (error) {
  23.     return {
  24.       success: false,
  25.       error: error instanceof Error ? error.message : 'Unknown error'
  26.     };
  27.   }
  28. });
复制代码
9. Codex 内部的模型提供者系统

Codex 本身也支持多种模型提供者,通过 ModelProviderInfo 配置:
  1. pub const OPENAI_PROVIDER_NAME: &str = "OpenAI";
  2. pub const OLLAMA_OSS_PROVIDER_ID: &str = "ollama";
  3. pub const LMSTUDIO_OSS_PROVIDER_ID: &str = "lmstudio";
  4. pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
  5.     use ModelProviderInfo as P;
  6.     [
  7.         ("openai", P::create_openai_provider()),
  8.         (OLLAMA_OSS_PROVIDER_ID, create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Responses)),
  9.         (LMSTUDIO_OSS_PROVIDER_ID, create_oss_provider(DEFAULT_LMSTUDIO_PORT, WireApi::Responses)),
  10.     ]
  11.     .into_iter()
  12.     .map(|(k, v)| (k.to_string(), v))
  13.     .collect()
  14. }
  15. pub struct ModelProviderInfo {
  16.     pub name: String,
  17.     pub base_url: Option<String>,
  18.     pub env_key: Option<String>,
  19.     pub query_params: Option<HashMap<String, String>>,
  20.     pub http_headers: Option<HashMap<String, String>>,
  21.     pub request_max_retries: Option<u64>,
  22.     pub stream_max_retries: Option<u64>,
  23.     pub stream_idle_timeout_ms: Option<u64>,
  24.     pub requires_openai_auth: bool,
  25.     pub supports_websockets: bool,
  26. }
复制代码
Codex 的模型提供者支持:

  • 内置提供者:OpenAI、Ollama、LM Studio
  • 自定义提供者:用户可在 config.toml 中添加自定义提供者
  • 重试策略:可配置请求和流的重试次数
  • WebSocket 支持:部分提供者支持 WebSocket 传输
实践

配置示例

appsettings.json 配置多个提供者:
  1. {
  2.   "AI": {
  3.     "Providers": {
  4.       "DefaultProvider": "ClaudeCodeCli",
  5.       "Providers": {
  6.         "ClaudeCodeCli": {
  7.           "Type": "ClaudeCodeCli",
  8.           "Model": "claude-sonnet-4-20250514",
  9.           "WorkingDirectory": "/path/to/workspace",
  10.           "PermissionMode": "acceptEdits",
  11.           "AllowedTools": ["file-edit", "command-run", "bash"]
  12.         },
  13.         "CodexCli": {
  14.           "Type": "CodexCli",
  15.           "Model": "gpt-4.1",
  16.           "ExecutablePath": "codex",
  17.           "SandboxMode": "enabled",
  18.           "WebSearchMode": "auto",
  19.           "NetworkAccessEnabled": false
  20.         }
  21.       },
  22.       "ScenarioProviderMapping": {
  23.         "CodeAnalysis": "ClaudeCodeCli",
  24.         "CodeGeneration": "CodexCli",
  25.         "Refactoring": "ClaudeCodeCli",
  26.         "Debugging": "CodexCli"
  27.       },
  28.       "FallbackChain": ["CodexCli", "ClaudeCodeCli"]
  29.     },
  30.     "Selector": {
  31.       "EnableCache": true,
  32.       "CacheExpirationSeconds": 300
  33.     }
  34.   }
  35. }
复制代码
使用示例 - 后端服务
  1. public class AIOrchestrator
  2. {
  3.     private readonly IAIProviderFactory _providerFactory;
  4.     private readonly IAIProviderSelector _providerSelector;
  5.     private readonly ILogger _logger;
  6.     public AIOrchestrator(
  7.         IAIProviderFactory providerFactory,
  8.         IAIProviderSelector providerSelector,
  9.         ILogger logger)
  10.     {
  11.         _providerFactory = providerFactory;
  12.         _providerSelector = providerSelector;
  13.         _logger = logger;
  14.     }
  15.     public async Task ProcessRequestAsync(
  16.         AIRequest request,
  17.         BusinessScenario scenario)
  18.     {
  19.         _logger.LogInformation("Processing request for scenario: {Scenario}", scenario);
  20.         try
  21.         {
  22.             // 智能选择提供者
  23.             var providerType = await _providerSelector.SelectProviderAsync(scenario, request.CancellationToken);
  24.             // 获取提供者实例
  25.             var provider = await _providerFactory.GetProviderAsync(providerType);
  26.             if (provider == null)
  27.             {
  28.                 throw new InvalidOperationException($"Provider {providerType} not available");
  29.             }
  30.             _logger.LogInformation("Using provider: {Provider} for request", provider.Name);
  31.             // 执行请求
  32.             var response = await provider.ExecuteAsync(request, request.CancellationToken);
  33.             _logger.LogInformation("Request completed with provider: {Provider}, tokens used: {Tokens}",
  34.                 provider.Name,
  35.                 response.Usage?.TotalTokens ?? 0);
  36.             return response;
  37.         }
  38.         catch (Exception ex)
  39.         {
  40.             _logger.LogError(ex, "Failed to process request for scenario: {Scenario}", scenario);
  41.             throw;
  42.         }
  43.     }
  44. }
复制代码
使用示例 - 流式响应
  1. public async IAsyncEnumerable StreamResponseAsync(
  2.     AIRequest request,
  3.     BusinessScenario scenario)
  4. {
  5.     var providerType = await _providerSelector.SelectProviderAsync(scenario);
  6.     var provider = await _providerFactory.GetProviderAsync(providerType);
  7.     if (provider == null)
  8.     {
  9.         throw new InvalidOperationException($"Provider {providerType} not available");
  10.     }
  11.     await foreach (var chunk in provider.StreamAsync(request))
  12.     {
  13.         // 处理流式块
  14.         switch (chunk.Type)
  15.         {
  16.             case StreamingChunkType.ContentDelta:
  17.                 // 实时显示文本内容
  18.                 await SendToClientAsync(chunk.Content);
  19.                 break;
  20.             case StreamingChunkType.ToolCallDelta:
  21.                 // 处理工具调用
  22.                 await HandleToolCallAsync(chunk.ToolCallDelta);
  23.                 break;
  24.             case StreamingChunkType.Metadata:
  25.                 // 处理完成事件和统计
  26.                 if (chunk.IsComplete)
  27.                 {
  28.                     _logger.LogInformation("Stream completed, usage: {@Usage}", chunk.Usage);
  29.                 }
  30.                 break;
  31.             case StreamingChunkType.Error:
  32.                 // 处理错误
  33.                 _logger.LogError("Stream error: {Error}", chunk.ErrorMessage);
  34.                 throw new InvalidOperationException(chunk.ErrorMessage);
  35.         }
  36.     }
  37. }
复制代码
使用示例 - OpenSpec 命令
  1. public async Task<string> ExecuteOpenSpecCommandAsync(
  2.     string command,
  3.     string arguments,
  4.     BusinessScenario scenario)
  5. {
  6.     var providerType = await _providerSelector.SelectProviderAsync(scenario);
  7.     var provider = await _providerFactory.GetProviderAsync(providerType);
  8.     // 构建嵌入式命令提示
  9.     var commandPrompt = $"""
  10.         Execute the following OpenSpec command:
  11.         Command: {command}
  12.         Arguments: {arguments}
  13.         Please execute this command and return the results.
  14.         """;
  15.     var request = new AIRequest
  16.     {
  17.         Prompt = "Process this command request",
  18.         EmbeddedCommandPrompt = commandPrompt,
  19.         WorkingDirectory = Directory.GetCurrentDirectory()
  20.     };
  21.     var response = await provider.SendMessageAsync(request, commandPrompt);
  22.     return response.Content;
  23. }
复制代码
注意事项

1. 提供者健康检查

在切换提供者前,建议先调用 PingAsync 确保目标提供者可用:
  1. public async Task<bool> IsProviderHealthyAsync(AIProviderType providerType)
  2. {
  3.     var provider = await _providerFactory.GetProviderAsync(providerType);
  4.     if (provider == null) return false;
  5.     var testResult = await provider.PingAsync();
  6.     return testResult.Success &&
  7.            testResult.ResponseTimeMs < 5000; // 5 秒内响应视为健康
  8. }
复制代码
2. 会话隔离

使用 CessionId(Claude)或 ThreadId(Codex)确保会话隔离:

  • Claude Code CLI:使用 CessionId 作为会话唯一标识
  • Codex CLI:使用 ThreadId 作为会话标识
  1. // Claude Code CLI 会话选项
  2. var claudeSessionOptions = new ClaudeSessionOptions
  3. {
  4.     CessionId = CessionId.New(),  // 生成唯一 ID
  5.     WorkingDirectory = workspacePath,
  6.     AllowedTools = allowedTools,
  7.     PermissionMode = PermissionMode.acceptEdits
  8. };
  9. // Codex 线程选项
  10. var codexThreadOptions = new ThreadOptions
  11. {
  12.     Model = "gpt-4.1",
  13.     SandboxMode = "enabled",
  14.     WorkingDirectory = workspacePath
  15. };
复制代码
3. 错误处理

提供者不可用时的回退机制要健壮,确保至少有一个可用提供者:
  1. public async Task ExecuteWithFallbackAsync(
  2.     AIRequest request,
  3.     List preferredProviders)
  4. {
  5.     Exception? lastException = null;
  6.     foreach (var providerType in preferredProviders)
  7.     {
  8.         try
  9.         {
  10.             var provider = await _providerFactory.GetProviderAsync(providerType);
  11.             if (provider == null) continue;
  12.             // 尝试执行
  13.             return await provider.ExecuteAsync(request);
  14.         }
  15.         catch (Exception ex)
  16.         {
  17.             _logger.LogWarning(ex, "Provider {ProviderType} failed, trying next", providerType);
  18.             lastException = ex;
  19.         }
  20.     }
  21.     // 所有提供者都失败
  22.     throw new InvalidOperationException(
  23.         "All preferred providers failed. Last error: " + lastException?.Message,
  24.         lastException);
  25. }
复制代码
4. 配置验证

启动时验证所有配置的提供者设置,避免运行时错误:
  1. public void ValidateConfiguration(AIProviderOptions options)
  2. {
  3.     foreach (var (providerType, config) in options.Providers)
  4.     {
  5.         // 验证可执行文件路径(CLI 类型提供者)
  6.         if (IsCliBasedProvider(providerType))
  7.         {
  8.             if (string.IsNullOrWhiteSpace(config.ExecutablePath))
  9.             {
  10.                 throw new ConfigurationException(
  11.                     $"Provider {providerType} requires ExecutablePath");
  12.             }
  13.             if (!File.Exists(config.ExecutablePath))
  14.             {
  15.                 throw new ConfigurationException(
  16.                     $"Executable not found for {providerType}: {config.ExecutablePath}");
  17.             }
  18.         }
  19.         // 验证 API 密钥(API 类型提供者)
  20.         if (IsApiBasedProvider(providerType))
  21.         {
  22.             if (string.IsNullOrWhiteSpace(config.ApiKey))
  23.             {
  24.                 throw new ConfigurationException(
  25.                     $"Provider {providerType} requires ApiKey");
  26.             }
  27.         }
  28.         // 验证模型名称
  29.         if (string.IsNullOrWhiteSpace(config.Model))
  30.         {
  31.             _logger.LogWarning("No model configured for {ProviderType}, using default", providerType);
  32.         }
  33.     }
  34. }
复制代码
5. 缓存管理

提供者实例会被缓存,注意生命周期管理和内存使用:
  1. // 定期清理缓存
  2. public void ClearInactiveProviders(TimeSpan inactiveThreshold)
  3. {
  4.     var now = DateTimeOffset.UtcNow;
  5.     var keysToRemove = new List();
  6.     foreach (var (type, instance) in _cache)
  7.     {
  8.         // 假设提供者有 LastUsedTime 属性
  9.         if (instance.LastUsedTime.HasValue &&
  10.             now - instance.LastUsedTime.Value > inactiveThreshold)
  11.         {
  12.             keysToRemove.Add(type);
  13.         }
  14.     }
  15.     foreach (var key in keysToRemove)
  16.     {
  17.         _cache.TryRemove(key, out _);
  18.         _logger.LogInformation("Cleared inactive provider: {Provider}", key);
  19.     }
  20. }
复制代码
6. 日志记录

详细记录提供者选择、切换和执行过程,便于调试:
  1. public class AIProviderLogging
  2. {
  3.     private readonly ILogger _logger;
  4.     public void LogProviderSelection(
  5.         BusinessScenario scenario,
  6.         AIProviderType selectedProvider,
  7.         SelectionReason reason)
  8.     {
  9.         _logger.LogInformation(
  10.             "[ProviderSelection] Scenario={Scenario}, Provider={Provider}, Reason={Reason}",
  11.             scenario,
  12.             selectedProvider,
  13.             reason);
  14.     }
  15.     public void LogProviderSwitch(
  16.         AIProviderType fromProvider,
  17.         AIProviderType toProvider,
  18.         string reason)
  19.     {
  20.         _logger.LogWarning(
  21.             "[ProviderSwitch] From={FromProvider} To={ToProvider}, Reason={Reason}",
  22.             fromProvider,
  23.             toProvider,
  24.             reason);
  25.     }
  26.     public void LogProviderError(
  27.         AIProviderType provider,
  28.         Exception error,
  29.         AIRequest request)
  30.     {
  31.         _logger.LogError(error,
  32.             "[ProviderError] Provider={Provider}, RequestLength={Length}, Error={Message}",
  33.             provider,
  34.             request.Prompt.Length,
  35.             error.Message);
  36.     }
  37. }
复制代码
7. 线程安全

ConcurrentDictionary 等并发集合的使用确保线程安全:
  1. public class ThreadSafeProviderCache
  2. {
  3.     private readonly ConcurrentDictionary _cache;
  4.     private readonly ReaderWriterLockSlim _lock = new();
  5.     public IAIProvider? GetProvider(AIProviderType type)
  6.     {
  7.         // 读取操作无需锁
  8.         if (_cache.TryGetValue(type, out var provider))
  9.             return provider;
  10.         // 创建需要写锁
  11.         _lock.EnterWriteLock();
  12.         try
  13.         {
  14.             // 双重检查
  15.             if (_cache.TryGetValue(type, out provider))
  16.                 return provider;
  17.             var newProvider = CreateProvider(type);
  18.             if (newProvider != null)
  19.             {
  20.                 _cache[type] = newProvider;
  21.             }
  22.             return newProvider;
  23.         }
  24.         finally
  25.         {
  26.             _lock.ExitWriteLock();
  27.         }
  28.     }
  29. }
复制代码
8. 数据库迁移

会话线程绑定数据库结构变更时需要考虑数据迁移:
  1. public class SessionThreadMigration
  2. {
  3.     public async Task MigrateAsync(string dbPath)
  4.     {
  5.         var version = await GetSchemaVersionAsync(dbPath);
  6.         if (version >= 2) return; // 已是最新版本
  7.         using var connection = new SqliteConnection(dbPath);
  8.         connection.Open();
  9.         // 迁移到 v2:添加 CreatedAtUtc 列
  10.         if (version < 2)
  11.         {
  12.             _logger.LogInformation("Migrating SessionThreadBindings to v2...");
  13.             using var addColumnCommand = connection.CreateCommand();
  14.             addColumnCommand.CommandText = "ALTER TABLE SessionThreadBindings ADD COLUMN CreatedAtUtc TEXT;";
  15.             addColumnCommand.ExecuteNonQuery();
  16.             using var backfillCommand = connection.CreateCommand();
  17.             backfillCommand.CommandText =
  18.                 """
  19.                 UPDATE SessionThreadBindings
  20.                 SET CreatedAtUtc = COALESCE(NULLIF(UpdatedAtUtc, ''), $nowUtc)
  21.                 WHERE CreatedAtUtc IS NULL OR CreatedAtUtc = '';
  22.                 """;
  23.             backfillCommand.Parameters.AddWithValue("$nowUtc", DateTimeOffset.UtcNow.ToString("O"));
  24.             backfillCommand.ExecuteNonQuery();
  25.         }
  26.         await UpdateSchemaVersionAsync(dbPath, 2);
  27.         _logger.LogInformation("Migration to v2 completed");
  28.     }
  29. }
复制代码
总结

hagicode 通过提供者模式、工厂模式和选择器模式的组合,实现了一个灵活、可扩展的多 AI 提供者架构:

  • 统一接口抽象:IAIProvider 接口屏蔽了不同 CLI 的差异
  • 动态实例创建:AIProviderFactory 支持运行时创建提供者实例
  • 智能选择策略:AIProviderSelector 实现场景驱动的提供者选择
  • 会话状态持久化:通过数据库绑定确保会话连续性
  • 桌面端集成:AgentCliManager 支持用户选择和配置
这种架构设计的优势在于:

  • 可扩展性:添加新的 AI 提供者只需实现 IAIProvider 接口
  • 可测试性:提供者可以独立测试和模拟
  • 可维护性:每个提供者的实现独立,职责单一
  • 用户友好:支持场景自动选择和手动切换
通过这种设计,hagicode 成功实现了 Claude Code CLI 和 Codex CLI 的无缝切换与互操作,为开发者提供了灵活、强大的 AI 编码助手体验。
参考资料


  • HagiCode 项目地址:github.com/HagiCode-org/site
  • HagiCode 官网:hagicode.com
  • Claude Code 官方文档:docs.anthropic.com
  • OpenAI Codex 文档:platform.openai.com
  • Codex SDK 官方仓库:github.com/openai/codex
  • HagiCode 多平台 CLI 支持:https://docs.hagicode.com/blog/hagicode-ai-cli-multi-platform-support/

感谢您的阅读,如果您觉得本文有用,快点击下方点赞按钮
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

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