什么是 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. 1. 提示词冲突:不同 Skill 的指令风格、工作流程、输出格式相互干扰,Agent 不知道该遵循哪个

  2. 2. 工具选择混乱:多个 Skill 各自推荐使用不同的工具,模型在工具选择时频繁出错

  3. 3. 上下文窗口被挤占:多个 Skill 的内容同时注入,留给实际对话的空间变少

  4. 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

实现原理

整体调用流程

子 Agent 注册表 call_agent 工具 主 Agent 用户 子 Agent 注册表 call_agent 工具 主 Agent 用户 alt[需要委派][自行处理] 发送请求判断是否委派call_agent(agentName, description)查找 agentName返回 Agent 模块引用动态 import + createAgent()invoke(messages + 任务描述)执行(可能多轮工具调用)返回最终结果ToolMessage(结果文本)使用自身工具完成整理后的响应

调用流程
调用流程

核心: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

一个 call_agent 工具 + agentName 参数选择

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 会话
});
Langfuse记录
Langfuse记录

错误隔离

子 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. 1. Agent 扫描器src/lib/agents/scanner.js)——让 Agent 可以独立使用。如果子 Agent 不需要单独使用(只通过主 Agent 委派),可以跳过此步:

// scanner.js
{ filename: 'my-new-agent.js', module: myNewAgent },
  1. 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)

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐