在开发复杂的 AI Agent 时,Agent 通常需要经过"思考 -> 调用工具 -> 观察结果 -> 再思考"的多次循环才能完成任务。如何优雅地控制这个循环(例如何时停止、每一步如何动态调整参数)是构建健壮 Agent 的关键。
本文将基于 Vercel AI SDK 的最新特性,带你深入玩转 Agent 的循环控制 (Loop Control) 。为了贴合国内开发者的实际场景,本文的所有样例代码均采用阿里通义千问最新模型 (qwen-max) 进行演示!
🛠 环境与模型准备
通义千问提供了全面兼容 OpenAI 的 API 端点,因此在 Vercel AI SDK 中,我们可以直接使用 @ai-sdk/openai 来无缝接入:
Bash
bash
npm install ai @ai-sdk/openai zod
在代码中初始化 Qwen 模型实例:
TypeScript
php
import { createOpenAI } from '@ai-sdk/openai';
// 配置阿里百炼 (DashScope) 的兼容端点
const qwen = createOpenAI({
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: process.env.DASHSCOPE_API_KEY,
});
// 在后续的示例中,我们将统一使用 qwen('qwen-max')
一、 核心概念:什么是 Loop Control?
在使用 ToolLoopAgent 编排 Agent 时,循环会持续进行,直到满足以下任一条件才会停止:
- 模型返回了除
tool-calls(工具调用)之外的完成原因。 - 调用的工具没有 配备
execute(执行)函数。 - 工具调用触发了需要人工审批的拦截。
- 满足了开发者定义的停止条件 (Stop Condition) 。
Vercel AI SDK 提供了两个极其强大的参数来控制这个内部循环:
stopWhen: 决定何时终止执行。prepareStep: 在每一步执行前,动态修改运行设置(如切换模型、控制工具白名单、精简上下文等)。
二、 停止条件 (Stop Conditions)
stopWhen 参数控制 Agent 在拿到工具执行结果后是否继续下一步。默认情况下,Agent 最多执行 20 步(即内置的 stepCountIs(20))。
1. 组合内置条件
AI SDK 提供了现成的停止条件,你可以将它们组合在数组中,满足其一即停止:
TypeScript
javascript
import { ToolLoopAgent, stepCountIs, hasToolCall } from 'ai';
// 引入上面配置的 qwen
// import { qwen } from './qwen-setup';
const agent = new ToolLoopAgent({
model: qwen('qwen-max'),
tools: {
// 你的工具集,例如 search 等
},
stopWhen: [
stepCountIs(20), // 兜底策略:最多执行 20 步,防止死循环
hasToolCall('someTool'), // 业务逻辑:一旦调用了 'someTool' 工具就立刻停止
],
});
const result = await agent.generate({
prompt: '请调研并分析 2026 年前端开发趋势',
});
2. 编写自定义条件 (Custom Conditions)
面对复杂的业务场景,你可以获取到历史所有步骤 steps,编写自定义的停止逻辑。例如,根据消耗的 Token 成本来"熔断" Agent:
TypeScript
javascript
import { ToolLoopAgent, StopCondition, ToolSet } from 'ai';
const tools = { /* ... */ } satisfies ToolSet;
// 自定义条件:当 Token 估算成本超过 $0.50 时强制停止
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 },
);
// 按照自定义汇率或 token 价格估算
const costEstimate = (totalUsage.inputTokens * 0.01 + totalUsage.outputTokens * 0.03) / 1000;
return costEstimate > 0.5;
};
const agent = new ToolLoopAgent({
model: qwen('qwen-max'),
tools,
stopWhen: budgetExceeded,
});
三、 动态准备步骤 (Prepare Step) 神器
prepareStep 回调会在循环的每一步正式发起大模型请求之前运行。利用它,你可以实现高度智能的动态行为。
1. 动态模型切换 (Dynamic Model Selection)
前几步简单的资料检索我们可以用轻量级模型省钱提速,后面复杂的逻辑推理再无缝切换到 qwen-max:
TypeScript
dart
const agent = new ToolLoopAgent({
model: qwen('qwen-turbo'), // 初始默认使用较快/较便宜的模型
tools: { /* ... */ },
prepareStep: async ({ stepNumber, messages }) => {
// 如果循环超过 2 步,且上下文消息变长,说明任务变得复杂
// 动态返回新的配置,无缝切换到最强的 qwen-max 模型
if (stepNumber > 2 && messages.length > 10) {
return {
model: qwen('qwen-max'),
};
}
// 返回空对象则继续使用当前配置
return {};
},
});
2. 阶段性工具分配 (Tool Selection)
在复杂 SOP(标准作业程序)中,我们可以控制 Agent 在不同阶段能使用的工具:
TypeScript
kotlin
prepareStep: async ({ stepNumber }) => {
// 第一阶段:检索资料 (第 0-2 步)
if (stepNumber <= 2) {
return {
activeTools: ['search'], // 只开放搜索工具
toolChoice: 'required', // 强制要求大模型必须调用工具
};
}
// 第二阶段:数据分析 (第 3-5 步)
if (stepNumber <= 5) {
return { activeTools: ['analyze'] };
}
// 第三阶段:总结汇报 (第 6 步以上)
return {
activeTools: ['summarize'],
toolChoice: { type: 'tool', toolName: 'summarize' }, // 强制大模型调用特定的总结工具
};
}
3. 防止上下文撑爆 (Context Management)
长时间运行的 Agent 容易超出模型支持的最大 Token 长度。我们可以在这里动态裁剪传递给大模型的消息:
TypeScript
kotlin
prepareStep: async ({ messages }) => {
// 如果对话超过 20 条,进行滑动窗口裁剪
if (messages.length > 20) {
return {
messages: [
messages[0], // 永远保留第一条 System Prompt/User Instruction
...messages.slice(-10), // 只保留最近的 10 条交互记录
],
};
}
return {};
}
四、 强制工具调用 (Forced Tool Calling) 模式
很多时候,我们不希望 Agent 用自然语言"水字数",而是必须通过工具输出标准化的 JSON 结构结果。
解法: 结合 toolChoice: 'required',并设计一个不包含 execute 函数的 done 虚拟工具。
💡 原理: 因为模型被强制要求调用工具,而当它调用了没有执行函数的
done工具时,AI SDK 内部因为无代码可执行,会触发"循环中断"条件,从而完美终止 Agent,并将结构化参数原封不动地返回给你!
TypeScript
php
import { ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';
const agent = new ToolLoopAgent({
model: qwen('qwen-max'),
tools: {
search: searchTool,
analyze: analyzeTool,
done: tool({
description: '当你完成所有工作后,调用此工具输出最终结果',
inputSchema: z.object({
answer: z.string().describe('最终的分析报告'),
confidence: z.number().describe('可信度评分 0-1'),
}),
// 注意核心技巧:这里绝对不写 execute 函数!
}),
},
toolChoice: 'required', // 强制每一步都不能输出废话,必须用工具
});
const result = await agent.generate({
prompt: '请综合分析这份数据,并在完成后使用 done 工具提交最终报告。',
});
// 从 staticToolCalls (未被执行的工具调用列表) 中提取出最终答案
const finalToolCall = result.staticToolCalls[0];
if (finalToolCall?.toolName === 'done') {
console.log("最终报告:", finalToolCall.input.answer);
console.log("可信度:", finalToolCall.input.confidence);
}
五、 手动循环控制 (Manual Loop Control)
如果你发现 ToolLoopAgent 的声明式 API 依然无法满足极为复杂的定制需求,你可以退回到 AI SDK 的底层,使用 generateText 手动写一个 while 循环。这为你提供了 100% 的底层控制流掌控力:
TypeScript
arduino
import { generateText, ModelMessage } from 'ai';
const messages: ModelMessage[] = [{ role: 'user', content: '分析并解决这个系统 Bug...' }];
let step = 0;
const maxSteps = 10;
while (step < maxSteps) {
const result = await generateText({
model: qwen('qwen-max'),
messages,
tools: {
// 在这里挂载你的工具
},
});
// 将当前这一步的交互结果(包含 assistant 回复、工具调用结果等)追加到上下文池
messages.push(...result.response.messages);
// 如果模型直接生成了最终结论文本(并没有继续调用工具),则认为整个思考链条已闭环
if (result.text) {
break;
}
step++;
}
console.log("任务完成,最终输出:", messages[messages.length - 1].content);
结语
Vercel AI SDK 的 Loop Control 为构建复杂的 Agent 赋予了极大的灵活性和工程化能力:
stopWhen让你可以精准定义退出条件,避免死循环和天价 API 账单。prepareStep是实现 Agent "动态进化"的关键,可按需切换模型配置和分配工具白名单。- 创新的虚拟
done工具模式,是确保业务系统获得结构化数据的绝佳实践。
搭配国内顶尖的通义千问 qwen-max 模型强大的指令遵循能力,无论在落地效果还是接口兼容性上,都能为开发者提供丝滑的体验!
如果在开发 Agent 的路上对本文有所启发,欢迎点赞、收藏并在评论区交流你的最佳实践!👇