Spring AI 2.1 Message Parts: Reasoning That Survives the Round Trip

TL;DR · AI 摘要
Spring AI 2.1通过引入Message Parts机制,解决了多轮对话中推理数据丢失和顺序混乱的问题。
核心要点
- Spring AI 2.1将消息内容改为有序的message parts列表,确保推理数据完整性。
- 支持OpenAI、Anthropic、Gemini等8大主流模型的推理数据持久化。
- Session内存模块可持久化存储message parts,防止多轮对话中断。
结构提纲
按章节快速跳转。
思维导图
用一张图看清主题之间的关系。
查看大纲文本(无障碍 / 无 JS 友好)
- Spring AI 2.1 Message Parts
- 核心机制
- 有序列表结构
- 兼容多模型命名
- 改进点
- 解决顺序丢失问题
- 支持不透明数据存储
- Session内存持久化
金句 / Highlights
值得收藏与分享的关键句。
旧版本AssistantMessage无法保持内容块顺序,导致推理丢失。
新版本将消息内容改为有序列表,确保每次请求的顺序一致。
Session内存模块可持久化存储message parts,防止多轮对话中断。
Spring AI:消息片段文档 | 升级说明 | OpenAI 响应文档 | 会话文档
推理模型在回答之前不仅仅进行思考,它们会将部分思考过程返回给用户,并期望在下一次请求中重新获取这些内容。Claude 为其思考块添加签名,Gemini 在其函数调用中附加思考签名,OpenAI 的响应 API 返回加密的推理项。如果这些内容被丢弃、重新排序或修改,结果可能从 HTTP 400 错误到模型静默丢失先前推理并重新开始不等。
在此之前,Spring AI 的消息仅包含文本、工具调用和媒体的侧边列表,没有存储这些数据的空间,更不用说其顺序了。Spring AI 2.1 改变了数据模型本身:现在每条消息都以有序的消息片段列表形式保存内容。一个片段可以是响应的一部分,例如一段文本、一个推理块或一个工具调用。各提供商将这些片段称为内容块(Anthropic、Bedrock)、片段(Gemini)、项(OpenAI 响应)或块(Mistral),而 Spring AI 将它们统一映射为消息片段。
随着 2.1.0-RC1 版本的发布,主要的聊天模型开始生产和消费消息片段:OpenAI 响应、Anthropic、Google GenAI、OpenAI 聊天完成、Ollama、Bedrock Converse、DeepSeek 和 Mistral AI。该版本还新增了会话记忆功能,可以持久化这些片段。本文将介绍这一新抽象概念,解释响应 API 为何需要它,以及每个聊天模型从中获得的改进。归根结底,遵循三个规则:保持顺序、原样重放接收到的内容,并持久化片段而非文本。
为何字符串不足以满足需求
当启用思考功能时,Claude 在单次工具调用回合中会返回如下内容:
thinking "用户想要查询巴黎的天气。我应该调用 get_weather..." signature: "EqQBCgIYAhIM..."
text "让我为您查看。"
tool_use get_weather {"location": "Paris"}当您发送工具结果时,Anthropic 希望完全按照原始形式返回这一回合内容:相同的块、相同的顺序,包含签名。在 2.1 版本之前,AssistantMessage 无法实现这一点:
- 不同类型之间无顺序。 文本、工具调用和媒体分别存储在不同字段中,因此重放时必须猜测顺序。
- 无存储不透明数据的场所。 签名和加密推理是不透明数据:客户端无法读取它们,必须原样返回。它们要么存储在提供商特定的元数据键下,要么存储在包私有子类中。
- 流式传输丢失信息。 聚合器逐个合并块元数据键,导致思考内容通常仅保留最后一个片段。
- 内存丢失信息。
JdbcChatMemoryRepository仅存储文本和消息类型。
消息片段抽象概念
AbstractMessage 现在包含一个 parts: List<MessagePart>,被所有消息类型共享:

MessagePart 是一个密封接口,具有六个记录实现:
| 部分 | 包含内容 | 用途 | | --- | --- | --- | | TextPart | 一段文本 | 回答、提示 | | ReasoningPart | 推理文本、可选摘要、可选负载 | Claude 的思考过程、Gemini 的思考内容、DeepSeek 的 reasoning_content | | ToolCallPart | 一个 AssistantMessage.ToolCall、可选负载 | 函数调用 | | ToolResultPart | 一个工具结果,其内容是其他 MessagePart 的列表 | 携带非文本内容的工具结果 | | MediaPart | 一个 Media:图像、音频、视频、文档 | 多模态输入和输出 | | UnknownPart | Spring AI 未建模的块的原始 JSON | 服务器工具调用、引用、代码执行 |

引入 MessagePart 实现了以下目标:
- 顺序即数据。 各部分保留提供者生成它们时的顺序,因此推理块会始终出现在它所导致的工具调用之前。
- 不透明负载会被回放,而非被解释。 一个部分可以携带
OpaquePayload(provider, kind, data),例如 Anthropic 的signature、Gemini 的thought_signature或 OpenAI 的encrypted_content。生成它的ChatModel会原样返回它,其他所有聊天模型都会跳过它。这确保了在不同提供者之间移动历史记录时的安全性。 - 不会静默丢弃任何内容。 一个
ChatModel无法理解的块会变成UnknownPart,并原样回放给生成它的提供者。
此更改是向后兼容的。构造函数和构建器方法仍然有效,并按照 2.0 版本的顺序生成部分:文本、工具调用、媒体。getText()、getToolCalls() 和 getMedia() 现在是对部分的视图,而新的 getReasoning() 和 getParts() 返回所有内容并按顺序排列。需要更新的代码最可能是读取推理内容的部分,因为在 2.1 版本之前,每个聊天模型以不同的方式暴露了推理内容:
ChatResponse response = chatModel.call(prompt);
// 在 2.1 版本之前:每个聊天模型以自己的方式暴露推理内容
List<String> thoughts = response.getResults().stream() // Google GenAI
.filter(g -> Boolean.TRUE.equals(g.getOutput().getMetadata().get("isThought")))
.map(g -> g.getOutput().getText())
.toList();
String reasoning = ((DeepSeekAssistantMessage) response.getResult().getOutput())
.getReasoningContent(); // DeepSeek
// 2.1 版本:每个聊天模型都有一个统一的访问器
List<String> thoughts = response.getResult().getOutput().getReasoning().stream()
.map(ReasoningPart::text)
.toList();
String answer = response.getResult().getOutput().getText(); // 保持不变升级说明列出了每个聊天模型剩余的变更内容。在流式传输时,每个部分都会标记其内容块索引和partial标志,MessageAggregator会合并这些增量。聚合后的消息与阻塞调用返回的消息完全一致,包括签名。
[消息部分如何使响应API成为可能](https://spring.io/blog/2026/10/28/spring-ai-message-parts-reasoning-replay#how-message-parts-made-the-responses-api-possible)
Spring AI实现了两个OpenAI聊天模型端点。OpenAiChatModel使用Chat Completions,这是OpenAI最初的聊天API,许多其他服务器也实现了该接口。OpenAiResponsesChatModel是2.1版本新增的,使用OpenAI较新的Responses API(用于推理模型和代理的API)的无状态模式。
Responses API不会返回消息。每次调用会返回一个包含项目的output数组:reasoning、function_call和message,以及OpenAI自行运行的工具(如网络搜索)的项目。每个推理项目都带有自己的id和encrypted_content。
OpenAiResponsesChatModel的设计是无状态的:它不会在OpenAI端保存对话,但会在每次调用时重放所有内容,因此内存、顾问和RAG的工作方式与OpenAiChatModel完全一致。对于推理模型,只有在包含模型早期推理内容时,重放才能正常工作。OpenAI的推理指南要求从上次用户消息以来的推理项目、函数调用和函数调用输出。
在2.1版本之前,AssistantMessage无法忠实地保存这些项目:
- 它无法表达“推理→函数调用→消息”的顺序。
- 无法将数据附加到单个项目。每个推理项目都有id,并以加密形式(
encrypted_content)携带模型的推理内容,这两项必须随该项目一起返回。消息只能为整个响应提供一个元数据映射。 - 没有地方存储OpenAI自行运行的工具(如网络搜索)的项目,除非将其伪装成本地工具调用。
通过消息部分,映射是一对一的,并保持output数组的顺序:
| Responses API项目 | 消息部分 | | --- | --- | | reasoning | 带有摘要、OpaquePayload("openai.responses", "encrypted_content", …)和项目id的ReasoningPart | | function_call | 带有项目id的ToolCallPart | | message | TextPart | | image_generation_call | MediaPart | | 网络搜索、文件搜索、代码解释器、MCP、其他任何项目 | 带有原始JSON的UnknownPart |
在下一次请求时,聊天模型会按照相同顺序从消息部分重建输入项目。这种重放功能非常有价值。OpenAI现已归档的推理模型性能优化指南报告称,在SWE-bench测试中包含推理项目可带来约3%的性能提升。Responses API也是当前OpenAI模型中代理功能的端点。从GPT-5.4开始,Chat Completions不再支持除none以外的推理努力的工具调用。
其他所有聊天模型遵循相同模式:将每个响应块映射到一个部分,按顺序从各部分重建请求,并仅重放自己的负载内容。这种模式能带来的好处取决于每个提供商的具体需求:
| 聊天模型 | 当前可重放内容 | 改进效果 | | --- | --- | --- | | Anthropic | 签名并脱敏的思考内容(按块顺序);服务器工具块 | 工具调用时Claude能保持推理连续性,实现缓存命中 | | Google GenAI | 每个思考签名保留在承载它的部分 | Gemini 3+功能调用无错误,其他部分保持完整推理质量 | | OpenAI Chat Completions | reasoning_content | 在vLLM、gpt-oss、Kimi等兼容服务器上保持推理连续性 | | Ollama | thinking字段 | 在gpt-oss和qwen3工具循环中保持连续性 | | Bedrock Converse | 签名、脱敏和未签名的推理内容(包括流式传输时) | 延长思考时间的流式工具循环有效工作 | | DeepSeek | 流式传输和恢复消息的reasoning_content | 工具使用模式下无推理错误 | | Mistral AI | 按块顺序的思考块 | Mistral声称缺少这些块会导致输出质量下降 |
Anthropic
在工具使用回合中,Claude需要其思考块的返回,且"不能重新排列、编辑或部分删除这些块":修改后的块会触发HTTP 400错误。在2.1版本之前,Spring AI将回合重建为文本、思考、工具使用三部分(按Claude使用的顺序),同时会丢弃服务器工具块,流式思考内容也从未完整到达聚合消息。现在每个块作为一个部分,按顺序携带其签名进行重放。Anthropic指出这样能带来两个额外优势:保留思考内容"让Claude能从上次停止的位置继续推理",保留的思考块"能在工具使用期间实现缓存命中"。Claude Opus 4.5及更新版本、Sonnet 4.6及更新版本还能在上下文中保留早期回合的思考内容,这正是会话记忆的作用所在。
Google GenAI
Google的思考签名指南要求"将每个签名返回到接收它的精确部分"。在Gemini 3及更新版本中,功能调用缺少签名会导致请求"返回400错误"。在其他部分,"性能可能会下降"。此前Spring AI会将所有签名收集到一个列表中,再按位置重新附加到功能调用上。同时还会在工具循环中丢弃功能调用前的思考内容和文本。现在每个签名独立携带在自己的部分中,代码执行和服务器端工具部分也会按原始接收顺序返回。
OpenAI Chat Completions
OpenAI 自己的聊天完成 API 不返回推理文本,因此应使用响应来处理 OpenAI 的推理模型。OpenAiChatModel 服务于那些确实返回 reasoning_content 的兼容服务器,例如 vLLM、gpt-oss 部署和 Kimi。OpenAI 的 harmony 指南 要求在调用工具时将 gpt-oss 的思维链回传。这种回放功能在 2.0.x 版本中已通过元数据键实现,现在已集成到消息片段中,因此可以持久化。由其他提供商签名的推理内容不会被转发,对于拒绝该字段的服务器,可以使用 replayReasoningContent(false)。
Ollama
在 2.1 版本之前,思维内容仅存在于响应元数据中且不会被回传。在流式传输时,只有最后一个片段会被保留。现在,思维内容会通过助手消息的 thinking 字段进行回放,正如 Ollama 的 工具调用文档 所要求的:“在后续请求中,需将这些字段与任何工具结果一并返回”。收益取决于模型类型。gpt-oss 和 qwen3 模板使用当前工具循环的推理内容。
Bedrock Converse
Bedrock 应用 Claude 的规则:"在工具使用过程中,必须将思考块回传给 API 以用于最后一次助手消息"。BedrockProxyChatModel 之前会从流式响应中丢弃推理内容。现在,响应是按块顺序生成的单一片段,流式传输会携带推理内容、签名和经过删减的推理内容,甚至包括来自 gpt-oss 的未签名推理内容也会被回放。
DeepSeek
在 思考模式 中,携带工具的请求必须回传早期助手消息的 reasoning_content,否则 API 会返回 400 错误。在 2.1 版本之前,Spring AI 仅针对 DeepSeekAssistantMessage 实例发送该内容,因此流式响应和从内存中恢复的消息会丢失该信息。现在,任何助手消息(无论是否流式传输)都会回放其 ReasoningParts,DeepSeekAssistantMessage 已被弃用。
Mistral AI
Mistral 的 推理文档 指出:“始终回放完整的助手消息(包括 ThinkChunk)”。他们警告称,丢弃推理轨迹会“降低模型性能”。在 2.1 版本之前,MistralAiChatModel 从未回传思考内容。现在,思考内容、文本、引用和图像片段会作为独立部分按片段顺序回放,包括工具循环的多轮之间。
保留片段的内存:Spring AI Session
单个 ChatClient 调用内的回放只是故事的一半。调用之间,历史记录存储在聊天内存中,而 ChatMemory 仓库中只有内存型仓库会保留片段。使用其他类型仓库时,每次恢复对话回合模型都会从零开始进行推理。
Spring AI Session 现在作为 Spring AI 2.1 的一部分,填补了这一空白。会话是一个仅追加的事件日志。每条消息都以完整的内容存储,包括其所有部分、有效载荷和属性,而 JDBC 存储则以 JSON 格式保存这些内容。JdbcChatMemoryRepository 作为 ChatMemory 后端的 JDBC 存储,仅保留文本内容,完全丢弃工具调用和工具结果。对于推理模型,还有两个属性需要特别关注:
- 每次工具调用都会被记录。
SessionMemoryAdvisor在工具调用循环内部运行,因此它会存储每轮工具调用及其结果,而不仅仅是最终答案。每轮提示内容会根据会话重新构建,因此签名会完整地通过存储。 - 压缩操作遵循对话轮次。 当历史记录被裁剪或摘要时,截断始终发生在轮次边界。工具调用的结果永远不会丢失,其前面的推理内容也会始终保留。
添加 spring-ai-starter-model-session-jdbc 启动器,注册会话顾问并在每次调用时传递会话 ID:
@Bean
ChatClient chatClient(ChatClient.Builder builder, SessionService sessionService) {
return builder
.defaultAdvisors(SessionAdvisors.builder(sessionService)
.compaction(new TurnCountTrigger(20),
WindowCompactionStrategy.builder().lastTurns(10).build())
.build())
.build();
}
String answer = chatClient.prompt()
.user("Find the cheapest flight to Lisbon next Friday")
.advisors(SessionAdvisors.session("session-42"))
.call()
.content();通过上述配置,上述优势将跨调用和重启持续生效。Claude 仍能看到之前的推理过程,Gemini 会将每个签名正确返回到对应部分,DeepSeek 的推理模式规则在重新部署后依然有效。ChatMemory 及其存储库在 2.1 版本中已被弃用,将在 3.0.0 版本中彻底移除。迁移指南 映射了新旧 API 的对应关系。
### ⚠️ 需要了解的关键点
每个响应只生成一次。 在 2.1 版本之前,Anthropic、Google GenAI 和 Bedrock Converse 会将响应拆分为多个
Generation,分别对应推理、工具调用和文本。现在它们会为每个候选者返回一个整体。使用getResult().getOutput().getReasoning()读取推理内容。2.1 版本之前存储的历史记录不包含推理部分。 早期版本写入聊天内存的消息在读取时会丢失推理内容,因此无法重放。
跨供应商的历史记录是安全的,但并非完全无损。 由某个供应商签名的推理内容不会发送给其他供应商。在对话中切换模型时,文本和工具调用会保留,但其他供应商的推理内容不会保留。
保留部分属性。 Mistral 和 Bedrock(针对未签名的推理)仅在部分的
provider属性命名它们时才会重放推理。如果自行存储历史记录,请勿剥离属性。
结论
消息部分将 Spring AI 消息从带有附件的字符串转变为对模型生成内容及其顺序的忠实记录。关键见解:
- 顺序是数据的一部分。 推理模型会检查你返回内容的顺序和完整性。带有侧面列表的字符串无法承载这些信息。
- 原样回放接收到的内容。
OpaquePayload允许每个聊天模型返回其自身的签名而不做任何更改,并跳过其他所有模型的签名。 - 持久化存储部件而非文本。 会话内存存储每个工具轮次的所有部件,因此推理过程可以跨调用和重启持续存在。
资源
Spring AI
相关 Spring AI 文章