Spring AI 2.1 的工具确认:会话为什么要按轮压缩
发布时间:2026/10/27 8:07:06 锦皓数字建站

一句话结论这一版把智能体开发里最容易出事的两件东西做成了框架能力——工具调用可以暂停等待批准长会话按轮而不是按消息条数压缩。项内容摘要Spring AI 2.1.0-RC1 于 10 月 23 日发布工具调用确认让高危动作可以先停后放行会话记忆把「日志 视图」拆开压缩一定切在轮次边界上。本文拆清两处的机制与迁移代价用一个可运行脚本量化按消息截断会留下多少处被割裂的工具调用并给出 ChatMemory 体系弃用后的替换路径。前置知识Java 与 Spring Boot 的基本用法知道大模型多轮对话里 tool call 与 tool result 是成对出现的阅读时间约 14 分钟代码环境Python 3.13.12零第三方依赖用 Python 复刻压缩语义不需要启动 JDK读完你能做什么能讲清 requiresConfirmation 的暂停与恢复边界能在自己的会话压缩策略里避开「工具调用没结果」的坑能列出从 ChatMemory 迁到 Session Memory 要改的四个地方一手来源是 Spring 官方博客《Spring AI 2.1.0-RC1 and 2.0.2 Available Now》2026-10-23配套发布在同一天的还有 Spring Boot 4.2.0-RC1、Spring Boot 4.1.2、Spring Cloud 2026.0.0-RC1Paddington与 Spring Cloud 2025.1.4。本文只讨论与智能体工程直接相关的部分。一、这次 RC 里真正重要的只有两件事整份 Release Notes 可以分成三类前两类是骨架第三类是兼容性细节。类别内容影响面消息模型MessagePart 在 OpenAIChat Completions、Anthropic、Google GenAI、Bedrock Converse、Ollama、Mistral AI、DeepSeek 上原生生产与消费推理内容、工具调用与各家私有字段能在整段对话里保序智能体能力会话记忆Session Memory 工具调用确认tool call confirmation长会话怎么存、高危动作怎么拦检索与兼容支持调用方自带查询向量做相似度检索若干旧 API 标记为弃用向量库选型与迁移成本真正值得花时间的只有中间那一行。消息模型部分的结论可以一句话说完以前各家的私有字段例如 Anthropic 的 thinking signature、Gemini 的 thought signature在多轮之间容易丢现在整段对话里保序传递顺带加了一个开关spring.ai.openai.chat.replay-reasoning-content因为有些兼容端点如 DeepSeek 的 thinking 模式要求回传reasoning_content另一些如 Groq则会直接拒绝这个字段。二、工具确认把「模型说调就调」改成可暂停的循环智能体开发里有一堵所有人都撞过的墙工具调用循环默认是自动的。查天气无所谓删除文件、发起转账、发一封外部邮件你一定希望中间能插进一个人。2.1 给出的解法分三步第一步在工具上标注这需要批准Tool(description 向供应商发起付款申请, requiresConfirmation true) public String createPayment(String vendorId, BigDecimal amount) { ... }第二步运行时把这次调用变成一个暂停点。官方的表述是「暂停是一个普通响应恢复是一个普通请求」框架侧不保存任何状态——这意味着你不需要为此引入一套工作流引擎也不需要在框架外维护「谁挂起了」。第三步由应用侧按策略给出决策。策略可以按工具名、按元数据、也可以按入参来定对应ToolCallConfirmationPolicy与spring.ai.tools.confirmation.*这组配置决策对象是ToolCallDecision。这里有个容易被忽略的工程要点暂停发生在编排循环里而不是发生在工具实现里。决策进来之前工具代码没有被执行过一次被拒绝的时候模型拿到的是一个结果对象而不是一个异常——这两点决定了你可以把审批动作接到任何已有系统上而不必改造工具本身的签名。需要留个心2.1.0-RC1 目前只发到 Maven Central 的 RC 通道最新稳定版仍是 2.0.2官方参考文档页首也标注着文档中此处仍在开发中。据此推测 API 在正式版发布前仍可能微调上生产请等 2.1.0 GA官方博客此前预告的目标时间是 2026 年 11 月。三、会话记忆为什么要按轮压缩旧世界里大家用的是 ChatMemory——每个会话存一个扁平的消息列表超了就把最老的消息丢掉。这套做法在短对话里够用在真正的智能体里会出两件事。一是工具调用会被拦腰截断。模型一次吐出多个工具调用结果稍后一批回来。如果压缩动作用在消息条数上切点很可能落在「调用」与「结果」之间于是模型看到一堆没有结果的工具调用或者一堆看不到源头的结果。二是被丢掉的内容说不清去向。扁平列表丢了就是丢了事后既不知道丢了什么也没法解释为什么模型开始答非所问。2.1 的 Session Memory 把「存了什么」和「给模型看什么」拆成了两层层内容对应的 API事件日志只追加每条消息是一个 SessionEvent也可以自定义事件类型记录别的事情SessionService、spring-ai-starter-model-session-jdbc视图从日志的一个视窗构建 prompt切点由触发器与策略决定TurnCountTrigger、WindowCompactionStrategy摘要被挤出视图的部分可选地由模型先总结成一条事件CompactionSummarizer、ChatModelSummarizer关键约束写在官方原文里A compaction always cuts on a turn boundary, so the model never sees a tool call without its result.压缩一定切在轮次边界上模型永远不会看到一个缺了结果的工具调用。并且Compaction never rewrites the log——压缩从不改写日志只是追加一个边界标记来定义从何处开始的新视图。这两句加在一起的含义是日志是可审计的完整事实视图是可优化的成本开关二者不再互相污染。存储側给了InMemorySessionRepository给测试用与JdbcSessionRepository支持 PostgreSQL、MySQL、MariaDB、H2追加幂等、 compaction 带版本校验。调用侧每个请求都要显式传入会话 ID框架不提供共享的默认会话——这个设计是在逼你把「这个请求属于哪个会话」当成显式输入而不是靠线程上下文猜。四、动手验证把「切点」的代价算出来按轮压缩这条约束听起来像洁癖值得量化一下。下面这段脚本合成一段 24 轮的多轮会话每轮结构是「1 条用户指令 2~4 次工具调用 对应数量的结果 1 条结论」然后遍历所有可能的截断点统计有多少比例的切点会切出孤儿的工具调用或结果。# session_compaction.py —— 会话压缩该切在哪消息窗口 vs 轮次边界纯标准库 import random TOOLS [query_order, fetch_invoice, issue_refund, check_stock, notify_user] def make_session(n_turns, max_calls, seed2026): 每轮 1 条用户指令 N 个 tool_call N 个 tool_result 1 条结论。 rnd random.Random(seed) turns [] for t in range(1, n_turns 1): k rnd.randint(2, max_calls) ev [(user, user-%d % t)] ids [] for i in range(k): cid c%d-%d % (t, i) ids.append(cid) ev.append((tool_call, cid)) for cid in ids: ev.append((tool_result, cid)) ev.append((assistant, ans-%d % t)) turns.append(ev) return turns def flatten(turns): return [e for t in turns for e in t] def audit(view): calls {p for k, p in view if k tool_call} results {p for k, p in view if k tool_result} return len(calls - results), len(results - calls) # 有调用无结果 / 有结果无调用 if __name__ __main__: turns make_session(n_turns24, max_calls4) flat flatten(turns) bad 0 for n in range(1, len(flat)): a, b audit(flat[-n:]) if a b: bad 1 print(总事件 %d遍历 %d 个截断点切出孤儿的 %d 个占 %.1f%% % (len(flat), len(flat) - 1, bad, bad / (len(flat) - 1) * 100)) for n in (20, 40, 60, 80): a, b audit(flat[-n:]) print( 保留 %3d 条有调用无结果 %d有结果无调用 %d % (n, a, b)) for t in (3, 5, 8, 12): kept flatten(turns[-t:]) a, b audit(kept) print( 保留 %3d 轮视图 %3d 事件断裂 %d 处 % (t, len(kept), a b))本机 Python 3.13.12 实跑输出脚本同步归档在_脚本/session_compaction.py总事件 198遍历 197 个截断点切出孤儿的 126 个占 64.0% 保留 20 条有调用无结果 0有结果无调用 0 保留 40 条有调用无结果 0有结果无调用 1 保留 60 条有调用无结果 0有结果无调用 3 保留 80 条有调用无结果 0有结果无调用 3 保留 3 轮视图 21 事件断裂 0 处 保留 5 轮视图 37 事件断裂 0 处 保留 8 轮视图 57 事件断裂 0 处 保留 12 轮视图 95 事件断裂 0 处数字说明三件事。第一按消息条数截断的风险不是边缘情况。197 个可能的切点里有 126 个64.0%会切断工具调用与结果的配对。这不是小概率事件而是常态出问题的那天日志看起来只是「模型偶尔答非所问」。第二收益并不来自省得更多。保留 40 条得到 40 个事件的视图还带着 1 处断裂保留 5 轮得到 37 个事件的视图断裂为 0。规模接近语义质量完全不同——省下来的不是 token是事故。第三「有结果无调用」占多数。输出里主要是有结果无调用模型看到一堆找不到来源的结果而不是有调用无结果。这个方向更危险后者模型通常会重试前者会让模型基于它无法归因的信息继续往下答。五、迁移要改的四个地方旧写法现在的状态替换方案ChatMemory、MessageWindowChatMemory弃用计划 3.0.0 移除SessionService SessionAdvisorsMessageChatMemoryAdvisor、各存储实现同上同上存储换成 SessionRepository每个会话一个扁平消息列表语义上不够用只追加事件日志 可压缩视图spring-ai-session社区版已被官方吸收spring-ai-starter-model-session-jdbc另外三个兼容性细节值得在评审时提一句Bedrock 的requestParameters从字符串映射改成了 Map赋给旧类型会编译不过PgVectorStore不再写入距离元数据要读Document#getScore()依赖一路上移到 MCP Java SDK 2.1.0、OpenAI Java SDK 4.80.0 与 Anthropic Java SDK 2.72.0。还有一处检索增强容易错过现在支持用调用方自带的查询向量做相似度检索similaritySearch(float[], SearchRequest)pgvector、Qdrant、Redis、Elasticsearch、OpenSearch 已实现不支持的存储会立即报错fail-fast而不是静默降级。如果你的链路已经在别处算好了嵌入向量比如复用了检索侧的离线产出这条能省掉一次重复计算。六、常见误区误区一把 Session Memory 当成长期记忆。它解决的是一次长会话里短期上下文怎么存与怎么压官方文档明确把它与长期记忆的实现区分开。把用户画像之类的东西塞进事件日志很快就会在容量与成本上同时失控。误区二以为 requiresConfirmation 是安全边界。它是编排循环里的一个暂停点负责「让批准动作有机会发生」。真正的安全边界在承接这次调用的企业侧——审批系统、权限模型与出网策略各有各的位置。误区三沿用旧会话 ID 的隐式传递。新 API 要求每次调用显式传入会话 ID也没有共享默认会话。如果你的代码依赖 ThreadLocal 之类的隐式上下文迁移时会遇到「换个线程池会话就串了」这类问题。误区四拿 RC 直接上生产。文档页首标注此处仍在开发中API 在 GA 前可能调整。合理的做法是先在灰度环境用新 API 重写会话层等 2.1.0 正式版出来再切依赖。七、适用边界结论这一版把「长会话」和「高危动作」从每个团队各自造轮子的状态拉到了框架里有统一语义的位置代价是旧 API 的一轮换血。适用边界其一所有行为以 2.1.0-RC1 为准正式版发布前个别 API 可能调整上面的批量退休计划3.0.0 移除同样来自官方当前口径后续版本可变。其二本文的脚本是用 Python 复刻压缩语义做的对照实验目的是量化切点风险不是对 Spring AI 性能的评测真实会话里工具调用的分布与这里的均匀分布假设不同绝对值会变结论方向不变。其三压缩策略保留多少轮、是否启用摘要依赖具体业务的容忍度没有通用取值。可以先按本文的方法跑一遍自己的会话样本再定触发器阈值。把视角从框架挪到企业会话不再是内存里的一个对象它会变成一笔要留存、要能解释、必要时还得 完整还原的数据。这类能力落到国内的交付环境里通常有两层约束一是数据不出域会话痕迹与工具调用记录要落在内网可管的地方二是这些调用最终会穿过企业的接口编排层与 API 网关——那边才是统一做权限、配额与审计的位置框架侧的暂停点只是把决策权交还给它。金蝶天燕 Apusic 应用服务器 AAS 承担的是应用承载这一层它既要跑得动 Java 生态里这一轮框架升级也要承接信创 CPU 架构与操作系统组合的适配要求。能不能承接这类升级不看宣传口径看的是替换组件版本时有没有可回退的路。留给你的一个具体问题你的会话压缩策略现在是按消息条数还是按轮如果是前者按本文的方法跑一遍自己的会话样本切出孤儿调用的切点占比是多少——这个数字出来之后你打算改策略还是打算继续赌运气
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。