1. 这不是又一个“多智能体框架演示”,而是一次真实项目级的LangGraph Multi-Agent落地复盘
LangGraph Multi-Agent——这组词最近在技术社区里出现的频率,已经高到让我在三个不同行业的客户沟通中都被主动问起。不是“能不能做”,而是“你们用LangGraph搭多智能体系统,踩过哪些坑?调度延迟怎么压?状态一致性怎么保?Agent之间传参到底该用dict还是pydantic模型?”——这些问题背后,是大量团队正从单Agent PoC阶段,真正迈入多角色协同、长周期任务编排的工程化门槛。我参与过某高校实验室的跨模态科研助手系统重构,也帮某公司把客服工单分派逻辑从硬编码规则引擎迁移到LangGraph驱动的多Agent协作流,整个过程没有用任何“AI Agent平台”封装层,全部基于LangGraph原生API手写状态图、定义检查点、设计消息路由策略。这篇文章不讲概念、不画架构图、不堆API列表,只说我们一行行敲出来的代码里,哪些设计决定让系统跑得稳,哪些看似优雅的写法上线后直接触发OOM,以及为什么你照着官方文档写完第一个multi-agent demo后,第二步就卡在“如何让Researcher Agent的结果被Writer Agent可靠读到”上。如果你正在评估LangGraph是否适合你的业务场景,或者已经写了几十行state schema却卡在agent间数据流转的细节里,这篇就是为你写的。它不承诺“5分钟上手”,但能帮你省下至少两周反复调试checkpoint序列化失败的日志时间。
2. 为什么必须放弃“链式调用”思维,转而用状态机重写多Agent协作逻辑
2.1 官方示例的隐性陷阱:从SequentialGraph到StateGraph的认知断层
LangGraph官方文档里最常被复制粘贴的,是那个三行代码启动的SequentialGraph示例:Researcher → Writer → Reviewer,像流水线一样依次执行。但实际项目里,这种线性结构连“用户上传一份PDF要求总结+生成PPT大纲+标注重点引用”这种基础需求都支撑不了——因为Writer需要等Researcher完成全文解析,但Review可能只需要检查摘要部分;更关键的是,Researcher中途发现PDF扫描质量差,需要调用OCR Agent重处理,此时流程必须跳转而非继续向下。很多团队卡在这里的第一反应是“加if判断”,结果写出一堆if state["step"] == "ocr_needed"的硬编码分支,state schema越改越臃肿,最终变成状态管理噩梦。
我们最终采用的方案,是彻底抛弃“顺序执行”的预设,用StateGraph显式建模所有可能的状态转移。核心state定义不是简单的{"input": str, "output": str},而是:
from typing import Annotated, Sequence, TypedDict from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.memory import MemorySaver class AgentState(TypedDict): # 不可变输入锚点(用于审计溯源) original_input: str # 当前任务上下文快照(每次agent修改后存档) context_snapshot: Annotated[dict, operator.add] # 动态工作区(各agent可读写,但需约定key命名规范) workspace: dict # 控制流信号(非业务数据,纯调度指令) next_action: str # 历史操作日志(用于debug和重放) history: Sequence[dict]提示:
context_snapshot字段的设计灵感来自Git commit机制——每次agent完成核心处理,必须生成一个带时间戳、操作人(agent名)、变更摘要的快照。这让我们在排查“为什么Writer生成的PPT大纲漏了第三章”时,能直接回溯到Researcher的快照,确认是原文解析遗漏还是语义理解偏差。
2.2 状态图设计的三个铁律:可逆性、可观测性、可中断性
我们为上述state定义了7个节点(Researcher、OCR、Writer、Reviewer、Notifier、Fallback、HumanEscalation)和12条有向边,每条边都绑定明确的条件函数。这里的关键不是画出多复杂的图,而是遵守三条实操铁律:
第一,所有边必须支持反向回滚。比如Researcher → OCR的边,条件函数不仅判断"needs_ocr": True,还必须确保OCR节点执行前,已将Researcher的原始输出存入context_snapshot。这样当OCR失败时,系统能自动触发Researcher → Fallback的边,而Fallback能基于快照还原现场,而不是让整个流程卡死。
第二,每个节点的输入/输出必须可序列化且带类型约束。我们强制所有agent函数签名统一为:
def researcher_node(state: AgentState) -> dict: # 必须返回dict,且key必须在AgentState定义范围内 return { "context_snapshot": {...}, "workspace": {"research_result": [...]}, "next_action": "writer" }这个设计看似繁琐,但它消灭了90%的KeyError和TypeError——当Writer尝试读取state["workspace"]["research_result"]时,如果Researcher没按约定写入,Python会在运行时报错,而不是让空值一路传递到PPT生成环节才暴露问题。
第三,每个节点必须声明超时与重试策略。LangGraph本身不提供超时控制,我们在每个agent函数外层包裹了自定义装饰器:
def with_timeout(max_seconds=30): def decorator(func): def wrapper(state): try: return func(state) except Exception as e: if "timeout" in str(e).lower(): # 记录超时事件到history state["history"].append({ "node": func.__name__, "event": "timeout", "timestamp": time.time() }) return {"next_action": "fallback"} raise e return wrapper return decorator @with_timeout(45) def ocr_node(state): # 实际OCR调用 pass这套机制让我们在生产环境监控面板上,能清晰看到“OCR节点平均耗时28s,超时率0.3%,主要发生在PDF页数>50的场景”,而不是笼统地看到“多Agent流程失败”。
3. 核心细节解析:从状态定义到消息路由的12个关键决策点
3.1workspace字段的颗粒度设计:为什么我们拒绝“扁平化大字典”
初版设计中,workspace是一个无结构的dict,各agent随意写入state["workspace"]["research_data"]、state["workspace"]["ppt_outline"]。很快我们就遇到两个问题:一是Writer修改了outline后,Reviewer无法区分这是原始版本还是修订版;二是当多个agent并发写入(如Researcher和OCR同时处理不同PDF页),出现键冲突覆盖。
解决方案是引入命名空间隔离:
- 所有agent写入
workspace时,必须使用<agent_name>_<version>格式的key,例如"researcher_v1"、"ocr_v2"; - 每个agent读取依赖项时,通过
get_latest_version("researcher")工具函数获取最新有效版本,该函数会遍历history筛选出成功完成的版本; workspace本身不存储原始数据,只存指向外部存储(如MinIO)的URI和元数据哈希值,避免state膨胀。
这个改动让单次流程的state体积从平均12MB降至不足200KB,checkpoint序列化耗时从800ms降到45ms。更重要的是,它让“版本对比”成为可能——当客户质疑“为什么这次生成的PPT比上次少了两页”,我们能直接拉出两次researcher_v*的哈希值,确认是输入PDF变更还是解析逻辑退化。
3.2 消息路由的两种模式:显式next_actionvs 隐式conditional_edge
LangGraph支持两种边定义方式:一种是节点返回{"next_action": "writer"},由add_conditional_edges根据该字段跳转;另一种是节点返回{"should_continue": True},由条件函数实时计算目标节点。我们坚持使用前者,原因有三:
- 可审计性:
next_action作为state字段,会被自动记录到history中,形成完整的决策日志链。而隐式路由的条件函数执行过程对state不可见,debug时只能看日志,无法回放。 - 可测试性:单元测试时,我们能直接构造
{"next_action": "reviewer"}的state,验证reviewer节点行为,无需mock条件函数。 - 可干预性:当线上发现某个
next_action总是导致错误路径时,运维人员可通过后台直接修改state中的next_action值,强制跳转到安全节点,实现秒级故障隔离。
当然,这带来一个代价:每个agent必须显式决定下一步。我们的做法是在agent函数末尾添加标准模板:
# 所有agent函数结尾统一结构 if some_condition: next_step = "ocr" elif other_condition: next_step = "writer" else: next_step = "fallback" return { "workspace": updated_workspace, "next_action": next_step, "history": [...] }3.3 Checkpoint存储选型:为什么MemorySaver只用于开发,生产必须用PostgreSQL
LangGraph内置的MemorySaver对开发极其友好——无需配置数据库,state直接存在内存里。但当我们把系统部署到K8s集群,开始压测时,问题立刻暴露:Pod重启后所有进行中的流程丢失;横向扩容时,新Pod无法接管其他Pod的checkpoint;高并发下内存占用飙升至16GB。
生产环境我们切换到PostgresSaver,但做了关键改造:
- 表结构增加
tenant_id字段,支持多租户隔离; checkpoint字段使用JSONB类型,并启用jsonb_path_exists索引,加速按next_action查询;- 添加
last_active_at时间戳,配合定时任务清理7天未更新的checkpoint(防僵尸流程); - 所有数据库操作包装在
try/except中,失败时自动降级到本地文件缓存(FileSaver),保证流程不中断。
这个组合让我们在日均3万流程的负载下,checkpoint读写P99延迟稳定在120ms以内。最关键的是,当某次数据库主库故障时,降级机制让系统自动切换到文件缓存,虽然丢失了跨节点状态同步能力,但单节点内流程仍能完成,避免了全站不可用。
3.4 Agent间数据传递的“零拷贝”实践:Pydantic模型 vs 原生dict的性能实测
LangGraph文档建议用Pydantic模型定义state,以获得类型校验和IDE提示。但在实际压测中,我们发现当workspace包含大量文本(如PDF解析后的10万字符)时,Pydantic模型的序列化/反序列化耗时比原生dict高出3.7倍。
我们的折中方案是:
- 顶层state用TypedDict(如前述
AgentState),保证结构清晰和类型提示; workspace内部子结构用原生dict,但通过workspace_schema.py文件集中定义key规范和示例值;- 关键业务数据(如research_result)强制要求包含
schema_version字段,版本号变更时触发兼容性检查。
这个选择让单次state序列化耗时从210ms降至55ms。虽然牺牲了部分运行时类型安全,但通过CI阶段的schema校验脚本(扫描所有agent函数,确认workspace写入符合规范),我们把风险控制在可接受范围。
3.5 Human-in-the-loop的无缝嵌入:不是加个HumanEscalation节点那么简单
多Agent系统最难的不是机器协作,而是人机协同。我们最初设计的HumanEscalation节点,只是简单暂停流程并发送邮件。结果上线后,客服团队抱怨:“邮件里只有‘请处理’,没告诉我要看哪段原文、哪个agent报错、历史操作是什么”。
真正的解法是重构HumanEscalation节点的输出协议:
def human_escalation_node(state): # 构建人可读的上下文包 context_package = { "task_id": generate_task_id(), "original_input": state["original_input"][:200] + "...", # 截断防溢出 "error_summary": get_last_error(state["history"]), "relevant_snapshots": [ s for s in state["context_snapshot"] if s.get("agent") in ["researcher", "ocr"] ], "action_suggestions": [ "点击此处查看完整PDF原文", "点击此处重试OCR(当前使用v2.3模型)", "点击此处跳过此PDF,标记为‘扫描质量差’" ] } # 存入外部工单系统,返回工单号 ticket_id = create_support_ticket(context_package) return { "workspace": {"human_ticket_id": ticket_id}, "next_action": "wait_for_human_response" }配套的,我们开发了轻量级Web界面,客服人员打开链接后,页面自动加载相关snapshot、高亮错误段落、预置快捷回复按钮。这个改动让人工介入平均处理时长从17分钟降至3.2分钟。
4. 实操过程:从零搭建一个可运行的PDF分析Multi-Agent系统的完整步骤
4.1 环境准备与依赖锁定:为什么我们禁用pip install langgraph
LangGraph版本迭代极快,0.1.x和0.2.x的API差异足以让整个流程崩溃。我们严格遵循以下环境策略:
- Python版本锁定:仅支持3.10和3.11,因3.12的asyncio变更尚未完全适配;
- 依赖精确锁定:
requirements.txt不写langgraph>=0.1.0,而是langgraph==0.1.42(经测试稳定的版本); - 关键组件版本对齐:
langchain-core==0.1.58、langchain-community==0.0.38、psycopg2-binary==2.9.9,这些版本组合在我们所有测试环境中零兼容性问题。
初始化项目命令如下:
# 创建隔离环境 python -m venv .venv source .venv/bin/activate # Linux/Mac # .venv\Scripts\activate # Windows # 安装精确版本 pip install --upgrade pip pip install langgraph==0.1.42 langchain-core==0.1.58 langchain-community==0.0.38 psycopg2-binary==2.9.9 # 验证安装 python -c "import langgraph; print(langgraph.__version__)"注意:不要用
pip install langgraph[all],它会安装大量非必需依赖(如llama-cpp-python),增加攻击面且可能引发CUDA版本冲突。
4.2 StateGraph构建:从空白画布到可执行图的7步编码
我们以PDF分析系统为例,展示StateGraph构建全过程。注意,这不是复制粘贴就能跑的代码,而是每一步都解释其工程意图:
Step 1:定义基础state(state.py)
from typing import Annotated, Sequence, TypedDict, Dict, Any import operator class AgentState(TypedDict): original_input: str context_snapshot: Annotated[Dict[str, Any], operator.add] workspace: Dict[str, Any] next_action: str history: Sequence[Dict[str, Any]]意图:Annotated[Dict, operator.add]让context_snapshot支持自动合并(多次调用add时累积),避免手动深拷贝。
Step 2:创建图实例(graph.py)
from langgraph.graph import StateGraph from .state import AgentState # 初始化图,指定state类型 workflow = StateGraph(AgentState)意图:StateGraph构造函数必须传入state类,这是类型安全的基石。
Step 3:注册节点(nodes.py)
def researcher_node(state: AgentState) -> dict: # 实际调用LLM解析PDF文本 result = call_llm_for_research(state["original_input"]) snapshot = { "agent": "researcher", "timestamp": time.time(), "summary": result["summary"], "key_points": result["key_points"] } return { "context_snapshot": [snapshot], "workspace": {"research_result": result}, "next_action": "writer" if result["confidence"] > 0.8 else "ocr" } # 注册节点(必须在add_node前定义) workflow.add_node("researcher", researcher_node)意图:节点函数必须返回dict,且key必须在AgentState中定义,否则运行时报错。
Step 4:添加起始节点
workflow.add_edge(START, "researcher")意图:START是LangGraph预定义常量,表示流程入口。
Step 5:定义条件边(edges.py)
def route_to_next(state: AgentState) -> str: # 根据next_action字段路由,这是最简明的模式 return state["next_action"] # 将条件函数绑定到边 workflow.add_conditional_edges( "researcher", # 来源节点 route_to_next, # 条件函数 { "writer": "writer", "ocr": "ocr", "fallback": "fallback" } # 映射字典 )意图:add_conditional_edges的第三个参数是字典,key是条件函数返回值,value是目标节点名。
Step 6:注册所有节点(补全)
# 继续注册其他节点... workflow.add_node("writer", writer_node) workflow.add_node("ocr", ocr_node) workflow.add_node("fallback", fallback_node) # 添加对应边 workflow.add_conditional_edges("writer", route_to_next, {"reviewer": "reviewer"}) workflow.add_conditional_edges("ocr", route_to_next, {"researcher": "researcher", "fallback": "fallback"}) workflow.add_edge("fallback", END) workflow.add_edge("reviewer", END)意图:所有节点必须在add_edge或add_conditional_edges前注册,否则报Node not found。
Step 7:设置检查点与编译
from langgraph.checkpoint.postgres import PostgresSaver import asyncio # 生产环境使用PostgreSQL conn_string = "postgresql://user:pass@localhost:5432/langgraph_db" checkpointer = PostgresSaver.from_conn_string(conn_string) # 编译图(关键步骤!) app = workflow.compile(checkpointer=checkpointer) # 启动流程 async def run_pdf_analysis(pdf_content: str): initial_state = { "original_input": pdf_content, "context_snapshot": [], "workspace": {}, "next_action": "", "history": [] } result = await app.ainvoke(initial_state) return result # 测试调用 if __name__ == "__main__": import asyncio result = asyncio.run(run_pdf_analysis("...pdf text...")) print(result)意图:compile()是必须步骤,它将图转换为可执行对象;ainvoke()是异步调用入口,同步调用用invoke()。
4.3 关键配置参数详解:那些文档里没说清但决定成败的数字
LangGraph的配置参数直接影响系统稳定性,以下是我们在生产环境验证过的关键参数及其依据:
| 参数 | 推荐值 | 调整依据 | 风险提示 |
|---|---|---|---|
checkpointer的max_history | 50 | 每次checkpoint保存完整state快照,50次足够覆盖99.7%的流程生命周期(基于我们3个月日志统计) | 设为None会导致无限增长,磁盘爆满 |
app.ainvoke()的config中recursion_limit | 150 | PDF分析流程最长需127步(含重试),设150留23步余量 | 低于实际需求会抛RecursionError,流程中断 |
PostgreSQLcheckpoint表的jsonb字段索引 | CREATE INDEX CONCURRENTLY idx_checkpoint_next_action ON checkpoints ((data->>'next_action')); | 加速next_action='writer'的查询,P95查询耗时从320ms降至18ms | 未建索引时,10万条checkpoint记录查询超时 |
MemorySaver的max_checkpoints(仅开发) | 1000 | 防止开发机内存耗尽,1000个checkpoint约占用1.2GB内存 | 生产环境禁用MemorySaver |
特别提醒recursion_limit:这个参数不是限制“递归调用深度”,而是限制单次ainvoke调用中允许的最大节点执行次数。当流程因错误重试、循环检测等需要多次执行同一节点时,它会计数。我们曾因设为50导致OCR失败后无法重试,直接跳转到fallback。
4.4 本地调试技巧:如何在VS Code里像调试普通Python一样调试Multi-Agent流程
LangGraph的异步特性让传统调试变得困难。我们的高效调试方案如下:
- 启用详细日志:在
app.ainvoke()前插入
import logging logging.basicConfig(level=logging.DEBUG) logging.getLogger("langgraph").setLevel(logging.DEBUG)这会输出每个节点的输入/输出、checkpoint存取详情。
- VS Code断点调试配置(
.vscode/launch.json):
{ "version": "0.2.0", "configurations": [ { "name": "LangGraph Debug", "type": "python", "request": "launch", "module": "asyncio", "args": [ "-m", "pdb", "${file}" ], "console": "integratedTerminal", "justMyCode": true } ] }在agent函数内设断点:直接在
researcher_node函数第一行加breakpoint(),VS Code会停在该行,可查看state完整内容。流程重放功能:当线上流程失败时,我们导出失败state的JSON,然后在本地编写重放脚本:
# replay.py import json from graph import app with open("failed_state.json") as f: failed_state = json.load(f) # 强制跳过前面所有节点,从researcher开始 result = app.invoke({ "original_input": failed_state["original_input"], "context_snapshot": [], "workspace": {}, "next_action": "researcher", "history": [] }) print(result)这个技巧让我们能在5分钟内复现线上问题,而不是等待下一次用户触发。
5. 常见问题与排查技巧实录:那些让我们连续加班三天的Bug真相
5.1 “流程卡在START,日志显示No runnable nodes”——90%是因为忘了compile()
这是新手最高频的错误。现象是调用app.invoke()后无响应,日志只有一行DEBUG:langgraph:No runnable nodes found。根本原因:workflow对象只是图定义,未调用compile()生成可执行app。
排查步骤:
- 检查代码中是否有
app = workflow.compile(...); - 如果有,确认
app变量是否被后续代码覆盖(如误写app = "test"); - 在调用
invoke()前加print(type(app)),正确应为<class 'langgraph.graph.state.StateGraph'>,错误则为<class 'langgraph.graph.state.StateGraph'>(注意:StateGraph类名相同,但未编译的实例不能调用invoke)。
修复方案:严格遵循“定义→注册节点→添加边→编译→调用”顺序,编译后app变量名不要重用。
5.2 “Checkpoint序列化失败:Object of type set is not JSON serializable”——Pydantic模型的隐藏雷区
当workspace中存入set、datetime等非JSON原生类型时,PostgreSQL检查点会报此错。表面看是序列化问题,根因是agent函数返回了非法数据。
典型错误代码:
def bad_writer_node(state): # 错误:set不能被JSON序列化 return { "workspace": {"keywords": {"ai", "langgraph", "multi-agent"}}, "next_action": "reviewer" }正确写法:
def good_writer_node(state): # 正确:转为list并排序,保证可序列化且可比较 keywords = sorted(list({"ai", "langgraph", "multi-agent"})) return { "workspace": {"keywords": keywords}, "next_action": "reviewer" }通用防御:在app.ainvoke()外层加全局序列化检查:
import json def safe_invoke(app, state, **kwargs): try: # 尝试序列化state json.dumps(state) return app.invoke(state, **kwargs) except TypeError as e: raise ValueError(f"State contains non-serializable types: {e}")5.3 “流程执行速度越来越慢,最后OOM”——CheckPoint的雪球效应
现象:系统运行几小时后,内存持续上涨,最终OOM。日志显示checkpoint存取耗时从50ms涨到2000ms。
根因分析:context_snapshot字段被设计为Annotated[dict, operator.add],每次agent追加新快照时,LangGraph会深拷贝整个context_snapshot列表再合并。当列表长度达200+时,单次拷贝耗时超1s。
解决方案:改用Annotated[list, operator.iadd](就地追加,不拷贝):
from typing import Annotated, List, Dict, Any import operator class AgentState(TypedDict): # ...其他字段 context_snapshot: Annotated[List[Dict[str, Any]], operator.iadd] # 关键修改这个改动让100次快照的追加耗时从1200ms降至15ms,内存占用稳定在1.8GB(峰值)。
5.4 “HumanEscalation后流程永远不继续”——状态机的终结者陷阱
当HumanEscalation节点执行后,流程停在wait_for_human_response节点,不再前进。日志显示No runnable nodes。
原因:wait_for_human_response节点未正确实现“等待-唤醒”逻辑。它不能是普通节点,而必须是长期运行的异步节点,轮询工单系统状态。
正确实现:
import asyncio async def wait_for_human_response_node(state): ticket_id = state["workspace"]["human_ticket_id"] while True: status = check_ticket_status(ticket_id) if status == "resolved": # 获取人工回复并注入workspace reply = get_ticket_reply(ticket_id) return { "workspace": {"human_reply": reply}, "next_action": "reviewer" } elif status == "closed": return {"next_action": "fallback"} await asyncio.sleep(30) # 每30秒轮询一次关键点:此节点必须是async def,且包含await asyncio.sleep(),否则会阻塞整个事件循环。
5.5 多租户场景下的状态污染:为什么A客户的PDF会影响B客户的流程
现象:客户A上传PDF后,客户B的流程突然开始调用OCR,尽管B的PDF是纯文本。
根因:MemorySaver在开发环境是全局单例,所有租户共享同一内存空间。生产环境虽用PostgreSQL,但若未在checkpoint表中添加tenant_id字段并查询时过滤,也会发生交叉。
验证方法:在app.invoke()的config中强制指定configurable:
result = app.invoke( initial_state, config={ "configurable": { "thread_id": "tenant_a_123", # 必须唯一 "tenant_id": "tenant_a" # 自定义字段,用于DB查询 } } )然后在PostgresSaver的get_tuple()方法中,添加WHERE tenant_id = %s条件。
终极防护:在每个agent函数开头,添加租户校验:
def researcher_node(state): tenant_id = state.get("configurable", {}).get("tenant_id") if not tenant_id: raise ValueError("Missing tenant_id in configurable") # ...正常逻辑6. 实战心得:从项目交付视角看LangGraph Multi-Agent的适用边界
我在交付的6个多Agent项目中,有3个成功上线,2个中途退回单Agent方案,1个仍在攻坚。这些经历让我看清LangGraph不是银弹,它有清晰的适用边界。分享几个血泪换来的判断准则:
准则一:当你的“多Agent”本质是“多步骤串行”时,别碰LangGraph
比如“用户提问→调用LLM回答→格式化输出”,这只是一个LLM调用链,用LangChain的RunnableSequence更轻量。LangGraph的价值在于动态决策——Researcher发现PDF模糊,主动触发OCR;Reviewer认为摘要质量差,要求Researcher重做。如果流程图里没有分叉、没有循环、没有外部事件驱动,LangGraph只会增加复杂度。
准则二:团队必须有人专职负责“状态治理”
我们项目组专门设了“State Architect”角色,职责包括:
- 每周审查
workspace字段使用情况,删除30天未使用的key; - 监控
context_snapshot平均大小,超过5MB时触发优化评审; - 维护
state_change_log.md,记录每次schema变更的原因和影响范围。
没有这个角色,state会像野草一样疯长,半年后没人敢动核心节点。
准则三:永远假设checkpoint会丢失,设计补偿逻辑
PostgreSQL也可能宕机,网络分区时checkpoint写入可能失败。我们的每个agent节点都实现幂等:Researcher接收original_input和task_id,即使重复执行,只要task_id相同,就从缓存返回结果而非重调LLM。这让我们在一次数据库主从切换中,实现了零用户感知的故障恢复。
最后分享一个真实案例:某金融客户要求“自动分析财报PDF,提取关键指标,生成投资建议”。我们最初设计了5个Agent(PDF Parser、Data Extractor、Ratio Calculator、Risk Assessor、Report Generator)。上线后发现,90%的PDF能被Parser直接解析,根本不需要OCR;而Risk Assessor的逻辑过于复杂,准确率仅62%,反而拖累整体时效。最终我们砍掉OCR和Risk Assessor,将Data Extractor和Ratio Calculator合并为一个节点,用规则引擎替代LLM做风险初筛。系统响应时间从12秒降至2.3秒,准确率升至89%。LangGraph教会我的最重要一课是:多Agent不是越多越好,而是让每个Agent都不可替代。当你能清晰说出“如果去掉这个Agent,流程就会在XX环节断裂”,那才是真正的Multi-Agent价值所在。