技术速递|通过 .NET Aspire 使用本地 AI 模型:把 Ollama endpoint 改到 TaoToken 1. 为什么要在 .NET Aspire 里把 Ollama endpoint 换掉.NET Aspire 这两年在 .NET 圈子里热度一直不低它解决的核心问题是「本地编排多个服务太麻烦」。以前你要跑一个 API、一个前端、一个数据库、再加一个 AI 模型服务得开好几个终端手动记端口环境变量到处飞。Aspire 用 AppHost 项目把这些资源声明式地管起来一条dotnet run全给你拉起来还带一个 Dashboard 看健康状态和日志。Ollama 则是本地跑大模型的常用工具ollama pull llama3.2之后就能在本地起一个兼容 OpenAI 风格的 HTTP 服务。把 Ollama 挂进 Aspire用CommunityToolkit.Aspire.Hosting.Ollama这个托管集成几行代码就能让模型随应用一起启动、自动下载、健康检查。但实际开发里有个绕不开的痛点本地模型和线上模型的行为不一致。你在本地用llama3.2:1b调得好好的代码里写死了http://localhost:11434等要切到云端或者换一个统一的模型网关时就得改一堆配置。更麻烦的是团队协作——每个人本地 Ollama 版本、模型标签、显存情况都不一样IChatClient注入进去之后请求打到哪个 endpoint 全靠环境变量出了问题很难定位。我试过的一种做法是保留 Aspire 的编排能力但把「模型从哪来」这件事抽出来交给一个统一的 Key/API 通道。也就是说AppHost 里依然声明模型资源服务项目里依然注入IChatClient但底层 endpoint 指向 TaoToken 的 API 地址而不是localhost:11434。这样本地开发、CI 环境、预发环境用的是同一套 Key 和同一套模型 ID切换成本几乎为零。这篇文章就按这个思路走一遍先讲 Aspire Ollama 的原生接法再讲怎么把 endpoint 改到 TaoToken最后给一份可复制的 AppHost 声明、appsettings 片段和一次对话调用的验证步骤。适合已经在用 .NET Aspire、想把手头 AI 调用工程化的人也适合刚接触Microsoft.Extensions.AI抽象、想搞清楚IChatClient到底怎么注入的开发者。核心检索词先摆出来.NET Aspire 编排本地 AI 模型、Ollama endpoint 配置、Microsoft.Extensions.AI 的 IChatClient 注入、TaoToken 统一 API 通道。下面从环境准备开始。2. TaoToken 前置Key、Base URL 与模型 ID 三件套在动手改代码之前先把 TaoToken 这边的三样东西准备好。不管你后面是用 Ollama 托管集成还是直接走 HTTP本质上都需要三个参数Base URL、API Key、Model ID。这三件套在Microsoft.Extensions.AI的体系里对应的是IChatClient的底层配置缺一个都跑不起来。第一步打开 TaoToken 官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册并登录。登录之后进控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。控制台里能看到你的账户余额、调用统计以及最关键的 API Keys 管理入口。第二步创建 API Key。进 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 点新建复制出来的 Key 形如sk-xxxxxxxx。这个 Key 只显示一次建议直接存到本地用户机密里别硬编码进Program.cs。在 .NET 项目里可以用dotnet user-secrets init --project ./src/MyApi dotnet user-secrets set TaoToken:ApiKey sk-你的key --project ./src/MyApi第三步确认 Base URL 和模型 ID。TaoToken 的 API 地址是 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为HttpClient的BaseAddress或者 OpenAI SDK 的Endpoint使用。模型 ID 则取决于你想调哪个模型控制台的模型列表里能看到当前可用的 ID比如gpt-4o-mini、claude-3-5-sonnet这类。把它记下来后面 AppHost 和服务项目里都要用。这里有个容易踩的坑很多人会把 Base URL 写成https://taotoken.net/api/v1或者带斜杠的版本结果请求 404。正确的做法是 Base URL 只写到/api具体的路径由 SDK 或IChatClient的实现去拼。如果你用的是 OpenAI 兼容的客户端通常它会自动在 Base URL 后面加/v1/chat/completions所以你的 Base URL 保持https://taotoken.net/api就行。另外如果你打算长期在 Aspire 里跑编码类 Agent或者需要频繁调用多个模型做对比可以顺手看一下 Coding Plan 页面 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 它针对的是持续编码场景的额度方案。模型对话的在线调试入口在 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。这几个链接后面 CTA 还会用到先记着。三件套准备好之后接下来的问题就是Aspire 的 AppHost 里怎么声明这个「模型资源」以及服务项目里怎么把它注入成IChatClient。下一节给可复制的配置。3. 可复制配置AppHost 声明与 appsettings 片段这一节是全文最核心的部分直接给能跑的代码。整体结构分三层AppHost 项目负责声明资源服务项目负责注册IChatClient配置文件负责放 Key 和 endpoint。三层各司其职切换环境时只动配置不动代码。先看 AppHost 项目。假设你的解决方案叫AspireAiDemoAppHost 项目叫AspireAiDemo.AppHost。安装托管集成dotnet add ./AspireAiDemo.AppHost package CommunityToolkit.Aspire.Hosting.Ollama然后在Program.cs里声明 Ollama 资源和模型。注意这里我们保留 Ollama 的声明方式但把 endpoint 的指向通过环境变量交给服务项目去覆盖var builder DistributedApplication.CreateBuilder(args); // 声明 Ollama 资源数据卷持久化避免每次重启重新下载模型 var ollama builder.AddOllama(ollama) .WithDataVolume() .WithOpenWebUI(); // 声明一个聊天模型资源标签用 1b 版本本地下载快 var chat ollama.AddModel(chat, llama3.2:1b); // 把模型资源引用给 API 项目并等待模型下载完成 builder.AddProjectProjects.MyApi(api) .WithReference(chat) .WaitFor(chat) .WithEnvironment(TaoToken__BaseUrl, https://taotoken.net/api) .WithEnvironment(TaoToken__ModelId, gpt-4o-mini); builder.Build().Run();上面这段里WithEnvironment是关键。它把 TaoToken 的 Base URL 和模型 ID 注入到 API 项目的环境变量里服务项目启动时读这两个值。API Key 不建议写在这里因为 AppHost 的代码会进版本库Key 应该走用户机密或者 CI 的 secret 注入。接着看服务项目MyApi。安装两个包dotnet add ./src/MyApi package CommunityToolkit.Aspire.OllamaSharp dotnet add ./src/MyApi package Microsoft.Extensions.AI在Program.cs里注册IChatClient。这里用AddKeyedOllamaSharpChatClient注册一个带 key 的客户端再通过ChatClientBuilder挂上函数调用、OpenTelemetry 和日志中间件var builder WebApplication.CreateBuilder(args); // 从配置读取 TaoToken 三件套 var baseUrl builder.Configuration[TaoToken:BaseUrl] ?? https://taotoken.net/api; var modelId builder.Configuration[TaoToken:ModelId] ?? gpt-4o-mini; var apiKey builder.Configuration[TaoToken:ApiKey] ?? throw new InvalidOperationException(TaoToken:ApiKey 未配置); // 注册带 key 的 IChatClient底层指向 TaoToken 的 OpenAI 兼容端点 builder.AddKeyedOllamaSharpChatClient(chat, settings { settings.Endpoint new Uri(baseUrl); settings.ModelId modelId; settings.ApiKey apiKey; }); // 把 keyed 客户端包装成默认 IChatClient并挂中间件 builder.Services.AddChatClient(b b .UseFunctionInvocation() .UseOpenTelemetry(configure: t t.EnableSensitiveData true) .UseLogging() .Use(b.Services.GetRequiredKeyedServiceIChatClient(chat))); var app builder.Build(); app.MapPost(/chat, async (IChatClient chatClient, string question) { var response await chatClient.CompleteAsync(question); return response.Message; }); app.Run();对应的appsettings.json片段如下注意TaoToken:ApiKey留空实际值走用户机密或环境变量{ TaoToken: { BaseUrl: https://taotoken.net/api, ModelId: gpt-4o-mini, ApiKey: }, Logging: { LogLevel: { Default: Information, Microsoft.AspNetCore: Warning } } }如果你更习惯用 TOML 或者别的配置格式思路一样Base URL 写https://taotoken.net/apiModel ID 写你控制台里看到的模型 IDKey 走 secret。三件套齐了IChatClient就能正常发请求。这里要强调一点AddKeyedOllamaSharpChatClient的settings.Endpoint指向 TaoToken 之后Ollama 本地容器其实只作为 Aspire 的资源占位和健康检查存在真正的推理请求走的是 TaoToken 的 API。这样做的好处是本地开发时你依然能看到 Aspire Dashboard 里 Ollama 资源的状态但模型调用不受本地显存和模型下载速度的限制。如果你完全不想起 Ollama 容器也可以把AddOllama那段去掉只保留AddKeyedOllamaSharpChatClient的注册Aspire 依然能编排你的 API 项目。配置写完之后下一节验证一次真实请求看看返回结果长什么样。4. 验证请求一次对话调用与成功结果配置写完不验证等于没写。这一节从启动 Aspire 到发一次/chat请求把整个过程走一遍顺便说明成功结果应该长什么样。先启动 AppHostdotnet run --project ./AspireAiDemo.AppHost启动之后终端会输出 Aspire Dashboard 的地址通常是https://localhost:17xxx。打开 Dashboard你能看到ollama、chat、api三个资源。如果 Ollama 容器是第一次启动chat资源会显示 downloading 状态等模型下载完成变成 healthyapi才会启动——这就是WaitFor(chat)的作用。如果你已经把 endpoint 指向 TaoToken模型下载这一步其实不影响 API 的推理请求但 Aspire 的健康检查逻辑还是会等它。Dashboard 里api资源会显示它监听的端口比如http://localhost:5200。拿到端口之后用 curl 发一次请求curl -X POST http://localhost:5200/chat?question用一句话解释什么是依赖注入 \ -H Content-Type: application/json如果一切正常你会看到类似这样的返回{ role: assistant, contents: [ { type: text, text: 依赖注入是一种设计模式它把对象的创建和依赖关系的管理交给外部容器而不是让对象自己 new 依赖。 } ] }注意返回结构是Microsoft.Extensions.AI的ChatMessage序列化结果contents数组里是文本内容。如果你用的是CompleteAsync的重载返回ChatCompletion结构会略有不同但核心字段一致。再验证一下 OpenTelemetry 是否生效。回到 Dashboard进api资源的 Traces 页面你应该能看到一次chat操作的 span里面包含模型 ID、token 用量、耗时。UseOpenTelemetry(configure: t t.EnableSensitiveData true)这行配置会把请求和响应的内容也记录下来方便调试。生产环境记得把EnableSensitiveData关掉避免把用户输入写进 trace。如果你想在浏览器里直接和模型对话可以用 TaoToken 的模型对话入口 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 把同样的模型 ID 填进去对比一下 API 返回和网页返回是否一致。这一步能帮你确认问题出在客户端配置还是模型本身。验证通过之后你可能会遇到一些报错。下一节把常见的几个列出来对照着排查。5. 常见报错排查401、local proxy failed 与 reading choices这一节按真实报错来。下面这几个是我在 Aspire Ollama TaoToken 组合里实际遇到过的每个都给现象、原因和修法。报错一401 Unauthorized返回体里带invalid_api_key现象是/chat请求返回 401Dashboard 的 trace 里能看到请求打到了https://taotoken.net/api但被拒绝。原因通常是TaoToken:ApiKey没读到或者读到了空字符串。检查顺序先确认用户机密设置成功dotnet user-secrets list --project ./src/MyApi能看到TaoToken:ApiKey sk-xxx再确认Program.cs里读的是builder.Configuration[TaoToken:ApiKey]而不是TaoToken:ApiKey写成了别的层级。如果是在容器里跑环境变量要用双下划线TaoToken__ApiKey单下划线在 Linux 容器里不生效。报错二local proxy failed连接被拒绝这个报错通常出现在你保留了 Ollama 容器、但settings.Endpoint还指向http://localhost:11434的时候。Aspire 的 Ollama 容器端口是动态分配的写死 localhost 会连不上。修法是确认AddKeyedOllamaSharpChatClient里的settings.Endpoint用的是baseUrl变量而baseUrl来自配置的https://taotoken.net/api。如果你确实想连本地 Ollama应该用builder.AddOllamaSharpChatClient(chat)让 Aspire 自动注入连接信息而不是手写 endpoint。报错三reading choices 时抛 NullReferenceException现象是请求发出去了返回 200但反序列化时报reading choices相关的空引用。原因是 TaoToken 返回的 JSON 结构和 OllamaSharp 默认期望的结构不完全一致或者模型 ID 写错了导致返回体里没有choices字段。修法分两步先用 curl 直接打 TaoToken 的 API确认返回体里有choices数组再检查settings.ModelId是否和控制台里的模型 ID 完全一致大小写和连字符都不能错。如果模型 ID 对但结构还是不对可以在AddKeyedOllamaSharpChatClient之后加一层自定义的IChatClient装饰器手动处理响应映射。报错四OAuth 相关错误提示 token 过期如果你在 CI 里用 OAuth 方式拿 Key可能会遇到 token 过期。TaoToken 的 API Key 是长期有效的不存在 OAuth 刷新问题所以这个报错通常来自别的环节——比如你的 CI 配置里混用了别的服务的 OAuth token。检查appsettings和 CI secret 里有没有残留的AzureOpenAI:Key或者OAuth:Token配置把它们清掉统一用TaoToken:ApiKey。报错五Aspire Dashboard 里 api 资源一直不健康如果api资源一直显示 unhealthy但/chat能通多半是健康检查端点没配。Aspire 默认会探/health你的 API 项目如果没有映射健康检查就会一直不健康。加一行builder.Services.AddHealthChecks();和app.MapHealthChecks(/health);即可。这个和模型调用无关但会影响 Dashboard 的可读性。排查的时候有个通用技巧把UseLogging()的日志级别调到 Debug能看到IChatClient发出的原始请求和收到的原始响应。对照 TaoToken 的接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里的请求示例逐字段比对基本能定位到问题。6. 把 endpoint 固定下来长期编码与 Agent 场景的 CTA走到这里你已经有了一个能跑的 Aspire Ollama TaoToken 组合。但如果你打算把它用在长期编码或者 Agent 场景里还有几件事值得做。第一把三件套固定成团队规范。Base URL 统一写https://taotoken.net/apiModel ID 统一从控制台复制API Key 统一走用户机密或 CI secret。这样新同学 clone 下来配一次 secret 就能跑不用问「你本地 Ollama 是哪个版本」。第二如果你在 Aspire 里跑的是编码类 Agent比如自动改代码、跑测试、生成 PR 描述建议看一下 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。它针对的是高频、长上下文的编码调用比按次计费更适合 Agent 这种反复调用的场景。第三Key 的管理入口固定在 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 定期轮换。如果你在多个项目里用同一个 Key建议按项目建不同的 Key方便排查调用来源。第四接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里有完整的请求示例和错误码说明遇到 4xx 先查文档再改代码比盲目试参数快。最后说一个实际经验Aspire 的 Dashboard 在调试 AI 调用时特别好用因为你能同时看到资源健康状态、trace 和日志。把UseOpenTelemetry和UseLogging都打开模型调用的每一步都透明。等要上生产的时候再把EnableSensitiveData关掉把 endpoint 从配置里读代码一行不用改。这套结构跑顺之后本地模型和云端模型的切换就是改一个环境变量的事。