SpringAI2.0 Tool Calling 进阶:动态模式、ToolContext 与隐式解析

SpringAI 2.0 Tool Calling进阶:动态模式、ToolContext与隐式解析

引言

在企业级AI应用开发中,函数调用(Tool Calling)早已从「炫技」演变为「刚需」。当你的智能客服需要查询客户订单、当你的金融风控系统需要实时调取交易数据、当你的DevOps助手需要执行服务器操作------一切都离不开对工具调用生命周期的精细控制。

然而,真正在生产环境中落地时,开发者往往会遇到一系列棘手问题:工具定义如何动态调整?多租户场景下如何安全传递上下文?隐式工具解析何时生效?对话历史应该如何管理?这些问题在Spring AI 2.0版本中得到了系统性的解决。

本文基于Spring AI 2.0.0-M2/M3版本,深入剖析Tool Calling的全生命周期管理,涵盖从底层接口设计到高级实战场景的完整技术体系。


一、ToolDefinition与ToolCallback抽象设计

1.1 核心接口解析

Spring AI 2.0引入了全新的Tool Calling抽象层,其中最核心的两个接口是ToolDefinitionToolCallback。理解这两个接口的设计理念,是掌握整个工具调用体系的前提。

java 复制代码
public interface ToolDefinition {
    /**
     * 工具名称,在提供给模型的工具集中必须唯一
     */
    String name();

    /**
     * 工具描述,AI模型使用此描述确定工具功能
     * 描述的质量直接影响模型选择工具的准确性
     */
    String description();

    /**
     * 调用工具时使用的参数模式,遵循JSON Schema规范
     */
    String inputSchema();
}
java 复制代码
public interface ToolCallback {
    /**
     * 获取工具定义,包含名称、描述和输入模式
     */
    ToolDefinition getToolDefinition();

    /**
     * 获取工具元数据,控制工具执行行为
     */
    ToolMetadata getToolMetadata();

    /**
     * 使用给定输入执行工具
     */
    String call(String toolInput);

    /**
     * 使用给定输入和上下文执行工具
     * ToolContext携带运行时信息,如租户ID、用户权限等
     */
    String call(String toolInput, ToolContext toolContext);
}

1.2 业务场景:电商订单查询

在电商场景中,我们需要为AI助手提供订单查询能力。来看看如何正确设计工具定义:

java 复制代码
@Component
public class OrderQueryTool implements ToolCallback {

    private final OrderRepository orderRepository;

    public OrderQueryTool(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    @Override
    public ToolDefinition getToolDefinition() {
        return ToolDefinition.builder()
                .name("queryOrder")
                .description("查询用户订单详情,包括订单状态、金额、商品列表等信息。" +
                           "当用户询问订单物流、订单金额、订单商品时必须使用此工具。")
                .inputSchema("""
                    {
                        "type": "object",
                        "properties": {
                            "orderId": {
                                "type": "string",
                                "description": "订单编号,如ORD20240101001"
                            },
                            "queryType": {
                                "type": "string",
                                "enum": ["basic", "detail", "logistics"],
                                "description": "查询类型:basic-基本信息, detail-详细信息, logistics-物流信息"
                            }
                        },
                        "required": ["orderId"]
                    }
                    """)
                .build();
    }

    @Override
    public ToolMetadata getToolMetadata() {
        return ToolMetadata.builder()
                .returnDirect(false)  // 工具结果发送回AI模型进行总结
                .build();
    }

    @Override
    public String call(String toolInput, ToolContext toolContext) {
        try {
            Map<String, Object> input = objectMapper.readValue(toolInput, Map.class);
            String orderId = (String) input.get("orderId");
            String queryType = (String) input.getOrDefault("queryType", "basic");

            // 从ToolContext获取租户ID,确保数据隔离
            String tenantId = (String) toolContext.getContext().get("tenantId");

            Order order = orderRepository.findByOrderIdAndTenantId(orderId, tenantId);
            if (order == null) {
                return JsonResult.error("订单不存在或无权限访问");
            }

            return switch (queryType) {
                case "detail" -> objectMapper.writeValueAsString(order);
                case "logistics" -> objectMapper.writeValueAsString(order.getLogistics());
                default -> objectMapper.writeValueAsString(OrderBasicInfo.from(order));
            };
        } catch (Exception e) {
            return JsonResult.error("查询失败: " + e.getMessage());
        }
    }
}

1.3 踩坑经验

坑点一:描述质量决定调用准确率

很多开发者忽视description字段的重要性,随意写一句"查询订单"。实际上,这个描述会被发送给大模型,模型据此决定是否调用该工具。建议使用「功能+触发场景」的写法:

java 复制代码
// 错误的描述
.description("查询订单")

// 正确的描述(包含触发场景)
.description("查询用户订单信息。" +
             "当用户说'帮我看看订单'、'订单到哪了'、'订单多少钱'时必须调用此工具。")

坑点二:inputSchema必须严格遵循JSON Schema规范

如果Schema格式错误,模型可能无法正确解析参数。建议使用Java 15+的文本块(Text Blocks)编写:

java 复制代码
.inputSchema("""
    {
        "type": "object",
        "properties": {
            "amount": {
                "type": "number",
                "description": "金额,单位为元"
            }
        },
        "required": ["amount"]
    }
    """)

二、ToolContext最终类特性与Map传参模式

2.1 设计变更说明

Spring AI 2.0对ToolContext进行了重大调整:它变成了一个final类,不再支持继承扩展 。这一变化迫使开发者必须使用Map传参模式来传递上下文信息。

java 复制代码
// ToolContext是final类,无法继承
public final class ToolContext {
    private final Map<String, Object> context;

    public ToolContext(Map<String, Object> context) {
        this.context = Collections.unmodifiableMap(context);
    }

    public Map<String, Object> getContext() {
        return context;
    }
}

这种设计的核心目的是强制显式化上下文传递,避免通过继承带来的隐式依赖,让工具调用的数据流更加清晰可控。

2.2 正确用法:Map传参模式

java 复制代码
@Service
public class OrderService {

    private final ChatClient chatClient;

    public OrderService(ChatClient.Builder chatClientBuilder,
                        ChatModel chatModel) {
        this.chatClient = chatClientBuilder
                .defaultAdvisors(new ToolCallAdvisor())
                .build();
    }

    /**
     * 多租户场景下的订单咨询
     */
    public String queryOrderWithTenant(String userQuestion, String tenantId) {
        return chatClient.prompt()
                .user(userQuestion)
                .tools(new OrderQueryTool(orderRepository))
                // 通过Map传递租户上下文
                .toolContext(Map.of(
                    "tenantId", tenantId,
                    "userId", getCurrentUserId(),
                    "userRole", getCurrentUserRole()
                ))
                .call()
                .content();
    }

    /**
     * 传递动态上下文(运行时计算的值)
     */
    public String queryWithDynamicContext(String question) {
        Map<String, Object> dynamicContext = new HashMap<>();
        dynamicContext.put("tenantId", getCurrentTenant());
        dynamicContext.put("requestId", UUID.randomUUID().toString());
        dynamicContext.put("timestamp", System.currentTimeMillis());
        dynamicContext.put("featureFlags", getFeatureFlags());

        return chatClient.prompt()
                .user(question)
                .tools(new OrderQueryTool(orderRepository), new ProductQueryTool(productRepository))
                .toolContext(dynamicContext)
                .call()
                .content();
    }
}

2.3 金融场景:风控系统上下文传递

在金融风控场景中,安全上下文传递尤为重要:

java 复制代码
@Component
public class RiskControlTool implements ToolCallback {

    private final RiskControlService riskControlService;

    @Override
    public String call(String toolInput, ToolContext toolContext) {
        // 从上下文获取风控所需的敏感信息
        String userId = (String) toolContext.getContext().get("userId");
        String ipAddress = (String) toolContext.getContext().get("ipAddress");
        String deviceFingerprint = (String) toolContext.getContext().get("deviceFingerprint");
        String riskLevel = (String) toolContext.getContext().get("riskLevel");

        // 验证必要参数
        if (userId == null || ipAddress == null) {
            return JsonResult.error("缺少必要的风控参数");
        }

        // 执行风控检查
        RiskCheckResult result = riskControlService.checkUserRisk(
                userId, ipAddress, deviceFingerprint, riskLevel
        );

        return JsonResult.success(result);
    }

    @Override
    public ToolDefinition getToolDefinition() {
        return ToolDefinition.builder()
                .name("riskControlCheck")
                .description("执行用户风控检查,评估交易风险等级。" +
                           "当用户发起转账、支付、提现等敏感操作时必须调用。")
                .inputSchema("""
                    {
                        "type": "object",
                        "properties": {
                            "transactionType": {
                                "type": "string",
                                "enum": ["TRANSFER", "PAYMENT", "WITHDRAWAL", "REFUND"],
                                "description": "交易类型"
                            },
                            "amount": {
                                "type": "number",
                                "description": "交易金额,单位元"
                            },
                            "targetAccount": {
                                "type": "string",
                                "description": "目标账户ID"
                            }
                        },
                        "required": ["transactionType", "amount"]
                    }
                    """)
                .build();
    }
}

// 调用时传递完整风控上下文
public String processPayment(String userQuestion, HttpServletRequest request) {
    SecurityContext securityContext = SecurityUtils.getContext();

    return chatClient.prompt()
            .user(userQuestion)
            .tools(new RiskControlTool(riskControlService))
            .toolContext(Map.of(
                "userId", securityContext.getUserId(),
                "ipAddress", request.getRemoteAddr(),
                "deviceFingerprint", getDeviceFingerprint(request),
                "riskLevel", securityContext.getRiskLevel(),
                "accountBalance", accountService.getBalance(securityContext.getUserId())
            ))
            .call()
            .content();
}

2.4 踩坑经验

坑点一:传递null值导致NPE

ToolContext内部使用Collections.unmodifiableMap()包装,如果Map中包含null值,可能导致序列化问题。建议在传递前过滤:

java 复制代码
// 错误做法
.toolContext(Map.of(
    "tenantId", tenantId,  // 如果tenantId为null,会出问题
    "userId", userId
))

// 正确做法:过滤null值
public static Map<String, Object> sanitizeContext(Map<String, Object> raw) {
    return raw.entrySet().stream()
            .filter(e -> e.getValue() != null)
            .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
}

// 使用
.toolContext(sanitizeContext(Map.of(
    "tenantId", tenantId,
    "userId", userId
)))

坑点二:上下文在多轮对话中丢失

如果使用ToolCallAdvisor的默认配置,每轮工具调用都会重新创建ToolContext。如果需要跨轮次保持上下文,需要使用自定义逻辑:

java 复制代码
// 在工具类中维护一个静态Map来保存上下文(仅用于演示,生产环境请使用更优雅的方式)
public class ContextHolder {
    private static final ThreadLocal<Map<String, Object>> CONTEXT = new ThreadLocal<>();

    public static void setContext(Map<String, Object> context) {
        CONTEXT.set(new HashMap<>(context));
    }

    public static Map<String, Object> getContext() {
        return CONTEXT.get();
    }

    public static void clear() {
        CONTEXT.remove();
    }
}

// 在每次调用前设置上下文
public String queryWithContext(String question) {
    Map<String, Object> context = buildContext();
    ContextHolder.setContext(context);

    try {
        return chatClient.prompt()
                .user(question)
                .tools(toolCallback)
                .toolContext(context)  // 首次传递
                .call()
                .content();
    } finally {
        ContextHolder.clear();
    }
}

三、显式工具包含与隐式工具解析

3.1 概念澄清

Spring AI 2.0支持两种工具使用策略:

策略 说明 适用场景
显式工具包含 在请求时明确指定可用工具 工具数量较少、需要精确控制
隐式工具解析 运行时根据需求动态解析可用工具 工具数量较多、需要按需加载

3.2 显式工具包含

最常用的方式,在构建ChatClient时直接指定工具:

java 复制代码
@Configuration
public class ChatClientConfig {

    @Bean
    public ChatClient chatClient(ChatModel chatModel,
                                  OrderQueryTool orderQueryTool,
                                  ProductQueryTool productQueryTool,
                                  RiskControlTool riskControlTool) {
        return ChatClient.builder(chatModel)
                .defaultAdvisors(new ToolCallAdvisor())
                .build()
                .mutate()
                .tools(orderQueryTool, productQueryTool, riskControlTool)
                .build();
    }
}

或者在运行时动态指定:

java 复制代码
public String queryWithSpecificTools(String question, Set<String> enabledTools) {
    List<ToolCallback> tools = new ArrayList<>();

    if (enabledTools.contains("order")) {
        tools.add(new OrderQueryTool(orderRepository));
    }
    if (enabledTools.contains("product")) {
        tools.add(new ProductQueryTool(productRepository));
    }
    if (enabledTools.contains("risk")) {
        tools.add(new RiskControlTool(riskControlService));
    }

    return chatClient.prompt()
            .user(question)
            .tools(tools.toArray(new ToolCallback[0]))
            .call()
            .content();
}

3.3 隐式工具解析:运行时按需加载

当工具数量庞大时(如上百个微服务接口),可以使用隐式解析策略:

java 复制代码
@Component
public class DynamicToolRegistry implements ToolCallbackResolver {

    private final Map<String, ToolCallback> toolCache = new ConcurrentHashMap<>();
    private final ToolCallbackLoader loader;

    public DynamicToolRegistry(ToolCallbackLoader loader) {
        this.loader = loader;
    }

    @Override
    @Nullable
    public ToolCallback resolve(String toolName) {
        return toolCache.computeIfAbsent(toolName, name -> {
            // 懒加载:首次使用时从远程加载
            return loader.load(name);
        });
    }

    /**
     * 预热:提前加载常用工具
     */
    public void warmup(Set<String> toolNames) {
        toolNames.forEach(name -> resolve(name));
    }

    /**
     * 清理缓存
     */
    public void clearCache() {
        toolCache.clear();
    }
}
java 复制代码
/**
 * 工具加载器:从Spring容器或远程注册中心加载工具
 */
@Component
public class ToolCallbackLoader {

    private final ApplicationContext context;

    public ToolCallbackLoader(ApplicationContext context) {
        this.context = context;
    }

    public ToolCallback load(String toolName) {
        // 方式一:从Spring容器获取
        try {
            return context.getBean(toolName, ToolCallback.class);
        } catch (BeansException e) {
            // 方式二:从远程服务注册中心获取
            return loadFromRegistry(toolName);
        }
    }

    private ToolCallback loadFromRegistry(String toolName) {
        // 假设有一个远程工具注册服务
        ToolDefinition definition = remoteRegistry.getDefinition(toolName);
        return new DynamicToolCallback(definition, this::executeRemote);
    }

    private String executeRemote(ToolDefinition definition, String input, ToolContext context) {
        // 调用远程服务执行工具
        return remoteService.execute(definition.getName(), input);
    }
}

3.4 DevOps场景:动态工具加载

在DevOps自动化场景中,工具数量可能非常庞大:

java 复制代码
/**
 * DevOps工具注册中心
 * 支持从配置中心动态加载可用工具列表
 */
@Configuration
public class DevOpsToolRegistry {

    @Value("${devops.tools.enabled:*}")
    private String enabledToolsPattern;

    private final Map<String, ToolCallback> toolMap = new ConcurrentHashMap<>();

    @PostConstruct
    public void init() {
        // 从配置中心加载启用的工具
        List<String> enabledToolNames = resolveEnabledTools();

        for (String toolName : enabledToolNames) {
            toolMap.put(toolName, createTool(toolName));
        }
    }

    private List<String> resolveEnabledTools() {
        // 从Nacos/Apollo等配置中心获取
        return Arrays.asList(enabledToolsPattern.split(","));
    }

    private ToolCallback createTool(String toolName) {
        return switch (toolName) {
            case "deploy" -> new DeployTool();
            case "rollback" -> new RollbackTool();
            case "scale" -> new ScaleTool();
            case "logs" -> new LogsTool();
            case "monitor" -> new MonitorTool();
            default -> throw new IllegalArgumentException("未知工具: " + toolName);
        };
    }

    public ToolCallback getTool(String name) {
        ToolCallback tool = toolMap.get(name);
        if (tool == null) {
            throw new ToolNotFoundException("工具不存在或未启用: " + name);
        }
        return tool;
    }
}

3.5 流程图:显式与隐式策略对比

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                    显式工具包含策略                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   User Request                                                  │
│        │                                                         │
│        ▼                                                         │
│   ┌─────────┐    指定工具列表     ┌──────────┐                  │
│   │ ChatClient │ ──────────────▶ │ ToolCall │                  │
│   │ .tools()  │                  │  Advisor │                  │
│   └─────────┘                   └──────────┘                  │
│        │                              │                          │
│        ▼                              ▼                          │
│   ┌──────────────────────────────────────────┐                  │
│   │        直接调用指定工具的call方法         │                  │
│   └──────────────────────────────────────────┘                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│                    隐式工具解析策略                              │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   User Request                                                  │
│        │                                                         │
│        ▼                                                         │
│   ┌─────────┐    请求工具名      ┌──────────────┐                │
│   │ ChatClient │ ──────────────▶ │ ToolCallback │                │
│   │           │                  │  Resolver    │                │
│   └─────────┘                   └──────────────┘                │
│        │                              │                          │
│        ▼                              ▼                          │
│   ┌──────────────┐          ┌─────────────────┐                 │
│   │ 模型返回     │          │ 查找或加载工具  │                 │
│   │ tool_call   │          │ (懒加载/缓存)    │                 │
│   └──────────────┘          └─────────────────┘                 │
│        │                              │                          │
│        ▼                              ▼                          │
│   ┌──────────────────────────────────────────┐                  │
│   │        执行解析后的工具                   │                  │
│   └──────────────────────────────────────────┘                  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

3.6 踩坑经验

坑点一:隐式解析时工具未找到

如果ToolCallbackResolver返回null,会导致工具调用失败:

java 复制代码
// 错误:resolver返回null没有处理
@Override
public ToolCallback resolve(String toolName) {
    return toolMap.get(toolName);  // 如果不存在,返回null
}

// 正确:显式抛出异常
@Override
public ToolCallback resolve(String toolName) {
    ToolCallback tool = toolMap.get(toolName);
    if (tool == null) {
        throw new ToolResolutionException("工具未找到: " + toolName +
            ", 可用工具: " + toolMap.keySet());
    }
    return tool;
}

坑点二:隐式解析的性能问题

每次工具调用都会触发解析逻辑,应使用缓存:

java 复制代码
@Component
public class CachedToolResolver implements ToolCallbackResolver {

    private final ToolCallbackLoader loader;
    private final Cache<String, ToolCallback> cache;

    public CachedToolResolver(ToolCallbackLoader loader) {
        this.loader = loader;
        this.cache = Caffeine.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(Duration.ofMinutes(10))
                .build();
    }

    @Override
    public ToolCallback resolve(String toolName) {
        return cache.get(toolName, () -> loader.load(toolName));
    }
}

四、动态工具模式与运行时参数定义

4.1 动态工具模式原理

动态工具模式允许在运行时修改工具的参数定义,这在很多场景下非常有用:

  • 根据用户权限动态调整参数
  • 根据业务状态动态改变输入要求
  • 支持可选参数的动态增减

4.2 运行时参数修改

java 复制代码
/**
 * 动态工具:根据用户权限动态调整可用参数
 */
@Component
public class DynamicOrderTool implements ToolCallback {

    private final OrderRepository orderRepository;
    private final PermissionService permissionService;

    public DynamicOrderTool(OrderRepository orderRepository,
                           PermissionService permissionService) {
        this.orderRepository = orderRepository;
        this.permissionService = permissionService;
    }

    @Override
    public ToolDefinition getToolDefinition() {
        Set<String> userPermissions = permissionService.getCurrentUserPermissions();

        // 根据权限动态构建输入Schema
        Map<String, Object> properties = new LinkedHashMap<>();

        // 基础参数:所有用户都可用
        properties.put("orderId", Map.of(
            "type", "string",
            "description", "订单编号"
        ));

        // 敏感参数:仅管理员可见
        if (userPermissions.contains("ORDER_VIEW_SENSITIVE")) {
            properties.put("includePaymentInfo", Map.of(
                "type", "boolean",
                "description", "是否包含支付信息(需管理员权限)"
            ));
            properties.put("includeCustomerInfo", Map.of(
                "type", "boolean",
                "description", "是否包含客户敏感信息(需管理员权限)"
            ));
        }

        // 财务参数:仅财务人员可见
        if (userPermissions.contains("ORDER_VIEW_FINANCE")) {
            properties.put("includeFinancialDetails", Map.of(
                "type", "boolean",
                "description", "是否包含财务详情(需财务权限)"
            ));
        }

        List<String> required = List.of("orderId");

        String inputSchema = buildSchema(properties, required);

        return ToolDefinition.builder()
                .name("dynamicOrderQuery")
                .description("动态订单查询工具," +
                           "根据用户权限自动调整可查询的信息范围。")
                .inputSchema(inputSchema)
                .build();
    }

    private String buildSchema(Map<String, Object> properties, List<String> required) {
        Map<String, Object> schema = new LinkedHashMap<>();
        schema.put("type", "object");
        schema.put("properties", properties);
        schema.put("required", required);

        try {
            return objectMapper.writeValueAsString(schema);
        } catch (JsonProcessingException e) {
            throw new RuntimeException("构建Schema失败", e);
        }
    }

    @Override
    public String call(String toolInput, ToolContext toolContext) {
        // 实际执行逻辑
        return "{}";
    }
}

4.3 动态工具工厂模式

java 复制代码
/**
 * 动态工具工厂:根据运行时条件创建不同配置的工具
 */
@Component
public class DynamicToolFactory {

    private final Map<String, ToolCallback> toolCache = new ConcurrentHashMap<>();

    /**
     * 根据业务场景获取工具
     */
    public ToolCallback getTool(String scene, Map<String, Object> params) {
        String cacheKey = scene + "_" + params.hashCode();

        return toolCache.computeIfAbsent(cacheKey, key -> createTool(scene, params));
    }

    private ToolCallback createTool(String scene, Map<String, Object> params) {
        return switch (scene) {
            case "ORDER_BASIC" -> createBasicOrderTool();
            case "ORDER_ADVANCED" -> createAdvancedOrderTool(params);
            case "ORDER_FINANCE" -> createFinanceOrderTool();
            default -> throw new IllegalArgumentException("未知场景: " + scene);
        };
    }

    private ToolCallback createBasicOrderTool() {
        return new AbstractToolCallback() {
            @Override
            public ToolDefinition getToolDefinition() {
                return ToolDefinition.builder()
                        .name("basicOrderQuery")
                        .description("基础订单查询,仅返回订单基本状态")
                        .inputSchema("""
                            {
                                "type": "object",
                                "properties": {
                                    "orderId": {"type": "string"}
                                },
                                "required": ["orderId"]
                            }
                            """)
                        .build();
            }

            @Override
            public String call(String toolInput, ToolContext context) {
                // 简化实现
                return "{\"status\": \"已发货\"}";
            }
        };
    }

    private ToolCallback createAdvancedOrderTool(Map<String, Object> params) {
        boolean includeLogistics = (Boolean) params.getOrDefault("includeLogistics", false);
        boolean includeInvoice = (Boolean) params.getOrDefault("includeInvoice", false);

        return new AbstractToolCallback() {
            @Override
            public ToolDefinition getToolDefinition() {
                Map<String, Object> props = new LinkedHashMap<>();
                props.put("orderId", Map.of("type", "string"));

                if (includeLogistics) {
                    props.put("logisticsDetail", Map.of(
                        "type", "boolean",
                        "description", "是否包含详细物流信息"
                    ));
                }

                if (includeInvoice) {
                    props.put("invoiceInfo", Map.of(
                        "type", "boolean",
                        "description", "是否包含发票信息"
                    ));
                }

                return ToolDefinition.builder()
                        .name("advancedOrderQuery")
                        .description("高级订单查询,支持更多维度")
                        .inputSchema(buildSchema(props, List.of("orderId")))
                        .build();
            }

            @Override
            public String call(String toolInput, ToolContext context) {
                return "{}";
            }
        };
    }

    private ToolCallback createFinanceOrderTool() {
        // 财务专用工具实现
        return null;
    }

    private String buildSchema(Map<String, Object> properties, List<String> required) {
        try {
            Map<String, Object> schema = new LinkedHashMap<>();
            schema.put("type", "object");
            schema.put("properties", properties);
            schema.put("required", required);
            return objectMapper.writeValueAsString(schema);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

4.4 流程图:动态工具调用链路

复制代码
┌─────────────────────────────────────────────────────────────────┐
│                  动态工具调用完整流程                             │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   ┌─────────────┐                                               │
│   │ 用户请求    │                                               │
│   └──────┬──────┘                                               │
│          │                                                       │
│          ▼                                                       │
│   ┌─────────────────────────────────────────┐                    │
│   │  ChatClient.prompt()                    │                    │
│   │  .tools(dynamicTool)                    │                    │
│   └──────┬────────────────────────────────┘                    │
│          │                                                       │
│          ▼                                                       │
│   ┌─────────────────────────────────────────┐                    │
│   │  ToolCallAdvisor 开始执行                │                    │
│   │  1. 获取工具定义 (getToolDefinition)     │                    │
│   │  2. 将定义发送给LLM                     │                    │
│   └──────┬────────────────────────────────┘                    │
│          │                                                       │
│          ▼                                                       │
│   ┌─────────────────────────────────────────┐                    │
│   │  LLM返回tool_call请求                   │                    │
│   │  {                                      │                    │
│   │    name: "dynamicOrderQuery",           │                    │
│   │    arguments: {...}                     │                    │
│   │  }                                      │                    │
│   └──────┬────────────────────────────────┘                    │
│          │                                                       │
│          ▼                                                       │
│   ┌─────────────────────────────────────────┐                    │
│   │  ToolExecutionHandler执行工具           │                    │
│   │  call(toolInput, toolContext)           │                    │
│   └──────┬────────────────────────────────┘                    │
│          │                                                       │
│          ▼                                                       │
│   ┌─────────────────────────────────────────┐                    │
│   │  工具执行结果返回给LLM                  │                    │
│   │  LLM生成最终回复                        │                    │
│   └──────┬────────────────────────────────┘                    │
│          │                                                       │
│          ▼                                                       │
│   ┌─────────────┐                                               │
│   │ 返回结果    │                                               │
│   └─────────────┘                                               │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

4.5 踩坑经验

坑点一:Schema构建错误导致模型无法解析

动态构建Schema时,JSON格式必须严格正确:

java 复制代码
// 错误:手写字符串容易出错
.inputSchema("{\"type\": \"object\", \"properties\": {\"name\": {\"type\": \"string\"}");

// 正确:使用Jackson构建
private String buildCorrectSchema() {
    ObjectNode root = objectMapper.createObjectNode();
    root.put("type", "object");

    ObjectNode properties = root.putObject("properties");
    ObjectNode nameProp = properties.putObject("name");
    nameProp.put("type", "string");

    ArrayNode required = root.putArray("required");
    required.add("name");

    return root.toString();
}

坑点二:动态Schema导致模型困惑

频繁变化Schema会让模型无所适从。建议保持Schema相对稳定,通过参数组合实现动态效果:

java 复制代码
// 不推荐:每次调用都改变Schema结构
public ToolDefinition getToolDefinition() {
    // 动态改变结构 - 模型可能困惑
}

// 推荐:保持Schema稳定,通过参数控制行为
public ToolDefinition getToolDefinition() {
    return ToolDefinition.builder()
            .name("orderQuery")
            .inputSchema("""
                {
                    "type": "object",
                    "properties": {
                        "orderId": {"type": "string"},
                        "queryMode": {
                            "type": "string",
                            "enum": ["BASIC", "ADVANCED", "FINANCE"]
                        }
                    },
                    "required": ["orderId"]
                }
                """)
            .build();
}

五、ToolCallAdvisor的conversationHistoryEnabled

5.1 功能概述

ToolCallAdvisor是Spring AI 2.0引入的重要组件,它将工具调用循环从LLM内部转移到Advisor链中实现,使得工具调用过程对整个Advisor链可见、可拦截、可观测。

conversationHistoryEnabled是其中一个关键配置选项:

配置值 行为 适用场景
true(默认) 在工具调用迭代期间内部维护完整会话历史 简单对话场景,不需要自定义内存管理
false 禁用内部历史管理 与ChatMemory Advisor集成,需要精细控制

5.2 默认模式:启用内部历史

java 复制代码
@Configuration
public class AdvisorConfig {

    @Bean
    public ChatClient chatClient(ChatModel chatModel,
                                  ToolCallingManager toolCallingManager) {
        // 默认启用conversationHistoryEnabled
        ToolCallAdvisor toolCallAdvisor = ToolCallAdvisor.builder()
                .toolCallingManager(toolCallingManager)
                .conversationHistoryEnabled(true)  // 默认值,可以省略
                .advisorOrder(100)
                .build();

        return ChatClient.builder(chatModel)
                .defaultAdvisors(toolCallAdvisor)
                .build();
    }
}

这种模式下,ToolCallAdvisor会自动维护工具调用过程中的会话历史,包括:

  1. 用户消息
  2. AI响应(包含tool_call请求)
  3. 工具执行结果
  4. 最终AI响应
java 复制代码
// 完整示例:多轮工具调用
@Service
public class OrderAssistantService {

    private final ChatClient chatClient;

    public OrderAssistantService(ChatClient.Builder builder, ChatModel chatModel,
                                  ToolCallingManager toolCallingManager) {
        ToolCallAdvisor advisor = ToolCallAdvisor.builder()
                .toolCallingManager(toolCallingManager)
                .conversationHistoryEnabled(true)
                .build();

        this.chatClient = builder
                .chatModel(chatModel)
                .defaultAdvisors(advisor)
                .build();
    }

    public String assist(String userQuestion) {
        return chatClient.prompt()
                .user(userQuestion)
                .tools(new OrderQueryTool(orderRepository))
                .call()
                .content();
        // 在内部,Advisor会维护这样的历史:
        // 1. USER: 帮我查一下ORD20240101001的物流
        // 2. ASSISTANT: tool_calls=[{name: "queryOrder", args: {orderId: "ORD20240101001", queryType: "logistics"}}]
        // 3. TOOL: {物流信息: "已到达北京分部"}
        // 4. ASSISTANT: 您的订单已到达北京分部,预计明天送达...
    }
}

5.3 高级模式:禁用内部历史

当需要与ChatMemory Advisor集成,或者需要完全自定义会话管理时,需要禁用内部历史:

java 复制代码
@Configuration
public class AdvancedAdvisorConfig {

    @Bean
    public ChatClient chatClientWithMemory(ChatModel chatModel,
                                            ToolCallingManager toolCallingManager,
                                            ChatMemory chatMemory) {
        // 禁用ToolCallAdvisor的内部历史
        ToolCallAdvisor toolCallAdvisor = ToolCallAdvisor.builder()
                .toolCallingManager(toolCallingManager)
                .conversationHistoryEnabled(false)  // 禁用内部历史
                .advisorOrder(100)
                .build();

        // 添加ChatMemory Advisor来处理会话历史
        MessageChatMemoryAdvisor memoryAdvisor = new MessageChatMemoryAdvisor(chatMemory);
        memoryAdvisor.setOrder(200);  // 在ToolCallAdvisor之后执行

        return ChatClient.builder(chatModel)
                .defaultAdvisors(toolCallAdvisor, memoryAdvisor)
                .build();
    }
}
java 复制代码
/**
 * 使用外部ChatMemory的完整示例
 */
@Service
public class PersistentOrderAssistant {

    private final ChatClient chatClient;
    private final InMemoryChatMemory chatMemory;

    public PersistentOrderAssistant(ChatModel chatModel,
                                    ToolCallingManager toolCallingManager) {
        // 使用InMemoryChatMemory作为示例,生产环境可用Redis等
        this.chatMemory = new InMemoryChatMemory();

        ToolCallAdvisor toolCallAdvisor = ToolCallAdvisor.builder()
                .toolCallingManager(toolCallingManager)
                .conversationHistoryEnabled(false)
                .build();

        MessageChatMemoryAdvisor memoryAdvisor = new MessageChatMemoryAdvisor(chatMemory);
        memoryAdvisor.setOrder(200);

        this.chatClient = ChatClient.builder(chatModel)
                .defaultAdvisors(toolCallAdvisor, memoryAdvisor)
                .build();
    }

    /**
     * 带会话上下文的咨询
     */
    public String chat(String sessionId, String message) {
        // 为不同会话设置不同的内存
        chatMemory.setSessionId(sessionId);

        return chatClient.prompt()
                .user(message)
                .tools(new OrderQueryTool(orderRepository),
                       new ProductQueryTool(productRepository))
                .call()
                .content();
    }

    /**
     * 清除会话历史
     */
    public void clearSession(String sessionId) {
        chatMemory.clear();
    }
}

5.4 便捷方法

Spring AI还提供了便捷的禁用方法:

java 复制代码
// 方式一:使用conversationHistoryEnabled(false)
ToolCallAdvisor advisor = ToolCallAdvisor.builder()
        .conversationHistoryEnabled(false)
        .build();

// 方式二:使用disableInternalConversationHistory()(推荐)
ToolCallAdvisor advisor = ToolCallAdvisor.builder()
        .disableInternalConversationHistory()
        .build();

// 方式三:使用disableMemory()(已废弃,不推荐)
ToolCallAdvisor advisor = ToolCallAdvisor.builder()
        .disableMemory()  // 已标记为@Deprecated
        .build();

5.5 流程图:两种模式对比

复制代码
┌─────────────────────────────────────────────────────────────────┐
│           conversationHistoryEnabled = true                    │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   User Message                                                  │
│        │                                                        │
│        ▼                                                        │
│   ┌────────────────────────────────────────────────────────┐   │
│   │  ToolCallAdvisor 内部维护历史                           │   │
│   │  ┌──────────────────────────────────────────────────┐  │   │
│   │  │ [User] 查询ORD001订单                            │  │   │
│   │  │ [AI] tool_call: queryOrder(ORD001)              │  │   │
│   │  │ [Tool] {status: "shipped"}                      │  │   │
│   │  │ [AI] 订单已发货,预计明天送达                    │  │   │
│   │  └──────────────────────────────────────────────────┘  │   │
│   └────────────────────────────────────────────────────────┘   │
│        │                                                        │
│        ▼                                                        │
│   Final Response                                                │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────┐
│           conversationHistoryEnabled = false                   │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│   User Message                                                  │
│        │                                                        │
│        ▼                                                        │
│   ┌────────────────────┐     ┌────────────────────────────┐   │
│   │ ToolCallAdvisor    │────▶│ ChatMemory Advisor          │   │
│   │ 不保存历史          │     │ 外部管理会话历史             │   │
│   └────────────────────┘     └────────────────────────────┘   │
│        │                                │                       │
│        ▼                                ▼                       │
│   ┌────────────────────────────────────────────────────────┐   │
│   │  工具调用执行(使用外部ChatMemory存储历史)             │   │
│   └────────────────────────────────────────────────────────┘   │
│        │                                                        │
│        ▼                                                        │
│   Final Response + History Updated                             │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

5.6 踩坑经验

坑点一:禁用历史后忘记添加ChatMemory Advisor

java 复制代码
// 错误:禁用历史但没有提供替代方案
ToolCallAdvisor advisor = ToolCallAdvisor.builder()
        .conversationHistoryEnabled(false)
        .build();
// 结果:工具调用无法正确维护上下文,可能出现重复调用

// 正确:禁用时必须提供ChatMemory
ToolCallAdvisor advisor = ToolCallAdvisor.builder()
        .conversationHistoryEnabled(false)
        .build();

MessageChatMemoryAdvisor memoryAdvisor = new MessageChatMemoryAdvisor(chatMemory);

ChatClient.builder(chatModel)
        .defaultAdvisors(advisor, memoryAdvisor)
        .build();

坑点二:Advisor顺序错误

ToolCallAdvisor必须在ChatMemoryAdvisor之前执行:

java 复制代码
// 错误顺序
.defaultAdvisors(memoryAdvisor, toolCallAdvisor)
// ToolCallAdvisor后执行会导致历史还未保存就开始调用工具

// 正确顺序
.defaultAdvisors(toolCallAdvisor, memoryAdvisor)
// ToolCallAdvisor先执行工具调用,然后ChatMemory保存历史

坑点三:sessionId管理

使用外部ChatMemory时,注意sessionId的生命周期:

java 复制代码
public class SessionAwareChatMemory implements ChatMemory {

    private final Map<String, List<Message>> sessions = new ConcurrentHashMap<>();
    private String currentSessionId;

    public void setSessionId(String sessionId) {
        this.currentSessionId = sessionId;
    }

    @Override
    public void add(Message message) {
        sessions.computeIfAbsent(currentSessionId, k -> new ArrayList<>())
                .add(message);
    }

    @Override
    public List<Message> getLastMessages(int n) {
        List<Message> messages = sessions.getOrDefault(currentSessionId, Collections.emptyList());
        int start = Math.max(0, messages.size() - n);
        return messages.subList(start, messages.size());
    }

    @Override
    public void clear() {
        sessions.remove(currentSessionId);
    }
}

六、MCP集成与Tool Calling生态

6.1 MCP SDK 0.17.2更新

Spring AI对MCP(Model Context Protocol)的支持持续增强。MCP SDK已更新至0.17.x版本,引入了Mcp*ServerCustomizer接口,简化了MCP服务器的定制过程:

java 复制代码
/**
 * 使用McpServerCustomizer定制MCP服务器
 */
@Configuration
public class MCPServerConfig {

    @Bean
    public McpAsyncServer weatherMcpServer(
            McpServerProperties serverProperties,
            WeatherService weatherService) {

        return McpAsyncServer.builder(new StdIoTransportProvider())
                .serverInfo(ServerInfo.builder()
                        .name("weather-service")
                        .version("1.0.0")
                        .build())
                .customizer(serverBuilder -> {
                    // 使用Customizer定制服务器
                    serverBuilder.toolCustomizer(toolBuilder -> {
                        toolBuilder.description("提供定制描述");
                        return toolBuilder;
                    });
                })
                .toolRegistry(registry -> {
                    registry.register(
                            "getWeather",
                            Tool.fromMethod(ReflectionUtils.findMethod(
                                    WeatherService.class, "getWeather", String.class)),
                            weatherService
                    );
                })
                .build();
    }
}

6.2 混合使用MCP工具与本地工具

java 复制代码
/**
 * 同时使用MCP工具和本地工具的ChatClient
 */
@Configuration
public class HybridToolConfig {

    @Bean
    public ChatClient hybridChatClient(
            ChatModel chatModel,
            ToolCallingManager toolCallingManager,
            McpSyncClient mcpClient,  // MCP客户端
            LocalOrderTool localOrderTool) {  // 本地工具

        // 从MCP客户端获取远程工具
        List<ToolCallback> mcpTools = mcpClient.listTools().stream()
                .map(this::convertToToolCallback)
                .toList();

        // 合并本地工具和MCP工具
        List<ToolCallback> allTools = new ArrayList<>();
        allTools.add(localOrderTool);
        allTools.addAll(mcpTools);

        ToolCallAdvisor advisor = ToolCallAdvisor.builder()
                .toolCallingManager(toolCallingManager)
                .build();

        return ChatClient.builder(chatModel)
                .defaultAdvisors(advisor)
                .build()
                .mutate()
                .tools(allTools.toArray(new ToolCallback[0]))
                .build();
    }

    private ToolCallback convertToToolCallback(McpTool mcpTool) {
        return new AbstractToolCallback() {
            @Override
            public ToolDefinition getToolDefinition() {
                return ToolDefinition.builder()
                        .name(mcpTool.name())
                        .description(mcpTool.description())
                        .inputSchema(mcpTool.inputSchema())
                        .build();
            }

            @Override
            public String call(String toolInput, ToolContext context) {
                return mcpClient.callTool(mcpTool.name(), toolInput);
            }
        };
    }
}

七、最佳实践清单

7.1 工具定义最佳实践

  1. 描述要精准:使用「功能+触发场景」的描述模式
  2. Schema要完整 :必填参数必须标记在required
  3. 枚举要明确 :使用enum限制可选值范围
  4. 单位要注明:金额、时间等必须说明单位

7.2 ToolContext最佳实践

  1. 使用Map传参:遵循Spring AI 2.0的设计理念
  2. 过滤null值:避免序列化问题
  3. 敏感信息脱敏:不传递不必要的敏感数据
  4. 上下文预热:高频使用的上下文提前构建

7.3 工具解析策略选择

  1. 工具少用显式:10个以内工具直接指定
  2. 工具多用隐式:50个以上使用动态加载
  3. 缓存要到位:避免频繁解析
  4. 异常要明确:工具未找到时给出清晰提示

7.4 ToolCallAdvisor配置

  1. 简单场景用默认:启用内部历史即可
  2. 复杂场景用外部:需要自定义内存时禁用
  3. 顺序要正确:ToolCallAdvisor在前,ChatMemory在后
  4. session要管理:注意会话隔离

7.5 生产环境注意事项

  1. 超时控制:工具执行要有超时限制
  2. 重试机制:网络调用要支持重试
  3. 监控告警:工具调用失败要能及时发现
  4. 日志记录:保留完整的调用日志便于排查
  5. 熔断降级:依赖的外部服务要有熔断机制
java 复制代码
/**
 * 生产级工具调用配置
 */
@Configuration
public class ProductionToolConfig {

    @Bean
    public ChatClient productionChatClient(
            ChatModel chatModel,
            ToolCallingManager toolCallingManager,
            MeterRegistry meterRegistry) {

        // 配置重试机制
        RetryTemplate retryTemplate = RetryTemplate.builder()
                .maxAttempts(3)
                .fixedBackoff(1000)
                .retryOn(Exception.class)
                .build();

        // 配置超时
        ToolExecutionProperties timeout = new ToolExecutionProperties();
        timeout.setExecutionTimeout(Duration.ofSeconds(30));

        ToolCallAdvisor advisor = ToolCallAdvisor.builder()
                .toolCallingManager(toolCallingManager)
                .conversationHistoryEnabled(true)
                .toolExecutionExceptionProcessor(exception -> {
                    // 异常转换:返回友好错误信息
                    log.error("工具执行失败: {}", exception.getMessage());
                    return JsonResult.error("服务暂时不可用,请稍后重试");
                })
                .build();

        return ChatClient.builder(chatModel)
                .defaultAdvisors(advisor)
                .build();
    }
}

结语

Spring AI 2.0的Tool Calling体系经过精心设计,为企业级AI应用提供了坚实的技术基础。从底层的ToolDefinitionToolCallback抽象,到中间的ToolContext传参模式,再到高阶的ToolCallAdvisor配置,每一层都体现了对工程实践的深刻理解。

在实际项目中,建议遵循「渐进式复杂」的原则:从最简单的显式工具调用开始,在真正需要时才引入动态工具、隐式解析等高级特性。同时,务必重视工具描述的质量和Schema的规范性------好的工具定义是AI准确调用的前提。

生产环境中,超时控制、重试机制、监控告警等非功能性需求同样不可或缺。希望本文能够帮助你在项目中更好地落地Spring AI Tool Calling。

相关推荐
阿达_优阅达2 小时前
告别手工对账:xSuite 如何帮助 SAP 企业实现财务全流程自动化?
服务器·数据库·人工智能·自动化·sap·企业数字化转型·xsuite
旗讯数字2 小时前
生产业纸质加工单识别结构化方案,破解车间数字化痛点——旗讯数字
人工智能·数字化·表格识别
大任视点2 小时前
AI赋能线下娱乐新风口:上海潮玩鸟“智能弹珠机”全国市场正式启动
人工智能·业界资讯
人工智能AI技术2 小时前
算力涨价自救:CPU本地部署MiMo-V2-Pro,极简工程化方案
人工智能
华农DrLai2 小时前
什么是Prompt工程?为什么提示词的质量决定AI输出的好坏?
数据库·人工智能·gpt·大模型·nlp·prompt
阿里云大数据AI技术2 小时前
检索的终局是决策:OLAP 如何重塑 Hologres 多模混合检索的价值边界
人工智能
老纪的技术唠嗑局2 小时前
给 OpenClaw 装上长期记忆:PowerMem 1.0.0 正式发布
人工智能
土豆.exe2 小时前
OpenClaw 安全保险箱怎么做?从 ClawVault 看 AI Agent 的原子化控制、检测与限额
人工智能·网络安全·ai安全·openclaw
wuguan_2 小时前
Halcon图像处理
图像处理·人工智能·计算机视觉·halcon