主题
LangGraph Agent 编排
你有没有遇到过这种情况:一个任务交给 AI,第一版跑出来的结果还不错,改了两轮需求之后,整个流程变得像一团线。Prompt 越来越长,中间产物不知道存在哪,出错后只能从头再来。
很多 Agent 项目就是这样从“单次模型调用”滑向“多阶段工作流”的。一旦你开始处理需求解析、资料召回、结构规划、产物生成、审稿、改写、交付检查,单纯靠 if-else 串联已经很难维护了。
1. 学习目标
读完这篇,我们会把 LangGraph 放在 Agent 系统的整体位置上看。重点理解四件事:为什么复杂任务需要一张图;节点之间应该用什么方式交接;条件路由和人工确认点怎么设计;哪些任务必须阻塞用户,哪些可以放到后台。
2. 为什么 Agent 系统需要一张图
前面几篇文章已经拆过资料记忆、上下文调度、Prompt 分层、任务状态和混合检索。单个模块能跑通之后,新的问题会冒出来:这些模块按什么顺序执行,哪些步骤可以并行,哪些结果必须人工确认,失败后从哪个中间状态恢复。
一个典型任务很少只有“输入 -> 生成 -> 输出”。用户提交目标后,系统至少要处理八类动作:
- 判断对象、场景、范围和交付格式。
- 召回项目文档、历史产物、官方资料和审稿规则。
- 生成结构,并标记资料缺口。
- 生成产物。
- 审稿,检查事实、结构、语气和引用。
- 根据审稿意见改写。
- 做格式、链接和交付前检查。
- 写回可复用资料、审稿结论和 trace。
这些步骤之间有依赖,也有分支。资料不足时要追问或补检索;事实冲突时要人工确认;审稿失败时要回到改写;格式检查失败时只修局部。
用一串 if-else 可以做出早期原型,但后续会遇到三个维护问题:
- 中间结果难以复用。
- 失败位置难以定位。
- 新增 Agent 后容易影响既有路径。
LangGraph 适合承载这类流程,因为它把执行步骤、共享 State、条件路由、持久化和 interrupt 机制放到同一个图模型里。对 Agent 系统来说,这张图就是运行时契约:每个 Agent 只关心自己读写哪些字段,路由函数根据结构化结果决定下一步。
LangGraph 把执行步骤建模为图:节点是 Python 函数(可以只包含普通代码,也可以调用 LLM),边是节点之间的流转,条件边是根据当前 State 动态选择下一个节点的路由函数。它底层用消息传递(message passing)驱动执行,节点完成后把状态更新沿边发送给下一个节点,整个执行过程按离散的“super-step”推进。没有依赖关系的节点会在同一个 super-step 里并行运行,所以图模型天然支持并行分支,而不需要额外写并发控制代码。
3. 一条任务请求的主路径
先看一条最小可用的同步主路径:
txt
接收任务
-> 解析任务目标
-> 资料召回
-> 结构规划
-> 产物生成
-> 审稿
-> 是否通过?
├── 否 -> 改写 -> 再审稿
└── 是 -> 交付检查 -> 返回结果这条路径里,每个节点只承担一类责任,便于替换、重试和观测。
| 节点 | 输入 | 输出 | 失败处理 |
|---|---|---|---|
| 解析任务目标 | 用户需求、项目配置 | 对象、场景、范围、交付格式 | 缺少关键字段时返回澄清问题 |
| 资料召回 | 任务目标、检索策略 | 资料卡片、来源、可信度 | 标记资料不足,不直接编造 |
| 结构规划 | 任务目标、资料卡片 | 任务结构、每部分要回答的问题 | 冲突资料进入人工确认 |
| 产物生成 | 结构、资料、风格规则 | 产物版本 | 保留低置信度标记 |
| 审稿 | 产物、资料、检查清单 | 问题清单、阻塞级别 | P0/P1 问题阻塞交付 |
| 改写 | 产物、审稿意见 | 修订产物 | 保留修订说明 |
| 交付检查 | 修订产物、格式规则 | 可交付版本 | 格式失败只回到局部修正 |
这张表需要早于代码出现,因为它定义的是图的边界。后面无论使用 LangGraph、Temporal,还是自研 workflow runner,都要先确认每个节点的输入输出契约。没有这份契约,Agent 数量增加以后,系统很容易变成“节点名称很多,但每个节点都在读写全部上下文”。
4. State 是 Agent 之间的公共契约
LangGraph 使用共享 State 让节点交接数据。Agent 系统的 State 可以按用途拆成几组:任务目标、资料来源、结构、产物版本、审稿问题和观测字段。
typescript
type AgentTaskState = {
taskId: string;
stage:
| "intake"
| "research"
| "plan"
| "execute"
| "review"
| "rewrite"
| "delivery_check"
| "done"
| "blocked";
goal: {
topic: string;
audience?: string;
channel?: string;
format?: string;
};
sources: Array<{
id: string;
title: string;
url?: string;
reliability: "official" | "project" | "secondary" | "unknown";
usedInSections: string[];
}>;
plan?: Array<{
heading: string;
question: string;
sourceIds: string[];
}>;
product?: string;
reviewFindings: Array<{
severity: "P0" | "P1" | "P2";
message: string;
targetSection?: string;
resolved: boolean;
}>;
traceId: string;
};节点应该围绕 State 字段编程。资料召回节点只负责写 sources,结构规划节点只读取 goal 和 sources,审稿节点只读取 product、sources 和检查清单。这样拆分以后,替换资料检索实现时,不需要改执行节点;替换审稿策略时,也不需要改结构规划节点。
State 设计时还要区分“给模型看的上下文”和“给系统看的控制字段”。产物草稿、资料卡片和风格规则可以进入模型上下文;stage、traceId、reviewFindings[].severity、resolved 这类字段更适合作为路由、恢复和排障依据。把两类信息混在一段自然语言里,后续恢复任务时会缺少稳定判断点。
LangGraph 通过 reducer 合并各个节点返回的部分 State 更新。默认行为是覆盖,但对于列表字段(如 sources、reviewFindings),通常需要追加语义,可以用 operator.add 或 add_messages 做 reducer,避免并行节点写同一份列表时互相覆盖。如果图的内部节点需要传递一些不需要暴露给调用方的字段,可以定义 private State channel;也可以为图显式声明 input/output schema,让外部只看到必要的字段。这些机制共同保证:节点之间只通过约定好的字段通信,新增或替换节点时不会污染全局 State。
5. 条件路由和人工确认点
Agent 任务里最重要的分支通常来自质量门禁:
- 资料不足:回到资料召回,或向用户追问。
- 来源冲突:进入人工确认点。
- 结构缺少主问题:回到结构规划。
- 审稿有 P0/P1 问题:回到改写。
- 只有格式问题:进入格式修正,不重写产物。
用 LangGraph 表达时,条件路由应该尽量依赖结构化字段,避免让模型用一段自然语言决定下一步。例如审稿节点输出 blocked: true、blockingReasons: ['source_conflict'],路由函数再决定进入 humanReviewNode。
人工确认点也应该是图的一部分。适合人工确认的地方包括:
- 用户需求不完整。
- 官方资料之间存在冲突。
- 产物涉及法律、医疗、金融等高风险判断。
- 交付前需要确认项目规范或敏感表述。
这些确认点不应该只散落在前端弹窗里,而要写回 State。任务暂停、恢复和排障时,系统需要知道它停在这里的原因、等待谁确认、确认后应该回到哪一个节点。LangGraph 的 interrupt 机制可以用来表达这种暂停点;业务层还需要把确认原因、候选选项和用户选择持久化下来。
实现动态路由时,LangGraph 提供条件边(add_conditional_edges)和 Command 两种选择。条件边只负责决定下一步;如果节点想同时更新 State 并指定下一步,可以返回 Command(update=..., goto=...)。需要人工确认时,节点里调用 interrupt(value) 暂停执行,并把确认请求持久化到 checkpointer;用户确认后通过 Command(resume=...) 恢复,执行会从暂停点继续。要注意:同一个节点的静态边和动态路由不要混用,否则两条路径可能同时执行,导致行为难以预料。
多 Agent 系统里常见的组织方式有两种:Supervisor 模式(一个路由节点统一调度多个子节点)和 Swarm 模式(节点之间可直接交接)。路由集中时更容易追踪和扩展;节点直接交接时延迟更低,但路由逻辑会分散。LangGraph 的低层图模型都能表达这两种模式,选择取决于你的团队更在意可追踪性还是端到端延迟。
在 Agent 项目里,人工确认点不宜过多。适合阻塞主路径的,通常是会影响事实安全、项目风险或产物方向的判断。普通语气微调、标题备选、段落顺序建议,可以交给审稿 Agent 或后台评估,不必每次都打断用户。
6. 同步路径和后台路径
Agent 系统里有些动作必须影响本次返回,有些动作可以后台完成。
| 路径 | 任务 | 是否阻塞用户 |
|---|---|---|
| 同步路径 | 需求解析、资料召回、结构规划、产物生成、审稿、交付检查 | 阻塞 |
| 人工确认路径 | 资料冲突确认、交付前确认 | 阻塞到用户决策 |
| 后台路径 | 资料索引更新、审稿结论沉淀、指标聚合、trace 持久化 | 不阻塞 |
这一区分会影响等待时间和交付稳定性。产物返回前必须完成基本事实检查;新的审稿规则写入长期知识库、资料索引刷新、指标聚合,可以放到后台。后台任务失败时要记录 trace、进入重试队列或待处理列表,不应该让用户已经收到的产物回滚。
这一区分还会直接影响持久化策略。LangGraph 通过 checkpointer(例如 PostgresSaver)在 super-step 边界保存 State。对于需要跨多次请求、等待用户确认或可能长时间运行的任务,持久化 checkpointer 是必需的;否则进程重启后任务状态会丢失,interrupt 也无法恢复。后台任务即使失败,也能从最后一个 committed checkpoint 重新调度,而不会让已经交付的产物回滚。
更稳妥的做法,是在 State 里显式记录后台任务的派生状态,例如 backgroundJobs: [{ type, status, retryCount }]。这样主路径可以先完成交付,运维和产品侧仍然能看到哪些资料写回失败、哪些审稿结论没有进入知识库。
7. 可观测性从图节点开始
图编排天然适合记录 trace。每个节点都可以对应一个 span,记录输入摘要、输出摘要、工具调用、耗时、失败原因和降级状态。
Agent 任务至少要记录这些字段:
taskId、traceId、stage- 使用了哪些资料来源
- 每个 Agent 的输入摘要和输出版本
- 工具调用耗时与失败原因
- 审稿问题数量和严重级别
- 人工确认点的进入原因
- 最终是否交付、是否降级交付
有了这些字段,用户反馈“这个结果像拼接”时,排查可以从 trace 开始:资料召回是否太宽,结构是否没有合并重复观点,审稿是否没有阻塞结构问题,改写是否只做了表面润色。
可观测性也会反过来影响图设计。如果一个节点里同时做资料召回、结构规划和产物生成,trace 只能告诉我们“执行节点失败”。拆成独立节点以后,失败原因会更接近可行动的修复项:检索策略要收窄、结构聚合要增强,或者审稿门禁要提高阻塞级别。
trace 不仅可以离线排查,也可以实时流给用户。LangGraph 的 astream_events(version='v2') 会输出 on_chat_model_stream、on_tool_start/on_tool_end、on_chain_start/on_chain_end 等事件,前端可以根据这些事件展示进度、工具调用和当前节点,而不是让用户面对一个长时间空白等待最终产物。
8. 总结
在 Agent 项目里,LangGraph 的主要价值是把任务执行过程从一次模型调用,组织成可恢复、可审计、可插入人工判断的工作流。
学习这部分时,可以先抓住三件事:
- State 字段是否表达了任务需要长期保留的中间结果。
- 节点是否有清晰的输入输出和失败处理。
- 路由条件是否来自可检查字段,而不是一段无法复盘的自然语言判断。
这三件事成立后,多智能体系统才有继续扩展的基础。后续增加事实核查 Agent、风格改写 Agent、交付检查 Agent,主要是在图里新增节点和路由,而不是让一个大 Prompt 继续膨胀。