Spring AI Tool Calling:让模型调用业务接口

0 阅读10分钟

摘要

大模型只能生成文本,无法天然知道订单状态、库存数量、用户权限或企业内部系统数据。要让 AI 应用完成真实业务任务,需要让模型提出结构化的工具调用请求,由服务端完成参数校验、权限判断和业务执行,再把工具结果交回模型生成最终回答。

Spring AI 提供了 Tool Calling 抽象,可以将 Java 方法、函数或 ToolCallback 暴露为模型可调用的工具。本文围绕“订单查询与售后客服”场景,介绍如何设计工具契约、注册工具、执行调用、处理多轮工具链,并重点讨论:

  • Tool Calling 与普通 Prompt 的区别;
  • @Tool、MethodToolCallback 和 ToolCallback 的使用方式;
  • 工具参数的结构化描述和校验;
  • 用户权限、租户隔离和高风险操作审批;
  • 模型连续调用多个工具时的状态管理;
  • 流式输出、超时、重试、幂等和审计;
  • 如何避免让模型直接获得数据库或 Shell 权限。

一、背景与问题

1. 仅靠 Prompt 无法访问业务系统

下面的 Prompt 只能让模型根据用户输入生成文字:

用户:查询订单 O1001 当前状态。
模型:我无法访问你的订单系统。

如果把订单数据直接写进 Prompt,又会出现数据过期、权限绕过和上下文泄露问题。

更合理的流程是:

用户问题
    ↓
模型判断需要查询订单
    ↓
模型生成结构化工具调用
    ↓
服务端校验并执行订单查询
    ↓
工具结果返回模型
    ↓
模型生成用户可读答案

2. Tool Calling 的基本结构

工具调用包含四个角色:

角色作用
工具名称标识要调用的业务能力
参数 Schema描述参数名称、类型和约束
服务端执行器校验权限并执行真实业务逻辑
工具结果将执行结果返回给模型或前端

模型只负责提出调用意图,不能直接绕过服务端执行器访问数据库。

3. 为什么不能把所有方法都暴露成工具

暴露工具意味着模型可能在满足条件时请求调用。以下能力需要特别谨慎:

  • 修改订单;
  • 关闭工单;
  • 发送邮件和短信;
  • 删除文件;
  • 修改权限;
  • 执行退款;
  • 访问生产数据库;
  • 执行 Shell 命令。

建议按风险分级:

只读查询
    ↓
草稿和预览
    ↓
用户确认
    ↓
执行写操作

二、核心概念

1. Function Calling 与 Tool Calling

Function Calling 侧重模型返回一个结构化函数名和参数;Tool Calling 是更宽泛的能力,工具可以是 Java 方法、HTTP API、数据库查询、搜索服务或人工审批流程。

应用层可以统一成:

public interface BusinessTool {

    String name();

    ToolResult execute(ToolContext context, JsonNode arguments);
}

2. Spring AI 的工具抽象

Spring AI 支持通过 @Tool 注解、ToolCallback、ToolCallbackProvider 或 Function 等方式提供工具。工具定义最终会转换为模型能够理解的名称、描述和参数 Schema。

业务服务通常使用:

ChatClient
   ↓
ToolCallback
   ↓
业务方法
   ↓
业务结果

3. 工具描述比方法名更重要

工具描述会参与模型决策。下面的描述过于模糊:

@Tool
public Object query(String id) {
    ...
}

更好的描述应说明:

  • 什么时候使用;
  • 参数代表什么;
  • 返回结果包含什么;
  • 不应该用于什么;
  • 是否只读;
  • 是否需要用户确认。

4. 工具调用不是权限模型

即使模型决定调用 queryOrder,服务端仍然要校验:

当前用户
    ↓
是否属于当前租户
    ↓
是否拥有订单访问权限
    ↓
订单是否属于当前用户
    ↓
是否允许当前 Agent 使用该工具
    ↓
执行查询

模型输出的参数、用户输入的订单号和客户端传入的租户 ID 都不能直接作为最终授权依据。

三、工作原理

1. 一次工具调用的完整流程

1. 服务端向模型发送工具定义
2. 模型返回 tool_call
3. 应用解析工具名称和 JSON 参数
4. 校验工具是否在白名单
5. 校验参数格式和业务权限
6. 执行 Java 业务方法
7. 保存工具调用记录
8. 将工具结果加入对话消息
9. 再次调用模型
10. 返回最终答案

2. 多工具调用

一个问题可能需要多个工具:

用户:我的订单什么时候到?如果超过承诺日期,能否申请补偿?
    ↓
查询订单
    ↓
查询物流
    ↓
查询售后政策
    ↓
模型整合结果

每个工具调用都应该有独立的 toolCallId、状态、参数摘要和结果摘要。

3. 工具调用状态

REQUESTED
    ↓
VALIDATING
    ├─ REJECTED
    └─ APPROVED
          ↓
       RUNNING
          ├─ COMPLETED
          ├─ FAILED
          ├─ TIMEOUT
          └─ CANCELLED

4. 工具结果不等于最终答案

工具结果可能是内部结构化数据:

{
  "orderId": "O1001",
  "status": "SHIPPED",
  "internalRiskScore": 0.81
}

最终返回给用户时,不应该自动暴露 internalRiskScore。工具执行结果需要经过输出策略或 DTO 转换。

四、实战示例

1. 定义只读订单工具

import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

@Component
public class OrderTools {

    private final OrderQueryService orderQueryService;

    public OrderTools(OrderQueryService orderQueryService) {
        this.orderQueryService = orderQueryService;
    }

    @Tool(description = """
            查询当前用户有权访问的订单状态。
            只用于订单查询,不执行修改、取消或退款。
            当用户没有提供明确订单号时,不要猜测订单号。
            """)
    public OrderSummary queryOrder(
            OrderToolContext context,
            String orderId) {
        return orderQueryService.queryOwnedOrder(
                context.tenantId(),
                context.userId(),
                orderId
        );
    }
}

OrderToolContext 不应该由模型填写,应该由服务端从认证上下文生成:

public record OrderToolContext(
        UUID tenantId,
        UUID userId,
        String requestId) {
}

2. 使用 ToolCallback

import org.springframework.ai.tool.ToolCallbacks;
import org.springframework.ai.tool.ToolCallback;

ToolCallback[] callbacks = ToolCallbacks.from(
        orderTools
);

String answer = chatClient.prompt()
        .user(question)
        .tools(callbacks)
        .call()
        .content();

不同 Spring AI 版本中工具 API 的包名和方法名可能变化,项目应以锁定版本的官方 Tool Calling 文档为准。

3. 使用 ChatClient 注册工具

@Service
public class CustomerAssistant {

    private final ChatClient chatClient;
    private final ToolCallback[] orderToolCallbacks;

    public CustomerAssistant(
            ChatClient.Builder builder,
            OrderTools orderTools) {
        this.chatClient = builder.build();
        this.orderToolCallbacks = ToolCallbacks.from(orderTools);
    }

    public String answer(String question) {
        return chatClient.prompt()
                .system("""
                        你是企业客服助手。
                        查询订单时只能使用工具返回的事实。
                        工具调用失败时说明暂时无法查询,
                        不要编造订单状态。
                        """)
                .user(question)
                .tools(orderToolCallbacks)
                .call()
                .content();
    }
}

4. 工具参数校验

模型返回的 JSON 仍然是不可信输入:

public record QueryOrderRequest(
        @NotBlank
        @Pattern(regexp = "^[A-Z0-9-]{4,32}$")
        String orderId) {
}

在执行前还需要:

if (!orderIdBelongsToUser(
        context.tenantId(),
        context.userId(),
        request.orderId())) {
    throw new AccessDeniedException("order is not accessible");
}

5. 高风险工具增加确认

退款工具不应该和查询工具一样自动执行:

public record ToolApproval(
        String approvalId,
        String userId,
        String toolName,
        String argumentsHash,
        Instant expiresAt) {
}

流程:

模型提出退款请求
    ↓
服务端校验订单和退款金额
    ↓
返回 approval_required
    ↓
用户确认具体订单和金额
    ↓
服务端校验 approvalId
    ↓
执行退款

确认必须绑定具体参数,不能只确认“允许退款”这一抽象动作。

6. 工具调用审计

CREATE TABLE ai_tool_call (
    id BIGSERIAL PRIMARY KEY,
    tenant_id BIGINT NOT NULL,
    conversation_id BIGINT NOT NULL,
    message_id BIGINT NOT NULL,
    tool_call_id VARCHAR(128) NOT NULL,
    tool_name VARCHAR(128) NOT NULL,
    arguments_json JSONB,
    arguments_hash VARCHAR(128),
    status VARCHAR(32) NOT NULL,
    result_summary TEXT,
    error_code VARCHAR(64),
    created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
    completed_at TIMESTAMPTZ
);

日志中不要保存完整密码、Token、银行卡号和未脱敏工具结果。

7. 工具调用超时和取消

return Mono.fromCallable(() -> tool.execute(context, arguments))
        .subscribeOn(Schedulers.boundedElastic())
        .timeout(Duration.ofSeconds(5))
        .onErrorMap(
                TimeoutException.class,
                error -> new ToolTimeoutException(tool.name())
        );

阻塞式数据库或 HTTP 客户端不能直接占用 WebFlux 事件线程。工具超时后要更新调用状态,并阻止迟到结果覆盖已经失败或取消的任务。

8. 限制工具调用次数

public record ToolPolicy(
        int maxCallsPerTurn,
        Set<String> allowedTools,
        boolean requireApprovalForWrites) {
}

调用策略应限制:

  • 单轮最大工具次数;
  • 单个工具最大重试次数;
  • 工具参数大小;
  • 工具结果大小;
  • 工具链最长深度;
  • 单次任务总耗时。

9. 流式 Tool Calling

流式模型可能先返回工具调用片段,再返回工具参数。不要在收到第一个片段时立即执行:

接收工具名称和参数片段
    ↓
持续拼接并验证 JSON
    ↓
确认参数完整
    ↓
执行权限检查
    ↓
执行工具
    ↓
发送 tool_call 状态事件

高风险工具还要暂停流,等待用户确认。

五、常见问题与实践建议

1. 模型为什么不调用工具

可能原因:

  • 工具描述不清楚;
  • 当前模型不支持工具调用;
  • 工具没有注册到本次请求;
  • 用户问题不需要工具;
  • 参数 Schema 不完整;
  • 模型被系统 Prompt 要求直接回答;
  • Provider 兼容层丢失了工具字段。

排查时记录工具列表、模型能力、请求模式和模型原始 tool call,但注意脱敏。

2. 模型调用了错误工具

可以通过:

  • 缩小每次请求的工具集合;
  • 改善工具名称和描述;
  • 将只读和写入工具分开;
  • 在系统 Prompt 中明确决策边界;
  • 增加服务端意图和权限校验;
  • 对高风险工具强制人工确认。

不要只依赖 Prompt 让模型“永远不要调用某工具”。

3. 工具结果太长

工具结果过长会消耗上下文。应返回摘要或分页结果:

{
  "items": [
    {
      "id": "O1001",
      "status": "SHIPPED"
    }
  ],
  "nextCursor": "..."
}

模型需要详细数据时,再通过下一次工具调用获取指定内容。

4. 工具失败后是否重试

查询类工具可以有限重试;写操作必须使用幂等键,避免重复执行:

tool_name + tenant_id + business_request_id

写入业务系统前,服务端先检查请求是否已经成功执行。

5. 工具调用和事务怎么配合

不要把长时间模型调用放在数据库事务中。推荐:

创建任务和消息
    ↓ 提交事务
执行工具和模型
    ↓
保存结果
    ↓
短事务更新状态

6. 是否可以让模型直接执行 SQL

不建议让模型直接连接生产数据库。更安全的方式是:

  • 暴露固定业务查询工具;
  • 使用只读数据库账号;
  • 限制表和字段;
  • 限制查询耗时和返回行数;
  • 做 SQL 审计;
  • 对高敏感字段脱敏。

六、进阶思考

1. 工具目录和动态注册

当工具数量增加后,可以设计工具目录:

Tool Catalog
  ├─ tool metadata
  ├─ required scopes
  ├─ risk level
  ├─ timeout
  ├─ rate limit
  └─ approval policy

Agent 根据当前用户和任务只获得一部分工具,避免把全量工具描述发送给模型。

2. Tool Calling 与 Agent 状态机

多步任务可以用状态机管理:

UNDERSTAND
    ↓
PLAN
    ↓
CALL_TOOL
    ↓
CHECK_RESULT
    ├─ NEED_MORE_TOOL
    ├─ NEED_APPROVAL
    └─ ANSWER

比起让模型无限循环调用工具,状态机更容易设置预算、超时和人工接管。

3. 工具结果可信度

工具返回的内容也可能来自外部系统或用户可编辑字段。模型不能把所有工具结果都当成系统规则:

业务工具结果 = 事实数据
用户备注和网页文本 = 不可信内容
系统授权和策略 = 服务端规则

4. 评估 Tool Calling

测试集应包括:

  • 应该调用哪个工具;
  • 参数是否正确;
  • 无权限时是否拒绝;
  • 工具失败时是否正确处理;
  • 需要确认的动作是否暂停;
  • 是否出现重复调用;
  • 是否在预算和次数限制内完成。

指标包括工具选择准确率、参数准确率、成功率、平均调用次数和越权拒绝率。

结论

Spring AI Tool Calling 的核心不是给模型增加几个 Java 方法,而是建立一条受控的业务执行链路:

  • 模型提出结构化调用意图;
  • 服务端验证参数和权限;
  • 业务工具执行真实操作;
  • 工具结果经过脱敏和摘要;
  • 模型根据结果生成最终回答;
  • 每次调用都可追踪、可取消、可审计。

查询工具可以先从只读、低风险场景开始;涉及修改、发送、删除和资金操作时,必须增加幂等、审批和人工确认。模型能力可以帮助系统理解用户意图,但最终业务权限必须由服务端掌握。

参考资料