Tool Calling 工具调用:让 Agent 查询数据库、调用接口和执行任务

前言

前一篇我们解决了一个关键问题:让模型稳定返回后端能解析的结构化结果。

但 Agent 到这里还只能"理解"。

例如用户说:

text 复制代码
帮我查订单 10001 的物流。

模型可以识别出:

json 复制代码
{
  "intent": "QUERY_LOGISTICS",
  "orderNo": "10001"
}

但模型本身并不知道订单 10001 的真实物流信息。

要获取真实数据,就需要 Tool Calling。

可以把 Tool Calling 理解为:

text 复制代码
模型负责决定调用什么工具
后端负责安全地执行工具
工具结果再交给模型组织成最终回答

这一篇我们会学习:

  1. Tool Calling 是什么
  2. 工具和普通接口有什么区别
  3. 如何设计工具名称、参数和返回值
  4. 为什么模型不能直接操作数据库
  5. Java 中如何封装工具
  6. 如何做权限校验、参数校验和审计日志
  7. 如何避免危险工具调用

一、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 个工具。

这通常不是好做法。

工具过多会导致:

  1. 模型更难选择正确工具
  2. 工具描述占用更多 Token
  3. 权限管理更复杂
  4. 调试成本更高
  5. 工具误调用概率增加
  6. 高风险操作更难控制

更推荐从少量、高价值、职责清晰的工具开始。

例如订单助手第一版只提供:

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
  }
}

文字说明

工具定义通常包含:

  1. 工具名称
  2. 工具描述
  3. 参数 Schema
  4. 必填字段
  5. 字段说明
  6. 是否允许额外参数

模型会根据工具描述判断是否调用。

所以描述要准确。

例如:

text 复制代码
仅当用户提供明确订单号时调用。

可以减少模型在订单号缺失时盲目调用工具。


六、为什么模型不能直接连接数据库

有些人会想:

text 复制代码
既然模型能生成 SQL,那让模型直接查数据库不就行了?

不建议这样做。

原因包括:

  1. 模型可能生成错误 SQL
  2. 模型可能生成危险 SQL
  3. 模型可能查询超出权限的数据
  4. 模型可能返回敏感字段
  5. 难以做稳定审计
  6. SQL 结构变化后容易失效
  7. 容易受到提示词注入影响

正确方式应该是:

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": "订单查询暂时失败,请稍后重试。"
}

文字说明

这样做有几个好处:

  1. 不暴露数据库和系统实现
  2. 模型更容易理解错误类型
  3. 用户看到的提示更友好
  4. 后端日志仍然可以记录完整异常
  5. 方便 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. 成本失控
  2. 响应时间过长
  3. 工具被重复调用
  4. 外部系统被大量请求
  5. 复杂异常导致死循环

十七、常见问题

1. 模型调用了不存在的工具

后端不能猜测模型想调用什么。

应该返回受控错误:

json 复制代码
{
  "success": false,
  "errorCode": "TOOL_NOT_FOUND",
  "message": "当前不支持该工具调用。"
}

同时记录日志,用于优化工具描述和 Prompt。


2. 工具参数不合法怎么办

例如模型返回:

json 复制代码
{
  "orderNo": "查询全部用户订单"
}

应该通过 DTO 校验拒绝。

不能因为模型"看起来是在完成任务",就绕过参数校验。


3. 工具调用成功,但模型总结错了怎么办

工具结果是真实的,但模型可能仍然理解错。

例如工具返回:

text 复制代码
status = WAITING_SHIPMENT

模型却说:

text 复制代码
订单已经发货。

这种问题需要:

  1. 清晰的工具返回字段
  2. Prompt 约束"只能基于工具结果回答"
  3. 关键状态使用模板化后端文案
  4. 记录结果用于评估
  5. 高风险业务避免让模型自由解释

4. 能不能让模型执行 Shell 命令

默认不建议。

如果确实需要操作服务器,也应该:

text 复制代码
限定命令白名单
限制执行目录
限制参数范围
隔离执行环境
记录审计日志
增加人工确认
禁止 root 权限

不要给模型任意 Shell 权限。


十八、实际开发建议

  1. 工具要少而精,优先暴露高价值、职责单一的能力。

  2. 模型只负责选择工具,真正执行必须经过后端校验。

  3. 工具内部优先调用 Service,不要让模型或工具直接操作数据库。

  4. 工具参数必须使用 DTO、Validation 和业务校验。

  5. 查询类工具与修改类工具要区分风险等级。

  6. 高风险操作必须增加用户确认和审计日志。

  7. 工具结果要脱敏、限长、结构化,不要直接返回底层异常和全量数据。

  8. 为 Agent 设置工具调用次数、超时和成本限制。


十九、总结

这一篇我们完成了 Agent 从"理解用户问题"到"调用真实业务能力"的关键一步。

Tool Calling 的本质是:

text 复制代码
模型负责决策
工具负责执行
后端负责安全和业务规则

整个流程可以总结为:

text 复制代码
用户输入
    |
模型选择工具
    |
后端校验工具和参数
    |
Service 执行业务
    |
Mapper 查询或修改数据
    |
工具结果返回模型
    |
模型生成最终回复

学完这一篇后,Agent 已经不只是聊天,而是可以在受控范围内查询订单、调用接口、创建任务和连接业务系统。

下一篇我们继续学习 ReAct 模式,理解 Agent 如何在"思考、行动、观察"之间完成多步骤任务。

相关推荐
武子康1 小时前
Pi Agent Loop 源码解析:Context、Streaming、Tool Calling、Steering 与停止条件
人工智能·llm·agent
小当家.1051 小时前
MCP 协议深度解析:AI 领域的 USB-C 接口
开发语言·人工智能·agent·tool·mcp
Yan_chen6661 小时前
CTFHub SQL 布尔盲注实战攻略
数据库·sql·实战·布尔盲注·ctfhub闯关
不一样的少年_1 小时前
不用LangChain:用 200 行代码手搓 Agent 对话记忆与上下文压缩
人工智能·agent·ai编程
粥里有勺糖1 小时前
Harness 学习笔记分享(Part 1)
面试·github·agent
狂师1 小时前
推荐一款开源 Skill:让 AI Agent 给你做一份"能改"的 PPT,支持 上千套模板!
人工智能·agent
杨_晨2 小时前
LLM输出康熙部首冲突
数据库·python·mysql·ai
爱看报的猿2 小时前
【金仓数据库征文】MySQL至金仓KES异构数据库平滑迁移与性能深度调优实战
数据库·数据仓库·mysql·金仓数据库征文