第 18 章 · Agent 入门:ToolLoopAgent 与循环控制
本章目标:
- 理解 Agent 三要素:LLM、Tools 与 Loop,以及
ToolLoopAgent如何封装它们- 学会创建 Agent:模型/工具/instructions 配置与三种使用方式(generate/stream/UI stream)
- 掌握循环控制:
stopWhen停止条件(isStepCount/hasToolCall/isLoopFinished)与自定义条件- 使用
prepareStep实现动态模型切换、逐步设置覆盖、上下文压缩与分阶段工具选择- 了解 call options(
callOptionsSchema+prepareCall)实现按请求定制 Agent 行为
18.1 什么是 Agent
Agent 是在循环中使用工具完成任务的大语言模型(LLM)。三个组件协同工作:
- LLM:处理输入并决定下一步动作;
- Tools:扩展文本生成之外的能力(读文件、调用 API、写数据库);
- Loop:编排执行过程,包括——上下文管理(维护对话历史并决定每步模型看到什么)、停止条件(判断任务何时完成)。
ToolLoopAgent 类
ToolLoopAgent 类封装了这三个组件。下面是一个用多个工具循环完成任务的 agent:
import { ToolLoopAgent, tool, createGateway } from 'ai';
import { z } from 'zod';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const weatherAgent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
weather: tool({
description: 'Get the weather in a location (in Fahrenheit)',
inputSchema: z.object({
location: z.string().describe('The location to get the weather for'),
}),
execute: async ({ location }) => ({
location,
temperature: 72 + Math.floor(Math.random() * 21) - 10,
}),
}),
convertFahrenheitToCelsius: tool({
description: 'Convert temperature from Fahrenheit to Celsius',
inputSchema: z.object({
temperature: z.number().describe('Temperature in Fahrenheit'),
}),
execute: async ({ temperature }) => {
const celsius = Math.round((temperature - 32) * (5 / 9));
return { celsius };
},
}),
},
});
const result = await weatherAgent.generate({
prompt: 'What is the weather in San Francisco in celsius?',
});
console.log(result.text); // agent's final answer
console.log(result.steps); // steps taken by the agent这个 agent 会自动:① 调用 weather 工具获取华氏温度;② 调用 convertFahrenheitToCelsius 转换;③ 用结果生成最终文本回答。ToolLoopAgent 处理了循环、上下文管理与停止条件。
为什么推荐 ToolLoopAgent
- 减少样板代码——托管循环与消息数组;
- 提升可复用性——定义一次,全应用使用;
- 简化维护——单一位置更新配置。
大多数场景从 ToolLoopAgent 出发;只有需要对每一步显式控制的复杂结构化工作流,才直接使用核心函数(generateText、streamText)。当需要确定性、可重复的输出时,则应使用结构化 workflow 模式(条件分支 + 标准函数 + 错误处理 + 显式控制流)。
18.2 创建与配置 Agent
实例化 ToolLoopAgent 即完成定义:
import { ToolLoopAgent, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const myAgent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
instructions: 'You are a helpful assistant.',
tools: {
// Your tools here
},
});它接受与 generateText / streamText 相同的设置。提供工具供 agent 完成任务:
import { ToolLoopAgent, tool, createGateway } from 'ai';
import { z } from 'zod';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const codeAgent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
runCode: tool({
description: 'Execute Python code',
inputSchema: z.object({
code: z.string(),
}),
execute: async ({ code }) => {
// Execute code and return result
return { output: 'Code executed successfully' };
},
}),
},
});Loop Control
默认情况下 agent 运行 20 步(stopWhen: isStepCount(20))。每一步中模型要么生成文本(agent 完成),要么调用工具(AI SDK 执行该工具后开启新一轮生成):
import { ToolLoopAgent, isStepCount, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
stopWhen: isStepCount(50), // Increase default from 20 to 50.
});可以组合多个条件:
import { ToolLoopAgent, isStepCount, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
stopWhen: [
isStepCount(20), // Maximum 20 steps
yourCustomCondition(), // Custom logic for when to stop
],
});Tool Choice
控制 agent 如何使用工具:toolChoice: 'required' 强制用工具、'none' 禁用工具、'auto'(默认)由模型决定,也可以强制使用特定工具:
import { ToolLoopAgent, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
weather: weatherTool,
cityAttractions: attractionsTool,
},
toolChoice: {
type: 'tool',
toolName: 'weather', // Force the weather tool to be used
},
});Structured Output
通过 Output 定义结构化输出 schema:
import { ToolLoopAgent, Output, createGateway } from 'ai';
import { z } from 'zod';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const analysisAgent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
output: Output.object({
schema: z.object({
sentiment: z.enum(['positive', 'neutral', 'negative']),
summary: z.string(),
keyPoints: z.array(z.string()),
}),
}),
});
const { output } = await analysisAgent.generate({
prompt: 'Analyze customer feedback from the last quarter',
});18.3 System Instructions 定义行为
System instructions 决定 agent 的行为、个性与约束。几种典型写法:
// 基础角色设定
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
instructions:
'You are an expert data analyst. You provide clear insights from complex data.',
});// 详细行为准则
const customerSupportAgent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
instructions: `You are a customer support specialist for an e-commerce platform.
Rules:
- Never make promises about refunds without checking the policy
- Always be empathetic and professional
- If you don't know something, say so and offer to escalate
- Keep responses concise and actionable
- Never share internal company information`,
tools: {
checkOrderStatus,
lookupPolicy,
createTicket,
},
});还可以写工具使用指引(如「先广搜再精读、交叉验证来源」)和格式风格要求(如「Markdown 输出、避免行话」)。
18.4 使用 Agent 的三种方式
一次性文本生成——generate():
const result = await myAgent.generate({
prompt: 'What is the weather like?',
});
console.log(result.text);流式响应——stream():
const result = await myAgent.stream({
prompt: 'Tell me a story',
});
for await (const chunk of result.textStream) {
console.log(chunk);
}面向 UI 的 API 响应——createAgentUIStreamResponse()(用于聊天应用的路由):
// In your API route (e.g., app/api/chat/route.ts)
import { createAgentUIStreamResponse } from 'ai';
export async function POST(request: Request) {
const { messages } = await request.json();
return createAgentUIStreamResponse({
agent: myAgent,
uiMessages: messages,
});
}18.5 生命周期回调
Agent 提供生命周期回调用于日志、可观测性、调试与遥测:
const result = await myAgent.generate({
prompt: 'Research and summarize the latest AI trends',
onStart({ modelId }) {
console.log('Agent started', { modelId });
},
onStepStart({ stepNumber, modelId }) {
console.log(`Step ${stepNumber} starting`, { modelId });
},
onToolExecutionStart({ toolCall }) {
console.log(`Tool call starting: ${toolCall.toolName}`);
},
onToolExecutionEnd({ toolCall, toolExecutionMs, toolOutput }) {
console.log(
`Tool call finished: ${toolCall.toolName} (${toolExecutionMs}ms)`,
{
success: toolOutput.type === 'tool-result',
},
);
},
onStepEnd({ stepNumber, usage, performance, finishReason, toolCalls }) {
console.log(`Step ${stepNumber} completed:`, {
inputTokens: usage.inputTokens,
outputTokens: usage.outputTokens,
outputTokensPerSecond: performance.effectiveOutputTokensPerSecond,
stepTimeMs: performance.stepTimeMs,
finishReason,
toolsUsed: toolCalls?.map(tc => tc.toolName),
});
},
onEnd({ usage, steps }) {
console.log('Agent finished:', {
totalSteps: steps.length,
totalTokens: usage.totalTokens,
});
},
});可用回调一览:
onStart:操作开始时调用一次(任何 LLM 调用之前),收到模型信息、messages、settings 和runtimeContext;onStepStart:每步(LLM 调用)之前调用;onToolExecutionStart/onToolExecutionEnd:工具execute执行前后调用,后者带toolExecutionMs与判别联合toolOutput(成功含output,失败含error);onStepEnd:每步完成后调用,含 usage、性能、finish reason、tool calls;onEnd:所有步骤结束后调用,含全部步骤结果与总用量。
回调既可在构造函数里做 agent 级追踪,也可在 generate()/stream() 调用时做按次追踪;两者都提供时会依次调用(构造函数在前):
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
onStepEnd: async ({ stepNumber, usage }) => {
// Agent-wide logging
console.log(`Agent step ${stepNumber}:`, usage.totalTokens);
},
});
// Method-level callback runs after constructor callback
const result = await agent.generate({
prompt: 'Hello',
onStepEnd: async ({ stepNumber, usage }) => {
// Per-call tracking (e.g., for billing)
await trackUsage(stepNumber, usage);
},
});此外可用 InferAgentUIMessage 为 UI 组件或持久化推断 agent 的 UIMessage 类型:
import { ToolLoopAgent, InferAgentUIMessage } from 'ai';
// export type MyAgentUIMessage = InferAgentUIMessage<typeof myAgent>;18.6 循环控制深入:停止条件
循环持续到以下情况之一发生:
- 返回的 finish reason 不是 tool-calls;
- 被调用的工具没有
execute函数; - 某个 tool call 需要审批;
- 满足某个停止条件。
内置停止条件
isStepCount(count)——达到指定步数后停止;hasToolCall(...toolNames)——任一指定工具被调用时停止;isLoopFinished()——永不触发,让循环自然跑完。
提高步数上限到 50:
import { ToolLoopAgent, isStepCount, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
// your tools
},
stopWhen: isStepCount(50), // Increasing the default of 20 to 50.
});
const result = await agent.generate({
prompt: 'Analyze this dataset and create a summary report',
});⚠️ isLoopFinished() 要谨慎使用——没有步数上限时,若模型不断发起 tool calls,可能无限运行并产生高额成本。
多个条件取「或」,任一满足即停:
import { ToolLoopAgent, isStepCount, hasToolCall, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
// your tools
},
stopWhen: [
isStepCount(20), // Maximum 20 steps
hasToolCall('someTool', 'done'), // Stop after calling either tool
],
});自定义停止条件
针对特定需求构建自定义条件,例如检测答案标记:
import { ToolLoopAgent, StopCondition, ToolSet, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const tools = {
// your tools
} satisfies ToolSet;
const hasAnswer: StopCondition<typeof tools> = ({ steps }) => {
// Stop when the model generates text containing "ANSWER:"
return steps.some(step => step.text?.includes('ANSWER:')) ?? false;
};
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools,
stopWhen: hasAnswer,
});
const result = await agent.generate({
prompt: 'Find the answer and respond with "ANSWER: [your answer]"',
});自定义条件能拿到跨步骤信息,例如实现预算熔断:
const budgetExceeded: StopCondition<typeof tools> = ({ steps }) => {
const totalUsage = steps.reduce(
(acc, step) => ({
inputTokens: acc.inputTokens + (step.usage?.inputTokens ?? 0),
outputTokens: acc.outputTokens + (step.usage?.outputTokens ?? 0),
}),
{ inputTokens: 0, outputTokens: 0 },
);
const costEstimate =
(totalUsage.inputTokens * 0.01 + totalUsage.outputTokens * 0.03) / 1000;
return costEstimate > 0.5; // Stop if cost exceeds $0.50
};18.7 prepareStep:逐步定制执行
prepareStep 回调在循环每一步之前运行,不返回更改则沿用初始设置。可用于修改设置、管理上下文、基于执行历史实现动态行为。
动态模型切换
根据步骤需求切换模型:
import { ToolLoopAgent, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-4o-mini'), // Default model
tools: {
// your tools
},
prepareStep: async ({ stepNumber, messages }) => {
// Use a stronger model for complex reasoning after initial steps
if (stepNumber > 2 && messages.length > 10) {
return {
model: gateway('openai/o4-mini'),
};
}
// Continue with default settings
return {};
},
});模型调用设置覆盖
为单步覆盖 provider 无关的调用设置——例如 tool-calling 步骤需要比最终回答更确定的采样:
import { ToolLoopAgent, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
temperature: 0.7,
tools: {
// your tools
},
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
return {
temperature: 0,
maxOutputTokens: 300,
};
}
return {};
},
});prepareStep 可覆盖 maxOutputTokens、temperature、topP、topK、presencePenalty、frequencyPenalty、stopSequences、seed、reasoning。覆盖只作用于当前一步;省略或 undefined 时用顶层值;falsy 值(如 temperature: 0、seed: 0、空数组)会被保留。
上下文管理与压缩
长时间运行的 agent 会累积大量 tool results、reasoning parts 和 assistant messages。利用 prepareStep 变更后续步骤使用的消息状态即可实现压缩(何时压缩由你决定)。返回的 messages 会成为后续步骤的基础,新响应追加其上;initialMessages 是原始输入、responseMessages 是已积累的模型/工具响应,用于重建。
pruneMessages 辅助函数提供了内置的消息裁剪能力:
import { ToolLoopAgent, pruneMessages, type ModelMessage, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const COMPACTION_THRESHOLD = 100_000;
const estimateTokens = (messages: ModelMessage[]) => {
return JSON.stringify(messages).length / 4;
};
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
// your tools
},
prepareStep: async ({ messages }) => {
if (estimateTokens(messages) > COMPACTION_THRESHOLD) {
return {
messages: pruneMessages({
messages,
reasoning: 'all',
toolCalls: 'before-last-3-messages',
emptyMessages: 'remove',
}),
};
}
},
});同样的模式也适用于 generateText 和 streamText。
分阶段工具选择
控制每一步可用的工具:
import { ToolLoopAgent, createGateway } from 'ai';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
tools: {
search: searchTool,
analyze: analyzeTool,
summarize: summarizeTool,
},
prepareStep: async ({ stepNumber, steps }) => {
// Search phase (steps 0-2)
if (stepNumber <= 2) {
return {
activeTools: ['search'],
toolChoice: 'required',
};
}
// Analysis phase (steps 3-5)
if (stepNumber <= 5) {
return {
activeTools: ['analyze'],
};
}
// Summary phase (step 6+)
return {
activeTools: ['summarize'],
toolChoice: 'required',
};
},
});也可以在某一步强制使用特定工具:
prepareStep: async ({ stepNumber }) => {
if (stepNumber === 0) {
// Force the search tool to be used first
return {
toolChoice: { type: 'tool', toolName: 'search' },
};
}
// ...
return {};
},18.8 Call Options:按请求定制 Agent
Call options 允许向 agent 传入类型安全的结构化输入,基于具体请求动态修改任意设置。三步走:
- 定义 schema——用
callOptionsSchema声明接受的输入; - 配置
prepareCall——用这些输入修改 agent 设置; - 运行时传参——调用
generate()或stream()时提供 options。
基础示例:运行时把用户上下文注入 prompt:
import { ToolLoopAgent, createGateway } from 'ai';
import { z } from 'zod';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const supportAgent = new ToolLoopAgent({
model: gateway('openai/gpt-5'),
callOptionsSchema: z.object({
userId: z.string(),
accountType: z.enum(['free', 'pro', 'enterprise']),
}),
instructions: 'You are a helpful customer support agent.',
prepareCall: ({ options, ...settings }) => ({
...settings,
instructions:
settings.instructions +
`\nUser context:
- Account type: ${options.accountType}
- User ID: ${options.userId}
Adjust your response based on the user's account level.`,
}),
});
// Call the agent with specific user context
const result = await supportAgent.generate({
prompt: 'How do I upgrade my account?',
options: {
userId: 'user_123',
accountType: 'free',
},
});此时 options 参数是必需且被类型检查的——缺失或类型错误都会导致 TypeScript 报错。
动态模型选择
根据请求特征选择模型:
import { ToolLoopAgent, createGateway } from 'ai';
import { z } from 'zod';
const gateway = createGateway({ apiKey: process.env.AI_GATEWAY_API_KEY ?? '' });
const agent = new ToolLoopAgent({
model: gateway('openai/gpt-4o-mini'), // Default model
callOptionsSchema: z.object({
complexity: z.enum(['simple', 'complex']),
}),
prepareCall: ({ options, ...settings }) => ({
...settings,
model:
options.complexity === 'simple'
? gateway('openai/gpt-4o-mini')
: gateway('openai/o4-mini'),
}),
});
// Use faster model for simple queries
await agent.generate({
prompt: 'What is 2+2?',
options: { complexity: 'simple' },
});
// Use more capable model for complex reasoning
await agent.generate({
prompt: 'Explain quantum entanglement',
options: { complexity: 'complex' },
});prepareCall 只需返回想改动的设置,其余保持不变。审批策略等 agent 设置同样可以从 prepareCall 按 call options 动态返回(详见下一章)。
本章小结
- Agent = LLM + Tools + Loop;
ToolLoopAgent封装三者,自动处理循环、上下文管理与停止条件,是构建 agent 的推荐方式; - 三种使用方式:
generate()(一次性)、stream()(流式)、createAgentUIStreamResponse()(聊天路由); - 循环终止四条件:非 tool-calls finish、无 execute 工具、待审批 tool call、命中
stopWhen;内置条件有isStepCount/hasToolCall/isLoopFinished,支持数组组合与自定义函数(如预算熔断); prepareStep可逐步切换模型、覆盖采样设置、用pruneMessages压缩上下文、按阶段收窄activeTools;- Call options(
callOptionsSchema+prepareCall)让同一 agent 按请求注入用户上下文、动态选模与改配置,全程类型安全。
🛠️ 动手实践
- 用
ToolLoopAgent实现一个天气查询 agent(模拟天气工具 + 华氏转摄氏工具),分别用generate()和stream()调用并观察result.steps中每步的工具调用链。 - 构建一个研究型 agent:
stopWhen: [isStepCount(10), hasToolCall('submit_report')],再添加一个自定义StopCondition在累计 token 成本超过阈值时中断。 - 写一个分阶段 agent:前两步只允许搜索工具且
toolChoice: 'required',之后切到分析模型并用pruneMessages把早期工具结果压缩掉。