一、为什么需要工具调用?
大模型很强,但它并不是万能的。
在普通问答场景中,我们可以直接问模型:
text
请解释一下什么是 Spring AI。
请总结一下这份医学影像报告。
请帮我生成一段产品介绍。
这类问题主要依赖模型已有知识和语言生成能力。
但是,一旦进入真实业务系统,我们很快会遇到另外一类问题:
text
今天的天气怎么样?
当前订单状态是什么?
这位患者最近一次检查结果是什么?
请帮我创建一个质控任务。
请查询数据库中某个病例的历史报告。
请调用系统接口生成一条工单。
这类问题只靠模型自己是无法完成的。
原因很简单:
text
模型不知道实时数据;
模型不能直接访问数据库;
模型不能直接调用业务系统;
模型不能替代权限校验;
模型不能自己执行真实操作。
这时就需要工具调用。
工具调用的核心思想是:
text
模型负责理解用户意图和生成调用参数;
应用程序负责执行真实工具或业务接口;
工具结果再交给模型生成最终回答。
也就是说,工具调用把大模型从"只会回答问题"扩展成了"可以协助完成任务"。
对于生产级 AI 应用来说,这是从聊天机器人走向业务 Agent 的关键能力。
二、什么是 Tool Calling?
Tool Calling,也叫 Function Calling。
在 Spring AI 中,可以把 Tool 理解成应用系统暴露给模型使用的一组能力。
这些能力可以是:
text
查询数据库;
调用 HTTP 接口;
访问文件系统;
调用搜索服务;
创建业务记录;
发送消息;
触发工作流;
查询用户权限;
调用第三方系统。
例如,一个医学影像报告质控系统中,可以定义这些工具:
text
查询患者基本信息;
查询历史检查;
查询报告模板;
查询质控规则;
创建质控任务;
保存质控结果;
推送人工审核;
查询 AI 检测结果。
模型本身不会直接访问这些系统。
模型只会判断:
text
我现在需要调用哪个工具?
这个工具需要哪些参数?
工具返回后,我该如何组织最终回答?
真正执行工具调用的是 Spring 应用。
这点非常重要。
因为在生产系统中,权限、安全、审计、事务、异常处理都必须由应用系统掌控,不能交给模型自由执行。
三、工具调用主要解决两类问题
Spring AI 官方文档中提到,工具主要用于两类场景:信息检索和执行动作。
1. 信息检索类工具
这类工具用于获取模型本身不知道的信息。
例如:
text
查询当前时间;
查询天气;
查询数据库记录;
查询文件内容;
查询知识库;
查询患者历史检查;
查询订单状态;
查询用户权限。
比如用户问:
text
明天是星期几?
如果模型没有当前时间,它就无法准确回答。
这时可以提供一个工具:
java
class DateTimeTools {
@Tool(description = "获取用户所在时区的当前日期和时间")
String getCurrentDateTime() {
return LocalDateTime.now()
.atZone(LocaleContextHolder.getTimeZone().toZoneId())
.toString();
}
}
然后在调用模型时提供这个工具:
java
String response = ChatClient.create(chatModel)
.prompt("明天是星期几?")
.tools(new DateTimeTools())
.call()
.content();
模型发现自己需要当前时间,就会请求调用 getCurrentDateTime 工具。
工具返回当前时间后,模型再基于这个结果回答用户。
2. 执行动作类工具
这类工具用于让系统执行某个真实操作。
例如:
text
创建工单;
发送邮件;
设置提醒;
提交表单;
创建数据库记录;
触发审核流程;
生成报告任务;
推送通知。
比如用户说:
text
帮我创建一个报告质控任务。
模型可以理解用户意图,但它不能自己写数据库。
所以我们可以提供一个工具:
java
class QcTaskTools {
private final QcTaskService qcTaskService;
QcTaskTools(QcTaskService qcTaskService) {
this.qcTaskService = qcTaskService;
}
@Tool(description = "根据报告ID创建医学影像报告质控任务")
QcTaskResult createQcTask(
@ToolParam(description = "报告ID") Long reportId) {
return qcTaskService.createTask(reportId);
}
}
这样,模型可以根据用户请求生成 reportId 参数,由应用执行创建任务逻辑。
执行完成后,工具结果再返回给模型,模型再告诉用户:
text
已为该报告创建质控任务,任务编号为 QC-20260603-001。
四、工具调用的整体流程
工具调用的流程可以理解为六步:
text
1. 应用把可用工具定义发送给模型;
2. 模型判断是否需要调用工具;
3. 如果需要,模型返回工具名称和参数;
4. Spring 应用根据工具名称找到对应 ToolCallback;
5. 应用执行工具,并获得工具结果;
6. 工具结果返回给模型,模型生成最终回答。
也可以用更工程化的方式理解:
text
用户问题
↓
ChatClient + Tools
↓
模型判断需要调用工具
↓
返回 toolName + arguments
↓
Spring AI 执行 ToolCallback
↓
工具结果返回模型
↓
模型生成最终回答
这里要特别注意:
text
模型只是发起工具调用请求;
工具执行权始终在应用程序手里。
这也是工具调用在生产环境中可控的基础。
五、Spring AI 中的核心概念
Spring AI 的工具调用主要涉及几个核心概念。
1. Tool
Tool 是暴露给模型使用的能力。
在代码中,可以是一个普通 Java 方法,也可以是函数式对象,最终都会被封装为 ToolCallback。
最常见的方式是使用 @Tool 注解。
java
class DateTimeTools {
@Tool(description = "获取当前日期和时间")
String getCurrentDateTime() {
return LocalDateTime.now().toString();
}
}
2. ToolCallback
ToolCallback 是 Spring AI 对工具的统一抽象。
不管底层工具来自:
text
Java 方法;
Function;
Supplier;
Consumer;
自定义实现;
Spring Bean;
动态解析器。
最终都可以被抽象成 ToolCallback,供模型调用。
3. ToolDefinition
ToolDefinition 用来描述工具。
它通常包括:
text
工具名称;
工具描述;
输入参数 Schema。
模型依赖这些信息判断工具该不该调用,以及如何构造参数。
4. ToolCallingManager
ToolCallingManager 负责管理工具执行生命周期。
它的职责包括:
text
解析工具定义;
判断工具调用请求;
查找工具;
执行工具;
处理工具返回结果;
处理工具异常。
如果使用 Spring AI 默认配置,大多数情况下不需要手动操作 ToolCallingManager。
但在复杂 Agent 或需要自定义执行流程时,可以进一步扩展它。
六、使用 @Tool 声明工具
最简单的方式是使用 @Tool 注解。
例如:
java
import org.springframework.ai.tool.annotation.Tool;
import org.springframework.ai.tool.annotation.ToolParam;
class PatientTools {
private final PatientService patientService;
PatientTools(PatientService patientService) {
this.patientService = patientService;
}
@Tool(description = "根据患者ID查询患者基本信息")
PatientInfo getPatientInfo(
@ToolParam(description = "患者ID") Long patientId) {
return patientService.findById(patientId);
}
}
这里有两个关键点。
1. @Tool 的 description 很重要
description 不是给开发者看的,而是给模型看的。
模型会根据 description 判断:
text
这个工具是干什么的;
什么时候应该调用;
调用前需要哪些条件;
返回结果可以用于什么。
所以,工具描述不能太随意。
不建议这样写:
java
@Tool(description = "查询")
更推荐写成:
java
@Tool(description = "根据患者ID查询患者基本信息,包括姓名、性别、年龄和患者编号")
描述越清楚,模型越不容易误用工具。
2. @ToolParam 要说明参数含义
工具参数也要写清楚。
例如:
java
@Tool(description = "根据检查ID查询医学影像检查信息")
StudyInfo getStudyInfo(
@ToolParam(description = "检查ID,来自业务系统中的 studyId") Long studyId) {
return studyService.findById(studyId);
}
如果参数格式有要求,也要写明。
例如:
java
@Tool(description = "设置用户提醒")
void setReminder(
@ToolParam(description = "提醒时间,格式为 ISO-8601,例如 2026-06-03T10:30:00")
String time) {
reminderService.setReminder(LocalDateTime.parse(time));
}
对于模型来说,参数说明越明确,生成正确参数的概率越高。
七、把工具提供给 ChatClient
定义好工具后,可以在调用 ChatClient 时传入:
java
String response = ChatClient.create(chatModel)
.prompt("请查询患者 1001 的基本信息")
.tools(new PatientTools(patientService))
.call()
.content();
这种方式表示:
text
这个工具只在当前请求中可用。
这通常是更安全的做法。
因为不是所有工具都应该在所有场景中暴露给模型。
例如:
text
查询工具可以开放给某些问答场景;
写入工具只应该开放给明确授权的操作场景;
删除工具一般不建议直接暴露;
高风险工具必须增加人工确认。
八、defaultTools:默认工具要谨慎使用
Spring AI 也支持在 ChatClient.Builder 中配置默认工具:
java
ChatClient chatClient = ChatClient.builder(chatModel)
.defaultTools(new DateTimeTools())
.build();
这样,所有基于这个 ChatClient 的请求都可以使用这些工具。
这对通用工具很方便,例如:
text
获取当前时间;
查询当前用户语言;
查询系统帮助文档;
查询公开知识库。
但默认工具也有风险。
如果把高权限工具设置成 defaultTools,例如:
text
删除数据;
修改订单;
创建付款;
发送外部消息;
变更患者报告状态。
模型可能在不合适的场景下调用它们。
所以生产环境建议:
text
低风险、通用、只读工具可以考虑 defaultTools;
高风险、写操作、强业务权限工具应按请求动态传入;
涉及删除、提交、付款、审批等动作时应增加二次确认。
九、工具调用不等于让模型拥有权限
这是生产级工具调用最重要的原则之一。
模型不能替代权限系统。
例如用户说:
text
帮我删除这个患者的检查记录。
即使模型判断应该调用删除工具,也不能直接执行。
应用必须先做权限判断:
java
@Tool(description = "删除指定检查记录")
DeleteResult deleteStudy(
@ToolParam(description = "检查ID") Long studyId,
ToolContext toolContext) {
Long userId = (Long) toolContext.getContext().get("userId");
if (!permissionService.canDeleteStudy(userId, studyId)) {
throw new SecurityException("当前用户无权删除该检查记录");
}
return studyService.delete(studyId);
}
这里模型只是辅助理解用户意图。
真正的权限判断必须由业务系统完成。
换句话说:
text
模型可以建议调用工具;
但系统必须决定是否允许调用工具。
十、ToolContext:给工具传递上下文
有些信息不应该让模型看到,但工具执行时又需要用到。
例如:
text
tenantId;
userId;
角色信息;
登录机构;
数据权限范围;
当前业务对象ID;
请求来源。
这些信息适合通过 ToolContext 传给工具。
示例:
java
class CustomerTools {
@Tool(description = "查询客户信息")
Customer getCustomerInfo(Long id, ToolContext toolContext) {
String tenantId = (String) toolContext.getContext().get("tenantId");
return customerRepository.findById(id, tenantId);
}
}
调用时传入上下文:
java
String response = ChatClient.create(chatModel)
.prompt("请查询客户 42 的信息")
.tools(new CustomerTools())
.toolContext(Map.of("tenantId", "acme"))
.call()
.content();
这样有两个好处:
text
模型不需要知道 tenantId 等敏感上下文;
工具执行时仍然可以使用这些上下文做权限和数据隔离。
在多租户系统中,ToolContext 非常重要。
十一、Return Direct:工具结果是否直接返回?
默认情况下,工具执行结果会先返回给模型,模型再结合结果生成最终回答。
流程是:
text
工具执行结果
↓
返回模型
↓
模型加工
↓
返回用户
但有些场景不需要模型再次加工。
例如:
text
工具已经返回标准 JSON;
工具返回的是文件下载地址;
工具返回的是固定格式业务结果;
工具结果应该直接交给前端处理。
这时可以使用 returnDirect = true。
java
class CustomerTools {
@Tool(description = "查询客户信息", returnDirect = true)
Customer getCustomerInfo(Long id) {
return customerRepository.findById(id);
}
}
这样工具结果会直接返回给调用方,而不是再交给模型生成回答。
生产环境中可以这样选择:
text
需要自然语言解释:返回给模型再生成回答;
需要标准业务数据:returnDirect;
需要前端直接渲染:returnDirect;
需要防止模型篡改工具结果:returnDirect。
十二、工具返回结果要可序列化
工具方法可以返回对象,但返回结果最终需要转换成字符串,发送给模型或调用方。
因此,工具返回对象应尽量简单、稳定、可序列化。
例如:
java
public record PatientInfo(
Long patientId,
String name,
String gender,
Integer age
) {
}
不建议直接返回复杂的 ORM 实体对象。
例如:
java
PatientEntity
├── studies
├── reports
├── images
├── hospital
├── doctors
└── ...
这样容易带来几个问题:
text
返回内容过大;
包含敏感字段;
循环引用导致序列化失败;
模型接收到大量无关信息;
Token 成本过高。
建议为工具调用专门设计 DTO。
原则是:
text
只返回模型完成当前任务所需的最小信息。
十三、工具调用和 RAG 的区别
工具调用和 RAG 经常一起出现,但它们不是一回事。
| 能力 | 核心作用 | 典型场景 |
|---|---|---|
| RAG | 从知识库检索上下文 | 根据文档回答问题 |
| Tool Calling | 调用外部函数或业务能力 | 查数据库、调接口、执行业务动作 |
例如医学影像报告质控系统:
text
RAG:
检索"胸部CT报告质控规则"。
Tool Calling:
查询患者性别、检查部位、历史检查、报告状态。
可以这样组合:
text
用户提交报告
↓
RAG 检索相关质控规则
↓
Tool 查询患者信息和检查信息
↓
Chat Model 综合分析
↓
结构化输出质控结果
↓
Tool 保存质控结果或创建复核任务
RAG 解决的是"模型依据什么知识回答"。
工具调用解决的是"模型如何访问或操作业务系统"。
十四、工具调用和结构化输出的关系
工具调用用于让模型调用外部能力。
结构化输出用于让模型返回可解析结果。
二者经常组合使用。
例如:
text
工具调用:查询患者信息、报告信息、历史检查。
结构化输出:返回风险等级、问题列表、复核建议。
示例对象:
java
public record ReportQcResult(
Boolean passed,
String riskLevel,
List<QcProblem> problems,
String suggestion
) {
}
public record QcProblem(
String type,
String description,
String evidence
) {
}
调用过程可以是:
java
ReportQcResult result = chatClient.prompt()
.user("""
请结合患者信息、检查信息和报告内容进行质控分析。
如果缺少患者信息,可以调用工具查询。
""")
.tools(new PatientTools(patientService),
new StudyTools(studyService))
.call()
.entity(ReportQcResult.class);
这样系统既可以让模型使用工具获取必要信息,又能让最终结果以结构化对象返回。
这就是生产级 AI 系统常见的组合方式。
十五、框架托管执行和用户控制执行
Spring AI 支持两种工具执行方式。
1. 框架托管执行
这是默认方式。
也就是:
text
模型请求调用工具;
Spring AI 自动执行工具;
工具结果自动返回模型;
模型生成最终结果。
大多数场景使用这种方式就够了。
它的优点是:
text
代码简单;
开发效率高;
适合普通工具调用;
适合快速集成业务系统。
2. 用户控制执行
如果你需要完全控制工具调用过程,可以关闭内部工具执行。
例如:
java
ChatOptions chatOptions = ToolCallingChatOptions.builder()
.toolCallbacks(ToolCallbacks.from(new MathTools()))
.internalToolExecutionEnabled(false)
.build();
这种方式适合复杂场景:
text
需要记录每一步工具调用;
需要人工审批后再执行;
需要多 Agent 协同;
需要自定义工具调用循环;
需要把工具调用过程写入 Chat Memory;
需要对工具参数做二次校验。
简单说:
text
普通业务用框架托管;
复杂 Agent 用用户控制。
十六、ToolCallAdvisor:把工具调用纳入 Advisor 链
Spring AI 还支持通过 ToolCallAdvisor 实现工具调用。
它的价值在于可以把工具调用放进 Advisor 链中,与其他能力协同。
例如:
text
Chat Memory;
RAG Advisor;
日志记录;
权限上下文;
工具调用;
观测追踪。
这种方式更适合复杂生产系统。
因为在真实业务中,工具调用往往不是孤立动作。
例如医学影像报告质控 Agent:
text
读取记忆上下文;
检索质控规则;
查询患者信息;
调用工具;
记录执行日志;
生成结构化结果;
保存审计记录。
这些步骤如果都能进入统一的 Advisor 链,系统会更容易治理和扩展。
十七、异常处理:工具失败时怎么办?
工具调用一定会失败。
例如:
text
数据库连接失败;
接口超时;
权限不足;
参数不合法;
业务数据不存在;
第三方服务不可用。
Spring AI 中,工具执行异常可以由 ToolExecutionExceptionProcessor 处理。
生产环境中需要提前设计失败策略。
1. 查询类工具失败
例如查询患者信息失败,可以返回:
text
未能查询到患者信息,请稍后重试或转人工处理。
也可以让模型基于错误信息继续回答:
text
当前无法获取患者信息,因此不能完成完整质控分析。
2. 写操作工具失败
例如创建质控任务失败,不应该让模型假装成功。
必须明确返回失败:
text
质控任务创建失败,原因:当前用户无权限或服务异常。
3. 高风险工具失败
例如涉及报告状态变更、审核提交、删除数据,建议直接抛异常并进入人工处理。
不要让模型自行补救。
十八、生产级安全建议
工具调用是 AI 应用中最需要重视安全的部分。
因为一旦工具可以写数据库、调接口、发消息,模型就从"生成内容"进入了"影响真实系统"的阶段。
1. 工具最小暴露
不要把所有 Service 方法都暴露给模型。
应该只暴露必要工具:
text
只读工具优先;
写操作谨慎;
删除操作尽量不暴露;
高风险动作必须审批。
2. 工具内部必须做权限校验
不能因为工具是模型调用的,就绕过权限。
每个工具都应该像普通业务接口一样校验:
text
用户身份;
租户范围;
角色权限;
数据权限;
操作权限。
3. 参数必须二次校验
模型生成的参数不一定可靠。
工具执行前必须校验:
text
参数是否为空;
格式是否合法;
枚举值是否正确;
ID 是否存在;
当前用户是否有权访问该 ID。
4. 高风险动作要二次确认
例如:
text
删除数据;
提交审核;
发送消息;
修改报告;
创建付款;
变更状态。
这类动作不建议模型直接执行。
更稳妥的流程是:
text
模型生成操作建议
↓
系统展示待确认内容
↓
用户确认
↓
后端执行工具
↓
记录审计日志
5. 保留审计日志
工具调用必须记录:
text
谁发起;
什么时候发起;
调用了哪个工具;
工具参数是什么;
工具返回什么;
是否成功;
失败原因是什么;
模型最终回答是什么。
对于医疗、金融、政务、企业管理系统,这一点非常关键。
十九、在医学影像报告质控中的实战设计
假设我们要开发一个医学影像报告质控助手。
用户输入:
text
请检查这份报告是否存在性别逻辑问题和图文不一致问题。
系统可以提供以下工具:
java
class ReportQcTools {
@Tool(description = "根据报告ID查询报告内容")
ReportInfo getReportInfo(
@ToolParam(description = "报告ID") Long reportId) {
return reportService.findReportInfo(reportId);
}
@Tool(description = "根据患者ID查询患者基本信息")
PatientInfo getPatientInfo(
@ToolParam(description = "患者ID") Long patientId) {
return patientService.findPatientInfo(patientId);
}
@Tool(description = "根据检查ID查询检查部位和检查类型")
StudyInfo getStudyInfo(
@ToolParam(description = "检查ID") Long studyId) {
return studyService.findStudyInfo(studyId);
}
@Tool(description = "保存报告质控结果")
SaveQcResult saveQcResult(QcResultRequest request) {
return qcResultService.save(request);
}
}
调用方式:
java
ReportQcResult result = chatClient.prompt()
.user("""
请对报告进行质控分析。
必要时调用工具查询报告、患者和检查信息。
分析完成后返回结构化质控结果。
""")
.tools(new ReportQcTools())
.call()
.entity(ReportQcResult.class);
这个流程中,模型可以:
text
理解用户质控意图;
决定需要查询患者信息;
决定需要查询报告内容;
结合工具结果进行推理;
返回质控问题和建议。
但系统仍然要负责:
text
权限控制;
数据脱敏;
工具参数校验;
工具结果过滤;
质控结果落库;
人工复核;
审计追踪。
这样才是生产级工具调用,而不是简单 Demo。
二十、常见误区
误区一:工具调用就是让模型直接调接口
不是。
模型不会直接调接口。
模型只会发出工具调用请求,真正执行的是应用程序。
误区二:工具越多越好
不是。
工具越多,模型越容易选错工具,也会增加安全风险。
应该按场景暴露工具,而不是全量暴露。
误区三:工具描述随便写也可以
不建议。
工具描述越模糊,模型越容易误用。
工具描述应该清楚说明:
text
工具用途;
调用条件;
参数含义;
返回内容;
限制边界。
误区四:模型调用工具后就可以直接相信结果
不能。
工具结果是真实数据,但模型对工具结果的解释仍然可能出错。
关键业务仍然需要结构化校验、规则校验和人工复核。
误区五:Tool Calling 可以替代业务流程
不能。
工具调用只是让模型参与流程编排。
业务流程、权限体系、事务一致性和审计机制仍然属于应用系统。
二十一、生产级最佳实践总结
1. 按场景暴露工具
不同业务场景提供不同工具,不要把所有工具默认开放给模型。
2. 工具描述要清晰
@Tool(description = "...") 和 @ToolParam(description = "...") 要认真写。
它们直接影响模型是否正确调用工具。
3. 工具参数要校验
模型生成的参数不能直接信任。
业务系统必须做二次校验。
4. 权限控制不能省略
工具内部必须做用户权限、租户隔离和数据范围控制。
5. 写操作要谨慎
创建、修改、删除、提交、发送等动作要增加确认和审计。
6. 工具结果要最小化
只返回当前任务需要的信息,避免暴露敏感字段和过大数据。
7. 保留完整调用日志
工具调用是生产级 AI 系统的重要审计对象。
8. 与 Memory、RAG、结构化输出组合使用
工具调用不是孤立能力。
更成熟的 AI 应用通常是:
text
Chat Memory 保持上下文;
RAG 提供知识依据;
Tool Calling 获取或操作业务数据;
Structured Output 返回可解析结果;
Observability 负责追踪和监控。
二十二、总结
Spring AI 的工具调用,是构建生产级 AI 应用和 Agent 系统的关键能力。
它让大模型不再只是回答问题,而是可以在受控范围内使用外部工具,完成查询、检索、创建任务、触发流程等操作。
但工具调用的本质不是"让模型拥有系统权限",而是:
text
让模型提出工具调用意图;
让应用系统执行真实业务能力;
让业务系统控制权限、安全、事务和审计。
在生产实践中,我们要始终记住:
text
模型负责理解和推理;
工具负责连接外部能力;
应用负责安全和治理;
人负责高风险决策确认。
只有这样,工具调用才能真正从 Demo 走向可靠、可控、可维护的生产系统。