第5篇:《AI学会调接口了:一个@Tool注解让大模型查了今天的天气》

承上:上一篇我们让AI学会了流式输出,体验已经和ChatGPT一样丝滑了。但它依然是个"嘴强王者"------能说会道,但查不了数据库、调不了接口、做不了任何实事。今天,我们要给AI装上第一只手。

1. 问题场景:AI的"信息孤岛"

先看一个让人血压升高的对话:

翻译成人话就是:"这事我干不了,你找别人吧。"

任何后端老鸟看到这个回答都会想:我明明有一个天气接口,AI为什么不能自己调?

答案很简单:AI根本不知道你有这个接口。它的世界就是一个黑盒子,除非你主动告诉它。

这就引出了今天的主角------Function Calling(函数调用) ,Spring AI中对应的是 @Tool 注解。

2. 核心概念:用类比理解Function Calling

用你最熟悉的场景来类比:

你招了一个实习生,他很聪明,但对你公司的系统一无所知。

你想让他帮忙查订单,就必须先告诉他:

  1. 有这个能力:"我们有一个查订单的接口"
  2. 怎么用:"入参是订单号,出参是订单状态、金额、时间"
  3. 什么时候用:"用户提到'查订单'、'订单状态'时,就调这个接口"

Function Calling就是干这三件事的:

  1. 声明工具 :用 @Tool 注解标记一个方法
  2. 描述Schema :用 @ToolParam 描述入参
  3. AI决策:AI根据用户问题,自动判断要不要调工具、传什么参数

3. 第一只手:查天气

3.1. 模拟天气服务

typescript 复制代码
public class WeatherTools {
    @Tool(description = "查询指定城市在指定日期的天气情况")
    public String getWeather(@ToolParam(description = "城市名称,例如:北京、上海、深圳") String city) {
        // 模拟数据
        Map<String, String> weatherMap = Map.of(
                "北京", "晴,25°C,微风",
                "上海", "多云转阴,28°C,东南风3级",
                "深圳", "雷阵雨,30°C,湿度85%",
                "成都", "阴天,22°C,空气质量优"
        );

        String weather = weatherMap.getOrDefault(city, "数据暂未覆盖该城市");
        return city  + " 天气:" + weather;
    }
}

拆解这个注解三件套:

注解 作用 类比
@Tool 声明这是一个AI可调用的工具 告诉实习生"有查天气这个能力"
description 描述工具的用途 告诉实习生"这个接口是干什么的"
@ToolParam 描述每个参数的含义 告诉实习生"city是什么,date是什么格式"

这些描述不是给人看的,是给AI看的。 AI会根据这些描述来决定:要不要调这个工具、传什么参数。

3.2. 注册工具

arduino 复制代码
package com.yunxi.ai.service;
import com.yunxi.ai.tools.WeatherTools;
import lombok.extern.slf4j.Slf4j;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;

@Slf4j
@Service
public class ChatService {

    private final ChatClient chatClient;

    private final WeatherTools weatherTools;

    public ChatService(ChatClient.Builder builder, WeatherTools weatherTools) {
        this.chatClient = builder
                .defaultAdvisors(new SimpleLoggerAdvisor())
                .build();
        this.weatherTools = weatherTools;
    }

    /**
     * 流式调用
     */
    public Flux<String> chatFlux(String message) {
        return chatClient.prompt()
                .system("你是一个专业的助手,可以回答用户的问题")
                .user(message)
                .tools(weatherTools)
                .stream()
                .content()
                .doOnCancel(() -> {
                    // 用户关闭了连接,可以在这里记录日志或释放资源
                    log.info("用户中断了对话,停止Token消耗");
                });
    }
}

3.3. 测试效果

发生了什么?

  1. AI分析用户问题,判断需要调用 getWeather
  2. AI自动提取参数:city=北京date=今天日期
  3. AI调用工具,拿到结果:"北京 2026-07-23 天气:晴,25°C,微风"
  4. AI把结果组织成自然语言回复给用户

这是整个交互最巧妙的地方,由一个叫 ToolCallingAdvisor 的组件负责,其工作就像一个"智能调度器",它会自动处理模型和工具之间的多次来回交互:

  1. 模型决策:接收到你的问题和工具清单后,模型会判断是否需要调用工具。
  2. 发起调用请求:如果需要,模型会返回一个结构化的指令,指明要调用的工具名和参数。
  3. 框架执行ToolCallingAdvisor 拦截到这个指令,找到对应的本地 Java 方法,传参并执行。
  4. 结果回传 :执行结果被返回给 ToolCallingAdvisor,它再将结果作为"新上下文"发回给模型。
  5. 生成最终回答:模型拿到工具返回的真实数据后,才能生成最终的自然语言回复

这就是Function Calling的完整闭环。

4. 看AI到底调了啥

我们先看一下一次对话,客户端和大模型的交互日志:

perl 复制代码
2026-07-24T11:32:24.507+08:00 DEBUG 12511 --- [nio-9999-exec-2] o.s.web.servlet.DispatcherServlet        : GET "/chat/call?message=%E5%8C%97%E4%BA%AC%E7%9A%84%E5%A4%A9%E6%B0%94", parameters={masked}
2026-07-24T11:32:24.508+08:00 DEBUG 12511 --- [nio-9999-exec-2] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to com.yunxi.ai.controller.ChatController#chatCall(String)
2026-07-24T11:32:24.523+08:00 DEBUG 12511 --- [nio-9999-exec-2] o.s.web.client.DefaultRestClient         : Writing [ChatCompletionRequest[messages=[ChatCompletionMessage[rawContent=你是一个专业的助手,可以回答用户的问题, role=SYSTEM, name=null, toolCallId=null, toolCalls=null, refusal=null, audioOutput=null, annotations=null], ChatCompletionMessage[rawContent=北京的天气, role=USER, name=null, toolCallId=null, toolCalls=null, refusal=null, audioOutput=null, annotations=null]], model=deepseek-v4-pro, store=null, metadata=null, frequencyPenalty=null, logitBias=null, logprobs=null, topLogprobs=null, maxTokens=null, maxCompletionTokens=null, n=null, outputModalities=null, audioParameters=null, presencePenalty=null, responseFormat=null, seed=null, serviceTier=null, stop=null, stream=false, streamOptions=null, temperature=0.7, topP=null, tools=[org.springframework.ai.openai.api.OpenAiApi$FunctionTool@3f3d5fc3], toolChoice=null, parallelToolCalls=null, user=null, reasoningEffort=null, webSearchOptions=null]] as "application/json" with org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
2026-07-24T11:32:25.776+08:00 DEBUG 12511 --- [nio-9999-exec-2] o.s.web.client.DefaultRestClient         : Reading to [org.springframework.ai.openai.api.OpenAiApi$ChatCompletion]
2026-07-24T11:32:25.777+08:00 DEBUG 12511 --- [nio-9999-exec-2] o.s.a.m.tool.DefaultToolCallingManager   : Executing tool call: getWeather
2026-07-24T11:32:25.778+08:00 DEBUG 12511 --- [nio-9999-exec-2] o.s.ai.tool.method.MethodToolCallback    : Starting execution of tool: getWeather
2026-07-24T11:32:25.778+08:00 DEBUG 12511 --- [nio-9999-exec-2] o.s.ai.tool.method.MethodToolCallback    : Successful execution of tool: getWeather
2026-07-24T11:32:25.778+08:00 DEBUG 12511 --- [nio-9999-exec-2] o.s.a.t.e.DefaultToolCallResultConverter : Converting tool result to JSON.
2026-07-24T11:32:25.780+08:00 DEBUG 12511 --- [nio-9999-exec-2] o.s.web.client.DefaultRestClient         : Writing [ChatCompletionRequest[messages=[ChatCompletionMessage[rawContent=你是一个专业的助手,可以回答用户的问题, role=SYSTEM, name=null, toolCallId=null, toolCalls=null, refusal=null, audioOutput=null, annotations=null], ChatCompletionMessage[rawContent=北京的天气, role=USER, name=null, toolCallId=null, toolCalls=null, refusal=null, audioOutput=null, annotations=null], ChatCompletionMessage[rawContent=好的,我来帮您查询北京的天气。, role=ASSISTANT, name=null, toolCallId=null, toolCalls=[ToolCall[index=null, id=call_00_8gwC0nhjGCrlHMcUg1KF6313, type=function, function=ChatCompletionFunction[name=getWeather, arguments={"city": "北京"}]]], refusal=null, audioOutput=null, annotations=null], ChatCompletionMessage[rawContent="北京 天气:晴,25°C,微风", role=TOOL, name=getWeather, toolCallId=call_00_8gwC0nhjGCrlHMcUg1KF6313, toolCalls=null, refusal=null, audioOutput=null, annotations=null]], model=deepseek-v4-pro, store=null, metadata=null, frequencyPenalty=null, logitBias=null, logprobs=null, topLogprobs=null, maxTokens=null, maxCompletionTokens=null, n=null, outputModalities=null, audioParameters=null, presencePenalty=null, responseFormat=null, seed=null, serviceTier=null, stop=null, stream=false, streamOptions=null, temperature=0.7, topP=null, tools=[org.springframework.ai.openai.api.OpenAiApi$FunctionTool@4b8f7cbf], toolChoice=null, parallelToolCalls=null, user=null, reasoningEffort=null, webSearchOptions=null]] as "application/json" with org.springframework.http.converter.json.MappingJackson2HttpMessageConverter
2026-07-24T11:32:27.775+08:00 DEBUG 12511 --- [nio-9999-exec-2] o.s.web.client.DefaultRestClient         : Reading to [org.springframework.ai.openai.api.OpenAiApi$ChatCompletion]
2026-07-24T11:32:30.525+08:00 DEBUG 12511 --- [nio-9999-exec-2] m.m.a.RequestResponseBodyMethodProcessor : Using 'text/plain', given [*/*] and supported [text/plain, */*, application/json, application/*+json]
2026-07-24T11:32:30.526+08:00 DEBUG 12511 --- [nio-9999-exec-2] m.m.a.RequestResponseBodyMethodProcessor : Writing ["北京今天的天气情况如下:<EOL><EOL>- **天气**:晴 ☀️<EOL>- **温度**:25°C<EOL>- **风力**:微风<EOL><EOL>天气不错,非常适合出行!请问还有什么可以帮您的吗?"]
2026-07-24T11:32:30.528+08:00 DEBUG 12511 --- [nio-9999-exec-2] o.s.web.servlet.DispatcherServlet        : Completed 200 OK 

4.1. 🔍 日志分步解读

4.1.1. 接收请求和准备第一轮请求

sql 复制代码
GET "/chat/call?message=北京的天气"
...
Writing [ChatCompletionRequest... messages=..., tools=[...]]
  • 含义 :你发起了"北京的天气"这个请求,Spring AI 把它和工具说明(tools=[...])一起打包,准备发给 DeepSeek 模型。
  • 关键点 :此时请求体里包含了你的问题(USER)和工具定义(tools),但还没有任何 toolCalls

4.1.2. 模型决策 & 工具执行 ✅

yaml 复制代码
Executing tool call: getWeather
Starting execution of tool: getWeather
Successful execution of tool: getWeather
  • 含义 :这是最核心的部分!DeepSeek 模型收到了工具清单后,决定调用 getWeather 工具,Spring AI 自动执行了你本地的 Java 方法。
  • 关键点这三行日志就是你本地工具被成功调用的铁证。说明模型决策、框架拦截、方法反射执行,整个链路是通的。

4.1.3. 第二轮请求(带工具结果)

css 复制代码
Writing [ChatCompletionRequest... messages=[  ...,  ChatCompletionMessage[rawContent=好的,我来帮您查询北京的天气。, role=ASSISTANT, toolCalls=[ToolCall...]], 
  ChatCompletionMessage[rawContent="北京 天气:晴,25°C,微风", role=TOOL, ...]
]]
  • 含义:这是第二轮请求的日志。Spring AI 把以下内容再次发给 DeepSeek:
    1. 历史对话(系统提示 + 你的问题)
    2. 模型第一轮的决策role=ASSISTANT,内含 toolCalls
    3. 你本地工具的执行结果role=TOOL,内容是"北京 天气:晴,25°C,微风")
  • 关键点 :这就是你之前问"为什么 toolCalls 是空的"的答案 ------ toolCalls 出现在第二轮请求的"历史消息"里 ,而不是最终响应的 ChatResponse 里。

4.1.4. 生成最终回答

css 复制代码
Reading to [ChatCompletion...]
Writing ["北京今天的天气情况如下:..."]
Completed 200 OK
  • 含义:DeepSeek 模型基于你工具返回的真实数据(晴,25°C),润色生成了最终的自然语言回答,返回给你。

4.2. 📊 Spring AI + DeepSeek 工具调用完整交互图

bash 复制代码
┌─────────┐      ┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│  用户    │      │ Spring AI   │      │  DeepSeek   │      │  本地工具    │
│ (Client) │      │  框架层     │      │   模型      │      │ (Java方法)   │
└────┬────┘      └──────┬──────┘      └──────┬──────┘      └──────┬──────┘
     │                  │                    │                    │
     │ ① GET /chat?     │                    │                    │
     │   message=北京的天气│                    │                    │
     │─────────────────>│                    │                    │
     │                  │                    │                    │
     │                  │ ② 构建 Prompt      │                    │
     │                  │   - System Msg     │                    │
     │                  │   - User Msg       │                    │
     │                  │   - Tools (说明书)  │                    │
     │                  │                    │                    │
     │                  │ ③ POST /v1/chat/   │                    │
     │                  │   completions      │                    │
     │                  │  (第一轮请求)      │                    │
     │                  │───────────────────>│                    │
     │                  │                    │                    │
     │                  │                    │ ④ 模型推理决策      │
     │                  │                    │    "需要调用       │
     │                  │                    │     getWeather"    │
     │                  │                    │                    │
     │                  │ ⑤ 返回 tool_calls  │                    │
     │                  │   getWeather(北京) │                    │
     │                  │<───────────────────│                    │
     │                  │                    │                    │
     │                  │ ⑥ 解析 tool_calls  │                    │
     │                  │   "model 想调用    │                    │
     │                  │    getWeather"     │                    │
     │                  │                    │                    │
     │                  │ ⑦ 反射调用本地方法  │                    │
     │                  │─────────────────────────────────────────>│
     │                  │                    │                    │
     │                  │ ⑧ 执行结果返回     │                    │
     │                  │   "晴, 25°C, 微风" │                    │
     │                  │<─────────────────────────────────────────│
     │                  │                    │                    │
     │                  │ ⑨ 构建第二轮请求    │                    │
     │                  │   - 历史消息       │                    │
     │                  │   - tool_calls     │                    │
     │                  │   - tool 执行结果   │                    │
     │                  │                    │                    │
     │                  │ ⑩ POST /v1/chat/   │                    │
     │                  │   completions      │                    │
     │                  │  (第二轮请求)      │                    │
     │                  │───────────────────>│                    │
     │                  │                    │                    │
     │                  │                    │ ⑪ 模型生成最终回答  │
     │                  │                    │    基于工具结果     │
     │                  │                    │    润色成自然语言   │
     │                  │                    │                    │
     │                  │ ⑫ 返回最终回答     │                    │
     │                  │   "北京天气晴朗..." │                    │
     │                  │<───────────────────│                    │
     │                  │                    │                    │
     │ ⑬ 返回最终结果   │                    │                    │
     │  "北京天气晴朗..."│                    │                    │
     │<─────────────────│                    │                    │
     │                  │                    │                    │
步骤 时间戳 事件 说明
11:32:24.507 用户发起请求 GET /chat/call?message=北京的天气
11:32:24.523 第一轮请求发出 携带 tools工具说明
11:32:25.776 收到模型决策 模型返回 tool_calls
⑥-⑧ 11:32:25.777-778 执行本地工具 DefaultToolCallingManager+ MethodToolCallback
11:32:25.780 第二轮请求发出 携带工具执行结果
11:32:27.775 收到最终回答 模型基于工具结果生成
11:32:30.525 返回给用户 最终输出

5. 多个@Tool方法:一个类暴露多个工具

less 复制代码
@Component
public class CommonTools {

    @Tool(description = "查询指定城市的天气")
    public String getWeather(
            @ToolParam(description = "城市名称") String city,
            @ToolParam(description = "日期") LocalDate date) {
        // ...
    }

    @Tool(description = "查询指定城市的限行规则")
    public String getTrafficRule(
            @ToolParam(description = "城市名称") String city) {
        return city + "今日限行尾号:3和8";
    }

    @Tool(description = "获取当前服务器时间")
    public String getCurrentTime() {
        return LocalDateTime.now().toString();
    }
}

AI会根据用户问题自动选择:

  • "今天天气怎么样" → 调 getWeather
  • "今天限行吗" → 调 getTrafficRule
  • "现在几点" → 调 getCurrentTime
  • "今天天气和限行都说一下" → 可能依次调用多个工具

6. 本篇避坑指南

6.1. 坑1:AI不调用工具,直接瞎编答案

AI有时候会"懒",明明有工具却不调用,直接编一个答案。

解决 :在 @Tool(description) 里写得足够明确,必要时在SystemMessage里强调"必须调用工具获取实时数据,不要编造"。

6.2. 坑2:AI编造参数

用户问"火星天气",AI也可能传 city=火星。如果你的工具不支持,返回错误提示即可。

6.3. 坑3:参数类型不匹配

@ToolParam 标注了 LocalDate date,但AI传了 "今天"。Spring AI会尽量做类型转换,但复杂的日期格式建议用 String 接收后手动解析。

6.4. 坑4:Tool方法执行时间过长

工具调用默认有超时限制。如果调第三方接口很慢,考虑异步处理或设置合理的超时时间。

6.5. 坑5:忘记注册Tools

scss 复制代码
// 错误:忘了加 .tools()
chatClient.prompt().user(question).call().content();

// 正确
chatClient.prompt().user(question).tools(weatherTools).call().content();

忘了加 .tools(),AI不会调用任何工具,直接靠自己回答。这是新手最容易犯的错。

7. 本篇小结

这一篇我们让AI长出了第一只手:

关键点 说明
@Tool 声明一个AI可调用的工具方法
@ToolParam 描述参数的Schema,AI据此传参
.tools(bean) 注册工具到ChatClient
自动决策 AI根据用户问题判断要不要调工具、传什么参数

从这一篇开始,AI不再是"嘴炮",它能真正干活了。

但一个工具还不够。下一篇,我们要给AI装上"瑞士军刀"------同时暴露查订单、查库存、查物流三个工具,让它自己决定该用哪个。


本文与DeepSeek协作完成

相关推荐
格尔曼Noah1 小时前
插入USB设备时,vmware不提示连接到主机还是虚拟机
ai编程
春风野草1 小时前
AI Agent 工程底座实战:模型路由、限流、错误处理和成本意识
aigc·ai编程
孤狼GPT2 小时前
从聊天工具到开发系统:ChatGPT、Codex、Plus与Pro正在重新分工
chatgpt·ai编程·codex·chatgpt plus·chatgpt pro
小兔子2 小时前
我仿照OpenAI的测试方法,给自己的AI Agent做了个逃逸测试,结果..
openai
cooldream20093 小时前
AI 编程系列之 11:AI Coding 工程师的能力模型——5 年后的护城河
ai编程·vibe coding·claude code
Ai拆代码的曹操3 小时前
一篇搞懂 opencode 的三层错误处理架构
openai
烬羽3 小时前
AI 写代码总翻车?试试"先画图再砌墙"的 Vibe Coding 三步法
react.js·ai编程·vibecoding
太平洋月光3 小时前
AI 快捷指令:Cursor Rules · Commands · Skills
前端·ai编程
唐老板3 小时前
AI 编程的保密底线:企业代码不能这么漏
ai编程