AI Agent 开发实战(四):三大基石之工具调用 —— 让 LLM 长出手脚
AI Agent 开发实战(四):三大基石之工具调用 —— 让 LLM 长出手脚
前三篇我们聊了 Agent 的概念架构、LLM 调用与 Prompt 工程、记忆系统。今天到了三大基石的最后一环:工具调用(Tool Calling)。没有工具的 Agent 只是聊天机器人,有了工具的 Agent 才能真正"做事"。
一、为什么 Agent 需要工具?
LLM 本质上是一个文本生成模型,它:
- 不知道今天的天气
- 查不了数据库
- 调不了 API
- 算不准复杂算术
它有的只是训练数据中的"知识记忆"。但真实业务场景中,Agent 需要和外部世界交互——查订单、发通知、调接口、跑脚本。
工具调用就是给 LLM 长出"手脚"的机制。
┌─────────┐ 结构化指令 ┌──────────┐
│ LLM │ ──────────────────▶ │ 工具执行器 │
│ (大脑) │ ◀────────────────── │ (手脚) │
└─────────┘ 执行结果 └──────────┘
│ │
│ 可能多轮:LLM 判断 │ 实际调用
│ "还需要再查一个" │ HTTP / DB / Shell
▼ ▼
最终回复给用户 外部系统
二、工具调用的本质:一次"LLM → 执行 → 回传"的闭环
工具调用的核心流程其实很简单,就三步:
- LLM 输出结构化指令:告诉框架"我要调用哪个工具,参数是什么"
- 框架执行工具:实际调用对应的函数/API
- 结果回传 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 自动完成:
- 将
getWeather工具定义发给 LLM - LLM 返回
tool_calls: [{name: "get_weather", arguments: {city: "北京"}}] - 框架调用
weatherTools.getWeather("北京", "celsius") - 结果回传 LLM
- 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 篇,系列目录:
- AI Agent 核心概念与架构
- 三大基石之 LLM 调用与 Prompt 工程
- 三大基石之记忆系统
- 本文:三大基石之工具调用
更多推荐


所有评论(0)