ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

VS接入阿里Qwen双通道:云端与本地大模型在ASP.NET MVC中的工程实践

VS接入阿里Qwen双通道:云端与本地大模型在ASP.NET MVC中的工程实践 这段时间我把VS里的AI接入从单一默认模型改成了既能连云端、又能连本地的一条双通道链路。理完之后发现一个很实用的组合GitHub Copilot这条线可以接到阿里Qwen的云端API上本地开发机又可以跑一个私有的Qwen模型节点两者共用一套C#/ASP.NET MVC工程骨架互不冲突。可能有人觉得不就是换个大模型API嘛实际操作下来IDE配置、服务层抽象、流式输出、显存和成本控制每一样都有自己的一套脾气。这篇文章就是把我这次搭建本机LLM工程的整体思路、配置步骤、关键代码和踩坑记录都写出来给那些在VS里写.NET、又想合规又省成本地接上国产大模型的同行做个参考。1. 云、本地、IDE三条线动手前先定的架构决策1.1 为什么一定要云本地双通道单走一条不行吗先从需求说起。我用VS写代码默认的AI助手是走远端服务的代码上下文会被送到远端处理。对大多数个人项目没问题但对企业内部项目尤其是有客户数据的MVC站点代码和业务数据能不能出内网不是技术问题而是合规问题。这时候有一条本地推理通道至少能把低敏感度的编码辅助留下来把真正需要大模型能力的功能放到本地跑。另一面是成本和延迟。拿我实际测试来说走云端qwen-plus处理一段中等长度文档一次请求大概几厘钱到几分钱看起来不贵但MVC站点只要面向多个用户开放每个用户每次点击都可能触发几次调用一个月下来就不是小钱。而本地跑一个7B的量化模型只花电费响应速度在普通消费级显卡上也有每秒20到50个token日常问答完全够用。所以我的方案不是二选一而是按任务分流高频、轻量、隐私敏感的任务走本地复杂推理、长文档、知识问答这种本地小模型搞不定的才走云端。还有可用性问题。云端API偶尔会遇到限流、故障或者网络抖动如果业务逻辑强依赖大模型上游一抖页面就跟着卡。本地模型反而稳定一断电一开机就能继续用。这也是我坚持做双通道的根本原因——关键任务不能被单一供应商绑架。1.2 VS、Copilot、MVC在这个工程里各自扮演什么角色这个工程里其实有三层。第一层是IDE入口也就是Visual Studio里的AI编程助手。Copilot这类工具负责帮我们写代码、解释代码、生成单元测试它本质上是把编辑器里的上下文交给LLM并拿回补全/聊天结果。要让这一步接上Qwen可以走VS的Chat自定义模型配置也可以装阿里的通义灵码扩展后者默认用的就是Qwen系列。第二层是业务出口也就是ASP.NET MVC应用。MVC站点里要嵌入AI能力比如站内智能客服、工单摘要、文档问答不能直接把IDE的配置搬过来因为站点运行在服务器上面对的是一整批用户需要后台服务、鉴权、并发控制、流式传输。这一层我把它设计成服务层统一把请求转发给模型。第三层是模型资源池包括云端Qwen和本地Qwen两个节点。这一层是供上两层共享的IDE通过OpenAI兼容协议访问MVC服务层也通过同样的协议访问。正因为两端协议一致我只需要写一套客户端逻辑然后通过配置切换端点即可。这三层的关系用一句话概括IDE和MVC都是消费方模型池是供给方中间靠一个标准协议把两侧解耦。1.3 一次完整请求的走向从页面按钮到模型返回假设MVC页面里有个输入框用户问帮我把这段项目日志按风险等级分类。请求会先进入我的一个控制器Action控制器不直接碰LLM而是调用服务层的AIClient。AIClient会根据配置文件里的Provider值做路由如果当前是local就发到http://localhost:11434/v1/chat/completions如果是cloud就发到阿里DashScope的OpenAI兼容端点。模型计算完结果以后通过SSEServer-Sent Events一段一段返回给浏览器前端拿到内容边接收边渲染看起来就是打字机效果。有一点很关键IDE端的请求和MVC端的请求共用同一个协议、不同接入点。也就是说我在VS里配好Copilot/灵码的模型端点和我在MVC里配的端点指向同一个模型池但IDE的请求头里带的是IDE的鉴权信息MVC的请求头里带的是服务层的鉴权信息。这个隔离在工程上很重要不能让IDE的密钥跑到业务代码里更不能让业务密钥跑到IDE配置里。2. 云端接线VS的AI助手对接阿里Qwen DashScope2.1 准备阶段开通百炼、创建API-KEY、选对模型名云端这条线相对简单因为阿里已经提供了OpenAI兼容接口不需要自己去拼HTTP协议。第一步去阿里云百炼控制台开通模型服务。如果之前没用过需要先实名认证然后在模型服务里开通我需要的模型。日常我用qwen-plus作为默认模型qwen-turbo作为轻量模型qwen-max留给复杂推理场景。这三个模型的定位差异很明显turbo便宜快速但推理上限低一点plus综合性价比高max回答问题更稳但更贵。对VS里的编码辅助来说qwen-plus已经够用对MVC站点里的业务问答我会根据问题类型动态选。第二步创建API-KEY。在百炼控制台的API-KEY管理里生成一个以sk-开头的密钥。这个密钥很重要只能保存第一次显示出来的完整值后面控制台只显示打码后的内容。建议把密钥放到服务端的配置里不要提交进Git仓库更不要写进前端JavaScript。第三步确认Endpoint。阿里DashScope的OpenAI兼容模式base_url是https://dashscope.aliyuncs.com/compatible-mode/v1Chat补全的完整路径是该地址加上/chat/completions。这里有个容易错的地方很多人照着旧文档写成/v1/compatible-mode那是旧的调用风格新的兼容模式路径必须是/compatible-mode/v1。配置VS或者写代码之前建议先在终端里用curl验证一遍避免后面排错浪费时间。2.2 VS端接入Copilot Chat的自定义模型与灵码扩展VS里接云端Qwen有两种主流做法取决于你想让哪个入口用上Qwen。做法一如果你习惯用VS的AI聊天面板Copilot Chat新版VS提供了自定义模型接入能力可以在设置里添加一个OpenAI兼容的模型端点填上2.1里的地址、密钥和模型名。保存以后聊天面板的模型下拉框里就能看到你配置的Qwen模型。这个方式的好处是沿用了Copilot的交互习惯补全、解释、重构都还在原来的界面里。做法二直接安装阿里通义灵码扩展。在VS的扩展菜单里搜索TONGYI Lingma或者通义灵码安装后登录阿里云账号它会自动帮你配好Qwen连接。这个方案对不想研究自定义配置的人最友好而且通义灵码还内置了代码片段生成、单元测试生成这些编码辅助能力更贴近国内开发习惯。我自己的选择是编码辅助用灵码扩展MVC服务层里的AI功能用自定义OpenAI兼容客户端。这样IDE和业务出口各管各的都走Qwen但又不会互相干扰。一个重要提醒如果VS里同时装了GitHub Copilot和灵码注意它们会抢快捷键和功能面板。我在VS里把灵码设为主要的代码生成入口让Copilot退居二线使用体验会顺畅很多。2.3 云端通道实测两个高频翻车点云端通道我踩过两个坑基本每次换一台电脑都会犯。第一个坑是Endpoint拼错。DashScope的OpenAI兼容地址是/compatible-mode/v1写的顺序一颠倒VS或代码里报的错就是401、404混合着来看起来像鉴权失败其实是路径不对。解决方法是先在终端跑通curl确认无误再配VS。第二个坑是模型名不一致。不同平台的模型命名风格不一样OpenAI那边是gpt-4o这种点号风格阿里这边是qwen-plus这种横杠风格。如果你在DashScope里把模型名填成qwen_plus马上报Model Not Found。这个错很隐蔽因为VS的配置界面不一定立刻显式报错可能等到你真正发消息才提示。还有一个不算坑、但容易被忽略的点流式响应也计费。很多人以为只要不流式输出就能省token其实计费是按输入和输出token总量算的流式只是传输方式不同跟计费无关。另外如果MVC里有多用户同时调用建议在服务层做一层简单限流否则一个用户的循环请求就能把月度预算烧掉一截。3. 本地接线Ollama承载Qwen开发机变成私有推理节点3.1 为什么选Ollama而不是自己装Python环境本地跑LLM的方案不少最硬核的是直接装Python的transformers或vLLM自己写模型加载代码。这种做法的坏处是环境维护成本太高换个显卡、升级个驱动就要折腾一天。另外还有llama.cpp流派性能和可控性好但C编译和CMake配置对很多.NET开发者来说比较陌生。最后我选了Ollama原因有三个。一是安装极简。Windows上装个安装包或者直接执行一条命令就能把服务跑起来。二是有现成的模型库ollama pull qwen2.5:7b就能把量化好的Qwen拉下来不需要你关心GGUF格式和量化位数的细节。三是它自带OpenAI兼容接口http://localhost:11434/v1/chat/completions这意味着我在MVC里的客户端代码几乎不用改只要把base_url从云端换成localhost鉴权头去掉即可。对C#工程来说这个兼容性太重要了等于把本地模型包装成了一个私有OpenAI接口。3.2 完整命令链安装、拉模型、启动、验证假设你的开发机是Windows且已经装了VS接下来就是一套命令的事。第一步安装Ollama。到官网下载Windows版安装时它会问要不要装命令行工具建议勾选。装完以后打开PowerShell或者CMD。第二步拉取Qwen模型。我推荐先试qwen2.5:7b对大多数人来说是性价比最高的起点ollama pull qwen2.5:7b如果显存够大比如24GB可以拉qwen2.5:14b。如果显存只有4GB到6GB可以拉qwen2.5:3b虽然模型小但处理简单的摘要、命名实体提取还是够用的。第三步启动服务。新版Ollama安装以后服务是默认自启的如果没有手动执行ollama serve正常启动后监听在11434端口。第四步验证OpenAI兼容接口。在另一个终端窗口执行curl http://localhost:11434/v1/chat/completions -H Content-Type: application/json -d {\model\:\qwen2.5:7b\,\messages\:[{\role\:\user\,\content\:\你好\}]}返回的JSON里带choices数组就说明接口通了。这个curl验证在后续对接VS和MVC时非常有用能快速定位是模型的问题还是代码的问题。3.3 硬件选型多大显存配什么档位的Qwen本地模型的体验基本由显存决定。下面这个表是我在几台机器上实测的参考值按量化模型Q4估算模型最低显存推荐显存实测速度token/秒适合场景qwen2.5:0.5b1GB2GB60以上名称分类、关键词提取qwen2.5:3b3GB4GB40-60轻量问答、文本调优qwen2.5:7b6GB8GB20-40日常问答、总结、改写qwen2.5:14b12GB16GB10-20复杂推理、代码生成如果是纯CPU环境7B模型在CPU上也能跑但速度会掉到每秒几到十几个token做批量任务会很吃力。这种情况下建议用小模型或者干脆走云端通道。有一点需要注意显存占用不只是模型文件的大小。模型运行时的KV Cache、临时激活也会吃显存所以表格里最低显存是按实际能跑起来算的不是模型文件的理论大小。我自己在8GB显卡上跑7B模型大约占用6GB左右还有大概2GB余量Windows桌面还能正常用。3.4 把Ollama配成Windows服务免去手动启动的麻烦开发机上我一般懒得每次手动启动Ollama可以用Windows的计划任务或者直接设置环境变量让它常驻。如果不想用计划任务最省事的方案是安装Ollama的时候选开机自动启动。它默认还会监听127.0.0.1只允许本机访问这对开发阶段来说是安全的。如果有团队协作需要让其他机器也访问你的本地模型可以设置OLLAMA_HOST环境变量指定监听地址但注意这会把你机器上的模型服务暴露到局域网必须有网络隔离和访问控制再开启。4. 双通道服务层ASP.NET MVC里的完整工程骨架4.1 为什么不能每张页面直接new HttpClient很多初学者会在控制器里写类似using var client new HttpClient()的代码。这在调试时可以但进了生产环境就麻烦了。HttpClient底层是socket连接池频繁创建会耗尽可用端口导致连接失败或者出现Only one usage of each socket address这类错误。正确做法是把HttpClient注册成单例通过依赖注入交给控制器和服务层复用。另外LLM请求通常比较久尤其本地模型在小显存机器上一次请求可能要几十秒。默认的HttpClient超时是100秒看起来够用但如果你在做流式输出——即边接收边渲染——超时计算是从开始请求到读完响应体的整段时间。一个长回答可能持续3到5分钟默认超时就会把连接切断。我建议根据流式场景单独设置或者干脆不设Timeout在底层用读超时和CancellationToken控制。4.2 统一抽象让云端和本地用同一套代码我设计了这样一个接口MVC控制器只依赖它不依赖具体的云或本地实现public interface ILLMClient { Taskstring ChatAsync(string prompt, CancellationToken ct); IAsyncEnumerablestring ChatStreamAsync(string prompt, CancellationToken ct); }ChatAsync给不需要流式的场景用比如后台任务、批量处理ChatStreamAsync给页面流式渲染用。两个方法内部都走OpenAI兼容协议区别只在于是否解析SSE流。接下来是两个实现类。云端实现CloudQwenClient只需要注入HttpClient、Endpoint、ApiKey、ModelName四个配置然后把请求头带上Authorization: Bearer。本地实现LocalOllamaClient几乎一样只是Endpoint指向localhost:11434/v1而且不需要ApiKey。因为两个类的代码重复度达到80%以上我实际是把公共部分抽到基类或者一个OpenAICompatClient里只有鉴权和base_url不同。这里有个工程上的细节当你在MVC里同时注册两个Client一定要用接口加名称的方式注册比如services.AddHttpClient(cloud)和services.AddHttpClient(local)然后按名称取出。否则容器里同一个接口有两个实现控制器会犯迷糊不知道注入哪一个。4.3 配置驱动改一行配置切换云端/本地我在appsettings.json里这样设计AiSettings: { Provider: local, DefaultSystemPrompt: 你是一个严谨的C#工程师助手。回答要简洁、可执行。, Cloud: { Endpoint: https://dashscope.aliyuncs.com/compatible-mode/v1, ApiKey: sk-your-key-here, Model: qwen-plus, Temperature: 0.3 }, Local: { Endpoint: http://localhost:11434/v1, ApiKey: , Model: qwen2.5:7b, Temperature: 0.3 } }服务层启动时用一个Factory根据Provider值决定返回云端Client还是本地Client。切换环境只需要改Provider一个词开发机写local部署到有公网密钥的服务器写cloud。这个设计的价值在于业务代码完全感知不到底层模型在哪将来如果要把模型换成别的厂商只需新增一个配置节和对应Client实现控制器一行都不用改。4.4 SSE流式输出MVC控制器和前端怎么配合ASP.NET Core MVC的控制器可以直接返回流。我写了一个Action接收用户输入把整个响应作为text/event-stream推给前端[HttpPost] public async Task ChatStream(string prompt, CancellationToken ct) { Response.ContentType text/event-stream; Response.Headers.CacheControl no-cache; var client _aiClientFactory.Create(); await foreach (var token in client.ChatStreamAsync(prompt, ct)) { var data $data: {JsonSerializer.Serialize(new { token })}\n\n; await Response.WriteAsync(data, ct); await Response.Body.FlushAsync(ct); } await Response.WriteAsync(data: [DONE]\n\n, ct); }注意SSE格式是每行以data:开头两个换行结尾。前端用fetch拿到响应体再通过ReadableStream逐段读取每次读到新增内容就append到页面DOM节点上。网上很多例子是把整个Response先读完再一起显示那其实根本不是流式用户要等所有token生成完才会看到结果。真正流式的关键是Response.Body.FlushAsync每拿到一段就立刻刷给浏览器。前端脚本核心是const reader response.body.getReader(); const decoder new TextDecoder(); while (true) { const { value, done } await reader.read(); if (done) break; const text decoder.decode(value, { stream: true }); resultDiv.innerHTML text; }这样用户在页面里看到的是渐进式输出而不是转圈等半天体验完全不同。4.5 安全和访问控制密钥不能上页面在MVC工程里做LLM接入有个很容易犯的错是把API-Key写在前端调用里。用fetch直接调DashScope不行吗技术上可以但密钥暴露在对所有人可见的浏览器源码里等于把你的计费账户公开了。正确做法是前端只访问自己的控制器由服务层保管密钥再在后端统一加上频率限制。哪怕只给内部系统用我都建议加一个简单的按用户/按IP的限流器防止某个用户恶意刷请求。5. 最容易翻车的五个细节流式、超时、幻觉与端口5.1 流式响应的SSE解析chunk被切断怎么办Ollama和DashScope返回的流式响应每个数据块都是一个完整的JSON但经过HttpClient的StreamReader读取时可能一行文本被网络切成两段也可能一次readline拿到两条data。我的处理方式是先把行缓存起来判断是否以data:开头再反序列化。如果反序列化失败就把这一行和下一行拼起来重试。很多人在这一步踩坑以为是模型输出乱码其实是网络层把数据切碎了。还需要注意流式响应的最后一个事件通常是data: [DONE]。判断到这个标记就要停止读取不要再试图反序列化。5.2 超时与并发MVC全局配置要盯紧LLM服务的响应时间波动极大。云端qwen-plus快的时候1秒就有首token慢的时候可能要到10到20秒。如果服务层设置了过短的HttpClient超时比如30秒偶尔遇到上游慢请求用户页面就报错。我建议把超时设到120秒以上并且用CancellationTokenSource实现用户断开页面时取消模型请求避免后台任务一直在等一个没人看的响应。并发方面本地模型同时只能处理有限的请求数。多个请求同时打给Ollama它会排队但排队久了会导致每个请求都很慢。我在服务层加了SemaphoreSlim控制最大并发数为2超出就直接返回当前推理服务繁忙请稍后再试的提示比让所有请求都陷入长队列健康得多。5.3 模型幻觉与上下文截断给MVC业务加护栏本地小模型在长文档上容易丢上下文云端大模型虽然好一点但同样不能保证百分之百正确。在业务场景里我不建议把模型结果直接当成最终结果展示尤其涉及金额、日期、合同条款这类数据时。我的做法是在系统提示词里明确要求如果信息不足请直接说不知道不要推测同时限制每次对话的历史消息数量超出就做截断或摘要防止上下文过长导致模型输出质量下降。还有Temperature参数。做事实提取和分类时我设到0到0.3做文案生成、头脑风暴时可以升到0.7以上。这个参数的影响很多人低估了同样是qwen-plusTemperature0.9和0.1的输出风格差异非常大。5.4 从Key、Query、Value看网关与RAG的延伸最近看到有人在讨论LLM应用的三个核心要素Key是我是谁Query是我在找什么Value是我能提供什么。这个框架放到MVC的LLM工程里同样适用。Key对应服务层的鉴权信息谁在调用、什么权限Query对应业务请求用户到底在问什么Value对应知识库或业务数据模型能从哪里拿到事实依据。如果只是简单地把用户问题透传给模型那这只是一个套了壳的聊天框。更进一步的工程化是给请求加Value——把知识库检索结果、数据库里的业务数据作为上下文拼进提示词让模型基于真实数据回答。这就演变成了RAG。本地模型在这个场景的优势特别明显因为知识库数据往往敏感走本地模型等于数据不出内网。将来可以在这个双通道骨架上加一个检索层请求先检索本地向量库再拼上下文交给模型这就是一个完整的私有RAG问答系统。5.5 端口、内存和进程本地服务的环境卫生Ollama默认监听11434端口。如果MVC部署的服务器上还有其他服务占用这个端口Ollama会起不来。排查时用netstat -ano | findstr 11434看看是谁占用的。另外本地模型加载以后内存占用是常驻的不会因为对话结束就释放。如果你在8GB显存的机器上跑7B模型又开了VS、浏览器、若干Docker容器显存会非常紧张。我遇到过一次整机UI卡死后来发现是显存被模型吃满导致显卡驱动重置。解决办法是把模型降到3B档位或者在同一时间只跑一个重负载任务。最后如果开发机同时开着VS的灵码和Ollama注意确认两者用的是不同通道VS里的灵码默认走云端Ollama走本地端口两者互不干扰。但如果你的VS自定义聊天被配成连local端点那VS的每个补全请求都会打到本地模型这时候本地模型的速度会直接影响你的编码体验。每次搭这类工程我最深的体会是模型本身不是难点难点是把模型接入到已经存在的工程体系里还要保证稳定、省心、可维护。这次的双通道骨架从IDE到MVC再到模型池层层解耦之后后续加RAG、加多模型切换、加结构化输出都不需要动控制器的代码。最后分享一个小技巧在接VS或MVC之前永远先用curl把模型接口跑通。这个习惯帮我省了至少一半的排错时间——接口通了剩下的问题基本都在自己的代码和配置里。
返回列表