Spring AI 2.0 Tool Search:工具多了,只把相关的发给模型

各位朋友大家好,我是一言,继续更新 Spring AI 2.0 系列。

上一篇写的是 Structured Output,模型结果可以直接进入 Java 业务代码。这一篇接着解决一个很实际的问题:项目里的工具越来越多了,应该怎么办?

我给电商助手注册了 12 个工具:查订单、取消订单、申请退款、查库存、开发票、查优惠券、查积分......

用户只问了一个问题:

帮我查订单 A1024 的物流状态。

默认情况下,第一轮模型请求会带上全部 12 个工具:

text 复制代码
[default] tools=[
  applyRefund, cancelOrder, createInvoice, queryCoupon,
  queryInventory, queryInvoice, queryLogistics,
  queryMemberPoints, queryOrder, queryProduct,
  queryRefund, reserveInventory
]

实际执行时,模型会挑选需要的工具。第一轮请求里,它会先看到这 12 个工具的定义。每份定义都包含工具名称、用途说明和输入参数的 JSON Schema。

拿 queryOrder 来说,实际生成的定义拆开以后是这样的:

json 复制代码
{
  "name": "queryOrder",
  "description": "Query order shipping status, carrier and tracking number by order id",
  "inputSchema": {
    "type": "object",
    "properties": {
      "orderId": {
        "type": "string",
        "description": "Order id, for example A1024"
      }
    },
    "required": ["orderId"],
    "additionalProperties": false
  }
}

一个工具已经有这么多内容了。12 个工具全部放进去,模型收到的远远超过 12 个方法名。

工具少的时候问题不大。接入几个 MCP Server 以后,几十个工具一起进入请求,输入 Token 会增加,模型选工具也会更费劲。

Spring AI 2.0.1 官方文档给了一个更大的场景:多个服务聚合以后,很容易出现 50 多个工具,工具定义可能占用 55,000 多个 Token。面对 30 多个名称相近的工具时,模型选择的准确率也会下降。

Spring AI 2.0 给出的解决方案叫 Tool Search:先找工具,再把找到的工具交给模型。

下面使用 Java 21、Spring Boot 4.0.8 和 Spring AI 2.0.1 跑一遍。

用了 Tool Search 以后,我又拿同一个问题试了一次:

text 复制代码
[tool-search] round #1 tools=[toolSearchTool]
[tool-search] round #2 tools=[queryOrder, toolSearchTool]
[tool-search] search result=["queryOrder"]
[tool] queryOrder(A1024)
[tool-search] round #3 tools=[queryOrder, toolSearchTool]
[tool-search] order result="Order A1024 is SHIPPED via SF Express, tracking number SF1234567890"
[answer] 订单 A1024 已发货,承运商是顺丰,运单号 SF1234567890。

我再把每一轮工具定义中 name、description 和 inputSchema 的字符数统计出来:

模型请求 携带的工具 定义字符数
默认方式第 1 轮 12 个业务工具 3783
Tool Search 第 1 轮 toolSearchTool 1055
Tool Search 第 2 轮 toolSearchTool、queryOrder 1407

这里统计的是字符数,不能直接换算成模型 Token。它能说明一个问题:当前查询用不到的退款、库存和发票等工具定义,已经从模型请求中移走了。

Spring 官方也做过一组 28 个工具的初步手工测试。Lucene 方案的总 Token 用量下降了 34%---64%,节省主要来自 Prompt Token。测试只跑了少量轮次,这组数字用来观察方向更合适。

第一轮只有一个内置工具 toolSearchTool。模型先查找"查询订单物流状态"需要什么能力,索引返回 queryOrder。下一轮请求才会加入 queryOrder 的完整定义,接着执行 Java 方法并生成答案。

可以把这套机制概括成一句话:

工具留在索引里,相关定义再进模型请求。

先把 Starter 加进来

Spring AI 2.0.1 可以直接使用自动配置:

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-tool-search-advisor</artifactId>
</dependency>

这次先用 Lucene 做关键词检索:

properties 复制代码
spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=lucene

enabled=true 会启用 ToolSearchToolCallingAdvisor,并替换默认的 Tool Calling advisor。原来的工具注册方式不用修改:

java 复制代码
@Component
public class CommerceTools {

    @Tool(name = "queryOrder",
            description = "Query order shipping status, carrier and tracking number by order id")
    public String queryOrder(
            @ToolParam(description = "Order id, for example A1024") String orderId) {
        return "Order %s is SHIPPED via SF Express, tracking number SF1234567890"
                .formatted(orderId);
    }

    // cancelOrder、applyRefund、queryInventory ...
}

继续用 defaultTools() 注册:

java 复制代码
@Bean
ChatClient chatClient(ChatClient.Builder builder, CommerceTools tools) {
    return builder
            .defaultTools(tools)
            .build();
}

sessionId 这一行不能少

调用的时候,要给 advisor 一个会话 ID:

java 复制代码
String answer = chatClient.prompt()
        .user("帮我查订单 A1024 的物流状态")
        .advisors(a -> a.param(
                ChatMemory.CONVERSATION_ID,
                "order-session-1"))
        .call()
        .content();

Tool Search 会根据会话创建工具索引,并记录本轮已经找到的工具。默认的会话键就是 ChatMemory.CONVERSATION_ID。

我把这一行删掉以后,2.0.1 会直接抛出 IllegalArgumentException。即使没有接 Chat Memory,也要传一个稳定的业务会话 ID,比如当前对话 ID 或请求链路 ID。

工具搜不到,先改 description

这篇使用 Lucene 检索工具名称和描述。搜索结果准不准,和 @Tool 怎么写有直接关系。

这样的描述没有多少区分度:

java 复制代码
@Tool(description = "处理订单")
public String queryOrder(String orderId) { ... }

换成具体的动作、对象和参数:

java 复制代码
@Tool(description = "Query order shipping status, carrier and tracking number by order id")
public String queryOrder(String orderId) { ... }

我做了一组 Lucene 对照。工具名和搜索词保持一致,只修改 description:

text 复制代码
search query: query order shipping status
description: Process business request
result: []

description: Query order shipping status, carrier and tracking number by order id
result: [queryOrder]

模糊描述没有达到 Lucene 的命中阈值,具体描述返回了 queryOrder。

工具多了以后,下面这样的命名很容易添乱:

text 复制代码
handle
process
execute

我会把工具说明写得像接口说明,至少说清三件事:

  1. 做什么动作;
  2. 操作什么对象;
  3. 需要什么关键参数。

比如 queryOrder 负责订单和发货状态,queryLogistics 负责运单轨迹。两段描述要把这层边界写出来,搜索结果才容易区分。

三种索引,我会这样选

Spring AI 2.0.1 提供了三种索引:

  • regex:默认方案,按照工具名模式匹配,适合命名规则非常统一的工具;
  • lucene:关键词检索,无需 Embedding 模型,普通业务项目可以从这里开始;
  • vector:语义检索,需要 VectorStore,适合用户说法和工具描述差异较大的场景。

这篇选择 Lucene,原因很简单:依赖少,搜索结果也容易排查。先把工具名称和描述写清楚,通常比一开始就接向量库更重要。

最后再记一句:工具留在索引里,相关定义再进模型请求。

下一篇继续写 Chat Memory:多轮对话里,Spring AI 到底替我们记了什么。

相关推荐
郭小李2 小时前
spring mvc 工作流程
java·spring·mvc
乌暮3 小时前
综合案例:宠物电商系统
java·开发语言·spring·mvc·restful·1024程序员节
万联WANFLOW3 小时前
Kimi K3.1 Pro 接口接入指南:模型选型、费用计算与零数据保留
java·spring boot·python·安全·spring·大模型
学到老活到老3 小时前
Spring 事务怎样生效?为什么加了 @Transactional 仍不回滚?
java·spring
学到老活到老3 小时前
Spring 怎样创建 Bean?@Autowired 在哪一步生效?
java·spring
木头科技4 小时前
【AI 工程化第十八篇】Spring AI 语音 Agent 实战:实时转写、WebSocket、流式 TTS 与打断机制
java·人工智能·安全·spring·性能优化
步行cgn4 小时前
Spring MVC 核心配置文件详解:springmvc.xml 每一行配置的意义
xml·spring·mvc
Flynn Liang6 小时前
Spring AI ChatClient 详解:system/user/assistant 角色、call 与 stream 到底怎么选
java·人工智能·spring·springai·流式输出·chatclient·java接入大模型
萧瑟余晖6 小时前
Spring Data Redis详解
spring·面试