概览

supervisor pattern 是一种 multi-agent 架构,其中中心 supervisor agent 会协调专门的工作 agent。当任务需要不同类型的专业能力时,这种方法尤其有效。与其构建一个跨领域管理工具选择的 agent,不如创建多个聚焦的专家,并由理解整体工作流的 supervisor 负责协调。 在本教程中,你将构建一个个人助手系统,并通过贴近真实场景的工作流展示这些优势。系统将协调两个职责根本不同的专家:
  • 一个处理日程安排、可用性检查和事件管理的 calendar agent
  • 一个管理沟通、起草消息和发送通知的 email agent
你还将加入 human-in-the-loop 审查,让用户可以按需批准、编辑和拒绝操作,例如外发电子邮件。

为什么使用 supervisor?

Multi-agent 架构允许你将工具分配给不同 worker,每个 worker 都有自己的 prompt 或指令。设想一个可以直接访问所有日历和电子邮件 API 的 agent:它必须从许多相似工具中做选择,理解每个 API 要求的精确格式,并同时处理多个领域。如果性能下降,将相关工具及其 prompt 拆分为逻辑分组会很有帮助,也便于迭代改进。

概念

本教程将介绍以下概念:

设置

安装

本教程需要 langchain 包:
npm install langchain
更多详情,请参阅安装指南

LangSmith

设置 LangSmith,以检查 agent 内部发生了什么。然后设置以下环境变量:
export LANGSMITH_TRACING="true"
export LANGSMITH_API_KEY="..."

组件

你需要从 LangChain 的集成套件中选择一个聊天模型:
👉 Read the OpenAI chat model integration docs
npm install @langchain/openai
import { initChatModel } from "langchain";

process.env.OPENAI_API_KEY = "your-api-key";

const model = await initChatModel("gpt-5.4");

1. 定义工具

首先定义需要结构化输入的工具。在真实应用中,这些工具会调用实际 API,例如 Google Calendar、SendGrid 等。在本教程中,你将使用 stub 来演示这种模式。
import { tool } from "langchain";
import { z } from "zod";

const createCalendarEvent = tool(
  async ({ title, startTime, endTime, attendees, location }) => {
    // Stub:在实践中,这会调用 Google Calendar API、Outlook API 等。
    return `Event created: ${title} from ${startTime} to ${endTime} with ${attendees.length} attendees`;
  },
  {
    name: "create_calendar_event",
    description: "创建日历事件。需要精确的 ISO 日期时间格式。",
    schema: z.object({
      title: z.string(),
      startTime: z.string().describe("ISO 格式:'2024-01-15T14:00:00'"),
      endTime: z.string().describe("ISO 格式:'2024-01-15T15:00:00'"),
      attendees: z.array(z.string()).describe("电子邮件地址"),
      location: z.string().optional(),
    }),
  }
);

const sendEmail = tool(
  async ({ to, subject, body, cc }) => {
    // Stub:在实践中,这会调用 SendGrid、Gmail API 等。
    return `Email sent to ${to.join(', ')} - Subject: ${subject}`;
  },
  {
    name: "send_email",
    description: "通过电子邮件 API 发送邮件。需要格式正确的地址。",
    schema: z.object({
      to: z.array(z.string()).describe("电子邮件地址"),
      subject: z.string(),
      body: z.string(),
      cc: z.array(z.string()).optional(),
    }),
  }
);

const getAvailableTimeSlots = tool(
  async ({ attendees, date, durationMinutes }) => {
    // Stub:在实践中,这会查询日历 API
    return ["09:00", "14:00", "16:00"];
  },
  {
    name: "get_available_time_slots",
    description: "检查指定日期中给定参会者的日历可用性。",
    schema: z.object({
      attendees: z.array(z.string()),
      date: z.string().describe("ISO 格式:'2024-01-15'"),
      durationMinutes: z.number(),
    }),
  }
);

2. 创建专门的 sub-agents

接下来,你将创建处理各个领域的专门 sub-agents。

创建 calendar agent

Calendar agent 会理解自然语言日程安排请求,并将其转换为精确的 API 调用。它会处理日期解析、可用性检查和事件创建。
import { createAgent } from "langchain";

const CALENDAR_AGENT_PROMPT = `
你是日历日程安排助手。
将自然语言日程安排请求(例如“下周二下午 2 点”)
解析为正确的 ISO 日期时间格式。
需要时使用 get_available_time_slots 检查可用性。
如果没有合适的时间段,请停止并在回复中确认不可用。
使用 create_calendar_event 安排事件。
始终在最终回复中确认已安排的内容。
`.trim();

const calendarAgent = createAgent({
  model: llm,
  tools: [createCalendarEvent, getAvailableTimeSlots],
  systemPrompt: CALENDAR_AGENT_PROMPT,
});
测试 calendar agent,查看它如何处理自然语言日程安排:
const query = "将团队会议安排在下周二下午 2 点,持续 1 小时";

const stream = await calendarAgent.stream({
  messages: [{ role: "user", content: query }]
});

for await (const step of stream) {
  for (const update of Object.values(step)) {
    if (update && typeof update === "object" && "messages" in update) {
      for (const message of update.messages) {
        console.log(message.toFormattedString());
      }
    }
  }
}
================================== Ai Message ==================================
Tool Calls:
  get_available_time_slots (call_EIeoeIi1hE2VmwZSfHStGmXp)
 Call ID: call_EIeoeIi1hE2VmwZSfHStGmXp
  Args:
    attendees: []
    date: 2024-06-18
    duration_minutes: 60
================================= Tool Message =================================
Name: get_available_time_slots

["09:00", "14:00", "16:00"]
================================== Ai Message ==================================
Tool Calls:
  create_calendar_event (call_zgx3iJA66Ut0W8S3NpT93kEB)
 Call ID: call_zgx3iJA66Ut0W8S3NpT93kEB
  Args:
    title: 团队会议
    start_time: 2024-06-18T14:00:00
    end_time: 2024-06-18T15:00:00
    attendees: []
================================= Tool Message =================================
Name: create_calendar_event

Event created: 团队会议 from 2024-06-18T14:00:00 to 2024-06-18T15:00:00 with 0 attendees
================================== Ai Message ==================================

团队会议已安排在下周二 6 月 18 日下午 2:00,持续 1 小时。如果需要添加参会者或地点,请告诉我!
Agent 将“下周二下午 2 点”解析为 ISO 格式("2024-01-16T14:00:00"),计算结束时间,调用 create_calendar_event,并返回自然语言确认。

创建 email agent

Email agent 负责消息撰写和发送。它专注于提取收件人信息、生成合适的主题行和正文,并管理电子邮件沟通。
const EMAIL_AGENT_PROMPT = `
你是电子邮件助手。
根据自然语言请求撰写专业邮件。
提取收件人信息,并生成合适的主题行和正文。
使用 send_email 发送消息。
始终在最终回复中确认已发送的内容。
`.trim();

const emailAgent = createAgent({
  model: llm,
  tools: [sendEmail],
  systemPrompt: EMAIL_AGENT_PROMPT,
});
使用自然语言请求测试 email agent:
const query = "向设计团队发送提醒,请他们审阅新的 mockups";

const stream = await emailAgent.stream({
  messages: [{ role: "user", content: query }]
});

for await (const step of stream) {
  for (const update of Object.values(step)) {
    if (update && typeof update === "object" && "messages" in update) {
      for (const message of update.messages) {
        console.log(message.toFormattedString());
      }
    }
  }
}
================================== Ai Message ==================================
Tool Calls:
  send_email (call_OMl51FziTVY6CRZvzYfjYOZr)
 Call ID: call_OMl51FziTVY6CRZvzYfjYOZr
  Args:
    to: ['design-team@example.com']
    subject: 提醒:请审阅新的 mockups
    body: 设计团队你好,

这是一封友好提醒,请你们尽早审阅新的 mockups。你们的反馈很重要,可以帮助确保项目时间线按计划推进。

如果有任何问题或需要更多信息,请告诉我。

谢谢!

此致,
================================= Tool Message =================================
Name: send_email

Email sent to design-team@example.com - Subject: 提醒:请审阅新的 mockups
================================== Ai Message ==================================

我已向设计团队发送提醒,请他们审阅新的 mockups。如果你还需要围绕这个主题进行其他沟通,请告诉我!
Agent 会从非正式请求中推断收件人,生成专业的主题行和正文,调用 send_email,并返回确认。每个 sub-agent 都聚焦于狭窄任务,并配有特定领域的工具和 prompt,因此能更好地完成自己的任务。

3. 将 sub-agents 包装成工具

现在,将每个 sub-agent 包装成 supervisor 可以调用的工具。这是创建分层系统的关键架构步骤。Supervisor 会看到 "schedule_event" 这样的高层工具,而不是 "create_calendar_event" 这样的低层工具。
const scheduleEvent = tool(
  async ({ request }) => {
    const result = await calendarAgent.invoke({
      messages: [{ role: "user", content: request }]
    });
    const lastMessage = result.messages[result.messages.length - 1];
    return lastMessage.text;
  },
  {
    name: "schedule_event",
    description: `
使用自然语言安排日历事件。

当用户想要创建、修改或检查日历约会时使用。
处理日期/时间解析、可用性检查和事件创建。

输入:自然语言日程安排请求(例如“下周二下午 2 点和设计团队开会”)
    `.trim(),
    schema: z.object({
      request: z.string().describe("自然语言日程安排请求"),
    }),
  }
);

const manageEmail = tool(
  async ({ request }) => {
    const result = await emailAgent.invoke({
      messages: [{ role: "user", content: request }]
    });
    const lastMessage = result.messages[result.messages.length - 1];
    return lastMessage.text;
  },
  {
    name: "manage_email",
    description: `
使用自然语言发送电子邮件。

当用户想要发送通知、提醒或任何电子邮件沟通时使用。
处理收件人提取、主题生成和邮件撰写。

输入:自然语言电子邮件请求(例如“向他们发送会议提醒”)
    `.trim(),
    schema: z.object({
      request: z.string().describe("自然语言电子邮件请求"),
    }),
  }
);
工具描述会帮助 supervisor 判断何时使用每个工具,因此要让描述清晰且具体。这里仅返回 sub-agent 的最终响应,因为 supervisor 不需要看到中间推理或工具调用。

4. 创建 supervisor agent

现在创建用于编排 sub-agents 的 supervisor。Supervisor 只会看到高层工具,并在领域层级做路由决策,而不是在单个 API 层级做决策。
const SUPERVISOR_PROMPT = `
你是乐于助人的个人助手。
你可以安排日历事件并发送电子邮件。
将用户请求拆解为合适的工具调用,并协调结果。
当请求涉及多个操作时,按顺序使用多个工具。
`.trim();

const supervisorAgent = createAgent({
  model: llm,
  tools: [scheduleEvent, manageEmail],
  systemPrompt: SUPERVISOR_PROMPT,
});

5. 使用 supervisor

现在,使用需要跨多个领域协调的复杂请求测试完整系统:

示例 1:简单单领域请求

const query = "将团队站会安排在明天上午 9 点";

const stream = await supervisorAgent.stream({
  messages: [{ role: "user", content: query }]
});

for await (const step of stream) {
  for (const update of Object.values(step)) {
    if (update && typeof update === "object" && "messages" in update) {
      for (const message of update.messages) {
        console.log(message.toFormattedString());
      }
    }
  }
}
================================== Ai Message ==================================
Tool Calls:
  schedule_event (call_mXFJJDU8bKZadNUZPaag8Lct)
 Call ID: call_mXFJJDU8bKZadNUZPaag8Lct
  Args:
    request: 将 Alice 和 Bob 的团队站会安排在明天上午 9 点。
================================= Tool Message =================================
Name: schedule_event

团队站会已安排在明天上午 9:00,参会者包括 Alice 和 Bob。如果你需要修改或添加更多详情,请告诉我!
================================== Ai Message ==================================

Alice 和 Bob 的团队站会已安排在明天上午 9:00。如果还需要其他安排或调整,请告诉我!
Supervisor 会识别出这是日历任务,调用 schedule_event,然后 calendar agent 处理日期解析和事件创建。
如需完整了解信息流,包括每次聊天模型调用的 prompt 和响应,请查看上述运行的 LangSmith trace

示例 2:复杂多领域请求

const query =
  "将和设计团队的会议安排在下周二下午 2 点,持续 1 小时," +
  "并给他们发送电子邮件提醒,请他们审阅新的 mockups。";

const stream = await supervisorAgent.stream({
  messages: [{ role: "user", content: query }]
});

for await (const step of stream) {
  for (const update of Object.values(step)) {
    if (update && typeof update === "object" && "messages" in update) {
      for (const message of update.messages) {
        console.log(message.toFormattedString());
      }
    }
  }
}
================================== Ai Message ==================================
Tool Calls:
  schedule_event (call_YA68mqF0koZItCFPx0kGQfZi)
 Call ID: call_YA68mqF0koZItCFPx0kGQfZi
  Args:
    request: 下周二下午 2 点和设计团队开会,持续 1 小时
  manage_email (call_XxqcJBvVIuKuRK794ZIzlLxx)
 Call ID: call_XxqcJBvVIuKuRK794ZIzlLxx
  Args:
    request: 给设计团队发送电子邮件提醒,请他们审阅新的 mockups
================================= Tool Message =================================
Name: schedule_event

你与设计团队的会议已安排在下周二 6 月 18 日下午 2:00 到 3:00。如果你需要添加更多详情或进行修改,请告诉我!
================================= Tool Message =================================
Name: manage_email

我已向设计团队发送电子邮件提醒,请他们审阅新的 mockups。如果需要包含更多信息或收件人,请告诉我!
================================== Ai Message ==================================

你与设计团队的会议已安排在下周二 6 月 18 日下午 2:00 到 3:00。

我也已向设计团队发送电子邮件提醒,请他们审阅新的 mockups。

如果你想向会议添加更多详情,或在邮件中加入其他信息,请告诉我!
Supervisor 识别出该请求同时需要日历和电子邮件操作,于是为会议调用 schedule_event,然后为提醒调用 manage_email。每个 sub-agent 完成自己的任务,supervisor 再将两个结果合成为连贯响应。
请参阅 LangSmith trace,查看上述运行的详细信息流,包括每次聊天模型调用的 prompt 和响应。

完整工作示例

以下是整合后的可运行脚本:

理解架构

你的系统有三层。底层包含需要精确格式的严格 API 工具。中间层包含 sub-agents,它们接收自然语言,将其转换为结构化 API 调用,并返回自然语言确认。顶层包含 supervisor,它会路由到高层能力并综合结果。 这种关注点分离带来多个好处:每一层都有聚焦的职责,你可以添加新领域而不影响现有领域,也可以独立测试和迭代每一层。

6. 添加 human-in-the-loop 审查

对于敏感操作,加入 human-in-the-loop 审查 是谨慎做法。LangChain 提供内置 middleware 用于审查工具调用,在本例中也就是 sub-agents 调用的工具。 为两个 sub-agents 添加 human-in-the-loop 审查:
  • create_calendar_eventsend_email 工具配置为触发中断,并允许所有响应类型approveeditreject
  • 仅向顶层 agent 添加 checkpointer。暂停和恢复执行需要它。
import { createAgent, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";

const calendarAgent = createAgent({
  model: llm,
  tools: [createCalendarEvent, getAvailableTimeSlots],
  systemPrompt: CALENDAR_AGENT_PROMPT,
  middleware: [ 
    humanInTheLoopMiddleware({
      interruptOn: { create_calendar_event: true },
      descriptionPrefix: "日历事件待批准",
    }),
  ],
});

const emailAgent = createAgent({
  model: llm,
  tools: [sendEmail],
  systemPrompt: EMAIL_AGENT_PROMPT,
  middleware: [ 
    humanInTheLoopMiddleware({
      interruptOn: { send_email: true },
      descriptionPrefix: "外发电子邮件待批准",
    }),
  ],
});

const supervisorAgent = createAgent({
  model: llm,
  tools: [scheduleEvent, manageEmail],
  systemPrompt: SUPERVISOR_PROMPT,
  checkpointer: new MemorySaver(),
});
重复同一个查询。请注意,这里会将中断事件收集到列表中,便于后续访问:
const query =
  "将和设计团队的会议安排在下周二下午 2 点,持续 1 小时," +
  "并给他们发送电子邮件提醒,请他们审阅新的 mockups。";

const config = { configurable: { thread_id: "6" } };

const interrupts: any[] = [];
const stream = await supervisorAgent.stream(
  { messages: [{ role: "user", content: query }] },
  config
);

for await (const step of streamA) {
  for (const update of Object.values(step)) {
    for (const message of update.messages) {
      console.log(message.toFormattedString());
    }
    const interrupt = update.__interrupt__?.[0];
    interrupts.push(interrupt);
    console.log(`\nINTERRUPTED: ${interrupt?.id}`);
  }
}
================================== Ai Message ==================================
Tool Calls:
  schedule_event (call_t4Wyn32ohaShpEZKuzZbl83z)
 Call ID: call_t4Wyn32ohaShpEZKuzZbl83z
  Args:
    request: 将和设计团队的会议安排在下周二下午 2 点,持续 1 小时。
  manage_email (call_JWj4vDJ5VMnvkySymhCBm4IR)
 Call ID: call_JWj4vDJ5VMnvkySymhCBm4IR
  Args:
    request: 给设计团队发送电子邮件提醒,请他们在下周二下午 2 点的会议前审阅新的 mockups。

INTERRUPTED: 4f994c9721682a292af303ec1a46abb7

INTERRUPTED: 2b56f299be313ad8bc689eff02973f16
这一次执行被中断了。检查中断事件:
for (const interrupt of interrupts) {
  for (const request of interrupt.value.actionRequests) {
    console.log(`INTERRUPTED: ${interrupt.id}`);
    console.log(`${request.description}\n`);
  }
}
INTERRUPTED: 4f994c9721682a292af303ec1a46abb7
日历事件待批准

Tool: create_calendar_event
Args: {'title': '和设计团队开会', 'start_time': '2024-06-18T14:00:00', 'end_time': '2024-06-18T15:00:00', 'attendees': ['design team']}

INTERRUPTED: 2b56f299be313ad8bc689eff02973f16
外发电子邮件待批准

Tool: send_email
Args: {'to': ['designteam@example.com'], 'subject': '提醒:请在下周二下午 2 点会议前审阅新的 mockups', 'body': "团队你好,\n\n这是一封提醒,请在下周二下午 2 点的会议前审阅新的 mockups。你们的反馈和见解会对我们的讨论和后续步骤很有价值。\n\n请确保你们已经看过设计,并准备好在会议中分享想法。\n\n谢谢!\n\n此致,\n[你的名字]"}
你可以使用 Command 并通过 ID 引用每个中断,为其指定决策。更多细节请参阅 human-in-the-loop guide。为了演示,这里会接受日历事件,但编辑外发电子邮件的主题:
import { Command } from "@langchain/langgraph";

const resume: Record<string, any> = {};
for (const interrupt of interrupts) {
  const actionRequest = interrupt.value.actionRequests[0];
  if (actionRequest.name === "send_email") {
    // 编辑电子邮件
    const editedAction = { ...actionRequest };
    editedAction.args.subject = "Mockups 提醒";
    resume[interrupt.id] = {
      decisions: [{ type: "edit", editedAction }]
    };
  } else {
    resume[interrupt.id] = { decisions: [{ type: "approve" }] };
  }
}

const resumeStream = await supervisorAgent.stream(
  new Command({ resume }),
  config
);

for await (const step of resumeStream) {
  for (const update of Object.values(step)) {
    if (update && typeof update === "object" && "messages" in update) {
      for (const message of update.messages) {
        console.log(message.toFormattedString());
      }
    }
  }
}
================================= Tool Message =================================
Name: schedule_event

你与设计团队的会议已安排在下周二 6 月 18 日下午 2:00 到 3:00。
================================= Tool Message =================================
Name: manage_email

你给设计团队的电子邮件提醒已发送。发送内容如下:

- 收件人:designteam@example.com
- 主题:Mockups 提醒
- 正文:提醒他们在下周二下午 2 点会议前审阅新的 mockups,并请求他们准备反馈和讨论内容。

如果你还需要其他帮助,请告诉我!
================================== Ai Message ==================================

- 你与设计团队的会议已安排在下周二 6 月 18 日下午 2:00 到 3:00。
- 已向设计团队发送电子邮件提醒,请他们在会议前审阅新的 mockups。

如果你还需要其他帮助,请告诉我!
运行会带着你的输入继续执行。

7. 高级:控制信息流

默认情况下,sub-agents 只会接收来自 supervisor 的请求字符串。你可能想传递额外上下文,例如对话历史或用户偏好。

向 sub-agents 传递额外对话上下文

import { getCurrentTaskInput } from "@langchain/langgraph";
import { type BuiltInState, HumanMessage } from "langchain";

const scheduleEvent = tool(
  async ({ request }, config) => {
    // 自定义 sub-agent 接收的上下文
    // 从 config 访问完整线程消息
    const currentMessages = getCurrentTaskInput<BuiltInState>(config).messages;
    const originalUserMessage = currentMessages.find(HumanMessage.isInstance);
    const prompt = `
你正在协助处理以下用户问题:

${originalUserMessage?.content || "没有可用上下文"}

你负责处理以下子请求:

${request}
    `.trim();

    const result = await calendarAgent.invoke({
      messages: [{ role: "user", content: prompt }],
    });
    const lastMessage = result.messages[result.messages.length - 1];
    return lastMessage.text;
  },
  {
    name: "schedule_event",
    description: "使用自然语言安排日历事件。",
    schema: z.object({
      request: z.string().describe("自然语言日程安排请求"),
    }),
  }
);
这允许 sub-agents 查看完整对话上下文,有助于消解“明天同一时间安排它”这类引用前文对话的歧义。
你可以在 LangSmith trace 的聊天模型调用中查看 sub-agent 接收到的完整上下文。

控制 supervisor 接收的信息

你还可以自定义哪些信息会回流到 supervisor:
const scheduleEvent = tool(
  async ({ request }) => {
    const result = await calendarAgent.invoke({
      messages: [{ role: "user", content: request }]
    });

    const lastMessage = result.messages[result.messages.length - 1];

    // 选项 1:仅返回确认消息
    return lastMessage.text;

    // 选项 2:返回结构化数据
    // return JSON.stringify({
    //   status: "success",
    //   event_id: "evt_123",
    //   summary: lastMessage.text
    // });
  },
  {
    name: "schedule_event",
    description: "使用自然语言安排日历事件。",
    schema: z.object({
      request: z.string().describe("自然语言日程安排请求"),
    }),
  }
);
重要: 确保 sub-agent prompts 强调最终消息应包含所有相关信息。常见失败模式是 sub-agents 执行了工具调用,但没有在最终响应中包含结果。
如需查看演示完整 supervisor pattern、human-in-the-loop 审查和高级信息流控制的完整工作示例,请参阅 LangChain.js 示例中的 supervisor_complete.ts

8. 关键要点

Supervisor pattern 会创建多层抽象,其中每一层都有明确职责。设计 supervisor 系统时,应从清晰的领域边界开始,并为每个 sub-agent 提供聚焦的工具和 prompt。为 supervisor 编写清晰的工具描述,在集成前独立测试每一层,并根据具体需求控制信息流。
何时使用 supervisor pattern当你有多个彼此不同的领域(日历、电子邮件、CRM、数据库)、每个领域都有多个工具或复杂逻辑、希望集中控制工作流,并且 sub-agents 不需要直接与用户对话时,请使用 supervisor pattern。对于只有少量工具的简单场景,请使用单个 agent。当 agents 需要与用户对话时,请改用 handoffs。对于 agents 之间的 peer-to-peer 协作,请考虑其他 multi-agent patterns。

后续步骤

了解用于 agent-to-agent 对话的 handoffs,探索 context engineering 以微调信息流,阅读 multi-agent overview 以比较不同模式,并使用 LangSmith 调试和监控你的 multi-agent 系统。