☰
VS接入阿里Qwen双通道:云端与本地大模型在ASP.NET MVC中的工程实践
2026/10/2 3:52:32 网站建设 项目流程

这段时间我把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兼容端点。模型计算完结果以后,通过SSE(Server-Sent Events)一段一段返回给浏览器,前端拿到内容边接收边渲染,看起来就是打字机效果。

有一点很关键:IDE端的请求和MVC端的请求共用同一个协议、不同接入点。也就是说,我在VS里配好Copilot/灵码的模型端点,和我在MVC里配的端点,指向同一个模型池,但IDE的请求头里带的是IDE的鉴权信息,MVC的请求头里带的是服务层的鉴权信息。这个隔离在工程上很重要,不能让IDE的密钥跑到业务代码里,更不能让业务密钥跑到IDE配置里。

2. 云端接线:VS的AI助手对接阿里Qwen DashScope

2.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/v1,Chat补全的完整路径是该地址加上/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 { Task<string> ChatAsync(string prompt, CancellationToken ct); IAsyncEnumerable<string> 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-plus,Temperature=0.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把模型接口跑通。这个习惯帮我省了至少一半的排错时间——接口通了,剩下的问题基本都在自己的代码和配置里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询