A17第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

多轮工具执行

多轮工具执行 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

读完本课你能实现受限的多轮工具执行:循环怎么转、每一步怎么记录、次数上限和取消条件怎么设,以及超限之后怎么让模型体面收场。

问题场景

用户问「A16 课讲了什么,和站里哪些练习相关」。模型先检索课程,再读正文,又想检索练习——它连环举手要工具,根本没打算停。一次失控的循环意味着:十几秒的等待、成倍的 token 账单、和一段谁也看不懂的执行过程。

核心解释

循环结构本身不复杂:上一轮以 finish_reason: "tool_calls" 结束、工具结果已回传,就再跑一轮。关键是给它笼子。本站 app/api/ai/chat/route.ts 的真实结构(简化):

const MAX_ROUNDS = 3; // 笼子一:轮次上限
let round = 0;

async function runRound() {
  round += 1;
  await streamChatCompletion(
    { messages, tools: ASSISTANT_TOOLS, signal }, // signal 来自 abort 链(A12)
    callbacks // onTextDelta / onToolCalls / onDone / onError
  );
  // 笼子二:取消条件——出错、被取消、或到达上限都不再继续
  const lastRole = messages[messages.length - 1]?.role;
  if (round < MAX_ROUNDS && lastRole === "tool" && !errorHappened) {
    await runRound();
  }
}
await runRound();

三个笼子的含义:

  1. 轮次上限(本站 3):检索 → 读正文 → 再检索一次足够覆盖本站场景。上限的存在把「失控循环」变成「最多损失 N 轮」。
  2. 取消条件:用户停止(abort 链传导,A12)、工具执行出错、模型不再申请工具,三种情况都要立刻退出循环——条件写在递归判断里,一处管住。
  3. 超限收场:到上限时如果模型还想调工具,最后一轮不带 tools 再请求一次,或追加一条「请基于已获得的信息直接回答」的提示。它必须作答,哪怕答案不完整——把不完整交还用户,好过无限烧钱。

每一步都要记录——给界面、给排障、给成本核算:

// 每次工具执行后(A15 的执行点扩展)
push({ type: "tool", name, status: "done", summary: result.slice(0, 120) });
// ↑ A10 事件协议里的 tool 事件:前端能显示「正在检索课程…」
toolLog.push({ round, name, args: call.function.arguments, summary: result.slice(0, 120), ms: cost });
// ↑ 服务端留存完整输入输出(脱敏后可入 A13 的调用记录),复盘时有据可查

示例拆解

一次健康的两轮执行长什么样(trace 示例):

round 1  finish_reason=tool_calls
  └─ search_courses {"query":"工具参数 校验"} -> 4 门课程(120ms)
round 2  finish_reason=tool_calls
  └─ get_lesson {"course_code":"A16"} -> 正文 3000 字摘要(80ms)
round 3  finish_reason=stop,正文开始输出,done
工具日志:2 次调用,全部命中白名单,结果截断后回传

关键点:

  • 每轮的 messages 是累积的:原始问题 + 助手的 tool_calls + 工具结果 + ……模型每一轮都看得到全部历史,所以工具结果的摘要质量直接影响最终回答。
  • 结果回传前的截断(本站 8000 字符)在多轮场景尤其重要:三轮各塞全文,上下文直接爆窗(A02)。
  • toolLog 里 round 字段让前端能按轮次分组显示进度,也让「哪一轮慢了」一眼可见。
  • 验证循环是否受控:数 trace 里的轮数是否 ≤ MAX_ROUNDS,以及超限场景是否走了「不带 tools 强制作答」的分支。

动手任务

  1. 把本站的 MAX_ROUNDS 临时改成 1,问一个必须两轮才能答好的问题,观察它如何被强制收场;改回 3。
  2. 在 executeTool 外围加轮次日志(round、工具名、耗时),跑一次多轮对话,把 trace 保存成笔记。
  3. 实现「超限收场」分支:到达上限仍有 tool_calls 时,用不带 tools 的最后一次请求让模型作答,并在界面提示「已达到工具执行上限」。

常见错误

  • 没有上限的 while 循环:模型坚持要工具就永远轮下去,token 和时间双输。上限必须有,且要可配置。
  • 到上限直接掐断不收场:用户只看到回答凭空消失。超限要触发「基于已有信息作答」的最后一轮。
  • 每轮结果全量回传:两三轮就把上下文撑爆,报「context length」错误(A02 的单次容量口径)。
  • 出错后继续下一轮:错误已经发生还跑下一轮,只会基于坏结果继续编。取消条件里必须有「出错即停」。

检查与复盘

自查清单:

  • 能说出循环的三要素:继续条件、轮次上限、取消条件
  • 每步工具执行有记录(轮次、工具名、入参摘要、结果摘要)
  • 超限收场路径验证过:模型基于已有信息作答而非卡死

参考资料

  • OpenAI 文档:Function calling —— 官方的调用循环说明与「处理可能失败」的建议
  • OpenAI 文档:API Reference - Chat —— finish_reason 取值与 tool 消息结构
  • 到这里,A01-A17 的主线就走完了:一次真实调用、一套流式协议、一个受控工作台、一组带白名单的工具——回去把 A14 的验证清单再跑一遍,感受一下现在你对每个环节的把握。
检查题共 2 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1读代码中等

阅读多轮工具执行的循环控制代码。三个「笼子」分别是什么?到上限时如果模型还想调工具,会发生什么?

const MAX_ROUNDS = 3;
let round = 0;
async function runRound() {
  round += 1;
  await streamChatCompletion({ messages, tools: ASSISTANT_TOOLS, signal }, callbacks);
  const lastRole = messages[messages.length - 1]?.role;
  if (round < MAX_ROUNDS && lastRole === "tool" && !errorHappened) {
    await runRound();
  }
}
await runRound();
Q2故障定位较难

bug 现场:助手被问「A16 课讲了什么,和站里哪些练习相关」,服务端日志显示工具调用连跑了十几轮还没停,最后上游报 context length 超限错误,整个请求失败。当前实现是 while(true) 循环:只要 finish_reason 是 tool_calls 就继续,工具结果全量回传。指出违反课程口径的地方并给出修法。

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...