工具调用的工作过程
工具调用的工作过程 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
读完本课你能说清工具调用的三方职责——模型提出调用、服务端校验执行、结果回传继续生成;并追踪一个只读工具从声明到回传的完整流程。
问题场景
问学习助手「站里有哪些讲状态的课」,它自信地列了五门——其中两门根本不存在。模型没有检索能力,它只是在生成「看起来像答案的文本」。要让它基于真实数据回答,就得给它工具。
核心解释
工具调用的三方职责必须分清:
- 模型:提出调用,不执行。 你在请求里声明工具清单(名称、用途、参数结构),模型判断当前问题需要工具时,输出一个「调用申请」——
finish_reason: "tool_calls",附带工具名和参数。它自始至终碰不到你的数据库。 - 服务端:校验执行。 你的代码检查工具名是否在白名单、参数是否合法,然后执行真实逻辑(查库、读文件),把结果变成一条
role: "tool"消息。 - 回传与继续: 把工具消息追加进 messages 再请求一次,模型基于真实结果生成回答;它可能再次申请工具——循环与上限是 A17 课的事。
声明与申请的数据形状(本站 lib/ai/ 的真实设计):
// 服务端声明:白名单内的只读检索工具(lib/ai/tools.ts)
const searchCourses: ToolDefinition = {
type: "function",
function: {
name: "search_courses",
description: "检索站内课程:按关键词匹配标题、简介与正文。回答学习问题前先检索。",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "检索关键词" },
track: { type: "string", description: "可选,限定方向" },
},
required: ["query"],
},
},
};
// 模型的申请:finish_reason 为 "tool_calls" 时出现
{
"choices": [{
"finish_reason": "tool_calls",
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": { "name": "search_courses", "arguments": "{\"query\":\"状态\"}" }
}]
}
}]
}
两个关键认知:
arguments是字符串,不是对象——模型生成的是 JSON 文本,服务端要JSON.parse并校验(A16 课专门讲)。- description 写的是给模型看的「使用说明书」:什么时候该用、参数什么含义。写得含糊,模型就乱调或该调不调。
示例拆解
服务端执行环节(本站 executeTool 的骨架):
// 白名单校验在先,执行在后(app/api/ai/chat/route.ts)
const allowed = ASSISTANT_TOOLS.some((t) => t.function.name === name);
let result: string;
if (!allowed) {
result = JSON.stringify({ error: "工具不在白名单内" }); // 拒绝也是结果,回传给模型
} else {
const exec = executeTool(name, call.function.arguments);
result = exec.result; // 结构化 JSON 字符串
refs.push(...exec.refs); // 同时生成给前端展示的课程引用
}
messages.push({ role: "tool", content: result.slice(0, 8000), tool_call_id: call.id });
关键点:
- 工具结果也过一条协议:以
tool_call_id对应申请,内容截断到上限(本站 8000 字符),避免一次检索把上下文撑爆(A02)。 - 拒绝也返回结构化错误而不是抛异常:让模型「知道被拒」并换路,而不是让整个请求崩掉。
tool角色的结果不直接展示给用户;给用户的是服务端整理的refs引用(A10 事件协议里的refs事件)。- 本站三个工具全是只读检索(search_courses / get_lesson / search_documents):先只用读工具建立信任,写操作工具的风险控制后面再谈。
动手任务
- 在
executeTool的入口和出口各加一行日志,问一个会触发检索的问题,在服务端控制台完整追一次「申请 → 校验 → 执行 → 回传」。 - 故意把问题问得与站内内容无关(如「今天天气」),观察模型是否还申请工具——想想 description 里的「回答学习问题前先检索」起了什么作用。
- 解释任务:为什么「模型提出调用」而不是「模型直接查库」?从安全和可控两个角度各写一句。
常见错误
- 以为模型执行了工具:模型只会输出申请文本;执行永远发生在你的服务端代码里。
- 白名单形同虚设:拿到工具名直接执行,不校验是否在声明清单里——模型也可能「幻觉」出不存在的工具名。
- 工具结果原样全量塞回去:一次全文检索回传几万字,上下文瞬间爆掉;必须截断与摘要。
- description 写成内部注释:模型靠它决定「何时用、怎么用」,含糊的描述直接导致乱调参数。
检查与复盘
自查清单:
- 能完整说出三方职责与「申请 → 校验执行 → 回传 → 继续」的循环
- 能默写 ToolDefinition 与 tool_calls 的关键字段
- 知道工具结果以 tool 角色、按 tool_call_id 对应回传
参考资料
- OpenAI 文档:Function calling —— 工具声明与调用循环的官方说明
- OpenAI 文档:API Reference - Chat —— tools 参数与 tool 消息的字段定义
- 下一课 A16 深入参数:模型给的 arguments 是不可信输入。
读完这一课了?
标记完成后,工作台会安排接下来的学习。