Spring AI 源码解析(二):ChatModel 调用链路与消息处理
2026/6/1 1:52:58 网站建设 项目流程

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。了解每一步做了什么,排查问题时就能快速定位到具体环节。

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

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

立即咨询