Hagicode 多 AI 提供者切换与互操作实现方案
Hagicode 多 AI 提供者切换与互操作实现方案
Section titled “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 对工具调用的表示和生命周期管理不同
- 会话生命周期:需要正确管理每个提供者的会话创建、恢复和终止
架构设计思路
Section titled “架构设计思路”hagicode 采用了提供者模式(Provider Pattern)结合工厂模式来抽象 AI 服务的调用。这种设计的核心思想是:
- 统一接口抽象:定义
IAIProvider接口作为所有 AI 提供者的统一抽象 - 工厂创建实例:通过
AIProviderFactory根据类型动态创建对应的提供者实例 - 智能选择逻辑:使用
AIProviderSelector根据场景和配置自动选择最合适的提供者 - 会话状态管理:通过数据库持久化会话与 CLI 线程的绑定关系
| 组件 | 职责 | 语言 |
|---|---|---|
IAIProvider | 统一提供者接口 | C# |
AIProviderFactory | 创建和管理提供者实例 | C# |
AIProviderSelector | 智能选择提供者 | C# |
ClaudeCodeCliProvider | Claude Code CLI 实现 | C# |
CodexCliProvider | Codex CLI 实现 | C# |
AgentCliManager | 桌面端 CLI 管理 | TypeScript |
1. 核心接口设计
Section titled “1. 核心接口设计”IAIProvider 接口 定义了统一的提供者抽象:
public interface IAIProvider{ /// <summary> /// 提供者显示名称 /// </summary> string Name { get; }
/// <summary> /// 是否支持流式响应 /// </summary> bool SupportsStreaming { get; }
/// <summary> /// 提供者能力描述 /// </summary> ProviderCapabilities Capabilities { get; }
/// <summary> /// 执行单个 AI 请求 /// </summary> Task<AIResponse> ExecuteAsync(AIRequest request, CancellationToken cancellationToken = default);
/// <summary> /// 执行流式 AI 请求 /// </summary> IAsyncEnumerable<AIStreamingChunk> StreamAsync(AIRequest request, CancellationToken cancellationToken = default);
/// <summary> /// 检查提供者连接性和响应速度 /// </summary> Task<ProviderTestResult> PingAsync(CancellationToken cancellationToken = default);
/// <summary> /// 发送带嵌入式命令的消息 /// </summary> IAsyncEnumerable<AIStreamingChunk> SendMessageAsync( AIRequest request, string? embeddedCommandPrompt = null, CancellationToken cancellationToken = default);}接口设计的关键特性:
- 统一的请求/响应模型:所有提供者使用相同的
AIRequest和AIResponse类型 - 流式支持:通过
IAsyncEnumerable<AIStreamingChunk>统一流式输出 - 能力描述:
ProviderCapabilities描述提供者支持的功能(流式、工具、最大 token 等) - 嵌入式命令:
SendMessageAsync支持将 OpenSpec 命令嵌入到提示中
2. 提供者类型枚举
Section titled “2. 提供者类型枚举”public enum AIProviderType{ ClaudeCodeCli, // Anthropic Claude Code OpenCodeCli, // 其他 CLI(可扩展) GitHubCopilot, // GitHub Copilot CodebuddyCli, // Codebuddy CodexCli // OpenAI Codex}这个枚举为系统支持的所有提供者提供了类型安全的表示。
3. 工厂模式实现
Section titled “3. 工厂模式实现”AIProviderFactory 负责创建和管理提供者实例:
public class AIProviderFactory : IAIProviderFactory{ private readonly ConcurrentDictionary<AIProviderType, IAIProvider> _cache; private readonly IOptions<AIProviderOptions> _options; private readonly IServiceProvider _serviceProvider;
public Task<IAIProvider?> GetProviderAsync(AIProviderType providerType) { // 使用缓存避免重复创建 if (_cache.TryGetValue(providerType, out var cached)) return Task.FromResult<IAIProvider?>(cached);
// 从配置中获取提供者配置 var aiOptions = _options.Value; if (!aiOptions.Providers.TryGetValue(providerType, out var config)) { _logger.LogWarning("Provider '{ProviderType}' not found in configuration", providerType); return Task.FromResult<IAIProvider?>(null); }
// 根据类型创建提供者 var provider = providerType switch { AIProviderType.ClaudeCodeCli => _serviceProvider.GetService(typeof(ClaudeCodeCliProvider)) as IAIProvider, AIProviderType.CodexCli => _serviceProvider.GetService(typeof(CodexCliProvider)) as IAIProvider, AIProviderType.GitHubCopilot => _serviceProvider.GetService(typeof(CopilotAIProvider)) as IAIProvider, _ => null };
if (provider != null) { _cache[providerType] = provider; }
return Task.FromResult<IAIProvider?>(provider); }}工厂模式的优势:
- 实例缓存:避免重复创建相同类型的提供者
- 依赖注入:通过
IServiceProvider创建实例,支持依赖注入 - 配置驱动:从配置文件读取提供者配置
- 异常处理:创建失败时返回 null,便于上层处理
4. 智能选择器
Section titled “4. 智能选择器”AIProviderSelector 实现提供者选择策略:
public class AIProviderSelector : IAIProviderSelector{ private readonly BusinessLayerConfiguration _configuration; private readonly IAIProviderFactory _providerFactory; private readonly IMemoryCache _cache;
public async Task<AIProviderType> SelectProviderAsync( BusinessScenario scenario, CancellationToken cancellationToken = default) { // 1. 尝试从场景映射获取提供者 if (_configuration.ScenarioProviderMapping.TryGetValue(scenario, out var providerType)) { if (await IsProviderAvailableAsync(providerType, cancellationToken)) { _logger.LogDebug("Selected provider '{Provider}' for scenario '{Scenario}'", providerType, scenario); return providerType; }
_logger.LogWarning("Configured provider '{Provider}' for scenario '{Scenario}' is not available", providerType, scenario); }
// 2. 尝试使用默认提供者 if (await IsProviderAvailableAsync(_configuration.DefaultProvider, cancellationToken)) { _logger.LogDebug("Using default provider '{Provider}' for scenario '{Scenario}'", _configuration.DefaultProvider, scenario); return _configuration.DefaultProvider; }
// 3. 尝试回退链 foreach (var fallbackProvider in _configuration.FallbackChain) { if (await IsProviderAvailableAsync(fallbackProvider, cancellationToken)) { _logger.LogInformation("Using fallback provider '{Provider}' for scenario '{Scenario}'", fallbackProvider, scenario); return fallbackProvider; } }
// 4. 无法找到可用提供者 throw new InvalidOperationException( $"No available AI provider found for scenario '{scenario}'"); }
public async Task<bool> IsProviderAvailableAsync( AIProviderType providerType, CancellationToken cancellationToken = default) { var cacheKey = $"provider_available_{providerType}";
// 使用缓存减少 Ping 调用 if (_configuration.EnableCache && _cache.TryGetValue<bool>(cacheKey, out var cached)) { return cached; }
var provider = await _providerFactory.GetProviderAsync(providerType); var isAvailable = provider != null;
if (_configuration.EnableCache && isAvailable) { _cache.Set(cacheKey, isAvailable, TimeSpan.FromSeconds(_configuration.CacheExpirationSeconds)); }
return isAvailable; }}选择器策略:
- 场景映射优先:首先检查业务场景是否有特定的提供者映射
- 默认提供者回退:场景映射失败时使用默认提供者
- 回退链兜底:逐个尝试回退链中的提供者
- 可用性缓存:缓存提供者可用性检查结果,减少 Ping 调用
5. Claude Code CLI 提供者实现
Section titled “5. Claude Code CLI 提供者实现”public class ClaudeCodeCliProvider : IAIProvider{ private readonly ILogger<ClaudeCodeCliProvider> _logger; private readonly IClaudeStreamManager _streamManager; private readonly ProviderConfiguration _config;
public string Name => "ClaudeCodeCli"; public bool SupportsStreaming => true;
public ProviderCapabilities Capabilities { get; }
public async Task<AIResponse> ExecuteAsync(AIRequest request, CancellationToken cancellationToken = default) { _logger.LogInformation("Executing AI request with provider: {Provider}", Name);
var sessionOptions = ClaudeRequestMapper.MapToSessionOptions(request, _config);
var messages = _streamManager.SendMessageAsync(request.Prompt, sessionOptions, cancellationToken);
var responseBuilder = new StringBuilder(); ResultMessage? finalResult = null;
await foreach (var streamMessage in messages) { switch (streamMessage.Message) { case ResultMessage result: finalResult = result; responseBuilder.Append(result.Result); break; } }
if (finalResult != null) { return ClaudeResponseMapper.MapToAIResponse(finalResult, Name); }
return new AIResponse { Content = responseBuilder.ToString(), FinishReason = FinishReason.Unknown, Provider = Name }; }}Claude Code CLI 提供者的特点:
- 流式管理器集成:使用
IClaudeStreamManager与 Claude CLI 通信 - CessionId 会话隔离:使用
CessionId作为会话唯一标识,与系统 sessionId 区分 - 工作目录配置:支持配置工作目录、权限模式等
- 工具支持:支持 AllowedTools、DisallowedTools 等工具权限配置
6. Codex CLI 提供者实现
Section titled “6. Codex CLI 提供者实现”public class CodexCliProvider : IAIProvider{ private readonly ILogger<CodexCliProvider> _logger; private readonly CodexSettings _settings; private readonly ConcurrentDictionary<string, string> _sessionThreadBindings;
public string Name => "CodexCli"; public bool SupportsStreaming => true;
public ProviderCapabilities Capabilities { get; }
public async IAsyncEnumerable<AIStreamingChunk> StreamAsync( AIRequest request, [EnumeratorCancellation] CancellationToken cancellationToken = default) { _logger.LogInformation("Executing streaming AI request with provider: {Provider}", Name);
var codex = CreateCodexClient(); var thread = ResolveThread(codex, request);
var currentTurn = 0; var activeToolCalls = new Dictionary<string, AIToolCallDelta>();
await foreach (var threadEvent in thread.RunStreamedAsync(BuildPrompt(request), cancellationToken)) { if (threadEvent is TurnStartedEvent) { currentTurn++; }
switch (threadEvent) { case ItemCompletedEvent { Item: AgentMessageItem message }: var messageText = message.Text ?? string.Empty; yield return new AIStreamingChunk { Content = messageText, Type = StreamingChunkType.ContentDelta, IsComplete = false }; break;
case ItemStartedEvent or ItemUpdatedEvent or ItemCompletedEvent: var toolChunk = BuildToolChunk(threadEvent, currentTurn); if (toolChunk?.ToolCallDelta != null) { yield return toolChunk; } break;
case TurnCompletedEvent turnCompleted: activeToolCalls.Clear(); yield return new AIStreamingChunk { Content = string.Empty, Type = StreamingChunkType.Metadata, IsComplete = true, Usage = MapUsage(turnCompleted.Usage) }; break; } }
BindSessionThread(request.SessionId, thread.Id); }
private CodexThread ResolveThread(Codex codex, AIRequest request) { var sessionId = request.SessionId;
// 检查是否已有绑定的线程 if (!string.IsNullOrWhiteSpace(sessionId) && _sessionThreadBindings.TryGetValue(sessionId, out var threadId) && !string.IsNullOrWhiteSpace(threadId)) { _logger.LogInformation("Resuming Codex thread {ThreadId} for session {SessionId}", threadId, sessionId); return codex.ResumeThread(threadId, threadOptions); }
_logger.LogInformation("Starting new Codex thread for session {SessionId}", sessionId ?? "(none)"); return codex.StartThread(threadOptions); }}Codex CLI 提供者的特点:
- JSON 事件流处理:解析 Codex 的 JSON 事件流(TurnStarted、ItemStarted、TurnCompleted 等)
- 会话线程绑定:使用 SQLite 数据库持久化会话与线程的绑定关系
- 线程复用:支持恢复已有线程,保持会话连续性
- 工具调用追踪:追踪活动工具调用状态,正确处理工具生命周期
7. 会话线程绑定机制
Section titled “7. 会话线程绑定机制”Codex CLI 使用 SQLite 数据库持久化会话与线程的绑定:
public class CodexCliProvider : IAIProvider{ private const int SessionThreadBindingRetentionDays = 30; private readonly ConcurrentDictionary<string, string> _sessionThreadBindings; private readonly string _sessionThreadBindingDatabaseConnectionString; private readonly string _sessionThreadBindingDatabasePath;
private void BindSessionThread(string? sessionId, string? threadId) { if (string.IsNullOrWhiteSpace(sessionId) || string.IsNullOrWhiteSpace(threadId)) { return; }
// 内存缓存 _sessionThreadBindings.AddOrUpdate(sessionId, threadId, (_, _) => threadId);
// 持久化到 SQLite PersistSessionThreadBinding(sessionId, threadId); }
private void PersistSessionThreadBinding(string sessionId, string threadId) { try { using var connection = new SqliteConnection(_sessionThreadBindingDatabaseConnectionString); connection.Open();
using var upsertCommand = connection.CreateCommand(); upsertCommand.CommandText = """ INSERT INTO SessionThreadBindings (SessionId, ThreadId, CreatedAtUtc, UpdatedAtUtc) VALUES ($sessionId, $threadId, $createdAtUtc, $updatedAtUtc) ON CONFLICT(SessionId) DO UPDATE SET ThreadId = excluded.ThreadId, UpdatedAtUtc = excluded.UpdatedAtUtc; """; var nowUtc = DateTimeOffset.UtcNow.ToString("O"); upsertCommand.Parameters.AddWithValue("$sessionId", sessionId); upsertCommand.Parameters.AddWithValue("$threadId", threadId); upsertCommand.Parameters.AddWithValue("$createdAtUtc", nowUtc); upsertCommand.Parameters.AddWithValue("$updatedAtUtc", nowUtc); upsertCommand.ExecuteNonQuery(); } catch (Exception ex) { _logger.LogWarning( ex, "Failed to persist Codex session-thread binding for session {SessionId} to {DatabasePath}", sessionId, _sessionThreadBindingDatabasePath); } }
private void LoadPersistedSessionThreadBindings() { using var connection = new SqliteConnection(_sessionThreadBindingDatabaseConnectionString); connection.Open();
using var loadCommand = connection.CreateCommand(); loadCommand.CommandText = "SELECT SessionId, ThreadId FROM SessionThreadBindings;"; using var reader = loadCommand.ExecuteReader(); while (reader.Read()) { var sessionId = reader.GetString(0); var threadId = reader.GetString(1); _sessionThreadBindings[sessionId] = threadId; } }}会话线程绑定的优势:
- 会话恢复:系统重启后可以恢复之前的会话
- 线程复用:同一会话可以复用已有的 Codex 线程
- 自动清理:超过 30 天的绑定会被自动清理
8. 桌面端 CLI 管理
Section titled “8. 桌面端 CLI 管理”hagicode-desktop 通过 AgentCliManager 管理 CLI 选择:
export enum AgentCliType { ClaudeCode = 'claude-code', Codex = 'codex', // 未来可扩展: Aider, Cursor 等其他 CLI}
export class AgentCliManager { private static readonly STORE_KEY = 'agentCliSelection'; private static readonly EXECUTOR_TYPE_MAP: Record<AgentCliType, string> = { [AgentCliType.ClaudeCode]: 'ClaudeCodeCli', [AgentCliType.Codex]: 'CodexCli', };
constructor(private store: any) {}
async saveSelection(cliType: AgentCliType): Promise<void> { const selection: StoredAgentCliSelection = { cliType, isSkipped: false, selectedAt: new Date().toISOString(), };
this.store.set(AgentCliManager.STORE_KEY, selection); }
loadSelection(): StoredAgentCliSelection { return this.store.get(AgentCliManager.STORE_KEY, { cliType: null, isSkipped: false, selectedAt: null, }); }
getCommandName(cliType: AgentCliType): string { switch (cliType) { case AgentCliType.ClaudeCode: return 'claude'; case AgentCliType.Codex: return 'codex'; default: return 'claude'; } }
getExecutorType(cliType: AgentCliType | null): string { if (!cliType) return 'ClaudeCodeCli'; return this.EXECUTOR_TYPE_MAP[cliType] || 'ClaudeCodeCli'; }}桌面端 IPC 处理器示例:
ipcMain.handle('llm:call-api', async (event, manifestPath, region) => { if (!state.llmInstallationManager) { return { success: false, error: 'LLM Installation Manager not initialized' }; }
try { const prompt = await state.llmInstallationManager.loadPrompt(manifestPath, region);
// 根据用户选择确定 CLI 命令 let commandName = 'claude'; if (state.agentCliManager) { const selectedCliType = state.agentCliManager.getSelectedCliType(); if (selectedCliType) { commandName = state.agentCliManager.getCommandName(selectedCliType); } }
// 使用对应的 CLI 执行 const result = await state.llmInstallationManager.callApi( prompt.filePath, event.sender, commandName );
return result; } catch (error) { return { success: false, error: error instanceof Error ? error.message : 'Unknown error' }; }});9. Codex 内部的模型提供者系统
Section titled “9. Codex 内部的模型提供者系统”Codex 本身也支持多种模型提供者,通过 ModelProviderInfo 配置:
pub const OPENAI_PROVIDER_NAME: &str = "OpenAI";pub const OLLAMA_OSS_PROVIDER_ID: &str = "ollama";pub const LMSTUDIO_OSS_PROVIDER_ID: &str = "lmstudio";
pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> { use ModelProviderInfo as P;
[ ("openai", P::create_openai_provider()), (OLLAMA_OSS_PROVIDER_ID, create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Responses)), (LMSTUDIO_OSS_PROVIDER_ID, create_oss_provider(DEFAULT_LMSTUDIO_PORT, WireApi::Responses)), ] .into_iter() .map(|(k, v)| (k.to_string(), v)) .collect()}
pub struct ModelProviderInfo { pub name: String, pub base_url: Option<String>, pub env_key: Option<String>, pub query_params: Option<HashMap<String, String>>, pub http_headers: Option<HashMap<String, String>>, pub request_max_retries: Option<u64>, pub stream_max_retries: Option<u64>, pub stream_idle_timeout_ms: Option<u64>, pub requires_openai_auth: bool, pub supports_websockets: bool,}Codex 的模型提供者支持:
- 内置提供者:OpenAI、Ollama、LM Studio
- 自定义提供者:用户可在 config.toml 中添加自定义提供者
- 重试策略:可配置请求和流的重试次数
- WebSocket 支持:部分提供者支持 WebSocket 传输
appsettings.json 配置多个提供者:
{ "AI": { "Providers": { "DefaultProvider": "ClaudeCodeCli", "Providers": { "ClaudeCodeCli": { "Type": "ClaudeCodeCli", "Model": "claude-sonnet-4-20250514", "WorkingDirectory": "/path/to/workspace", "PermissionMode": "acceptEdits", "AllowedTools": ["file-edit", "command-run", "bash"] }, "CodexCli": { "Type": "CodexCli", "Model": "gpt-4.1", "ExecutablePath": "codex", "SandboxMode": "enabled", "WebSearchMode": "auto", "NetworkAccessEnabled": false } }, "ScenarioProviderMapping": { "CodeAnalysis": "ClaudeCodeCli", "CodeGeneration": "CodexCli", "Refactoring": "ClaudeCodeCli", "Debugging": "CodexCli" }, "FallbackChain": ["CodexCli", "ClaudeCodeCli"] }, "Selector": { "EnableCache": true, "CacheExpirationSeconds": 300 } }}使用示例 - 后端服务
Section titled “使用示例 - 后端服务”public class AIOrchestrator{ private readonly IAIProviderFactory _providerFactory; private readonly IAIProviderSelector _providerSelector; private readonly ILogger<AIOrchestrator> _logger;
public AIOrchestrator( IAIProviderFactory providerFactory, IAIProviderSelector providerSelector, ILogger<AIOrchestrator> logger) { _providerFactory = providerFactory; _providerSelector = providerSelector; _logger = logger; }
public async Task<AIResponse> ProcessRequestAsync( AIRequest request, BusinessScenario scenario) { _logger.LogInformation("Processing request for scenario: {Scenario}", scenario);
try { // 智能选择提供者 var providerType = await _providerSelector.SelectProviderAsync(scenario, request.CancellationToken);
// 获取提供者实例 var provider = await _providerFactory.GetProviderAsync(providerType); if (provider == null) { throw new InvalidOperationException($"Provider {providerType} not available"); }
_logger.LogInformation("Using provider: {Provider} for request", provider.Name);
// 执行请求 var response = await provider.ExecuteAsync(request, request.CancellationToken);
_logger.LogInformation("Request completed with provider: {Provider}, tokens used: {Tokens}", provider.Name, response.Usage?.TotalTokens ?? 0);
return response; } catch (Exception ex) { _logger.LogError(ex, "Failed to process request for scenario: {Scenario}", scenario); throw; } }}使用示例 - 流式响应
Section titled “使用示例 - 流式响应”public async IAsyncEnumerable<AIStreamingChunk> StreamResponseAsync( AIRequest request, BusinessScenario scenario){ var providerType = await _providerSelector.SelectProviderAsync(scenario); var provider = await _providerFactory.GetProviderAsync(providerType);
if (provider == null) { throw new InvalidOperationException($"Provider {providerType} not available"); }
await foreach (var chunk in provider.StreamAsync(request)) { // 处理流式块 switch (chunk.Type) { case StreamingChunkType.ContentDelta: // 实时显示文本内容 await SendToClientAsync(chunk.Content); break;
case StreamingChunkType.ToolCallDelta: // 处理工具调用 await HandleToolCallAsync(chunk.ToolCallDelta); break;
case StreamingChunkType.Metadata: // 处理完成事件和统计 if (chunk.IsComplete) { _logger.LogInformation("Stream completed, usage: {@Usage}", chunk.Usage); } break;
case StreamingChunkType.Error: // 处理错误 _logger.LogError("Stream error: {Error}", chunk.ErrorMessage); throw new InvalidOperationException(chunk.ErrorMessage); } }}使用示例 - OpenSpec 命令
Section titled “使用示例 - OpenSpec 命令”public async Task<string> ExecuteOpenSpecCommandAsync( string command, string arguments, BusinessScenario scenario){ var providerType = await _providerSelector.SelectProviderAsync(scenario); var provider = await _providerFactory.GetProviderAsync(providerType);
// 构建嵌入式命令提示 var commandPrompt = $""" Execute the following OpenSpec command: Command: {command} Arguments: {arguments}
Please execute this command and return the results. """;
var request = new AIRequest { Prompt = "Process this command request", EmbeddedCommandPrompt = commandPrompt, WorkingDirectory = Directory.GetCurrentDirectory() };
var response = await provider.SendMessageAsync(request, commandPrompt);
return response.Content;}1. 提供者健康检查
Section titled “1. 提供者健康检查”在切换提供者前,建议先调用 PingAsync 确保目标提供者可用:
public async Task<bool> IsProviderHealthyAsync(AIProviderType providerType){ var provider = await _providerFactory.GetProviderAsync(providerType); if (provider == null) return false;
var testResult = await provider.PingAsync();
return testResult.Success && testResult.ResponseTimeMs < 5000; // 5 秒内响应视为健康}2. 会话隔离
Section titled “2. 会话隔离”使用 CessionId(Claude)或 ThreadId(Codex)确保会话隔离:
- Claude Code CLI:使用
CessionId作为会话唯一标识 - Codex CLI:使用
ThreadId作为会话标识
// Claude Code CLI 会话选项var claudeSessionOptions = new ClaudeSessionOptions{ CessionId = CessionId.New(), // 生成唯一 ID WorkingDirectory = workspacePath, AllowedTools = allowedTools, PermissionMode = PermissionMode.acceptEdits};
// Codex 线程选项var codexThreadOptions = new ThreadOptions{ Model = "gpt-4.1", SandboxMode = "enabled", WorkingDirectory = workspacePath};3. 错误处理
Section titled “3. 错误处理”提供者不可用时的回退机制要健壮,确保至少有一个可用提供者:
public async Task<AIResponse> ExecuteWithFallbackAsync( AIRequest request, List<AIProviderType> preferredProviders){ Exception? lastException = null;
foreach (var providerType in preferredProviders) { try { var provider = await _providerFactory.GetProviderAsync(providerType); if (provider == null) continue;
// 尝试执行 return await provider.ExecuteAsync(request); } catch (Exception ex) { _logger.LogWarning(ex, "Provider {ProviderType} failed, trying next", providerType); lastException = ex; } }
// 所有提供者都失败 throw new InvalidOperationException( "All preferred providers failed. Last error: " + lastException?.Message, lastException);}4. 配置验证
Section titled “4. 配置验证”启动时验证所有配置的提供者设置,避免运行时错误:
public void ValidateConfiguration(AIProviderOptions options){ foreach (var (providerType, config) in options.Providers) { // 验证可执行文件路径(CLI 类型提供者) if (IsCliBasedProvider(providerType)) { if (string.IsNullOrWhiteSpace(config.ExecutablePath)) { throw new ConfigurationException( $"Provider {providerType} requires ExecutablePath"); }
if (!File.Exists(config.ExecutablePath)) { throw new ConfigurationException( $"Executable not found for {providerType}: {config.ExecutablePath}"); } }
// 验证 API 密钥(API 类型提供者) if (IsApiBasedProvider(providerType)) { if (string.IsNullOrWhiteSpace(config.ApiKey)) { throw new ConfigurationException( $"Provider {providerType} requires ApiKey"); } }
// 验证模型名称 if (string.IsNullOrWhiteSpace(config.Model)) { _logger.LogWarning("No model configured for {ProviderType}, using default", providerType); } }}5. 缓存管理
Section titled “5. 缓存管理”提供者实例会被缓存,注意生命周期管理和内存使用:
// 定期清理缓存public void ClearInactiveProviders(TimeSpan inactiveThreshold){ var now = DateTimeOffset.UtcNow; var keysToRemove = new List<AIProviderType>();
foreach (var (type, instance) in _cache) { // 假设提供者有 LastUsedTime 属性 if (instance.LastUsedTime.HasValue && now - instance.LastUsedTime.Value > inactiveThreshold) { keysToRemove.Add(type); } }
foreach (var key in keysToRemove) { _cache.TryRemove(key, out _); _logger.LogInformation("Cleared inactive provider: {Provider}", key); }}6. 日志记录
Section titled “6. 日志记录”详细记录提供者选择、切换和执行过程,便于调试:
public class AIProviderLogging{ private readonly ILogger _logger;
public void LogProviderSelection( BusinessScenario scenario, AIProviderType selectedProvider, SelectionReason reason) { _logger.LogInformation( "[ProviderSelection] Scenario={Scenario}, Provider={Provider}, Reason={Reason}", scenario, selectedProvider, reason); }
public void LogProviderSwitch( AIProviderType fromProvider, AIProviderType toProvider, string reason) { _logger.LogWarning( "[ProviderSwitch] From={FromProvider} To={ToProvider}, Reason={Reason}", fromProvider, toProvider, reason); }
public void LogProviderError( AIProviderType provider, Exception error, AIRequest request) { _logger.LogError(error, "[ProviderError] Provider={Provider}, RequestLength={Length}, Error={Message}", provider, request.Prompt.Length, error.Message); }}7. 线程安全
Section titled “7. 线程安全”ConcurrentDictionary 等并发集合的使用确保线程安全:
public class ThreadSafeProviderCache{ private readonly ConcurrentDictionary<AIProviderType, IAIProvider> _cache; private readonly ReaderWriterLockSlim _lock = new();
public IAIProvider? GetProvider(AIProviderType type) { // 读取操作无需锁 if (_cache.TryGetValue(type, out var provider)) return provider;
// 创建需要写锁 _lock.EnterWriteLock(); try { // 双重检查 if (_cache.TryGetValue(type, out provider)) return provider;
var newProvider = CreateProvider(type); if (newProvider != null) { _cache[type] = newProvider; } return newProvider; } finally { _lock.ExitWriteLock(); } }}8. 数据库迁移
Section titled “8. 数据库迁移”会话线程绑定数据库结构变更时需要考虑数据迁移:
public class SessionThreadMigration{ public async Task MigrateAsync(string dbPath) { var version = await GetSchemaVersionAsync(dbPath);
if (version >= 2) return; // 已是最新版本
using var connection = new SqliteConnection(dbPath); connection.Open();
// 迁移到 v2:添加 CreatedAtUtc 列 if (version < 2) { _logger.LogInformation("Migrating SessionThreadBindings to v2...");
using var addColumnCommand = connection.CreateCommand(); addColumnCommand.CommandText = "ALTER TABLE SessionThreadBindings ADD COLUMN CreatedAtUtc TEXT;"; addColumnCommand.ExecuteNonQuery();
using var backfillCommand = connection.CreateCommand(); backfillCommand.CommandText = """ UPDATE SessionThreadBindings SET CreatedAtUtc = COALESCE(NULLIF(UpdatedAtUtc, ''), $nowUtc) WHERE CreatedAtUtc IS NULL OR CreatedAtUtc = ''; """; backfillCommand.Parameters.AddWithValue("$nowUtc", DateTimeOffset.UtcNow.ToString("O")); backfillCommand.ExecuteNonQuery(); }
await UpdateSchemaVersionAsync(dbPath, 2); _logger.LogInformation("Migration to v2 completed"); }}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/
感谢您的阅读,如果您觉得本文有用,快点击下方点赞按钮👍,让更多的人看到本文。
本内容采用人工智能辅助协作,经本人审核,符合本人观点与立场。
- 本文作者: newbe36524
- 本文链接: https://docs.hagicode.com/blog/2026-03-09-hagicode-multi-ai-provider-switching-interop/
- 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载时请注明出处!