当前位置: 首页 > news >正文

Spring AI 源码解析(二):ChatModel 调用链路与消息处理

Spring AI 源码解析(二):ChatModel 调用链路与消息处理

上篇我们看完了自动配置,这一篇进入最核心的部分——一次chatClient.prompt().user("你好").call().content()到底经历了什么。

ChatClient 的门面模式

先看 ChatClient 的接口设计。为什么它要拆成PromptRequestSpecCallPromptRequestSpec两层?.prompt().call()之间是构建阶段.call()之后是执行阶段。分两层后 IDE 补全时,该出现的方法不会混淆。比如你在.call()之后只看到content()entity()chatResponse()这些结果提取方法,不会看到user()system()等构建方法。

publicinterfaceChatClient{ChatClient.PromptRequestSpecprompt();ChatClient.PromptRequestSpecprompt(Promptprompt);ChatClient.PromptRequestSpecprompt(Stringprompt);interfacePromptRequestSpec{PromptRequestSpecuser(Stringtext);PromptRequestSpecsystem(Stringtext);PromptRequestSpecmessages(List<Message>messages);PromptRequestSpecoptions(ChatOptionsoptions);CallPromptRequestSpeccall();StreamPromptRequestSpecstream();}interfaceCallPromptRequestSpec{Stringcontent();ChatResponsechatResponse();<T>Tentity(Class<T>type);List<String>list();Map<String,Object>map();}}

DefaultChatClient 的实现

ChatClient 本身不干活,它是门面,背后是 ChatModel:

publicclassDefaultChatClientimplementsChatClient{privatefinalChatModelchatModel;protectedclassDefaultCallPromptRequestSpecimplementsCallPromptRequestSpec{privateList<Message>messages=newArrayList<>();@OverridepublicStringcontent(){Promptprompt=buildPrompt();ChatResponseresponse=chatModel.call(prompt);returnresponse.getResult().getOutput().getContent();}}}

这里的response.getResult().getOutput().getContent()这一串 getter 链,ChatResponse 里包含的信息远不止文本内容——还有 finishReason(为什么结束)、usage(token 消耗)、metadata(元数据)。如果你只关心文本,用content()就行;如果你需要知道 token 消耗,用chatResponse()

ChatResponseresponse=chatClient.prompt().user("你好").call().chatResponse();Generationresult=response.getResult();intinputTokens=response.getMetadata().getUsage().getInputTokens();intoutputTokens=response.getMetadata().getUsage().getOutputTokens();

这个在实际项目中很有用——监控 token 消耗、统计成本,都靠这个。

ChatModel.call() 的核心链路

OpenAiChatModel 的 call 方法是整个链条的核心:合并选项 → 格式转换 → 带重试的 HTTP 调用 → 解析响应。

publicclassOpenAiChatModelimplementsChatModel{privatefinalOpenAiApiopenAiApi;privatefinalOpenAiChatOptionsdefaultOptions;privatefinalList<FunctionCallback>toolFunctions;privatefinalRetryTemplateretryTemplate;@OverridepublicChatResponsecall(Promptprompt){OpenAiChatOptionsmergedOptions=mergeOptions(prompt.getOptions());ChatCompletionRequestrequest=toChatCompletionRequest(prompt,mergedOptions);ChatCompletionResultresult=retryTemplate.execute(ctx->openAiApi.chatCompletion(request));returntoChatResponse(result,prompt.getInstructions());}}

mergeOptions 的优先级

mergeOptions决定了配置的覆盖顺序。这在实际项目中非常关键:

// 场景一:一切用默认配置chatClient.prompt().user("你好").call().content();// 场景二:运行时覆盖 model 和 temperaturechatClient.prompt().user("帮我写首诗").options(OpenAiChatOptions.builder().model("gpt-4o-mini").temperature(0.8).build()).call().content();// 场景三:多次调用共享 system promptvarspec=chatClient.prompt().system("你是诗人");spec.user("写首关于春天的").call();spec.user("写首关于秋天的").call();

配置优先级从上到下:运行时 options > 创建 Model 时的 defaultOptions > application.yml 中的配置。如果在代码里配置了.options(...),将会覆盖配置文件中的配置,例如在配置文件里设了temperature=0(要求严谨回答)将不会生效。

消息转换:跨厂商的兼容问题

从 Spring AI 的统一 Message 格式转到 OpenAI 的 ChatCompletionMessage:

privateChatCompletionRequesttoChatCompletionRequest(Promptprompt,OpenAiChatOptionsoptions){List<ChatCompletionMessage>messages=prompt.getInstructions().stream().map(this::toChatCompletionMessage).collect(Collectors.toList());returnnewChatCompletionRequest(messages,options);}privateChatCompletionMessagetoChatCompletionMessage(Messagemessage){returnnewChatCompletionMessage(message.getContent(),ChatCompletionMessage.Role.valueOf(message.getMessageType().name().toLowerCase()));}

这段代码看起来简单,但切换到不同厂商时会有兼容性问题。Spring AI 内部定义了统一的 MessageType 枚举(USER、ASSISTANT、SYSTEM、TOOL),然后每个厂商的适配器自己转成厂商的格式。

在实际使用中,消息转换出可能会遇到的几个问题:

问题现象原因
角色不匹配Ollama 调用报 400消息角色名不对应
消息顺序错乱模型回答质量差System 消息位置不对
空内容异常反序列化抛 NPEAPI 返回了 null delta

特别是消息顺序——System 消息必须放在最前面,这是大部分 LLM 的硬性要求。如果你通过 Advisor 在运行时注入了新的 System 消息,顺序问题就需要自己留意了。

角色映射关系

Spring AI 的 MessageType 和 OpenAI Role 的对应关系:

Spring AI MessageTypeOpenAI Role说明
USERuser用户问题
SYSTEMsystem系统提示词
ASSISTANTassistantAI 回复
TOOLtool工具调用结果

如果你自定义 Message 类型,需要确保 MessageType 能映射到目标厂商支持的角色。否则 API 调用会报 400。

响应解析:注意空指针

privateChatResponsetoChatResponse(ChatCompletionResultresult,List<Message>instructions){List<Generation>generations=result.choices().stream().map(choice->{AssistantMessagemessage=newAssistantMessage(choice.message().content(),Map.of("role",choice.message().role().name()));returnnewGeneration(message,Map.of("finishReason",choice.finishReason()));}).collect(Collectors.toList());returnnewChatResponse(generations,Map.of("model",result.model(),"usage",result.usage().toString()));}

这段代码有个潜在问题:网络超时或 API 异常时,result.choices()可能为空或者为 null。stream()调用直接 NPE。

在实际开发中可能遇到这个问题——OpenAI API 偶尔返回的 choices 数组是空的。虽然不频繁,但每次出现就会抛异常。所以建议在自己的业务代码里也加一层保护:

Stringcontent=chatClient.prompt().user(msg).call().content();// 如果 content 为 null 或者 response 出错,要有兜底逻辑

重试机制的配置经验

@Bean@ConditionalOnMissingBeanpublicRetryTemplateopenAiRetryTemplate(OpenAiConnectionPropertiesproperties){RetryTemplateretryTemplate=newRetryTemplate();retryTemplate.setRetryPolicy(newSimpleRetryPolicy(properties.getMaxRetries()));ExponentialBackOffPolicybackOff=newExponentialBackOffPolicy();backOff.setInitialInterval(1000);backOff.setMaxInterval(10000);retryTemplate.setBackOffPolicy(backOff);returnretryTemplate;}

Spring AI 默认的 RetryTemplate 配置是:最多重试 3 次、初始间隔 1 秒、指数退避到最大 10 秒。这个配置在大部分场景下够用,但我根据项目需求调过几次:

批量处理场景(比如凌晨定时分析文档):

@BeanpublicRetryTemplatebatchRetryTemplate(){RetryTemplateretryTemplate=newRetryTemplate();retryTemplate.setRetryPolicy(newSimpleRetryPolicy(5));ExponentialBackOffPolicybackOff=newExponentialBackOffPolicy();backOff.setInitialInterval(2000);backOff.setMaxInterval(30000);retryTemplate.setBackOffPolicy(backOff);returnretryTemplate;}

实时对话场景(比如客服机器人):

@BeanpublicRetryTemplatechatRetryTemplate(){RetryTemplateretryTemplate=newRetryTemplate();retryTemplate.setRetryPolicy(newSimpleRetryPolicy(1));// 只重试一次ExponentialBackOffPolicybackOff=newExponentialBackOffPolicy();backOff.setInitialInterval(500);backOff.setMaxInterval(2000);retryTemplate.setBackOffPolicy(backOff);returnretryTemplate;}

注意@ConditionalOnMissingBean——你只要自己定义一个 RetryTemplate Bean,框架就用你的。如果你不定义,框架就用默认的。

调用链路总结

用一张图概括整个流程:

chatClient.prompt().user("你好").call().content() ↓ ChatClient 构建 Prompt(收集 user、system 消息和 options) ↓ OpenAiChatModel.call(prompt) ├→ mergeOptions() 合并配置(运行时 > 默认 > 配置文件) ├→ toChatCompletionRequest() Message → OpenAI 格式 ├→ RetryTemplate.execute() 重试(默认 3 次,指数退避) │ └→ OpenAiApi.chatCompletion() RestClient POST 请求 └→ toChatResponse() JSON → ChatResponse 对象 ↓ 提取 content() 返回文本

每一步都可能出问题。消息格式错了报 400,API 超时报 504,Choices 为空抛 NPE。了解每一步做了什么,排查问题时就能快速定位到具体环节。

http://www.jsqmd.com/news/926350/

相关文章:

  • 手把手教你:在Docker容器或WSL里修复Ubuntu的systemctl命令报错(附原理图解)
  • AI写论文的宝藏工具!4款AI论文写作助手,让你的写作过程更顺畅
  • 你的无线网卡支持Monitor模式吗?在Ubuntu上快速自查与选购指南(避坑无线网卡驱动)
  • 循环结构:死循环,循环嵌套
  • 如何用VinXiangQi打造你的智能象棋AI助手:从零开始到专业级分析
  • 深入xv6内核:为每个进程创建独立内核页表到底解决了什么问题?
  • 同样叫 OpenClaw,为什么 .NET 版和原生版根本不是一回事
  • AI 写代码的安全性漏洞与 Token 浪费,两个工具搞定
  • Matlab版柔性车间调度工具包:用NSGA-II同时压缩短工期和降能耗
  • 运维效率翻倍:用Xmanager + SSH隧道安全访问内网Linux图形界面(保姆级配置)
  • Browser Use — AI驱动浏览器自动化的全新范式
  • Word文档样式一致性检查与批注批量导出工具(Python实现)
  • 保姆级教程:在Linux上从零配置TongLINKQ 8.1.15.2客户端,实现与服务端通信
  • 光学加密技术如何革新音频安全防护
  • 2026 青岛纹眉门店实地体验测评:多家门店综合实力盘点 - 小艾信息发布
  • JDK8 Optional详解入门:彻底告别Java空指针异常
  • Beyond Compare 5逆向工程:RSA非对称加密授权机制深度解析与密钥生成器实战
  • Cora和Citeseer数据集上可直接运行的GCN链路预测代码包(含预处理、训练与评估)
  • 2026年台州税务代理公司选对=合规高效 企赢税务智能财税推荐(含联系方式) - 本地品牌推荐
  • 2026年Trae与Claude Code优缺点对比:深度横评解析
  • MATLAB近场动力学三模型对比包:含稳定化实现、零能模式修正与能量/位移可视化
  • 运维排查手记:一次用户被锁定的故障,我是如何用faillock命令快速定位并解决的
  • Java TCP聊天室完整实现:含可运行工程、操作视频与详细课程设计文档
  • STM32F103 RGB灯PWM调光工程(KEIL环境,J-Link/ST-Link双调试器支持)
  • 2026 年郑州化妆品柜展柜厂家技术与服务分析报告
  • STM32F103扫地机器人实战工程:FreeRTOS多任务调度+IAP远程升级+电池与传感器全链路管理
  • 十年 PM 走心总结:职场管理者的底层逻辑
  • 告别Ubuntu 22.04默认Dock:这几个gsettings命令和Gnome扩展让你效率翻倍
  • 微信小程序人脸实时定位源码(含相机调用、检测框绘制与多页面示例)
  • 告别系统升级焦虑:Ubuntu 22.04 LTS 到 24.04 LTS 保姆级升级指南(含 do-release-upgrade 详解)