本文介绍如何在 Spring AI 中使用工具调用(Tool Calling / Function Calling):让大模型在对话中按需调用你定义的 Java 方法(查员工、查请假、获取当前日期等),并根据工具返回结果生成回复。本文基于 chat-model 模块,通过 @Tool 注解和 ChatClient.defaultTools() 实现员工与日期相关的工具调用。
源代码
如果您想自己尝试,可以查看我的源代码。为此,您必须克隆我的示例 GitHub 仓库 。然后,您只需按照我的说明操作即可。
为什么需要工具调用
大模型只能基于训练数据和当前对话生成文本,无法直接查数据库、调接口、算日期。工具调用 的做法是:
- 定义工具:用 Java 方法实现「查员工」「查请假」「获取当前日期」等能力,并用 @Tool 注解标注,让 Spring AI 生成工具描述供模型使用。
- 注册到 ChatClient:通过 defaultTools(…) 把这些工具注册到对话客户端。
- 模型决策:用户提问时,模型根据问题决定是否调用工具、调用哪个工具、传什么参数;框架执行工具后把结果返回给模型,模型再生成最终回复。
这样,对话既能用自然语言,又能触发真实业务逻辑(查库、写库等),适合客服、内部助手、数据查询等场景。
本示例默认使用 OpenAI,并通过 Maven Profile 切换 Anthropic 与 Ollama。工具调用无需额外专用依赖。
项目结构概览
- Application:Spring Boot 入口。
- ChatToolController:
POST /api/chat/tool,接收用户输入,调用带工具的 ChatClient,返回模型回复。 - EmployeeTools:员工相关工具(查员工、查请假、申请请假),内部调用 EmployeeService。
- DateTimeTools:日期工具(获取当前日期)。
- Employee / EmployeeService:员工实体与内存模拟数据(emp1001、emp1002、emp1003 等)。
定义工具:@Tool
用 @Tool 注解标注方法,并写清 description,模型会根据描述决定何时调用以及传什么参数。方法参数名和类型会参与生成工具的 schema(如 empId、date)。
员工工具示例(EmployeeTools):
@Servicepublic class EmployeeTools { private final EmployeeService employeeService;
@Tool(description = "Get employee details for a given employee id") public Employee getEmployee(String empId) { return employeeService.getEmployee(empId); }
@Tool(description = "Find employees who are on leave for a given date in YYYY-MM-DD format") public List<Employee> findEmployeesOnLeave(LocalDate date) { return employeeService.findEmployeesOnLeave(date); }
@Tool(description = "Apply leave for a given employee id and date in YYYY-MM-DD format") public void applyLeave(String empId, LocalDate date) { employeeService.applyLeave(empId, date); }}日期工具示例(DateTimeTools):
class DateTimeTools { @Tool(description = "Get the current date in the ISO-8601 format yyyy-MM-dd") String getCurrentDate() { return LocalDate.now().toString(); }}- 参数类型如 LocalDate、String 会被序列化/反序列化,模型传入的字符串(如
"2025-01-01")会转换为对应类型。 - description 中写清「做什么、参数含义、格式」(如 YYYY-MM-DD),有助于模型正确选择工具和填参。
注册工具并配置 ChatClient
在构造 ChatClient 时通过 defaultTools(…) 注册一个或多个工具类实例,并可配合 defaultSystem(…) 设定助手角色,让模型知道「只根据工具返回的数据回答」:
@RestController@RequestMapping("/api/chat/tool")class ChatToolController { private final ChatClient chatClient;
ChatToolController(ChatClient.Builder builder, EmployeeTools employeeTools) { this.chatClient = builder .defaultSystem(""" You are a helpful assistant for our company. You always respond based on the data you have from tools available to you. If you don't know the answer, you will respond with "I don't know". """) .defaultTools(employeeTools, new DateTimeTools()) .defaultAdvisors(new SimpleLoggerAdvisor()) .build(); }
@PostMapping Output chat(@RequestBody @Valid Input input) { String response = chatClient.prompt(input.prompt()).call().content(); return new Output(response); }}- defaultTools(employeeTools, new DateTimeTools()):注册员工工具和日期工具,模型在对话中可自动选择调用。
- defaultSystem(…):设定系统提示,约束模型依据工具结果回答,不知道时说 “I don’t know”。
- defaultAdvisors(new SimpleLoggerAdvisor()):可选,便于在日志中查看请求/响应与工具调用。
一次用户提问可能触发多轮「模型 → 调用工具 → 返回结果 → 模型再生成」;框架会处理好工具调用的往返,最终返回给接口的是模型的最后一轮文本回复。
运行与验证
本示例默认使用 OpenAI,亦可通过 Maven Profile 切换 Anthropic 与 Ollama。请先设置对应的环境变量:
export OPENAI_API_KEY=<your-openai-api-key>export ANTHROPIC_API_KEY=<your-anthropic-api-key>运行命令示例:
- OpenAI(默认):
mvn spring-boot:run - Anthropic:
mvn spring-boot:run -Panthropic - Ollama:
mvn spring-boot:run -Pollama
我这里使用 DeepSeek 模型启动应用
export DEEPSEEK_API_KEY=<your-deepseek-api-key>mvn spring-boot:run -Dspring-boot.run.profiles=deepseek启动后进行测试:
# 获取当前日期(会调用 DateTimeTools.getCurrentDate)curl -s -X POST http://localhost:8080/api/chat/tool \ -H "Content-Type: application/json" \ -d '{"prompt":"What is today'\''s date?"}'
# 查询员工(会调用 EmployeeTools.getEmployee)curl -s -X POST http://localhost:8080/api/chat/tool \ -H "Content-Type: application/json" \ -d '{"prompt":"Get details of employee id emp1002"}'
# 查询某天请假员工(会调用 findEmployeesOnLeave)curl -s -X POST http://localhost:8080/api/chat/tool \ -H "Content-Type: application/json" \ -d '{"prompt":"Which employees are on leave on 2025-01-01?"}'
# 申请请假(会调用 applyLeave)curl -s -X POST http://localhost:8080/api/chat/tool \ -H "Content-Type: application/json" \ -d '{"prompt":"Apply leave for employee id emp1001 on 2025-04-01"}'经测试发现出现以下异常:
org.springframework.ai.retry.NonTransientAiException: HTTP 400 - {"error":{"message":"Missing `reasoning_content` field in the assistant message at message index 2. For more information, please refer to https://api-docs.deepseek.com/guides/thinking_mode#tool-calls","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}在当前的场景( DeepSeek R1/V3 深度思考模式 + Function Calling 工具调用 )下,直接使用标准 OpenAI 协议客户端(如 Spring AI 1.1.2 的 OpenAiChatModel )会失败。
原因如下:
1. 核心差异:reasoning_content 字段
DeepSeek 的深度思考模型(R1)在返回结果时,会将“思考过程”和“最终回答”分开:
- OpenAI 标准协议 :通常只包含 content 字段。
- DeepSeek 扩展协议 :包含 reasoning_content (思维链) + content (最终回答)。
2. 工具调用(Tool Calling)的特殊性
在多轮对话中,尤其是涉及工具调用时,客户端必须将 完整的对话历史 发送回服务器,以便模型了解上下文。
- DeepSeek 的强制校验 :DeepSeek API 规定,如果上一轮助手(Assistant)的回复中包含了 reasoning_content ,那么在下一轮请求(比如提交工具执行结果)时, 必须 在历史消息中原样带回这个 reasoning_content 字段。
- OpenAI 协议客户端的“丢包”行为 :
- Spring AI 的 OpenAiChatModel 是强类型的。
- 它接收到 JSON 响应时,只映射它认识的字段(即标准的 content ), 会自动忽略并丢弃 它不认识的 reasoning_content 。
- 当它再次构建历史消息发送给 DeepSeek 时,发送的消息里就只剩下了 content ,丢失了 reasoning_content 。
3. 导致的后果
由于历史消息中丢失了 reasoning_content ,DeepSeek 服务器在校验上下文时会发现数据不完整,从而抛出 HTTP 400 错误:
Missing 'reasoning_content' field in the assistant message...解决办法:
1、引入 spring-ai-starter-model-deepseek 依赖
<profile> <id>deepseek</id> <dependencies> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-deepseek</artifactId> </dependency> <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-retry</artifactId> </dependency> </dependencies></profile>2、 application.properties 添加配置
spring.ai.deepseek.api-key=${DEEPSEEK_API_KEY}spring.ai.deepseek.chat.options.model=deepseek-reasoner3、重写 DeepSeekChatModel,在构建发送给 API 的消息列表时,增加了对 DeepSeekAssistantMessage 的类型检查和字段提取
// 修改前(逻辑缺失):// 只传递 content,忽略了 reasoning_content
// 修改后(补丁逻辑):String reasoningContent = null;// 检查消息是否为 DeepSeek 专用消息类型if (message instanceof DeepSeekAssistantMessage) { // 提取 reasoning_content reasoningContent = ((DeepSeekAssistantMessage) message).getReasoningContent();}
// 在构造 ChatCompletionMessage 时传入 reasoningContentreturn List.of(new ChatCompletionMessage( assistantMessage.getText(), ChatCompletionMessage.Role.ASSISTANT, null, null, toolCalls, isPrefixAssistantMessage, reasoningContent // <--- 关键修复:透传该字段));4、使用 deepseek Maven profile 启动应用
mvn spring-boot:run -Pdeepseek小结
- 工具调用 = 用 @Tool 定义方法 + ChatClient.defaultTools(…) 注册 + 模型在对话中自动选择工具与参数,框架执行后把结果交回模型生成回复。
- description 要写清工具用途和参数格式(如日期 YYYY-MM-DD),便于模型正确调用。
- 本示例通过 EmployeeTools(查员工、查请假、申请请假)和 DateTimeTools(当前日期)演示 Spring AI Tool Calling 的完整流程;可将工具替换为你的业务逻辑(查库、调 API 等)即可扩展为实际应用。