上一篇讲 RAG,我们解决了"模型不知道业务知识怎么办":先检索相关资料,放进 Context,再让模型有依据地回答。
但知道怎么做,不等于真的去做。用户问"怎么申请开门",客服可以解释规则;用户说"帮我把门打开",生成一句"已经打开了",门并不会因此打开。
同样的问题也出现在查订单、取消预约和发送邮件里:LLM 负责生成内容,为什么 AI 应用却能操作外部系统?
这篇就把中间那条连接拆开讲。我们先认识工具,再看模型怎样提出调用,最后用我的自习室智能客服串起一次开门操作。

复习第一篇的总架构图,LLM的AI模型推理返回了结果内容,但是实际并没有操作我们任何功能,最后还是要本地应用执行工具调用。如果没有工具调用,结果只是字符串,AI模型没有调用任何外部能力。
一、AI 能做事,是因为应用接上了执行能力
先说结论:
模型提出调用请求,应用程序执行工具,业务系统返回实际结果。
模型可以理解"帮我开门"的意思,也可以生成"调用开门工具,手机号是某个值"这样的请求。就是说LLM等AI模型预测返回了结果内容,但是实际并没有操作我们任何功能,最后还是要连接业务系统、检查预约、请求门禁接口,依然是程序在做。
Tool Calling 增加的,是自然语言到业务调用之间的一层转换,而不是让模型直接控制了所有外部系统。

从后端开发的角度看,可以把调用请求想成一张"待处理的业务单":上面写着动作和参数,但处理是否获准、最后是否成功,必须由接单的程序判断。
所以,"模型返回调用开门工具"和"门禁返回开门成功"是两个不同事件。就像前端提交支付请求,不代表支付已经完成。
本文聚焦应用自定义的业务工具。有些平台还提供由平台执行的内置搜索等工具,执行位置会不同;但"生成请求"和"执行动作"的区别仍然存在。
二、Tool 是什么,模型怎样知道有哪些工具?
Tool 就是应用开放的一项外部能力。它背后可以是一个 Java 方法,也可以是封装好的 HTTP API。以函数形式提供工具时,经常会看到 Function Calling 这个名称。
普通程序调用函数,通常由开发者写好的分支决定函数名和参数。接入模型后,则可以让模型根据用户表达,提出该调用哪个已开放的工具。
不过,模型不会自动扫描你的 Service,也不知道哪个方法能开门。你得先给它一本"工具说明书"。
| 工具的两面 | 包含什么 | 谁使用 |
|---|---|---|
| 工具说明 | 名称、用途、参数约定 | 模型据此选择工具、生成参数 |
| 工具实现 | 业务代码、接口调用、结果处理 | 应用负责执行和控制边界 |
例如,开门工具可以这样描述:
json
{
"type": "function",
"function": {
"name": "openDoor",
"description": "用户请求开门时使用,需要预约手机号",
"parameters": {
"type": "object",
"properties": {
"tel": {
"type": "string",
"description": "用户提供的预约手机号"
}
},
"required": ["tel"]
}
}
}
这里的名称告诉模型"调用谁",描述告诉模型"什么时候用",参数约定告诉模型"还需要什么信息"。
这份说明没有执行代码。 模型不需要知道门禁接口地址、设备凭证,也不需要阅读整个后端工程。应用只暴露业务能力的入口,内部实现仍由程序持有。
工具描述也不是越宽泛越好。"处理用户请求"很难让模型判断边界,而"用户请求开门时使用,需要预约手机号"就清楚得多。好的工具应表达一个明确的业务动作。
下文的 JSON 与 Java 代码均为根据客服场景简化的教学示例;手机号是示例值,代码片段不构成独立可运行工程。为突出机制,统一调用对象用 ToolInvocation(name, arguments) 表示,这是本文的教学抽象。
三、模型怎样提出调用:原生工具调用与约定 JSON
上面总结了模型LLM层只是返回了结果内容,并没有真正执行操作。最后怎么执行呢?
"给模型提供工具"之后,还要解决另一个问题:程序怎样识别模型是在正常回答,还是请求执行一个动作?
这里有两种常见接法。它们的区别首先在于调用请求放在哪里、格式由谁约定 ,而不是执行权交给谁。
有些AI模型API接口是支持返回工具调用的,有些没有。
对于新接入,如果模型和接口支持原生工具调用,我会优先使用它;约定 JSON 则作为兼容接入或理解机制的方案。它们能接到同一个执行层,但不意味着协议支持和维护成本完全相同。
1. 原生工具调用:接口提供专门的调用结构
如果所使用的模型及接口API支持原生工具调用,应用可以在请求的 tools 中提供工具定义,接口则通过专门的响应结构表达调用请求。
以 tool_calls 这种接口形式为例,响应中的关键片段如下:
json
{
"tool_calls": [
{
"id": "call_001",
"type": "function",
"function": {
"name": "openDoor",
"arguments": "{\"tel\":\"13800000000\"}"
}
}
]
}
这相当于模型交出一张调用单:"请执行 openDoor,参数是这个手机号。"其中 arguments 是 JSON 字符串,需要继续解析,不能当作已经执行过的结果。
注意两个名字的区别:请求中的 tools 是可用工具目录;响应中的 tool_calls 是本次调用请求。 它们不是同一份数据。
字段名也不是所有模型通用。例如 Claude 接口使用 tool_use 内容块表达工具调用,接入时应读取对应接口的定义,而不是在所有响应里寻找 tools。官方工具使用说明
支持工具调用也不意味着每轮都调用工具。在允许模型自主选择的配置下,普通咨询可以直接回答,缺少信息时可以追问,需要外部能力时才提出调用。
2. 兼容方式:约定 JSON,再解析调用意图
如果当前接入没有使用原生工具调用接口,也可以在提示词里约定:需要执行动作时,按指定 JSON 格式回复;普通回答使用另一种消息类型。
例如,约定 action 表示消息用途,name 表示工具名,arguments 保存独立参数:
json
{
"action": "tool_call",
"name": "openDoor",
"arguments": {
"tel": "13800000000"
}
}
这里的 JSON 是模型普通回复的正文,不是接口自动提供的工具调用字段。程序先解析正文,再判断 action,最后取出函数名和参数。
不需要操作时,可以返回 action: "answer" 和回答内容。这样的区分,是为了避免把普通说明误判为执行指令。
这不是临时杜撰的做法。LangChain 的经典 Structured Chat Agent 就通过提示词约定 JSON 的 action 和 action_input,表达工具名与输入。它证明这条工程路径有真实先例,但不代表它是今天所有新项目的首选。LangChain 官方示例
本文使用 action、name、arguments 是为了简单清晰,并不是引用一个统一行业标准。专业的做法,是把它明确当作应用层调用协议,而不是把自定义字段包装成模型原生能力。
这条路能把已有业务能力接到普通对话模型上,但格式识别的责任更多落在应用侧。模型可能夹带解释文字、漏字段,或者生成不在约定范围内的动作,程序必须明确拒绝或要求重新生成。
即使使用了 JSON 输出约束,也不能把"能够生成合法 JSON"等同于"能够可靠地选择工具"。语法正确、参数符合约定、业务允许执行,是三件不同的事。
3. 两种响应,进入同一个执行入口
原生方式按接口结构解析,约定 JSON 方式按应用协议解析。两条路径都要检查工具名与参数,再统一成:
text
ToolInvocation
name = openDoor
arguments = { tel: "13800000000" }

这样,后面的业务执行层不必关心上游响应来自哪个模型,也不需要在每个业务函数里分别处理两种 JSON。
这种分层带来的实际价值是:替换模型或调整消息格式时,主要修改模型适配部分;开门、查预约等业务逻辑仍然保持自己的职责边界。
原生调用也需要解析和校验,只是调用信号由接口明确表达。若要把结果回传模型,适配层还应保留调用 ID 和原始调用消息,不能在统一参数时把这些关联信息丢掉。
四、把调用单变成业务动作:我的智能客服开门案例
我的自习室智能客服既处理规则咨询,也需要帮用户开门(开门时还要校验是否有预约)、协助预约。接入工具的意义,是把用户的自然语言接到已有业务服务上。

图中展示操作型请求的简化路径;RAG 标注聚焦检索准备,后续 LLM 完成生成。并不是每条咨询都需要执行工具。
可以用下面这段对话理解处理过程:
用户:帮我开一下门。
客服:请提供预约手机号。
用户:13800000000。
应用:根据调用请求处理预约与门店信息,并请求门禁接口。
客服:根据业务返回状态提示结果。

前两轮解决的是意图和参数问题。有了必要信息,模型才能提出开门请求;程序收到请求后,再决定它是否符合实际业务条件。
执行层最核心的工作,是把已经检查过的工具名称映射到固定的业务入口:
java
// 代码片段:请求已完成格式、工具名和参数检查
String name = invocation.name();
Map<String, Object> arguments = invocation.arguments();
switch (name) {
case "openDoor":
String tel = (String) arguments.get("tel");
return industryService.openDoor(
tel, request.getAccountType(), arguments, response
);
default:
throw new IllegalArgumentException("不支持的工具");
}
这里没有执行模型生成的代码,也没有根据任意文本反射调用项目方法。模型只能提出工具目录内的动作,Java 负责把动作接到受控的业务服务。
沿着开门服务往下看,还要处理预约信息、门店定位和门禁接口。把这些规则留在业务服务里,才能让"聊天开门"和其他开门入口复用同一套业务判断。
例如,手机号只是查找预约的输入,不应该由模型凭空生成门店 ID,更不能由一句"用户说有预约"代替预约系统的数据。模型负责提取表达,业务系统负责提供事实。
工具执行之后,回复可以有两种处理方式:
- 直接返回业务提示。 如果结果已经明确,例如预约不存在或接口返回操作成功,应用可以组织确定的提示发给用户。
- 把结果交回模型。 如果需要结合上下文解释或汇总,应用保留原调用消息,并按接口要求关联调用 ID 回传结果,再请求模型生成回答。
第二种方式中,模型应依据工具结果组织语言,不能把失败改写成成功。第一种方式也并不缺少 Tool Calling:真实动作已经由工具完成,没有必要为了形式完整再调用一次模型。
这个案例的关键不是"模型学会了门禁协议",而是模型输出调用单,程序把调用单接到业务服务,最后用实际结果闭合这次请求。
五、工具怎么接进来:直接接入与 MCP 接入
前面讲的是模型怎样表达调用请求。现在把视线往后移一步:应用已经拿到工具名和参数,怎样连接真正的工具?
1. 直接接入:由应用自己连接业务函数或 API
最直观的方式就是前面的 Java 分发:应用在自己的代码里维护工具与函数的映射,执行时直接调用 Service,或者通过适配代码请求外部 HTTP API。
这里的"直接"描述的是接入方式,不代表所有能力都在本机。一个本地 Java 方法也可以继续请求远程门禁服务。
2. MCP 接入:通过统一协议发现和调用工具
另一种方式,是由 MCP Server 暴露工具,应用通过 MCP Client 获取工具说明,并发起工具调用。Server 再连接具体业务能力,返回结果。
应用把获取到的工具说明适配给模型;模型提出调用后,应用再把它转成对应的 MCP 调用。MCP 接通的是工具,不是让模型越过应用直接操作业务系统。
MCP Server 可以在本地运行,也可以部署在远程。因此,准确的对比是"直接接入与 MCP 接入",不是"本地工具与 MCP 工具"。MCP 官方架构说明

这两组概念属于不同层次:
| 要解决的问题 | 可采用的方式 |
|---|---|
| 模型怎样表达"我要调用工具" | 原生工具调用,或约定 JSON |
| 应用怎样连接并执行工具 | 直接接入函数/API,或通过 MCP 接入 |
因此,原生工具调用可以连接本地函数,也可以连接 MCP 工具;约定 JSON 在完成适配后,同样可以交给这两种执行方式。
本篇只讲清这层关系,不把"使用 MCP"当作更高级的必选项。单个业务系统可以直接接入;需要在多个应用间复用工具时,再考虑协议化接入的价值和成本。
六、让工具执行可靠,关键仍在程序
看到这里,工具调用并不神秘。真正需要认真设计的,是从"模型提出请求"到"程序允许执行"之间的边界。
第一,参数不足,先补齐。 用户只说"帮我开门",不能为了凑齐 JSON 就猜一个手机号。模型可以追问,程序也必须拦住缺少必填参数的调用。
第二,参数完整,不等于已经授权。 手机号格式正确,不代表它属于当前用户;能找到预约,也不意味着任意会话都可以操作。身份、资源归属和业务条件要由后端检查,必要的确认也不能只写在提示词里。
第三,请求已发出,不等于操作成功。 接口拒绝时应返回失败;超时且无法确认状态时,应说明结果待确认,不能直接宣称成功或无条件重试。涉及外部状态的操作还需要防重复、必要确认和操作记录。
这三条并不是 AI 独有的要求,而是业务接口原本就需要承担的责任。调用方换成模型之后,这些责任不会消失。
再与上一篇连起来看:RAG 把相关资料放入 Context,帮助模型理解和回答;Tool Calling 则把模型提出的动作连接到业务执行。检索本身也能被封装成工具,两者不是互斥关系。

蓝色区域强调检索增强生成,橙色节点强调按需执行。知识文档是示例,图中不展开普通回答和失败处理分支。
回到标题,AI 应用之所以能调用外部能力,不是因为一段回答自动变成了动作,而是因为应用建立了这条明确的连接:
工具说明让模型知道能请求什么,调用结构让程序知道要处理什么,业务实现决定能否执行以及实际结果。
下一篇继续讲 MCP:当工具需要被多个应用复用时,它怎样统一发现与调用,又和普通 API 有什么区别。