ARTICLE DETAIL

资讯详情

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

【Spring AI 入门与实战】04-模型抽象层-一行配置切换模型

【Spring AI 入门与实战】04-模型抽象层-一行配置切换模型 模型抽象层一行配置切换 OpenAI/DeepSeek/通义/智谱本文是专栏《Spring AI 入门与实战》的第 4 篇上一篇我们聊了《第一个 AI 应用ChatClient 十分钟上手》。这一篇钻到 ChatClient 背后ChatModel/EmbeddingModel 抽象是怎么工作的为什么OpenAI 兼容协议成了国产模型接入的事实标准怎么在一个应用里让多个模型共存并按业务路由以及模型版本锁定与降级链路这两个生产必备课题。一、场景引入朋友的创业团队用 DeepSeek 跑了三个月的客服助手一切正常。某个周一早上群里炸了DeepSeek 平台限流客服接口大面积超时客服主管在群里 所有人。他们想立刻切到之前申请好的通义千问结果发现整个 AI 代码里十几处地方直接引用了 DeepSeek 特有的响应字段切换意味着一轮回归测试加连夜改代码——不敢动只能干等平台恢复。事后复盘问题不在 DeepSeek在于代码和具体模型焊死了。模型会限流、会涨价、会停服旧版本、会有新模型性价比反超换模型在 AI 应用的生命周期里不是意外是必然事件。这一篇讲的抽象层就是让你的代码在模型变更那天只需要改配置、最多改路由而不需要改业务逻辑。二、核心讲解2.1 抽象是怎么设计的Spring AI 的模型抽象核心是几个接口以ChatModel为例EmbeddingModel、ImageModel同构publicinterfaceChatModelextendsModelPrompt,ChatResponse{// 最底层接收 Prompt含消息列表与参数返回 ChatResponseChatResponsecall(Promptprompt);// 便捷方法单条字符串进字符串出defaultStringcall(Stringmessage){...}// 流式版本defaultFluxChatResponsestream(Promptprompt){...}}围绕这个接口每个厂商有各自的实现与自动配置OpenAI 的 starter 装配OpenAiChatModelAnthropic 的装配AnthropicChatModelOllama 的装配OllamaChatModel。而ChatOptions模型调用参数如 temperature、model、maxTokens同样有抽象层与厂商层——通用参数定义在ChatOptions接口OpenAiChatOptions等子类补充厂商特有参数。这套设计的直接收益就是你已经体验过的事实spring.ai.openai.*下换三行配置同样的chatModel.call(...)代码从 DeepSeek 换到通义。自动配置做的事也不复杂读取连接属性base-url、api-key构造一个指向该端点的 HTTP 客户端读chat.options.*构造默认参数然后装配出一个ChatModelBean 放进容器。理解这一点很重要因为它是下一节多模型共存的基础——自动配置只能给你一个但你可以自己造多个。2.2 OpenAI 兼容协议事实标准为什么一个名为 openai 的 starter 能连 DeepSeek、通义、智谱因为 OpenAI 早年定义的 HTTP API 形状——POST /v1/chat/completions、Authorization: Bearer key、messages 数组加 SSE 流式——成了行业事实标准。国产主流模型几乎都提供兼容端点模型base-url模型名示例DeepSeekhttps://api.deepseek.comdeepseek-chat、deepseek-reasoner阿里通义https://dashscope.aliyuncs.com/compatible-modeqwen-plus、qwen-turbo、qwen-max智谱 GLMhttps://open.bigmodel.cn/api/paas/v4glm-4.5、glm-4.5-air切换就是改配置三份对照# DeepSeekspring.ai.openai:base-url:https://api.deepseek.comapi-key:${DEEPSEEK_API_KEY}chat.options.model:deepseek-chat# 通义千问spring.ai.openai:base-url:https://dashscope.aliyuncs.com/compatible-modeapi-key:${DASHSCOPE_API_KEY}chat.options.model:qwen-plus# 智谱spring.ai.openai:base-url:https://open.bigmodel.cn/api/paas/v4api-key:${ZHIPU_API_KEY}chat.options.model:glm-4.5要清醒的一点兼容≠完全兼容。各家在 JSON Schema 结构化输出、工具调用的细节字段、流式事件类型上都有差异和阉割“能聊天不代表所有功能可用”。选定模型后把你用到的每个特性结构化输出、工具调用、流式都过一遍冒烟测试再谈替换。2.3 多模型共存一个应用里的模型路由单模型配置在现实中不够用客服走便宜档、报告生成走推理档、测试环境走 Ollama 本地小模型。让多个模型共存思路是自己构造多个ChatModelBean绕开自动配置只能装配一个的限制再为每个模型配一个ChatClient!-- 依赖不变多个 OpenAI 兼容端点仍共用 openai starter --dependencygroupIdorg.springframework.ai/groupIdartifactIdspring-ai-starter-model-openai/artifactId/dependencyapp:models:deepseek:base-url:https://api.deepseek.comapi-key:${DEEPSEEK_API_KEY}model:deepseek-chat# 通用档客服、问答qwen:base-url:https://dashscope.aliyuncs.com/compatible-modeapi-key:${DASHSCOPE_API_KEY}model:qwen-plus# 备用档 文案档packagecom.example.models;importorg.springframework.ai.chat.model.ChatModel;importorg.springframework.ai.openai.OpenAiChatModel;importorg.springframework.ai.openai.OpenAiChatOptions;importorg.springframework.ai.openai.api.OpenAiApi;importorg.springframework.beans.factory.annotation.Qualifier;importorg.springframework.boot.context.properties.ConfigurationProperties;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;ConfigurationpublicclassMultiModelConfig{BeanpublicChatModeldeepseekChatModel(MultiModelPropsprops){varpprops.deepseek();OpenAiApiapiOpenAiApi.builder().baseUrl(p.baseUrl()).apiKey(p.apiKey()).build();returnOpenAiChatModel.builder().openAiApi(api).defaultOptions(OpenAiChatOptions.builder().model(p.model()).temperature(0.5).build()).build();}BeanpublicChatModelqwenChatModel(MultiModelPropsprops){varpprops.qwen();OpenAiApiapiOpenAiApi.builder().baseUrl(p.baseUrl()).apiKey(p.apiKey()).build();returnOpenAiChatModel.builder().openAiApi(api).defaultOptions(OpenAiChatOptions.builder().model(p.model()).temperature(0.8).build()).build();}}ConfigurationProperties(app.models)publicrecordMultiModelProps(Modeldeepseek,Modelqwen){publicrecordModel(StringbaseUrl,StringapiKey,Stringmodel){}}注意ConfigurationProperties这段代码需要加EnableConfigurationProperties(MultiModelProps.class)或用ConfigurationPropertiesScan才生效。接着用这两个 ChatModel 构建两个 ChatClient并写一个按业务路由的门面packagecom.example.models;importorg.springframework.ai.chat.client.ChatClient;importorg.springframework.stereotype.Service;ServicepublicclassChatRouter{publicenumTier{STANDARD,CREATIVE}privatefinalChatClientstandard;// deepseek-chatprivatefinalChatClientcreative;// qwen-pluspublicChatRouter(ChatClient.Builderbuilder,org.springframework.ai.chat.model.ChatModeldeepseekChatModel,org.springframework.ai.chat.model.ChatModelqwenChatModel){// 注意Builder 默认绑定容器里的 ChatModel这里分别显式指定this.standardChatClient.builder(deepseekChatModel).defaultSystem(你是严谨的客服助手。).build();this.creativeChatClient.builder(qwenChatModel).defaultSystem(你是创意文案助手。).build();}publicStringask(Tiertier,Stringquestion){ChatClientclient(tierTier.CREATIVE)?creative:standard;returnclient.prompt().user(question).call().content();}}这个结构有几个讲究路由逻辑集中在一个类而不是散落在各 Controller 的 if-else 里每档模型的系统提示词、temperature 各自独立将来加智谱就是加一个 Bean、一个枚举值。如果路由规则复杂按时段、按用户等级、按成本预算把它抽成独立策略类即可门面不动。2.4 参数调优通用经验值temperature分类/抽取/SQL 生成 0~0.2问答与客服 0.3~0.6文案创意 0.8~1.2。topP与 temperature 二选一调即可不要同时大改两者。需要更窄的分布时把 topP 降到 0.8 左右比极端压 temperature 更平滑。maxTokens作为成本上限设置按业务回答长度的 1.2 倍取值。模型选择优先于参数调优参数只能在模型能力范围内微调deepseek-reasoner和deepseek-chat的差距远大于 temperature 从 0.3 调到 0.7。三、生产视角模型版本要锁定不要用最新。配置里写deepseek-chat这种别名是方便但它背后的快照会随平台静默更新你今天验证过的提示词效果下个月可能悄悄漂移。生产做法能锁具体版本号就锁版本号锁不了就在可观测性第 22 篇里记录每次响应返回的模型标识变更时告警。同时订阅所选平台的模型停服公告——旧版本模型下线是各平台常态操作提前一两个月就要排期验证替代版本。降级链路是必需品不是加分项。多模型共存最大的价值就在这。最小实现调用失败超时、429、5xx时按优先级切下一个模型。用 Spring Retry 包一层即可起步publicStringaskWithFallback(Stringquestion){try{returnstandard.prompt().user(question).call().content();}catch(Exceptione){log.warn(主模型调用失败降级到备用模型: {},e.getMessage());returncreative.prompt().user(question).call().content();}}进阶版是熔断 备用 排队主模型连续失败触发熔断一段时间内直接走备用避免每个请求都先超时一次超时等待本身就是成本。用 Resilience4j 的 CircuitBreaker 注解可以做到声明式实现思路与数据库故障切换一脉相承不再展开。成本用模型分档直接控制。多模型路由天然是成本治理工具把高成本模型划为需要审批的 Tier把日志摘要、内部质检这类高频低价值场景钉死在最低档。每周拉一次各档位调用量 × 单价的报表比任何省钱技巧都有效——前提还是那句话得有观测数据第 22 篇。数据合规随模型走。不同模型的部署位置境外/境内、公有/私有决定了能发什么数据过去。路由层是做合规拦截的好位置涉密数据只允许路由到私有化部署的 Ollama/Qwen 专区这个规则用路由枚举加一条断言就能实现。四、踩坑记录坑一多 Bean 冲突。定义了两个ChatModelBean 后启动直接失败Parameter 0 of constructor in com.example.models.ChatRouter required a single bean, but 2 were found: - deepseekChatModel: defined in com.example.models.MultiModelConfig - qwenChatModel: defined in com.example.models.MultiModelConfig报错本身不冤容器里有两个ChatModelSpring 不知道该把哪个注给ChatClient.Builder自动配置的 Builder 也依赖唯一的 ChatModel同样受影响。第一反应加Qualifier能解决注入点但自动配置的 Builder 还是懵的。正解是给主力模型标Primary让它继续充当默认模型其他模型按需注入。另外一个隐蔽的次生坑自动配置看到你自己定义了ChatModelBean 后条件装配会退位ConditionalOnMissingBean某些 starter 提供的默认参数装配也随之失效——所以我在上面每个 Bean 里都显式传了defaultOptions不依赖任何隐式默认。坑二切换智谱时模型名 404。配置从 DeepSeek 切到智谱第一发请求就报org.springframework.web.client.HttpClientErrorException$NotFound: 404 Not Found on POST request for https://open.bigmodel.cn/api/paas/v4/v1/chat/completions看 URL 就破案了/api/paas/v4/v1/chat/completions——路径里出现了两个版本段。Spring AI 的 OpenAI 客户端会在 base-url 后拼接默认路径/v1/chat/completions而智谱的兼容端点本身就以/api/paas/v4结尾它不需要也不认识后面的/v1。解决方法有两个要么把 base-url 写成https://open.bigmodel.cn/api/paas/v4并用配置项覆盖拼接路径spring.ai.openai.chat.completions-path设为/chat/completions要么自定义 Bean 时在OpenAiApi构造时指定路径。教训OpenAI 兼容各家对路径、鉴权头的实现细节不一接入新模型先用 curl 手发一次请求确认路径形状再进 Spring AI。五、小结与练习本篇要点ChatModel/EmbeddingModel 是模型抽象层自动配置按 starter 装配实现OpenAI 兼容协议是国产模型接入的事实标准DeepSeek/通义/智谱共用 openai starter切换只改 base-url/api-key/model一个应用多模型共存用多个手写 ChatModel Bean 按业务路由的 ChatClient 门面主力模型标Primary参数上 temperature 按场景分档maxTokens 管成本生产必备模型版本锁定与降级链路路由层同时是成本与合规的执行点。练习在 2.3 节代码基础上加第三档本地 Ollama提示引入spring-ai-starter-model-ollama本地ollama pull qwen2.5:1.5b后Ollama 的 ChatModel Bean 与 OpenAI 系互不冲突可直接装配然后把askWithFallback改成三级降级DeepSeek 失败切通义、再失败切本地模型。跑通后故意断掉一个 Key观察降级是否生效。下一篇《Prompt 工程PromptTemplate 与系统提示词》我们从换模型转向喂模型——同样一个模型提示词的写法能带来比换模型更大的效果差异。
返回列表