
1. 为什么单 Agent 跑得好好的一上多 Agent 就翻车我最早做 Agent 项目的时候也是从 LangChain 那一套AgentExecutor起步的。单个 Agent 挂三五个工具跑个查询、算个数、调个接口体验相当顺滑demo 阶段几乎没出过什么大问题。那时候我天真地以为多 Agent 无非就是把几个 Agent 拼起来让它们互相调用一下能有多难结果第一次真正上手多 Agent 编排我就被打脸了。三个 Agent 协作处理一个稍微复杂点的任务token 消耗直接飙到单 Agent 的七八倍而且输出质量不升反降——Agent A 把任务丢给 Agent BAgent B 又觉得这事该 Agent C 干Agent C 转了一圈又绕回 Agent A最后要么死循环要么给出一个谁都不满意的缝合怪答案。更离谱的是有时候明明任务很简单几个 Agent 却在那儿互相客套你一句请帮我处理我一句好的我来看看光寒暄就烧掉几千 token。后来我复盘了很久才想明白一件事多 Agent 不是玄学它本质上是两个工程问题——token 账本和干扰问题。你把这两个问题想清楚了多 Agent 就从碰运气变成了可设计。这也是我从 LangChain 迁移到 LangGraph 的核心动机因为 LangGraph 的StateGraph模型恰好给了你管理这两个问题的抓手。这篇文章我想聊的就是这套东西为什么单 Agent 的线性思维在多 Agent 场景下会失效LangGraph 的图结构到底解决了什么token 账本该怎么算、怎么省Agent 之间的干扰该怎么隔离。适合已经用过 LangChain、想往多 Agent 方向走但被坑过的朋友也适合还在观望、想知道多 Agent 到底值不值得上的同学。我会尽量把每一步的为什么讲透而不是只丢一段能跑的代码。2. 从 LangChain 到 LangGraph到底换的是什么思路2.1 LangChain 的 AgentExecutor 为什么撑不住多 AgentLangChain 的AgentExecutor本质上是一个while 循环模型输出一个动作执行工具把结果塞回上下文再让模型决定下一步直到模型说我结束了。这个循环对单 Agent 非常友好因为只有一个决策者上下文是线性的谁说了什么一目了然。但多 Agent 场景下这个模型立刻暴露三个硬伤。第一控制流是隐式的。你没法在代码层面明确说这一步必须由检索 Agent 干完才能进到总结 Agent你只能靠 prompt 去求模型按顺序来。模型心情好就按顺序心情不好就跳步你还没法拦。第二状态是散落的。每个 Agent 有自己的 memory中间结果靠消息传递一旦某个环节出错你很难定位到底是哪个 Agent 把状态搞脏了。我踩过最坑的一次是三个 Agent 共享一个 conversation buffer结果 A 的中间推理被 B 当成了用户指令直接跑偏。第三没有断点和回滚。多 Agent 流程动辄十几步中间任何一步失败你只能从头再来token 白烧。这在生产环境里是致命的。2.2 LangGraph 的 StateGraph 把控制流显式化了LangGraph 的核心抽象是StateGraph你可以把它理解成一张有向图节点是 Agent 或者普通函数边是流转条件整个图共享一个State通常是个 TypedDict 或者 Pydantic 模型。这个设计的关键价值在于控制流从模型决定变成了你决定 模型在节点内决定。你可以在图里明确画出检索 → 判断 → 总结的路径模型只在每个节点内部做它擅长的事节点之间怎么走由你的边和条件函数说了算。我举个具体的对比。同样是查资料然后写报告这个任务LangChain 写法一个 Agentprompt 里写先搜索再总结然后祈祷它照做。LangGraph 写法search_node和summary_node两个节点中间一条边search_node只负责调搜索工具summary_node只负责拿搜索结果写报告。职责清晰各管一段。这个差别看起来小但在多 Agent 场景下是质变。因为一旦控制流显式了你才有可能去算 token 账和隔离干扰——这两件事都依赖于你清楚地知道哪一步在花 token哪一步可能被污染。2.3 一张表看清两者的适用边界维度LangChain AgentExecutorLangGraph StateGraph控制流隐式模型驱动显式图驱动状态管理散落在 memory / 消息里统一 State可检查点多 Agent 支持靠 prompt 硬凑原生支持节点即 Agent断点续跑基本没有内置 checkpointer调试难度出错难定位每步状态可打印适合场景单 Agent、简单工具调用多 Agent、复杂流程编排我的经验是单 Agent 用 LangChain 完全够别为了新而新。一旦你的流程出现多个角色分工需要条件分支需要人工介入需要断点续跑这几个信号中的任意一个就该考虑 LangGraph 了。3. token 账本多 Agent 烧钱的真凶在哪3.1 先搞清楚 token 到底花在哪很多人以为多 Agent 费 token 是因为Agent 多其实不是。真正的原因是上下文重复传递和无效往返。我做过一个实测一个调研 写作的两 Agent 任务单次执行消耗大约 12000 token。拆开看系统提示词两个 Agent 各一份约 1500 token工具定义重复注入约 2000 token中间结果在 Agent 间传递约 4000 token无效往返A 问 B、B 反问 A约 3000 token真正用于推理和生成的约 1500 token你看真正干活的 token 只占 12%剩下 88% 全是管理开销。这就是多 Agent 烧钱的真相——不是模型笨是架构在漏。3.2 用 StateGraph 把 token 账算清楚LangGraph 的好处是State 是显式的你可以精确控制每个节点能看到什么。这就给了你省 token 的抓手。我的做法是给 State 分层而不是把所有东西塞进一个大字典from typing import TypedDict, Annotated from langgraph.graph import StateGraph, START, END import operator class ResearchState(TypedDict): # 只给检索节点看的字段 query: str raw_docs: Annotated[list, operator.add] # 只给写作节点看的字段 outline: str draft: str # 全局元信息不参与推理 token_used: Annotated[int, operator.add]关键在于每个节点只读取它需要的字段只写入它负责的字段。检索节点不需要知道draft长什么样写作节点也不需要看到raw_docs的全部原文——它只需要看到摘要。这一步做完我那个两 Agent 任务的 token 从 12000 降到了 5000 左右降幅接近 60%。原因很简单写作节点不再被迫吞下所有原始文档只吃检索节点提炼过的摘要。3.3 三个立竿见影的省 token 技巧技巧一中间结果做摘要不做透传。Agent A 的输出不要原封不动丢给 Agent B而是让 A 自己先压缩一遍。比如检索 Agent 拿到 10 篇文档不要全传让它输出每篇一句话摘要 关键数据写作 Agent 拿摘要就够了。我实测这一步能省 40% 以上的传递 token。技巧二工具定义按需注入。别把所有工具都塞给所有 Agent。检索 Agent 只给它搜索工具写作 Agent 只给它文本处理工具。工具定义本身很占 token一个复杂工具的描述动辄几百 token五个工具就是一两千。技巧三给循环设硬上限。LangGraph 的图里条件边很容易写出死循环。一定要在 State 里加个step_count超过阈值强制走END。我一般设 10 到 15 步超过就说明流程设计有问题该回去改图而不是让它继续烧。提示token 账本不是让你抠门而是让你把钱花在刀刃上。省下来的 token 预算应该投到真正需要推理的节点上比如让写作 Agent 用更强的模型而不是让所有 Agent 都用顶配。4. 干扰问题多 Agent 互相带偏怎么破4.1 干扰的三种典型形态多 Agent 的干扰问题我总结下来有三种。第一种是上下文污染。Agent A 的中间推理过程被 Agent B 当成了事实依据。比如 A 在思考用户可能想要 X但也可能是 YB 看到这句话直接当成用户想要 X 和 Y然后基于错误前提往下走。第二种是角色漂移。你给 Agent B 设定的角色是审稿人结果它干着干着开始帮 A 写内容了因为它看到了 A 的草稿忍不住帮忙。这是 prompt 隔离没做好。第三种是目标冲突。Agent A 的目标是尽快给出答案Agent B 的目标是确保答案准确两者在多轮交互中互相拉扯最后谁也没赢输出一个四不像。4.2 用节点隔离切断污染链LangGraph 的节点天然是隔离的——节点之间只通过 State 通信不共享上下文。这是它相比 LangChain 多 Agent 的最大优势。具体怎么做我的原则是每个 Agent 节点只接收结论不接收过程。def research_node(state: ResearchState): # 检索节点只输出结论不输出思考过程 docs search(state[query]) summary summarize(docs) # 压缩成结论 return {raw_docs: [summary], token_used: count_tokens(summary)} def write_node(state: ResearchState): # 写作节点只看到摘要看不到原始推理 draft llm.invoke(f基于以下资料写作{state[raw_docs]}) return {draft: draft}注意research_node返回的是summary而不是docs也不是模型的完整思考链。这样写作节点永远看不到检索节点的内心戏污染链就断了。4.3 角色隔离给每个 Agent 一个信息边界除了节点隔离还要在 prompt 层面做角色隔离。我的做法是给每个 Agent 明确三件事你是谁、你能看到什么、你不能做什么。比如审稿 Agent 的 prompt 我会这么写你是审稿人。你的唯一职责是检查草稿的事实准确性和逻辑连贯性。 你只能看到草稿本身不要尝试重写它。 如果发现问题只输出问题清单不要输出修改后的版本。最后那句不要输出修改后的版本很关键。我踩过的坑就是审稿 Agent 太热心直接重写了一遍结果写作 Agent 的活白干了还多烧了一轮 token。4.4 一个干扰排查速查表现象可能原因排查方法解决方向输出跑偏上下文污染打印每个节点的 State 快照中间结果做摘要切断过程传递Agent 越权角色漂移检查 prompt 的角色约束明确不能做什么反复拉扯目标冲突看多轮交互的日志统一目标或引入仲裁节点死循环条件边设计错误加 step_count 观察设硬上限重构图token 暴涨无效往返统计每节点 token减少 Agent 间对话轮次5. 手把手搭一个不烧钱的多 Agent 流程5.1 场景设定与整体图设计我拿一个真实做过的场景来演示技术调研报告生成。需求是给一个技术主题自动检索资料、分析要点、生成一份结构化报告。我设计三个 Agent 节点加一个仲裁节点planner把主题拆成 3 到 5 个检索子问题researcher针对每个子问题检索并摘要writer基于摘要写报告reviewer检查报告决定是否需要返工图的结构是START → planner → researcher → writer → reviewer → (条件) → writer 或 END。5.2 关键节点的实现细节先定义 Stateclass ReportState(TypedDict): topic: str sub_questions: list[str] findings: Annotated[list[str], operator.add] draft: str review_notes: str revision_count: int token_used: Annotated[int, operator.add]planner节点只做一件事——拆问题输出一个字符串列表。这里我特意让它输出 JSON方便解析def planner_node(state: ReportState): resp llm.invoke( f把主题拆成3-5个检索子问题只输出JSON数组{state[topic]} ) questions json.loads(resp.content) return {sub_questions: questions, token_used: resp.usage.total_tokens}researcher节点遍历子问题每个问题检索后立刻摘要只把摘要写进 Statedef researcher_node(state: ReportState): findings [] tokens 0 for q in state[sub_questions]: docs search(q) summary llm.invoke(f用三句话总结{docs}) findings.append(summary.content) tokens summary.usage.total_tokens return {findings: findings, token_used: tokens}reviewer节点是条件边的决策者它输出一个布尔值决定是否返工def reviewer_node(state: ReportState): resp llm.invoke( f检查报告是否合格只回答PASS或FAIL{state[draft]} ) passed PASS in resp.content return { review_notes: resp.content, revision_count: state[revision_count] 1, token_used: resp.usage.total_tokens, } def should_revise(state: ReportState): if state[revision_count] 2: return END # 硬上限最多返工两次 if FAIL in state[review_notes]: return writer return END5.3 参数选择与 token 预算分配这套流程里我做了几个刻意的参数选择都是有理由的。返工上限设为 2。我试过设 3 和 5实测下来 2 次之后质量提升微乎其微但 token 翻倍。2 是个性价比拐点。researcher 用便宜模型writer 用强模型。检索摘要这种活小模型完全够用没必要上顶配。写作才需要强模型。这样整体成本能压下来一半左右。每个节点的 token 预算单独记录。State 里的token_used用operator.add累加跑完一次就能看到总账。我一般会设一个总预算比如 20000 token超了就报警。实测下来这个三 Agent 流程处理一个中等复杂度的技术主题总消耗在 8000 到 12000 token 之间比早期我那个全塞给一个 Agent的方案省了大概 40%而且输出质量更稳定因为每个环节职责清晰。6. 踩过的坑和排查实录6.1 那些让我熬夜的典型问题坑一State 字段用 list 忘了加 reducer。LangGraph 里如果多个节点往同一个 list 字段写数据不加Annotated[list, operator.add]会直接报错或者覆盖。我第一次遇到的时候排查了半小时以为是图的问题其实是 State 定义的问题。坑二条件边返回了不存在的节点名。should_revise返回的字符串必须是图里真实存在的节点名或者END。我写错过一次返回了Writer大写结果图直接崩了报错信息还不明显。坑三checkpointer 没配断点续跑失效。LangGraph 的断点续跑依赖 checkpointer我一开始没配以为它自动就有结果每次中断都从头来。配上MemorySaver或者持久化的 checkpointer 之后中断恢复才真正可用。坑四Agent 之间用自然语言传递结构化数据。我早期让 Agent A 输出一段话Agent B 去解析结果 B 经常解析错。后来改成 A 直接输出 JSONB 用json.loads解析稳定性立刻上来了。能用结构化数据就别用自然语言传。6.2 调试多 Agent 的三个实用手段手段一每个节点打印 State 快照。在节点函数开头加一行print(state)跑一次就能看到数据怎么流的。这是最笨但最有效的方法。手段二用 LangSmith 或者自建 trace。多 Agent 的调用链很长光看 print 不够需要可视化的 trace。LangSmith 能直接看到每个节点的输入输出和 token 消耗排查效率高很多。手段三单节点隔离测试。图跑不通的时候别急着调整个图先把可疑节点单独拎出来测。给它喂一个假的 State看它输出对不对。我大部分 bug 都是这么定位的。6.3 常见问题速查报错或现象根因快速修复InvalidUpdateErrorState 字段缺 reducer加Annotated[list, operator.add]图跑不起来条件边返回非法节点名检查返回值拼写用常量中断后从头跑没配 checkpointer加MemorySaver或持久化 savertoken 超预算循环没上限加revision_count硬阈值输出质量忽高忽低上下文污染中间结果做摘要隔离节点Agent 不按角色走prompt 约束不足补不能做什么的负向约束注意多 Agent 的调试成本远高于单 Agent所以能不上多 Agent 就别上。我见过太多项目明明单 Agent 加几个工具就能解决非要拆成五个 Agent最后维护成本爆炸。多 Agent 的价值在于职责分离带来的可控性如果你的任务本身不需要这种分离那就是过度设计。7. 我对多 Agent 的一点个人体会做了这么多多 Agent 项目我最大的体会是多 Agent 的难点从来不在怎么让它们协作而在怎么让它们不互相添乱。LangChain 到 LangGraph 的迁移表面上是换了个框架本质上是把控制权从模型手里拿回到工程师手里。StateGraph 给你的不是更聪明的 Agent而是更清晰的结构——而清晰的结构才是省 token 和防干扰的前提。如果你现在正准备上多 Agent我的建议是先别急着写代码拿张纸把图画出