第 6 章:子图与并发
supervisor/researcher 子图、asyncio.gather 与汇总。
学习目标
理解当前项目为什么拆出 supervisor 子图和 researcher 子图,以及父图如何并发调用多个 researcher 子图后再汇总结果。
它是什么
子图是已经编译好的 LangGraph,可以作为父图的一个节点,也可以在父节点内部被调用。当前项目中父子图状态 schema 不同,所以 supervisor 在节点内部把工具调用转换成 researcher 输入,再调用 researcher_subgraph.ainvoke(...)。asyncio.gather 用来同时启动多个 researcher,提高吞吐,但也会放大模型请求和工具调用成本。
当前项目怎么用
当前主实现有三层:
| 层级 | 图 | 职责 |
|---|---|---|
| 外层图 | deep_researcher |
澄清用户、生成 research brief、调用 supervisor 子图、生成最终报告 |
| supervisor 子图 | supervisor_subgraph |
决定要不要继续研究、是否发起多个 ConductResearch |
| researcher 子图 | researcher_subgraph |
针对单个 topic 做工具循环,最后压缩成 compressed_research |
关键代码在 supervisor_tools():
research_tasks = [
researcher_subgraph.ainvoke({
"researcher_messages": [HumanMessage(content=tool_call["args"]["research_topic"])],
"research_topic": tool_call["args"]["research_topic"],
}, context=runtime.context)
for tool_call in allowed_conduct_research_calls
]
tool_results = await asyncio.gather(*research_tasks)
然后把每个 researcher 的 compressed_research 包装回 ToolMessage,交给 supervisor 下一轮判断。
新版 API 提醒
当前依赖里 StateGraph 的新签名是:
StateGraph(state_schema, context_schema=None, *, input_schema=None, output_schema=None, ...)
当前主实现和示例都使用 input_schema / output_schema。旧教程里的 input=...、output=...、config_schema=... 只能作为迁移阅读材料,具体对照见第 14 章。
最小真实 Agent
示例文件:06_subgraphs_and_concurrency.py。
示例只保留一个 researcher 子图:
researcher_graph = StateGraph(
ResearcherState,
input_schema=ResearcherState,
output_schema=ResearcherOutput,
)
父图的 run_researchers 节点会并发调用两次:
results = await asyncio.gather(
*(
researcher_graph.ainvoke({"topic": topic}, context=runtime.context)
for topic in state["topics"]
)
)
每个子图内部真实调用一次模型,生成一句摘要。父图只接收 summary 输出,不暴露子图内部消息。
运行
uv run python docs/langgraph-learning/examples/06_subgraphs_and_concurrency.py
预期现象:
- 输出
子图数: 2。 - 输出两条 topic 对应的模型摘要。
- 没有搜索、MCP 或工具调用。
常见误区
以为子图会自动懂父图状态。 不会。父子状态 schema 不同的时候,要在父节点里显式转换输入和输出。
并发等于免费提速。 不是。asyncio.gather 会同时发起多个模型/工具请求,速度可能更快,但费用、限流和失败面都会扩大。
output schema 是安全红线。 它只控制 invoke 返回什么字段,不等于流式输出永远隐藏内部状态;流式章节会单独讲 output_keys 和 subgraph state。
本次真实验证
已使用默认 openai:gpt-5.5 并发运行两个 researcher 子图。结果为:
子图数: 2
- 子图隔离状态是指在 LangGraph 中让子图拥有独立的状态结构与更新逻辑,从而降低与父图的耦合并提升流程复用性与可维护性。
- 学习如何在 LangGraph 中使用 `asyncio.gather` 并发执行多个异步节点或任务以提升工作流效率。
本次没有触发工具、搜索或 MCP;成本来自两个并发的短模型请求。
相关资源
查看示例代码:docs/langgraph-learning/examples/06_subgraphs_and_concurrency.py
"""Chapter 6: invoke two compiled subgraphs concurrently with real models.""" import asyncio from typing import TypedDict from dotenv import load_dotenv from langchain.chat_models import init_chat_model from langchain_core.messages import HumanMessage from langgraph.graph import END, START, StateGraph from langgraph.runtime import Runtime from open_deep_research.configuration import Configuration load_dotenv() class ResearcherState(TypedDict): topic: str summary: str class ResearcherOutput(TypedDict): summary: str class ParentState(TypedDict): topics: list[str] summaries: list[str] configurable_model = init_chat_model( configurable_fields=("model", "max_tokens", "api_key"), ) async def summarize_topic( state: ResearcherState, runtime: Runtime[Configuration], ): settings = runtime.context model = configurable_model.with_config( { "configurable": { "model": settings.research_model, "max_tokens": 120, }, "tags": ["langsmith:nostream"], } ) response = await model.ainvoke( [ HumanMessage( content=f"只用一句中文概括这个 LangGraph 学习主题: {state['topic']}" ) ] ) return {"summary": str(response.content)} researcher_graph = ( StateGraph( ResearcherState, context_schema=Configuration, input_schema=ResearcherState, output_schema=ResearcherOutput, ) .add_node("summarize_topic", summarize_topic) .add_edge(START, "summarize_topic") .add_edge("summarize_topic", END) .compile() ) async def run_researchers( state: ParentState, runtime: Runtime[Configuration], ): results = await asyncio.gather( *( researcher_graph.ainvoke( {"topic": topic}, context=runtime.context, ) for topic in state["topics"] ) ) return {"summaries": [result["summary"] for result in results]} async def main(): parent_graph = ( StateGraph(ParentState, context_schema=Configuration) .add_node("run_researchers", run_researchers) .add_edge(START, "run_researchers") .add_edge("run_researchers", END) .compile() ) result = await parent_graph.ainvoke( { "topics": ["子图隔离状态", "asyncio.gather 并发调用"], "summaries": [], }, context=Configuration.from_env(), ) print(f"子图数: {len(result['summaries'])}") for summary in result["summaries"]: print("- " + summary) if __name__ == "__main__": asyncio.run(main())