大模型再能聊,它本身算不了实时库存、查不了数据库。要让它"动手",得靠 Function Calling:模型决定调哪个函数、填什么参数,Java 侧真实执行,再把结果喂回去。这趟趟过的坑,比想象中深。
工具注册:把 Java 方法暴露给模型
用 Spring AI(1.0 GA 稳定版)注册一个工具很简单,加 @Tool 注解,方法签名就是模型看到的"能力描述":
@Tool(description = "根据商品ID查询实时库存数量")
public int queryStock(@ToolParam(description = "商品ID") String skuId) {
return stockService.getAvailable(skuId);
}
描述写得好不好,直接决定模型调不调得对。description 不是给程序看的,是给模型理解的——我一开始写"查询库存",模型经常错调成"下单",改成"查询某商品当前可售库存数量,不含已锁定"后,准确率明显上去。
参数校验:模型也会胡填
模型生成的参数不保证合法。一次它给 skuId 填了 "123" 还带了单位 "123件",下游解析炸了。必须在执行前校验:
public int queryStock(String skuId) {
if (skuId == null || !skuId.matches("^\\d{6,12}$")) {
throw new ToolArgumentException("skuId 应为6-12位数字,收到: " + skuId);
}
return stockService.getAvailable(skuId);
}
关键是把校验失败的异常转回给模型,而不是吞掉。模型看到"参数不合法"的反馈,会自己纠正重试,这正是 Function Calling 比普通问答强的地方——它能根据执行结果迭代。
执行结果回填:片段而非全量
库存返回就一个数字,简单。但某次"查用户订单列表"返回了 2000 条 JSON,塞回 prompt 直接撑爆上下文,还让模型跑偏。我们约定工具返回"给模型看的摘要"和"给系统用的完整数据"分离:
// 回填给模型的,是精炼过的一句话
return ToolResponse.of("该用户最近30天有3笔订单,状态:已发货2、待付款1");
// 完整数据另存,供后续步骤(如支付)直接取,不再让模型复述
回填内容要短、要结构化("字段:值"),模型才好用。把原始大对象直接丢回去,既费 token 又容易出错。
错误处理:三层防御
- 参数层:如上,校验不合法直接回退提示;
- 执行层:下游超时、降级。库存服务挂了,工具返回"库存服务暂不可用,建议稍后再问",而不是抛 500 让整个对话崩;
- 模型层:模型调了不存在的工具或填了无法修复的参数,捕获后返回可控话术,并询问用户澄清。
我们加了一个总超时:单个工具执行超过 3 秒就中断,返回"操作超时"。否则慢工具会卡住整个对话流,用户以为 AI 死了。
一次真实排错
模型反复调 queryStock 却总拿不到正确值。查日志发现,它把"iPhone 15"当 skuId 传了进来——它没意识到需要先 searchSkuByName 拿到 ID。修复是在工具链里补一个"按名称搜 SKU"的工具,并在 queryStock 描述里写明"需先通过名称搜索获取 skuId"。这揭示了 Function Calling 的隐藏成本:工具之间的依赖和顺序,得靠描述设计清楚,否则模型会在错误的工具间打转。
调用链追踪
工具调用次数一多,排障要靠追踪。我们在每次工具调用前后打结构化日志(见可观测性那篇),记录工具名、入参(脱敏后)、耗时、结果摘要。一次模型反复调同一工具却总拿不到正确值,靠这条日志快速定位是提示词没说清依赖顺序。没有追踪,这种"模型打转"问题极难发现,只会笼统归咎于"模型不行"。
留个问题
关于《Function Calling 实战:让大模型调用 Java 方法》里这个坑,你当时是怎么处理的?欢迎在评论区聊聊你踩过的类似情况。