Spring AI企业级应用实战(5):Tool Calling、参数校验、幂等、审批与审计

文章摘要

前四篇已经完成DeepSeek接入、ChatClient统一调用层、SSE流式输出和Chat Memory持久化。本篇进入企业Agent最关键的执行能力:Tool Calling。Spring AI 2.0把工具执行循环统一提升到ToolCallingAdvisor,并支持@ToolToolCallback、Tool Context、流式调用和MCP工具。但框架能够执行工具,并不代表业务可以安全执行。本文从零实现查询库存和取消订单两个工具,增加动态工具白名单、参数校验、Tool Context、多租户权限、风险分级、用户确认、数据库幂等、结果脱敏和审计,形成一套生产级工具调用骨架。

一、为什么Tool Calling是Agent分水岭

普通聊天系统:

用户问题
→ 大模型生成文本
→ 返回答案

Agent系统:

用户目标
→ 模型选择工具
→ 应用执行真实动作
→ 工具结果返回模型
→ 模型继续判断
→ 最终回答

一旦工具可以:

  • 查询数据库;
  • 修改订单;
  • 发邮件;
  • 创建工单;
  • 删除文件;
  • 修改权限;
  • 调用MCP Server;

模型输出就会影响真实世界。

因此生产系统必须坚持:

模型只能提出工具调用请求,应用程序才拥有执行权。

二、本篇目标

实现两个工具:

query_inventory
→ 查询库存,低风险,只读

cancel_order
→ 取消订单,高风险,有副作用

完整链路:

认证用户
→ 选择允许工具
→ ChatClient发送工具定义
→ 模型生成Tool Call
→ Spring AI参数解析
→ Tool Context注入身份
→ 业务策略校验
→ 用户确认
→ 幂等执行
→ 审计
→ 返回模型
→ 生成最终回答

三、项目结构

spring-ai-enterprise
└── src/main/java/com/zyentor/ai
    ├── config
    │   └── ToolCallingConfig.java
    ├── tool
    │   ├── EnterpriseTools.java
    │   ├── ToolRiskLevel.java
    │   ├── ToolPolicyService.java
    │   ├── ToolPermissionService.java
    │   ├── ToolConfirmationService.java
    │   ├── ToolIdempotencyService.java
    │   ├── ToolAuditService.java
    │   ├── ToolResult.java
    │   └── ToolContextKeys.java
    ├── inventory
    │   ├── QueryInventoryRequest.java
    │   ├── InventoryResult.java
    │   └── InventoryService.java
    ├── order
    │   ├── CancelOrderRequest.java
    │   ├── CancelOrderResult.java
    │   └── OrderService.java
    ├── service
    │   └── EnterpriseAgentService.java
    └── web
        └── AgentController.java

四、依赖配置

            org.springframework.ai
            spring-ai-bom
            2.0.0
            pom
            import






        org.springframework.ai
        spring-ai-starter-model-deepseek



        org.springframework.boot
        spring-boot-starter-webflux



        org.springframework.boot
        spring-boot-starter-validation



        org.springframework.boot
        spring-boot-starter-jdbc



        org.postgresql
        postgresql
        runtime

五、Spring AI 2.0工具循环发生了什么变化

Spring AI 1.x时代,部分ChatModel内部维护自己的工具执行循环。

2.0统一为:

ChatClient
→ Advisor Chain
→ ToolCallingAdvisor
→ ToolCallingManager
→ ToolCallback

默认情况下,ChatClient自动注册ToolCallingAdvisor

完整循环:

1. 工具定义发送给模型
2. 模型返回工具名称和参数
3. ToolCallingAdvisor定位ToolCallback
4. 应用执行工具
5. 工具结果加入对话
6. 再次调用模型
7. 模型生成最终回答或继续调用工具

同步和流式模式都可以使用该机制。

六、定义统一ToolResult

不要让每个工具随意返回字符串。

package com.zyentor.ai.tool;

public record ToolResult(
        boolean success,
        String code,
        String message,
        boolean terminal,
        boolean retryable,
        T data
) {
    public static  ToolResult success(
            T data
    ) {
        return new ToolResult(
                true,
                "OK",
                "执行成功",
                true,
                false,
                data
        );
    }

    public static  ToolResult failure(
            String code,
            String message,
            boolean retryable
    ) {
        return new ToolResult(
                false,
                code,
                message,
                true,
                retryable,
                null
        );
    }

    public static  ToolResult confirmation(
            T confirmationData
    ) {
        return new ToolResult(
                false,
                "CONFIRMATION_REQUIRED",
                "需要用户确认",
                true,
                false,
                confirmationData
        );
    }
}

字段含义:

success
→ 业务是否成功

code
→ 稳定错误码

terminal
→ 模型是否应停止重复调用

retryable
→ 是否允许重试

data
→ 结构化结果

七、定义工具上下文键

package com.zyentor.ai.tool;

public final class ToolContextKeys {

    public static final String TENANT_ID =
            "tenantId";

    public static final String USER_ID =
            "userId";

    public static final String CONVERSATION_ID =
            "conversationId";

    public static final String REQUEST_ID =
            "requestId";

    public static final String PERMISSIONS =
            "permissions";

    public static final String CONFIRMATION_TOKEN =
            "confirmationToken";

    private ToolContextKeys() {
    }
}

Tool Context中的数据不会作为工具Schema发送给模型。

这非常适合传递:

  • tenantId;
  • userId;
  • 权限;
  • Trace ID;
  • 确认凭证;
  • 服务身份。

不要让模型自己生成这些安全上下文。

八、定义库存查询参数

package com.zyentor.ai.inventory;

import org.springframework.ai.tool.annotation.ToolParam;

public record QueryInventoryRequest(
        @ToolParam(
            description = "商品SKU,例如SKU-10001"
        )
        String sku,

        @ToolParam(
            description = "仓库编码,例如WH-HZ-01。未知时可以为空",
            required = false
        )
        String warehouseCode
) {
}

返回:

public record InventoryResult(
        String sku,
        String warehouseCode,
        long availableQuantity,
        long lockedQuantity,
        Instant updatedAt
) {
}

九、定义取消订单参数

package com.zyentor.ai.order;

import org.springframework.ai.tool.annotation.ToolParam;

public record CancelOrderRequest(
        @ToolParam(
            description = "完整订单号,例如A202607290001"
        )
        String orderId,

        @ToolParam(
            description = "取消原因,不得为空"
        )
        String reason
) {
}

返回:

public record CancelOrderResult(
        String orderId,
        String previousStatus,
        String currentStatus,
        String cancellationId,
        Instant cancelledAt
) {
}

十、先做确定性业务参数校验

模型根据JSON Schema生成参数,但业务仍要重新校验。

@Component
public class ToolBusinessValidator {

    private static final Pattern SKU_PATTERN =
            Pattern.compile("SKU-[0-9]{5}");

    private static final Pattern ORDER_PATTERN =
            Pattern.compile("A[0-9]{12}");

    public void validate(
            QueryInventoryRequest request
    ) {
        if (
            request.sku() == null
            || !SKU_PATTERN.matcher(
                    request.sku()
            ).matches()
        ) {
            throw new IllegalArgumentException(
                    "SKU格式错误"
            );
        }
    }

    public void validate(
            CancelOrderRequest request
    ) {
        if (
            request.orderId() == null
            || !ORDER_PATTERN.matcher(
                    request.orderId()
            ).matches()
        ) {
            throw new IllegalArgumentException(
                    "订单号格式错误"
            );
        }

        if (
            request.reason() == null
            || request.reason().isBlank()
        ) {
            throw new IllegalArgumentException(
                    "取消原因不能为空"
            );
        }
    }
}

JSON Schema验证负责结构,业务Validator负责领域规则。

十一、定义风险等级

public enum ToolRiskLevel {
    LOW,
    MEDIUM,
    HIGH,
    CRITICAL
}

本篇:

query_inventory
→ MEDIUM

cancel_order
→ HIGH

库存虽然只读,但属于企业内部数据,仍需权限校验。

十二、权限服务

@Service
public class ToolPermissionService {

    public void requirePermission(
            ToolContext context,
            String permission
    ) {
        Object value = context.getContext()
                .get(ToolContextKeys.PERMISSIONS);

        Set permissions = toPermissionSet(value);

        if (!permissions.contains(permission)) {
            throw new AccessDeniedException(
                    "缺少权限:" + permission
            );
        }
    }

    private Set toPermissionSet(
            Object value
    ) {
        if (value instanceof Set rawSet) {
            return rawSet.stream()
                    .map(Object::toString)
                    .collect(Collectors.toUnmodifiableSet());
        }

        return Set.of();
    }
}

工具方法不能从模型参数读取权限。

十三、用户确认不能只靠一句Prompt

错误方案:

System Prompt:取消订单前请询问用户是否确认。

模型可能:

  • 忘记询问;
  • 错误理解“好的”;
  • 在上下文中找到旧确认;
  • 确认的是A订单,执行B订单;
  • 重放旧工具调用。

推荐确认Token绑定:

用户
工具
参数Hash
会话
有效期

十四、确认记录

CREATE TABLE ai_tool_confirmation (
    confirmation_token VARCHAR(128) PRIMARY KEY,
    tenant_id VARCHAR(64) NOT NULL,
    user_id VARCHAR(64) NOT NULL,
    conversation_id VARCHAR(128) NOT NULL,
    tool_name VARCHAR(100) NOT NULL,
    arguments_hash VARCHAR(64) NOT NULL,
    status VARCHAR(30) NOT NULL,
    expires_at TIMESTAMP NOT NULL,
    confirmed_at TIMESTAMP
);

状态:

PENDING
CONFIRMED
CONSUMED
EXPIRED
CANCELLED

确认只能消费一次。

十五、确认服务

@Service
public class ToolConfirmationService {

    public ToolResult
    requireConfirmation(
            ToolContext context,
            String toolName,
            Object arguments,
            String humanReadableSummary
    ) {
        String token = readConfirmationToken(context);
        String argumentsHash = sha256(arguments);

        if (token == null) {
            ConfirmationPayload payload = createPending(
                    context,
                    toolName,
                    argumentsHash,
                    humanReadableSummary
            );

            return ToolResult.confirmation(payload);
        }

        assertConfirmed(
                token,
                context,
                toolName,
                argumentsHash
        );

        consume(token);

        return null;
    }
}

这里返回null仅作为服务内部“无需再确认”的信号,不是工具最终返回值。工具方法仍然不能向模型返回null

十六、幂等表

CREATE TABLE ai_tool_idempotency (
    tenant_id VARCHAR(64) NOT NULL,
    idempotency_key VARCHAR(200) NOT NULL,
    tool_name VARCHAR(100) NOT NULL,
    arguments_hash VARCHAR(64) NOT NULL,
    status VARCHAR(30) NOT NULL,
    business_result_id VARCHAR(128),
    result_json TEXT,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL,
    PRIMARY KEY (tenant_id, idempotency_key)
);

状态:

PROCESSING
SUCCEEDED
FAILED

十七、幂等键怎么生成

对于取消订单:

tenantId
+conversationId
+toolName
+orderId
public String cancelOrderKey(
        String tenantId,
        String conversationId,
        String orderId
) {
    return sha256(String.join(
            ":",
            tenantId,
            conversationId,
            "cancel_order",
            orderId
    ));
}

如果同一订单允许多次不同业务动作,应该加入业务请求ID,而不是只用订单号。

十八、幂等执行服务

@Service
public class ToolIdempotencyService {

    private final JdbcClient jdbcClient;
    private final ObjectMapper objectMapper;

    @Transactional
    public  ToolResult executeOnce(
            String tenantId,
            String idempotencyKey,
            String toolName,
            Object arguments,
            Supplier action
    ) {
        String argumentsHash = sha256(arguments);

        Optional> stored = find(
                tenantId,
                idempotencyKey
        );

        if (stored.isPresent()) {
            return handleStored(
                    stored.get(),
                    argumentsHash
            );
        }

        insertProcessing(
                tenantId,
                idempotencyKey,
                toolName,
                argumentsHash
        );

        try {
            T result = action.get();

            markSucceeded(
                    tenantId,
                    idempotencyKey,
                    result
            );

            return ToolResult.success(result);
        }
        catch (RuntimeException exception) {
            markFailed(
                    tenantId,
                    idempotencyKey,
                    exception
            );

            throw exception;
        }
    }
}

关键业务还要考虑:

业务事务已提交
但幂等结果未写入

可以使用:

  • 同一数据库事务;
  • Outbox;
  • 业务系统原生幂等键;

解决一致性问题。

十九、审计事件

public record ToolAuditEvent(
        String auditId,
        String requestId,
        String conversationId,
        String tenantId,
        String userId,
        String toolName,
        String argumentsHash,
        String policyDecision,
        String confirmationToken,
        String idempotencyKey,
        String executionStatus,
        String businessResultId,
        long durationMs,
        Instant occurredAt
) {
}

不要把完整工具参数无脑写进日志。

应对:

  • 手机号;
  • 身份证;
  • 地址;
  • Token;
  • 密钥;
  • 支付信息;

脱敏或只保存Hash。

二十、实现EnterpriseTools

package com.zyentor.ai.tool;

import org.springframework.ai.chat.model.ToolContext;
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.stereotype.Component;

@Component
public class EnterpriseTools {

    private final ToolBusinessValidator validator;
    private final ToolPermissionService permissionService;
    private final ToolConfirmationService confirmationService;
    private final ToolIdempotencyService idempotencyService;
    private final ToolAuditService auditService;
    private final InventoryService inventoryService;
    private final OrderService orderService;

    @Tool(
        name = "query_inventory",
        description = """
        根据SKU查询企业内部实时库存。
        用户询问当前可用数量、锁定数量或指定仓库库存时使用。
        不用于预测未来库存,不用于修改库存。
        """
    )
    public ToolResult queryInventory(
            QueryInventoryRequest request,
            ToolContext toolContext
    ) {
        long start = System.nanoTime();

        validator.validate(request);
        permissionService.requirePermission(
                toolContext,
                "inventory:read"
        );

        String tenantId = requiredContext(
                toolContext,
                ToolContextKeys.TENANT_ID
        );

        InventoryResult result = inventoryService.query(
                tenantId,
                request.sku(),
                request.warehouseCode()
        );

        auditService.success(
                toolContext,
                "query_inventory",
                request,
                null,
                elapsedMs(start)
        );

        return ToolResult.success(result);
    }

    @Tool(
        name = "cancel_order",
        description = """
        取消一个尚未发货且允许取消的订单。
        该工具会改变真实订单状态,必须经过当前用户明确确认。
        不用于退款,也不用于取消已经发货或已完成订单。
        """
    )
    public ToolResult cancelOrder(
            CancelOrderRequest request,
            ToolContext toolContext
    ) {
        long start = System.nanoTime();

        validator.validate(request);
        permissionService.requirePermission(
                toolContext,
                "order:cancel"
        );

        ToolResult confirmation =
                confirmationService.requireConfirmation(
                        toolContext,
                        "cancel_order",
                        request,
                        "确认取消订单"
                                + request.orderId()
                                + ",原因为:"
                                + request.reason()
                );

        if (confirmation != null) {
            auditService.pendingConfirmation(
                    toolContext,
                    "cancel_order",
                    request,
                    confirmation.data(),
                    elapsedMs(start)
            );

            return confirmation;
        }

        String tenantId = requiredContext(
                toolContext,
                ToolContextKeys.TENANT_ID
        );

        String conversationId = requiredContext(
                toolContext,
                ToolContextKeys.CONVERSATION_ID
        );

        String idempotencyKey =
                idempotencyService.cancelOrderKey(
                        tenantId,
                        conversationId,
                        request.orderId()
                );

        ToolResult result =
                idempotencyService.executeOnce(
                        tenantId,
                        idempotencyKey,
                        "cancel_order",
                        request,
                        () -> orderService.cancel(
                                tenantId,
                                request.orderId(),
                                request.reason()
                        )
                );

        auditService.success(
                toolContext,
                "cancel_order",
                request,
                idempotencyKey,
                elapsedMs(start)
        );

        return result;
    }
}

二十一、为什么Tool Context非常关键

模型只看到工具业务参数:

{
  "orderId": "A202607290001",
  "reason": "客户不再需要"
}

模型看不到:

tenantId
userId
permissions
confirmationToken
requestId

这些由服务端通过:

.toolContext(Map.of(...))

注入。

避免模型伪造:

{
  "tenantId": "OTHER_TENANT",
  "isAdmin": true
}

二十二、配置ChatClient

@Configuration
public class ToolCallingConfig {

    @Bean
    ChatClient enterpriseAgentChatClient(
            ChatClient.Builder builder,
            EnterpriseTools tools,
            MessageChatMemoryAdvisor memoryAdvisor
    ) {
        return builder
                .defaultSystem("""
                        你是企业业务Agent。

                        规则:
                        1. 库存、订单和实时状态必须调用工具,不得推测。
                        2. 工具返回CONFIRMATION_REQUIRED时,必须向用户展示确认内容。
                        3. 未获得确认前不得再次尝试高风险工具。
                        4. 工具返回失败时,准确说明错误,不得伪造成功。
                        5. 不得要求用户提供tenantId、权限或系统Token。
                        """)
                .defaultTools(tools)
                .defaultAdvisors(memoryAdvisor)
                .build();
    }
}

ToolCallingAdvisor由ChatClient自动注册,通常不需要手工再注册一个。

二十三、按用户动态选择工具

把所有工具交给所有用户是不安全的。

推荐:

@Service
public class AllowedToolService {

    private final EnterpriseTools tools;

    public Object[] toolsFor(
            CurrentUser user
    ) {
        List allowed = new ArrayList();

        if (user.hasPermission("inventory:read")) {
            allowed.add(tools);
        }

        return allowed.toArray();
    }
}

如果一个工具类包含不同权限的方法,最好拆成多个工具类或使用更细粒度ToolCallback注册表。

工具不可见本身就是权限控制的一部分。

二十四、EnterpriseAgentService

@Service
public class EnterpriseAgentService {

    private final ChatClient chatClient;

    public EnterpriseAgentService(
            @Qualifier("enterpriseAgentChatClient")
            ChatClient chatClient
    ) {
        this.chatClient = chatClient;
    }

    public String chat(
            AgentRequest request,
            CurrentUser user
    ) {
        Map toolContext =
                new HashMap();

        toolContext.put(
                ToolContextKeys.TENANT_ID,
                user.tenantId()
        );

        toolContext.put(
                ToolContextKeys.USER_ID,
                user.userId()
        );

        toolContext.put(
                ToolContextKeys.PERMISSIONS,
                user.permissions()
        );

        toolContext.put(
                ToolContextKeys.CONVERSATION_ID,
                request.conversationId()
        );

        toolContext.put(
                ToolContextKeys.REQUEST_ID,
                request.requestId()
        );

        if (request.confirmationToken() != null) {
            toolContext.put(
                    ToolContextKeys.CONFIRMATION_TOKEN,
                    request.confirmationToken()
            );
        }

        return chatClient.prompt()
                .advisors(spec -> spec.param(
                        ChatMemory.CONVERSATION_ID,
                        request.conversationId()
                ))
                .toolContext(toolContext)
                .user(request.message())
                .call()
                .content();
    }
}

会话ID需要同时用于:

Chat Memory
Tool Context
审计与幂等

但这些系统仍然是不同职责,不能只保存一份字符串就认为治理完成。

二十五、确认流程如何完成第二次调用

第一次:

{
  "conversationId": "C1001",
  "requestId": "R1001",
  "message": "取消订单A202607290001,因为客户不再需要"
}

工具返回:

{
  "code": "CONFIRMATION_REQUIRED",
  "data": {
    "confirmationToken": "CT-ABC",
    "summary": "确认取消订单A202607290001,原因为:客户不再需要",
    "expiresAt": "2026-07-29T10:30:00Z"
  }
}

用户点击确认后,业务接口先把Token状态改为CONFIRMED

第二次请求:

{
  "conversationId": "C1001",
  "requestId": "R1002",
  "message": "我确认执行刚才的取消订单操作",
  "confirmationToken": "CT-ABC"
}

工具服务校验Token绑定的:

用户
租户
会话
工具
参数Hash
有效期

全部一致后才执行。

二十六、为什么不能只把confirmationToken发给模型

模型可能:

  • 在后续对话中复述Token;
  • 把旧Token用于新参数;
  • 在日志或回答中泄露Token;
  • 被Prompt Injection诱导使用Token。

Token应该由前端和业务后端通过受控字段传递,再放入Tool Context。

不要把Token拼进用户自然语言Prompt。

二十七、returnDirect什么时候使用

默认工具结果会回到模型,模型再生成最终回答。

对于某些工具可以:

@Tool(
    description = "返回下载文件",
    returnDirect = true
)

适合:

  • 已经是最终结构化结果;
  • 文件下载信息;
  • RAG原始结果直接返回;
  • 不希望模型二次改写。

不适合取消订单这类需要模型解释结果的工具。

注意:如果模型同一轮请求多个工具,只有全部工具都设置returnDirect=true时,结果才会直接返回。

二十八、结果转换与脱敏

默认结果转换器会把Java结果转换成发送给模型的字符串。

敏感业务建议自定义:

ToolCallResultConverter

只保留:

  • 必要字段;
  • 脱敏信息;
  • 业务状态;
  • 下一步。

例如库存工具不应返回:

  • 成本价;
  • 供应商底价;
  • 内部锁定原因;
  • 其他租户库存。

二十九、工具结果要控制大小

推荐限制:

最多20条记录
最多32KB JSON
包含nextCursor

工具返回:

{
  "total": 238,
  "returned": 20,
  "nextCursor": "CURSOR-2",
  "items": []
}

不要把整张表返回模型。

三十、流式Tool Calling

public Flux stream(
        AgentRequest request,
        CurrentUser user
) {
    return chatClient.prompt()
            .advisors(spec -> spec.param(
                    ChatMemory.CONVERSATION_ID,
                    request.conversationId()
            ))
            .toolContext(buildToolContext(
                    request,
                    user
            ))
            .user(request.message())
            .stream()
            .content();
}

生产前端应区分事件:

MODEL_TEXT
TOOL_CALL_STARTED
CONFIRMATION_REQUIRED
TOOL_EXECUTION_COMPLETED
FINAL_TEXT
ERROR
DONE

仅返回纯文本Chunk会让前端难以展示工具进度和确认卡片。

三十一、Tool Calling与Chat Memory的顺序

默认情况下,Memory Advisor通常位于Tool Calling循环外部,只保存:

用户消息
最终Assistant消息

不保存每个Tool Request和Tool Response。

优点:

  • Memory更干净;
  • 避免工具参数泄露;
  • 多数Repository都支持;
  • Token更可控。

只有确实需要完整工具轨迹时,才考虑把Memory放进循环,并确认Repository支持全部工具消息类型。

完整工具审计仍应使用独立审计表,不能依赖Chat Memory。

三十二、工具太多时启用ToolSearch

当工具达到几十到数百个:

spring.ai.chat.client.tool-search-advisor.enabled=true
spring.ai.chat.client.tool-search-advisor.tool-index-type=vector

但要注意:

  • 工具索引按Session隔离;
  • 仍然需要权限过滤;
  • 工具搜索只负责发现;
  • 发现结果不等于允许执行;
  • 需要评测工具召回率。

三十三、测试查询工具

@SpringBootTest
class InventoryToolTest {

    @Test
    void shouldQueryInventory() {
        String answer = agentService.chat(
                new AgentRequest(
                        "C1001",
                        "R1001",
                        "查询SKU-10001在杭州仓的可用库存",
                        null
                ),
                testUser(Set.of("inventory:read"))
        );

        assertThat(answer)
                .contains("SKU-10001");
    }
}

还要验证服务是否真的被调用,而不是模型根据常识编造答案。

三十四、测试无权限

@Test
void shouldDenyWithoutPermission() {
    assertThatThrownBy(() ->
            agentService.chat(
                    request,
                    testUser(Set.of())
            )
    ).isInstanceOf(AccessDeniedException.class);
}

不要只断言最终文本里出现“没有权限”,因为那可能只是模型自己说的。

三十五、测试高风险确认

测试流程:

第一次调用
→ 必须返回CONFIRMATION_REQUIRED
→ 订单状态不变

确认Token
→ 第二次调用
→ 订单取消一次

第三次重放Token
→ 拒绝

并发测试:

两个请求同时使用同一确认Token和幂等键
→ 只能一个业务事务成功

三十六、测试工具重复调用

模拟模型连续两次请求:

cancel_order
cancel_order

断言:

订单只取消一次
返回同一业务结果
审计记录可解释第二次为幂等命中

三十七、生产观测指标

tool_selection_count
tool_execution_count
tool_success_rate
tool_failure_rate
tool_argument_validation_failure_count
tool_permission_denied_count
tool_confirmation_required_count
tool_confirmation_expired_count
tool_idempotency_hit_count
tool_duplicate_execution_prevented_count
tool_result_size_bytes
tool_execution_duration_ms
tool_loop_iteration_count

按:

  • 工具;
  • 模型;
  • 租户;
  • 业务场景;
  • Prompt版本;

拆分观察。

三十八、生产安全清单

□ 工具只暴露给有权限用户
□ 模型无法生成tenantId和权限
□ 参数经过Schema与业务双重校验
□ 高风险工具必须确认或审批
□ 确认绑定用户、会话、工具和参数Hash
□ 确认Token一次性且有有效期
□ 副作用工具使用数据库级幂等
□ 工具结果经过字段白名单和脱敏
□ 返回大小受到限制
□ 工具超时和重试有边界
□ 审计不依赖Chat Memory
□ 流式事件能表示确认和执行状态
□ ToolSearch不会跨租户共享索引
□ 所有工具都有负责人和版本

三十九、本篇完整调用链

HTTP请求
→ 用户认证
→ 会话归属校验
→ EnterpriseAgentService
→ 计算允许工具
→ ChatClient
→ MessageChatMemoryAdvisor
→ ToolCallingAdvisor
→ 模型选择工具
→ Tool Context注入身份
→ 参数校验
→ 权限策略
→ 确认或审批
→ 幂等执行
→ 结果脱敏
→ 审计
→ 工具结果返回模型
→ 最终回答

四十、下一篇预告

下一篇继续实现:

Spring AI企业级应用实战(6):MCP Client、远程工具、OAuth与多服务治理。

将把本地工具扩展到:

多个MCP Server
+远程工具发现
+协议版本
+OAuth Scope
+超时熔断
+工具列表缓存
+多租户隔离

总结

Spring AI 2.0已经提供完整的Tool Calling基础机制,但企业生产安全取决于应用层是否补齐:

工具白名单
+Tool Context
+参数校验
+权限
+确认与审批
+幂等
+结果脱敏
+审计

模型负责决定“可能需要调用什么”,业务系统负责决定“是否允许、如何执行,以及如何确保只执行一次”。

延伸阅读

如果你正在关注企业级 AI 应用、Spring AI、RAG、Agent 与 MCP 工程化落地,欢迎访问 智元界

https://www.zyentor.com/

智元界将持续分享可运行的技术实战、架构设计、问题排查与企业应用案例。