Skip to content

第 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:

ts
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 出发;只有需要对每一步显式控制的复杂结构化工作流,才直接使用核心函数(generateTextstreamText)。当需要确定性、可重复的输出时,则应使用结构化 workflow 模式(条件分支 + 标准函数 + 错误处理 + 显式控制流)。

18.2 创建与配置 Agent

实例化 ToolLoopAgent 即完成定义:

ts
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 完成任务:

ts
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 执行该工具后开启新一轮生成):

ts
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.
});

可以组合多个条件:

ts
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'(默认)由模型决定,也可以强制使用特定工具:

ts
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:

ts
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 的行为、个性与约束。几种典型写法:

ts
// 基础角色设定
const agent = new ToolLoopAgent({
  model: gateway('openai/gpt-5'),
  instructions:
    'You are an expert data analyst. You provide clear insights from complex data.',
});
ts
// 详细行为准则
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()

ts
const result = await myAgent.generate({
  prompt: 'What is the weather like?',
});

console.log(result.text);

流式响应——stream()

ts
const result = await myAgent.stream({
  prompt: 'Tell me a story',
});

for await (const chunk of result.textStream) {
  console.log(chunk);
}

面向 UI 的 API 响应——createAgentUIStreamResponse()(用于聊天应用的路由):

ts
// 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 提供生命周期回调用于日志、可观测性、调试与遥测:

ts
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() 调用时做按次追踪;两者都提供时会依次调用(构造函数在前):

ts
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 类型:

ts
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:

ts
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,可能无限运行并产生高额成本。

多个条件取「或」,任一满足即停:

ts
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
  ],
});

自定义停止条件

针对特定需求构建自定义条件,例如检测答案标记:

ts
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]"',
});

自定义条件能拿到跨步骤信息,例如实现预算熔断:

ts
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 回调在循环每一步之前运行,不返回更改则沿用初始设置。可用于修改设置、管理上下文、基于执行历史实现动态行为。

动态模型切换

根据步骤需求切换模型:

ts
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 步骤需要比最终回答更确定的采样:

ts
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 可覆盖 maxOutputTokenstemperaturetopPtopKpresencePenaltyfrequencyPenaltystopSequencesseedreasoning。覆盖只作用于当前一步;省略或 undefined 时用顶层值;falsy 值(如 temperature: 0seed: 0、空数组)会被保留。

上下文管理与压缩

长时间运行的 agent 会累积大量 tool results、reasoning parts 和 assistant messages。利用 prepareStep 变更后续步骤使用的消息状态即可实现压缩(何时压缩由你决定)。返回的 messages 会成为后续步骤的基础,新响应追加其上;initialMessages 是原始输入、responseMessages 是已积累的模型/工具响应,用于重建。

pruneMessages 辅助函数提供了内置的消息裁剪能力:

ts
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',
        }),
      };
    }
  },
});

同样的模式也适用于 generateTextstreamText

分阶段工具选择

控制每一步可用的工具:

ts
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',
    };
  },
});

也可以在某一步强制使用特定工具:

ts
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 传入类型安全的结构化输入,基于具体请求动态修改任意设置。三步走:

  1. 定义 schema——用 callOptionsSchema 声明接受的输入;
  2. 配置 prepareCall——用这些输入修改 agent 设置;
  3. 运行时传参——调用 generate()stream() 时提供 options。

基础示例:运行时把用户上下文注入 prompt:

ts
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 报错。

动态模型选择

根据请求特征选择模型:

ts
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 按请求注入用户上下文、动态选模与改配置,全程类型安全。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. ToolLoopAgent 默认的最大步数是多少?

2. 以下哪一项不是 agent 循环的终止条件?

3. 关于 prepareStep 返回的设置覆盖,说法正确的是?

4. callOptionsSchema + prepareCall 组合的作用是?

🛠️ 动手实践

  1. ToolLoopAgent 实现一个天气查询 agent(模拟天气工具 + 华氏转摄氏工具),分别用 generate()stream() 调用并观察 result.steps 中每步的工具调用链。
  2. 构建一个研究型 agent:stopWhen: [isStepCount(10), hasToolCall('submit_report')],再添加一个自定义 StopCondition 在累计 token 成本超过阈值时中断。
  3. 写一个分阶段 agent:前两步只允许搜索工具且 toolChoice: 'required',之后切到分析模型并用 pruneMessages 把早期工具结果压缩掉。