资讯详情

资讯详情

第4章:Spring AI 2.0采样参数与推理模型

上章思考题答案ChatOptions接口本身的常见可变入口是各实现类的mutate()。若运行时对象不是DeepSeekChatOptions强转会失败。多模型工程应依赖ChatOptions上稳定的字段模型名、温度、最大 tokenDeepSeek 专有字段再在确认类型后使用。不要在业务服务里写死强转除非该服务的注入点就是DeepSeekChatModel。先怀疑消息没带上。token 粒度通常不会把两句中文收成 1。应断言prompt.getInstructions()的角色序列和订单号文本再比较promptTokens。结构断言失败就是没带上结构对、token 却几乎不变才去查用量字段是否被覆盖成缓存值。1. 项目背景1.1 业务场景客服主管拿同一道题问了两次机器人「订单超时未发货能不能赔」第一次回答像公告列出三条步骤。第二次回答像在聊天还多了一句「也可以考虑补偿优惠券」。制度并没有优惠券。主管要求对外话术稳定、短、不新增补偿方式对内可以有一份更慢、带推理过程的答案供质检抽查但不能直接贴给用户。1.2 痛点温度、topP、最大 token 和模型名都写在一个 yml 里全站共用。问候接口想要一点变化超时赔付接口却被同一套参数带着跑于是赔付建议开始漂移。另有人把模型改成deepseek-reasoner后接口耗时从一秒变成十几秒网关先超时用户看到 504质检却说「模型其实想好了」。推理文本如果和正式回答拼在同一个字符串里展示用户会看到大段自言自语。没有分层时的路径一个全局温度 → 话术漂移 → 换推理模型 → 时延打满网关 → 推理过程泄露到对话框。试运行那天主管在用户群里看到一段「让我先想清楚赔偿边界……」后面才是正式答复。那段自言自语来自推理字段被整对象序列化进了调试接口。本章把旋钮拆开并规定用户响应里只有正文。对外超时设为 8 秒质检入口不走这条超时。用户入口的线程池也不要和质检共用一个很小的上限否则一次慢的抽查会把问候请求排在后面。本章实验在笔记本上串行跑两次即可不要用生产线程池压测。同一问题: 超时未发货能不能赔deepseek-v4-flash temperature 0.2deepseek-reasoner短答复 低时延reasoningContent 仅内部正式答复2. 项目设计小胖说「温度不就是火候吗火大一点回答鲜活火小一点回答死板。客服要亲切把温度调到 1.5。」小白问「亲切和编造优惠券怎么区分火候大了同一道题两次回答不一样测试的黄金句还怎么写topP 和温度一起拧是更稳还是更乱我需要知道实验记录里一次只许动哪一个旋钮。」小胖又说「那就把最大 token 卡死它爱编也编不长。窗口限量总不会端出一盆。」小白追问「卡死之后用户看到半句话算稳定吗推理模型是不是本来就会先写很长一段思路你这一卡正式答案还没出来就断了还有温度 0 是不是就等于把制度原文背下来背不下来的时候你怪参数还是怪没给制度」大师把旋钮分开「temperature在DeepSeekChatOptions里默认 0.7范围按供应商文档是 0 到 2。售后对外话术用 0.2让同一制度题少换说法。topP是另一种采样约束供应商建议不要和温度同时大改。本章实验一次只动一个旋钮。maxTokens限制的是生成长度不是事实正确性。它能阻止长篇不能阻止错误的短句。」技术映射火候是temperature另一次筛选是topP篇幅上限是maxTokens。三者都在DeepSeekChatOptions。小胖还想同时拧 topP「窗口只留一种汤会不会太单调topP 再松一点回答就有个人味道。」小白说「两个旋钮一起动实验记录无法写原因。温度 1.3 且 topP 很低时你甚至说不清是火候还是筛选把句子变短。本章一次只动温度topP 保持默认写进实验表的备注栏。」小白再问「温度 0.2 仍写出优惠券算参数失败还是提示词失败我不会用调低温度来掩盖没有制度约束。」大师说「算提示词和断言的失败不算采样成功。低温度只减少漂移不制造事实。禁止项断言必须留在测试里。若 0.2 仍出现优惠券把样本交给第 5 章的模板去收紧店规而不是把温度调到 0 碰运气。实验表的备注栏写明 topP 未改。谁若同时改了 topP这张表作废重新跑。」小白问模型「deepseek-chat和deepseek-reasoner还在枚举里。新代码写哪个」小胖说「聊天用 chat动脑子用 reasoner名字已经说明问题了。」小白追问「枚举上的Deprecated(since 2.0.0)你看见没有默认常量DEFAULT_CHAT_MODEL已经是DEEPSEEK_V4_FLASH。推理字段还在不在content里」大师对源码「DeepSeekApi.ChatModel里DEEPSEEK_CHAT和DEEPSEEK_REASONER都标记了废弃。当前默认是deepseek-v4-flash另有deepseek-v4-pro。废弃名仍可能被接口接受但新代码优先用 V4。推理过程不在getText()而在DeepSeekAssistantMessage.getReasoningContent()。DeepSeekChatModel构建助手消息时把协议字段reasoning_content放进这个属性。对外只展示getText()。本章对比两组一组是默认的deepseek-v4-flash加温度 0.2一组是仍可用的deepseek-reasoner用来观察推理字段和耗时。V4 是否返回推理内容以当时的接口为准代码必须对 null 安全。」技术映射对外正文是AssistantMessage.getText()对内思路是DeepSeekAssistantMessage.getReasoningContent()。默认模型常量是DEEPSEEK_V4_FLASH。小胖看着秒表「慢的那个更聪明客服就用慢的。」小白问预算「网关超时 8 秒。推理模型 P95 若是 15 秒用户收到的是 504不是聪明的答案。token 也可能因为推理过程上涨。质检要看思路用户不要看。这两条链路是一个接口的两个字段还是两个接口」小白接着问「温度实验和模型实验叠在同一次调用里表格还能不能看出是谁让回答变长我要求实验记录一次只改一列。」大师给默认选择「面向用户的超时赔付回答用deepseek-v4-flash温度 0.2maxTokens256只返回getText()。质检抽查另开一个内部接口显式指定deepseek-reasoner或后续的 V4 推理配置把reasoningContent写进质检库不写进用户响应。内部接口的网关超时单独放宽并限流。不要用一个 Bean 的默认模型服务这两种时延。温度对照固定模型为 flash模型对照固定温度为 0.2。两张表不要合并成一次『我觉得更好』的结论。测试把禁止项写进自动断言运维只接受打印出来的毫秒和 token不接受口头的快慢。」技术映射用户链路和质检链路是两次Prompt各带自己的DeepSeekChatOptions不是把推理文本拼进用户可见字符串。3. 项目实战3.1 环境准备继续使用第 2 章工程。不要改 yml 的默认模型对比放在请求级 options 里避免把全站问候接口一起切到推理模型。准备秒表System.nanoTime()。密钥仍是DEEPSEEK_API_KEY。同一道题固定为订单已超时未发货。根据制度只能登记超时并告知用户等待或申请退款不能发明优惠券或现金赔偿。请用两句话回答能不能赔。3.2 分步实现步骤 1同一模型上只改温度看话术是否漂移步骤目标证明温度会改变措辞所以对外接口要把温度写死在请求上。packagecom.linlicang.aftersale;importorg.springframework.ai.chat.model.ChatResponse;importorg.springframework.ai.chat.prompt.Prompt;importorg.springframework.ai.deepseek.DeepSeekChatModel;importorg.springframework.ai.deepseek.DeepSeekChatOptions;importorg.springframework.ai.deepseek.api.DeepSeekApi;publicclassSamplingLab{publicstaticfinalStringQUESTION 订单已超时未发货。根据制度只能登记超时并告知用户等待或申请退款不能发明优惠券或现金赔偿。请用两句话回答能不能赔。 ;privatefinalDeepSeekChatModelchatModel;publicSamplingLab(DeepSeekChatModelchatModel){this.chatModelchatModel;}publicChatResponseask(doubletemperature){DeepSeekChatOptionsoptionsDeepSeekChatOptions.builder().model(DeepSeekApi.ChatModel.DEEPSEEK_V4_FLASH).temperature(temperature).maxTokens(256).build();returnthis.chatModel.call(newPrompt(QUESTION,options));}}运行结果文字描述温度 0.2 的两次回答用词接近都包含「退款」或「等待」不出现「优惠券」。温度 1.3 的两次回答措辞差更大仍不应出现优惠券若出现记为失败样本而不是「更有人味」。可能的坑只跑一次就下结论。采样实验至少同一温度跑两次。仍可能碰巧相似所以断言放在禁止项不含「优惠券」「现金」上不把某一句全文写死。步骤 2对比 flash 与 reasoner 的耗时和 token步骤目标拿出一张能给主管看的表模型、耗时毫秒、prompt token、completion token、是否含推理文本。packagecom.linlicang.aftersale;importorg.springframework.ai.chat.messages.AssistantMessage;importorg.springframework.ai.chat.model.ChatResponse;importorg.springframework.ai.chat.prompt.Prompt;importorg.springframework.ai.deepseek.DeepSeekAssistantMessage;importorg.springframework.ai.deepseek.DeepSeekChatOptions;importorg.springframework.ai.deepseek.api.DeepSeekApi;publicrecordModelSample(Stringmodel,longelapsedMillis,intpromptTokens,intcompletionTokens,Stringanswer,Stringreasoning){publicstaticModelSamplecapture(StringmodelName,ChatResponseresponse,longelapsedNanos){AssistantMessageoutputresponse.getResult().getOutput();Stringreasoning;if(outputinstanceofDeepSeekAssistantMessagedeepSeekMessagedeepSeekMessage.getReasoningContent()!null){reasoningdeepSeekMessage.getReasoningContent();}returnnewModelSample(modelName,elapsedNanos/1_000_000,response.getMetadata().getUsage().getPromptTokens(),response.getMetadata().getUsage().getCompletionTokens(),output.getText(),reasoning);}}在SamplingLab中增加publicModelSampletime(DeepSeekApi.ChatModelmodel){DeepSeekChatOptionsoptionsDeepSeekChatOptions.builder().model(model).temperature(0.2).maxTokens(512).build();longstartSystem.nanoTime();ChatResponseresponsethis.chatModel.call(newPrompt(QUESTION,options));longelapsedSystem.nanoTime()-start;returnModelSample.capture(model.getValue(),response,elapsed);}运行结果文字描述deepseek-v4-flash的elapsedMillis通常低于deepseek-reasoner。reasoner 的reasoning字段往往非空answer仍应是短答复。flash 的reasoning允许为空字符串。completion token 在 reasoner 一侧更高。把四列打印成文本表即可不必上图表库。可能的坑用getText()判断有没有推理。推理不在正文里时这个判断永远是否。必须instanceof DeepSeekAssistantMessage。废弃枚举会有编译警告本章实验方法可以保留对DEEPSEEK_REASONER的引用并在注释里写明「仅用于对照用户链路禁止作为默认」。把两次ModelSample抄进实验记录列不要只留「感觉更快」。至少包括模型名、毫秒、两侧 token、答案是否含「优惠券」、推理字段长度。推理字段长度用reasoning.length()为空则记 0。用户链路的默认选择写在记录最后一行DEEPSEEK_V4_FLASH、温度 0.2、最大 token 256公开 DTO 不含推理。质检若要 reasoner另起一行写明网关超时必须单独配置不能沿用用户链路的 8 秒。运行结果补充人工阅读这张表就能回答主管的三个问题对外用哪个模型、慢多少、推理文本有没有漏到答案字段。自动测试不把某一次的毫秒数写死。步骤 3用户响应里去掉推理字段步骤目标即使内部调用了 reasoner返回给 Controller 的 DTO 也只有正文。packagecom.linlicang.aftersale;publicrecordPublicAnswer(Stringtext,StringfinishReason){publicstaticPublicAnswerfrom(org.springframework.ai.chat.model.ChatResponseresponse){vargenerationresponse.getResult();returnnewPublicAnswer(generation.getOutput().getText(),generation.getMetadata().getFinishReason());}}质检 DTO 才包含reasoning。两个方法不要复用同一个返回类型。运行结果文字描述PublicAnswer的字符串里没有reasoningContent的大段自我对话。质检对象里能看到。日志若打印整个DeepSeekAssistantMessage.toString()推理文本仍会进日志这不算「没返回给用户」。可能的坑toString()包含reasoningContent。访问日志不要打印消息对象本身。步骤 4把实验收成一张可以贴进评审的文本表步骤目标主管和测试看到同一份四列记录而不是两段聊天截图。在测试方法末尾用固定格式打印列与列之间用竖线避免中文逗号把数字拆开model | ms | promptTokens | completionTokens | reasoningChars | coupon deepseek-v4-flash | 900 | 40 | 60 | 0 | false deepseek-reasoner | 4000 | 40 | 300 | 800 | false数字以当次运行为准。coupon列由answer.contains(优惠券)得出必须是 false 才允许把该模型留在候选名单。reasoningChars是推理字段长度不是正文长度。评审时如果有人提议把 reasoner 设成用户默认模型把这张表的毫秒列指给他看并重申用户网关是 8 秒。运行结果文字描述控制台两行数据flash 的推理长度为 0 或很短reasoner 的推理长度通常大于 0耗时通常更长。自动断言仍只锁禁止项和公开 DTO不锁毫秒。表留给人工选型。可能的坑两次调用之间改了 yml 并重启表上的模型名和 options 不一致。实验期间不要改默认配置全部差异放在请求级DeepSeekChatOptions。另一坑是把推理正文本身贴进评审邮件。邮件只贴长度和是否含优惠券。表贴出之前开发把公开 DTO 的字段列表附在表下只有text和finishReason。测试核对 reasoner 那一行的答案列不含推理原文。运维核对毫秒列是否已经超过用户网关的 8 秒超过则该模型不能进用户默认配置。三个人看同一张表的不同列避免再开一轮「谁觉得更快」的讨论。表的文件名带上日期和 git 提交号例如sampling-20261006.txt放在测试输出目录而不是源码目录避免把某次 token 数提交进仓库变成过期断言。3.3 完整代码清单SamplingLab、ModelSample、PublicAnswer和下一节测试。源码对照models/spring-ai-deepseek/.../DeepSeekChatOptions.javamodels/spring-ai-deepseek/.../DeepSeekAssistantMessage.javamodels/spring-ai-deepseek/.../api/DeepSeekApi.java中的ChatModel枚举models/spring-ai-deepseek/.../DeepSeekChatModel.java中组装reasoningContent的位置3.4 测试验证packagecom.linlicang.aftersale;importorg.junit.jupiter.api.Test;importorg.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.boot.test.context.SpringBootTest;importorg.springframework.ai.deepseek.api.DeepSeekApi;importstaticorg.assertj.core.api.Assertions.assertThat;SpringBootTestEnabledIfEnvironmentVariable(namedDEEPSEEK_API_KEY,matches.)classSamplingLabTest{AutowiredprivateSamplingLabsamplingLab;TestvoidlowTemperatureDoesNotInventCoupon(){Stringoncethis.samplingLab.ask(0.2).getResult().getOutput().getText();Stringtwicethis.samplingLab.ask(0.2).getResult().getOutput().getText();System.out.println(t0.2-aonce);System.out.println(t0.2-btwice);assertThat(once).doesNotContain(优惠券);assertThat(twice).doesNotContain(优惠券);assertThat(once).doesNotContain(现金);}TestvoidreasonerIsSlowerAndKeepsReasoningOffPublicDto(){ModelSampleflashthis.samplingLab.time(DeepSeekApi.ChatModel.DEEPSEEK_V4_FLASH);ModelSamplereasonerthis.samplingLab.time(DeepSeekApi.ChatModel.DEEPSEEK_REASONER);System.out.println(flash);System.out.println(reasoner);assertThat(flash.answer()).doesNotContain(优惠券);assertThat(reasoner.answer()).doesNotContain(优惠券);assertThat(reasoner.elapsedMillis()).isPositive();PublicAnswerpublishednewPublicAnswer(reasoner.answer(),STOP);assertThat(published.text()).doesNotContain(reasoner.reasoning());}}第二条断言在reasoning为空时会通过得太容易。补一条若reasoning非空则published.text()不等于把推理和答案拼在一起的字符串并且published.toString()不包含推理正文。预期控制台能看到两行ModelSamplereasoner 的耗时和 completion token 不低于 flash 是常见结果但不要把「必定更慢」写成唯一断言网络抖动会让单次样本反转。把耗时打印出来给人工看自动断言集中在「不发明优惠券」和「推理不进公开 DTO」。客服场景的默认选择写进测试注释和 Wiki用户接口使用DEEPSEEK_V4_FLASH、温度 0.2、最大 token 256推理模型只给质检。4. 项目总结4.1 优点与缺点对比项全局一个温度、一个模型请求级 DeepSeekChatOptions话术稳定性被最活跃的那个接口拖着走赔付接口可单独压低温度时延推理模型拖垮问候用户链路不使用 reasoner推理过程混在正文里reasoningContent与getText()分离实验成本改配置影响全站一次请求一个选项废弃名容易继续写 deepseek-chat枚举把废弃暴露给编译器优点同一道业务题可以量化耗时和 token而不是凭感觉说「这个模型更好」。温度实验和模型实验分开结论不会缠在一起。推理文本有独立字段公开 API 可以物理上不返回它。默认模型已经指向 V4新代码少踩废弃名。缺点单次样本不能代表 P95本章数字只适合选型讨论不适合写进 SLA。低温度不能保证不编造仍要靠禁止项断言和制度检索。废弃的 reasoner 名称以后可能直接失效质检链路要准备迁移到 V4 的推理配置。maxTokens设大才会看全推理成本上升快。4.2 适用场景适用对外话术要稳定的制度问答例如能不能赔。需要向业务展示「换模型要多花多少时间和 token」。质检要留推理过程产品要求用户看不到。一次只调整温度或模型做对照实验。代码评审里禁止把废弃模型写成 Bean 默认值。不适用需要统计意义上的 P95。那要固定脚本跑上百次并拉开网关超时不是本章的两次调用。任务本身依赖图像或语音模型。采样参数的名字看起来像但选项类型不是DeepSeekChatOptions。4.3 注意事项不要同时把温度调到 1.5 且把 topP 调到极端值否则无法解释是哪一个旋钮导致漂移。用户响应 DTO 不要包含DeepSeekAssistantMessage。序列化整个消息对象会带出reasoningContent。DEEPSEEK_CHAT与DEEPSEEK_REASONER的废弃注解是编译期信号。压制警告而不改调用点会在供应商下线旧名时同时故障。推理调用的maxTokens要大于用户链路否则正式答案被推理过程挤掉finishReason变为截断。4.4 常见踩坑全站接口突然变慢。有人把 yml 的model改成deepseek-reasoner做实验后提交了。根因是默认 Bean 被质检需求污染。处理回滚配置质检使用请求级 options。用户看到大段「首先、其次、让我思考」。根因是 Controller 返回了getReasoningContent()或日志采集把消息toString回显到了前端调试接口。处理公开类型只用PublicAnswer调试接口加内网和权限。低温度仍然承诺了现金赔偿。根因是把采样参数当成事实约束。处理禁止项断言进 CI正式环境还要接第 14 章以后的制度检索不能只靠温度。4.5 思考题质检接口和用户接口共用一个DeepSeekChatModelBean只是Prompt上的 options 不同。这够不够若不够差在超时、限流还是日志reasoningContent为 null 时你的质检页面显示空白还是显示「本模型未返回推理」两种选择对排障有什么差别答案在第 5 章开头。延伸阅读与资源10倍开发者的 Dify 魔法书从零构建全栈 AI 应用后端工程师转型AI第一课-Ollama 与私有化大模型实战大型语言模型(LLM) vLLM 高性能推理落地实战Agent开发之LlamaIndex 实战修炼与源码进阶大语言模型Transformers 实战修炼与源码剖析
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →