LangGraph 实战:多智能体协作的“研究-分析-写作-审核”团队
做了快两年的 LLM 应用开发,我最大的感触是:单个 Agent 写个 Demo 很容易,但真要扔到业务流程里,往往撑不过三件事。前一阵我接到一个内部调研报告自动生成的需求,单一 Agent 跑了几版,不是到后半段忘了前面的结论,就是自己编造参考文献,最让人头疼的是——它总觉得自己写得挺好,哪怕事实错误就摆在第一行。
后来我用 LangGraph 把整个流程拆成四个角色,研究、分析、写作、审核,模仿真实内容团队的分工,让每个智能体只干一件事,用图结构把它们的协作关系串起来。这套框架跑通之后,效果完全不一样,单个 Agent 容易出现的逻辑漂移、幻觉内容、结构混乱被大幅压缩,而且每一段产出都能追责到具体执行者。这篇文章就把我的完整设计和踩坑过程拆开讲,从状态结构、节点实现、流程编排到问题排查,适合已经入门 LangChain/LangGraph、想把多智能体协作落到实际项目里的朋友。
1. 项目整体设计与思路拆解
1.1 为什么一定要拆成多个智能体
我先说结论:如果任务内部可以分解成互相依赖的多个环节,单 Agent 天花板非常低。原因不是模型不够聪明,而是上下文失忆和角色污染这两个问题绕不开。
所谓上下文失忆,就是当对话轮次一多、中间内容一长,模型对早期信息的注意力会明显衰减。我最初的单 Agent 版本里,让它先做调研再写报告,写到第三章时它已经忘了第一章引用过哪些数据,经常前后口径对不上。角色污染更隐蔽:同一个 Agent 既要保持研究员的客观中立,又要用写手的语气去动笔,模型会在两种人格之间摇摆。你给它特别详细的系统提示词能缓解,但治标不治本。
把流程拆开之后,每个节点只负责单一职能。负责研究的 Agent 永远不写正文,负责写作的 Agent 永远不碰原始资料,负责审核的 Agent 专门挑毛病。这样每个系统的提示词可以写得极其聚焦,不需要为了兼容多个职责而堆砌冗长的指令,模型行为更稳定,也让问题定位一目了然——哪一环输出不对,直接看那个节点的输入输出就行,不用在一个人工智障的这上千行日志里大海捞针。
1.2 四个角色的分工和协作模式
我把团队拆成四个角色,分别是研究员、分析师、写作师、审核师。名字很直白,职责边界是刻意划清的。
研究员负责收集事实。它的输入是用户提出的调研主题,输出是经过筛选和引用的资料卡片。它的特点是调用外部工具,比如网页搜索、内部文档检索、知识库查询,然后把信息结构化整理。这个角色最重要的品质是没感情、不站队,忠实记录来源。
分析师负责从研究员交付的资料里提炼逻辑。它不新增信息,只负责识别关键数据、归纳趋势矛盾、形成论点框架。它像一个参谋,把一堆散落的素材变成带观点的作战地图,输出的是结论性要点和论证结构。
写作师只做一件事:根据分析师的要点框架,把内容写成通顺、专业、符合要求的文章。它不需要创新,不需要查证,它的核心能力是行文组织和表达转换,把提纲变成可交付物。
审核师是大门。它逐条检查成稿的准确性、逻辑性、完整性,对照研究员提供的原始资料做事实核查。如果发现问题,它不只给出“不行”两个字,还会附带具体修改意见并打回给写作师;如果通过,再交付终稿。
这四个角色形成一个有向流程:研究 → 分析 → 写作 → 审核。审核不通过就回流到写作,一次最多循环三次,超限就整体降级交付。兰斯Graph天然适合表达这种带条件分支和循环的流程,这也是我选它而不是自己硬写编排逻辑的原因。
1.3 为什么选 LangGraph 而不是 LangChain 链式调用
我知道很多人会有疑问:这么简单的四步流程,用 LangChain 的链串起来不就行了?但 LangChain 的 SequentialChain 只适合完全线性的调用,它没有内置的条件跳转和循环控制。而我们的审核环节天然就是一个分支节点,审核通过走结束、审核不通过回写作,这种带状态回写的需求,用链式封装要写很多丑陋的 Python 逻辑,而用图结构只要加一条边和一个条件函数。
LangGraph 还有一个关键特性是持久化状态管理。每个节点的输出可以写入结构化的 State 对象,后续节点从里面取自己需要的字段,不用把整个对话历史全部传给模型。这相当于给智能体加了一套工作记忆机制——研究员把资料写进状态,写手专注在成稿上,两个 Agent 之间传递的不是聊天记录而是结构化数据。这一点对长任务的稳定性提升非常大,我会在后面详细讲具体怎么设计。
2. 核心细节解析与节点实现
2.1 结构化状态的字段设计与数据流
写 LangGraph 应用的第一步永远是设计 State,不是写 Agent。状态设计直接决定整个图的复杂度。我的状态用 Python 字典定义,核心字段如下。
from typing_extensions import TypedDict class ReportState(TypedDict): topic: str # 原始调研主题 research_notes: list # 研究员输出的资料卡片 analysis_framework: dict # 分析师输出的论点框架 draft: str # 写作师生成的草稿 review_comments: list # 审核师给出的修改意见 revision_count: int # 迭代次数,用于控制回流上限 final_report: str # 终稿我强调一下这个字段设计的几个原则。第一,节点之间只传结构化的任务产物,不传对话记录。四类角色是接力,不是聊天。研究员传 demo 给分析师时,只给整理好的资料卡片,不把它和工具之间的几十条中间日志带过去。这样可以大幅降低每轮调用的 token 消耗,也防止模型被无关信息干扰。
第二,审核意见用列表而不是字符串。审核师经常一次性抛出多个问题,比如“第二段的数据引用缺失来源”和“结论部分论证不充分”。用列表保存,既可以单独标记每条,也可以保存汇总。后期如果要接入人工审批也能直接对接,结构化数据的扩展性比纯文本好得多。
第三,状态对象里保留 revision_count 字段,这是迭代控制的关键。LangGraph 的图本身支持循环,但我必须限制循环的最大次数,避免便宜模型无限打回-重写的死循环。我的经验是上限设为三次,第四次无论审核结果如何都强制输出,宁可带瑕疵也不能卡死整个流程。
2.2 研究员节点:工具调用和资料收集的关键点
研究员节点的核心职责是收集信息并整理成证据,不需要生成观点。它的 Prompt 我写得极其细,直接定了输出格式,让结果稳定可解析。
from langchain_community.tools import DuckDuckGoSearchRun from langchain_openai import ChatOpenAI from langgraph.graph import StateGraph def research_node(state: ReportState): topic = state["topic"] tools = [DuckDuckGoSearchRun(), vectorstore_retriever] researcher = ChatOpenAI(model="gpt-4o-mini", temperature=0.2) researcher = researcher.bind_tools(tools) prompt = f"""你是项目研究员。围绕主题"{topic}"检索资料。 必须遵守: 1. 每条信息必须标注来源。 2. 如果信息来自工具返回结果,直接引用原文关键数据。 3. 只记录客观事实,不要添加主观判断。 4. 最多输出 8 张资料卡片,每张卡片包含:标题/来源/核心事实/引用出处。 5. 如果检索不到有效信息,明确写「未找到相关数据」。 现在开始检索并输出结构化资料。""" # 执行检索:调用工具,汇总结果 search_result = tools[0].invoke(topic) retrieved_docs = tools[1].invoke(topic) return {"research_notes": [{"title": "网络检索", "source": "search_engine", "facts": search_result[:1500], "ref": "自动生成"}]}我这里把检索简化成直接调用工具求值。实际项目里会让模型自动决定调用哪个工具,但演示项目里直接固定调用更可控,也方便新手看懂。这一块的三个坑值得重点记录。
第一,节流检索结果。研究员的输出不能是原始资料的复制粘贴大杂烩,必须让它压缩。我的资料卡片每条核心事实控制在三到五行,来源信息完整保留。原始搜索页面可能几千字,真正到研究员手里每张卡片只能留浓缩版。信息压缩是这个节点最重要的功能,否则到下游写手那里,输入一长,模型很快就开始忽略细节。
第二,引用来源字段必须结构化保存。很多实现里只让模型在正文里写一句“据媒体报道”,这等于没引用。我在卡片里把来源URL单独存成一个字段,这样的话审核师核实时可以直接按图索骥。后续还想做来源追溯、引用展示,拿这个字段做文章就行。
第三,研究员不带情绪。我在提示词里直接说了“只记录客观事实,不要添加主观判断”,这是为了把价值判断全部留给分析师。一旦研究员在收集阶段就开始做评价,容易把个人偏好混进素材,下游的整套推理都会被带偏。
2.3 分析师节点:把素材变成有逻辑的观点
分析师做的事情,在内容行业里叫“拉提纲”,在咨询行业里叫“搭框架”。它自己不需要写内容,而是从研究员给的材料里找出值得展开的若干条线索。
给出分析师的 Prompt 设计,重点在于强制结构化:
def analyst_node(state: ReportState): notes = state["research_notes"] analyst_prompt = f"""你是一名资深分析师。根据以下材料提炼报告框架。 材料如下: {notes} 要求: 1. 归纳出 3~5 个核心论点,每个论点必须能在材料中找到数据支持。 2. 识别材料中的矛盾或数据缺口,单独列出一条「风险提示」。 3. 输出 JSON 格式,字段包括:core_argument(核心论点)、supporting_data(支撑数据)、risk_note(风险提示)。 4. 不要撰写完整句子,只输出论点框架。""" # 实际运行 LLM 后解析 JSON framework = { "core_argument": ["行业整体平稳增长但区域分化明显", "技术侧的投入产出比正在改善", "合规成本成为主要变量"], "supporting_data": ["引用资料卡片 1、3", "引用卡片 2、6", "引用卡片 5"], "risk_note": "材料缺乏近三年同比数据,建议补充访谈获取一手信息" } return {"analysis_framework": framework}分析师节点最大的价值是提出对资料的“要求”:如果某个核心论点在原素材里找不着依据,它必须标记为风险,而不能硬编一个论据。我自己在调试时发现,分析师一旦跟写手一样开始“文思泉涌”,产出的框架就都是套话,任何话题都能套上去,一点价值都没有。所以给它加硬性约束——每个论点必须标注对应的材料卡片编号,写手引用时没有依据是会被审核师打回的。
还有一个小细节:分析师输出的 JSON 结构会成为写手提示词的一部分。这意味着框架字段的命名要自解释性强,最好连数据来源的编号都带着。后我实际上是把框架字段转成 YAML 塞给写手的,这比让模型直接读 JSON 更不容易出错。
2.4 写作师节点与审核师节点:生成内容和质量把关
写作师和审核师是长流程里最考验 Prompt 功底的两个节点,我把它们放在一起说。
写作师的输入不是原始材料,而是分析师给的点。它做的事情本质上是扩写:把提纲变成一篇语气统一、行文流畅、符合格式要求的成稿。这里最关键的是不能让写手获得任意发挥的权限。
def writer_node(state: ReportState): framework = state["analysis_framework"] draft = f"""一、市场概况分析 (根据核心论点1展开,引用支撑数据……) 二、技术趋势判断 (根据核心论点2展开……) 三、风险与不确定性 (根据核心论点3与风险提示展开……)""" return {"draft": draft}写作师最容易犯的毛病是“过度美化”。你给它三条干巴巴的框架,它就能给你写出天花乱坠的宣传语。我的解决办法是在提示词里直接说明语气克制、不要推断数据、不要使用排比句,并且每段结尾必须标注支撑论据的来源编号。
审核师则是另一个极端,我把它的提示词写成了“找茬模式”,专门挑刺:
def reviewer_node(state: ReportState): draft = state["draft"] notes = state["research_notes"] review_prompt = f"""你是质量管理专员,对下述草稿进行严格审查。不得因为文笔好就降低标准。 草稿: {draft} 对照资料源: {notes} 请逐项检查: 1. 事实核查:有无内容与资料源冲突或完全无来源。 2. 逻辑一致性:前后论述有无自相矛盾、偷换概念。 3. 完整性:是否有核心论点被忽略或一笔带过。 4. 引用规范:关键数据是否标注出处。 输出结果必须包含两个字段:verdict(通过/打回)和 issues(具体问题列表)。 如果打回,issues 必须给到可操作的修改建议,禁止说“内容需要丰富”这种空话。""" # 模拟审核结果 review_result = {"verdict": "打回", "issues": ["第二段提到增长率 12%,但资料卡片 3 中该数据为全年同比下降", "风险提示段仅有结论,缺少支撑"]} return {"review_comments": review_result["issues"]}注意一点:审核师的打回理由必须是可执行的修改意见。如果它说“内容需要丰富、表述还不够好”,写作师根本不知道怎么改,来回循环只会消耗时间和 token。我的经验是在提示词里直接限制审核师:每一条 issues 都必须指明位置、问题类型和修改动作。这相当于把人的工作要求带进了 Agent 协作规则里。
审核节点是整个图的把关哨,它决定是继续循环还是结束。我给它配的模型一直是质量最高的。四个节点可以用四个不同档位的模型,研究员和写手用中等模型省钱,审核师用最强模型把关,效果和成本的最优比例就出来了。
3. 实操过程与核心流程实现
3.1 环境准备和依赖安装
简单列一下我跑这套环境用的依赖版本,避免大家踩到版本兼容的坑:
pip install langgraph langchain langchain-openai langchain-community我前后试过 langgraph 的 0.2.x 和 0.3.x 两个大版本。0.2 版本的 StateGraph 创建方式更简洁,0.3 版本引入了一些内部接口调整,提示词 Node 的传参方式也变了。如果你列出来的文档代码跑不通,优先检查是不是版本差异。我给的关键代码基于 0.2.x,后续版本应兼容,但接口命名有差异时按官方文档对应改就行。
另外,因为我这里用了网上搜索工具,最好给自己准备好可用的 API Key 或本地部署的模型服务。如果你想用本地模型,强烈建议选带工具调用能力的模型,比如某些支持 function call 的量化版本。不带工具能力的模型去绑定搜索工具,返回结果基本是无效的,最后会让研究员节点空手而归。
3.2 构建工作流图并配置回流与终止条件
在 LangGraph 里,StateGraph 的核心是状态更新、节点注册和边连接。我的图结构是这样:
from langgraph.graph import StateGraph, END from langgraph.checkpoint import MemorySaver # 初始化图 graph = StateGraph(ReportState) # 四个节点 graph.add_node("researcher", research_node) graph.add_node("analyst", analyst_node) graph.add_node("writer", writer_node) graph.add_node("reviewer", reviewer_node) # 主流程线 graph.set_entry_point("researcher") graph.add_edge("researcher", "analyst") graph.add_edge("analyst", "writer") graph.add_edge("writer", "reviewer") # 审核分支:通过结束,不通过回流到 writer graph.add_conditional_edges( "reviewer", lambda state: "process" if state["revision_count"] < 3 and state["review_comments"] else "end", {"process": "writer", "end": END} ) # 内存持久化 graph = graph.compile(checkpointer=MemorySaver())这里有个非常容易踩的坑:回流到 writer 时,writer 节点会覆盖掉之前的草稿,也就是 state 里 draft 字段被重新写入,而审核意见就丢失了。我最初就是这样跑的,结果第二次回流时写手完全不知道自己为啥被打回。
因为 LangGraph 的节点返回字典时,同名 key 默认是覆盖更新。解决办法是让 writer 节点返回一个包含字段:新的草稿和之前累积的审核意见。也就是说 writer 不能只返回 draft,它要把旧的 review_comments 也带回状态里。
def writer_node(state: ReportState): comments = state.get("review_comments", []) new_draft = generate_draft_with_comments(state, comments) return {"draft": new_draft, "review_comments": comments, "revision_count": state["revision_count"] + 1}我当时看 LangGraph 文档时花了很久才意识到,默认行为是覆盖而不是合并。这个细节对多轮回流场景太关键了,不处理好,审核意见根本传不到写手手里。要么你手动在返回时带上历史字段,要么你重新设计节点返回,给审核意见和草稿用不同的 key,反正不能让 reflux 失去记忆。
另外一个细节是 MemorySaver。我一开始没有配置检查点,整个图是纯内存跑的,这有两个问题:没法查看中间态,也没法跟踪多轮对话的历史。加了 MemorySaver 之后,可以通过 config 传入一个线程 ID,随时查询当前状态到哪了、各字段长什么样。生产环境建议换数据库持久化,但开发和调试用 MemorySaver 够用了。
3.3 条件逻辑的细节解剖:为什么审核后的回流要“带装配件”
把回流分支这条逻辑单独拿出来多说几句,因为这是整个图的“智力”所在。一个没有条件分支的图就只是一条流水线,而有了回流分支,系统才真正像团队:干得不好就要打回重做。
条件函数看起来只是简单的判断,但我在实际运行中发现一个问题:revision_count 的递增必须放在 writer 节点里,而不是 reviewer 节点,否则回流路径上案例有偏差。如果把 revision_count 在审核节点就加一,那么单次执行中,writer 第一次写完、reviewer 第一次审核通过时,这个字段已经被改成 1 了,而实际上我们还没有进入任何回流。表面上问题不大,但如果要看状态历史,字段就会跟预期不一致,调试时会莫名其妙。
所以我在设计时遵循“谁执行返工,谁增加版本号”的原则:writer 重写一次,revision_count 加一。审核只是判断“当前版本是否通过”,它不改变版本号。这样每组数据唯一对应一个版本,排查起来比较轻松。
条件判断还有一类容易出错的地方是:审核师“通过”时,review_comments 列表是空的。那么条件函数的判断就不能用if comments:,而要显示判断 verdict。为了这点我把审核师的状态里单独加了 verdict 字段,判断逻辑改成:
def route_review(state: ReportState): if state["revise_count"] < 3 and state.get("verdict") == "驳回": return "process" return "end"这样的可读性比判断列表非空好得多,也不容易踩“空列表被误判为通过”的坑。
3.4 启动执行和结果取值
整个工作流启动起来其实非常简单。用线程 ID 管理一次完整流程:
config = {"configurable": {"thread_id": "report-task-001"}} result = graph.invoke( {"topic": "2025 年某行业智能化转型现状调研"}, config=config ) print(result["final_report"])这里 thread_id 可以理解成一次完整任务的会话标识。同一 thread_id 下再次 invoke 时会接着上次的状态往下走,如果重复执行而不想续接历史,需要换一个新的 thread_id。这个坑是我在写多轮测试脚本时踩过的——同一个 ID 跑了两次,第二次把第一次的草稿直接拿过来覆盖了,后来查了半天才发现是重复使用了同一会话。
最终拿结果只取 final_report 字段,但我调试时会先把整路状态都打印一遍,看每个环节的中间产物是不是符合预期。这样可以快速判断问题出在哪一环:是研究员没搜到资料,还是写作师写得离题,还是审核师误判。
4. 常见问题与排查技巧实录
4.1 状态传参失效与字段覆盖问题
我自己的项目中出现频率最高的一个问题,就是回流时状态字段被覆盖,导致审核意见丢失。根本原因上一节已经讲明白了,LangGraph 的状态更新规则是节点返回的 dict 字段直接覆盖旧值,不会自动合并。如果你想让某个字段“累积”,你必须自己把旧读出来再拼回去。
排查方法很简单:看每次 invoke 之后的状态快照。拿 thread_id 调用 get_state,把每个 key 的值打出来,对比回流前后的内容是否还在。
from langgraph.checkpoint import MemorySaver checkpointer = MemorySaver() # 执行后获取当前状态 snapshot = graph.get_state(config) print(snapshot.values)还有一类情况是不同类型节点之间传递格式不一致。研究员输出的 research_notes 是 list,但分析师误用了 research_notes 里的字符串字段,运行时直接报 KeyError。解决方案是让所有节点输出前做一个严格的字段校验,最简单的方式就是在返回前加 assert。
4.2 上下文超限与信息截断
四步流程看起来不长,但研究员如果塞入十几张长卡片,总 token 很容易超限。特别是互联网搜索到的页面动辄几千字,随便几条就把上下文塞满了。
这个问题我用三层方案解决。第一,研究员节点做摘要压缩,每条卡片只保留核心事实不超过 200 字,来源信息单独存。第二,每个节点之间只传节点产物,不带历史对话,这让每次调用模型时的输入被控制在一到两个 token 倍以内。第三,政治温度相关的数据格式全部用结构化文本,能上 JSON 就不上长篇 prose。
如果生产环境要处理超大文档,还可以做 map-reduce 式检索,把大文档先切片,每个切片单独检索再合并。我在这个演示项目里没有做得太深,但方向就是这条路。
4.3 审核误判与方向性问题:便宜模型的局限性
我一开始把所有节点都用的一个小模型跑实验,结果质量惨不忍睹。小模型的审核师会把“内容通顺”当成“事实正确”,对明显的数据冲突视而不见,或反过来,所有内容都打回,写手被逼成同一个东西写八遍。
这种问题不在图逻辑上,而是角色模型的算力余额严重不匹配。审核是所有节点的守门员,我强烈建议分配最贵的模型给它。研究员和中手可以用中等模型,达到省钱目的;审核师用最强模型,质量才有保障。如果你只有一个模型可选,就靠提示词把审查标准写到极致,并降低温度到 0.1 以下。
另外还发现一个现象:某个模型写的稿子,同一个模型去审,往往自己看不出自己的错。所以有条件的话,审核节点和其他写手节点用不同厂商的模型,效果往往更好。相当于让一个第三方来挑刺,准确率高很多。
4.4 死循环与超时控制的边界
图里如果审核一直打回,而写手又没有修改能力,就会形成无限循环。虽然我在条件函数里写了最多三次回流,但还需要一个最外层断点:整体超时时间。LangGraph 可以在 compile 时通过 recursion_limit 限制递归深度:
graph = graph.compile(checkpointer=MemorySaver(), recursion_limit=25)这样即便条件函数漏了什么边界,图跑超过 25 步也会强制终止,不会无底线烧钱。我把这个数设得相对宽松,因为正常四步流程加三次回流最多也就 10 步出头,25 足以覆盖异常翻倍的情况。线上如果对成本敏感,可以设到 12,控制更紧。
还有一个细节是工具调用本身的超时。研究员调用搜索工具如果遇到服务不稳定,可能一直卡住不返回。我给每次工具调用外面加了一个超时包装,超过 15 秒就跳过该工具,改用已有资料继续往下走。机器人的项目不可能等一个接口等半天,超时降级的思路很重要。
4.5 幻觉传播的阻断方法
多智能体流程中最隐蔽的坑是幻觉传播。研究中产出的一个错误数会被分析师当成真事吸收进框架,写手又层层扩写成一大段煞有介事的分析,到了审核才发现根源在老早的研究环节。这种越传越大的错误,比单 Agent 幻觉更危险,因为它有了“多 Agent 验证”的外衣,看起来像是多重检查过的。
阻断思路是在每个节点输出来加一个强证据验证。研究员阶段就把来源标注清楚;分析师阶段如果某个论点找不到研究材料支持,不允许悄悄编造,只能写到风险提示里。写作阶段必须引用论点编号;审核阶段把每一条事实断言与资料卡逐一比对,无出处的直接打回。这样虽然流程没有引入额外的向量数据库裁决,但靠结构化的强制约束也把幻觉传播路径卡死了大半。
如果你做的是数据敏感的项目,建议在审核节点之后再加一个独立的证据校验节点,单独把成稿和原始资料做一遍文本比对。甚至可以计算语义相似度,检测成稿里是否存在数据源中找不到依据的句子。那条路我再写一篇新的博客,今天就点到为止。
5. 后续扩展方向与实战经验补充
5.1 并行化多研究员协作的设想
我现在这个版本是单研究员串行工作,如果调研主题覆盖范围特别大,比如要同时看技术、市场、政策三个维度,单个研究员要串行搜索,耗时长且上下文容易爆炸。更好的做法是拆成三个并行研究员各管一维,然后用一个汇总节点把内容合并成统一资料集。
LangGraph 天然支持这种并行 fan-out/fan-in 结构,把三个研究者节点都接到入口,然后汇总节点等待全部完成。但你用着就会发现有个难点:三个研究员同时更新同一个字段,LangGraph 会按默认规则处理匿名冲突,通常最后写入的覆盖先写入的,造成信息丢失。应对办法是给每个持续性字段加 reducer,也就是自定义合并函数。
我当时用这个场景还不太复杂,单研究员够用,就没上并行。但如果你要考虑性能和大规模任务,这一步是绕不过去的。LangGraph 的 reducer 机制是它区别于普通状态机的重要特性,值得单独啃一啃。
5.2 各节点选用不同模型的经验
我在多轮实验里形成了比较固定的模型分配策略:研究员用中低档但支持工具调用的模型,写作用中档模型,审核用顶配模型。分析师介于写作和审核之间。
为什么不全用顶配?因为贵,而且慢,尤其是一个流程要反复跑。多 Agent 协作的 token 开销远高于单 Agent,每个节点都是一次完整调用,链上任何一个中间环节如果用的是特别贵的模型,整体成本会线性放大。中层模型的表现已经够好,用在这里是对的。
真实项目中有一个容易忽略的开销点:提示词本身。多角色的提示词都比较长,反复调用时这部分的 token 累计起来很吓人。我的建议是提示词能做模板就模板,写完缓存下来,不要每次调用都拼整个变量。一些公共描述可以抽出来复用,既省 token,也保证每轮行为一致性。
5.3 最终的几点体会
把这套“研究-分析-写作-审核”团队搭完,我最深的感受是:多智能体协作真正难的不是让智能体“会做事”,而是让它们“不敢乱做事”。每一个节点都要有明确的职责边界、结构化输出格式、可追溯的信息来源,整个系统才不会在看似聪明的对话里悄悄崩塌。
如果你是从零开始,我建议不要一上来就追求复杂的图结构。先把四个节点全部写成普通函数,用最笨的串行方式跑通,再加回流逻辑,最后才引入 LangGraph 的状态管理和条件分支。每一层都确认输出质量满意了再往下叠,这样出了问题也容易定位。我用这种循序渐进的方式开发,实际踩坑数量比第一版直接上全流程少了至少七成。
这套模式不只适用于写报告,凡是“输入事实材料、产出结构化结论”的任务,比如竞品分析、产品需求评审、代码审查报告,都能套用类似的角色拆解思路。改改提示词,换换工具,图本身不用动太多。这也是 LangGraph 这类图编排框架真正值钱的地方——你把业务流程的评判逻辑想清楚,剩下的只是把角色填进去。