Hagicode 多 AI 提供者切换与互操作实现方案
odex 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# |
ClaudeCodeCliProvider |
Claude Code CLI 实现 | C# |
CodexCliProvider |
Codex CLI 实现 | C# |
AgentCliManager |
桌面端 CLI 管理 | TypeScript |
解决
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. 提供者类型枚举
public enum AIProviderType |
|
{ |
|
ClaudeCodeCli, // Anthropic Claude Code |
|
OpenCodeCli, // 其他 CLI(可扩展) |
|
GitHubCopilot, // GitHub Copilot |
|
CodebuddyCli, // Codebuddy |
|
CodexCli // OpenAI Codex |
|
} |
这个枚举为系统支持的所有提供者提供了类型安全的表示。
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. 智能选择器
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 提供者实现
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 提供者实现
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. 会话线程绑定机制
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 管理
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 内部的模型提供者系统
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 |
|
} |
|
} |
|
} |
使用示例 - 后端服务
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; |
|
} |
|
} |
|
} |
使用示例 - 流式响应
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 命令
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. 提供者健康检查
在切换提供者前,建议先调用 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. 会话隔离
使用 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. 错误处理
提供者不可用时的回退机制要健壮,确保至少有一个可用提供者:
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. 配置验证
启动时验证所有配置的提供者设置,避免运行时错误:
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. 缓存管理
提供者实例会被缓存,注意生命周期管理和内存使用:
// 定期清理缓存 |
|
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. 日志记录
详细记录提供者选择、切换和执行过程,便于调试:
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); |
|
} |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)