2024 年我做 Agent,起点是一个很小的 ReAct 循环:模型决定下一步是否调用工具,程序执行工具,再把结果追加回消息。进入 Vue 转 React 的迁移后,问题很快不再是“模型会不会调用工具”,而是另一组更难的问题:任务被中断后怎样恢复,失败后怎样收敛,哪些修改可以自动执行,哪些必须停下来让人确认,以及最终如何证明迁移没有丢失业务行为。
LangGraph 解决的是后一组问题的一部分。它把共享状态、执行节点、条件路由、持久化和中断点变成显式结构,让 Agent 不再只是一个持续增长的消息列表。但我在使用这类框架时更关心一个架构问题:到底应该不断把经验固化到编排框架里,还是应尽量让模型通过更多计算、搜索和反馈自己找到路径?
这个问题决定 LangGraph 最终会成为可靠的执行底座,还是逐渐膨胀成一套用代码复刻人类经验的专家系统。
《The Bitter Lesson》带来的反问
Rich Sutton 在 The Bitter Lesson 中总结了一个历史规律:长期看,能够持续利用更多计算的通用方法,往往会超过人类把领域知识手工编码进系统的方案。棋类中的大规模搜索和自我对弈学习、语音识别中的统计学习、视觉中的端到端表征学习,都出现过类似迁移。
这篇文章不应被简化成“不要写规则”或“模型会解决一切”。它更尖锐的部分在于:人很容易把自己当前理解到的领域结构写进系统,这在短期内通常有效,也更有掌控感;但这些结构会随着数据、模型和计算能力变化而快速变成约束。真正可扩展的是搜索与学习这种能利用新增计算、从反馈中改进的元方法,而不是某一代工程师对问题的固定解释。(P.S. 自动驾驶的演进也能论证这个事情)
放到 Agent 上,这个反问很具体。
当模型选错工具时,我们可以继续在 LangGraph 里补路由分支;当模型修改代码后漏掉列表刷新时,我们可以再加一个“提交后刷新”的节点;当检索结果不相关时,我们可以再加一条资产类型规则。短期看,这些补丁都会提高某一批 case 的成功率。但如果每个失败都被翻译成一个新的固定节点、条件分支和特例状态,图会逐步承担原本应该由模型能力、检索、测试和搜索承担的复杂性。模型升级后,旧分支未必仍然必要;业务变化后,它们又可能成为错误路径。
因此,讨论 LangGraph 除了思考“如何提升迁移的准确性”外还要问:这个决策的价值是否来自确定性控制,还是只是因为当前模型还不够强? 前者应留在编排框架,后者应优先保留给模型和反馈闭环。
先区分两类复杂性
我把 Agent 系统中的复杂性先分成两类。这个区分比“模型节点还是普通节点”更重要。
第一类是环境与责任带来的确定性约束。例如权限校验、幂等控制、事务边界、速率限制、审计、部署审批、密钥管理。这些事情即使模型能力继续提升,也不能交由概率性判断决定。模型可以提出“调用退款接口”的建议,却不能决定当前账号是否有退款权限;模型可以推断某个文件可能无引用,却不能绕过删除前的保护策略。这类逻辑的正确性来自系统不变量,应该由工具实现、策略引擎或图上的受控节点执行。
第二类是任务求解带来的开放性不确定性。例如从仓库里理解模块依赖,决定 Vue 插槽在 React 中应映射为 render prop 还是列配置,选择需要阅读哪些文件,解释一个失败测试,生成修复候选方案。这些问题的输入分布和解法都持续变化。将它们压缩为固定 if-else,短期可控,长期通常维护不动。更合理的方向是给模型提供读取、搜索、运行检查和回滚等能力,让它在受限空间内探索,并根据外部反馈修正。
LangGraph 的位置恰好在两者之间:它不应替模型做开放式求解,也不能只做消息转发;它应定义模型探索的边界、可用资源和可验证的出口。
编排框架应该做什么
基于上述区分,我认为图里应优先固化四类内容。
1. 状态所有权与生命周期
在最初的 ReAct 循环里,messages 同时承载对话、工具结果、任务进度、失败记录和最终结论。它既是模型上下文,又被迫充当任务数据库。长任务中,这会让“已经验证的事实”和“模型的一次猜测”混在一起。
迁移任务需要的状态并不复杂,但必须有明确所有权。例如 contract 由只读调研节点产生,checks 只能由检查节点追加,approval 只能由人工或审批服务变更。状态还需要记录证据来源、代码版本和失效条件:一个 reload 方法“已不被调用”的结论,应关联搜索范围和提交版本;共享资产契约变化后,依赖它的迁移单元应回到待复核,而不是继续显示完成。
这部分主要是想建立事实与假设的存储协议。模型可以提出候选结论,但不能把候选直接变成已验证的状态。
2. 有副作用操作的事务边界
读取代码、生成计划和解释日志可以允许模型多次尝试。修改文件、调用内部写接口、删除资产、发布构建则需要不同处理。框架应把这些动作包装为可校验、可审计、尽可能可重试的工具调用,并明确失败后的恢复语义。
LangGraph 的中断点适合放在高风险边界上:例如删除仍有潜在引用的共享组件,修改权限或支付接口,批量写入数据,触发发布。中断之前应沉淀计划、影响范围、调用方搜索结果和已执行检查;恢复之后才允许执行对应写操作。这里的关键不是“人一定比模型正确”,而是责任归属和事故成本需要系统提供一个明确的决策点。
同时要避免把审批当作安全的替代品。工具侧仍需做鉴权、参数校验、幂等键、并发控制和审计记录。图只决定何时进入这个边界,不能替代边界本身。
3. 停止、预算与降级策略
模型的探索不是无限的。早期 Agent 曾出现空结果后连续换参数重试,单轮看每一次都合理,组合起来却是循环。把“最多十轮”写在循环里能止损,但不能解释何时应重试、何时应向用户追问、何时应转人工。
这类约束应由编排层统一承担:最大工具调用数、token 与时间预算、同类错误的重试上限、危险工具的调用次数、外部服务不可用时的降级路径。这些是资源和风险控制,不是模型应该学习的业务知识。模型可以提出下一步候选,路由器依据结构化的错误类别和剩余预算决定继续、改变策略、暂停还是失败退出。
4. 验收门与证据链
“模型已完成”不是完成条件。Vue 转 React 的案例里,类型检查通过只能说明代码可编译,不能说明权限拦截、请求参数、埋点和弹窗关闭后的列表刷新仍在。完成边应该由可观察证据驱动:类型检查、构建、定向测试、浏览器流程验证,以及明确列出的人工未验证项。
这里适合固化的是验收机制,而不是每个页面的业务步骤。框架负责要求“涉及写操作必须有检查证据”;模型通过读取当前代码、历史状态和测试反馈,决定具体应该覆盖哪些行为、如何修复失败。前者稳定,后者随着项目演化。
模型应该做更多什么
如果编排框架承担了上述确定性工作,模型就不应只被限制为“填参数的函数调用器”。在开放问题上,应让模型做更多,并让它获得比长提示词更有效的反馈。
用搜索替代预设路径
以组件迁移为例,固定流程可以要求“先分析,再修改,再检查”,但不能预先枚举所有正确的分析路径。LegacyTable 的风险可能藏在 props、动态插槽、ref 方法、调用方、全局样式或接口分页参数中。与其维护一张越来越长的专家规则树,不如让模型基于任务目标搜索仓库、比较调用点、提出假设,再通过类型系统、搜索结果和运行验证排除错误路径。
这对应《The Bitter Lesson》所说的 search。这里的搜索不只是树搜索算法,也包括在受控工具空间中选择读取什么、检查什么、尝试哪些低风险修复。前提是每一步有预算、可观测性和可回滚性,而不是让模型无限调用工具。
用反馈学习替代规则堆叠
2024 年写工具调用时,我反复调整过 schema、工具描述与错误返回。它们依然必要,因为模型需要理解接口语义;但如果每次失败都只增加一条提示词,系统很快会积累大量互相干扰的自然语言补丁。
更有价值的做法是把失败变成可分析的数据:任务是什么,模型选择了什么工具,参数为何不合法,工具返回了什么结构化错误,后续修复是否成功,最终检查是否通过。离线评测可以找出工具描述、检索策略、模型版本或图路由的薄弱处;在线日志可以识别新出现的失败分布。必要时再通过更好的模型、针对性的训练或策略优化改进,而不是首先把失败固化成静态分支。
这不是说每个团队都应训练模型。大多数场景先做到可复放的任务集、结构化轨迹和回归评测就足够。关键是把“经验”沉淀为能被评估和修正的样本,而不是只沉淀为某位工程师记得的一条规则。
用当前证据覆盖历史经验
我在项目记忆和 RAG 实践中得到的结论是:检索到的经验只能缩小搜索范围,不能替代当前判断。一个历史条目说“展示插槽优先改为列配置”,在另一个调用方依赖 reload ref 方法时就不成立。经验库应该提供候选方案、适用条件和反例;模型需要回到当前代码验证前提,框架再要求它把验证证据写入状态。
这也是避免专家系统化的关键。不要让图路由“看到表格就走列配置节点”,而要让模型在当前契约下选择方案;不要让旧经验直接授权修改,而要把它转化为待验证的检查项。
一个迁移图的责任划分
用 Vue 转 React 的迁移单元举例,我会把图设计成如下分工,而不是把每一步细节都写死:
其中,图固定的是阶段、读写权限、状态字段、可走的风险边界、预算和完成条件;模型负责调研时读哪些文件、计划中采用何种组件映射、失败后先修哪处、是否还需要检索历史经验。这样设计保留了模型随着能力与计算提升继续变强的空间,也使模型不能跳过工程系统的硬边界。
状态可以收敛为一组机器可读的事实,而不是完整对话:
type MigrationState = {
unit: string
contract: ComponentContract | null
plan: MigrationPlan | null
assumptions: Array<{ content: string; status: "pending" | "verified" | "rejected" }>
changedFiles: string[]
checks: CheckResult[]
risks: string[]
budget: { toolCallsRemaining: number; deadline: string }
approval: "not_required" | "pending" | "approved" | "rejected"
}比字段数量更重要的是升级规则:模型创建的是 pending 假设;搜索、测试或人工确认才能将其转为 verified;上游契约和依赖版本变化后,相关结论应失效或待复核。checkpoint 保存的是执行恢复所需的状态,不是永久正确的知识库;长期经验也不应绕过当前任务的验证。
决定一段逻辑放在哪里
“让框架薄而硬”不是让架构师少做设计,而是要求每一段控制逻辑有明确归属。实际设计时,我会用四个问题判断一段逻辑该放在工具层、图编排层、模型上下文,还是评测与数据闭环中。
| 判断问题 | 更适合的位置 | 原因 |
|---|---|---|
| 做错后是否会越权、丢数据、重复扣款或违反合规要求? | 工具层、策略层 | 必须在模型输出之外独立成立,对所有调用方一致生效。 |
| 是否涉及任务阶段、暂停恢复、资源配额或多系统调用顺序? | 图编排层 | 这些是执行生命周期问题,需要可观察、可恢复的确定性控制流。 |
| 输入是否开放、语义是否依赖当前代码或数据、可行方案是否会持续变化? | 模型节点 | 固定规则很快覆盖不全,应允许模型借助检索与工具在当前证据上求解。 |
| 是否频繁出现但仍不能确定为系统不变量? | 评测集、经验库、训练或提示策略 | 先保留为可度量的样本,确认跨场景稳定后再决定是否固化。 |
例如“生产库写入必须审批”显然是工具和流程边界;“任务超过预算时返回未完成状态”属于编排层;“这个 Vue 插槽应改为 render prop 还是列配置”属于模型要解决的问题;“模型经常忽略 reload 的调用方”应先进入回归用例,检查是搜索不足、上下文不足、工具描述不清还是模型能力不足。只有最后确认它表达的是稳定契约,例如“删除公开方法前必须搜索调用方”,才把它提升为流程门禁。
这个顺序很重要。直接把失败 case 写成图节点,最快但也最容易过拟合;先把它变成可复放样本,团队才能知道新增节点真正改善了什么,以及模型或工具升级后是否可以删除它。
反例:并非所有领域规则都应交给模型
《The Bitter Lesson》容易被误读为领域知识应该全部退出系统。对生产 Agent 来说,这同样危险。长期可扩展的方法并不意味着可以取消领域模型,而是要区分领域知识的两种形态。
第一种是描述世界的知识。例如某个页面为什么在弹窗提交后要刷新列表、某个接口字段的业务含义、某类迁移中插槽如何转换。这类知识有大量例外,且会随代码与业务演进。把它写成过细路由,通常会形成难维护的专家系统。模型、检索和测试应承担理解与适配的主要工作。
第二种是定义系统允许什么的知识。例如退款必须满足订单状态和发起人权限,生产数据删除必须经过审批,迁移期间旧资产只允许在无引用后清理。这些不是帮助模型“理解得更好”的提示,而是系统对外承诺。它们必须在模型失误、提示注入、工具被其他程序直接调用时仍然成立,因而应该被编码为策略、约束和测试。
两者表面上都是规则,架构意义却不同。前者主要服务求解效率,应优先保留可替换性;后者定义安全与一致性,必须追求确定性。错误地把前者当后者,会使图不断膨胀;错误地把后者当前者,则会把安全寄托在模型的服从性上。
还有一个常见反例是高价值、低频且缺少反馈的操作。模型可能很擅长在大量普通迁移任务中选择工具,但一次数据库修复或一次生产发布造成的损失足够高,即使成功率很高也不能完全自动化。这里的自动化级别应由影响范围、可逆性、检测延迟和人工复核成本共同决定,而不是由模型在离线 benchmark 上的平均分决定。
不要把 DAG 当成真实世界
LangGraph 很容易让人产生一种错觉:只要把节点和边画清楚,任务就是可控的。实际上,图只描述 Agent 的控制面,并不能完整描述真实世界的数据面。
仍以迁移为例,图可以保证“实施节点之后必须经过检查节点”,却不能保证检查覆盖了真实风险。类型检查只观察类型契约;构建只观察打包路径;浏览器流程测试只覆盖被设计的场景;人工审批也只能审阅当时展示出来的信息。若 LegacyTable 的旧实现中有一个埋在全局事件总线里的刷新副作用,图上的边没有丢,最终行为仍然可能丢失。
因此图的每个完成状态都应回答两个问题:它依据什么证据成立,这份证据覆盖什么、没有覆盖什么。下面两种状态语义完全不同:
checks:
- name: typecheck
status: passed
evidence: pnpm typecheck
covers: TypeScript 类型与模块解析
does_not_cover: 运行时请求参数、权限与用户交互checks:
- name: order-list-refresh
status: verified
evidence: 测试环境中提交成功后观察到列表重新请求,参数与旧版本一致
covers: OrderList 的一个刷新路径
does_not_cover: 其他调用方和失败重试路径第一种是工程检查,第二种是业务行为证据。把二者混成一个 passed: true,后续的模型、人和系统都会高估完成度。LangGraph 可以强制关键节点写入证据字段,但“什么证据足够”仍要由领域架构和测试策略决定。
失败不是同一种失败
只有把失败分类,图的路由和模型的改进才有依据。将所有异常统一送回“再试一次”节点,通常只会增加成本。结合 2024 年的工具调用和迁移实践,我会至少区分五类:
- 输入或契约错误:参数格式不合法、枚举值错误、缺少必要上下文。应由工具返回可操作的结构化错误,模型可以补充信息或修正参数。
- 瞬态环境错误:网络超时、限流、依赖服务短暂不可用。编排层应有指数退避、重试上限和降级策略;模型不应靠改写参数解决超时。
- 求解错误:模型漏读调用方、误判代码语义、选择了不合适的迁移方案。应补充当前证据、切换检索策略或进入定位节点,而不是盲目重放原调用。
- 验证失败:代码已修改,但类型检查、测试或运行验证发现差异。模型可根据失败输出修复;图要保留失败发生时的计划、diff 与检查证据,避免下一轮重新猜测。
- 策略拒绝与证据不足:权限不足、风险操作未审批、无法证明行为正确、预算耗尽。这不是技术失败,不能靠重试解决;系统应停在可交接的未完成状态。
这种分类同时避免两个极端:一个是把所有失败都交给模型,使它围绕系统异常产生无意义推理;另一个是把所有失败都写成静态分支,使图成为异常处理的百科全书。图处理类别、预算与状态转移,模型处理需要理解语义的修复和下一步探索。
评估框架是否真的创造价值
引入 LangGraph 后,最容易出现的自我安慰是“轨迹更清晰了,所以系统更可靠”。轨迹清晰是必要条件,但不等于效果提升。是否值得保留这层框架,至少应比较同一批任务在简单循环和图编排下的结果。
评测集不能只有容易成功的 happy path。以迁移任务为例,应包含:公开 ref 方法仍有调用方、接口字段名称相同但语义变化、旧代码含隐式副作用、构建通过但流程失败、工具超时、审批拒绝、checkpoint 恢复后上游契约已变化等 case。每个任务都要给出允许修改范围、不可违反的约束和可观测验收条件。
指标也不能只看最终成功率:
- 任务质量:通过验收的比例、行为回归率、人工复查发现的问题数。
- 安全与边界:越权调用数、未审批高风险操作数、预算超限次数、错误重试次数。
- 恢复能力:中断后恢复成功率、恢复后重复工作量、状态与代码不一致的比例。
- 效率:平均工具调用数、输入输出 token、首个可验证结果的时间、人工介入时长。
- 可维护性:新增一种失败模式时需要修改的节点数、规则数量增长、模型或工具升级后可删除的分支数。
最后一项往往被忽略。一个框架即使短期提升了通过率,若每多一个 case 就要加三条边和两个状态字段,维护曲线会很快失控。反过来,若模型升级后同一张图能以更少的节点完成更多任务,才符合“框架为能力增长提供底座”的预期。
评测结果还要能够归因。若成功率下降,不能立刻判断是 LangGraph 不适合:可能是模型版本变化、工具接口变更、检索索引过期、测试环境不稳定,或任务集加入了更难样本。节点级轨迹、输入版本、工具返回和验收结果需要关联,才能区分是模型求解问题、编排问题还是环境问题。
演进策略:先增加能力,再增加路由
面对一个新的失败 case,我倾向按下面的顺序处理。
第一步,复放轨迹并核对事实。失败是因为模型没有拿到必要信息,还是拿到了却误判;是工具表达不清,还是工具本身不具备所需能力;是验证没有覆盖,还是状态与代码已经分叉。没有这一步,后续优化多半是在猜。
第二步,补充能力或反馈。可能是添加只读搜索工具、让工具错误返回可修复信息、补一个定向测试、改进检索过滤,或把运行结果结构化返回给模型。这些改动扩大了通用求解空间,通常能覆盖同类但不同表述的任务。
第三步,加入评测并观察稳定性。一次修复成功不足以成为规则。应在独立 case 上观察它是否减少错误、是否带来新的误用,以及成本是否可接受。
最后才考虑新增确定性路由或策略门禁。满足以下条件时,固化才更合理:它表达的是权限、数据一致性、资源上限、审批责任等系统不变量;或者该模式跨任务稳定出现,自动检查能够明确判断,且交给模型没有额外收益。例如“生产写入必须有审批令牌”“删除公开 API 前必须没有引用”“同一幂等键只能执行一次”都适合硬化。
这条顺序不是反对流程设计,而是避免团队把“当前模型犯了一个错”直接等同于“系统应该永久新增一条规则”。前者可能因模型、提示、检索或工具改进而消失,后者会永久增加系统的认知负担。
LangGraph 的优点与误区
在这个定位下,LangGraph 有几个明确优势:
- 把控制流从提示词中拿出来。 阶段切换、中断恢复、预算限制和完成条件不再依赖模型是否“记得遵守”。
- 让状态成为可审计对象。 任务能够跨会话交接,且可以追溯某个结论的来源、检查与失效条件。
- 容纳确定性与概率性协作。 静态检查、权限校验和审批可用普通节点实现;开放式分析、计划与修复保留给模型节点。
- 为评测提供稳定切面。 可以分别度量调研节点是否找全依赖、计划是否触及风险、实施是否通过检查,而非只看一段最终自然语言回答。
但有三类常见误区。
第一,把节点拆得越细就越可靠。节点过细会把暂时的人类理解编码成不可维护的路由图,模型升级或业务变化后,图反而限制求解空间。一个节点应对应稳定的责任边界或可验证的阶段,不应只是把提示词里的每一句要求翻译成方块。
第二,把 checkpoint 当成记忆系统。checkpoint 解决恢复与回放,不解决知识有效性、经验检索和上下文预算。将全量状态反复注入模型,只会重演历史膨胀和缓存失效的问题。
第三,把人工中断当成安全机制。审批只能决定是否继续,不会自动修复越权、重复写入和数据泄露。安全约束必须落在工具与基础设施层,且对模型、人工调用和自动化任务一致生效。
我的判断
LangGraph 的价值不在于把 Agent 变成一张更复杂的流程图,而在于为模型的搜索和试错提供一个可恢复、可限制、可验证的执行环境。它解决的是执行系统如何承接概率性智能,而不是用流程图代替智能本身。
从《The Bitter Lesson》的角度看,长期不应把所有智能都固化在编排层。模型、搜索、检索、测试反馈与计算能力会持续演进;今天为了弥补模型缺陷写下的路径,很可能成为明天的限制。编排框架应当保持“薄而硬”:薄,是少承载具体领域解法,避免变成专家规则库;硬,是对状态、权限、副作用、预算和证据保持确定性约束。
这也意味着架构师的工作没有减少,只是重心发生变化。过去的重点是把已知流程描述得足够完整;在 Agent 系统中,更重要的是设计可探索的工具空间、可靠的状态协议、可诊断的反馈、可复放的评测,以及模型不能绕过的安全边界。前者追求“把正确路径写出来”,后者追求“让系统能在约束内持续找到并验证正确路径”。
因此我的落地原则是:让模型在可验证的任务空间里尽可能多地探索,让框架只对探索不能越过的边界负责。 当一个新节点被提出时,先问它是在表达系统不变量,还是在复刻当前人的解题经验。前者值得固化;后者应优先变成工具、反馈、评测样本或候选经验,由模型在当前证据下重新判断。

