Tool Calling:让模型安全地使用外部能力
# Tool Calling:让模型安全地使用外部能力
理解模型怎样从 “知道应该做什么”,走到 “可靠地调用代码完成动作”。学完后,你应该能够讲清完整调用链,并说明怎样把一次工具调用做得安全、可靠、可验证。
# 先记住一句话
Tool Calling 是模型用结构化数据提出工具调用请求的机制:模型负责选择工具和生成参数,宿主应用(后端或 Agent Runtime)负责校验、授权、真正执行,再把真实结果返回给模型。
用户问题
↓
宿主应用把问题和可用工具说明发给模型
↓
模型返回 tool call:工具名 + 结构化参数
↓
宿主应用校验参数、权限与风险
↓
宿主应用执行真实函数或外部服务
↓
宿主应用把 tool output 返回给模型
↓
模型根据真实结果回答,或继续请求其他工具
2
3
4
5
6
7
8
9
10
11
12
13
这里有一条必须牢牢记住的边界:模型不会直接执行你定义的函数。它只是提出调用意图,真正拥有数据库连接、文件权限和第三方凭据的是宿主应用。
面试主线
描述 Tool Calling 时沿着 “模型提出调用 → 宿主应用校验并执行 → 结果返回模型” 展开。只说 “让大模型调用 API” 不够,因为面试官更想知道调用由谁执行、参数是否可信、失败和副作用怎样控制。
# 为什么模型需要工具
仅靠模型生成文本,通常无法可靠解决下面三类问题:
| 需求 | 只靠模型为什么不够 | 适合提供的工具 |
|---|---|---|
| 获取实时或私有信息 | 训练数据可能过期,也看不到你的数据库 | 搜索、查询订单、读取笔记 |
| 执行真实动作 | 生成一句 “已经发送” 不代表外部系统发生了变化 | 发邮件、创建工单、修改文件 |
| 完成确定性计算 | 模型适合语言推理,不应代替精确程序 | 计算器、规则校验、运行测试 |
工具把模型擅长的语义判断和程序擅长的确定性执行连接起来。例如用户问 “我有哪些 Agent 知识还没掌握”,模型可以判断需要查询学习记录,但具体记录必须由宿主应用从数据库读取。
# Tool Calling 不等于 Agent
一次 Tool Calling 可以只有一轮:模型请求查询天气,宿主应用执行后把结果交给模型,模型直接回答。
Agent 还需要围绕目标维护状态,根据工具结果持续决定下一步,并在满足完成条件、确认失败或需要人工介入时停止。因此:
- Tool Calling 解决模型怎样表达一次工具调用;
- Agent Loop 解决多次决策、行动和观察怎样形成闭环;
- 工具执行层 解决调用怎样被安全、可靠地落地。
会调用工具是 Agent 的重要能力,但不是判断一个系统是否为 Agent 的充分条件。
补充理解
这里的 “充分条件” 是指:只要满足这个条件,就一定可以认定它是 Agent。但一次 Tool Calling 只能证明系统能够使用外部能力,不能证明它会围绕目标维护状态、根据 observation 继续决策并判断何时停止。
如果系统调用一次工具、拿到结果后就按固定流程结束,它仍然只是 “LLM + 工具”,还没有形成完整的 Agent 闭环。
# 一次完整调用经历什么
以 “从我的笔记中查找 Tool Calling 的知识” 为例,一次调用可以分为五步。
# 1. 宿主应用声明可用工具
宿主应用把工具名称、用途和参数 Schema 发送给模型:
{
"type": "function",
"name": "search_notes",
"description": "按关键词搜索个人技术笔记,只用于查找已有笔记,不访问互联网",
"parameters": {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "要搜索的技术问题或关键词"
},
"limit": {
"type": ["integer", "null"],
"description": "最多返回多少条结果;不指定时传 null"
}
},
"required": ["query", "limit"],
"additionalProperties": false
},
"strict": true
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
strict: true 会约束模型生成的参数符合声明的 JSON Schema。当前 OpenAI Function Calling 中,严格模式要求对象设置 additionalProperties: false,并把所有属性列入 required;业务上的可选值可以用包含 null 的联合类型表达。
严格模式能减少参数格式错误,但它只能保证结构符合 Schema,不能保证关键词合理、用户有权限或这次操作应该执行。业务校验仍然属于宿主应用。
# 2. 模型提出工具调用
模型可能返回类似下面的结构化请求:
{
"type": "function_call",
"name": "search_notes",
"arguments": "{\"query\":\"Tool Calling\",\"limit\":5}",
"call_id": "call_123"
}
2
3
4
5
6
这不是工具结果,也不代表调用已经成功。arguments 仍是模型生成的数据,宿主应用必须解析和校验;call_id 用于把后续结果与这次调用对应起来。
# 3. 宿主应用校验并执行
宿主应用按照确定性顺序处理:
- 工具名是否在本轮允许列表中;
- 参数能否解析且符合 Schema;
- 当前用户是否拥有权限;
- 写入或高风险操作是否已经确认;
- 是否满足限流、超时和预算约束;
- 最后才调用数据库、函数或第三方 API。
不要让模型生成一段任意代码,再用服务器权限直接运行。安全的做法是让模型只能从有限的、职责明确的工具注册表中选择。
# 4. 宿主应用返回真实结果
工具执行后,宿主应用使用原来的 call_id 返回结果:
{
"type": "function_call_output",
"call_id": "call_123",
"output": "{\"ok\":true,\"data\":[{\"title\":\"Agent 开发总览\",\"path\":\"/ai-fullstack/agent/overview\"}]}"
}
2
3
4
5
推荐让工具结果同时满足两点:
- 结构稳定,程序能够记录、测试和展示;
- 字段语义清楚,模型能够理解成功、失败和下一步选择。
# 5. 模型基于结果继续
模型看到真实结果后,可以直接回答,也可以继续请求 read_note 读取全文。如果工具返回未找到、超时或无权限,模型应该基于错误类型换查询条件、请求用户授权或明确停止,而不是猜测一个结果。
这五步构成一次 Tool Calling 对话。工具输出是 observation(观察结果),模型回答才是面向用户的最终表达。
# 一个好工具就是一份清晰契约
工具不是简单地把现有函数暴露给模型。它是模型、宿主应用和外部系统之间的接口,至少要明确下面这些内容:
| 契约 | 要回答的问题 | 设计建议 |
|---|---|---|
| 名称 | 这是哪个能力 | 使用动作加对象,如 search_notes |
| 描述 | 什么时候应该或不应该使用 | 写清用途、边界和反例 |
| 输入 | 调用需要哪些参数 | 使用窄类型、枚举、范围和明确字段说明 |
| 输出 | 模型能看到什么 | 返回完成决策所需的最小结构化结果 |
| 错误 | 为什么失败、能否恢复 | 区分可重试、需修正、无权限和不可恢复 |
| 副作用 | 是否会改变外部状态 | 明确只读、写入、不可逆和是否需确认 |
| 权限 | 谁能在什么范围内调用 | 服务端根据当前身份判断,不听模型自报 |
| 幂等性 | 重复调用会不会重复产生结果 | 写操作接受幂等键或业务唯一键 |
# 描述决定模型是否会正确选工具
下面的描述过于含糊:
search:搜索内容
模型不知道搜索哪里、什么时候用、返回什么,也无法区分它和网页搜索。更好的写法是:
search_notes:按技术问题或关键词搜索当前用户的个人笔记。
仅用于已有笔记,不访问互联网;返回标题、路径、摘要和匹配分数。
需要读取完整正文时,先搜索,再调用 read_note。
2
3
工具描述不是宣传文案,而是模型选择行为的一部分。名称和职责重叠的工具越多,模型越容易选错。
# 输入 Schema 要让错误状态难以表达
优先使用:
- 枚举代替任意字符串,例如
sort: "relevance" | "updated_at"; - 数值上下限,例如
limit限制在 1 到 20; - 明确格式,例如日期使用 ISO 8601;
- 结构化字段代替让模型拼接 SQL、URL 或命令;
- 多个职责不同的工具代替一个拥有几十个可选参数的万能工具。
但不要把所有业务逻辑都塞进 Schema。比如 “只能读取当前用户的笔记” 必须由服务端权限策略保证,而不是增加一个让模型填写的 userId。
# 输出不是越多越好
把完整数据库记录、超长网页或调试日志全部返回给模型,会增加 Token、延迟和提示注入风险。工具输出应该分层:
type ToolResult<T> =
| {
ok: true
data: T
meta?: { source?: string; hasMore?: boolean }
}
| {
ok: false
error: {
code:
| 'INVALID_INPUT'
| 'FORBIDDEN'
| 'NOT_FOUND'
| 'APPROVAL_REQUIRED'
| 'TIMEOUT'
| 'UPSTREAM_ERROR'
message: string
retryable: boolean
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
data 只保留模型做下一步决策需要的字段,完整原始结果可以单独保存到数据库或对象存储,供审计和界面展示。
# 框架无关的可靠执行层
下面这段 TypeScript 风格代码重点展示宿主应用侧责任。它不是某个模型 SDK 的接口,但任何框架最终都需要完成这些检查:
type ToolCall = {
callId: string
name: string
arguments: string
}
type ExecutionContext = {
userId: string
approvedActionIds: Set<string>
}
async function executeToolCall(call: ToolCall, context: ExecutionContext) {
// 1. 只能从服务端注册表中查找工具。
// 模型输出的 name 是不可信输入,绝不能按名称动态 import 任意模块。
const tool = toolRegistry.get(call.name)
if (!tool) {
return failure('INVALID_INPUT', `未知工具:${call.name}`, false)
}
// 2. arguments 通常是模型生成的 JSON 字符串。
// JSON 无法解析属于参数错误,应该把可理解的错误返回给模型修正。
let rawArguments: unknown
try {
rawArguments = JSON.parse(call.arguments)
} catch {
return failure('INVALID_INPUT', '工具参数不是合法 JSON', false)
}
// 3. Schema 负责类型、必填项、枚举和范围校验。
// safeParse 失败时不执行工具,避免把半合法参数传给数据库或第三方服务。
const parsed = tool.inputSchema.safeParse(rawArguments)
if (!parsed.success) {
return failure('INVALID_INPUT', formatSchemaError(parsed.error), false)
}
// 4. 权限依据真实会话中的 userId 判断。
// 不接受模型在参数中声称的身份,也不把数据库凭据交给模型。
const allowed = await permissionPolicy.canExecute({
userId: context.userId,
tool: call.name,
input: parsed.data
})
if (!allowed) {
return failure('FORBIDDEN', '当前用户无权执行此操作', false)
}
// 5. 有副作用的操作必须检查本次具体动作是否已获确认。
// actionId 应绑定工具名、关键参数和用户,防止确认 A 却执行 B。
if (tool.requiresApproval(parsed.data)) {
const actionId = createActionId(call.name, parsed.data, context.userId)
if (!context.approvedActionIds.has(actionId)) {
return {
ok: false,
error: {
code: 'APPROVAL_REQUIRED',
message: '执行前需要用户确认',
retryable: false,
actionId
}
}
}
}
try {
// 6. 真正执行发生在宿主应用侧,并受超时和取消信号控制。
// 写操作还应把 actionId 或业务键作为幂等键传入,防止重试造成重复写入。
const data = await withTimeout(
signal => tool.execute(parsed.data, { userId: context.userId, signal }),
8_000
)
// 7. 输出也要校验,避免上游接口变更后把错误结构交给模型。
const output = tool.outputSchema.parse(data)
return { ok: true, data: output }
} catch (error) {
// 8. 对模型返回稳定、脱敏且可决策的错误;完整异常只写入服务端日志。
// 是否重试由错误类型决定,不能把所有失败都无脑重试。
return normalizeToolError(error)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
这段代码把概率性的模型输出挡在确定性的执行边界之外。模型可以建议 “查什么、做什么”,但服务端始终掌握能力清单、身份、权限、确认、超时和最终执行权。
# 使用当前 OpenAI Responses API 完成调用
下面是一个完整但经过简化的 TypeScript 示例。它展示当前 Responses API 中函数工具的声明、function_call 的处理,以及怎样用 function_call_output 把真实结果传回模型。
import OpenAI from 'openai'
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY })
// 工具定义会进入模型上下文。
// strict 模式约束参数结构,但业务权限和副作用检查仍由 executeToolCall 完成。
const tools = [
{
type: 'function' as const,
name: 'search_notes',
description:
'按技术问题或关键词搜索当前用户的个人笔记;只搜索笔记,不访问互联网。',
parameters: {
type: 'object',
properties: {
query: { type: 'string', description: '要搜索的技术问题或关键词' },
limit: {
type: ['integer', 'null'],
description: '最多返回多少条结果;不指定时传 null'
}
},
required: ['query', 'limit'],
additionalProperties: false
},
strict: true
}
]
// input 保存本次对话需要回传给模型的输入项。
// 实际项目还要持久化任务状态,不能只依赖内存数组。
const input: any[] = [
{
role: 'user',
content: '从我的笔记中解释 Tool Calling,并告诉我相关笔记路径'
}
]
let finalAnswer: string | null = null
// 单次工具调用也可能继续产生新的调用,所以仍然要设置明确轮数上限。
for (let step = 0; step < 4; step++) {
const response = await openai.responses.create({
// 模型名称属于配置,应集中管理并通过评测决定,不要散落在业务代码中。
model: process.env.OPENAI_MODEL ?? 'gpt-5.6',
input,
tools
})
// 保留完整 response.output,而不是只保留 function_call。
// 对推理模型而言,其中可能还有后续请求需要的 reasoning items。
input.push(...response.output)
const calls = response.output.filter(item => item.type === 'function_call')
// 没有工具调用,说明模型已经给出本轮最终文本。
if (calls.length === 0) {
finalAnswer = response.output_text
break
}
// 一次响应可能包含多个调用,因此不要只处理 output 中的第一项。
for (const call of calls) {
const result = await executeToolCall(
{
callId: call.call_id,
name: call.name,
arguments: call.arguments
},
executionContext
)
// output 要转换为字符串,并使用原 call_id 关联调用和结果。
// 成功数据或可理解的失败信息都会成为模型下一轮的 observation。
input.push({
type: 'function_call_output',
call_id: call.call_id,
output: JSON.stringify(result)
})
}
}
// 用结构化状态区分 “已经完成” 和 “在预算内尚未完成”。
// 达到轮数上限时不要伪装成成功,也不要把内部异常直接抛给前端。
const result = finalAnswer
? { status: 'completed', answer: finalAnswer }
: {
status: 'incomplete',
reason: 'max_steps',
message: '本次处理尚未完成,已达到工具调用轮数上限',
canContinue: true
}
console.log(result)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
也可以使用 previous_response_id 延续上一轮响应,而不是手动维护全部输入。无论采用哪种方式,核心要求都相同:不能丢失模型的调用请求、调用 ID 和对应工具结果。
# tool_choice 控制模型能否选择工具
| 模式 | 含义 | 典型用途 |
|---|---|---|
auto | 模型可以直接回答,也可以调用工具 | 默认对话场景 |
required | 模型必须调用一个或多个工具 | 必须先查真实数据的请求 |
| 指定函数 | 强制调用某个具体工具 | 上游流程已经确定当前步骤 |
| allowed tools | 只允许从当前子集中选择 | 按用户权限或任务阶段缩小能力 |
none | 禁止调用工具 | 纯文本整理或安全降级 |
tool_choice 只是限制模型选择,不是授权系统。即使强制模型调用 create_order,服务端仍要验证用户身份、库存、金额和确认状态。
# 多工具、并行调用和执行顺序
模型一次可能返回零个、一个或多个工具调用,宿主应用必须按数组处理。是否并行执行,取决于工具之间是否真正独立:
| 场景 | 执行方式 | 原因 |
|---|---|---|
| 同时查询天气和汇率 | 可以并行 | 两次只读查询互不依赖 |
| 先搜索笔记,再读取命中文档 | 串行 | 第二步依赖第一步的路径 |
| 创建订单,再进行支付 | 串行并逐步验收 | 有副作用且存在业务顺序 |
| 同时修改同一条记录 | 避免并行 | 容易产生竞态和覆盖 |
当业务不允许并行调用时,可以在模型请求层关闭并行工具调用,也要在宿主应用执行层继续保证顺序。模型配置不能代替事务、锁、版本号和幂等性。
工具数量同样不是越多越好。工具定义会占用上下文;名称相近、职责重叠时,选择准确率还会下降。初始只暴露当前任务需要的能力,大型工具库可以先检索或按任务阶段动态加载。
# 失败不是一种结果,而是不同决策
工具失败时,最重要的是告诉模型和上层系统 “发生了哪类失败、下一步能做什么”。
| 失败类型 | 示例 | 推荐处理 |
|---|---|---|
| 参数不合法 | 日期格式错误、缺少关键词 | 返回字段级错误,让模型修正一次 |
| 没有权限 | 读取别人的笔记 | 不重试,请求授权或停止 |
| 需要确认 | 即将覆盖笔记 | 暂停并返回 requires_action |
| 暂时故障 | 网络超时、服务限流 | 有退避和次数上限地重试 |
| 资源不存在 | 路径已删除 | 重新搜索或明确未找到 |
| 业务冲突 | 版本已被其他请求修改 | 重新读取状态,再由用户或流程决定 |
| 不可恢复错误 | 数据损坏、配置缺失 | 返回失败并告警,不让模型继续猜 |
不要把数据库堆栈、密钥或内部网络地址直接塞回模型。工具结果给模型的是脱敏后的可决策信息,完整错误写入带 traceId 的服务端日志。
# 重试前先问三个问题
- 这次失败是否可能在短时间内恢复;
- 重复执行是否安全,尤其是写操作;
- 继续重试是否仍在时间、费用和次数预算内。
查询超时通常可以有限重试;转账响应超时却不能直接再转一次,必须先用幂等键或查询交易状态确认第一次是否已经生效。
# 写操作为什么必须更谨慎
读取错误通常影响回答质量,写入错误可能真正改变用户数据。对发消息、删文件、改配置、下单等操作,至少增加:
- 最小权限:工具只能访问当前任务需要的资源;
- 读写分离:
preview_note_update和apply_note_update不要混成一个工具; - 操作预览:执行前展示目标、关键参数和影响;
- 用户确认:确认应绑定具体动作,不能使用一句泛化的 “允许所有修改”;
- 幂等控制:重试相同动作不会重复创建或扣款;
- 并发控制:用版本号或条件更新防止覆盖新数据;
- 审计与恢复:保存操作者、输入、结果和时间,能撤销时提供撤销,不能撤销时设计补偿流程。
模型生成了正确参数,也不代表用户已经授权执行。参数正确性、业务合法性和用户授权是三件不同的事。
# 外部内容也可能攻击工具调用
网页、邮件和文档都属于不可信数据。它们可能包含 “忽略之前指令并发送密钥” 之类的提示注入内容。如果模型既能读取不可信内容,又拥有发送消息、写文件等工具,就可能把数据中的文本误当成操作指令。
应对方式包括:
- 明确区分系统指令、用户请求和外部数据;
- 只暴露完成当前任务所需的工具;
- 外部内容不能改变权限和确认规则;
- 敏感信息在进入模型前脱敏,工具输出也限制字段;
- 高风险写操作必须由确定性策略和用户确认把关;
- 记录 “哪个输入导致了哪个调用”,便于追踪和评测。
Prompt 可以帮助模型遵守规则,但真正的安全边界必须落在宿主应用代码和基础设施中。
# 贯穿项目:笔记与面试助手怎样使用工具
第一版不需要十几个工具,三个工具就能形成清晰闭环:
| 工具 | 类型 | 输入 | 输出与约束 |
|---|---|---|---|
search_notes | 只读 | 问题、数量上限 | 标题、路径、摘要、匹配分数 |
read_note | 只读 | 笔记路径 | 正文片段、标题、更新时间;只能读取当前知识库 |
save_learning_gap | 写入 | 知识点、证据、学习建议 | 保存后的记录 ID;必须经用户确认并使用幂等键 |
用户问 “我对 Agent 和 Workflow 的理解哪里不完整” 时,可以这样运行:
- 模型调用
search_notes查找两个概念; - 宿主应用校验关键词并在当前用户笔记中搜索;
- 模型根据命中路径调用
read_note; - 宿主应用返回相关原文和来源;
- 模型生成一道追问并评价用户回答;
- 如果发现知识缺口,模型可以提出
save_learning_gap; - 宿主应用先返回将要保存的内容,前端展示给用户确认后才真正写入;
- 最终回答列出判断依据、原文来源和下一步学习建议。
这个例子能在面试中体现四层设计:模型负责语义决策,工具提供真实能力,宿主应用控制权限与状态,评测验证工具选择和最终结果。
# 可以怎样验证项目效果
建立一组固定问题,至少记录:
- 工具选择准确率:该搜索时是否搜索,不该调用时是否直接回答;
- 参数有效率:Schema 和业务校验一次通过的比例;
- 引用正确率:回答是否真的由返回的笔记支持;
- 任务完成率:是否满足用户请求,而不是只完成了工具调用;
- 写操作确认率:需要确认的动作是否都被拦截;
- 失败恢复率:可恢复错误是否换参数或有限重试;
- 延迟和成本:每次任务的模型轮数、工具耗时和 Token。
同时保存调用轨迹:模型为什么选择工具、实际参数、校验结果、工具耗时、输出摘要和最终状态。没有轨迹,很难区分问题来自工具描述、模型选择、执行服务还是最终回答。
# Mastra 如何映射到这套模型
Mastra 把工具封装成带类型的函数,核心仍然是同一套契约:
const searchNotesTool = createTool({
id: 'search-notes',
description: '...',
inputSchema: /* 输入 Schema */,
outputSchema: /* 输出 Schema */,
execute: /* 宿主应用侧执行逻辑 */
})
const interviewAgent = new Agent({
instructions: '...',
model: /* 模型 */,
tools: { searchNotesTool }
})
2
3
4
5
6
7
8
9
10
11
12
13
可以这样理解映射关系:
createTool集中定义名称、描述、输入输出和执行逻辑;Agent的tools决定本次 Agent 可以使用哪些能力;- 模型根据指令、用户消息、工具描述和 Schema 决定是否调用;
- Mastra 帮你处理工具调用循环、流式事件和跟踪等框架工作;
- 权限、审批、幂等和业务验收仍然要根据项目要求设计。
Mastra 还可以把 MCP Server 提供的工具加载给 Agent。无论工具来自本地 createTool 还是远程 MCP,它在运行时仍会被模型选择并产生 Tool Calling。MCP 解决能力怎样标准化接入,Mastra 负责框架编排,Tool Calling 是模型提出调用的机制。
学习 Mastra 时不要只记 createTool 的参数,而要能把每个配置对应回工具契约、执行边界和失败路径。具体 API 可能变化,这些设计原则不会因为换框架而失效。
# 常见失败与定位方法
| 现象 | 常见根因 | 优先检查 |
|---|---|---|
| 不调用工具就编答案 | 工具描述不清、指令允许猜测 | 调用轨迹、工具适用条件、评测样本 |
| 总是选错工具 | 工具职责重叠、一次暴露过多 | 名称、反例、动态工具集合 |
| 参数经常不合法 | Schema 太宽或字段说明模糊 | 严格模式、枚举、字段描述 |
| 工具成功但回答错误 | 输出字段不清、结果未正确回传 | call_id、输出结构、完整上下文 |
| 工具失败后继续猜 | 错误被吞掉或没有失败策略 | 错误分类、retryable、停止条件 |
| 写操作被执行两次 | 超时后盲目重试 | 幂等键、执行记录、状态查询 |
| 延迟和成本过高 | 工具过多、输出过长、调用轮数过多 | Token、工具耗时、循环次数 |
| 测试正常但线上越权 | 测试只覆盖 Schema | 身份、资源范围、审批与审计 |
调试时不要只看最终回答。一次调用至少应该能关联:请求、模型响应、工具名、参数、校验、执行结果、耗时、错误、重试和最终答案。
# 高频面试题与回答
回答顺序:先说结论,再解释原因,最后补一个例子或工程边界。不要逐字背诵,记住这条表达主线即可。
1. 30 秒回答:什么是 Tool Calling参考答案
可以直接回答:
Tool Calling 可以理解为:模型负责提出调用请求,程序负责真正执行。应用先把工具名称、用途和参数 Schema 告诉模型;模型选择工具并生成参数;宿主应用校验参数、权限和风险后执行,再把真实结果交回模型。它让模型能查询实时数据或操作外部系统,但超时、重试、幂等和安全都必须由程序控制。
2. 2 分钟回答:你会怎样设计一个可靠工具参考答案
可以直接回答:
我会从工具契约开始,而不是直接把现有 API 全部暴露给模型。每个工具只做一件事,名称和描述要讲清楚什么时候该用、什么时候不该用;输入使用严格 Schema、枚举和范围限制,输出使用稳定的成功或失败结构,只保留下一步真正需要的信息。
执行时,我会把模型生成的参数当作不可信输入。服务端先检查工具白名单和 Schema,再根据真实用户身份做授权;写操作还要预览和二次确认,并用幂等键防止重复执行。调用外部服务时设置超时、取消和限流,错误则分成可重试、参数需修正、无权限和不可恢复几类,不能让模型盲目重试。
最后我会记录调用轨迹,并用固定样本检查工具是否选对、参数是否有效、任务是否完成,以及失败恢复、延迟和费用。这样工具既容易被模型理解,也能被程序校验和排查。
3. 项目回答:你在项目里怎样使用 Tool Calling参考答案
可以直接回答:
我在个人笔记与面试助手里先设计了三个工具:search_notes 搜索笔记,read_note 读取命中文档,save_learning_gap 保存用户确认的薄弱知识点。模型只负责根据问题选择工具和生成参数,宿主应用使用当前登录用户的身份限制数据范围,并把真实查询结果交回模型。
读取工具默认可以执行,但写入学习记录会先返回操作预览,只有用户确认后才执行;写入还带幂等键,避免网络超时或重试产生重复记录。工具结果使用统一的成功和错误结构,找不到资料时模型必须说明证据不足,不能根据自身记忆冒充笔记内容。
我用固定问题集检查该搜索时是否正确选择工具、回答是否有真实来源、写操作是否都经过确认,并记录每一步参数、结果和耗时。这个设计的重点是把模型决策和宿主应用执行分开,使工具调用可控、可观察,也能证明最终任务真的完成。
4. 为什么模型返回了函数名和参数,不代表函数已经执行?参考答案
因为模型只生成了 “我想调用哪个函数、参数是什么”,它通常没有数据库连接、文件权限或第三方凭据。宿主应用还要检查工具、参数和用户权限,再真正执行函数,并把结果按调用 ID 交回模型。所以 tool call 只是请求,不是执行结果,更不能证明任务已经完成。
5. Function Calling 和 Tool Calling 有什么区别?参考答案
Function Calling 通常特指开发者声明的自定义函数;Tool Calling 范围更大,还可能包含平台提供的搜索、文件和代码执行工具。不同 SDK 的名字可能不同,但核心流程一样:先向模型描述工具,模型返回结构化请求,应用或平台执行,再把真实结果交回模型。
6. `strict: true` 能保证什么,不能保证什么?参考答案
strict: true 主要保证模型生成的参数符合声明的 JSON Schema,例如字段、类型和枚举正确。它不能保证金额合理、用户有权限、资源真实存在,也不能保证执行安全。所以 Schema 通过后,服务端仍要做业务校验、授权、确认和结果验证。
7. 为什么不能让模型直接生成 SQL、Shell 或 URL 再执行?参考答案
因为 SQL、Shell 和任意 URL 的能力范围太大,很难只靠 Schema 限制,容易造成注入、越权或破坏性操作。更安全的方式是把能力封装成职责明确的工具,让模型只填写业务参数,SQL、命令和请求地址由程序通过固定模板或参数化查询生成。确实需要运行代码时,也要放进受限沙箱。
8. 工具执行失败后应该把什么返回给模型?参考答案
要返回让模型知道下一步该怎么做的结构化错误,例如错误代码、简要原因和能否重试。参数错误可以让模型修正,无权限就停止或申请授权,网络超时只能有限重试。完整堆栈留在服务端日志里,用 traceId 关联,不能把密钥和数据库细节返回给模型。
9. 哪些工具可以并行调用,哪些必须串行?参考答案
判断标准是依赖关系和副作用。互不依赖的只读查询可以并行,例如同时查天气和汇率;后一步依赖前一步结果时必须串行,例如先搜索再读取文档。创建、支付或修改同一资源的写操作也应保守串行,并用事务、锁、版本号或幂等键保证正确性。
10. 怎样防止写工具因重试而执行两次?参考答案
我会给一次业务动作分配唯一的幂等键,并在服务端保存这个键的执行状态和结果。相同请求再次到达时,服务端返回第一次的结果,不再重复写入。简单说,同一件事请求多次,业务效果仍然只发生一次。数据库唯一约束、事务和条件更新可以作为最后一道保障。
11. `tool_choice` 和权限控制有什么区别?参考答案
tool_choice 控制模型这一轮是否调用工具、必须调用哪个工具,属于模型侧配置;权限控制则由服务端根据真实身份和资源范围决定工具能不能执行。前者只是限制模型的选择范围,不是安全边界。即使强制模型选择某个工具,也不代表用户自动获得了执行权限。
12. 为什么工具不是越多越好?参考答案
因为每个工具的描述和 Schema 都会占用 Context,工具越多、职责越相似,模型越容易选错,Token 和延迟也会增加。生产系统通常根据用户权限和当前任务阶段,只给模型当前需要的工具;工具很多时可以先检索或动态加载,再用评测检查选择是否准确。
13. MCP 和 Tool Calling 是什么关系?参考答案
Tool Calling 解决的是模型怎样提出一次结构化调用;MCP 解决的是外部工具和资源怎样用统一协议被发现和接入。MCP Server 的工具加载进 Agent 后,模型通常仍通过 Tool Calling 来选择它。MCP 统一了接入方式,但授权、审批、幂等和任务闭环仍由宿主应用负责。
14. 为什么工具输出还要做 Schema 校验和裁剪?参考答案
因为工具和第三方接口也可能返回缺字段、类型变化或异常内容。Schema 校验能及时发现接口变化,裁剪则避免把大对象、敏感字段和无关文本全部塞进 Context,减少 Token 和提示注入风险。完整原始数据可以单独保存,模型只拿下一步需要的结构化结果。
15. 用笔记助手讲一遍完整的 Tool Calling 链路。参考答案
以笔记助手为例:应用先把问题和 search_notes、read_note 的说明发给模型;模型选择工具并生成参数;应用校验参数和用户权限后执行,再把带 callId 的真实结果交回模型。模型可以继续读取原文并生成带来源的答案。若要保存薄弱点,写操作必须先让用户确认,并用幂等键防止重复写入;最后再检查引用和任务状态,确认是否真的完成。
# 接下来学什么
下一篇学习 Workflow 与 Agent Loop,重点理解怎样把一次 Tool Calling 扩展为完整的 “决策 → 行动 → 观察 → 再决策” 闭环。