LLM Tornado:统一 .NET AI 开发框架,实现多模型智能体编排
1. 项目概述为什么我们需要一个统一的 .NET AI 开发框架如果你在过去一两年里尝试过用 .NET 来接入各种大语言模型LLMAPI那你大概率经历过我当初的困境每个厂商都有自己的SDK、自己的认证方式、自己的请求响应格式。今天想用 OpenAI 的 GPT-4 做个聊天明天客户要求换成 Claude后天又需要集成自家的私有模型。光是管理不同 SDK 的版本、处理五花八门的错误码、统一日志和监控就足以让一个简单的 POC 项目变得臃肿不堪。更别提当你开始构建真正的 AI 应用时——那些涉及多步骤推理、工具调用、文件处理甚至多智能体协作的复杂工作流。你会发现现有的 .NET 生态里要么是过于底层、只提供基础 HTTP 调用的“薄封装”要么就是绑定在特定厂商技术栈上的“厚框架”一旦你想换供应商或者混合使用就得面临大量的适配和重写工作。LLM Tornado的出现就是为了解决这个核心痛点。它不是一个简单的 API 客户端包装而是一个提供商无关Provider-agnostic的 .NET SDK。你可以把它理解为一个“翻译官”加“调度中心”它用一套统一的、强类型的 C# 接口抽象了背后 30 多家 AI 服务提供商从 OpenAI、Anthropic 到 Google、DeepSeek甚至本地的 Ollama、vLLM的差异。这意味着你写的业务逻辑代码——比如一个处理用户查询、调用工具、生成最终回复的智能体——只需要写一次。今天用 GPT-4 跑明天把模型名字换成Claude-3.7-Sonnet代码一行都不用改Tornado 会自动帮你处理认证、序列化、错误重试等所有脏活累活。我最初接触这个项目是因为团队需要为一个教育平台构建一个能同时处理文本、图像问答并能根据上下文自动调用不同知识库工具的 AI 助手。在评估了多个方案后LLM Tornado 以其极简的 API 设计、对多模态的原生支持以及强大的智能体编排能力脱颖而出。经过近半年的生产环境使用处理了数十亿 Token 的请求后我可以负责任地说它极大地提升了我们的开发效率和系统的可维护性。接下来我将从一个深度使用者的角度为你拆解 LLM Tornado 的核心设计、实战技巧以及那些官方文档里不会写的“踩坑”经验。2. 核心架构与设计哲学统一抽象的力量2.1 核心抽象层TornadoApi与ConversationLLM Tornado 的基石是TornadoApi类。它的构造函数设计就体现了其“多供应商统一管理”的思想你可以一次性传入所有你需要用到的 API 密钥。框架内部维护了一个提供者Provider到密钥的映射当你指定一个模型如ChatModel.OpenAi.Gpt4.O发起请求时它会自动选择正确的提供者和密钥。// 一次性配置所有供应商密钥后续无需关心 TornadoApi api new TornadoApi([ new (LLmProviders.OpenAi, Environment.GetEnvironmentVariable(OPENAI_API_KEY)), new (LLmProviders.Anthropic, Environment.GetEnvironmentVariable(ANTHROPIC_API_KEY)), new (LLmProviders.Google, Environment.GetEnvironmentVariable(GOOGLE_API_KEY)), // ... 可以继续添加更多 ]);比这更巧妙的是Conversation对象。它不仅仅是一个请求的封装而是一个有状态的对话会话。你可以通过流式FluentAPI 构建多轮对话并且这个对话对象可以跨不同的模型执行前提是模型能力兼容。这种设计让对话逻辑和模型调用彻底解耦。// 创建一个对话并预设系统指令 var conversation api.Chat.CreateConversation(ChatModel.Anthropic.Claude3.Sonnet) .AppendSystemMessage(你是一个严谨的代码审查助手只讨论技术问题用中文回复。); // 第一轮用户输入 conversation.AppendUserInput(请帮我审查这段C#代码的线程安全性问题...); var firstResponse await conversation.GetResponse(); // 基于上一轮回复继续追问conversation 内部自动维护了历史消息 conversation.AppendUserInput(你刚才提到的锁用 ReaderWriterLockSlim 会不会更好); var secondResponse await conversation.GetResponse();实操心得模型名称的“魔法”你既可以使用强类型的ChatModel.OpenAi.Gpt4.O也可以直接传递字符串gpt-4o。Tornado 内部有一个智能的模型解析器能根据字符串模式匹配到正确的提供商。这在动态配置模型的场景下非常有用比如从数据库或配置中心读取模型名称。但要注意使用字符串时如果拼写错误或使用了不支持的模型别名会在运行时抛出异常。我建议在项目启动时用一个简单的测试请求验证所有配置的模型字符串是否有效。2.2 供应商扩展VendorExtensions兼容性与独特功能的平衡这是 LLM Tornado 设计中最精妙的部分之一。不同 AI 厂商的 API 除了标准功能外都有自己独特的参数和功能。例如只有 Anthropic 的 Claude 3.7 有“思考预算”Thinking Budget只有 OpenAI 有“种子”Seed参数用于确定性输出。如果框架为了统一而阉割这些功能就失去了实用性如果为每个厂商都暴露一套独有的 API又破坏了抽象。Tornado 的解决方案是VendorExtensions。它是一个强类型的、可扩展的容器允许你在发起请求时注入特定供应商的专属参数。var request new ChatRequest { Model ChatModel.Anthropic.Claude37.Sonnet, VendorExtensions new ChatRequestVendorExtensions( new ChatRequestVendorAnthropicExtensions { Thinking new AnthropicThinkingSettings { BudgetTokens 4000, // 为 Claude 3.7 设置 4000 token 的思考预算 Enabled true } } ) }; var conversation api.Chat.CreateConversation(request);当你使用VendorExtensions时框架会确保这些参数仅在调用对应的供应商这里是 Anthropic时被序列化并发送。如果你错误地将这个包含 Anthropic 扩展的请求对象用于 GPT-4这些扩展参数会被静默忽略不会引起错误。这既保证了你能用上各家最前沿的功能又维持了核心 API 的简洁和统一。避坑指南扩展参数的序列化早期版本中某些复杂嵌套对象的序列化可能会因为命名策略camelCase vs snake_case导致参数无法被供应商正确识别。如果你发现某个供应商的独特功能不生效首先检查VendorExtensions里对象的属性是否标注了正确的JsonPropertyName。最稳妥的方法是直接查看框架源码中对应供应商的请求模型定义通常在LlmTornado.Vendor命名空间下模仿其结构。2.3 响应抽象的三层模型Tornado 为聊天响应提供了三个不同粒度的抽象层适应从简单到复杂的所有场景Response(字符串)最常用的一层只关心最终的文本输出。对于简单的问答、摘要、翻译任务这层就够了。string answer await conversation.GetResponse();ResponseRich(富响应)当需要处理工具调用Function Calling、多模态内容图片、音频或获取元数据如消耗的 Token 数时使用这层。它返回一个ChatRichResponse对象包含分块Blocks信息。ChatRichResponse richResponse await conversation.GetResponseRich(); foreach (var block in richResponse.Blocks) { if (block.Type ChatRichResponseBlockTypes.Message) Console.WriteLine(block.Message); else if (block.Type ChatRichResponseBlockTypes.ToolCall) // 处理工具调用 }ResponseRichSafe(安全富响应)这是生产环境的“守护神”。网络请求可能失败超时、供应商内部错误等。GetResponse()和GetResponseRich()在遇到 HTTP 错误时会直接抛出异常。而GetResponseRichSafe()返回一个ChatResponseRichSafe对象它总是成功返回不抛异常但内部包含一个Result枚举来指示状态成功、网络错误、提供商错误等以及错误详情。这让你可以更优雅地进行错误处理和降级。var safeResponse await conversation.GetResponseRichSafe(); if (safeResponse.Result ChatResponseResult.Success) { // 处理成功响应 } else { _logger.LogError($请求失败: {safeResponse.ErrorMessage}); // 触发降级逻辑如切换备用模型 }经验之谈何时用哪一层原型开发、内部工具直接用GetResponse()简单粗暴。需要工具调用或复杂交互的智能体必须用GetResponseRich()。任何面向用户的生产服务强烈建议使用GetResponseRichSafe()或将其包裹在try-catch中。AI API 的不稳定性远高于普通 REST API必须有完善的错误处理。3. 智能体编排实战从单兵作战到军团协作LLM Tornado 的Agents包是其皇冠上的明珠。它引入了三个核心概念Orchestrator编排器代表整个工作流图、Runner运行器代表图中的节点/单个智能体、Advancer推进器代表图中的边/转移逻辑。这套模型让你能以可视化的方式设计和执行复杂的、有状态的 AI 工作流。3.1 构建你的第一个智能体工作流假设我们要构建一个“内容创作助理”它的工作流是1) 根据主题生成大纲2) 根据大纲分章节撰写文章3) 对文章进行语法和风格检查。// 1. 定义 Runner智能体节点 public class OutlineGeneratorRunner : RunnerBase { public override async TaskRunResult Run(RunContext context) { var conversation _api.Chat.CreateConversation(ChatModel.Gpt4.Turbo) .AppendSystemMessage(你是一个专业的编辑擅长生成结构清晰的文章大纲。) .AppendUserInput($请为以下主题生成文章大纲{context.GetInputstring(topic)}); var outline await conversation.GetResponse(); context.SetOutput(outline, outline); return RunResult.Success; } } public class ArticleWriterRunner : RunnerBase { public override async TaskRunResult Run(RunContext context) { var section context.GetInputstring(section); var conversation _api.Chat.CreateConversation(ChatModel.Claude3.Haiku) // 使用更快的模型写初稿 .AppendSystemMessage(根据给定的小节标题撰写一段约300字的详细内容。) .AppendUserInput($小节标题{section}); var content await conversation.GetResponse(); context.SetOutput(content, content); return RunResult.Success; } } public class ProofreaderRunner : RunnerBase { public override async TaskRunResult Run(RunContext context) { var fullArticle context.GetInputstring(fullArticle); var conversation _api.Chat.CreateConversation(ChatModel.Gpt4.O) // 使用更强的模型润色 .AppendSystemMessage(你是一名资深校对检查并优化以下文章的语法、用词和流畅度。直接返回优化后的全文。) .AppendUserInput(fullArticle); var polished await conversation.GetResponse(); context.SetOutput(polishedArticle, polished); return RunResult.Success; } } // 2. 使用 Builder Pattern 编排工作流 var orchestrator new OrchestratorBuilder() .AddRunnerOutlineGeneratorRunner(generate_outline, runner runner.WithInput(topic, 人工智能在医疗诊断中的应用与挑战)) .Then() // “然后” 是一个 Advancer连接前后两个 Runner .AddRunnerArticleWriterRunner(write_intro, runner runner.WithDynamicInput(section, ctx 引言 ctx.GetPreviousOutputstring(outline).Split(\n)[0])) .Then() .AddRunnerArticleWriterRunner(write_body, runner runner.WithDynamicInput(section, ctx 主体部分 ctx.GetPreviousOutputstring(outline).Split(\n)[1])) .Then() // 可以并行执行多个 Runner .AddParallelRunners(builder builder .AddRunnerArticleWriterRunner(write_conclusion, ...) .AddRunnerFactCheckerRunner(check_facts, ...) // 假设还有一个事实核查Runner ) .AfterAll() // 等待所有并行任务完成 .AddRunnerProofreaderRunner(proofread, runner runner.WithDynamicInput(fullArticle, ctx string.Join(\n\n, ctx.GetOutputsFrom(write_intro, write_body, write_conclusion).Select(o o.Value)))) .Build(); // 3. 执行工作流 var executionResult await orchestrator.ExecuteAsync(); if (executionResult.IsSuccess) { var finalArticle executionResult.Context.GetOutputstring(polishedArticle); Console.WriteLine($最终文章\n{finalArticle}); }这个例子展示了几个关键特性有状态上下文RunContext数据在 Runner 之间通过context对象传递。动态输入WithDynamicInput允许一个 Runner 的输入依赖于之前 Runner 的输出实现了灵活的依赖关系。并行执行AddParallelRunners可以让你同时运行多个不依赖的智能体提升效率。Builder 模式通过链式调用直观地定义工作流的拓扑结构代码即文档。3.2 高级编排条件分支与循环真实场景的工作流很少是直线型的。Tornado Agents 支持通过ConditionalAdvancer实现条件分支以及通过循环逻辑实现迭代处理。// 条件分支示例根据生成大纲的质量决定是继续写作还是重新生成 var orchestrator new OrchestratorBuilder() .AddRunnerOutlineGeneratorRunner(generate_outline) .Then() .AddConditionalAdvancer(check_quality, context { var outline context.GetPreviousOutputstring(outline); // 用一个简单的规则判断质量实践中可以用另一个LLM来评估 bool isHighQuality outline.Length 500 outline.Contains(第一章) outline.Contains(总结); // 返回下一个要执行的 Runner 的名称 return isHighQuality ? write_article : regenerate_outline; }) .OnCondition(regenerate_outline) .AddRunnerOutlineGeneratorRunner(regenerate_outline) // 跳回第一步但可以传入不同的参数 .ThenJumpTo(check_quality) // 再次检查质量 .OnCondition(write_article) .AddRunnerArticleWriterRunner(write_article) .Build();踩坑实录循环与状态管理在设计包含循环的工作流时例如反复优化一段文本直到满意务必注意上下文状态的清理。RunContext会累积所有 Runner 的输出。如果不加处理每次循环都可能把旧数据带进去导致输入越来越臃肿甚至超出模型上下文长度。一个最佳实践是在循环开始的 Runner 里明确指定它需要哪些输入并使用context.SetOutput覆盖而不是追加关键输出。或者更优雅的方式是利用Scoped Context的概念虽然 Tornado 当前版本没有显式提供但可以通过创建子 Orchestrator 来模拟将循环内的状态隔离。3.3 可视化与调试Mermaid 图导出这是 Tornado Agents 一个非常实用的功能。你可以将构建好的Orchestrator导出为 Mermaid 流程图定义从而直观地看到你的智能体工作流长什么样。string mermaidGraph orchestrator.ToMermaid(); // 将 mermaidGraph 字符串复制到支持 Mermaid 的编辑器如 Typora、Obsidian、GitHub Markdown中 Console.WriteLine(mermaidGraph);生成的图表能清晰显示节点Runner、边Advancer、并行和条件分支对于复杂工作流的设计、评审和调试有巨大帮助。在团队协作时把这个图贴到文档里比千言万语都管用。4. 深度集成MCP、A2A 与生产化工具链4.1 模型上下文协议MCP集成MCPModel Context Protocol是让 AI 智能体安全、标准化地访问外部数据和工具的一种协议。LlmTornado.Mcp包让你能在 Tornado 应用中轻松集成 MCP 服务器。核心价值将工具的定义和执行分离。你的智能体代码只关心“调用一个获取天气的工具”而具体的工具实现可能是调用某个内部 API或者查询数据库则部署在一个独立的 MCP 服务器中。这带来了安全性和可维护性的提升工具服务器的权限可以严格控制工具升级也无需重启主智能体应用。// 连接到本地的天气 MCP 服务器可能是一个独立的进程 await using IMcpClient mcpClient await McpClientFactory.CreateAsync(new StdioClientTransport(path/to/weather_server)); // 从服务器获取所有可用的工具定义 ListTool availableTools await mcpClient.ListTornadoToolsAsync(); // 在你的智能体对话中使用这些工具 var conversation api.Chat.CreateConversation(new ChatRequest { Model ChatModel.Gpt4.O, Tools availableTools, // 直接注入从 MCP 获取的工具列表 ToolChoice new OutboundToolChoice(get_weather) // 或 ToolChoice.Required 让模型自己选 }); conversation.AppendUserInput(旧金山下周一的天气怎么样); var response await conversation.GetResponseRich(async functionCalls { foreach (var call in functionCalls) { // 框架自动将模型推断的参数填充到 call.Arguments // 我们将其转发给 MCP 服务器执行 await call.ResolveRemote(call.Arguments); // call.Result 会被 MCP 服务器的响应填充 } });部署注意MCP 服务器通常通过标准输入输出Stdio或 HTTP 与客户端通信。在生产环境你需要确保 MCP 服务器进程的稳定性和生命周期管理。可以考虑用System.Diagnostics.Process启动和管理子进程或者将 MCP 服务器部署为独立的微服务通过 HTTP 连接。4.2 智能体间通信A2AA2AAgent-to-Agent协议是 LLM Tornado 为多智能体系统设计的通信层。它解决了智能体之间如何发现彼此、如何安全地交换消息和调用服务的问题。想象一个场景一个“客服智能体”接到一个复杂的订单查询它可以把“物流查询”子任务委托给专门的“物流智能体”后者再调用“库存智能体”获取信息。// 智能体 A 注册自己提供的服务 var agentA new TornadoAgent(InventoryAgent); agentA.RegisterTool(new ToolFunction(check_stock, 检查产品库存, ...)); // 智能体 B 发现并调用智能体 A 的服务 var agentB new TornadoAgent(OrderAgent); var a2aClient new A2AClient(); var availableAgents await a2aClient.DiscoverAgentsAsync(); var inventoryAgent availableAgents.First(a a.Name InventoryAgent); var stockResult await inventoryAgent.InvokeToolAsyncint(check_stock, new { productId P12345 });A2A 协议底层可以基于多种传输方式如 HTTP、gRPC、消息队列。LlmTornado.A2A包提供了基础抽象你需要根据实际基础设施例如在 Kubernetes 内用服务发现或用 Redis Pub/Sub 做消息总线来实现具体的IA2ATransport。4.3 生产就绪特性守卫Guardrails与可观测性对于企业级应用仅有功能是不够的还需要安全、稳定和可观测。守卫框架Guardrails允许你在请求到达 LLM 之前以及响应返回给用户之前插入检查点。例如内容过滤检查用户输入或模型输出是否包含敏感词、个人身份信息PII。成本控制估算本次请求的 Token 消耗如果超过预算则拒绝或降级模型。速率限制对特定用户或 API 密钥进行限流。输出格式验证确保模型返回的 JSON 符合预定模式。public class ToxicityGuardrail : IGuardrail { public async TaskGuardrailResult BeforeRequestAsync(ChatRequest request, CancellationToken ct) { // 调用一个本地的或快速的文本毒性检测模型/服务 bool isToxic await _toxicityDetector.IsToxic(request.Messages.Last().Content); return isToxic ? GuardrailResult.Block(输入包含不当内容。) : GuardrailResult.Pass(); } } // 在 Orchestrator 或 Conversation 上注册守卫 orchestrator.AddGuardrail(new ToxicityGuardrail());OpenTelemetry 集成Tornado 内置了对 OpenTelemetry 的支持。这意味着所有的 LLM 调用、工具执行、智能体步骤都可以自动生成追踪Traces、指标Metrics和日志Logs。你可以将这些数据导出到 Jaeger、Prometheus、Application Insights 等可观测性后端从而清晰地看到 AI 工作流的性能瓶颈、错误分布和成本构成。// 通常在你的 Program.cs 或 Startup 中配置 services.AddOpenTelemetry() .WithTracing(builder builder .AddSource(LlmTornado) // 启用 Tornado 的追踪 .AddJaegerExporter()) .WithMetrics(builder builder .AddMeter(LlmTornado.Metrics) // 启用 Tornado 的指标 .AddPrometheusExporter());5. 性能调优与实战避坑指南经过大量实际使用我总结了一些关键的性能优化点和常见问题解决方案。5.1 连接池与 HTTP 客户端管理LLM Tornado 底层使用HttpClient。在高并发场景下错误地使用HttpClient会导致端口耗尽和性能下降。框架内部已经做了优化但作为使用者你仍需注意复用TornadoApi实例这是一个重量级对象包含了认证信息、提供者映射和内部缓存。务必将其注册为单例Singleton在你的依赖注入容器中。配置超时和重试AI API 响应可能较慢。通过自定义IHttpClientFactory为不同的供应商配置不同的超时和重试策略。services.AddHttpClient(OpenAiClient) .ConfigurePrimaryHttpMessageHandler(() new SocketsHttpHandler { PooledConnectionLifetime TimeSpan.FromMinutes(5) // 连接池生命周期 }) .AddPolicyHandler(GetRetryPolicy()); // 添加 Polly 重试策略 // 然后在创建 TornadoApi 时传入自定义的 HttpClient var api new TornadoApi(providers, httpClientFactory: myCustomFactory);5.2 流式响应Streaming的内存优化对于长文本生成使用流式响应 (StreamResponse) 可以显著提升用户体验首个 Token 到达时间快并降低内存压力无需等待完整响应在内存中拼接。但流式处理需要更小心// 正确做法使用 StringBuilder 或直接写入输出流 var responseBuilder new StringBuilder(); await conversation.StreamResponseRich(new ChatStreamEventHandler { MessagePartHandler async (part) { if (part.Text ! null) { responseBuilder.Append(part.Text); // 如果是 Web 应用可以在这里通过 SignalR 或 Server-Sent Events 推送到前端 await _hubContext.Clients.Client(connectionId).SendAsync(ReceiveToken, part.Text); } }, OnUsageReceived (usage) { _logger.LogInformation($本次请求消耗: {usage.TotalTokens} tokens); } }); // 最终结果在 responseBuilder 中常见陷阱不要在MessagePartHandler中执行耗时或可能阻塞的操作如复杂的数据库查询这会拖慢整个流式响应的速度。应该将接收到的 Token 快速缓存或转发处理逻辑放在流式结束后进行。5.3 多供应商故障转移与负载均衡这是 LLM Tornado 的杀手级特性之一。你可以轻松实现“模型降级”或“多活”策略。public class ResilientLlmService { private readonly TornadoApi _api; private readonly ListChatModel _modelPriorityList new() { ChatModel.Gpt4.O, // 主选质量最高 ChatModel.Claude3.Sonnet, // 备选1 ChatModel.Gemini2Flash, // 备选2速度最快 ChatModel.DeepSeek.Chat // 备选3成本最低 }; public async Taskstring GetCompletionWithFallback(string prompt) { foreach (var model in _modelPriorityList) { try { var result await _api.Chat.CreateConversation(model) .AppendUserInput(prompt) .GetResponseSafe(); // 使用 Safe 方法避免异常 if (result.Result ChatResponseResult.Success) return result.Response; // 如果是速率限制错误可以等待后重试同一个模型 if (result.Error?.IsRateLimitError true) { await Task.Delay(TimeSpan.FromSeconds(10)); continue; // 重试当前模型 } // 其他错误尝试下一个模型 } catch (Exception ex) { _logger.LogWarning(ex, $Model {model} failed, trying next.); } } throw new InvalidOperationException(All configured models failed.); } }更高级的策略可以基于成本、延迟、当前负载动态调整优先级列表甚至实现一个简单的加权随机负载均衡器。5.4 上下文长度管理与智能压缩处理长文档时上下文窗口限制是永恒的挑战。Tornado 内置了Compaction功能可以智能地压缩历史对话。var conversation api.Chat.CreateConversation(ChatModel.Claude3.Sonnet) .AppendSystemMessage(你是一个摘要助手。) .AppendUserInput(veryLongDocument); // 启用自动压缩当上下文接近模型限制时会自动尝试总结旧消息 conversation.RequestParameters.EnableAutoCompaction true; conversation.RequestParameters.CompactionStrategy CompactionStrategy.Summarize; // 或 ExtractKeyPoints var summary await conversation.GetResponse();但自动压缩是一把双刃剑可能会丢失重要细节。对于关键任务我推荐手动管理上下文分块处理将长文档分割成有重叠的块分别处理后再合并结果。Map-Reduce 模式用多个智能体并行处理不同部分Map再用一个智能体整合结果Reduce。这正是 Tornado Agents 擅长的。外部记忆对于超长对话不要把所有历史都塞进上下文。将历史摘要和关键事实存入向量数据库如内置支持的 Pinecone、Chroma在需要时进行检索。6. 生态系统与社区如何高效获取帮助与贡献LLM Tornado 拥有一个活跃且快速增长的社区。除了官方文档以下资源对我解决问题至关重要GitHub Issues 与 Discussions这是获取帮助和了解最新动态的第一站。维护者响应迅速很多复杂的使用场景都有现成的讨论。Feature Matrix项目根目录下的FeatureMatrix.md文件是一个宝藏。它用表格清晰列出了每个供应商对每个 API 端点聊天、嵌入、图像生成等的支持情况。在决定使用某个供应商的某个高级功能前务必先查此表。示例项目Demo仓库中的LlmTornado.Demo项目包含了从基础到高级的数十个示例覆盖了聊天、嵌入、图像、音频、智能体、MCP、A2A 等所有功能。这是最好的学习材料我建议直接克隆项目从 Demo 代码开始修改和实验。Discord 社区官方 Discord 频道是实时交流的好地方很多资深用户和贡献者都在那里分享经验。如果你发现某个供应商的新 API 或某个功能缺失贡献代码是受欢迎的。项目结构清晰添加一个新的供应商实现通常遵循固定模式在LlmTornado.Vendor下创建一个新的 Provider 类实现标准的接口IChatEndpointIEmbeddingEndpoint等。从模仿现有的 Anthropic 或 Google 实现开始是最快的路径。最后一个让我决定在关键项目中采用 LLM Tornado 的重要因素是它的许可证和稳定性承诺。它采用宽松的 MIT 许可证可以放心用于商业项目。核心 API 设计稳定在主要版本更新中会保持向后兼容。对于一个需要长期维护的企业级 AI 应用来说技术选型的长期可维护性与功能本身同样重要。LLM Tornado 在这两者之间取得了很好的平衡。