在AI Agent应用里,“一人分饰多角”早就不是什么新鲜玩法,真正难的是让多个角色在一个工作流里像真实团队那样分工协作、有节奏地推进产出。我最近用 LangGraph 把“研究—分析—写作—审核”这条内容生产线完整落地跑通,整个过程踩了不少坑,也沉淀了不少经验。如果你正在纠结“多智能体到底该怎么编排”,这篇文章应该能帮你省下好几天的弯路。
LangGraph 本身是专为有状态、可编排的 Agent 工作流设计的框架,它不强求每个智能体都独立决策,而是允许你用“图”的方式定义角色之间的传递关系。这套“研究-分析-写作-审核”四个节点的团队结构,核心价值在于把大任务拆成职责明确的子任务,每一环都有独立的输入输出和质检标准,最终拿到一篇经过信息核实、逻辑梳理、文案润色和合规检查的完整内容。适合正在学习 Agent 编排的开发者、已经在用大模型 API 做内容产品、但觉得单次调用不够稳定的工程师参考。
1. 整体设计思路:为什么用“研究-分析-写作-审核”四段式
1.1 单一调用和流水线的本质差别
直接用大模型一次生成一篇文章,表面上省事,实际上有三大痛点:第一,没有信息检索环节,模型容易凭“幻觉”编造事实;第二,没有论证过程,内容的深度和逻辑性很难保证;第三,没有独立审核环节,格式错误、事实偏差、合规风险全都堆到最后一刻才暴露。
用多智能体协作的方式,本质上并不是“多调几次模型”,而是把创作过程拆解成一条生产流水线。研究节点负责输入“外部素材”,分析节点负责把这些素材加工成结构化的洞察,写作节点把洞察转变成自然语言,审核节点站在最终读者和风险视角做质检。每一级只需要做好一件事,提示词的约束、模型的能力、评估的指标,都可以针对单一职责精细设计。
1.2 LangGraph 相比手动循环和 LangChain Agent 的优势
不少朋友会问:既然我可以用 Python 写个 for 循环一步步调用 API,为什么还要引入 LangGraph?我的实际体会是,团队协作流程里最大的问题不是“调用”,而是“状态”。每个角色都需要访问前序角色的产出,有些流程还需要根据中间结果决定后续执行路径。用裸代码实现这些逻辑,条件分支多了之后,代码会迅速腐化,状态维护就是一场灾难。
LangGraph 提供的是定义良好的状态图模型:节点是函数,边是状态传递规则,条件边则允许你根据状态内容动态选择下一步。管理者不再需要手工维护一堆全局变量,框架本身负责把每个节点的输出写入共享状态,也支持通过 checkpoint 机制保存每轮执行的历史快照。这种模型正好贴合“研究-分析-写作-审核”这种既固定又存在反馈回路的流程。
1.3 团队成员的职责边界定义
在动手编码之前,最值得花时间的事就是把四个角色的职责边界写清楚。研究智能体对外负责检索和素材收集,对内输出事实清单和来源摘要;分析智能体不写市场稿,只做逻辑推演、论点权重排序和风险识别;写作智能体的核心目标是可读性和结构完整性,它不应该自己编造新事实;审核智能体则更像一个“守门人”,分段检查事实一致性、文风统一、格式规范和内容安全。
这样划分之后,每个节点的 Prompt 不需要写成长篇大论的“万能指令”,只需要聚焦本环节的岗位职责。比如研究智能体的 Prompt 里不需要出现“请写一篇好文”,审核智能体的 Prompt 也不需要出现“请搜索资料”。职责边界越清晰,后续调试时你就越容易判断到底是哪个环节出了问题。
2. 项目环境与基础框架搭建
2.1 环境准备与依赖安装
我这边推荐用 Python 3.11 以上的环境,新版本对异步支持和类型标注体验都更好。项目根目录下建立一个虚拟环境,然后安装核心依赖:
python -m venv .venv source .venv/bin/activate pip install langgraph langchain-core langchain-openai如果你需要让研究智能体具备联网搜索能力,大概率还会用到搜索 API 或爬虫组件,这部分建议集成到检索工具层里,不要在 LangGraph 主流程里硬编码。安装完依赖后,建议先跑一个最小用例确认 API 连通性,再开始搭图结构。
注意:LangGraph 的版本迭代速度很快,早期版本和当前版本的 API 有差异。建议你在项目里锁住主版本,并参照自己的实际版本阅读官方文档,避免直接用网上旧代码生搬硬套。
2.2 LangGraph 的核心抽象:StateGraph
LangGraph 的核心抽象是 StateGraph。你定义一个全局状态类型,然后创建 StateGraph 实例,往里面添加节点和边。每个节点都是一个接收状态、返回状态更新的函数;每条边则定义了状态更新的流向。
我对初学者的建议是先忘掉 Agent、Tool 这些上层封装,把 LangGraph 当成一个“带 checkpoint 的分布式状态机”来理解。像下面的代码就是最基础的状态图雏形:
from typing import TypedDict from langgraph.graph import StateGraph, END class WorkflowState(TypedDict): topic: str research_notes: list analysis_result: dict draft: str review_comments: list def research_node(state: WorkflowState): return {"research_notes": ["素材1", "素材2"]} def analysis_node(state: WorkflowState): return {"analysis_result": {"key_points": ["要点A"]}} workflow = StateGraph(WorkflowState) workflow.add_node("research", research_node) workflow.add_node("analysis", analysis_node) workflow.add_edge("research", "analysis") workflow.add_edge("analysis", END)这里每个节点函数返回的内容,会被框架自动合并进全局状态。后面的节点可以直接从 state 里拿到前序节点的输出,不必再显式传参。这种设计大大降低了节点之间的耦合度,也为后续插入审核反馈回路留下了扩展空间。
2.3 模型调用层与结构化输出配置
多智能体协作时,如果每个节点都返回自由文本,下游节点解析状态会非常痛苦。我的做法是给绝大多数节点配置结构化输出,即让模型直接按照 JSON Schema 返回结果。这里用到 LangChain 的 with_structured_output,能省掉自己写解析校验的流程:
from langchain_openai import ChatOpenAI from pydantic import BaseModel, Field class ResearchOutput(BaseModel): facts: list[str] = Field(description="核实过的事实清单") sources: list[dict] = Field(description="信息来源列表") contradictions: list[str] = Field(description="发现的信息矛盾点") llm = ChatOpenAI(model="gpt-4o", temperature=0.3) research_llm = llm.with_structured_output(ResearchOutput)用 Pydantic 模型定义输出结构以后,不管底层模型怎么换,节点内部拿到的永远是一个类型明确的可操作对象。审核节点后期要做自动检查,也是直接遍历这些结构化字段,而不是对一整篇长文本做正则匹配。
3. 四个智能体的核心实现细节
3.1 研究智能体:素材采集与事实核对
研究节点不仅要“会调用搜索工具”,更要能在几轮检索之后对结果做基本的可信度判断。我的设计是让研究智能体先根据用户主题,自动拆解出 5~8 个关键问题;针对每个问题触发检索工具;最后把检索到的内容总结成结构化素材包。
这里有个值得分享的细节:搜索工具返回的原始内容是混乱的,直接塞给模型既浪费 token 又容易让模型被噪声带偏。所以我在研究节点里加了文档清洗流程,保留标题、发布时间、核心段落,去掉导航、广告和无关 HTML 标签。清洗之后,再让模型基于这些内容做多文档摘要。
def research_node(state: WorkflowState) -> dict: subtopics = research_llm.invoke( f"针对主题 '{state['topic']}',生成需要调研的子问题列表" ) collected = [] for sub in subtopics: raw_docs = search_tool.run(sub) cleaned = [clean_document(d) for d in raw_docs[:5]] collected.extend(cleaned) parsed = research_llm.invoke( {"documents": collected, "topic": state["topic"]} ) return {"research_notes": parsed.facts, "sources": parsed.sources}你可以注意到,我在这里用了一次模型调用来拆分问题,然后对每个子问题做检索,最后再用一次模型调用聚合结果。两阶段的模式比“先检索再一次性总结”效果稳定得多,因为后者经常因为输入过长而丢掉尾部的细节信息。
3.2 分析智能体:逻辑框架与论点挖掘
分析节点的工作是把研究节点给出的零散事实,变成一个有逻辑主线的分析框架。这个节点不需要接触原始文档,只依赖结构化素材工作,我因此在提示词里明确要求:“如果发现素材不足以支撑论点,请明确标注缺失信息,不要猜测补充。”
分析输出包含三部分:核心结论、论点列表、风险与盲区。论点列表里的每一项都带权重字段,范围从 0 到 1,供写作节点决定内容篇幅的分配。风险与盲区则专门记录素材中的矛盾点、数据缺口和敏感表述,这些信息会直接传给审核节点用于重点复核。
实践中我发现,分析节点最怕的其实是“过度分析”。如果不给输出长度设置上限,模型会生成几百个论点,看起来认真,其实根本没法用。后来我在 Schema 里强制设置论点数量最多写 6 个,并给每个论点限定 50 字以内的摘要,写作阶段的质量反而明显上去了。约束不是限制模型能力,而是帮助模型聚焦真正重要的推理。
3.3 写作智能体:结构化表达与自然语言转换
写作节点接到的是分析框架,输出的是完整文章。它的系统提示词里强调三件事:第一,严格遵循分析节点给出的论点结构,不能自行增删主论点;第二,每个论点下方需要通过事实素材来支撑,不能写空话;第三,保持平实、克制的表达风格,避免营销化措辞。
代码实现上,我会把分析结果中的 key_points 渲染成一段 JSON,然后拼进用户消息:
def writing_node(state: WorkflowState) -> dict: analysis = state["analysis_result"] structured_prompt = f""" 请基于以下分析框架完成写作: 核心结论:{analysis["core_conclusion"]} 论点列表:{json.dumps(analysis["key_points"], ensure_ascii=False)} 素材:{state["research_notes"]} 要求: 1. 每个论点独立成段; 2. 不能出现论点之外的新主张; 3. 段落之间用过渡句衔接。 """ draft = writing_llm.invoke(structured_prompt) return {"draft": draft}有朋友可能会问,为什么写作节点不直接用流式输出?我的答案是:流水线里的中间环节最好不要流式,因为下游审核节点需要拿到完整内容做校验,流式输出只会增加状态管理的复杂度。如果你需要最终面向用户的界面展示流式效果,可以在整个工作流跑完、输出最终答案时再做一层缓冲流式处理。
3.4 审核智能体:多维度质检与反馈回路
审核节点是整个团队里最有“门槛”的一环。它不直接产出内容,而是对草稿进行四维检查:事实一致性(内容与素材是否吻合)、逻辑连贯性(论点和论据是否匹配)、风格统一性(语气、术语是否前后一致)、合规安全性(有无敏感表述或误导性内容)。
审核结果用三个字段表示:问题列表、严重程度、修改建议。当存在严重问题时,我的工作流不会直接走完结,而是带着审核意见跳回写作节点进行二次修订:
def review_node(state: WorkflowState) -> dict: review = review_llm.invoke({ "draft": state["draft"], "research_notes": state["research_notes"], "analysis_result": state["analysis_result"] }) return {"review_comments": review.issues, "severity": review.severity} def route_after_review(state: WorkflowState): if state.get("severity") == "high" and state.get("revision_count", 0) < 2: return "writer" return END这里我用了一个 revision_count 字段记录修订次数,避免出现无限循环。第二个问题:如果一轮修订之后还有严重问题,工作流直接以“需要人工介入”作为最终输出,而不是继续让模型自我纠正。这个“人工兜底”机制非常关键,因为多智能体系统不可能 100% 自愈,必要的人工审查环节反而是对整个流程质量的最大保障。
4. 状态持久化与工作流运行调试
4.1 使用 Checkpointer 保存执行记录
LangGraph 的一个明显优势是自带持久化层。通过配置 Checkpointer,你可以将每轮执行过程保存到本地或云端数据库。这在长耗时任务里尤其好用,因为脚本中断后可以从最近的 checkpoint 恢复,而不是从头开始跑完整条链路。
我使用的是 SqliteSaver,配置成本很低:
from langgraph.checkpoint.sqlite import SqliteSaver with SqliteSaver.from_conn_string("checkpoints.sqlite") as saver: graph = workflow.compile(checkpointer=saver) config = {"configurable": {"thread_id": "article-001"}} result = graph.invoke( {"topic": "如何评估开源项目的可持续发展性"}, config )thread_id 是标识一次完整工作流会话的钥匙。每次 invoke 使用相同的 thread_id,LangGraph 会从上次的状态继续执行。这个特性对实现“人工介入后继续运行”的场景特别方便:审核发现问题,工程师手动把状态里的某字段修掉,再次 invoke,流程就从修改点继续走。
4.2 事项确认:状态更新与节点返回的合并逻辑
初次接触 LangGraph 的人,最容易踩的坑是误以为节点的返回值会“覆盖”整个状态。实际机制是:节点返回的 dict 里的每个 key 会与已有状态执行合并。如果你在某个节点返回的 key 之前已经存在,它会被新值覆盖;如果你返回的 key 不存在,则新增字段。
所以,节点函数里千万不要做state["research_notes"].append(...)后直接return state这种写法,因为某些 reducer 配置下这会引发不可预期的重复更新。规范做法是创建一个新的 dict,只包含你想更新的字段:
return {"research_notes": new_notes}另一个容易疏忽的点是:如果你希望在状态里维护 list 类型的累积字段,需要在 TypedDict 定义时配置 reducer 注解。LangGraph 提供了operator.add这类开箱即用的 reducer,用于处理 list 追加逻辑。
4.3 多轮运行的性能优化
多智能体工作流一旦跑起来,token 消耗相当可观。我的项目里全流程包含至少 6 次大模型调用,如果研究和审核阶段触发多次反馈,调用次数还会上涨。为了控制成本,我做了三件事:压缩研究节点输入、限制审核节点迭代轮数、统一使用较短的输出约束。
压缩输入的思路非常简单:人工阅读长文档时可以快速扫视,模型处理长文档时却要逐个 token 计算。让模型直接处理 2 万字符的文档,倒不如先把文档按章节切块、每块生成摘要,再把摘要拼接成压缩版素材。这样既不影响信息完整性,又让每次调用省下一大半 token 开销。
5. 常见问题与排错经验实录
5.1 模型结构化输出偶尔返回无效 JSON
无论是用 function calling 还是 JSON mode,模型总有小概率返回格式不合法。这个问题在独立调用时只是报错重试,但放到工作流里就会导致整个节点崩溃。我的处理方案是在节点入口处增加一层解析容错:先尝试直接解析,失败后把错误信息反馈给模型,请求它重新生成一次。
def safe_parse(raw_output: str, pydantic_model): try: return pydantic_model.model_validate_json(raw_output) except Exception: repair_prompt = f"以下输出不符合 JSON 格式,请修正:{raw_output}" repaired = llm.invoke(repair_prompt) return pydantic_model.model_validate_json(repaired)这个方法不能保证 100% 成功,但至少把因为偶发格式错误导致全链路失败的概率降到很低。核心流程跑稳以后,再考虑引入更严格的自动校验体系。
5.2 审核节点误报严重问题导致循环修订
审核模型有时会变得过于“严苛”,把一些语气风格问题当成严重事实错误,然后把工作流打入“需要修订”的分支,白白多消耗两轮 token。这种现象在开环流程里不致命,在闭环反馈里就会被放大,甚至把最终输出改得面目全非。
后来我在审核提示词里加了“问题分级标准”,明确列出什么样的问题属于 blocking、什么样属于 non-blocking。同时,我给每个问题增加了置信度字段,只有置信度高于 0.75 且类型为 blocking 时,才触发返工路由。经过这轮调整,误报导致的无效循环减少了 80% 以上。
5.3 状态对象体积过大导致执行缓慢
保存大量素材原文会让状态对象变得非常臃肿,每轮节点执行都要对这个大对象做序列化和传递,拖慢整体速度。优化思路是只保留中间产物在状态里,原始文档写入外部存储,通过 ID 引用。审核节点需要看原文时,根据 ID 从存储里拉取,不占状态的尺寸。
class WorkflowState(TypedDict): topic: str research_notes: list source_refs: list[str] analysis_result: dict draft: strsource_refs 存储的是文档 ID 列表,而不是整篇文档。这样状态对象一直保持轻量,执行速度和调试清晰度都有明显提升。你可以根据项目的规模决定这种优化是否到位:如果只是偶尔跑几十篇内容,不优化也没关系;如果要接进线上服务,状态优化几乎是必须的。
6. 延伸扩展:如何把单一团队升级为可插拔工厂
目前的“研究-分析-写作-审核”是一个相对固定的线性加反馈结构。如果想把这套系统复用到更多内容类型,比如营销文案、技术教程、产品说明书,没必要重新搭一套图,只需要把各节点的提示词和 Schema 参数化。
我在项目里增加了一层“模板配置”,用 JSON 文件保存不同内容类型对应的节点配置:
{ "article_type": "technical_tutorial", "research": { "subtopic_count": 6, "require_code_examples": true }, "writer": { "style": "plain", "max_section_depth": 3 }, "reviewer": { "blocking_rules": ["fact_error", "misleading_code"] } }工作流的图结构不需要变,每个节点启动前读取这块配置,就会表现出完全不同的行为特征。这种设计让系统具备很强的扩展空间,后续接入更多垂直领域,只需要补充配置和微调少量逻辑。
如果你想把审核环节升级成“多轮对抗式审查”,也只需要在图里继续添加新的审核节点,用条件边串联起来,LangGraph 对这种扩展非常友好。整体思路就是保持状态模型稳定、节点行为可配置、路由逻辑尽量集中在少数条件函数里,系统复杂度就能一直控制在可维护的范围内。
7. 我在实际跑完这套流程后的一些体会
把“研究-分析-写作-审核”这套团队真正落地,我最强烈的感受是:多智能体的瓶颈往往不在模型能力,而在流程设计。研究节点的检索质量不高,分析节点拿到错误素材再怎么聪明也写不出好内容;审核节点提了一堆空泛意见,反馈回路也只是白白消耗 token。每个节点的输入输出质量,决定了最终系统的天花板。
另外,不要迷信“全自动”。我一开始尝试完全无人参与,结果发现部分领域内容仍然需要人工校准。后来我把系统调整为“自动跑主流程 + 低频人工抽检 + 高危内容人工复核”,整体效率和可靠性同时得到提升,实际运行稳定很多。这套折中策略比追求极致自动化要务实得多,也更容易说服团队其他角色接受这套多智能体工作流。