Ohhnews

分类导航

$ cd ..
Baeldung原文

Spring AI的动态工具发现

#spring ai#工具搜索#token节省#动态工具发现#人工智能

Spring AI 的动态工具发现

1. 概述

在构建 AI 集成系统时,我们通常会为 AI 客户端提供大量工具。每次请求时,我们都会将所有可用工具的定义发送给 LLM,以便其决定使用哪些工具。结果,在模型处理用户查询之前,我们就浪费了大量 token。本文探讨如何使用工具搜索工具解决这个问题。

2. 工具搜索工具的工作原理

使用工具搜索工具,我们不再将所有工具定义随上下文一起发送。只有当模型实际需要工具时,我们才会暴露它们。首先,我们会在启动时索引所有已注册的工具。我们将它们存储在 ToolSearcher 中,但不会发送给 LLM。接着,我们在初始请求中仅发送工具搜索工具本身。这保持了提示的简洁和专注。当模型需要某个能力时,它会用自然语言查询调用工具搜索工具。

我们将此视为发现信号,并使用配置的策略触发对索引工具的搜索。然后,我们只从 ToolSearcher 返回最相关的匹配结果,并将其定义注入下一个 LLM 请求中,从而使模型看到一组聚焦的工具,而不是完整的注册表。

一旦相关工具可用,模型就会选择并调用实际的工具。我们执行该工具,并将结果返回给 LLM,然后 LLM 利用它生成最终答案。

3. 构建旅行助手示例

让我们构建一个帮助用户规划旅行的旅行助手。我们连接了多个工具,例如航班、酒店、天气和景点。我们采用工具搜索工具的方法,避免一开始就将所有工具发送给 LLM,而是在运行时动态发现工具。

3.1. 依赖关系

首先,我们添加工具搜索依赖支持:

$ xml
<dependency>
    <groupId>org.springaicommunity</groupId>
    <artifactId>tool-search-tool</artifactId>
    <version>${tool-search-tool.version}</version>
</dependency>

同时,添加正则搜索器依赖:

$ xml
<dependency>
    <groupId>org.springaicommunity</groupId>
    <artifactId>tool-searcher-regex</artifactId>
    <version>${tool-search-tool.version}</version>
</dependency>

使用它,我们将获得一个 regex 工具搜索策略。其他可用策略可以在项目仓库中找到。

3.2. 航班工具

创建一个简单的 FlightTools。我们将用它来获取可用的航班选项。此外,我们创建一些人工工具来模拟上下文过载:

$ java
public class FlightTools {
    @Tool(description = "Searches available flights between two cities")
    public List<FlightOption> searchFlights(String from, String to, String departureDate) {
        return List.of(
          new FlightOption(
            "Romania Airlines",
            from,
            to,
            departureDate,
            249.99
          )
        );
    }
}

这里我们返回一个航班选项。

3.3. TokenCounterAdvisor

现在创建一个简单的 TokenCounterAdvisor,用于统计生成最终结果所使用的 token 数量。我们将用它来比较不同配置(开启/关闭工具搜索)下的 token 使用情况:

$ java
public class TokenCounterAdvisor implements BaseAdvisor {
    private static final Logger log = LoggerFactory.getLogger(TokenCounterAdvisor.class);
    private final AtomicInteger totalTokenCounter = new AtomicInteger(0);

    @Override
    public String getName() {
        return "TokenCounterAdvisor";
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE - 1;
    }

    @Override
    public ChatClientRequest before(ChatClientRequest chatClientRequest, AdvisorChain advisorChain) {
        return chatClientRequest;
    }

    @Override
    public ChatClientResponse after(ChatClientResponse chatClientResponse, AdvisorChain advisorChain) {
        var usage = chatClientResponse.chatResponse().getMetadata().getUsage();
        totalTokenCounter.addAndGet(usage.getTotalTokens());
        log.info("Total tokens spent: {}", totalTokenCounter.get());
        return chatClientResponse;
    }
}

这里我们将 token 数量存储在 AtomicInteger 字段中,并在执行期间记录这个信息。我们将此 advisor 附加到最大顺序,使其在处理管道的末尾运行。因此,它在所有其他 advisor 完成后捕获 token 总使用量。

3.4. 配置

接下来,添加 TravelAssistantConfig 实现:

$ java
@Configuration
public class TravelAssistantConfig {

    @Bean
    ToolSearcher toolSearcher() {
        return new RegexToolSearcher();
    }

    @Bean
    ToolSearchToolCallAdvisor toolSearchToolCallAdvisor(ToolSearcher toolSearcher) {
        return ToolSearchToolCallAdvisor.builder()
          .toolSearcher(toolSearcher)
          .maxResults(5)
          .build();
    }

    @Bean
    ChatClient chatClient(ToolSearchToolCallAdvisor toolSearchToolCallAdvisor, OpenAiChatModel model) {
        return ChatClient.builder(model)
          .defaultTools(
            new FlightTools(),
            new RandomTools()
          )
          .defaultAdvisors(toolSearchToolCallAdvisor, new TokenCounterAdvisor())
          .build();
    }

    @Bean
    ChatClient chatClientWithoutToolsSearch(OpenAiChatModel model) {
        return ChatClient.builder(model)
          .defaultTools(
            new FlightTools(),
            new RandomTools()
          )
          .defaultAdvisors(new TokenCounterAdvisor())
          .build();
    }
}

我们配置了一个使用动态工具发现的旅行助手,而不是将所有工具加载到 LLM 中。接着,我们设置了一个 ToolSearcher,使用 RegexToolSearcher 实现。这允许我们基于命名模式和快速的关键词类查询来匹配工具。然后,创建 ToolSearchToolCallAdvisor 并将其连接到搜索器。之后,构建 ChatClient 并注册航班工具。

按照设计,我们添加了包含许多无关工具定义的 RandomTools。但是,我们不会一开始就将这些工具定义发送给 LLM,而只是在系统中索引它们。最后,我们在开始时只向模型暴露工具搜索工具。模型随后使用它来发现对于给定请求实际需要哪些工具。 此外,我们配置了另一个不使用的 ToolSearchToolCallAdvisorChatClient bean。

3.5. 调用旅行助手

最后,创建一个 ToolsSearchToolLiveTest,为两个客户端提供类似的测试用例:

$ java
@SpringBootTest
@ActiveProfiles("toolsearchtool")
class ToolsSearchToolLiveTest {

    @Autowired
    private ChatClient chatClient;

    @Autowired
    private ChatClient chatClientWithoutToolsSearch;

    @Test
    void shouldFindFlightsBetweenRomaniaAndCroatiaUsingToolsSearch() {
        String response = getClientResponseString(chatClient);
        assetClientResponse(response);
    }

    @Test
    void shouldFindFlightsBetweenRomaniaAndCroatiaWithoutToolsSearch() {
        String response = getClientResponseString(chatClientWithoutToolsSearch);
        assetClientResponse(response);
    }

    private static void assetClientResponse(String response) {
        assertThat(response).isNotBlank();
        assertThat(response).containsIgnoringCase("Croatia");
        assertThat(response).containsIgnoringCase("flight");
    }

    private String getClientResponseString(ChatClient chatClientWithoutToolsSearch) {
        return chatClientWithoutToolsSearch.prompt()
          .user("""
                  Find available flights from Romania to Croatia next week.
                  """)
          .call()
          .content();
    }
}

我们用相同的提示调用了两个旅行顾问客户端,并获得了相同的结果。现在,比较两者使用的 token 数量:

[2026-05-24 11:39:07] [INFO] [c.b.s.t.TokenCounterAdvisor] - Total tokens spent: 974 //使用工具搜索工具
[2026-05-24 11:39:10] [INFO] [c.b.s.t.TokenCounterAdvisor] - Total tokens spent: 3685 //不使用工具搜索工具

可见,token 使用量的差异非常关键。我们的系统中工具越多,工具搜索工具带来的 token 节省就越大。

4. 结论

在本文中,我们回顾了工具搜索工具,并演示了它如何帮助减少实际场景中的 token 使用。使用它,我们可以构建拥有数百个附属工具的大型 AI 集成系统,并高效地使用它们,而不会浪费 token。此外,我们可以探索其他工具搜索策略,例如向量搜索,甚至构建自定义策略,使工具发现更加高效。

与往常一样,代码可在 GitHub 上获取。