前言
前一篇我们解决了一个关键问题:让模型稳定返回后端能解析的结构化结果。
但 Agent 到这里还只能"理解"。
例如用户说:
text
帮我查订单 10001 的物流。
模型可以识别出:
json
{
"intent": "QUERY_LOGISTICS",
"orderNo": "10001"
}
但模型本身并不知道订单 10001 的真实物流信息。
要获取真实数据,就需要 Tool Calling。
可以把 Tool Calling 理解为:
text
模型负责决定调用什么工具
后端负责安全地执行工具
工具结果再交给模型组织成最终回答
这一篇我们会学习:
- Tool Calling 是什么
- 工具和普通接口有什么区别
- 如何设计工具名称、参数和返回值
- 为什么模型不能直接操作数据库
- Java 中如何封装工具
- 如何做权限校验、参数校验和审计日志
- 如何避免危险工具调用
一、Tool Calling 是什么
Tool Calling 又常被称为:
text
Function Calling
工具调用
函数调用
它的核心流程是:
text
用户提出问题
|
模型理解意图
|
模型选择工具并生成参数
|
后端校验工具和参数
|
后端执行工具
|
工具返回真实结果
|
模型根据结果生成最终回答
例如:
text
用户:查订单 10001 的物流。
模型不会直接回答物流状态,而是请求:
json
{
"name": "queryOrderLogistics",
"arguments": {
"orderNo": "10001"
}
}
后端调用订单服务,拿到真实数据:
json
{
"orderNo": "10001",
"status": "IN_TRANSIT",
"latestMessage": "包裹已到达分拨中心"
}
模型再回答:
text
订单 10001 当前正在运输中,最新物流状态是:包裹已到达分拨中心。
二、Tool Calling 和普通 API 调用的区别
普通 API 调用通常由前端或后端代码明确决定。
text
前端调用 /order/10001/logistics
而 Tool Calling 中,调用哪个工具由模型在允许范围内决定。
text
用户自然语言
|
模型判断
|
选择 queryOrderLogistics
|
后端执行工具
但这里必须强调:
text
模型有调用建议权
后端拥有最终执行权
模型不能绕过:
text
权限校验
参数校验
业务规则
审计日志
风控规则
三、工具不是"把所有接口暴露给模型"
一个常见误区是:
text
项目有 200 个接口,就给 Agent 暴露 200 个工具。
这通常不是好做法。
工具过多会导致:
- 模型更难选择正确工具
- 工具描述占用更多 Token
- 权限管理更复杂
- 调试成本更高
- 工具误调用概率增加
- 高风险操作更难控制
更推荐从少量、高价值、职责清晰的工具开始。
例如订单助手第一版只提供:
text
getCurrentUser
queryOrderDetail
queryOrderLogistics
queryRefundStatus
createAfterSaleTicket
不要一开始就开放:
text
deleteOrder
updateOrderStatus
executeSql
runShellCommand
四、一个好工具应该具备什么特点
一个好工具通常具备下面几个特征。
1. 名称明确
不推荐:
text
doOrder
handleData
queryInfo
推荐:
text
queryOrderDetail
queryOrderLogistics
createAfterSaleTicket
工具名称应该让模型和开发者都能看懂。
2. 职责单一
不推荐:
text
manageOrder
因为它可能包含查询、修改、删除、退款等太多行为。
推荐拆开:
text
queryOrderDetail
queryOrderLogistics
applyRefund
cancelOrder
这样权限控制和审计更清晰。
3. 参数清晰
不推荐:
json
{
"data": "10001"
}
推荐:
json
{
"orderNo": "10001"
}
参数名称要表达业务含义。
4. 返回结果稳定
不推荐直接返回数据库所有字段。
推荐返回 Agent 真正需要的信息:
json
{
"orderNo": "10001",
"orderStatus": "PAID",
"deliveryStatus": "WAITING_SHIPMENT",
"latestLogisticsMessage": null
}
返回字段越稳定,模型越容易正确理解。
5. 具备安全边界
工具必须校验:
text
当前用户是谁
能否访问目标数据
参数是否合法
当前操作是否允许
是否需要二次确认
五、工具定义示例
下面是 queryOrderLogistics 的工具说明。
json
{
"name": "queryOrderLogistics",
"description": "查询当前登录用户指定订单的物流信息。仅当用户提供明确订单号时调用。",
"parameters": {
"type": "object",
"required": [
"orderNo"
],
"properties": {
"orderNo": {
"type": "string",
"description": "订单号,只允许数字和字母,长度不超过 32 位。"
}
},
"additionalProperties": false
}
}
文字说明
工具定义通常包含:
- 工具名称
- 工具描述
- 参数 Schema
- 必填字段
- 字段说明
- 是否允许额外参数
模型会根据工具描述判断是否调用。
所以描述要准确。
例如:
text
仅当用户提供明确订单号时调用。
可以减少模型在订单号缺失时盲目调用工具。
六、为什么模型不能直接连接数据库
有些人会想:
text
既然模型能生成 SQL,那让模型直接查数据库不就行了?
不建议这样做。
原因包括:
- 模型可能生成错误 SQL
- 模型可能生成危险 SQL
- 模型可能查询超出权限的数据
- 模型可能返回敏感字段
- 难以做稳定审计
- SQL 结构变化后容易失效
- 容易受到提示词注入影响
正确方式应该是:
text
模型 -> 受控工具 -> Service -> Mapper -> 数据库
而不是:
text
模型 -> 任意 SQL -> 数据库
七、Java 中定义工具参数对象
先定义工具参数。
java
package com.example.agent.tool.order;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
public record QueryOrderLogisticsArgs(
@NotBlank(message = "订单号不能为空")
@Pattern(
regexp = "^[A-Za-z0-9]{1,32}$",
message = "订单号格式不正确"
)
String orderNo
) {
}
文字说明
工具参数和普通 Controller DTO 一样,也应该做参数校验。
模型生成的参数不是可信参数。
例如模型可能返回:
text
orderNo = "查询全部订单"
或者:
text
orderNo = "10001; delete from orders"
即使模型不会真的执行 SQL,参数校验仍然是必须的。
八、定义统一工具接口
可以给 Agent 工具定义一个统一接口。
java
package com.example.agent.tool;
public interface AgentTool<A, R> {
String name();
R execute(ToolContext context, A arguments);
}
再定义工具调用上下文。
java
package com.example.agent.tool;
public record ToolContext(
Long userId,
String username,
String traceId
) {
}
文字说明
ToolContext 用于保存当前请求上下文。
例如:
text
当前登录用户 ID
用户名
traceId
租户 ID
用户角色
工具执行时不能依赖模型传入"当前用户是谁"。
当前用户必须来自 JWT、拦截器或 ThreadLocal。
九、实现查询订单物流工具
下面以订单物流工具为例。
java
package com.example.agent.tool.order;
import com.example.agent.tool.AgentTool;
import com.example.agent.tool.ToolContext;
import com.example.entity.Order;
import com.example.exception.BusinessException;
import com.example.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
@RequiredArgsConstructor
public class QueryOrderLogisticsTool
implements AgentTool<QueryOrderLogisticsArgs, OrderLogisticsResult> {
private final OrderService orderService;
@Override
public String name() {
return "queryOrderLogistics";
}
@Override
public OrderLogisticsResult execute(
ToolContext context,
QueryOrderLogisticsArgs arguments
) {
Order order = orderService.queryUserOrder(
context.userId(),
arguments.orderNo()
);
if (order == null) {
throw new BusinessException(404, "未查询到当前用户的订单");
}
return new OrderLogisticsResult(
order.getOrderNo(),
order.getDeliveryStatus(),
order.getLogisticsCompany(),
order.getLatestLogisticsMessage()
);
}
}
返回对象:
java
package com.example.agent.tool.order;
public record OrderLogisticsResult(
String orderNo,
String deliveryStatus,
String logisticsCompany,
String latestLogisticsMessage
) {
}
文字说明
这个工具没有直接调用 Mapper,而是调用:
java
OrderService
原因是订单归属、订单状态、权限校验等业务规则应该放在 Service 层。
工具本身只是 Agent 和业务服务之间的一层适配。
十、Service 层继续负责业务和权限
java
package com.example.service;
import com.example.entity.Order;
public interface OrderService {
Order queryUserOrder(Long userId, String orderNo);
}
java
package com.example.service.impl;
import com.example.entity.Order;
import com.example.mapper.OrderMapper;
import com.example.service.OrderService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {
private final OrderMapper orderMapper;
@Override
public Order queryUserOrder(Long userId, String orderNo) {
return orderMapper.selectByOrderNoAndUserId(orderNo, userId);
}
}
Mapper 接口:
java
package com.example.mapper;
import com.example.entity.Order;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface OrderMapper {
Order selectByOrderNoAndUserId(
@Param("orderNo") String orderNo,
@Param("userId") Long userId
);
}
Mapper XML:
xml
<select id="selectByOrderNoAndUserId" resultType="com.example.entity.Order">
select
order_no,
delivery_status,
logistics_company,
latest_logistics_message
from orders
where order_no = #{orderNo}
and user_id = #{userId}
</select>
文字说明
这里的关键是:
sql
and user_id = #{userId}
即使模型生成了一个真实存在的订单号,也只能查询当前登录用户自己的订单。
这才是 Agent 工具正确的安全边界。
十一、工具注册中心
当 Agent 有多个工具时,可以通过注册中心统一管理。
java
package com.example.agent.tool;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
public class ToolRegistry {
private final Map<String, AgentTool<?, ?>> toolMap;
public ToolRegistry(List<AgentTool<?, ?>> tools) {
this.toolMap = tools.stream()
.collect(Collectors.toMap(
AgentTool::name,
Function.identity()
));
}
public AgentTool<?, ?> getTool(String toolName) {
return toolMap.get(toolName);
}
}
文字说明
Spring 会自动注入所有实现了:
java
AgentTool
接口的工具。
例如:
text
QueryOrderLogisticsTool
QueryOrderDetailTool
CreateAfterSaleTicketTool
注册中心再通过工具名称找到对应实现。
这个设计比在代码中写大量:
java
if ("queryOrderLogistics".equals(toolName)) {
...
}
更清晰,也方便后续扩展。
十二、工具执行器需要做什么
工具执行器不应该拿到模型请求后直接执行。
建议至少做这些检查:
text
1. 工具是否存在
2. 工具是否属于当前 Agent 的允许列表
3. 工具参数是否可以解析
4. 参数是否通过 Validation
5. 当前用户是否有权限
6. 是否超过调用次数限制
7. 是否属于高风险操作
8. 是否需要人工确认
9. 是否记录审计日志
可以把执行流程理解成:
text
模型请求工具
|
检查工具白名单
|
解析参数
|
参数校验
|
权限校验
|
执行业务服务
|
脱敏工具结果
|
记录日志
|
返回模型
十三、工具错误结果不要直接抛给模型
工具失败时,不建议把底层异常直接给模型。
例如数据库异常:
text
SQLSyntaxErrorException: Unknown column ...
不应该作为模型上下文返回。
可以转换成受控结果:
json
{
"success": false,
"errorCode": "ORDER_QUERY_FAILED",
"message": "订单查询暂时失败,请稍后重试。"
}
文字说明
这样做有几个好处:
- 不暴露数据库和系统实现
- 模型更容易理解错误类型
- 用户看到的提示更友好
- 后端日志仍然可以记录完整异常
- 方便 Agent 决定是否重试或降级
十四、高风险工具必须增加确认
下面这些操作属于高风险工具:
text
删除数据
取消订单
申请退款
创建支付
发送外部消息
修改权限
发布内容
执行部署命令
不建议模型一次决定后直接执行。
更推荐的流程:
text
用户提出请求
|
模型生成操作草稿
|
后端展示确认信息
|
用户明确确认
|
后端再次校验权限和状态
|
执行工具
|
记录审计日志
例如用户说:
text
帮我取消订单 10001。
Agent 应该先回答:
text
订单 10001 当前状态为已付款,取消后将发起退款流程。是否确认取消?
用户确认后,才允许执行取消工具。
十五、工具结果也要控制长度
工具返回数据不要过大。
例如查询订单列表时,不应该把 500 条订单全部交给模型。
更推荐:
text
工具层分页
只返回必要字段
限制最大记录数
摘要化返回
敏感字段脱敏
例如:
json
{
"total": 128,
"items": [
{
"orderNo": "10001",
"status": "PAID",
"amount": 99.00
}
]
}
如果用户需要更多内容,Agent 可以继续追问或分页查询。
十六、工具调用需要限制次数
Agent 可能出现这种情况:
text
调用工具
|
结果不满意
|
再次调用相同工具
|
继续调用
|
进入循环
所以需要限制:
text
单次请求最大工具调用次数
单个工具最大调用次数
单个工具超时时间
总请求超时时间
最大 Token 消耗
例如:
text
单次 Agent 请求最多调用 5 次工具
同一个工具最多调用 2 次
单个工具超时 5 秒
文字说明
这些限制不是为了让 Agent "变笨",而是防止:
- 成本失控
- 响应时间过长
- 工具被重复调用
- 外部系统被大量请求
- 复杂异常导致死循环
十七、常见问题
1. 模型调用了不存在的工具
后端不能猜测模型想调用什么。
应该返回受控错误:
json
{
"success": false,
"errorCode": "TOOL_NOT_FOUND",
"message": "当前不支持该工具调用。"
}
同时记录日志,用于优化工具描述和 Prompt。
2. 工具参数不合法怎么办
例如模型返回:
json
{
"orderNo": "查询全部用户订单"
}
应该通过 DTO 校验拒绝。
不能因为模型"看起来是在完成任务",就绕过参数校验。
3. 工具调用成功,但模型总结错了怎么办
工具结果是真实的,但模型可能仍然理解错。
例如工具返回:
text
status = WAITING_SHIPMENT
模型却说:
text
订单已经发货。
这种问题需要:
- 清晰的工具返回字段
- Prompt 约束"只能基于工具结果回答"
- 关键状态使用模板化后端文案
- 记录结果用于评估
- 高风险业务避免让模型自由解释
4. 能不能让模型执行 Shell 命令
默认不建议。
如果确实需要操作服务器,也应该:
text
限定命令白名单
限制执行目录
限制参数范围
隔离执行环境
记录审计日志
增加人工确认
禁止 root 权限
不要给模型任意 Shell 权限。
十八、实际开发建议
-
工具要少而精,优先暴露高价值、职责单一的能力。
-
模型只负责选择工具,真正执行必须经过后端校验。
-
工具内部优先调用 Service,不要让模型或工具直接操作数据库。
-
工具参数必须使用 DTO、Validation 和业务校验。
-
查询类工具与修改类工具要区分风险等级。
-
高风险操作必须增加用户确认和审计日志。
-
工具结果要脱敏、限长、结构化,不要直接返回底层异常和全量数据。
-
为 Agent 设置工具调用次数、超时和成本限制。
十九、总结
这一篇我们完成了 Agent 从"理解用户问题"到"调用真实业务能力"的关键一步。
Tool Calling 的本质是:
text
模型负责决策
工具负责执行
后端负责安全和业务规则
整个流程可以总结为:
text
用户输入
|
模型选择工具
|
后端校验工具和参数
|
Service 执行业务
|
Mapper 查询或修改数据
|
工具结果返回模型
|
模型生成最终回复
学完这一篇后,Agent 已经不只是聊天,而是可以在受控范围内查询订单、调用接口、创建任务和连接业务系统。
下一篇我们继续学习 ReAct 模式,理解 Agent 如何在"思考、行动、观察"之间完成多步骤任务。