LangGraph实战:用图编排搭建多Agent数据分析平台
1. 项目概述LangGraph 到底解决了什么问题前阵子有朋友问我手上有一堆散落的业务数据每天的运营报表、用户反馈、竞品动态靠人工整理太慢想用大模型搭一个自动分析平台但要自己处理数据流转、多步骤执行、状态保存这些东西越搞越乱。我当时的回答是直接用 LangGraph 从 0 到 1 搭一个 Multi-Agent 分析平台把采集、分析、质检、出报告这几个环节拆给不同 Agent 协作状态流转交给图来管理整体会比想象中简单也远比裸调大模型要可控得多。这个项目本身没有特别玄乎的东西核心就一句话用 LangGraph 把多个大模型 Agent 编排成一个有向图每个 Agent 负责分析流程里的一个角色比如数据采集、指标计算、结论生成、质量校验。图上的节点就是单个 Agent 或工具调用边就是流转条件而全局的 State状态负责在各个节点之间传递数据。它解决的核心痛点是多 Agent 系统最难的其实不是“让每个 Agent 能干活”而是“怎么控制它们按顺序干活、怎么共享中间结果、怎么在出错后恢复”。LangGraph 用图结构把这些问题全部显式化于是整个分析平台在工程上变得可设计、可调试、可恢复不再是一堆 Prompt 在互相踩脚。适合看这篇内容的人我觉得分三类第一类是已经用 LangChain 写过简单 Chain、但对多 Agent 编排还是“靠循环if 硬写”的开发者第二类是负责数据分析或数据产品、想把大模型能力引入到自动化报表和分析流程里的工程同学第三类是纯粹想搞懂 LangGraph 状态机、图执行、多智能体协作原理的学习者。下面我按实际搭建顺序来拆从架构设计到核心代码再到我实测踩过的坑尽量把关键的“为什么”说透。2. 架构设计与核心概念拆解2.1 Multi-Agent 分析平台的角色分工一个分析平台如果只挂一个大模型 Agent让它从头干到尾最常见的问题就是一锅粥它又要检索数据又要做数值计算又要写分析结论还要自我检查结果要么工具调用混乱要么中间状态丢失要么生成了看起来很专业但实际漏洞百出的结论。所以 Multi-Agent 的核心思路不是堆多个模型而是把复杂任务拆成不同职能的“专职角色”。我这次设计的分析平台按职能拆成五个节点采集节点Collector负责接收用户输入判断需要哪些数据源并调用搜索或数据库查询工具拿回原始材料加工节点Processor负责清洗、去重、格式整理输出结构化中间数据分析节点Analyst负责对加工后的数据做指标计算、趋势判断和业务解读质检节点Inspector负责检查分析结果中是否有数据矛盾、幻觉、遗漏指标质量不合格就打回重做输出节点Reporter负责把最终分析整理成结构化报告。这个角色的划分有讲究。很多人会问为什么不把加工和分析合并成一个 Agent我的经验是分析 Agent 一旦同时承担“看懂原始数据”和“产出结论”这两件事它容易在结论里脑补一些原始数据里没有的细节因为你喂给它的是未经整理的脏数据它就会“合理”地忽略部分内容。而分离出加工节点后分析 Agent 拿到的是一份干净、字段对齐的数据它的任务边界就非常明确幻觉率明显下降。质检节点放在最后不是为了挑刺而是给整个平台加一道保底防线这在实际业务里极其重要否则分析内容直接给业务方出了问题很麻烦。2.2 LangGraph 的状态机与图执行模型LangGraph 最核心的抽象是 StateGraph。你可以把 State 理解成一条流水线上所有人共用的“工作台单子”上面写着当前做到哪一步、中间产物是什么、哪项任务完成了、哪些任务失败了。节点就是每个工位上的人只干自己的活然后把结果写回工作台单子。边的意义是“下一站怎么走”可以是有向无环的直连也可以是带条件的路由——根据工作台单子上的某个字段决定是走校验通过还是返工。这里我多说一句很多人第一次用 LangGraph 会把它当成“更高级的 Chain”其实不对。Chain 是写死顺序的而 StateGraph 是运行时根据状态动态决定路径的这是质的区别。比如质检节点发现指标异常它可以把状态里的 need_rework 字段设为 true条件边就会把流程拉回加工节点重新处理而不是所有任务都线性跑完。这种能力来自 LangGraph 的底层设计它本质上是一个可暂停、可恢复、可分支的状态机执行引擎而不是 Prompt 顺序拼接。在本次项目中我采用了一个带 supervisor 节点的架构变体。所谓 supervisor就是主控 Agent负责把用户的复杂问题拆解成子任务再决定派发给哪个子 Agent 执行。这样比所有 Agent 都直接接收用户消息更清晰因为用户的需求往往很模糊比如“分析一下本月用户增长放缓的原因”这种问题直接丢给采集 Agent 或分析 Agent 都不合适需要一个“调度员”先想明白要查哪些数据、做哪些分析再分派下去。supervisor 本身也用语言模型驱动但它的职责不是写报告而是出“派工单”。2.3 技术选型为什么选 LangGraph 而不是硬编码如果只是搭一个简单的分析流程说实话用十来个 if-else 加循环也能凑出来。但一旦 Agent 数量超过三个、分支逻辑多起来硬编码的状态管理会失控因为你需要在每个分支里手动维护“哪些中间变量已经生成”“哪些分支已经执行”这种信息代码会迅速膨胀到没法维护。我这次选 LangGraph 的几层理由可以用一张表说清楚方案状态管理分支控制可恢复性调试体验适用规模硬编码 循环全手动容易漏靠变量和 if分支多了逻辑混乱无日志零散2 个以内 Agent裸 LangChain Agent有少量 memory 概念但多 Agent 间难共享不支持复杂路由弱靠回调追踪单 Agent 为主其他多 Agent 框架对话式对话流偏隐式受对话轮次限制一般黑盒感强对话场景强LangGraph显式 State节点共享原生条件边、Send API支持 Checkpoint 恢复可视化完整任意多 Agent 编排当然这不是说 LangGraph 天下第一而是它在本场景下最合适分析平台天然是任务流多、分支多、容错要求高的场景图的显式建模方式刚好贴合这种需求。使用对话式多 Agent 框架在处理“数据采集→清洗→建模→质检”这类固定流程时会比较别扭你得用对话轮数硬拗任务流而且底层状态不容易透出出问题以后很难定位是哪个环节产生了幻觉。LangGraph 则把每一步的状态变化都摆在明面上调试体验好得多。2.4 状态设计State 里到底放什么State 是整个平台的命脉我直接贴出我当时设计的核心结构然后解释每个字段为什么存在from typing import TypedDict, Annotated, List, Optional import operator class AnalysisState(TypedDict): # 用户输入与最终输出 user_request: str final_report: Optional[str] # 工作流控制字段 tasks: Annotated[List[dict], operator.add] # 子任务列表 need_rework: bool # 是否返工 # 数据产物 raw_data: List[dict] # 采集结果 processed_data: List[dict] # 清洗后数据 analysis_result: Optional[str] # 分析中间产出 quality_check: Optional[dict] # 质检结论 # 运行信息 current_role: str # 当前执行角色 errors: Annotated[List[str], operator.add] # 错误记录字段设计有几个关键考虑。首先是tasks用Annotated[List[dict], operator.add]这个写法这告诉 LangGraph多个节点往这个字段里追加内容时用 add 操作而不是覆盖方便主控 Agent 分派多个任务后各子 Agent 各自追加自己的任务结果。其次是所有中间产物字段都设计成 Optional因为不同分支不一定都会产生对应数据比如采集失败时raw_data可能为空质检节点必须能处理这种情况。最后是need_rework这种控制字段它负责驱动条件边做出返工或流转决策。关于 State 设计我有一条血泪经验不要在 State 里塞大模型生成的完整长文本作为中间传递物。比如让分析 Agent 把完整报告写到analysis_result然后传给输出节点这会让图的状态对象越滚越大checkpoint 写入变慢而且在并行分支时容易发生状态竞争。更合理的做法是 intermediary data 只放关键结论和结构化数据摘要最终完整报告在最后一个节点一次性生成。这个小调整能让整个平台的性能和稳定性上一个台阶。3. 完整搭建流程从环境到核心代码3.1 环境准备与依赖安装项目基于 Python 3.10 以上版本我用的是 3.11。这里直接给出我实测可用的安装命令建议在一个干净的虚拟环境里操作python -m venv venv source venv/bin/activate # Windows 下为 venv\Scripts\activate pip install langgraph langchain-core langchain-openai如果你还需要用到联网搜索或数据库查询工具另外安装对应的 SDK 就好这里不展开。有一点需要强调LangGraph 本身对模型接口是抽象的langchain-openai不是唯一选择你可以用任何兼容 OpenAI 接口格式的模型服务或者本地部署的模型服务。只要把ChatOpenAI的 base_url 指向你的端点就行。我第一次搭的时候用的就是一个本地部署模型设置 base_url 之后跑得很顺畅没有额外坑。安装完成后先做一个最简单的验证构建一个只有两个节点的图跑通 invoke确认整个执行链路没问题。我经常看到有人一上来就搭复杂多 Agent结果环境变量都没配好报错了也分不清是模型问题还是框架问题。先走通最小链路后面的排查范围会小很多。3.2 模型层封装与工具注册我习惯在项目里单独建一个llm.py来封装模型实例避免在每个节点里重复写模型初始化。因为分析平台里不同角色对模型能力的要求不完全一样采集节点主要做信息抽取用轻量模型就够分析和质检节点需要更强推理能力可以用更强的模型。所以与其全局统一用一个模型我建议做成参数化的模型工厂from langchain_openai import ChatOpenAI def get_llm(role: str): if role collector: return ChatOpenAI(modelbase-model, temperature0.1) if role analyst: return ChatOpenAI(modelstrong-model, temperature0.2) if role supervisor: return ChatOpenAI(modelstrong-model, temperature0.3) return ChatOpenAI(modelbase-model, temperature0.1)这里temperature的设置是有讲究的。质检和采集场景我需要模型输出尽量稳定所以温度压到 0.1分析场景我希望它保留一点开放性能给出不同角度的解读所以 0.2 到 0.3。但无论哪个角色都不要把温度设到 0.7 以上否则分析结论可能每跑一次都不一样业务方没法信任。工具注册使用tool装饰器。比如一个查询数据库的工具from langchain_core.tools import tool tool def query_business_data(sql: str) - list: 执行只读 SQL 查询并返回结果列表 # 这里调用你自己的数据库连接注意做 SQL 注入过滤和只读限制 return execute_readonly_query(sql)工具函数用 docstring 说明用途特别重要因为模型是通过工具的名字和描述来决定“什么时候该调它”的。描述写得越精确工具调用的误判率越低。我见过把“查询月活趋势”写得太笼统结果模型在分析阶段也去调它而不是直接使用已经加工好的processed_data这种问题通过调整工具描述基本能解决。3.3 核心节点实现采集、分析、质检、输出每个节点本质上就是一个函数接收当前state执行逻辑返回一个新的状态字典。LangGraph 会把你返回的字段合并进全局状态。下面我用分析节点作为例子展示完整写法from langchain_core.messages import HumanMessage, SystemMessage def analyst_node(state: AnalysisState): llm get_llm(analyst) messages [ SystemMessage(content你是业务分析师。请基于给定的结构化数据从趋势、原因、风险三个维度完成分析输出 JSON 格式结论。), HumanMessage(content( f用户问题{state[user_request]}\n f清洗后的数据{json.dumps(state[processed_data], ensure_asciiFalse)[:3000]} )) ] response llm.invoke(messages) # 这里可以加一层输出格式解析把非 JSON 内容修正为 JSON return {analysis_result: response.content, current_role: analyst}注意几个细节。第一我传给模型的数据做了截断只取前三千字符为什么因为processed_data可能很长全量塞进 Prompt 会让上下文迅速膨胀还会稀释模型的注意力。截断可能导致信息丢失解决方式是加工节点在写processed_data时就生成好关键摘要让分析节点优先看摘要。第二节点返回的字典里加了current_role这个字段这在调试时非常有价值一旦流程出错你能从状态里直接看出当前执行到哪个角色排查效率翻倍。质检节点的逻辑稍微复杂一点它要分析上一阶段产出的 JSON 结论检查字段完整性、数值一致性比如报告中写的增长率与输入数据算出来的增长率是否一致、有没有明显幻觉。我给出一个带返工分支的示例def inspector_node(state: AnalysisState): llm get_llm(inspector) check_prompt f请检查以下分析结论是否有问题 分析结论{state[analysis_result]} 原始数据摘要{json.dumps(state[processed_data], ensure_asciiFalse)[:2000]} 你只需要回答 JSON{{passed: true/false, issues: [问题列表]}} resp llm.invoke([HumanMessage(contentcheck_prompt)]) parsed json.loads(resp.content) return { quality_check: parsed, need_rework: not parsed[passed], errors: [] if parsed[passed] else [质检未通过需要返工] }这里返回need_rework就是给条件边做决策用的。需要注意的是质检节点返回的errors用[]而不是直接省略这是为了避免类型不一致导致的合并错误——当你用operator.add做累加时如果某次返回了非列表类型LangGraph 的 reducer 会直接报错。这种小细节在写多节点系统时最容易忽略我后面还会专门讲。3.4 图的编排条件边、并行分支与编译节点都准备好了接下来就是图的组装。这是 LangGraph 项目里最核心的代码也是后续扩展的骨架。我直接给出简化但完整的图构建逻辑from langgraph.graph import StateGraph, START, END from langgraph.types import Send # 任务分派supervisor 负责拆解任务然后并行发送给子 Agent def dispatch_task(state: AnalysisState): tasks state[tasks] return [Send(analyst_worker, {task: t, user_request: state[user_request]}) for t in tasks] def route_after_quality(state: AnalysisState): if state.get(need_rework): return processor return reporter builder StateGraph(AnalysisState) builder.add_node(supervisor, supervisor_node) builder.add_node(collector, collector_node) builder.add_node(processor, processor_node) builder.add_node(analyst, analyst_node) builder.add_node(inspector, inspector_node) builder.add_node(reporter, reporter_node) # 线性主链路 返工环 并行分派 builder.add_edge(START, supervisor) builder.add_edge(supervisor, collector) builder.add_edge(collector, processor) builder.add_conditional_edges(processor, dispatch_task, [analyst]) builder.add_edge(analyst, inspector) builder.add_conditional_edges(inspector, route_after_quality, {processor: processor, reporter: reporter}) builder.add_edge(reporter, END) graph builder.compile()这里涉及 LangGraph 两个比较高级的用法我觉得值得展开讲一下。第一个是SendAPI。它不是普通的边而是允许一个节点动态地向某个目标节点发送多个独立任务。在上面例子中主控节点把任务拆成多个子任务后通过Send并行派发给分析节点每个任务携带不同的 task 数据。这个能力比在单个分析节点里循环调用模型要高效得多因为 LangGraph 能对这些并行分支做并发执行耗时不再是多个任务串行相加。第二个是add_conditional_edges。它接受一个路由函数根据函数返回值决定下一个节点。返回的 key 映射到具体节点名。这里有三个非常容易踩坑的点路由函数的名字写错了会报图编译错误映射字典里的目标节点必须在图中先注册add_conditional_edges的第三个参数如果传字典的话key 必须和路由函数返回值精确匹配。我第一次就把retry写成了rework结果图编译直接报“Invalid conditional edge”找了两小时才发现是字典 key 不一致。这种错误在 LangGraph 里非常常见编译阶段的报错信息其实已经足够明确只是要静下心来顺着映射关系查。编译之后返回的graph对象就可以直接调用了。invoke是同步执行ainvoke是异步执行。如果你跑的是简单 Demoinvoke就够了如果平台要承载并发请求记得用ainvoke。3.5 持久化与会话管理分析平台不是一次性脚本用户可能查了第一轮数据接着想继续追问“那为什么流失用户集中在低活跃群体”。这就要求平台记住上一轮对话和中间处理结果LangGraph 通过 checkpointer 机制解决这个问题。我推荐至少使用MemorySaver内存级开发用或SqliteSaver落盘级生产可用from langgraph.checkpoint.memory import MemorySaver from langgraph.checkpoint.sqlite import SqliteSaver # 生产环境建议用 SqliteSaver数据库文件可保留状态 with SqliteSaver.from_conn_string(checkpoints.db) as saver: graph builder.compile(checkpointersaver) config {configurable: {thread_id: user-session-001}} result graph.invoke({user_request: 分析本周新增用户表现}, config)thread_id就是会话标识。同一个 thread_id 的多次调用会被 LangGraph 视为同一个会话状态会在节点之间保留模型也能通过系统的 message 历史看到之前的交互。这个设计极大地简化了“多轮追问”场景的开发你不需要自己维护对话历史 buffer把 thread_id 传到前端按会话维度存取就可以。但是thread_id是全程保留所有状态消息的所以在多轮对话后状态会膨胀。我的做法是在主控节点加一条“历史摘要整理”逻辑当消息数量超过阈值就用一个摘要 Agent 把旧的交互压缩成一段摘要文本替换掉原始消息列表。这个思路本质上就是大模型应用里常说的“上下文窗口管理”在 LangGraph 里实现它非常自然因为你有完整的状态访问权可以在任何节点的开头做消息裁剪。3.6 流式输出提升交互体验分析平台如果等全部节点跑完再一次性返回结果用户体验是比较差的尤其是多 Agent 逐级分析时耗时可能达到十几二十秒甚至更长。LangGraph 原生支持流式输出有两种方式值得掌握。第一种是stream它按流程输出每个节点的输入输出。第二种是astream_events它可以把粒度细化到模型内部的每个 token 生成事件。做前端交互时我倾向于在关键节点开着stream_modeupdates让前端逐步展示“正在采集数据”“正在分析数据”“正在质检”这些状态效果就是用户能实时看到系统的分析进度而不是对着一片空白等待。代码类似这样async for chunk in graph.astream( {user_request: 分析竞品动态}, config, stream_modeupdates ): for node_name, node_output in chunk.items(): print(f节点 {node_name} 更新: {node_output.get(current_role, )})流式输出的调试价值也很大。多 Agent 流程里如果某个节点异常流式输出能清晰地告诉你是在哪个节点、哪个时刻出的问题。我习惯在开发阶段开着流式跑一遍完整流程眼过一遍所有节点的更新内容基本就能定位到 80% 的编排问题比反复看日志强得多。4. 实测中的典型问题与排查实录4.1 状态污染reducer 累加导致的重复数据我的第一个问题是在质检通过后第二次调用同一个 thread_id 发起新请求发现processed_data里出现了重复内容。原因是tasks和processed_data这类字段我一开始用了operator.add做合并但分布式节点在某些分支下会重复调用于是上一次会话的数据又被追加了一次导致数据翻倍。这个问题非常隐蔽因为第一次运行是正常的第二次运行才开始出现重复。排查思路是这样我先把thread_id换掉结果一切正常说明数据残留问题存在于同一个会话状态中。然后我检查了每个节点返回的字段发现加工节点在返工路径下被调用了两次每次都在processed_data上追加内容。解决办法是调整状态设计加工节点每次返回时先清空旧列表再写入新列表而不是持续追加。具体做法是把processed_data从Annotated[List, operator.add]改为普通覆盖字段class AnalysisState(TypedDict): processed_data: List[dict] # 直接覆盖不累加这里想提醒各位operator.add只能用在“确实需要多节点共同累积”的字段上比如errors、tasks。凡是“单一责任节点产出、后续节点整体消费”的数据就应该用覆盖式否则早晚会遇到重复数据问题。这是我踩过最深的一个坑分享出来希望大家别重复踩。4.2 模型陷入工具调用循环给分析 Agent 加护栏分析 Agent 在调用查询工具时有时候会陷入“反复查询但就是不给结论”的死循环。比如它为了算一个指标连续查了五次 SQL每次都在调整参数却一直不生成最终分析。原因是模型在多次工具调用后“迷失”了它不确定当前数据已经够用于是继续查询以求保险。LangGraph 本身并不会阻止这种行为它只是忠实地执行工具调用和状态更新所以需要我们自己加上护栏。我是用两层措施解决的。第一层是限制工具调用的最大次数在节点内部使用with_message_historybind_tools时设置max_tool_calls参数超了就强制走一次“无工具总结”路径。第二层是在系统 Prompt 里写死一条规则当已经拿到足够的processed_data时禁止调用查询工具直接基于现有数据做分析。双管齐下后循环问题基本消失。还有一个技巧在工具调用节点附近加一个“熔断开关”比如检测到连续 N 次相同的工具调用就把状态里的need_rework置为 true走返工分支重新理清任务而不是继续撞墙。这个设计思路在任何多 Agent 系统里都适用。4.3 上下文溢出长数据导致模型“失忆”采集和加工阶段拿到的原始数据如果很大比如几千条用户反馈全量塞进分析节点的 Prompt模型虽然不会立刻报错但注意力会被长文本稀释导致分析质量显著下降。更麻烦的是多轮会话叠加历史消息后上下文会迅速逼近模型窗口上限。对此我的方案是“摘要优先、原文兜底”。加工节点在输出processed_data时同时生成一份data_summary包含总体统计、关键分布、异常点。分析 Agent 的 Prompt 默认只给摘要只有分析 Agent 明确表示“需要查看特定原始数据详情”时才通过工具调用获取细节。这套设计既保证了分析质量又控制了上下文规模。另外在多轮会话时务必做历史摘要压缩别让旧对话把窗口占满。4.4 并行节点执行时的状态更新冲突使用 Send API 做并行分派时我遇到过一个问题多个并行分支同时尝试更新同一个errors字段导致最终状态里错误信息丢失或者被覆盖。LangGraph 对并行节点的状态合并策略依赖 reducer如果某个字段没有定义 reducer后写入的会直接覆盖先写入的这在并行场景下很难预判。我的解决思路是并行分支不要修改共享字段每个分支只更新自己独有的task_result_xxx字段最后在聚合节点统一汇总。聚合节点再根据所有子任务的完成情况一次性更新errors和analysis_result。这样把“并行写”变成“并行读、串行写”彻底避免了冲突。4.5 调试工具推荐state 可视化与 get_stateLangGraph 本身带了一些可视化能力虽然依赖 Graphviz但确实能帮你把图结构给可视化出来。我强烈建议第一次搭完图后先看一眼整体结构确认边的流转方向符合直觉。代码很简单# 在 Jupyter 里直接显示 from IPython.display import Image, display display(Image(graph.get_graph().draw_mermaid_png()))注意如果你是在普通 Python 脚本里跑保存成图片再查看也是一样的效果。这行代码能让你直观地看到有没有意外的边比如 star 到某个节点之间是不是多了一条漏写的边。另外graph.get_state(config)这个方法非常实用。它可以查看当前会话在某一个执行时刻的完整状态快照。当流程中断或报错后你用 thread_id 去查询状态能看到所有字段的最新值再结合流式输出基本能定位到问题节点。我在生产环境排查问题时靠这招解决了不少疑难杂症。5. 扩展方向与我的个人心得搭建完这个 Multi-Agent 分析平台后我最大的感触是LangGraph 真正厉害的地方不在于它提供了多少“Agent 能力”而在于它把多 Agent 系统的工程复杂度降到了一个可控的范围内。图结构让每一个环节都是显式的、可检查的状态机制让流程可以被中断、被恢复、被多轮追问而 checkpoint 能力直接解决了会话记忆问题。这三板斧合在一起多 Agent 应用才真正从“演示”走向了“可用”。如果后续要进一步扩展我会考虑三个方向。一是引入人机协同审查节点比如在质检不通过或分析结果涉及重大决策时系统暂停并请求用户确认这就是 LangGraph 官方重点推荐的 human-in-the-loop 模式实现方式是在关键节点前后打断图执行等待用户输入后再继续。二是做评估集回归为每类分析任务准备一批标准测试用例每次修改 Prompt 或工具逻辑后自动跑一遍防止一个环节优化导致另一个环节劣化。三是将流式输出接到消息队列多个用户同时使用平台时保证任务状态和输出不互相干扰。最后分享一个个人建议不要为了多 Agent 而多 Agent。如果某个分析任务单个 Agent 加工具就能高质量完成那就不用硬拆。我在实践里有一条判断标准——当单个 Agent 的 Prompt 超过一定长度、工具数量超过三四个、或者流程里出现明显需要“返工校验”的业务逻辑时才值得拆成 Multi-Agent。否则引入多个角色只会增加调用成本和延迟收益却很有限。能用一个节点解决的问题就不要用两个节点解决这个原则在任何架构设计里都成立。