四、Spring AIAlibaba · Tools(工具调用)

一、为什么需要 Tools

LLM 只会"说话",不会"做事",也不知道实时信息。Tools 让模型通过结构化输入直接对接外部系统(API、数据库、文件系统)。

两类典型用途:

类别 目标 例子
信息检索 增强模型知识,回答原本答不了的问题(RAG) 查天气、查新闻、查数据库记录
执行操作 自动化原本需人工或硬编码的任务 发邮件、建订单、订机票、按 TDD 生成 Java 类

关键安全认知

Model 永远无法访问你提供的 API,它只能"请求"调用并给出参数;真正执行的是客户端应用程序。

这是工具调用最重要的安全边界 ------ 权限、鉴权、副作用控制全在你手里。

另注:OpenAI / Anthropic / Gemini 等有服务器端内置工具(Web 搜索、代码解释器),属厂商能力,与本节的客户端工具体系不同。


二、Tool Calling 的六步序列

复制代码
① 聊天请求中携带 tool 定义(名称 + 描述 + 输入 schema)
② 模型决定调用 → 返回 tool 名 + 符合 schema 的参数        [AssistantMessage.toolCalls]
③ 应用按名称找到 ToolCallback 并用参数执行它              [ToolCallingManager]
④ 应用处理执行结果
⑤ 应用把结果作为 ToolResponseMessage 发回模型
⑥ 模型结合结果生成最终响应

核心接口:ToolCallback ;生命周期管理者:ToolCallingManager。


三、定义工具的四种方式

3.1 声明式:@Tool 注解(最推荐)

java 复制代码
class DateTimeTools {

    @Tool(description = "Get the current date and time in the user's timezone")
    String getCurrentDateTime() {
        return LocalDateTime.now().atZone(LocaleContextHolder.getTimeZone().toZoneId()).toString();
    }

    @Tool(description = "Set a user alarm for the given time, provided in ISO-8601 format")
    void setAlarm(@ToolParam(description = "Time in ISO-8601 format") String time) {
        LocalDateTime alarmTime = LocalDateTime.parse(time, DateTimeFormatter.ISO_DATE_TIME);
        System.out.println("Alarm set for " + alarmTime);
    }
}

String response = ChatClient.create(chatModel)
        .prompt("Can you set an alarm 10 minutes from now?")
        .tools(new DateTimeTools())     // 传入实例
        .call().content();

@Tool 四个属性

属性 说明
name 默认取方法名;同一请求内必须唯一
description 强烈建议必填,决定模型何时/如何调用;缺失会导致"该用不用"或"用错"
returnDirect 结果是否直接返回调用者而不回传模型
resultConverter 自定义结果序列化器

规则 :方法可静态/实例、任意可见性;参数支持基本类型、POJO、枚举、List、数组、Map 等;返回值必须可序列化(可为 void)。

AOT:若类不是 Spring bean,需加 @RegisterReflection(memberCategories = MemberCategory.INVOKE_DECLARED_METHODS)。

@ToolParam :description(格式/取值说明)+ required(默认 true)。加 @Nullable 且未显式标 required 则视为可选。

3.2 编程式:MethodToolCallback

java 复制代码
Method method = ReflectionUtils.findMethod(DateTimeTools.class, "getCurrentDateTime");
ToolCallback toolCallback = MethodToolCallback.builder()
    .toolDefinition(ToolDefinitions.builder(method)
            .description("Get the current date and time in the user's timezone")
            .build())
    .toolMethod(method)
    .toolObject(new DateTimeTools())   // 静态方法可省略此行
    .build();

Builder 参数 :toolDefinition(必需)、toolMetadata、toolMethod(必需)、toolObject、toolCallResultConverter。

⚠️ 方法 Tool 不支持的类型 :Optional、异步类型(CompletableFuture/Future)、响应式类型(Flow/Mono/Flux)、函数类型。

3.3 函数式:FunctionToolCallback

java 复制代码
public class WeatherService implements Function<WeatherRequest, WeatherResponse> {
    public WeatherResponse apply(WeatherRequest request) { return new WeatherResponse(30.0, Unit.C); }
}
public record WeatherRequest(String location, Unit unit) {}
public record WeatherResponse(double temp, Unit unit) {}

ToolCallback toolCallback = FunctionToolCallback
    .builder("currentWeather", new WeatherService())
    .description("Get the weather in location")
    .inputType(WeatherRequest.class)
    .build();

// 挂到 ChatClient
ChatClient.create(chatModel).prompt("...").toolCallbacks(toolCallback).call().content();

// 挂到 ChatModel(走 ChatOptions)
ChatOptions chatOptions = ToolCallingChatOptions.builder().toolCallbacks(toolCallback).build();
chatModel.call(new Prompt("What's the weather like in Copenhagen?", chatOptions));

支持 Function / Supplier / Consumer / BiFunction;输入输出必须是可序列化的 public POJO (也可 Void)。

⚠️ 函数 Tool 额外不支持:基本类型、集合类型(List/Map/Array/Set)→ 这些请改用「方法 Tool」。

3.4 动态式:@Bean + ToolCallbackResolver

java 复制代码
@Configuration(proxyBeanMethods = false)
class WeatherTools {
    public static final String CURRENT_WEATHER_TOOL = "currentWeather";

    @Bean(CURRENT_WEATHER_TOOL)
    @Description("Get the weather in location")
    Function<WeatherRequest, WeatherResponse> currentWeather() { return new WeatherService(); }
}

// 使用时只给名字
ChatClient.create(chatModel).prompt("...").toolNames("currentWeather").call().content();
ChatClient.builder(chatModel).defaultToolNames("currentWeather").build();
  • bean 名即工具名,建议用常量存名字(避免硬编码 + 运行时解析的类型不安全)。
  • 解析器:DelegatingToolCallbackResolver → SpringBeanToolCallbackResolver(找 Function 类 bean)+ StaticToolCallbackResolver(找 ToolCallback 类 bean)。

四、Tool 规范三件套

ToolCallback

java 复制代码
public interface ToolCallback {
    ToolDefinition getToolDefinition();   // 名称+描述+输入schema
    ToolMetadata   getToolMetadata();     // returnDirect 等附加设置
    String call(String toolInput);
    String call(String toolInput, ToolContext toolContext);
}

ToolDefinition

java 复制代码
public interface ToolDefinition {
    String name();
    String description();
    String inputSchema();
}

可手工写 schema:

java 复制代码
ToolDefinition toolDefinition = ToolDefinition.builder()
    .name("currentWeather")
    .description("Get the weather in location")
    .inputSchema("""
        {"type":"object","properties":{
            "location":{"type":"string"},
            "unit":{"type":"string","enum":["C","F"]}},
         "required":["location","unit"]}
        """)
    .build();

JSON Schema 定制

目的 可用注解(按优先级)
描述 @ToolParam(description) / @JsonClassDescription / @JsonPropertyDescription / @Schema(description)
可选性 @ToolParam(required=false) / @JsonProperty(required=false) / @Schema(required=false) / @Nullable

警告 :必需性设置错误会诱发幻觉。必填参数模型拿不到值时可能编造一个 → 无值真的可选就标 required = false。


五、结果转换与 returnDirect

结果转换

默认用 DefaultToolCallResultConverter(Jackson 转 JSON)把返回值序列化为 String 回传模型。

java 复制代码
@Tool(description = "Retrieve customer information", resultConverter = CustomToolCallResultConverter.class)
Customer getCustomerInfo(Long id) { ... }

returnDirect(绕过模型)

默认结果回传模型继续推理;设为 true 则直接返回调用者,适合:RAG 检索结果不想再加工、某些工具应终止 Agent 循环。

java 复制代码
@Tool(description = "Retrieve customer information", returnDirect = true)
Customer getCustomerInfo(Long id) { ... }

// 编程式
ToolMetadata.builder().returnDirect(true).build();

⚠️ 一次请求多个 tool call 时,必须全部 returnDirect=true 才直接返回,否则仍回传模型。


六、Tool 执行:框架控制 vs 用户控制

框架控制(默认)

internalToolExecutionEnabled = true(默认),DefaultToolCallingManager 在 ChatModel 内部透明完成:拦截 → 调用 → 回传 → 生成最终响应。

java 复制代码
public interface ToolCallingManager {
    List<ToolDefinition> resolveToolDefinitions(ToolCallingChatOptions chatOptions);
    ToolExecutionResult executeToolCalls(Prompt prompt, ChatResponse chatResponse);
}

⚠️ 内部往返消息不暴露给用户 ;想看到工具调用细节请改用用户控制模式。

自定义:@Bean ToolCallingManager toolCallingManager() { return ToolCallingManager.builder().build(); }

资格判定可换:ToolExecutionEligibilityPredicate。

用户控制(需要审计/人工确认时)

java 复制代码
ChatOptions chatOptions = ToolCallingChatOptions.builder()
    .toolCallbacks(new CustomerTools())
    .internalToolExecutionEnabled(false)      // 关键开关
    .build();
Prompt prompt = new Prompt("Tell me more about the customer with ID 42", chatOptions);
ChatResponse chatResponse = chatModel.call(prompt);

while (chatResponse.hasToolCalls()) {
    ToolExecutionResult result = toolCallingManager.executeToolCalls(prompt, chatResponse);
    prompt = new Prompt(result.conversationHistory(), chatOptions);
    chatResponse = chatModel.call(prompt);
}
System.out.println(chatResponse.getResult().getOutput().getText());

七、异常处理

工具抛异常 → 包装为 ToolExecutionException → 由 ToolExecutionExceptionProcessor 决定「转成错误消息回给模型」还是「抛给调用者」。

java 复制代码
@FunctionalInterface
public interface ToolExecutionExceptionProcessor {
    String process(ToolExecutionException exception);
}

默认策略(DefaultToolExecutionExceptionProcessor):

  • RuntimeException → 错误消息回传模型(让模型自行重试/换策略)
  • 检查异常与 Error(IOException、OutOfMemoryError)→ 总是抛出

全局开关:

属性 说明 默认
spring.ai.tools.throw-exception-on-error true=抛异常给调用者;false=转成消息回给模型 false
java 复制代码
@Bean
ToolExecutionExceptionProcessor toolExecutionExceptionProcessor() {
    return new DefaultToolExecutionExceptionProcessor(true);
}

自定义 ToolCallback 实现时,务必在 call() 中抛 ToolExecutionException 。

这与 ReactAgent 里的 ToolInterceptor 错误兜底是同一思想:把失败变成模型可读的信息,而不是让整条链路崩掉。


八、可观测性与日志

  • 观测点:spring.ai.tool(耗时、trace 传播)
  • tool 参数与结果导出为 span 属性:默认关闭(敏感信息)
  • 日志:org.springframework.ai 设为 DEBUG 即可看到全部 tool calling 关键操作

九、ToolContext:让工具"有记忆、知上下文"

工具签名里加 ToolContext 参数即可自动注入,且对 LLM 隐藏(模型只看到 input 的 schema)。

它提供五类信息:

能力 说明
State 执行中流动的可变数据(消息、计数器、自定义字段)
Context 不可变配置:user_id、会话信息、应用配置
Store 跨对话的长期记忆
Config RunnableConfig
Tool Call ID 当前调用 ID

访问 State

java 复制代码
public class ConversationSummaryTool implements BiFunction<String, ToolContext, String> {
    @Override
    public String apply(String input, ToolContext toolContext) {
        OverAllState state = (OverAllState) toolContext.getContext().get("state");
        RunnableConfig config = (RunnableConfig) toolContext.getContext().get("config");
        Map<String, Object> extraState = (Map<String, Object>) toolContext.getContext().get("extraState");

        List<Message> messages = (List<Message>) state.get("messages", new ArrayList<>());
        // 统计 user/assistant/tool 消息数 ...
        return "Conversation has %d user messages, %d AI responses, %d tool results".formatted(u, a, t);
    }
}

访问 Context(拿 user_id)

java 复制代码
public class AccountInfoTool implements BiFunction<String, ToolContext, String> {
    @Override
    public String apply(String query, ToolContext toolContext) {
        RunnableConfig config = (RunnableConfig) toolContext.getContext().get("config");
        String userId = (String) config.metadata("user_id").orElse(null);
        ...
    }
}

// 调用侧注入
RunnableConfig config = RunnableConfig.builder().addMetadata("user_id", "1");
agent.call("question", config);

更新 State

通过 Hook 在 AFTER_MODEL 等位置返回 Map 来更新:

java 复制代码
@Override
public CompletableFuture<Map<String, Object>> afterModel(OverAllState state, RunnableConfig config) {
    return CompletableFuture.completedFuture(Map.of(
        "user_name", "Alice", "last_updated", System.currentTimeMillis()));
}

长期记忆

java 复制代码
RedisSaver redisSaver = new RedisSaver(redissonClient);
ReactAgent agent = ReactAgent.builder().name("my_agent").model(chatModel)
    .tools(saveUserInfoTool, getUserInfoTool)
    .saver(redisSaver).build();

agent.call("Save user: id=abc123, name=Foo...", RunnableConfig.builder().threadId("session_1").build());
agent.call("Get user info for 'abc123'",            RunnableConfig.builder().threadId("session_2").build());
// 跨 session 仍能取到 → 这就是 Store 的价值

十、在 ReactAgent 中提供工具的 6 种方式

方式 用法 适用场景 优点 缺点
tools() 直接传 ToolCallback 实例 工具 < 5 个、编译期确定 简单、类型安全 多了代码冗长
methodTools() 传带 @Tool 的对象 工具逻辑成组、需访问成员变量 组织清晰 要写工具类
toolCallbackProviders() 实现 ToolCallbackProvider 运行时动态决定工具集 灵活 需实现接口
toolNames() + resolver() 只给名字 定义与使用解耦、配置化 解耦 必须配 resolver,否则抛异常
resolver() 自定义解析逻辑 多来源统一管理 高度灵活 需实现解析器
组合使用 以上混用 复杂/渐进迁移 最大灵活 复杂度上升
java 复制代码
ReactAgent agent = ReactAgent.builder()
    .name("combined_tool_agent").model(chatModel)
    .methodTools(calculatorTools)          // @Tool 类
    .toolCallbackProviders(toolProvider)   // 动态提供者
    .tools(searchTool)                     // 直接实例
    .saver(new MemorySaver())
    .build();

AssistantMessage response = agent.call("What's the weather like in San Francisco?");

十一、接入远程 MCP 工具

多数场景不必自己造工具,直接接 MCP Server(魔搭 ModelScope、百炼等平台)。

方式一:Spring Boot 自动发现(推荐)

xml 复制代码
<dependency>
    <groupId>org.springframework.ai</groupId>
    <artifactId>spring-ai-starter-mcp-client</artifactId>
</dependency>
yaml 复制代码
spring:
  ai:
    mcp:
      client:
        enabled: true
        name: saa-mcp-client
        toolcallback:
          enabled: true
        type: async
        streamable-http:
          connections:
            amap-maps:
              url: ${MODEL_SCOPE_AMAP_BASE_URL}
              endpoint: mcp
        sse:
          connections:
            12306-mcp:
              url: ${MODEL_SCOPE_12306_BASE_URL}
              sse-endpoint: sse
java 复制代码
@Service
public class RemoteMcpToolsExample {
    private final ToolCallbackProvider toolCallbackProvider;

    public void run() throws GraphRunnerException {
        Builder builder = ReactAgent.builder()
                .name("travel_planning_assistant").model(chatModel)
                .instruction("You are a helpful assistant with travel route planning and train ticket search.")
                .saver(new MemorySaver());
        builder.toolCallbackProviders(toolCallbackProvider);   // 一行接入全部远程工具
        ReactAgent agent = builder.build();
        ...
    }
}

方式二:MCP SDK 手工构建(脱离 Spring Boot)

关键四步:

  1. 创建传输层 HttpClientSseClientTransport / HttpClientStreamableHttpTransport
  2. 构建并初始化 McpSyncClient
  3. listTools() 拉取远程工具列表
  4. 逐个转成 FunctionToolCallback
java 复制代码
private ToolCallback createToolCallback(McpSchema.Tool mcpTool, McpSyncClient mcpClient, String serverName) {
    return FunctionToolCallback.builder(mcpTool.name(),
            (Map<String, Object> functionInput) -> { /* 真正发起 MCP 调用 */ })
            .description(mcpTool.description())
            .inputType(Map.class)
            .build();
}

记得在 finally 里逐个 client.close()。

流式下区分模型/工具输出

java 复制代码
stream.doOnNext(output -> {
    if (output.node().equals("_AGENT_MODEL_")) {
        answer.append(((StreamingOutput<?>) output).message().getText());
    } else if (output.node().equals("_AGENT_TOOL_")) {
        answer.append("Tool Call:").append(
            ((ToolResponseMessage) ((StreamingOutput<?>) output).message()).getResponses().get(0));
    }
}).blockLast();

十二、速查卡

java 复制代码
// 1) 声明式定义
@Tool(description = "一句话说清何时用、怎么用")
String xxx(@ToolParam(description = "格式说明") String p) { ... }

// 2) 挂到 ChatClient
ChatClient.create(chatModel).prompt(q).tools(new XxxTools()).call().content();

// 3) 挂到 ChatModel
new Prompt(q, ToolCallingChatOptions.builder().toolCallbacks(cb).build());

// 4) 用户控制执行
.internalToolExecutionEnabled(false)  +  while (resp.hasToolCalls()) { ... executeToolCalls ... }

// 5) 拿上下文(BiFunction 第二参)
BiFunction<String, ToolContext, String> → toolContext.getContext().get("config" / "state" / "extraState")

// 6) ReactAgent
ReactAgent.builder().tools(t1, t2).methodTools(obj).toolCallbackProviders(p).toolNames(n).resolver(r)

八条铁律

  1. description 决定一切 ------ 写得含糊,模型就不会用或乱用。
  2. 工具名在同一请求内必须唯一。
  3. 函数式工具不支持基本类型与集合,用 POJO 或改方法式。
  4. 方法式工具不支持 Optional / 异步 / 响应式类型。
  5. 参数默认必填;确实可缺省的标 required=false,否则诱发幻觉。
  6. 多个 tool call 同批返回时,全部 returnDirect=true 才直接返回给调用者。
  7. 自定义 ToolCallback 要在 call() 抛 ToolExecutionException。
  8. toolNames() 必须配 resolver()。

十三、对照本项目(xs-interview-agent)

当前项目完全没有使用工具调用,三次 LLM 调用都是纯 Prompt 驱动。最值得加的三个工具:

工具 类型 作用 实现要点
queryQuestionBank(category) 信息检索 从本地题库/ES 召回真实高频题,再让模型结合简历改写 解决"纯生成题目可能不专业"的问题;入参 record:category、difficulty、count
saveInterviewReport(...) 执行操作 评估报告落 MySQL,替代 ConcurrentHashMap 可设 returnDirect = true,无需回传模型二次加工
searchJD(keyword) 信息检索 拉岗位 JD,让简历评分对照真实招聘需求 正好呼应 resume-analysis-system.st 里"招聘需求提到的加分项"

落地最小改动 :出题环节在 MockInterviewService 里从 chatModel.call(prompt) 换成带工具的调用即可:

java 复制代码
ChatOptions options = ToolCallingChatOptions.builder()
        .toolCallbacks(questionBankTool)          // 题库工具
        .build();
// 或迁到 ReactAgent:.tools(questionBankTool).outputType(InterviewQuestions.class)

若想借助平台现成能力(如联网搜索最新技术趋势来出题),按第十一节加 spring-ai-starter-mcp-client 后 builder.toolCallbackProviders(toolCallbackProvider) 一行接入即可,不必自己写工具。

相关推荐
步行cgn1 小时前
Spring 事务传播行为详解
java·数据库·spring
anxiao_m2 小时前
跨地域大文件怎么传?2026主流传输软件实测对比
大数据·数据库·文件传输
Su米苏2 小时前
基于 Token 预算的上下文压缩控制器(Context Compaction Controller)
前端·数据库·人工智能
暖核2 小时前
Redis 从基础到集群实战:数据类型、客户端、高可用架构完整梳理
数据库·redis·架构
2401_888859712 小时前
STM32H733 MPU、AXI、FMC学习
java·开发语言·stm32·spring
神一样的老师2 小时前
WS63 访问 HTTPS 握手失败(-0x7780)根治
数据库·网络协议·https
Patrick在香港2 小时前
时间戳凭空早了 8 小时:datetime.utcnow() 弃用实测与漂移复盘
数据库·python·标准库·datetime·时区·弃用
CV工程师丁Sir4 小时前
ArkWeb 手记 04|DevTools 调试与缓存清理
java·spring·缓存·harmonyos
李兆龙的博客10 小时前
问津集 #26:Lakebase——Postgres 的版本化页面存储、数据库分支与计算弹性
数据库