Spring AI 1.0 Function Calling 避坑指南
Spring AI 1.0 Function Calling 生产级落地:从踩坑到最佳实践
随着 Spring AI 1.0 GA 版本的正式发布,Java 后端开发者接入大模型的能力有了质的飞跃。其中,Function Calling(函数调用/工具调用) 是让 LLM 从“聊天机器人”转变为“智能 Agent”的核心能力。
然而,在实际生产环境落地过程中,许多团队发现:Demo 跑通了,但一上量就报错;或者模型死活不调用工具;又或者传参格式千奇百怪。
本文将基于 Spring AI 1.0.0-M6/GA 版本,深入剖析 Function Calling 的底层机制,分享我在企业级项目中踩过的坑,并提供一套高可用、可维护的生产级代码范式。
一、 核心概念澄清:Tool vs Function
在 Spring AI 1.0 中,官方逐渐将术语从 Function 向 Tool 迁移(参考 MCP 协议趋势)。虽然 @Bean Function 依然有效,但推荐使用更语义化的 ToolCallback 接口。
为什么推荐 Tool?
- 上下文感知:Tool 可以访问
ChatClientRequestSpec,获取当前会话状态。 - 动态注册:支持运行时根据用户权限动态加载/卸载工具。
- 更好的测试性:解耦了业务逻辑与 Spring Bean 生命周期。
二、 标准实现范式(推荐写法)
很多教程还在使用旧的 @Bean + Function<String, String> 写法,这种方式缺乏类型安全且难以处理复杂对象。
✅ 最佳实践:基于 Record 的类型安全定义
假设我们需要一个“查询实时天气”的工具。
1. 定义输入/输出 DTO
利用 Java Record 自动生成不可变类,配合 Jackson 注解确保 JSON 映射准确。
import com.fasterxml.jackson.annotation.JsonClassDescription;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonPropertyDescription;
@JsonClassDescription("查询指定城市的实时天气信息")
public record WeatherRequest(
@JsonProperty(required = true)
@JsonPropertyDescription("城市名称,例如:北京、上海")
String city,
@JsonPropertyDescription("温度单位,celsius或fahrenheit,默认celsius")
String unit
) {}
public record WeatherResponse(
String city,
double temperature,
String condition,
long timestamp
) {}
2. 实现 ToolCallback
不要直接在 Controller 里写逻辑,封装为独立的 Tool 组件。
import org.springframework.ai.tool.ToolCallback;
import org.springframework.ai.tool.function.FunctionToolCallback;
import org.springframework.stereotype.Component;
@Component
public class WeatherTool implements ToolCallback {
private final WeatherService weatherService; // 你的业务服务
public WeatherTool(WeatherService weatherService) {
this.weatherService = weatherService;
}
@Override
public String getName() {
return "get_current_weather"; // ⚠️ 关键:工具名需全局唯一,建议使用蛇形命名法
}
@Override
public String getDescription() {
return "当用户询问天气情况时,必须调用此工具。仅支持中国境内城市。";
}
@Override
public Class<?> getInputType() {
return WeatherRequest.class;
}
@Override
public Object apply(Object input, ChatMemory chatMemory) {
// 1. 类型转换与安全校验
if (!(input instanceof WeatherRequest request)) {
throw new IllegalArgumentException("Invalid input type for WeatherTool");
}
// 2. 业务逻辑执行
try {
// 模拟耗时操作
return weatherService.query(request.city(), request.unit());
} catch (Exception e) {
// ⚠️ 关键点:永远不要让异常穿透到 LLM,返回友好的错误提示字符串
return "Error: Failed to fetch weather data. Reason: " + e.getMessage();
}
}
}
3. 注册到 ChatClient
@Bean
public ChatClient chatClient(ChatClient.Builder builder, List<ToolCallback> toolCallbacks) {
return builder
.defaultSystem("你是一个有用的天气助手。如果不知道答案,请说不知道,不要编造。")
.defaultTools(toolCallbacks) // 自动注入所有实现的 ToolCallback Bean
.build();
}
三、 生产环境高频踩坑与解决方案
🕳️ 坑位 1:模型“无视”工具描述,拒绝调用
现象:用户问“北京今天多少度?”,模型回答“我无法获取实时数据”。
原因分析:
- Description 太模糊:LLM 需要明确的触发条件。
- Temperature 设置过高:创意模式下模型倾向于自由发挥而非严格遵循指令。
- System Prompt 冲突:系统提示词未强调“优先使用工具”。
✅ 解决方案:
- 优化 Description:采用
When... Use...句式。Bad: "Query weather" Good: "Use this tool when the user asks about current weather conditions, temperature, or forecast for a specific location."
- 调整采样参数:
OpenAiChatOptions options = OpenAiChatOptions.builder() .temperature(0.0) // 工具调用场景建议设为 0 或极低值 .internalToolExecutionEnabled(true) // 确保开启内部执行 .build();
🕳️ 坑位 2:参数解析失败 / JSON Schema 异常
现象:日志报错 Conversion from JSON to ... failed 或 Missing required field。
原因分析:
- 嵌套对象过深:部分小模型对深层嵌套 JSON 处理能力弱。
- 枚举值未明确:LLM 生成了不在允许范围内的字符串。
- 字段名歧义:如
id这种通用字段名,模型可能混淆。
✅ 解决方案:
- 扁平化结构:尽量使用平铺的参数列表,避免复杂的嵌套 Record。
- 显式约束枚举:
@JsonPropertyDescription("Unit of measurement: 'c' for Celsius, 'f' for Fahrenheit") String unit; - 增加 Fallback 逻辑:在
apply方法中捕获反序列化异常,返回具体的字段缺失提示,让 LLM 有机会重试(ReAct 模式的基础)。
🕳️ 坑位 3:长耗时工具导致请求超时
现象:工具执行需要 5-10 秒(如调用外部慢速 API),HTTP 请求直接 Timeout。
原因分析:Spring AI 默认的 HTTP 客户端超时时间较短,且同步阻塞等待结果。
✅ 解决方案:
- 配置 RestClient 超时:
@Bean public RestClient.Builder restClientBuilder() { return RestClient.builder() .requestFactory(new HttpComponentsClientHttpRequestFactory()) .messageConverters(...) // 略 ; } // 并在 application.yml 中配置 spring.http.client.read-timeout=30s - 异步化改造(进阶):对于超过 3 秒的操作,建议将工具设计为“提交任务”,返回 TaskID,再配合另一个“查询任务状态”的工具,由 Agent 轮询。
四、 调试技巧:如何看清 LLM 到底发了什么?
很多时候 Bug 不在代码,而在 Prompt。你需要看到原始的 tool_calls 报文。
1. 开启 WireMock 或 Logging
在 application.yml 中开启 Feign/RestTemplate 日志:
logging:
level:
org.springframework.web.client.RestTemplate: DEBUG
# 如果使用 WebClient
reactor.netty.http.client: DEBUG
2. 自定义 Advisor 拦截请求
编写一个 CallAdvisor 打印发送给 LLM 的完整 Messages 和接收到的 Response。
@Component
public class DebugLoggingAdvisor implements CallAdvisor {
@Override
public AdvisedResponse aroundCall(AdvisedRequest advisedRequest, CallAroundAdvisorChain chain) {
// 打印发送给模型的 System Prompt 和 User Message
log.info("[LLM Request] Tools: {}", advisedRequest.toolNames());
AdvisedResponse response = chain.nextAroundCall(advisedRequest);
// 打印模型返回的原始文本及可能的 Tool Calls
log.info("[LLM Response] Content: {}", response.response().getResult().getOutput().getText());
return response;
}
// ... 其他必要方法实现
}
五、 总结与展望
Spring AI 1.0 的 Function Calling 已经足够成熟,能够支撑大多数企业级场景。要做好它,核心不在于框架本身,而在于:
- Prompt Engineering:像写 API 文档一样写
@Description。 - 防御性编程:永远假设 LLM 会传入非法参数、超长字符串或不存在的 ID。
- 可观测性:必须记录每一次 Tool 调用的入参、出参、耗时和 Token 消耗。
下一步,建议结合 Spring AI Alibaba 或 LangChain4j 探索多步推理(Chain-of-Thought)和自主 Agent 的构建。
互动:你在落地 Spring AI 工具调用时遇到过什么奇葩 Bug?欢迎在评论区留言讨论!
更多推荐

所有评论(0)