用 CopilotKit 实现子代理编排:Supervisor 委派模式与实时委派日志实战
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
CopilotKit 的 Sub-Agents 演示展示了一种典型的「监督者(Supervisor)委派」多代理架构:一个顶层 LLM 作为主管,将research_agent(研究)、writing_agent(写作)、critique_agent(评审)三个专业子代理以工具(tool)的形式暴露出来,并在每次委派时把记录追加到共享的代理状态(agent state)槽位中,让前端以「实时委派日志」的形式逐条呈现整个工作流。读完本文,你将掌握如何在 LangGraph 后端定义可委派的子代理、如何用Command一次性更新共享状态与消息,以及如何用 CopilotKit 前端在聊天界面旁渲染实时日志。
这个 Demo 演示了什么
本 Demo(源码位于 showcase/integrations/built-in-agent/src/app/demos/subagents)的核心是带实时日志的多代理委派:Supervisor LLM 编排三个以工具形式暴露的专职子代理,每一次委派都通过共享代理状态实时流式进入 UI。它由三部分构成:
- 三个专职子代理:
research_agent(收集事实)、writing_agent(起草文本)、critique_agent(评审草稿),每一个都是完整的create_agent(...),拥有自己独立的系统提示词、独立的 LLM 调用上下文。 - 子代理即工具:Supervisor 通过
@tool包装调用它们;每个包装器运行对应子代理,并向共享的delegations状态槽位追加一条记录。 - 实时委派日志:左侧面板从代理状态中渲染
delegations,随着 Supervisor 向外分发任务而不断增长。
从消息流的角度看,这是一个经典的「research → write → critique」流水线:研究代理产出事实要点,写作代理基于要点产出草稿,评审代理对草稿给出改进建议,最终由 Supervisor 汇总成面向用户的最终回答。
如何交互
点击建议提示(suggestion chip),或直接输入自己的指令,例如:
- "Produce a short blog post about the benefits of cold exposure training. Research first, then write, then critique."
- "Explain how large language models handle tool calling. Research, write a paragraph, then critique."
- "Summarize the current state of reusable rockets in 1 polished paragraph, with research and critique."
在 Supervisor 执行 research → write → critique 的过程中,右侧聊天区的SubAgentActivityCard会随工具调用实时出现,左侧委派日志会持续填充。建议提示的具体实现位于 showcase/integrations/built-in-agent/src/app/demos/subagents/suggestions.ts,页面骨架与左右分栏布局见 demo-layout.tsx。
核心模式:子代理即工具(Sub-Agents-as-Tools)
这一节的架构理念是理解整个 Demo 的关键。Supervisor 本身也是一个 LLM 代理,它没有直接"调用"子代理的魔法通道,而是把三个子代理分别封装成工具函数注册在自己的工具列表里。Supervisor LLM 通过常规的 tool-calling 机制"调用"这些工具来委派工作;每次工具调用都会同步运行对应的子代理,记录委派,并把子代理的输出以ToolMessage的形式交还给 Supervisor,供其下一步决策读取。
参考实现位于 showcase/integrations/langgraph-python/src/agents/subagents.py,其顶部注释对该模式做了精确定义:
Each sub-agent is a full
create_agent(...)under the hood. Every delegation appends an entry to thedelegationsslot in shared agent state so the UI can render a live "delegation log" as the supervisor fans work out and collects results. This is the canonical LangGraph sub-agents-as-tools pattern, adapted to surface delegation events to the frontend via CopilotKit's shared-state channel.
三个子代理使用同一个模型gpt-5.4,但各有独立的系统提示词、独立的内存与工具集——它们与 Supervisor 之间不共享记忆或工具,Supervisor 只能看到子代理的返回值:
_research_agent = create_agent( model=_sub_model, tools=[], system_prompt=( "You are a research sub-agent. Given a topic, produce a concise " "bulleted list of 3-5 key facts. No preamble, no closing." ), middleware=[HeaderForwardingMiddleware()], ) _writing_agent = create_agent( model=_sub_model, tools=[], system_prompt=( "You are a writing sub-agent. Given a brief and optional source " "facts, produce a polished 1-paragraph draft. Be clear and " "concrete. No preamble." ), middleware=[HeaderForwardingMiddleware()], ) _critique_agent = create_agent( model=_sub_model, tools=[], system_prompt=( "You are an editorial critique sub-agent. Given a draft, give " "2-3 crisp, actionable critiques. No preamble." ), middleware=[HeaderForwardingMiddleware()], )注意此处只挂载了HeaderForwardingMiddleware(用于把入站请求的x-*头透传到子代理的出站 LLM 调用,保证 aimock 录制回放匹配),而刻意不挂载完整的CopilotKitMiddleware——因为 Supervisor 已经为整次运行承担了 App-Context 与前端工具注入的职责,若在子代理上重复挂载会双重注入 prompt 状态。
委派工具的返回:一条 Command 做两件事
每个委派工具的核心返回值是 LangGraph 的Command,它在一次更新里同时完成两件事:向共享状态追加委派记录 + 把子代理结果作为工具消息喂回对话流。_delegation_update辅助函数封装了这一逻辑:
def _delegation_update(sub_agent, task, result, tool_call_id) -> Command: entry: Delegation = { "id": str(uuid.uuid4()), "sub_agent": sub_agent, "task": task, "status": "completed", "result": result, } return Command( update={ "delegations": [entry], "messages": [ ToolMessage( content=result, name=sub_agent, id=str(uuid.uuid4()), tool_call_id=tool_call_id, ) ], } )这里有两点值得展开:
delegations只追加新条目。因为AgentState.delegations上的 reducer 是operator.add(列表拼接),Command.update里绝不能回显已有的delegations,否则每步都会把历史记录重复追加一遍。ToolMessage必须带上tool_call_id。它来自ToolRuntime.tool_call_id,用于把子代理的返回结果正确关联到 Supervisor 发出的那次工具调用上,Supervisor 才能在下一步读取并继续。
三个工具research_agent、writing_agent、critique_agent都是@tool包装:内部调用_invoke_sub_agent(...)运行对应的create_agent实例,再从消息列表中倒序找出本次任务产出的最终文本(跳过只携带 tool_calls 的空AIMessage,并兼容内容块列表形式的流式返回),取不到时返回哨兵值"<sub-agent produced no output>"。
防循环:critique 单次上限
Supervisor LLM 偶尔会在同一份草稿上反复调用critique_agent,每次重跑产出近乎相同的输出。参考实现用模块级常量_MAX_CRITIQUE_ITERATIONS = 1硬性限流:工具内部读取runtime.state中已有的delegations,统计critique_agent的调用次数,达到上限后不再追加委派记录,而是直接返回一条引导 Supervisor 收尾的ToolMessage:
if prior_critiques >= _MAX_CRITIQUE_ITERATIONS: skip_message = ( "Critique already produced for this run. " "Stop calling critique_agent and return your final answer " "to the user now." ) return Command(update={"messages": [ToolMessage(...)]})选择返回 no-op 消息而非抛异常,是因为抛错会表现为失败的工具调用,从而打断 Supervisor 的最终总结。之所以不追加delegations条目,是因为 UI 按"一条委派一张卡片"渲染,一次 Supervisor 运行中评审卡片应当恰好只有一张。
共享状态:AgentState扩展
AgentState继承自langchain.agents.AgentState,只新增了一个字段delegations:
class AgentState(BaseAgentState): delegations: Annotated[list[Delegation], operator.add]Delegation是一个TypedDict,字段为id、sub_agent(限定取值research_agent/writing_agent/critique_agent)、task、status(当前恒为"completed")与result。
使用operator.addreducer 是必要而非可选的:当同一 Supervisor 步骤内存在并发子代理输出时,LangGraph 会因「同一 step 收到多个更新值」抛出INVALID_CONCURRENT_GRAPH_UPDATE("Can receive only one value per step. Use an Annotated key to handle multiple values."),reducer 负责把多路更新安全合并进单一列表。
最终导出的 Supervisor 图把三个工具与状态 schema 一并传入create_agent:
graph = create_agent( model=ChatOpenAI(model="gpt-5.4"), tools=[research_agent, writing_agent, critique_agent], middleware=[CopilotKitMiddleware()], state_schema=AgentState, system_prompt=(...), # 委派顺序说明 + 每个子代理至多调用一次 )系统提示词中明确了硬性编排规则:非平凡请求一律按research_agent -> writing_agent -> critique_agent顺序委派,每个子代理恰好调用一次,评审返回后不得再调用任何子代理,直接向用户给出融入评审意见的最终回答。
前端实现:CopilotKit 如何把状态变成实时日志
挂载与订阅
页面顶层用CopilotKitProvider 指定agent="subagents"(对应后端注册的代理 id),内部组件通过useAgent订阅状态变更与运行状态:
<CopilotKit runtimeUrl="/api/copilotkit" agent="subagents"> <DemoContent /> </CopilotKit>const { agent } = useAgent({ agentId: "subagents", updates: [UseAgentUpdate.OnStateChanged, UseAgentUpdate.OnRunStatusChanged], });UseAgentUpdate.OnStateChanged:让agent.state在每次后端共享状态更新时刷新,驱动委派日志增长;UseAgentUpdate.OnRunStatusChanged:让agent.isRunning实时反映 Supervisor 是否仍在运行,驱动"Supervisor running"徽标与活跃子代理横幅。
随后从状态中取出委派列表并推断当前活跃子代理:
const agentState = agent.state as SubagentsAgentState | undefined; const delegations = agentState?.delegations ?? []; const isRunning = agent.isRunning; const activeSubAgent = isRunning ? inferActiveSubAgent(delegations, agent.messages) : null;inferActiveSubAgent(见 active-subagent.ts)结合委派记录与消息流推断正在执行的子代理,供顶部的 supervisor-activity-banner.tsx 展示"正在执行哪个子代理、任务是什么"。
工具内联渲染:聊天流中的活动卡片
除侧边日志外,useRenderTool为三个子代理工具分别注册了渲染器,把"Researcher is running task Y"这样的活动以卡片形式内联进聊天流,用户无需盯着侧栏也能看到进展。每个工具渲染器接收流式参数、最终结果与一个inProgress → executing → complete的状态机:
useRenderTool( { name: "research_agent", parameters: z.object({ task: z.string() }), render: ({ parameters, status, result }) => ( <SubAgentActivityCard subAgent="research_agent" task={parameters?.task} status={status as SubAgentToolStatus} result={typeof result === "string" ? result : undefined} /> ), }, [], );writing_agent与critique_agent的注册方式完全一致(见 page.tsx),渲染组件实现在 subagent-activity-card.tsx。
委派日志组件
delegation-log.tsx 定义了对齐后端TypedDict的前端类型与日志 UI:
export type SubAgentName = | "research_agent" | "writing_agent" | "critique_agent"; export interface Delegation { id: string; sub_agent: SubAgentName; task: string; status: "completed"; result: string; }日志头部固定展示三个子代理角色指示芯片(无论是否已委派都常驻可见),并用data-fired标记哪些子代理已被调用;标题栏显示实时增长的N calls计数与Supervisor running脉冲徽标;正文区每条记录展示序号、角色徽章、任务描述与结果文本。空态提示文案为:"Ask the supervisor to complete a task. Every sub-agent it calls will appear here." 布局上左侧为日志、右侧为CopilotChat聊天面板,见 demo-layout.tsx。
内置代理(built-in-agent)中的等价实现
在 showcase/integrations/built-in-agent 这个 TypeScript/React 集成里,同一套子代理能力用@tanstack/ai的toolDefinition重新实现(subagent-tools.ts),工具名刻意与 LangGraph 参考实现保持一致(research_agent/writing_agent/critique_agent)——这个对齐是"承重"的:D5 fixtures 按 LangGraph 代理的工具名录制,aimock 也按工具名匹配。
三个角色各自成为一次带独立系统提示词的嵌套chat()调用,同样不共享记忆与工具:
const subagentRoles = [ { id: "research_agent", systemPrompt: "You are a research sub-agent. Given a topic, produce a concise " + "bulleted list of 3-5 key facts. No preamble, no closing.", }, { /* writing_agent ... */ }, { /* critique_agent ... */ }, ] as const; export function buildSubagentTools(parentAbortController: AbortController) { let critiqueCalls = 0; // 每次运行闭包,天然按 run 作用域隔离 return subagentRoles.map((role) => toolDefinition({ name: role.id, description: `Delegate a task to the ${role.id.replace(/_/g, " ")}.`, inputSchema: z.object({ task: z.string().describe(`Task description for the ${role.id.replace(/_/g, " ")}`), }), }).server(async ({ task }) => { if (role.id === "critique_agent") { critiqueCalls += 1; if (critiqueCalls > MAX_CRITIQUE_ITERATIONS) { return { role: role.id, text: "Critique already provided for this draft; skipping further review." }; } } const text = await chat({ adapter: openaiText("gpt-5.4", { fetch: forwardingFetch }), messages: [{ role: "user", content: task }], systemPrompts: [role.systemPrompt], abortController: parentAbortController, stream: false, }); return { role: role.id, text }; }), ); }三个实现细节值得留意:
- AbortController 逐运行透传:构造器接收父运行(Supervisor run)的
AbortController,子代理的chat()调用随父运行一起取消。若在模块导入期就构建工具,每次都会持有独立的AbortController,用户取消请求无法传导到飞行中的子代理调用,造成孤儿异步任务、token 计费与悬挂 Promise。 - critique 计数用闭包而非模块级:
buildSubagentTools每次运行调用一次,闭包变量天然按运行隔离;若放到模块作用域,上限会跨请求泄漏。 MAX_CRITIQUE_ITERATIONS = 1:与参考实现的_MAX_CRITIQUE_ITERATIONS对齐,防止 Supervisor 对同一草稿反复调用评审,避免聊天中堆叠评审卡片、委派日志出现重复行。
Supervisor 的系统提示词SUBAGENTS_PROMPT(定义于 demo-prompts.ts)是 LangGraph 参考实现提示词的移植,保证三个子代理"确实被调用、按合理顺序、各恰好一次";委派日志本身由/delegations状态槽位驱动,因此提示词无需操心 UI 细节。该代理在 api/copilotkit/route.ts 中注册:
subagents: createBuiltInAgent({ systemPrompt: SUBAGENTS_PROMPT }),并统一挂载在InMemoryAgentRunner上,与页面端agent="subagents"的声明一一对应。
小结:把这套模式移植到自己的项目
从参考实现(langgraph-python/src/agents/subagents.py)与内置代理实现(subagent-tools.ts)中,可以提炼出移植子代理编排的四步通用配方:
- 扩展共享状态:在
AgentState上新增delegations字段,并用operator.add作为 reducer,兼容并发委派写入; - 把子代理封装成工具:每个子代理是独立的
create_agent(...),外层用@tool(LangGraph)或toolDefinition(...).server(...)(@tanstack/ai)包装,工具内同步运行子代理并收集其最终文本; - 一次更新完成两件事:返回
Command(update={"delegations": [...], "messages": [ToolMessage(...)]}),既追加委派记录,又把结果作为工具消息喂回 Supervisor;ToolMessage务必携带正确的tool_call_id; - 前端订阅状态:
useAgent订阅OnStateChanged与OnRunStatusChanged,读取agent.state.delegations渲染实时日志,再用useRenderTool为每个子代理工具注册内联活动卡片。
配套的防循环约束(critique 单次上限)与取消传播(AbortController 透传)虽非必需,却是让真实 LLM 场景下日志干净、资源不泄漏的关键工程细节。更完整的 UI 与交互代码可直接参考 showcase/integrations/built-in-agent/src/app/demos/subagents 目录下的组件,以及与前端字节对齐的 LangGraph 参考实现 showcase/integrations/langgraph-python/src/agents/subagents.py。
【免费下载链接】CopilotKitThe Frontend Stack for Agents & Generative UI. React, Angular, Mobile, Slack, and more. Makers of the AG-UI Protocol项目地址: https://gitcode.com/GitHub_Trending/co/CopilotKit
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考