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抽象层,其中最核心的两个接口是ToolDefinition和ToolCallback。理解这两个接口的设计理念,是掌握整个工具调用体系的前提。
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会自动维护工具调用过程中的会话历史,包括:
- 用户消息
- AI响应(包含tool_call请求)
- 工具执行结果
- 最终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 工具定义最佳实践
- 描述要精准:使用「功能+触发场景」的描述模式
- Schema要完整 :必填参数必须标记在
required中 - 枚举要明确 :使用
enum限制可选值范围 - 单位要注明:金额、时间等必须说明单位
7.2 ToolContext最佳实践
- 使用Map传参:遵循Spring AI 2.0的设计理念
- 过滤null值:避免序列化问题
- 敏感信息脱敏:不传递不必要的敏感数据
- 上下文预热:高频使用的上下文提前构建
7.3 工具解析策略选择
- 工具少用显式:10个以内工具直接指定
- 工具多用隐式:50个以上使用动态加载
- 缓存要到位:避免频繁解析
- 异常要明确:工具未找到时给出清晰提示
7.4 ToolCallAdvisor配置
- 简单场景用默认:启用内部历史即可
- 复杂场景用外部:需要自定义内存时禁用
- 顺序要正确:ToolCallAdvisor在前,ChatMemory在后
- session要管理:注意会话隔离
7.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应用提供了坚实的技术基础。从底层的ToolDefinition和ToolCallback抽象,到中间的ToolContext传参模式,再到高阶的ToolCallAdvisor配置,每一层都体现了对工程实践的深刻理解。
在实际项目中,建议遵循「渐进式复杂」的原则:从最简单的显式工具调用开始,在真正需要时才引入动态工具、隐式解析等高级特性。同时,务必重视工具描述的质量和Schema的规范性------好的工具定义是AI准确调用的前提。
生产环境中,超时控制、重试机制、监控告警等非功能性需求同样不可或缺。希望本文能够帮助你在项目中更好地落地Spring AI Tool Calling。