LangChain 实战:用 SubAgent 解决多 Skills 冲突
如果子 Agent 执行了 10 次工具调用,直接合并到主 Agent 需要消耗 20+ 条消息的 Token(每次调用包含请求和响应),但通过 SubAgent 隔离后只消耗 1 条最终结果的 Token。子 Agent 执行时可能调用了十几次工具(读文件、调 API、搜索文档等),这些中间过程对主 Agent 是不可见的,主 Agent 只拿到子 Agent 的最终返回文本。SubAgent
什么是 SubAgent
SubAgent 是 LangChain Multi-Agent 架构中的一种模式:主 Agent 将子 Agent 作为工具协调调用。所有路由经过主 Agent,由它决定何时及如何调用每个子 Agent。
核心特征:
-
• 集中控制:主 Agent 统一接收请求,判断是否委派
-
• 作为工具调用:子 Agent 通过 LangChain Tool 机制调用
-
• 上下文隔离:子 Agent 内部的工具调用过程对主 Agent 不可见,只返回最终结果
-
• 并行执行:主 Agent 可在单轮中调用多个子 Agent
需要注意的是,子 Agent 本身也是独立的 Agent,用户可以直接使用。SubAgent 模式只是在主 Agent 中提供了一种调度机制,让主 Agent 可以按需委派任务给这些子 Agent。
为什么不用 Agent + Skills,而用 SubAgent
先说结论:子 Agent 内部本身就在用 Skills,Skills 是好东西。问题出在"一个 Agent 挂载多个 Skills 时会发生混乱"。
核心问题:多 Skills 混乱
Skills 的工作方式是将专业化的提示词和知识注入到 Agent 的上下文中。当只挂载一个 Skill 时,Agent 行为清晰可控。但当同一个 Agent 挂载多个 Skill 时:
SubAgent + 各自 Skills
call_agent
call_agent
call_agent
主 Agent
图表 Agent + Skill: mermaid
代码助手 + Skill: git-clone, dev-design
环境排查 + Skill: env-resolver
单 Agent + 多 Skills
注入
注入
注入
注入
Skill A: 技术设计文档
主 Agent
Skill B: 测试用例
Skill C: 环境排查
Skill D: 图表生成
混乱的提示词上下文
混乱的具体表现:
-
1. 提示词冲突:不同 Skill 的指令风格、工作流程、输出格式相互干扰,Agent 不知道该遵循哪个
-
2. 工具选择混乱:多个 Skill 各自推荐使用不同的工具,模型在工具选择时频繁出错
-
3. 上下文窗口被挤占:多个 Skill 的内容同时注入,留给实际对话的空间变少
-
4. 错误难以定位:出问题时不知道是哪个 Skill 导致的
解决方案:SubAgent 隔离
每个子 Agent 只挂载自己需要的 Skills,主 Agent 通过 call_agent 按需委派:
call_agent
图表任务
代码分析
环境排查
接口用例
用户请求
主 Agent
路由判断
图表 Agent
代码助手
环境排查 Agent
接口用例 Agent
返回结果给主 Agent
关键区别:
|
维度 |
单 Agent + 多 Skills |
SubAgent(各挂各自的 Skills) |
|---|---|---|
|
Skills 隔离 |
所有 Skills 共享一个上下文,互相干扰 |
每个 Agent 独享自己的 Skills,互不干扰 |
|
提示词清晰度 |
多个 Skill 指令混杂,Agent 困惑 |
每个 Agent 只看到相关指令,行为确定 |
|
错误隔离 |
一个 Skill 出问题影响整个 Agent |
子 Agent 崩溃不影响主 Agent |
|
独立维护 |
改一个 Skill 要考虑对其他 Skill 的影响 |
子 Agent 独立文件,独立迭代 |
|
上下文利用 |
所有 Skill 内容常驻,浪费窗口 |
只有被委派的子 Agent 加载自己的 Skills |
实现原理
整体调用流程
核心:subagentTools.js
子 Agent 注册表
使用动态 import() 打破循环依赖(Agent 模块导入 @/agents/tools,而 subagentTools 也在该目录中):
// 子 Agent 注册表 —— 使用动态 import() 惰性加载,打破循环依赖
const SUBAGENT_REGISTRY = {
'chart-agent': { import: () => import('@/agents/chart-agent'), name: '图表生成助手', description: '...' },
'code-assistant-agent': { import: () => import('@/agents/code-assistant-agent'), name: '代码助手', description: '...' },
// ... 更多子 Agent
};
惰性加载意味着:主 Agent 启动时不会加载所有子 Agent 的代码,只在 call_agent 被调用时才按需导入。
call_agent 工具(Single Dispatch 模式)
LangChain 提供两种将子 Agent 包装为工具的方式:
|
模式 |
说明 |
|---|---|
|
Tool per Agent |
每个子 Agent 一个独立工具 |
| Single Dispatch Tool |
一个 |
KuAI-test 使用 Single Dispatch Tool,子 Agent 数量多时统一管理更方便:
// call_agent 工具 —— 核心调度逻辑(伪代码)
tool(async ({ agentName, description, includeHistory }) => {
// 1. 从注册表查找 → 动态导入 → 创建子 Agent 实例
const agent = await registry[agentName].import().then(m => m.default.createAgent());
// 2. 构建输入:对话历史 + 任务描述(子 Agent 内部工具调用对主 Agent 不可见)
const input = includeHistory
? [...recentMessages, `[委派任务] ${description}`]
: [description];
// 3. 执行子 Agent(streamEvents 追踪工具调用,降级 invoke)
const result = await agent.streamEvents({ messages: input });
// 4. 只返回最终文本结果(中间工具调用过程被隔离)
return result.lastMessage.content;
})
上下文传递
SubAgent 是无状态的——子 Agent 不记住过去交互。主 Agent 通过传递最近 N 条历史消息来提供上下文。
但有一个关键细节:主 Agent 只传递用户对话历史和最终任务描述,不会传递子 Agent 内部的工具调用过程。子 Agent 执行时可能调用了十几次工具(读文件、调 API、搜索文档等),这些中间过程对主 Agent 是不可见的,主 Agent 只拿到子 Agent 的最终返回文本。
子 Agent 返回给主 Agent 的内容
子 Agent 内部
主Agent给子Agent的内容
用户对话历史
任务描述
工具调用: 读取文件
工具调用: 搜索文档
工具调用: 调用 API
...可能十几轮
只有最终文本结果
这意味着:
-
• 子 Agent 的工具调用细节不会污染主 Agent 的上下文,即使子 Agent 内部经历了大量工具调用,主 Agent 的对话历史只增加一条 ToolMessage
-
• 大幅节省 Token:如果子 Agent 执行了 10 次工具调用,直接合并到主 Agent 需要消耗 20+ 条消息的 Token(每次调用包含请求和响应),但通过 SubAgent 隔离后只消耗 1 条最终结果的 Token
-
• 主 Agent 上下文保持干净:不会被大量工具调用日志挤占,留给实际对话的空间更大
通过 maxHistoryLength 控制传递给子 Agent 的历史消息窗口大小,默认 15 条(约 3-5K tokens)。
include/exclude 过滤
支持按需过滤可调用的子 Agent,不同主 Agent 可以暴露不同的子集:
getSubagentTools({ include: ['chart-agent', 'code-assistant-agent'] }); // 只暴露部分
getSubagentTools({ exclude: ['langfuse-analysis-agent'] }); // 排除某些
主 Agent 如何集成
任意 Agent 都可以通过 getSubagentTools 获得子 Agent 调度能力。以 omni-assistant-agent.js 为例:
// 任意 Agent 集成 SubAgent 调度能力(伪代码)
const subagentTools = await getSubagentTools({
userId, sessionId, messages, // 传递用户信息和对话历史
maxHistoryLength: 15, // 限制上下文窗口
});
const agent = createAgent({ model, tools: [...otherTools, ...subagentTools], systemPrompt });
可观测性与错误处理
Langfuse 追踪
每次子 Agent 调用都创建独立的 Langfuse Trace,与主 Agent 的追踪建立父子关系:
// 每次子 Agent 调用创建独立的 Langfuse Trace
createLangfuseHandler({
sessionId,
tags: [agentName, 'subagent'],
metadata: { parentSessionId: sessionId }, // 关联主 Agent 会话
});
错误隔离
子 Agent 调用失败不会阻塞主 Agent:
try {
return await agent.invoke(...);
} catch (error) {
return `子 Agent 调用失败: ${error.message}`; // 主 Agent 收到后可用自身工具补救
}
如何新增子 Agent
创建 Agent 文件
在 src/agents/ 创建新 Agent(使用 defineAgent):
// src/agents/my-new-agent.js
export default defineAgent({
id: 'my-new-agent',
name: '新 Agent',
createAgent: async () => createReactAgent({ llm: model, tools: [...] }),
});
注册到两个地方
-
1. Agent 扫描器(
src/lib/agents/scanner.js)——让 Agent 可以独立使用。如果子 Agent 不需要单独使用(只通过主 Agent 委派),可以跳过此步:
// scanner.js
{ filename: 'my-new-agent.js', module: myNewAgent },
-
2. 子 Agent 注册表(
src/agents/tools/subagentTools.js)——让其他主 Agent 可以委派:
// subagentTools.js 注册表
'my-new-agent': { import: () => import('@/agents/my-new-agent'), name: '新 Agent', description: '...' },
无需修改已有主 Agent 的代码,call_agent 工具的 description 会自动包含新子 Agent。
创建自定义主 Agent
任意 Agent 都可以通过 getSubagentTools 获得子 Agent 调度能力,并通过 include/exclude 控制子集:
const subagentTools = await getSubagentTools({
include: ['chart-agent', 'code-assistant-agent'], // 只包含这两个
userId, sessionId, messages, maxHistoryLength: 10,
});
参考资料
LangChain Multi-Agent 文档:
https://docs.langchain.com/oss/javascript/langchain/multi-agent)
更多推荐




所有评论(0)