跳到主要内容

02 · generateText 多步工具循环

本章讲什么: 整个库最核心的一段——generateText 里那个 do…while 循环。它就是大家口中的 "agent loop"。读完你能讲清:一个 step 内发生什么、循环什么时候继续、什么时候停、ToolLoopAgent 为什么只是一层薄封装。

2.1 要解决的小问题

模型一次回复里可能说"我要调 getWeather('Tokyo')"。但模型自己不能执行任何东西——它只会输出一段"我想调这个工具"的意图。真正去查天气、把结果再喂回模型让它继续说话的,是 SDK。

所以"带工具的对话"天然是个循环:

调模型 → 模型说"调工具X" → SDK 执行X → 把结果拼进消息 → 再调模型
↑ │
└──────────────── 还想调工具就继续 ─────────────────────┘

一轮"调一次模型 + 执行这轮的工具"叫一个 stepgenerateText 的职责就是自动驱动这个循环,直到该停。

2.2 思路/直觉:三个问题

设计一个工具循环,要回答三件事。AI SDK 的答案是:

问题AI SDK 的答案
一个 step 内做什么?准备参数 → 调 doGenerate → 解析工具调用 → 并行执行工具 → 收集结果
循环什么时候继续?这一步有客户端工具调用、且都执行(或被拒)完了 → 继续给模型看结果
循环什么时候?模型没再调工具,命中 stopWhen(默认 isStepCount(1))

注意默认值:generateTextstopWhen 默认 isStepCount(1)(generate-text.ts:239)——单独用 generateText 默认只跑一步。是 ToolLoopAgent 把默认拨到 isStepCount(20)(见 2.6),才变成真正的多步 agent。

2.3 一个 step 内发生什么(主线代码走读)

循环体在 generateTextdo { … } while(…) 里(generate-text.ts:785-1350)。一个 step 顺序如下:

① prepareStep:给这一步定制参数(可选)

// generate-text.ts —— 简化
const prepareStepResult = await prepareStep?.({
model, steps, stepNumber: steps.length,
messages: stepInputMessages, runtimeContext, /* … */
});
const stepModel = resolveLanguageModel(prepareStepResult?.model ?? model);

prepareStep 是个回调,让你逐步换模型、改 system、裁剪消息、限制可用工具(generate-text.ts:805)。比如"前 3 步用便宜模型,之后换贵的"。

② 把内部消息翻译成模型方言

const promptMessages = await convertToLanguageModelPrompt({
prompt: { instructions: stepInstructions, messages: ... },
supportedUrls: await stepModel.supportedUrls, // 模型能原生吃的 URL 不下载
download, provider: stepModel.provider.split('.')[0],
});

convertToLanguageModelPrompt 调用(generate-text.ts:831)。这里就是 01 说的"统一格式→LanguageModelV4Prompt"翻译,且会按模型的 supportedUrls 决定哪些文件 URL 要先下载、哪些直接传给模型。

③ 真正打模型(带重试)

currentModelResponse = await retry(async () => {
const result = await stepModel.doGenerate({
...callSettings, tools: stepTools, toolChoice: stepToolChoice,
prompt: promptMessages, abortSignal: mergedAbortSignal, /* … */
});
// 补齐 response.id / timestamp / modelId
return { ...result, response: responseData };
});

generate-text.ts:932retry 来自 prepareRetries(默认 2 次)。这是唯一真正调到 provider 的地方。

④ 解析模型给的工具调用

const stepToolCalls = await Promise.all(
currentModelResponse.content
.filter(part => part.type === 'tool-call')
.map(toolCall => parseToolCall({ toolCall, tools, repairToolCall, ... })),
);

generate-text.ts:966parseToolCall 负责校验输入、必要时"修复"(见 03)。

⑤ 并行执行客户端工具

const toolExecutionResults = await executeTools({
toolCalls: clientToolCalls.filter(c => !c.invalid && !blocked),
tools, messages: stepMessages, /* 回调 onToolExecutionStart/End … */
});

generate-text.ts:1169executeTools 内部 Promise.all 把这一步的多个工具调用并行跑掉(generate-text.ts:1490)。注意:只执行 !providerExecuted 的工具(generate-text.ts:1159)——provider 自己执行的工具(如内置 web search)结果由模型那边带回,SDK 不碰。

⑥ 组装这一步的 StepResult,推进消息

steps.push(currentStepResult);
messagesForNextStep = [...stepMessages, ...stepResponseMessages];

generate-text.ts:1323-1325。工具结果被 toResponseMessages 拼成 tool 角色的消息,接到下一步的输入里——这就是"把结果喂回模型"

2.4 循环的继续/停止条件(最关键的几行)

整个 do…while 的判定浓缩在结尾:

// packages/ai/src/generate-text/generate-text.ts —— while 条件,简化
} while (
((clientToolCalls.length > 0 &&
clientToolOutputs.length + deniedToolApprovalResponses.length
=== clientToolCalls.length) // ① 这步有客户端工具,且都已执行或被拒
|| pendingDeferredToolCalls.size > 0) // ② 或有 provider 延迟结果待回
&& !(await isStopConditionMet({ stopConditions, steps })) // ③ 且没命中 stopWhen
);

generate-text.ts:1340-1350。用大白话:

  • 要继续,必须"这一步确实调了客户端工具,而且全跑完了"(条件①)。如果模型这一步没调任何客户端工具(纯文本回答),clientToolCalls.length > 0 为假 → 循环退出。模型停止要工具 = 自然终止,这是循环的主出口。
  • 同时还得没命中任何 stopWhen 条件(条件③)。stopWhen 是个"达到就停"的护栏。
  • 条件② 是给"延迟结果"工具留的口子(provider 端工具的结果可能晚到,见 pendingDeferredToolCalls,generate-text.ts:783)。

2.5 stopWhen:可组合的停止护栏

stopWhen 接受一个或一组 StopCondition——签名极简:看着 steps 数组返回 true/false

// packages/ai/src/generate-text/stop-condition.ts —— 简化
export type StopCondition<TOOLS> = (options: {
steps: Array<StepResult<TOOLS>>;
}) => boolean | PromiseLike<boolean>;

export function isStepCount(stepCount: number): StopCondition<any, any> {
return ({ steps }) => steps.length === stepCount; // 跑满 N 步就停
}

export function hasToolCall<TOOLS>(...toolName): StopCondition<TOOLS, any> {
return ({ steps }) =>
steps.at(-1)?.toolCalls?.some(c => toolName.includes(c.toolName)) ?? false;
}

isStepCount(stop-condition.ts:27)、hasToolCall(stop-condition.ts:48)。多个条件用 isStopConditionMet 评估,任一为真即停(stop-condition.ts:65,内部 Promise.all(...).some(...))。

常见用法:stopWhen: [stepCountIs(10), hasToolCall('finalAnswer')] —— "最多 10 步,或模型一旦调 finalAnswer 就停"。

精华: 停止逻辑被抽成纯函数谓词,而非散落在循环里的 if。要自定义"什么时候停",写个看 steps 的函数即可,不必碰核心循环。

2.6 ToolLoopAgent:为什么它只是薄封装

ToolLoopAgent.generate,会发现它几乎没干"loop"的活——loop 在 generateText 里。Agent 只是把配置打包 + 改默认值:

// packages/ai/src/agent/tool-loop-agent.ts —— 简化
async generate({ ...options }) {
const preparedCall = await this.prepareCall({ ...options });
return await generateText({ // ← 直接转调 generateText
...preparedCall,
...callbackArgs,
headers: this.agentHeaders(preparedCall),
});
}

ToolLoopAgent.generate(tool-loop-agent.ts:197)。两个关键差异:

  • 默认 stopWhen 拨到 20。 prepareCallstopWhen: this.settings.stopWhen ?? isStepCount(20)(tool-loop-agent.ts:132)。所以 agent 默认能跑 20 步,而裸 generateText 默认只跑 1 步。
  • 打 user-agent 标记。 agentHeaders 给请求加 ai-sdk-agent/tool-loop 后缀(tool-loop-agent.ts:185),用于用量归因。

Agent 还支持 prepareCall(每次调用前改参数)和 callOptionsSchema(校验调用选项)。类注释把循环的终止条件写得很清楚(tool-loop-agent.ts:28-38):模型给出非 tool-calls 的 finish reason、调了没有 execute 的工具、工具需要审批、或命中 stop condition。

一句话: "Agent" 在这个库里不是一个独立的大引擎,而是 generateText + 一组打包好的默认配置。心脏始终是 2.3 那个循环。

2.7 边界与局限

  • generateText 默认单步(isStepCount(1))。想要多步,要么显式给 stopWhen,要么用 ToolLoopAgent。这是新手常踩的坑。
  • 工具在同一步内并行执行(Promise.all)。若工具间有依赖,得靠模型分多步调用,SDK 不做步内编排。
  • 循环"继续"严格要求"这一步的客户端工具调用全部有了结果/拒绝"(条件①的相等判断),所以工具执行抛错也要落成一个 tool-error 输出,否则循环逻辑会卡(库内对 invalid call 会补 tool-error,generate-text.ts:1147)。

2.8 代码地图

主题文件符号
主循环packages/ai/src/generate-text/generate-text.tsgenerateText(do…while 在 785-1350)
工具并行执行packages/ai/src/generate-text/generate-text.tsexecuteTools
单个工具执行packages/ai/src/generate-text/execute-tool-call.tsexecuteToolCall
停止条件packages/ai/src/generate-text/stop-condition.tsisStepCount, hasToolCall, isStopConditionMet
逐步定制packages/ai/src/generate-text/prepare-step.tsPrepareStepFunction
步结果packages/ai/src/generate-text/step-result.tsStepResult, DefaultStepResult
Agent 封装packages/ai/src/agent/tool-loop-agent.tsToolLoopAgent

下一章:03 · 工具系统