☰
C#/.NET 转大模型:多模型适配别急着抽象
2026/10/14 13:59:07 网站建设 项目流程

版权与内容来源声明
本文为原创整理。文中涉及官方文档、开源仓库、论文与公开报道的内容,均在附表 A 中标注来源;引用官方原文保持原样,不作改写。文中命令、版本号与界面截图以本文成文时的实测/核验结果为准,标注「待验证」的部分请以你本地环境实际输出为判断依据。本文不推荐任何不合规的软件获取方式,也不对任何收益结果作承诺。转载请注明出处。

第1章 转大模型后,你第一反应是抽一层适配

1.1 .NET 后端的三种肌肉记忆

在 .NET 里写过几年业务的人,看到两套不同的实现,脑子里的第一反应通常是三个词。

  • 接口(interface):只声明「能做什么」,不关心「谁来做」。转到大模型接入的场景,就是「我不在乎背后是哪家的模型,我只要一个能把问题答出来的东西」。
  • 依赖注入(DI,Dependency Injection):人话就是「不在类里直接 new 具体实现,交给容器在启动时把实现塞进来」。你只依赖抽象,具体用哪一家,由配置决定。
  • DTO(Data Transfer Object,数据传输对象):人话就是「一个只装数据、不带方法的盒子」,专门用来在层与层之间传递值。

这三样东西凑在一起,会形成一种很硬的直觉:只要有两套实现,就该有一个接口把它们统一起来。

1.2 大模型接入刚好「看起来像那么回事」

你接完一家模型的对话接口,再去翻另一家的文档,会发现两边都是「把一串消息发进去,拿一段文本回来」。这个相似度很容易让人下判断:这不就是同一接口的两个实现吗,该抽一层。

方向不算错,问题在时机。本文只回答一件事:

什么时候该为多个模型供应商抽一层适配,什么时候纯属过度设计。

先把边界说清楚。本号之前那篇《保险客服转大模型:工单自动分类为什么先要卡住输出格式》讨论的是输出格式约束;本文讨论的是供应商抽象层。一个管「模型吐出来的东西长什么样」,一个管「我这一层代码怎么同时对接多家」。这两件事不要混着看。

1.3 本文的判据不靠「感觉」

成年人转行最怕听到的一句建议是「看情况」。所以后面每一章我都尽量落成「看什么信号、什么时候停手」的清单。结论先摆在这里:

抽象层不是写代码的起点,而是第二家模型的真实差异开始影响业务时的产物。

第2章 先看清两家官方接口到底差在哪(截至 2026-10-14)

2.1 请求结构:连「系统提示放哪」都不一样

先立一条纪律:厂商的字段和参数会过期,本文涉及的所有字段都按官方参考复核过,并写明时间点——截至 2026-10-14。你日后照抄之前,请回到官方参考再看一眼。

OpenAI 的 Chat Completions:messages是一个数组,系统提示就是数组里第一条role: "system"(也存在developer角色)的消息。model与messages是必填项。生成长度上限走max_completion_tokens;官方已把老的max_tokens标为弃用,并明确它与新的推理模型不兼容。请求体里还有stream、tools、response_format等字段。

Anthropic 的 Messages:system是顶层的独立参数,不是 messages 里的一条消息——这是两家最直观的结构差异。max_tokens是必填项。messages 里是交替的 user / assistant 轮次,同一角色连续出现会被合并。鉴权用x-api-key请求头(不是 Bearer 令牌),另外要配anthropic-version版本头来锁定接口版本。

维度OpenAI Chat CompletionsAnthropic Messages
系统提示位置messages 里 role 为 system(或 developer)的一条消息顶层独立参数 system,不是消息
消息列表messages 数组messages 数组,user / assistant 交替,连续同角色合并
生成长度上限max_completion_tokens;max_tokens 已弃用max_tokens(必填)
鉴权方式Authorization 头(Bearer 形式)x-api-key 头
版本控制通过模型名指定anthropic-version 请求头
采样参数temperature 等仍在用(截至 2026-10-14)temperature / top_p / top_k 已标弃用(见 2.3)

光看这张表,你可能觉得「不就是两三个字段挪个位置」。把请求骨架并排看一眼,差异会更具体。

⚠️代码待验证

{"model":"<模型名>","messages":[{"role":"system","content":"你是一个只回答要点的助手。"},{"role":"user","content":"把这段话压缩成一句。"}],"max_completion_tokens":256,"stream":false}

同样的需求,换成 Anthropic 的 Messages 接口,结构长这样:

⚠️代码待验证

{"model":"<模型名>","system":"你是一个只回答要点的助手。","messages":[{"role":"user","content":"把这段话压缩成一句。"}],"max_tokens":256,"stream":false}

请注意两处:系统提示跑到了顶层;max_tokens从可选变成了必填。这不是排版差异,是语义差异——它就是后面 2.3 要讲的「统一 DTO 会丢信息」的源头。

2.2 响应结构:一个 message,对着一组内容块

请求侧还算温和,响应侧的差别更值得警惕。

OpenAI返回的对象里有choices数组,取文本的方式是choices[0].message.content;finish_reason说明模型为什么停下,取值包括正常结束、达到长度上限、调用了工具、被内容过滤拦下等(其中旧的函数调用取值已标弃用)。用量统计在usage里,分输入、输出和总数。

Anthropic的content本身就是一个内容块数组:文本块是type为text的块,工具调用是type为tool_use的块(含 id / name / input),扩展思考还会带独立的思考块。停止原因在stop_reason(例如正常结束、工具调用、达到上限)。用量统计在usage里,分输入 token 与输出 token。

维度OpenAIAnthropic
取文本choices[0].message.contentcontent 数组里 type 为 text 的块
停止原因finish_reason(正常结束 / 长度 / 工具调用 / 内容过滤等)stop_reason(正常结束 / 工具调用 / 达到上限等)
工具调用message.tool_callscontent 里的 tool_use 块(含 id / name / input)
思考内容通常不作为独立内容块返回可有独立的思考块
用量统计usage 里的输入 / 输出 / 总数usage 里的输入 token 与输出 token

2.3 已经进了弃用区的那几个字段

字段会过期,这一条在这两个平台上都在发生,截至 2026-10-14:

  • OpenAI 侧:max_tokens已弃用,改用max_completion_tokens,且旧字段与新推理模型不兼容;seed(随机种子,用来让输出尽量可复现)也已被标记弃用。你要是打算靠seed做严格复现,得先接受「这只是尽力而为」这个前提。
  • Anthropic 侧:temperature、top_p、top_k三个采样参数在新一代模型上已被标记弃用;官方参考说明,在部分较新模型上,非默认的采样取值会被直接拒绝,并返回参数类的错误。⚠️ 具体从哪一代模型开始生效,我未能直连官方页面逐字复核(官方文档在当前网络下不可达),这一条标「待验证」,请以你实际使用模型的官方参考为准。

还有一个细节容易踩:top_p不要和temperature同时传。即使某家现在还能接受,这条「只传一个」的纪律在跨供应商时更省事。

这一章给出的所有字段,都请当成「截至 2026-10-14 的快照」。厂商字段的寿命经常比你的项目周期还短——这本身就是下一章要讲的抽象层问题。

第3章 为什么 .NET 那套「接口抽象」在模型接入上常常不灵

3.1 抽象层的隐藏成本是「最小公倍数」

在业务层抽接口,成本很低:订单服务和用户在接口上的形状几乎一样,统一起来是纯收益。

但在模型接入这一层,接口能暴露的只有两家都有的能力。任一家的独有能力,要么被削掉,要么被硬塞进一个谁都不像的通用字段。人话就是:为了「统一」,你把 A 家特有的功能砍了,或者塞进一个 B 家根本没有的位置。这不是抽象,是削足适履。

而且这里的代价往往是延迟暴露的:抽象层写好的当下,你看不出问题;等到真正接第二家、或用到某家的独有能力时,才发现当初那个「漂亮的统一接口」根本没有位置安放它。到那时候改动面已经扩散到业务代码,回退成本比一开始不抽要高得多。

3.2 反例:把 system 硬塞进 messages 的「统一 DTO」

很多人的第一版统一封装,长这样:

⚠️代码待验证

// 看起来很美:一个 DTO 打天下publicsealedclassUnifiedChatRequest{publicstringModel{get;init;}="";publicList<UnifiedMessage>Messages{get;init;}=new();publicintMaxTokens{get;init;}=512;publicboolStream{get;init;}}publicsealedclassUnifiedMessage{publicstringRole{get;init;}="user";// system 也往这里塞publicstringContent{get;init;}="";}

问题不在于这段代码写错了,而在于它先天丢信息:

  • 系统提示被你塞进了Messages里,转发给 Anthropic 时还得再捞出来放回顶层,一处漏改就静默失效;
  • Content是字符串,装不下 Anthropic 的「内容块数组」,一遇到工具调用或思考块就得转型;
  • MaxTokens这个名字在两家语义不同,你以为在传上限,其实两边的「上限」包含的东西不一样(见 6.3)。

3.3 「看起来统一」的三个危险信号

判断你的统一 DTO 是不是在丢信息,看三个地方就够:

  1. 系统提示的存放位置:如果它在 DTO 里被压在 messages 内,说明你默认了 OpenAI 的形状。
  2. content 的类型:如果是string,就等于提前宣布「不支持多内容块」。
  3. 停止原因和用量的字段名:如果 DTO 只暴露一个布尔「成功/失败」,那你已经丢掉了「为什么停」和「花了多少」这两类信息。

这三条任意中一条,都不要急着把这版 DTO 推广到第二家。

第4章 判断清单:什么时候该抽一层

4.1 先回答四个问题

不要一上来就问「怎么抽」,先问「要不要抽」。下面四个问题,每个都要能干脆地回答「是」或「否」:

  1. 你现在或未来三个月内,是否真的会同时接两家以上模型?只是「以后可能」不算。
  2. 供应商切换的频率有多高?一次上线用一年,和每周做 A/B 对比,是完全不同的两件事。
  3. 是否需要自动降级?也就是主供应商失败时,自动切到备用供应商,业务不中断。
  4. 是否需要统一流式与工具调用?比如前端只想认一种事件流,不想为每家写一套解析。

四个问题里,「是」少于两个,通常说明抽象层还没到该出现的时候。

4.2 取舍对照:什么时候该抽象

这张表是本文的核心判据,建议对着自己的项目逐行核。

你的情形建议理由
只接一家,且短期不换不抽抽象层只增加维护面,不换来任何东西
接两家,只是偶尔对比效果先不抽,写两个薄封装即可差异还没进业务,抽了也白抽
要按请求切供应商(A/B 或按成本路由)抽,但只抽「调用」这一层有真实切换需求,且需求边界清晰
需要主供应商失败时自动降级抽,且接口要显式表达「可降级」语义降级是业务需求,不是技术洁癖
前端只认一种流式事件流抽,并做事件归一化事件模型差异会外泄到前端
要用某家独有能力(思考、缓存等)不要把它压进统一接口压进去必然丢信息,走旁路透传

4.3 停手线:出现这些信号说明你抽过头了

抽象本身不坏,坏在停不下来。下面任意一条出现,就该往回收:

  • 统一接口里开始出现只有一家人用的参数(比如某个开关只在 A 家有效);
  • DTO 字段越加越多,而且都带着「optional / 供应商专用」的注释;
  • 每加一个供应商,你要改五个以上文件;
  • 为了「统一」,你开始主动丢掉数据(例如把思考内容或引用信息扔掉)。

一个简单的自检:如果你的抽象层在新增供应商时,需要修改的地方超过一处注册配置,那这层抽象的位置就放错了。

第5章 真要抽的时候,怎么抽才不丢信息

5.1 把抽象放在「能力」层,不放在「字段」层

这是最关键的一条设计纪律。人话解释:不要试图去统一各家的字段名,而要去统一「我要做什么」。

  • 别定义UnifiedRequest { System; Messages; MaxTokens; ... },这种「字段级统一」注定漏;
  • 而是定义能力:ChatOnce(一次性取回)、StreamChat(流式取回)、CallTools(工具调用),三到四个动词就够了;
  • 每家特有的东西,用一个「供应商专用」的旁路字段原样透传,别解析、别改写。

5.2 一个不丢信息的 C# 接口长什么样

⚠️代码待验证

// 能力层:只描述「能做什么」,不描述「字段怎么摆」publicinterfaceIChatCompletionClient{// 一次性取回Task<ChatResult>CompleteAsync(ChatRequestrequest,CancellationTokenct);// 流式取回:返回归一化后的增量IAsyncEnumerable<ChatDelta>StreamAsync(ChatRequestrequest,CancellationTokenct);}publicsealedrecordChatRequest(stringModel,IReadOnlyList<ChatTurn>Turns,string?SystemPrompt,// 顶层语义,不塞进 TurnsintMaxOutputTokens,IReadOnlyDictionary<string,object>?ProviderOptions=null);// 供应商专用,原样透传publicsealedrecordChatDelta(stringText,boolIsFinal);publicsealedrecordChatResult(stringText,string?StopReason,int?InputTokens,int?OutputTokens);

三个设计点值得单独说:

  • SystemPrompt是独立的顶层语义,转发时由各适配器自己决定放在哪(OpenAI 进数组首位,Anthropic 放顶层);
  • ProviderOptions是旁路透传口,思考开关、缓存键这类独有参数从这里走,接口本身不认识它们;
  • StopReason保留原始字符串,不在这一层硬编成枚举——两家的取值集合不一样,硬编就是自找麻烦。

5.3 用 DI 注册多供应商,把选择交给配置

到这一步,才是 .NET 的 DI 真正发挥价值的地方:接口是抽象的,实现按名字注册,用哪个由配置决定。

⚠️代码待验证

# 供应商配置:切换只改这里,不改代码providers:primary:kind:openai-compatiblebaseUrl:"https://<你的网关地址>/v1"model:"<模型名>"fallback:kind:anthropicbaseUrl:"https://<你的网关地址>"model:"<模型名>"routing:onFailure:fallback# 主供应商失败时自动降级

⚠️代码待验证

// 按名字注册多个实现,运行时按配置解析services.AddKeyedSingleton<IChatCompletionClient,OpenAiCompatibleClient>("primary");services.AddKeyedSingleton<IChatCompletionClient,AnthropicClient>("fallback");// 业务代码只依赖接口,不依赖具体供应商publicsealedclassSummarizer([FromKeyedServices("primary")]IChatCompletionClientclient){publicTask<ChatResult>RunAsync(stringtext,CancellationTokenct)=>client.CompleteAsync(newChatRequest("<模型名>",new[]{newChatTurn("user",text)},SystemPrompt:"只回答要点。",MaxOutputTokens:256),ct);}

注意这里的配置:onFailure指向降级供应商,这正是 4.1 里第三个问题问的东西。只有当你确实需要降级时,这套「按名字注册 + 配置路由」才值得写。

这份资料是什么:一份按阶段编排的大模型学习路线,从传统技术岗的能力对照讲起,写清哪些旧经验能直接复用、哪些要从头补。如果你正打算把 .NET 后端经验接到 AI 应用上,它可以帮你先划出学习顺序。放在资料包里,扫码即可获取:

第6章 三个最容易翻车的点(都与输出格式无关)

抽象层最容易漏的,恰恰是那些「看起来一样、其实不一样」的地方。以下三点,都与输出格式约束无关——那是另一篇文章聊的事。

6.1 流式:两家的「事件」根本不是一回事

流式传输(人话:模型一边生成、你一边收,不用等它全部说完)两家都用 SSE,但事件的切分方式不同。

  • OpenAI:每来一个分片就是一个 chunk,文本增量在分片的delta里。
  • Anthropic:有一整套命名事件流——message_start起头,随后每个内容块有content_block_start、一次或多次content_block_delta、content_block_stop,最后是message_delta和message_stop。工具参数是以input_json_delta的「不完整 JSON 片段」流出来的,要自己拼接。(以上均截至 2026-10-14)
维度OpenAIAnthropic
传输方式SSESSE
起始首个分片即开始message_start 事件
文本增量分片里的 deltacontent_block_delta 里的 text_delta
工具参数增量在工具调用分片里content_block_delta 里的 input_json_delta(不完整 JSON)
结束最后一个分片message_delta 加 message_stop

如果你不做归一化,前端就会同时面对两套解析逻辑。这是「该抽」的一个真实理由。

6.2 工具调用:schema 放哪、结果怎么回传,两家都不同

工具调用(人话:让模型自己决定去调用你写好的某个函数,比如查天气、查库存)是差异最集中的地方。

维度OpenAIAnthropic
工具定义tools 里 function 定义(含 parameters 的 JSON Schema)tools 里用 input_schema 放 JSON Schema
模型发起调用message.tool_callscontent 里的 tool_use 块
关联标识工具调用 idtool_use 块的 id
结果回传role 为 tool 的消息tool_result 内容块,带 tool_use_id
强制调用tool_choicetool_choice(auto / any / none / 指定工具)

差异里最容易被忽略的是回传:一家用一条角色为 tool 的消息,另一家用一个内容块加关联 id。你要是在 DTO 里把工具结果拍平成一句「字符串结果」,两家的关联关系就全丢了,多轮工具调用会当场乱套。

6.3 max_tokens 的语义不一样,别照搬同一个数

最后一条最隐蔽:生成长度上限在两家不是同一回事。

  • OpenAI 的max_completion_tokens是「生成 token 的上限」,官方说明它包含可见输出与推理 token;
  • Anthropic 的max_tokens是必填的「生成上限」。

判据很简单:不要拿同一个数字直接跨供应商填。对带推理的模型来说,你设的上限里可能有一部分被推理过程吃掉,可见输出会比你想的短。跨供应商时应各自按自己的口径设,或者干脆把上限调宽再按需截断。

顺带说一句容易被忽略的连带影响:如果抽象层把「长度上限」当成一个不含语义的整数往下传,那么当某个供应商把上限解释成「含推理」、另一个解释成「不含推理」时,你会得到两种完全不同的截断行为,而错误现场看起来只是「有时候答到一半断了」。这类问题的麻烦之处在于:它不会在写接口时报错,只会在某个特定模型上偶发,排查成本远高于当初多写一行注释的成本。

第7章 给 .NET 后端的一条落地路径

7.1 先跑通一家,再谈第二家

顺序不要颠倒:

  1. 先用官方 SDK 或裸 HTTP,把一家跑通,端到端能出结果;
  2. 把业务逻辑跑通,记录你实际用到的能力子集(多数场景其实只要「一次性取回」和「流式取回」两种);
  3. 真有了第二家、或真有了降级需求,再回头设计接口。

跳过第 2 步直接抽象,等于在没摸清用到哪些能力时就画好接口,后面必然反复改。

7.2 一份「先不抽象」的检查表

在动手写接口之前,先走一遍这张表。

检查项通过标准不通过怎么办
单供应商能否跑通端到端能出结果先别碰抽象,把这条打通
是否列得出实际用到的能力一页纸能写完(例如只要一次性与流式文本)说明还没到抽象时机
是否真有切换或降级需求有明确的业务触发条件不抽,写两个薄封装
独有能力是否有归属想清楚放哪,不塞进统一 DTO设计旁路透传字段
新增供应商的改动面预期只改注册配置超过这个范围说明抽象位置错了

7.3 如果一定要抽象,守住三条纪律

  • 接口只放两家都稳定的能力,独有能力走旁路透传;
  • 系统提示、停止原因、用量这三类信息,保留原始语义,不在中间层拍平;
  • 字段和参数随时会变(本文所有字段都只是截至 2026-10-14的快照),所以隔离要做得够薄,薄到厂商改字段时你只动一个适配器。

说到底,这段路和你在 .NET 里做的一切是一样的:抽象是为了让变化集中在一处,而不是为了让代码看起来整齐。当还没有变化时,整齐本身就是成本。

这份资料是什么:一份从私有化部署、Embedding 检索到 Agent 编排的视频课目录截图。如果你已经决定把接口抽出来、开始接第二家模型,它能让你看清从「接一家」到「做成一个能交付的应用」中间还差哪些环节。放在资料包里,扫码即可获取:

附表 A:本文引用事实与出处对照表

事实出处本文位置
OpenAI Chat Completions 请求字段:messages 数组、system / developer 角色、max_completion_tokens、max_tokens 已弃用且与新推理模型不兼容OpenAI API Reference,Chat Completions(platform.openai.com/docs/api-reference/chat/create,2026-10-14 经官方域名检索核验)2.1、2.3
OpenAI 响应结构:choices[0].message.content、finish_reason 取值、usage 三类 token 计数同上(官方 cookbook 与 API Reference 一致表述)2.2
Anthropic Messages 请求字段:system 为顶层参数、max_tokens 必填、messages 交替且连续同角色合并、x-api-key 与 anthropic-version 头Anthropic Messages API(platform.claude.com/docs/en/api/messages,原 docs.anthropic.com 已跳转,2026-10-14 核验)2.1
Anthropic 响应结构:content 为内容块数组(text / tool_use / thinking)、stop_reason、usage 输入与输出 token同上;配合官方 Streaming 文档(platform.claude.com/docs/en/build-with-claude/streaming)2.2、6.1
Anthropic 流式事件:message_start、content_block_start / delta / stop、message_delta、message_stop;text_delta 与 input_json_delta(不完整 JSON)Anthropic 官方 Streaming 文档(platform.claude.com/docs/en/build-with-claude/streaming,2026-10-14 核验)6.1
Anthropic 工具定义用 input_schema(JSON Schema),tool_choice 取值含 auto / any / none / 指定工具;结果以 tool_result 块带 tool_use_id 回传Anthropic 官方 Streaming 文档与 Messages API 参考(platform.claude.com,2026-10-14 核验)6.2
Anthropic 采样参数 temperature / top_p / top_k 已标弃用,部分较新模型对非默认采样值直接拒绝官方 Messages API 参考说明 + 论坛与第三方博客转述(社区讨论帖,非官方文档,未经官方确认);具体生效的模型边界未直连官方页面逐字复核 → 标「待验证」2.3
OpenAI seed 参数已标弃用(可复现性为尽力而为)OpenAI API Reference(platform.openai.com),2026-10-14 核验2.3
本文所用 OpenAI / Anthropic 官方文档页面在本机网络下直连返回禁止访问或地区限制跳转,故字段以官方域名检索快照核验,非直连抓取本机核验记录(2026-10-14)附表 A 说明

说明:本文所有代码示例均为讲解用骨架,未在本机实际运行,正文对应位置已标注「代码待验证」;示例中的模型名与网关地址均以占位符<...>表示,请替换为你自己的合规取值。

附表 B:术语速查表

术语一句话解释在本文哪里用到
接口(interface)只声明「能做什么」、不关心「谁来做」的类型契约1.1、5.2
依赖注入(DI)不在类里直接 new 实现,由容器在启动时把实现塞进来1.1、5.3
DTO只装数据、不带方法的传输用「盒子」1.1、3.2
供应商抽象层把多家模型接口包成一套统一调用方式的那层代码1.2、3.1、4.2
系统提示(system prompt)给模型的全局指令;OpenAI 放在消息数组里,Anthropic 放在顶层参数2.1、5.1
内容块(content block)Anthropic 响应里文本 / 工具调用 / 思考的基本单元2.2、3.2
停止原因(stop reason)模型为什么停止生成,比如正常结束、调用工具、达到上限2.2、5.2
流式(SSE)模型一边生成、客户端一边收,不用等全部说完6.1
工具调用(tool use)让模型自己决定去调用你写好的某个函数6.2
旁路透传供应商独有的参数原样带过去,中间层不解析、不改写5.1、5.2
降级主供应商失败时自动切到备用供应商,业务不中断4.1、5.3
最小公倍数式抽象接口只暴露几家都有的能力,导致独有能力被削掉或塞歪3.1

写在最后:这篇用到的资料

写这篇文章时,把相关的官方文档和源码又翻了一遍,顺手也整理了几份配套的东西:

  • 大模型学习路线图:从零基础到能自己动手做 Agent,按阶段说明每一步该学什么、哪些可以先跳过
  • 《LangChain + LangGraph + MCP 智能体开发实战》视频课:7 个模块,从私有化部署、Embedding+RAG 到 MCP+Agent 全流程
  • AI 大模型知识库(在线可查):Agent Skills 从入门到落地、Claude Skills 完全指南等专题,按目录浏览即可
  • 640 套 AI 大模型行业报告 + 经典 PDF 书籍:看行业落地案例和别人怎么做的时候用得上
  • 大模型零基础到精通教学视频:跟着敲一遍,比只读文档快得多

资料是我自己整理的,放在下面这个码上,扫码即可获取:






添加时备注「AI」,优先通过。

资料按「先路线、再动手、最后查漏」的顺序整理好了,建议先看学习路线那一份,照着它挑一条适合自己当前基础的路径再往下看。

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

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

立即咨询