当前位置: 首页 > 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.rkmt.cn/news/1437682.html

相关文章:

  • MATLAB版GA-PSO混合优化代码包:含交叉选择机制、双测试数据与详细中文使用指南
  • 同样叫 OpenClaw,为什么 .NET 版和原生版根本不是一回事
  • AI 写代码的安全性漏洞与 Token 浪费,两个工具搞定
  • Browser Use — AI驱动浏览器自动化的全新范式
  • JDK8 Optional详解入门:彻底告别Java空指针异常
  • MATLAB近场动力学三模型对比包:含稳定化实现、零能模式修正与能量/位移可视化
  • PHP人脸识别与图像AI处理集成
  • Matlab版双强度GS相位恢复工具包:含仿真、迭代求解与标准流程脚本
  • Python算法基础篇之斐波那契数列详解
  • 别再踩坑了!Ubuntu 22.04 上 Zabbix 6.0 保姆级安装与配置全记录(含MySQL 8.0适配)
  • CASME2微表情识别工具:支持摄像头实时捕捉、单图识别与视频逐帧分析
  • 锂离子电池RUL预测实战包:Python代码+多尺度采样数据+预训练时序模型
  • CentOS 7上Python 3连接达梦数据库:保姆级dmPython驱动编译安装指南(含环境变量避坑)
  • 避坑指南:在Ubuntu 20.04上从零搭建OSTrack训练环境(含GOT-10k数据集处理)
  • 【Gemini中文处理能力深度测评】:20年NLP专家实测12项指标,98.7%准确率背后的3大技术突破
  • 使用C语言重写“strcat”和“strcmp”两个方法
  • 别再死记硬背公式了!用Python从零手搓一个BP神经网络(附完整代码)
  • ICM20948九轴DMP姿态解算工程套件:含驱动配置、串口调试与3D可视化工具
  • PACS 影像云解决方案深度评测与选型指南
  • 告别重装烦恼:用CGI-Plus v5.0.0.6单文件版,5分钟搞定Windows系统备份与还原
  • 龙城秘境手游官网下载:2026 年 6 月最新官方下载渠道
  • Linux Mint系统恢复翻车实录:手把手教你正确配置Timeshift快照(附断电重启大法)
  • 新手学习全过程实录06——零基础搭建鸿蒙天气应用
  • 校园外卖系统毕业设计全套:SpringBoot+Vue可运行源码+数据库+论文+答辩PPT+实操视频
  • 厨房质检员——从阿明的“祖传配方“到标准化质检,看测试金字塔的落地
  • 视频号解析接口上线!无需登录,复制链接直接解析
  • 2026可靠科尔摩根驱动器选购推荐:伦茨制动器、伦茨变频器、伦茨控制器、伦茨电机、伦茨直流调速器、伦茨维修、伦茨驱动器选择指南 - 优质品牌商家
  • 无人机非定常气流控制与VPM模型实时实现
  • 再薅嘉立创羊毛
  • 联想电脑丢了F11一键还原?手把手教你用官方工具找回原厂系统(含Office)