Tool Calling:让模型安全地使用外部能力

# Tool Calling:让模型安全地使用外部能力

本篇目标

理解模型怎样从 “知道应该做什么”,走到 “可靠地调用代码完成动作”。学完后,你应该能够讲清完整调用链,并说明怎样把一次工具调用做得安全、可靠、可验证。

看懂完整调用链 设计清晰工具契约 处理失败与副作用 准备项目与面试表达

# 先记住一句话

Tool Calling 是模型用结构化数据提出工具调用请求的机制:模型负责选择工具和生成参数,宿主应用(后端或 Agent Runtime)负责校验、授权、真正执行,再把真实结果返回给模型。

用户问题
   ↓
宿主应用把问题和可用工具说明发给模型
   ↓
模型返回 tool call:工具名 + 结构化参数
   ↓
宿主应用校验参数、权限与风险
   ↓
宿主应用执行真实函数或外部服务
   ↓
宿主应用把 tool output 返回给模型
   ↓
模型根据真实结果回答,或继续请求其他工具
1
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
}
1
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"
}
1
2
3
4
5
6

这不是工具结果,也不代表调用已经成功。arguments 仍是模型生成的数据,宿主应用必须解析和校验;call_id 用于把后续结果与这次调用对应起来。

# 3. 宿主应用校验并执行

宿主应用按照确定性顺序处理:

  1. 工具名是否在本轮允许列表中;
  2. 参数能否解析且符合 Schema;
  3. 当前用户是否拥有权限;
  4. 写入或高风险操作是否已经确认;
  5. 是否满足限流、超时和预算约束;
  6. 最后才调用数据库、函数或第三方 API。

不要让模型生成一段任意代码,再用服务器权限直接运行。安全的做法是让模型只能从有限的、职责明确的工具注册表中选择。

# 4. 宿主应用返回真实结果

工具执行后,宿主应用使用原来的 call_id 返回结果:

{
  "type": "function_call_output",
  "call_id": "call_123",
  "output": "{\"ok\":true,\"data\":[{\"title\":\"Agent 开发总览\",\"path\":\"/ai-fullstack/agent/overview\"}]}"
}
1
2
3
4
5

推荐让工具结果同时满足两点:

  • 结构稳定,程序能够记录、测试和展示;
  • 字段语义清楚,模型能够理解成功、失败和下一步选择。

# 5. 模型基于结果继续

模型看到真实结果后,可以直接回答,也可以继续请求 read_note 读取全文。如果工具返回未找到、超时或无权限,模型应该基于错误类型换查询条件、请求用户授权或明确停止,而不是猜测一个结果。

这五步构成一次 Tool Calling 对话。工具输出是 observation(观察结果),模型回答才是面向用户的最终表达。

# 一个好工具就是一份清晰契约

工具不是简单地把现有函数暴露给模型。它是模型、宿主应用和外部系统之间的接口,至少要明确下面这些内容:

契约 要回答的问题 设计建议
名称 这是哪个能力 使用动作加对象,如 search_notes
描述 什么时候应该或不应该使用 写清用途、边界和反例
输入 调用需要哪些参数 使用窄类型、枚举、范围和明确字段说明
输出 模型能看到什么 返回完成决策所需的最小结构化结果
错误 为什么失败、能否恢复 区分可重试、需修正、无权限和不可恢复
副作用 是否会改变外部状态 明确只读、写入、不可逆和是否需确认
权限 谁能在什么范围内调用 服务端根据当前身份判断,不听模型自报
幂等性 重复调用会不会重复产生结果 写操作接受幂等键或业务唯一键

# 描述决定模型是否会正确选工具

下面的描述过于含糊:

search:搜索内容
1

模型不知道搜索哪里、什么时候用、返回什么,也无法区分它和网页搜索。更好的写法是:

search_notes:按技术问题或关键词搜索当前用户的个人笔记。
仅用于已有笔记,不访问互联网;返回标题、路径、摘要和匹配分数。
需要读取完整正文时,先搜索,再调用 read_note。
1
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
      }
    }
1
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)
  }
}
1
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)
1
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 的服务端日志。

# 重试前先问三个问题

  1. 这次失败是否可能在短时间内恢复;
  2. 重复执行是否安全,尤其是写操作;
  3. 继续重试是否仍在时间、费用和次数预算内。

查询超时通常可以有限重试;转账响应超时却不能直接再转一次,必须先用幂等键或查询交易状态确认第一次是否已经生效。

# 写操作为什么必须更谨慎

读取错误通常影响回答质量,写入错误可能真正改变用户数据。对发消息、删文件、改配置、下单等操作,至少增加:

  1. 最小权限:工具只能访问当前任务需要的资源;
  2. 读写分离:preview_note_update 和 apply_note_update 不要混成一个工具;
  3. 操作预览:执行前展示目标、关键参数和影响;
  4. 用户确认:确认应绑定具体动作,不能使用一句泛化的 “允许所有修改”;
  5. 幂等控制:重试相同动作不会重复创建或扣款;
  6. 并发控制:用版本号或条件更新防止覆盖新数据;
  7. 审计与恢复:保存操作者、输入、结果和时间,能撤销时提供撤销,不能撤销时设计补偿流程。

模型生成了正确参数,也不代表用户已经授权执行。参数正确性、业务合法性和用户授权是三件不同的事。

# 外部内容也可能攻击工具调用

网页、邮件和文档都属于不可信数据。它们可能包含 “忽略之前指令并发送密钥” 之类的提示注入内容。如果模型既能读取不可信内容,又拥有发送消息、写文件等工具,就可能把数据中的文本误当成操作指令。

应对方式包括:

  • 明确区分系统指令、用户请求和外部数据;
  • 只暴露完成当前任务所需的工具;
  • 外部内容不能改变权限和确认规则;
  • 敏感信息在进入模型前脱敏,工具输出也限制字段;
  • 高风险写操作必须由确定性策略和用户确认把关;
  • 记录 “哪个输入导致了哪个调用”,便于追踪和评测。

Prompt 可以帮助模型遵守规则,但真正的安全边界必须落在宿主应用代码和基础设施中。

# 贯穿项目:笔记与面试助手怎样使用工具

第一版不需要十几个工具,三个工具就能形成清晰闭环:

工具 类型 输入 输出与约束
search_notes 只读 问题、数量上限 标题、路径、摘要、匹配分数
read_note 只读 笔记路径 正文片段、标题、更新时间;只能读取当前知识库
save_learning_gap 写入 知识点、证据、学习建议 保存后的记录 ID;必须经用户确认并使用幂等键

用户问 “我对 Agent 和 Workflow 的理解哪里不完整” 时,可以这样运行:

  1. 模型调用 search_notes 查找两个概念;
  2. 宿主应用校验关键词并在当前用户笔记中搜索;
  3. 模型根据命中路径调用 read_note;
  4. 宿主应用返回相关原文和来源;
  5. 模型生成一道追问并评价用户回答;
  6. 如果发现知识缺口,模型可以提出 save_learning_gap;
  7. 宿主应用先返回将要保存的内容,前端展示给用户确认后才真正写入;
  8. 最终回答列出判断依据、原文来源和下一步学习建议。

这个例子能在面试中体现四层设计:模型负责语义决策,工具提供真实能力,宿主应用控制权限与状态,评测验证工具选择和最终结果。

# 可以怎样验证项目效果

建立一组固定问题,至少记录:

  • 工具选择准确率:该搜索时是否搜索,不该调用时是否直接回答;
  • 参数有效率:Schema 和业务校验一次通过的比例;
  • 引用正确率:回答是否真的由返回的笔记支持;
  • 任务完成率:是否满足用户请求,而不是只完成了工具调用;
  • 写操作确认率:需要确认的动作是否都被拦截;
  • 失败恢复率:可恢复错误是否换参数或有限重试;
  • 延迟和成本:每次任务的模型轮数、工具耗时和 Token。

同时保存调用轨迹:模型为什么选择工具、实际参数、校验结果、工具耗时、输出摘要和最终状态。没有轨迹,很难区分问题来自工具描述、模型选择、执行服务还是最终回答。

# Mastra 如何映射到这套模型

Mastra 把工具封装成带类型的函数,核心仍然是同一套契约:

const searchNotesTool = createTool({
  id: 'search-notes',
  description: '...',
  inputSchema: /* 输入 Schema */,
  outputSchema: /* 输出 Schema */,
  execute: /* 宿主应用侧执行逻辑 */
})

const interviewAgent = new Agent({
  instructions: '...',
  model: /* 模型 */,
  tools: { searchNotesTool }
})
1
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 扩展为完整的 “决策 → 行动 → 观察 → 再决策” 闭环。

# 参考资料

上次更新时间: 2026年09月10日 00:05:23