已完成联网核对:OpenAI 官方函数调用文档、DeepSeek Responses/Tool Calls 文档、阿里云百炼 Java SDK 示例、Spring AI 2.0 工具调用参考、LangChain4j 工具文档,均在近期更新。以下按你的固定模板成稿。
Function Calling 这件事,本质上是模型把 “我想调哪个函数、传什么参数” 写成一段结构化 JSON 交给你,真正执行代码的是你的 Java 服务,模型从头到尾都没有碰过你的数据库。想跑通它,只需要守住一条闭环:请求里带 tools 定义 → 模型返回函数名和 arguments → Java 端校验参数并执行 → 把结果以工具消息回传 → 模型生成最终答复或继续下一轮调用。Java 落地有三条路线:自己手写 HTTP 循环(控制力最强、排障最直接)、Spring AI 2.0 的 @Tool + ToolCallingAdvisor(声明式、循环托管)、LangChain4j 的 AI Services + @Tool(两层抽象、动态工具灵活)。2026 年这三个变化值得注意:strict: true 的结构化输出已成主流厂商标配,工具调用默认并行,MCP 让远程工具和本地 @Tool 共用同一套 ToolCallback 接口。
一、一次工具调用到底发生了什么
官方文档把这条链路拆成五步,缺任何一步都拿不到最终答案:
| 步骤 | 发起方 | 关键内容 |
|---|---|---|
| 1 | 你的 Java 应用 | 在请求中带 tools 数组,每个工具含 name、description、parameters(JSON Schema) |
| 2 | 模型 | 返回工具调用,含调用 id、函数名、arguments(是 JSON 字符串,不是对象) |
| 3 | 你的 Java 应用 | 解析参数、校验、分发到本地方法执行 |
| 4 | 你的 Java 应用 | 以工具角色消息回传结果,必须带对应的调用 id |
| 5 | 模型 | 结合工具输出生成自然语言答复,或再发起下一批调用 |
两套 API 的形状不一样,混着记最容易出 bug:
| 对比项 | Chat Completions | Responses API |
|---|---|---|
| 工具定义位置 | tools[].function.{name,description,parameters} | tools[] 顶层直接放 name、parameters |
| 模型返回 | message.tool_calls[],含 id / function.name / function.arguments | function_call item,含 call_id / name / arguments |
| 结果回传 | role: "tool" + tool_call_id | function_call_output item + call_id |
| 多轮衔接 | 自己维护完整 messages | 可用 previous_response_id 续接 |
DeepSeek 的文档里有一句提醒很值得贴在代码注释上:模型并不总是生成合法 JSON,还可能虚构出你 schema 里没定义的参数,所以执行前必须校验。
二、模型眼里的 “工具”,其实就是一段 JSON Schema
工具描述写得好不好,直接决定模型会不会在该调的时候调、调得对不对。几条硬经验:
- name 要唯一且稳定,别用中文、别带空格;OpenAI 与 DeepSeek 都限制在 128 字符内,通义千问的 DashScope 限制更紧,工具名最长 64 字符。
- description 写 “什么时候用”,不要写 “这是什么”。写 “用户询问物流进度、发货时间、退款到账时调用”,比写 “订单查询工具” 命中率高得多。
- parameters 只放模型必须填的字段,能用枚举就别用自由文本。
- 开 strict: true 时,Schema 必须落在厂商支持的子集内,否则请求直接报 400。
跨厂商 strict 子集的公共部分,可以按这张表自查:
| Schema 特性 | strict 模式下的处理 |
|---|---|
| 根节点 | 必须是 type: "object",不能是数组或 anyOf |
| 每个 object | 必须写 additionalProperties: false |
| required | 所有属性都要列进 required |
| 可选字段 | 用 "type": ["string","null"] 模拟,字段仍留在 required 里 |
| oneOf / allOf / not / if-then-else | 不支持,改用带判别字段的 anyOf |
| prefixItems(元组数组) | 不支持,改用同构数组 items |
| pattern / format / minLength | 多数厂商 strict 模式不支持,别指望它做格式约束 |
| minimum / maximum / minItems | 同上,校验交给 Java 端 |
| 嵌套深度 | 不超过 5 层 |
| 属性总数 | 不超过 100 个 |
还要分清 JSON mode 和 Structured Outputs:JSON mode 只保证 “能解析”,不保证符合你的 Schema;strict: true 才保证参数严格匹配。另外 Azure 与 OpenAI 都明确写了,结构化输出与并行工具调用不兼容,要开 strict 就把 parallel_tool_calls 设为 false。
三、三条 Java 路线先摆在一起看
| 路线 | 代表写法 | 循环谁管 | 适合场景 | 主要代价 |
|---|---|---|---|---|
| 手写 HTTP | 官方 Java SDK / HttpClient + Jackson | 自己 | 多厂商适配、需要精细排障、轻量服务 | 参数校验、重试、轮次控制全自己写 |
| Spring AI 2.0 | @Tool、MethodToolCallback、FunctionToolCallback | ToolCallingAdvisor | Spring Boot 体系内、要 MCP 与可观测性 | 2.0 有破坏性变更,旧写法要迁移 |
| LangChain4j | ToolSpecification + ToolExecutor、AI Services | AI Service 内部 | 工具动态装配、多智能体编排 | 概念多,需理解两层抽象 |
四、路线 A:不依赖框架,自己把循环跑通
这条路线最能看清原理,代码骨架就四段:定义工具、发请求、分发执行、回传结果。
record ToolCall(String id, String name, String arguments) {}
for (int turn = 0; turn < 6; turn++) {
var resp = client.chat(messages, tools); // 带上 tools 定义
var msg = resp.choices().get(0).message();
if (msg.toolCalls() == null || msg.toolCalls().isEmpty()) return msg.content();
messages.add(msg); // 先放模型这条带 tool_calls 的消息
for (var call : msg.toolCalls()) {
var args = mapper.readValue(call.function().arguments(), WeatherArgs.class); // 校验后再用
var result = dispatcher.invoke(call.function().name(), args);
messages.add(toolMessage(call.id(), result)); // 必须带回同一个 id
}
}
真正决定这套代码能不能上生产的,不是循环本身,而是循环外围的六件事:
| 防护点 | 具体做法 |
|---|---|
| 参数校验 | Jackson 反序列化 + Jakarta Validation 双重校验,非法参数直接返回结构化错误,不要执行 |
| 轮次上限 | 硬编码最大轮次(经验值 5–8),超限返回 “需要人工介入”,防止死循环烧钱 |
| 幂等 | 写操作类工具用业务单号做幂等键,模型重试同一调用不会重复扣款、重复下单 |
| 超时与重试 | 单工具超时独立设置,只对读操作重试,写操作不自动重试 |
| 权限隔离 | 工具按租户、角色做白名单,别把所有工具无差别暴露给所有会话 |
| 审计 | 记录调用 id、函数名、入参、出参、耗时,便于回溯模型为什么这么调 |
还有一条容易忽略:工具返回的内容对模型来说是 “不可信输入”。如果工具会读到外部网页、用户上传文本,返回前要过滤指令性内容,否则等于给提示注入开了后门。
五、路线 B:Spring AI 2.0 的声明式写法
在 Spring 体系里,最快的方式是给方法打注解,框架自动生成 JSON Schema 并托管整个循环。
class OrderTools {
@Tool(description = "按订单号查询订单状态,用户问物流、发货、退款进度时调用")
OrderStatus query(@ToolParam(description = "订单号,形如 SO20261113001") String orderNo,
ToolContext ctx) {
return repo.find(orderNo, ctx.getContext().get("tenantId")); // 租户信息不透给模型
}
}
String answer = ChatClient.create(chatModel)
.prompt("帮我看看 SO20261113001 到哪了")
.tools(new OrderTools())
.toolContext(Map.of("tenantId", "acme"))
.call().content();
几个必须知道的点:
- @Tool 可以打在 public、package-private、private 方法上,静态方法也行;name 默认取方法名,同一批工具里不能重名。
- 参数默认全是必填,要可选就加 @ToolParam(required = false) 或 @Nullable。
- ToolContext 里的东西只到工具,不会进模型上下文,适合放租户 ID、用户 ID、追踪号。
- returnDirect = true 表示工具结果直接返回给调用方,不再绕回模型,适合检索类工具,省一次往返。
- 循环由 ToolCallingAdvisor 负责,它是递归 advisor,每轮重新进入下游链;DefaultChatClient 只允许链上存在一个 ToolAdvisor,注册第二个会直接报错。
- 2.0 移除了 SpringBeanToolCallbackResolver 和 toolNames(),裸 Function Bean 按名字解析的写法没了,工具必须注册成显式的 ToolCallback Bean,用 FunctionToolCallback.builder("currentWeather", svc::getWeather).inputType(WeatherRequest.class) 构建。
- 想让模型 “先说出理由再调工具”,用 AugmentedToolCallbackProvider 给 Schema 追加一个 innerThought 字段,模型填、你的消费者接,工具本身收到的参数不变。
- 方法的参数和返回类型别用 Optional、CompletableFuture、Mono/Flux、Function,这些在工具调用链路里不被支持;函数式工具用 POJO 做入参,不要直接塞基本类型或 List/Map。
- MCP 工具走 @McpTool / @McpToolParam,和本地 @Tool 共用 ToolCallback 接口,可以混着传,但重名冲突框架不管,本地和远程同名要自己改名或用过滤器挡掉。
六、路线 C:LangChain4j 的两层抽象
LangChain4j 把选择权交给你:底层能精确控制 Schema 和执行器,高层用接口把一切都藏起来。
| 层次 | 组成 | 典型用法 |
|---|---|---|
| 低层 | ChatModel + ToolSpecification + ToolExecutor | 工具定义来自数据库或配置中心,运行时装配 |
| 高层 | AI Services + @Tool / @P | 声明一个接口,方法上打注解,框架自动转 ToolSpecification |
低层写法是 “一个定义配一个执行器”:ToolSpecification.builder().name(...).description(...).parameters(JsonObjectSchema...) 配 ToolExecutor,再包成 AiServiceTool 传给 AiServices;不想手写执行器就用 DefaultToolExecutor 反射调用 Java 方法。高层写法更省事:
interface Assistant { String chat(String msg); }
class OrderTools {
@Tool("按订单号查询订单状态")
String query(@P("订单号") String orderNo) { ... }
}
Assistant a = AiServices.builder(Assistant.class)
.chatModel(model).tools(new OrderTools()).build();
它的差异化能力集中在 “按次装配” 上:
- ToolProvider 每次调用都会被问一次 “这次要给哪些工具”,可以按 userMessage 内容判断,只有用户提到 “预订” 才挂订单工具,工具集再大也不会把上下文撑爆。
- InvocationParameters 和 @ToolMemoryId 能把用户 ID、会话 ID 这类隐藏参数送进工具,模型看不见。
- 一个 AI Service 可以直接当另一个 AI Service 的工具,RouterAgent 挂三个专家服务做分流就是官方示例;代价是多一次模型往返,且子服务读不到父服务的聊天记忆。
- 工具多到一定程度,配 toolSearchStrategy(new SimpleToolSearchStrategy()),配合 toolProvider 还能把 MCP 工具一起纳入检索。
选型上不用纠结:已经在 Spring Boot 里、想要自动配置和可观测性,选 Spring AI;要做复杂编排、多智能体、工具按租户动态装配,LangChain4j 更顺手。两者都支持流式、结构化输出和多模态,文档质量 Spring AI 略好,模型覆盖都很广。
七、多厂商差异:别把 OpenAI 的写法直接搬过去
同一个 tools 字段,各家脾气不同:
| 厂商 | 值得注意的差异 |
|---|---|
| OpenAI | 工具调用需走 Responses API(新模型已不支持 Chat Completions 调工具);strict 与并行调用不能同时开 |
| DeepSeek | strict 属 Beta,要把 base_url 指到 /beta 才生效;parallel_tool_calls 参数会被忽略,并行始终开启;思考模式下必须把 reasoning_content 原样回传,漏了直接 400 |
| 通义千问(DashScope) | 工具名最长 64 字符;description 必填;用 tools 时 result_format 必须设为 message;tool_choice=required 在 Qwen 系列不支持 |
| Gemini(LangChain4j 接入) | 用 toolConfig 控制,ANY 模式可指定允许调用的函数名列表,支持并行调用 |
最常踩的坑是思考模式的思维链回传。DeepSeek 的要求很明确:只要请求带了 tools,后续每一轮都要把上一轮的 reasoning_content 完整带回,包括那些没有发生工具调用的轮次。Java 里如果用 record 或 DTO 承接响应,很容易只映射 content 和 tool_calls,把 reasoning_content 丢掉,表现就是第二轮莫名 400。
八、上线前的验收清单
| 检查项 | 通过标准 |
|---|---|
| Schema 合法性 | 每个工具单独跑一遍 strict 子集校验,根节点是 object、全字段 required、additionalProperties:false |
| 参数健壮性 | 传入非法 JSON、缺字段、多字段、超长字符串,工具都不应执行 |
| 循环终止 | 构造一个永远要求调工具的模型响应,验证轮次上限能兜住 |
| 并行调用 | 一次返回 3 个工具调用时,结果按 id 正确配对,不串行阻塞 |
| 权限 | 普通租户无法触发管理员专属工具 |
| 成本 | 统计单次会话的工具轮次与 token 消耗,设预算告警 |
| 可观测 | 每次调用能查到函数名、入参、出参、耗时、调用 id |
验收通过之后,把工具当成对外 API 来治理:版本化、限流、灰度、下线公告。模型只是这些 API 的第一个调用者,不是唯一调用者。
【相关 FAQ】
问题 1:Java 项目做 Function Calling,一定要引入 Spring AI 或 LangChain4j 吗?
回答 1:不必。Function Calling 的协议层只是 HTTP 加 JSON,用官方 Java SDK 或 HttpClient 配 Jackson 就能跑通完整循环,适合工具少、要多厂商适配的服务;引入 Spring AI 或 LangChain4j 的价值在于托管循环、自动生成 Schema、统一本地工具与 MCP 工具。
问题 2:Java 实现 Function Calling 时,模型返回的 arguments 为什么必须校验?
回答 2:arguments 是模型生成的 JSON 字符串,官方文档明确说它不保证合法、也可能带上 Schema 里没定义的字段;Java 端要先反序列化再用 Bean Validation 校验,校验不过就返回结构化错误让模型重试,绝不能直接把参数拼进 SQL 或下游请求。
问题 3:Spring AI 的 @Tool 和 LangChain4j 的 @Tool 有什么区别,Java 项目怎么选?
回答 3:两者都是把 Java 方法转成 Function Calling 的工具定义,差别在循环和装配方式:Spring AI 由 ToolCallingAdvisor 托管循环、工具必须注册成 ToolCallback Bean,适合 Spring Boot 体系;LangChain4j 有 ToolSpecification 低层 API 和 ToolProvider 按次装配,适合工具动态变化和多智能体编排。
问题 4:开了 strict 模式,Java 端拿到的 Function Calling 参数就一定符合 Schema 吗?
回答 4:参数结构会严格贴合你提交的 JSON Schema,但前提是 Schema 本身落在厂商支持的子集内,否则请求会直接报错;另外 strict 只保证形状,不保证业务语义正确,像金额范围、订单号是否存在,仍然要在 Java 工具里做二次校验。
问题 5:Java 接入 Function Calling 后工具越来越多,上下文放不下怎么办?
回答 5:两条路。一是按会话动态收窄工具集,LangChain4j 用 ToolProvider 按用户消息判断该挂哪些工具,Spring AI 用 McpToolFilter 挡掉不该暴露的远程工具;二是上工具检索,只把与当前问题最相关的少数工具塞进请求,LangChain4j 的 SimpleToolSearchStrategy 就是干这个的。
【参考资料】
- [2026-11-10](DashScope API 参考:tools 字段与 tool_choice 取值)阿里云百炼
- [2026-11-05](How to implement tool calling by using Function Calling)阿里云帮助中心
- [2026-10-31](Responses API 参考:function_call、call_id 与 tools 定义)DeepSeek API 文档
- [2026-10-26](Tool Calling:@Tool、MethodToolCallback 与 ToolCallingAdvisor)Spring AI Reference
- [2026-10-21](Tool Calls:strict 模式与 JSON Schema 支持范围)DeepSeek API 文档
- [2026-10-19](工具调用:ToolContext、直接返回与工具定义方式)Spring AI 中文参考
- [2026-10-01](OpenWOP Spec v1 Tier 1 Structured-Output Compatibility Subset)GitHub
- [2026-09-14](工具(函数调用):ToolSpecification、AI Services 与 ToolProvider)LangChain4j 中文文档
- [2026-08-25](How to use structured outputs with Azure OpenAI in Microsoft Foundry Models)Microsoft Learn
- [2026-08-11](ツール(関数呼び出し):ツール検索と ToolProvider)LangChain4j 日本語ドキュメント
- [2026-06-15](Tool Calling in Spring AI 2.0: A Composable, Agentic Architecture)Spring Blog
- [2026-04-24](Open Responses Specification:tool_choice 与 allowed_tools)Open Responses
- [2026-03-24](Function Calling in the OpenAI API)OpenAI Help Center
【免责声明】
本文为技术原理与工程实践整理,接口字段、参数限制与版本行为均可能随各家官方文档更新而变化,落地前请以对应厂商最新文档为准。文中代码为示意性片段,省略了异常处理、鉴权、日志与并发控制,直接用于生产环境前请自行补全并充分测试。本文不构成任何投资建议、产品选型结论或对特定厂商的推荐,据此操作的风险由使用者自行承担。
