在构建复杂的 AI Agent 系统时,我们经常会遇到一个棘手的问题:上下文爆炸。
如果让主 Agent 直接去阅读长篇文档、检索引擎或者分析代码库,大量的中间信息不仅会迅速消耗 Context Window,还会导致模型"精神涣散",降低最终回答的质量。相比于在 LangGraph 等框架中手动编排复杂的节点图和状态流转,Vercel AI SDK 提供了一种更直观、原生的高级抽象:子代理 (Subagents) 。
今天我们就来详细拆解如何在 Vercel AI SDK 中使用子代理,并以阿里千问最新模型 (qwen-max) 为例进行实战演示。
什么是子代理?
一句话总结:子代理是一个可以被"父代理"作为工具 (Tool) 调用的独立 Agent。
子代理会在自己隔离的上下文中自主运行,执行具体的脏活累活。等任务完成后,它只将提炼后的结果返回给主代理。
适用场景分析
| ✅ 推荐使用子代理的场景 | ❌ 避免使用子代理的场景 |
|---|---|
| 任务需要吞吐大量的 Token(如文件阅读、信息搜索) | 任务非常简单且聚焦 |
| 需要并行处理相互独立的研究任务 | 简单的线性顺序处理即可完成 |
| 希望按特定能力隔离工具箱,避免主节点工具泛滥 | 当前工具集完全可以安全共存,且不越界 |
基础实战:不带流式输出的 Subagent
最简单的子代理模式不需要任何黑魔法。主代理只需要拥有一个在 execute 函数中调用另一个 Agent 的 Tool。
由于阿里千问 (Qwen) 已经完美兼容 OpenAI 的接口规范,我们可以直接使用 @ai-sdk/openai 配合 DashScope 的 Endpoint 来驱动我们的 Agent。
TypeScript
php
import { ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';
import { createOpenAI } from '@ai-sdk/openai';
// 配置阿里千问大模型
const dashscope = createOpenAI({
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
apiKey: process.env.DASHSCOPE_API_KEY,
});
const qwenModel = dashscope('qwen-max');
// 1. 定义一个专门用于"深度调研"的子代理
const researchSubagent = new ToolLoopAgent({
model: qwenModel,
instructions: `你是一个专业的学术和数据分析代理。
请自主完成调研任务。
IMPORTANT: 当你完成所有步骤后,请在最终回复中写下条理清晰的研究总结。`,
tools: {
// 假设这些是你写好的基础工具
read: readFileTool,
search: searchTool,
},
});
// 2. 创建一个工具,将工作委托给子代理
const researchTool = tool({
description: '深入研究一个主题或问题。',
inputSchema: z.object({
task: z.string().describe('需要完成的调研任务描述'),
}),
// 传入 abortSignal 以便在用户取消请求时中断子代理
execute: async ({ task }, { abortSignal }) => {
const result = await researchSubagent.generate({
prompt: task,
abortSignal,
});
return result.text; // 将总结文本返回给主代理
},
});
// 3. 主代理:专注于核心调度
export const mainAgent = new ToolLoopAgent({
model: qwenModel,
instructions: '你是一个全能的私人助理。遇到需要深度查阅或调研的问题时,请委派给 researchTool。',
tools: {
research: researchTool,
},
});
在这个模式下,主节点发起调用后会一直等待,直到子代理吐出最终文本。
(💡 Tip: 如果主流程被取消,abortSignal 会抛出 AbortError。为了防止后续对话出现不完整的工具调用记录,可以在解析历史记录时使用 convertToModelMessages(messages, { ignoreIncompleteToolCalls: true }))
进阶:流式进度展示 (Streaming Progress)
在真实的生产环境中,子代理干活可能需要好几十秒。为了用户体验,我们必须把子代理"正在搜索XXX"、"正在阅读XXX"的中间状态实时推送到前端 UI。
Vercel AI SDK 通过 async function* (异步生成器) 和 readUIMessageStream 优雅地解决了这个问题:
TypeScript
php
import { readUIMessageStream, tool } from 'ai';
const streamingResearchTool = tool({
description: '深入研究一个主题并返回流式进度。',
inputSchema: z.object({
task: z.string(),
}),
// 1. 将 execute 改为 async function*
execute: async function* ({ task }, { abortSignal }) {
// 2. 调用子代理的 stream 方法
const result = await researchSubagent.stream({
prompt: task,
abortSignal,
});
// 3. 拦截并 yield 出完整的、不断累加的 UI 消息状态
for await (const message of readUIMessageStream({
stream: result.toUIMessageStream(),
})) {
yield message;
}
},
});
核心秘籍:精准控制主模型"所见内容"
子代理之所以能解决"上下文爆炸",关键在于前端 UI 和主模型看到的内容是不一样的。
用户在 UI 上需要看到子代理调用的每一个工具、每一次思考,但主代理只需要看最后的那句"总结"。我们可以利用 toModelOutput 完美实现这种信息差:
TypeScript
javascript
const advancedResearchTool = tool({
// ...前面的 inputSchema 和 execute 逻辑保持不变...
toModelOutput: ({ output: message }) => {
// 从子代理的一大堆流式输出中,只提取最后一段文本作为总结
const lastTextPart = message?.parts.findLast(p => p.type === 'text');
return {
type: 'text',
value: lastTextPart?.text ?? '任务已完成。',
};
},
});
这样一来,子代理可能在背后消耗了 10 万 Token 来翻阅资料,但主代理的 Context Window 里仅仅增加了几百 Token 的总结。极大地保持了主节点逻辑的清晰!
在 UI 层优雅渲染
对于熟悉 React 生态的开发者来说(尤其是习惯了组件化思维或者刚从 Vue 3 迁移到 React 的朋友),结合 @ai-sdk/react 的 useChat Hook 来捕获这套复杂状态简直如丝般顺滑。
通过检查 part.state 和 part.preliminary,我们可以精准渲染出主代理的文本与子代理的动态进度:
TypeScript
ini
'use client';
import { useChat } from '@ai-sdk/react';
import type { InferAgentUIMessage } from 'ai';
// 假设你导出了 mainAgent
import type { MainAgentMessage } from '@/lib/agents';
export function Chat() {
const { messages } = useChat<MainAgentMessage>();
return (
<div className="flex flex-col gap-4">
{messages.map(message =>
message.parts.map((part, i) => {
// 渲染主代理对话
if (part.type === 'text') {
return <p key={i} className="text-gray-800">{part.text}</p>;
}
// 渲染子代理 (Research Tool) 状态
if (part.type === 'tool-research') {
return (
<div key={i} className="bg-gray-50 p-4 rounded-lg">
{part.state !== 'input-streaming' && (
<div className="font-bold">🔎 正在调研: {part.input.task}</div>
)}
{/* 捕获并渲染子代理返回的流式 / 最终结果 */}
{part.state === 'output-available' && (
<div className="mt-2 border-l-2 border-blue-400 pl-2">
{part.output.parts.map((nestedPart, j) => {
if (nestedPart.type === 'text') {
return <p key={j}>{nestedPart.text}</p>;
}
return null;
})}
</div>
)}
</div>
);
}
return null;
})
)}
</div>
);
}
⚠️ 避坑指南
在使用 Subagents 时,有几个细节需要特别留意:
- 不支持人工审批 :子代理内部的工具不能配置
needsApproval,所有的工具必须能够自动化静默执行。 - 默认上下文隔离 :子代理的每次调用都是一个白纸般的全新上下文。如果你确实需要让它知道主聊天的历史,需要手动在
execute中拼接messages(但这往往会削弱子代理"卸载上下文"的核心优势,建议谨慎使用)。
结语
子代理 (Subagents) 是将 AI 应用从"玩具"推向"企业级系统"的关键一环。它以极低的认知成本,赋予了单一 Agent 分治、并行和长文本处理的拓展能力。
如果你觉得有帮助,欢迎在评论区交流你在开发复杂 Agent 系统时遇到的痛点!👏