Spring AI 2.1.0-RC1 把 Tool Call Confirmation 放进了正式发布线:给 @Tool(requiresConfirmation = true) 的方法一挂上,模型同一次回复里只要出现这类调用,DefaultToolCallingManager 会在执行任何工具之前抛出 ToolCallConfirmationRequiredException,整段回复先停住。本地用 Spring Boot 4.2.0-RC1 直接驱动 ToolCallingManager(不调外部模型)复现了这一点:单独查航班状态会立刻执行;单独订票会暂停且计数为 0;同一次回复里同时返回 flightStatus 和 bookFlight 时,两个计数都仍是 0。恢复时再按 ToolCallDecisions 放行或拒绝,拒绝侧的工具结果就是你写的 deny 文案。

本文只覆盖确认门控这一条线。之前那篇讲 2.1.0-M1 Responses API 的文章里写过的 Responses 接入与限制,这里不重复。

发布依据

Git 标签 v2.1.0-RC1 指向提交 7782e72d4b8e4394e0a5cb6ee17315f24386f5aa,附注 tag 的 tagger 时间是 2026-10-22T17:01:21Z(换算到 Asia/Shanghai 为 2026-10-23 01:01)。Maven Central 上 org.springframework.ai:spring-ai-bom:2.1.0-RC1 的 maven-metadata.xml 里 lastUpdated 为 20261022125800。功能落地提交是 24c782a0(2026-10-14),提交说明写明:标了 requiresConfirmation 或被 ToolCallConfirmationPolicy 选中的工具,会在回复里任何调用执行前暂停循环;应用用带 ToolCallDecisions 的新重载恢复。行为细则以官方文档 Tool Call Confirmation 为准,文档抬头写着 available since Spring AI 2.1.0。

父 POM 用 Spring Boot 4.2.0-RC1,与 Spring AI RC1 父工程里的 spring-boot.version 一致。运行环境是 JDK 21.0.12.1、Maven 3.9.9。

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-parent</artifactId>
  <version>4.2.0-RC1</version>
</parent>

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>org.springframework.ai</groupId>
      <artifactId>spring-ai-bom</artifactId>
      <version>2.1.0-RC1</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-client-chat</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.ai</groupId>
  <artifactId>spring-ai-model</artifactId>
</dependency>

门控怎么标

文档示例与 jar 里的 org.springframework.ai.tool.annotation.Tool 都暴露了 requiresConfirmation()。示例工具如下:

static final class BookingTools {
    final AtomicInteger flightStatusCount = new AtomicInteger();
    final AtomicInteger bookFlightCount = new AtomicInteger();

    @Tool(description = "Get the status of a flight")
    String flightStatus(String flightNumber) {
        flightStatusCount.incrementAndGet();
        return "Flight " + flightNumber + " is on time";
    }

    @Tool(description = "Book a flight", requiresConfirmation = true)
    String bookFlight(String flightNumber) {
        bookFlightCount.incrementAndGet();
        return "Booked " + flightNumber;
    }
}

ToolCallingManager 用默认策略 ToolCallConfirmationPolicy.fromToolMetadata()。为了把门控本身测干净,这里手工构造带 ToolCall 的 ChatResponse,再调用 executeToolCalls,不经过真实 Chat 模型。

ToolCallingManager manager = ToolCallingManager.builder()
        .toolCallConfirmationPolicy(ToolCallConfirmationPolicy.fromToolMetadata())
        .build();

ToolCallingChatOptions options = ToolCallingChatOptions.builder()
        .toolCallbacks(callbacks)
        .build();

ChatResponse mixed = new ChatResponse(List.of(new Generation(
        AssistantMessage.builder()
                .content("")
                .toolCalls(List.of(
                        new AssistantMessage.ToolCall("call_status2", "function",
                                "flightStatus", "{\"flightNumber\":\"VX205\"}"),
                        new AssistantMessage.ToolCall("call_book2", "function",
                                "bookFlight", "{\"flightNumber\":\"VX205\"}")))
                .build())));

五组实测

启动日志确认运行在 Boot 4.2.0-RC1 上,管理器实现类是 DefaultToolCallingManager。常量 ToolCallDecision.DEFAULT_DENY_MESSAGE 打印为 The user did not allow this tool call.。

:: Spring Boot ::            (v4.2.0-RC1)
=== Spring AI 2.1.0-RC1 Tool Call Confirmation demo ===
ToolCallingManager class: org.springframework.ai.model.tool.DefaultToolCallingManager
DEFAULT_DENY_MESSAGE: The user did not allow this tool call.

S1:只有未标确认的 flightStatus。 executeToolCalls 直接成功,计数 flightStatusCount=1,工具结果是航班状态文案。

[S1] ungated-only executeToolCalls OK
[S1] flightStatusCount=1 bookFlightCount=0
  toolResult name=flightStatus id=call_status data="Flight VX205 is on time"

S2:只有标了确认的 bookFlight。 抛出 ToolCallConfirmationRequiredException,awaiting ids 为 [call_book],两个计数都是 0。

[S2] paused as expected
[S2] awaiting ids: [call_book]
[S2] flightStatusCount=0 bookFlightCount=0 (both should stay 0 until resume)

S3:同一次回复同时带 flightStatus 与 bookFlight。 文档写明:一条回复里只要有一个需要确认,整段先停,人决定之前这一回复里的调用都不会跑。实测 awaiting ids 只有 [call_book2](需要确认的那一个),但 flightStatusCount 同样还是 0:未标确认的工具也不会在暂停前先执行。

[S3] mixed reply paused as a whole
[S3] awaiting ids: [call_book2]
[S3] after pause: flightStatusCount=0 bookFlightCount=0
[S3] interpretation: ungated flightStatus in the same reply also did not run

S4:对混合回复恢复,放行 status、拒绝 book。 文档要求 decisions 覆盖该回复里的每一个 call。放行后 flightStatusCount=1,bookFlightCount 仍为 0;bookFlight 对应的 tool result 是自定义 deny 文案 User cancelled the booking.,工具方法体没有进入。

ToolCallDecisions decisions = ToolCallDecisions.of(
        ToolCallDecision.allow("call_status2"),
        ToolCallDecision.deny("call_book2", "User cancelled the booking."));
ToolExecutionResult resumed = manager.executeToolCalls(resumePrompt, mixed, decisions);
[S4] resumed with allow(status)+deny(book)
[S4] flightStatusCount=1 bookFlightCount=0
  toolResult name=flightStatus id=call_status2 data="Flight VX205 is on time"
  toolResult name=bookFlight id=call_book2 data=User cancelled the booking.

S5:对单独 bookFlight 用 allow 恢复。 bookFlightCount=1,结果为 Booked VX205。

[S5] resumed bookFlight with allow
[S5] bookFlightCount=1
  toolResult name=bookFlight id=call_book data="Booked VX205"

和 ChatClient 路径的关系

文档把确认检查放在 ToolCallingManager 里,并写明 ToolCallingAdvisor 会接住异常,把 awaiting 的 key 放进响应元数据;应用侧用 ToolCallDecisions.awaitingDecision(response) 识别暂停。本次演示直接打 manager,所以日志里看到的是异常路径,不是 ChatClient 包装后的元数据路径。Boot 自动配置还会认 spring.ai.tools.confirmation.enabled(默认 true)和 spring.ai.tools.confirmation.tools(按名字追加需确认的工具,例如 MCP 工具),以及容器里所有 ToolCallConfirmationPolicy bean,彼此用 or 合并。文档 Security 一节还强调:恢复时用的暂停回复要从应用自己的会话存储读,不要从客户端请求里取;框架不做审批人身份校验,也不防重放。

同一次 RC1 还把 Session Memory 并进了主线(提交 2b386912 起的一组),并把 ChatMemory 标成弃用。那条线更长,本文不展开。

— 感谢阅读 —
广告

一起交流

分享你的思考,让讨论更进一步。