AI Agent 开发实战(四):三大基石之工具调用 —— 让 LLM 长出手脚

前三篇我们聊了 Agent 的概念架构、LLM 调用与 Prompt 工程、记忆系统。今天到了三大基石的最后一环:工具调用(Tool Calling)。没有工具的 Agent 只是聊天机器人,有了工具的 Agent 才能真正"做事"。


一、为什么 Agent 需要工具?

LLM 本质上是一个文本生成模型,它:

  • 不知道今天的天气
  • 查不了数据库
  • 调不了 API
  • 算不准复杂算术

它有的只是训练数据中的"知识记忆"。但真实业务场景中,Agent 需要和外部世界交互——查订单、发通知、调接口、跑脚本。

工具调用就是给 LLM 长出"手脚"的机制。

┌─────────┐     结构化指令      ┌──────────┐
│   LLM   │ ──────────────────▶ │  工具执行器  │
│ (大脑)   │ ◀────────────────── │ (手脚)    │
└─────────┘     执行结果        └──────────┘
     │                              │
     │  可能多轮:LLM 判断          │  实际调用
     │  "还需要再查一个"            │  HTTP / DB / Shell
     ▼                              ▼
  最终回复给用户                  外部系统

二、工具调用的本质:一次"LLM → 执行 → 回传"的闭环

工具调用的核心流程其实很简单,就三步:

  1. LLM 输出结构化指令:告诉框架"我要调用哪个工具,参数是什么"
  2. 框架执行工具:实际调用对应的函数/API
  3. 结果回传 LLM:把执行结果塞回对话上下文,让 LLM 决定下一步

用伪代码表示:

while not finished:
    response = llm.chat(messages)
    
    if response.has_tool_calls():
        for tool_call in response.tool_calls:
            result = execute_tool(tool_call.name, tool_call.arguments)
            messages.append(tool_result_message(tool_call.id, result))
    else:
        # LLM 给出了最终回复
        print(response.content)
        finished = True

关键认知:LLM 本身不执行任何工具,它只是"决定"该调用什么。真正执行的是你写的代码(框架层)。


三、Function Calling 协议解析

OpenAI 定义的 Function Calling 协议已成为行业事实标准,大多数 LLM 提供商和框架都兼容这个格式。

3.1 工具定义

向 LLM 注册一个工具,本质是告诉它"你有这个能力":

{
  "type": "function",
  "function": {
    "name": "get_weather",
    "description": "获取指定城市的当前天气信息",
    "parameters": {
      "type": "object",
      "properties": {
        "city": {
          "type": "string",
          "description": "城市名称,如'北京'"
        },
        "unit": {
          "type": "string",
          "enum": ["celsius", "fahrenheit"],
          "description": "温度单位"
        }
      },
      "required": ["city"]
    }
  }
}

三个必填字段

字段 作用 要点
name 工具标识 要语义化,LLM 靠名字猜功能
description 功能描述 越清晰 LLM 选工具越准
parameters 参数 Schema JSON Schema 格式,required 必填

3.2 LLM 的响应

当 LLM 决定调用工具时,它不会生成普通文本,而是返回 tool_calls

{
  "role": "assistant",
  "content": null,
  "tool_calls": [
    {
      "id": "call_abc123",
      "type": "function",
      "function": {
        "name": "get_weather",
        "arguments": "{\"city\": \"北京\", \"unit\": \"celsius\"}"
      }
    }
  ]
}

3.3 工具结果的回传

{
  "role": "tool",
  "tool_call_id": "call_abc123",
  "content": "{\"city\": \"北京\", \"temperature\": 28, \"condition\": \"晴\"}"
}

一个完整的工具调用轮次 = 1 个 assistant(含 tool_calls) + N 个 tool(每个 tool_call_id 对应一个结果)


四、Java 生态实现:Spring AI 的 ToolCallback

前面我们用 Python 伪代码演示了流程,现在看看 Java 生态怎么落地。

4.1 Spring AI 的工具注册方式

Spring AI 提供了两种注册方式:

方式一:@Tool 注解(推荐)

@Component
public class WeatherTools {

    @Tool(description = "获取指定城市的当前天气信息")
    public String getWeather(
            @ToolParam(description = "城市名称") String city,
            @ToolParam(description = "温度单位,celsius或fahrenheit") String unit) {
        // 调用天气 API
        WeatherResponse response = weatherClient.query(city, unit);
        return String.format("城市:%s,温度:%s°%s,天气:%s",
                city, response.getTemp(), unit, response.getCondition());
    }
}

方式二:FunctionCallback 手动注册

@Bean
public FunctionCallback weatherFunction() {
    return FunctionCallback.builder()
            .name("get_weather")
            .description("获取指定城市的当前天气信息")
            .inputType(WeatherRequest.class)
            .function(req -> weatherClient.query(req.getCity(), req.getUnit()))
            .build();
}

两种方式对比:

维度 @Tool 注解 FunctionCallback
代码简洁度 ★★★★★ ★★★
灵活度 ★★★ ★★★★★
参数 Schema 自动推断 手动指定 inputType
适合场景 简单查询/操作 复杂参数、动态注册

4.2 将工具绑定到 ChatClient

@Autowired
private WeatherTools weatherTools;

public String chat(String userMessage) {
    return chatClient.prompt()
            .user(userMessage)
            .tools(weatherTools)  // 注册工具
            .call()
            .content();
}

当用户输入"北京今天天气怎么样",Spring AI 自动完成:

  1. getWeather 工具定义发给 LLM
  2. LLM 返回 tool_calls: [{name: "get_weather", arguments: {city: "北京"}}]
  3. 框架调用 weatherTools.getWeather("北京", "celsius")
  4. 结果回传 LLM
  5. LLM 组织最终自然语言回复

整个过程对你透明,你只写业务逻辑。


五、工具注册与发现机制

当 Agent 拥有几十甚至上百个工具时,如何管理?

5.1 分组注册

@Configuration
public class ToolConfig {

    @Bean
    @Description("天气相关工具组")
    public List<FunctionCallback> weatherTools() {
        return List.of(
            FunctionCallback.builder()
                .name("get_weather").description("查天气")
                .inputType(WeatherReq.class).function(this::queryWeather).build(),
            FunctionCallback.builder()
                .name("get_forecast").description("查预报")
                .inputType(ForecastReq.class).function(this::queryForecast).build()
        );
    }

    @Bean
    @Description("数据库查询工具组")
    public List<FunctionCallback> dbTools() { /* ... */ }
}

5.2 动态工具发现

更高级的做法是运行时动态选择工具:

public String chat(String userMessage, String[] enabledToolGroups) {
    List<FunctionCallback> activeTools = toolGroups.stream()
            .filter(g -> Arrays.asList(enabledToolGroups).contains(g.getGroup()))
            .flatMap(g -> g.getTools().stream())
            .collect(Collectors.toList());

    return chatClient.prompt()
            .user(userMessage)
            .functions(activeTools.toArray(new FunctionCallback[0]))
            .call()
            .content();
}

设计原则

原则 说明
单一职责 一个工具只做一件事
描述精确 description 决定 LLM 能不能选对工具
参数最小化 只传必要参数,减少 LLM 出错概率
结果结构化 返回 JSON 而非自然语言,LLM 解析更准

六、多工具编排:ReAct 循环中的工具选择

当 Agent 有多个工具可用时,LLM 如何选择?这就要回到 ReAct(Reasoning + Acting)模式:

用户: "帮我查下北京天气,如果下雨就查一下会议室是否空闲并预订"

┌─────────────────────────────────────────────┐
│  ReAct 循环                                   │
│                                               │
│  Thought: 用户想查天气,先调天气工具            │
│  Action: get_weather(city="北京")              │
│  Observation: 北京,晴,28°C                   │
│                                               │
│  Thought: 天气晴朗,不需要查会议室              │
│  Answer: 北京今天晴天28°C,不需要预订会议室~    │
└─────────────────────────────────────────────┘

如果天气是"暴雨":

│  Observation: 北京,暴雨,18°C                 │
│                                               │
│  Thought: 下暴雨需要室内会议,查一下会议室      │
│  Action: check_room(date="today", type="会议") │
│  Observation: A301空闲                         │
│                                               │
│  Thought: 有空会议室,执行预订                  │
│  Action: book_room(room="A301", date="today")  │
│  Observation: 预订成功                         │
│                                               │
│  Answer: 北京暴雨,已为您预订A301会议室          │

这就是 Agent 的"思考链"——每一步都根据上一步的结果决定下一步。传统硬编码流程做不到这种条件分支。


七、实战:天气 + 数据库查询的复合工具 Agent

来一个完整示例,用 Spring AI 搭一个能查天气、查订单的 Agent。

7.1 工具定义

@Component
public class OrderTools {

    @Tool(description = "根据订单号查询订单详情")
    public String queryOrder(
            @ToolParam(description = "订单号") String orderId) {
        Order order = orderService.getById(orderId);
        if (order == null) {
            return "订单不存在";
        }
        return String.format("订单号:%s,商品:%s,金额:%.2f,状态:%s,收货城市:%s",
                order.getId(), order.getItem(), order.getAmount(),
                order.getStatus(), order.getCity());
    }

    @Tool(description = "根据城市和日期查询该城市的发货订单列表")
    public String queryOrdersByCity(
            @ToolParam(description = "城市名称") String city,
            @ToolParam(description = "日期,格式yyyy-MM-dd") String date) {
        List<Order> orders = orderService.getByCityAndDate(city, date);
        return orders.stream()
                .map(o -> o.getId() + ":" + o.getItem() + " 金额" + o.getAmount())
                .collect(Collectors.joining("\n"));
    }
}

7.2 Agent 服务

@Service
public class OrderAssistant {

    private final ChatClient chatClient;

    public OrderAssistant(ChatClient.Builder builder,
                          WeatherTools weatherTools,
                          OrderTools orderTools) {
        this.chatClient = builder
                .defaultTools(weatherTools, orderTools)
                .build();
    }

    public String chat(String userMessage) {
        return chatClient.prompt()
                .user(userMessage)
                .call()
                .content();
    }
}

7.3 对话效果

用户: 北京今天天气怎么样?那边有多少待发货订单?

Agent 内部流程:
1. 调用 get_weather(city="北京")     → 晴, 28°C
2. 调用 query_orders_by_city(city="北京", date="2026-07-24") → 3条订单
3. 组织回复

Agent: 北京今天晴天28°C,挺适合出行的!目前北京有3笔待发货订单:
- ORD001: 无线耳机 金额¥299
- ORD005: 充电宝 金额¥128  
- ORD012: 手机壳 金额¥39

注意:用户只说了一句话,Agent 自动拆解为两个工具调用——这就是工具调用的魅力。


八、常见坑与最佳实践

坑 1:description 写得像注释,LLM 选错工具

// ❌ 太简略,LLM 可能和别的查询工具混淆
@Tool(description = "查询")
public String query(String type) { ... }

// ✅ 精确描述功能、参数含义、返回内容
@Tool(description = "根据订单号查询订单详情,返回商品名、金额、状态")
public String queryOrder(@ToolParam(description = "订单号,如ORD001") String orderId) { ... }

坑 2:工具返回一大段自然语言

// ❌ LLM 很难从自然语言中提取结构化信息
return "北京今天的天气是晴天,温度28摄氏度,湿度65%,风速3级...";

// ✅ 返回结构化 JSON
return "{\"city\":\"北京\",\"temp\":28,\"condition\":\"晴\",\"humidity\":65}";

坑 3:工具数量太多导致选择困难

LLM 在 10+ 工具场景下选工具的准确率会明显下降。

解决方案

方案 说明
工具分组 按业务域分组,每次只传入相关组
两阶段路由 先让 LLM 分类用户意图,再选工具子集
工具描述优化 让每个工具的 description 有明显区分度

坑 4:工具执行失败没有兜底

@Tool(description = "查询天气")
public String getWeather(String city) {
    // ❌ 如果 API 超时直接抛异常,整个 Agent 崩溃
    return weatherClient.query(city);  
}

@Tool(description = "查询天气")
public String getWeather(String city) {
    try {
        return weatherClient.query(city);
    } catch (Exception e) {
        // ✅ 返回友好错误信息,LLM 可以据此做下一步决策
        return "天气服务暂时不可用,请稍后再试或用其他方式获取";
    }
}

坑 5:安全边界缺失

工具调用 = LLM 可以执行代码,安全是第一优先级

// ❌ 直接拼接用户输入,SQL 注入风险
@Tool(description = "查询用户")
public String queryUser(String name) {
    return db.query("SELECT * FROM users WHERE name = '" + name + "'");
}

// ✅ 参数化查询
@Tool(description = "查询用户")
public String queryUser(String name) {
    return db.query("SELECT * FROM users WHERE name = ?", name);
}

安全清单

检查项 措施
SQL 注入 参数化查询
命令注入 禁止直接执行 shell 命令
数据越权 工具内部加权限校验
费率控制 限制工具调用次数/频率
敏感数据 工具返回前脱敏

九、三大基石回顾

至此,Agent 三大基石全部讲完:

┌──────────────────────────────────────────────┐
│              AI Agent 三大基石                 │
│                                               │
│  ┌─────────┐  ┌──────────┐  ┌──────────┐    │
│  │ LLM调用  │  │ 记忆系统  │  │ 工具调用  │    │
│  │ +Prompt  │  │ 短期+长期 │  │ 注册+执行 │    │
│  └────┬────┘  └────┬─────┘  └────┬─────┘    │
│       │            │             │           │
│       ▼            ▼             ▼           │
│   理解&推理     上下文保持     外部世界交互     │
│   生成决策     身份&历史      查询&操作        │
│                                               │
│       ┌──────────────────────────┐            │
│       │     ReAct 循环引擎        │            │
│       │ Thought→Action→Observe   │            │
│       └──────────────────────────┘            │
└──────────────────────────────────────────────┘
基石 解决什么问题 类比
LLM + Prompt 理解意图、推理决策 大脑
记忆系统 保持上下文、积累经验 海马体
工具调用 和外部世界交互 手脚

十、下篇预告

三大基石搭完了,接下来进入框架实战阶段。第5篇我们将横评 Java 生态三大 Agent 框架:Spring AI、LangChain4j、Apache SeaTunnel(如有更新),帮你选型。


本文是「AI Agent 开发实战」系列第 4 篇,系列目录:

  1. AI Agent 核心概念与架构
  2. 三大基石之 LLM 调用与 Prompt 工程
  3. 三大基石之记忆系统
  4. 本文:三大基石之工具调用
Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐