跳到主要内容

内容阅读

从 create_agent 到 deepagents:LangGraph 高层抽象省了什么、藏了什么

Agent阿新聊ai

TL;DR: v1 之后 LangGraph 的分层是:底层引擎(StateGraph)→ LangChain 1.0 的 create_agent(预置 agent 循环 + middleware 扩展)→ deepagents(虚拟文件系统、子 agent、任务规划的完整 harness)。高层省掉的是重复劳动,藏掉的是控制点。判断标准就一条:你要改的行为,middleware 钩子够不够用;不够,老老实实下沉到图。

先看分层地图

┌─────────────────────────────────────────────┐
│  deepagents        agent harness,开箱即用    │
│  (虚拟文件系统 / 子 agent / write_todos)     │
├─────────────────────────────────────────────┤
│  create_agent + middleware   预置 agent 循环  │
│  (LangChain 1.0,可插拔改行为)               │
├─────────────────────────────────────────────┤
│  StateGraph / functional API  编排引擎本体     │
│  (LangGraph,一切的地基)                     │
└─────────────────────────────────────────────┘

这个分层是 2025 年 10 月 1.0 版本刚调整的,中文互联网大量内容还没跟上:langgraph.prebuilt.create_react_agent 已废弃,替代者 create_agent 不在 LangGraph 包里,在 langchain.agents 里。它和旧版的区别不是改名,是架构:agent 循环的行为全部通过 middleware 插拔,不再靠往函数里塞参数。照旧教程写的第一行 import 就会撞上废弃警告,第 18 篇专门整理了 0.x 到 1.x 的迁移清单,这里只说分层本身。

先回答一个容易困惑的问题:为什么一个「LangGraph 系列」要用一篇的篇幅讲 langchain.agents 里的东西?因为分层倒过来了。1.0 之前,LangGraph 是「带图 API 的全家桶」,create_react_agent 是它的一个便捷入口;1.0 之后,LangGraph 收缩成底下那台引擎,上面可以套不同的壳,LangChain 的 create_agent 是其中一个壳,deepagents 是再往上的一个。第 01 篇讲的「LangGraph 是低层编排框架」就是这意思。所以「用不用 LangGraph」在今天很少是全有或全无的选择:你可能底下的 checkpoint 用它、中间的 agent 循环用 create_agent、只有一两个特殊流程下沉到 StateGraph。三层各自独立发版、独立演进,搞清楚每一层给你什么、拿走什么,是这篇的任务。

判断自己在哪一层有个简单办法,看 import。from langgraph.graph import StateGraph 是在底层;from langchain.agents import create_agent 是在中间层;from deepagents import create_deep_agent 是在顶层。三层不互斥,但行为预期完全不同:越往下,代码越多、控制越细、坑越具体;越往上,代码越少、默认越多、坑越整体。坑的形态变化值得多说一句:底层的坑是具体的,比如某个合并规则写错导致丢数据,定位到行;高层的坑是整体的,比如「agent 为什么自己决定不调这个工具」,定位到的是一整套默认策略。选择抽象层级,某种程度上是在选你愿意面对哪一种坑。

这篇从中间层开始讲,因为它是大多数业务系统该起步的位置:deepagents 面向的 open-ended 场景在业务系统里是少数,而所有底层能力你随时可以取用。

起步方向有两种策略,选哪种取决于项目性质。做原型、探索可能性,从上往下:先 deepagents 或 create_agent 快速跑通,感受到限制再下沉,抽象税付在探索期,沉没成本小。做核心系统、长生命周期,从下往上:先把状态结构和控制流用图显式建出来,再把标准循环部分换成 create_agent,这样每个抽象都是你主动引入、知道它藏了什么的,而不是默认继承、出了事才翻源码的。两种方向都合法,混乱的是方向摇摆:原型期用顶层快速验证,上线时原样带走,抽象税就跟着进了生产。

create_agent 的循环长什么样,middleware 改哪里

先看代码:

from langchain.agents import create_agent
from langchain.agents.middleware import (
    SummarizationMiddleware, HumanInTheLoopMiddleware,
)

agent = create_agent(
    model="openai:gpt-4o",
    tools=[search, send_email],
    system_prompt="你是客服助手,高风险操作先请示",
    middleware=[
        SummarizationMiddleware(max_tokens=4000),      # 上下文自动摘要
        HumanInTheLoopMiddleware(interrupt_on=["send_email"]),  # 工具级人审
    ],
)

要看懂 middleware 改的是什么,得先知道不加 middleware 时循环长什么样。create_agent 编译出来是一张图,里面跑的是标准的 agent 循环:模型收到系统提示、对话历史和工具清单,输出要么是回答、要么是工具调用;是工具调用就执行,把结果追加进消息流,再问一轮模型;直到模型不再要工具,循环结束。它的状态主体就是这份消息流,第 01 篇那个 40 行手写循环,在 create_agent 里被产品化成了这个引擎。两个衍生事实值得知道:循环有默认步数上限(和第 02 篇讲的 recursion_limit 是同一套机制),模型绕圈到上限会报错而不是永远跑下去;消息流是它唯一的工作记忆,第 04 篇讲的上下文成本问题在这里原样成立。每次循环开始前,middleware 有机会改「模型将看到什么」,每次工具执行前后,middleware 有机会拦截,每次循环结束,middleware 有机会处理消息堆积。它改的就是这几个时机,不改循环本身的形状。

middleware 提供的钩子覆盖了 agent 循环的各环节。逐个说它们在生产里对应什么需求:

  • 改 system prompt:按状态动态生成。客服 agent 的系统提示词要带当前用户的会员等级、历史工单摘要,这些每次循环都可能变,钩子在每次调模型前生成,而不是进程启动时定死。
  • 过滤工具:按用户权限裁剪可见工具。普通客服看不到退款执行工具,主管账号才能看到,同一个 agent 跑出不同的能力边界(权限体系的完整讨论在第 16 篇)。工具清单是模型决策的依据,看不见的工具等于不存在,这是比「调用了再拦截」更上游也更安全的做法。
  • 消息管理:摘要、裁剪。长对话把上下文吃爆是 agent 第一大成本来源(第 04 篇的账单),SummarizationMiddleware 这类现成件把「什么时候摘、摘多少」的脏活包掉。
  • 工具执行前后拦截:统一加日志、脱敏、参数校验,不用在每个工具函数里写一遍。审计要求的「谁在什么时候调了什么、参数是什么」就落在这里。
  • 输出校验:结构化输出不合法时拦下来,走重试而不是把坏数据漏给下游。

自己用 StateGraph 实现同样的效果,每个钩子都是一个节点加一堆条件边,几百行起步。对于「标准 ReAct 循环 + 少量定制」的应用,这一层的代码量优势是数量级的。更重要的是行为对齐:现成 middleware 是官方维护的,边界情况(空消息、超长工具结果、并发调用)别人已经踩过;自己写的钩子,每个边界情况都是自己的 bug 储备。

动手写自定义 middleware 之前,做两件事。第一,翻一遍现成清单(文末的 built-in 文档),摘要、人审、工具裁剪、输出校验这些高频需求大多已有现成件,自己写的第一个用途应该是「现成件真的没有」。第二,读一遍你要扩展的那个现成件的源码,它们普遍很短,读完你就知道钩子的真实输入输出和调用时机,比文档更准确。这两个习惯能把「自己写钩子」的风险压到很低:你写的不再是黑盒扩展,而是在一份你读过的短代码上加一个函数。

同一个需求,两层写法

用一个最小需求看两层的差别:客服 agent,能查订单、能发退款,发退款之前要人工审批。

中间层写法就是上面那段代码再加一个工具和一条人审配置:tools=[search_orders, refund]interrupt_on=["refund"],完成。你的代码里没有循环、没有暂停恢复的接线,行为全部由预置件组合出来。

底层写法是画一张小图:handle 节点跑模型决策,条件边判断模型要不要退款,要的话进 approve 节点手写 interrupt()(第 06 篇的最小示例),批准后进 refund 节点执行。你多了二三十行代码,换来的是:审批通过与否作为结构化字段留在状态里、审批环节可以插进任意位置、拒批后可以走自定义的安抚话术分支而不是简单终止。把两个版本各跑几十轮,还能看到行为差异:中间层的模型有时会绕开「先查再退」的理想顺序直接要退款(被审批拦下,安全但浪费一轮),底层版本可以把「先查后退」写进条件边结构,行为更贴流程。结构能约束的,就别指望提示词约束。

两个版本都是「对的」。需求停在「一个能审批的退款 agent」,中间层更快;这个需求长在「一条有审批链、金额分级、超时升级的退款流程上」,底层版本就是这条流程上的一环。判断的时机不是项目开始时,是需求第一次膨胀时,这正是前面三个控制点判据的用法。

HumanInTheLoopMiddleware:换了个地方踩坑

HumanInTheLoopMiddleware 值得单独说:它把人审从「你在节点里手写 interrupt」变成一行配置。interrupt_on=["send_email"] 的意思是:send_email 这个工具每次要执行前,自动暂停等人点头。底下的机制就是第 06 篇讲的 interrupt 原语,所以那篇里的坑一样存在,只是换了个地方踩:

  • 恢复等于重跑的语义不变:人批完从工具调用前继续,工具调用之前的副作用幂等照样要你自己保证。比如审批通过前的「预占库存」动作,恢复重跑时不能占两次。
  • payload 的匹配问题变成配置的匹配问题:哪个工具的审批、批了还是拒了、拒绝后走哪条路,配置时要设计清楚,不能全靠默认值兜底。
  • 它不管超时:审批人三天不批,流程就挂三天。超时兜底(自动驳回、自动升级、通知催办)是业务逻辑,middleware 给不了,第 06 篇讲过这是框架不管的问题。
  • 它也不解决「谁来审批」的组织问题:配置里写的是工具名,不是审批人。审批人路由、代理审批、逐级审批,都在你的业务层。

一句话:middleware 把人审的「接线」省了,人审的「制度」一点没省。能用它的地方放心用,但第 06 篇那六个坑还是必读,炸的方式一模一样。

middleware 组合的实际坑

单个 middleware 好理解,生产里真正花时间的是组合。三个经验性的坑,都来自「多个钩子改同一段东西」:

一是顺序。middleware 列表是有序的,钩子按顺序串成链:摘要和裁剪谁先执行,决定模型看到的上下文形态;权限过滤发生在工具清单生成之后还是之前,决定被过滤的工具是「看不见」还是「调了会报错」。举一个具体的错位:权限过滤排在摘要之后,摘要件可能已经把包含权限语义的消息摘掉了,过滤逻辑看到的上下文已经不完整,做出的裁剪自然不可信。顺序错了不一定报错,往往只是行为和预期差一点,而「差一点」在 agent 里会被模型放大成完全不同的行为路径。

二是相互作用。两个 middleware 都改消息列表,一个摘要、一个裁剪,各自单测都对,合起来可能出现摘要刚写完就被裁掉的浪费,或者两者对「消息边界」的理解不一致导致对话历史出现裂缝。组合之后要在集成测试里过一遍完整对话,只测单件不够。

三是密度。为一个 create_agent 写的 middleware 有七八个、彼此的执行顺序开始变得重要、新同事看不懂为什么 prompt 会被改三次,那就是在用插件系统模拟一个图。middleware 的合理密度是「每个钩子一件事」,超过之后,显式的图比隐式的钩子链更好调试,也更好向团队解释。

调试组合问题的基本手段是看中间产物:把 agent 的消息状态打出来,看每一轮模型实际收到了什么;用 updates 流式模式看它内部的节点推进(第 10 篇)。middleware 的文档会讲执行顺序,但「顺序加相互作用」只有你自己的测试能证明,这几十行测试代码是这一层抽象里性价比最高的投入。

deepagents:藏起来的完整 harness

再往上是 deepagents(create_deep_agent 入口),它默认给你一套 agent harness 的完整件:

  • 虚拟文件系统:agent 在隔离的文件空间里读写,可以配权限规则,不碰真实文件
  • 子 agent:一个 task 工具,把子任务派给独立上下文的无状态子 agent,主上下文不被污染
  • 任务规划:write_todos 工具,让 agent 自己维护任务清单
  • 审批:interrupt_on 指定哪些工具执行前要人审
  • 记忆:跨会话的文件式记忆(AGENTS.md 风格)

harness 这个词值得展开一下。它指「agent 的完整工作环境」:工具、工作空间、记忆、规划方式全部预置好,agent 一启动就在一个五脏俱全的车间里干活。编码类产品(比如 Claude Code 这类 CLI agent)就是这个形态的产品化版本。deepagents 把这个形态做成了 Python 里可以直接拿到的默认值:检索、编码、多步调研这类 open-ended 任务,启动成本接近零。判断需求是不是 open-ended 有个朴素标准:你能不能提前画出步骤图。能画出来的是流程,流程归 StateGraph;画不出来、要 agent 自己边干边定下一步的,才是 harness 的主场。

每一件默认物都有适用条件,逐个过:

虚拟文件系统适合「生成物先隔离再落盘」的场景(写代码、改文档)。它的价值不只是安全:产物先落在隔离空间里,人可以审、可以 diff、可以一键拒绝,这本身就是一种批量的人审形态,和第 06 篇的单点审批互补。不适合的是本来就要直连外部 API 的流程,套一层文件系统纯属绕路。

子 agent 的独立上下文解决了「主上下文被中间结果污染」的问题:几十次检索的原文全堆在主对话里,模型开始忘事,token 账单也失控(第 04 篇算过这笔账)。派给子 agent,它自己读几百页,只把结论一段话交回来。代价是子任务之间、子任务与主任务之间不共享细节,需要主 agent 显式传递;需要共享上下文的强协作任务,拆子 agent 反而制造信息断层。还有一点:子 agent 是无状态的,跨子任务要记的东西,得由主 agent 或外部存储负责。

write_todos 的任务清单对多步骤任务有可观的收益,模型不再中途忘记目标,人对进度也有了可看的抓手。对两三步就完成的任务纯属仪式,还多占上下文。

记忆是文件式的,跨会话存活。方便,但要想清楚三件事:存了什么(隐私边界,第 16 篇的原则同样适用)、存在哪(跟着部署走还是跟着用户走)、怎么清(用户要求删除时要能删干净)。记忆是唯一「今天写的字,半年后还会影响行为」的部件,出问题也最晚被发现。

它解决的问题是「通用 agent 的默认形态」。代价是你接受了它对 agent 行为的全部默认设计。改它的内部循环比改 create_agent 更难,因为它藏的不只是钩子,是整个执行策略。评估它有一个便宜的办法:花一个下午,拿你业务里最典型的 open-ended 任务让它在隔离环境跑一遍,重点看四件事:任务拆解是否符合你的审批要求、文件产物的格式是否可控、子 agent 的边界是否清晰、记忆里留下了什么。四个答案都满意,它就是你的加速器;有任何一条不满意,这条恰好就是你要下沉的部分。

一条节奏建议:团队第一次做 agent,先在 create_agent 层待一段时间再考虑 harness。原因不是能力,是调试锚点:你得先见过裸循环怎么决策、怎么绕圈、怎么把上下文吃爆,才能判断 harness 的某个默认行为是「设计如此」还是「对你的场景是坑」。直接从 deepagents 起步的团队,遇到第一个奇怪行为时会同时怀疑所有默认值;有循环经验的人,五分钟能把问题归到具体某一层。

藏掉了什么:三个丢失的控制点

用高层抽象,下面这些东西从「你写的」变成「它决定的」。它们不是理论缺陷,是会在 issue 列表里反复出现的故障类型,每一条给一个会真的咬人的场景。识别它们的用处在于:当你发现团队在用奇怪的手段绕某个限制(往消息里塞 JSON、用计数器模拟并行、靠翻日志找进度)时,能立刻认出「这是控制点丢失的补偿行为」,而不是把它当成代码风格问题放过去。

  1. 状态结构。 create_agent 的 state 是固定的消息流加少量元数据。你要结构化业务字段(订单号、审批链、已核销金额)挂在状态里跟着流程走,middleware 钩子够不着就得绕:塞进消息里让模型抄写,模型会抄错;放进程变量里,进程一重启全丢,checkpoint 里根本没有它。真实事故形态是「对话进行到第十轮,agent 忘了用户改过一次地址」,因为那个地址只存在于消息文本里,没有结构化的事实来源。
  2. 控制流。 图的分支、并行、子图编排,在 create_agent 里没有位置,它的控制流是写死的 agent 循环:调工具、看结果、再决策。需要「先并行调研三条线再汇合对比」的结构,它表达不了,硬做就变成让模型在一条上下文里串行地假装并行,慢且贵(并行原语见第 08 篇,编排结构见第 09 篇)。
  3. 恢复粒度。 整个 agent 是图里一个节点的产物,checkpoint 的粒度由它内部决定。业务上你想要「每个业务步骤边界存一次档,出问题从那一步继续」,它给不了;恢复语义整体由 harness 决定,你没法在特定位置插入存档点(checkpoint 语义见第 03 篇)。发现这个丢失的方式往往很疼:一次运行跑了几十分钟挂掉,恢复后从半个业务流程重新开始,日志里找不到「停在哪个业务步骤」。这条丢失还和成本联动:恢复重跑的部分不是免费的,模型调用重放一遍,token 账单跟着重放一遍。

这三个点就是「什么时候下沉」的判据。命中任意一个,直接用 StateGraph 写;都可以接受,高层抽象随便用。判据是行为性的,不是审美性的:不是「我喜欢图」或者「插件不够优雅」,是上面三个具体的需求有没有真的出现。

下沉不是推倒重来

听到「下沉」,很多人的想象是推倒重写。实际不是,因为中间层本身编译出来的就是一张 LangGraph 图,底层的所有能力对它照常生效。这意味着两条实用的混合路径。

第一,create_agent 当节点用。你可以在 StateGraph 里画业务的骨架(接收、预处理、执行、审核、落库),其中「执行」这一步是一整个 create_agent 的产物,它作为子图嵌进去,checkpoint、streaming、interrupt 这些底层能力对它照常生效。业务的分支与并行画在骨架上,agent 循环关在节点里,各归各位。

第二,逐钩子替换。从全 middleware 起步的项目,哪个钩子不够用了,就把那一个行为下沉成显式节点,其余不动。下沉是按行为逐个进行的,不存在「一次迁移窗口」。反方向也成立:底层图里写得烦的标准循环,可以整段换成 create_agent 节点。分层不是选边站,是搬运自由。

嵌入时有几样东西是天然共享的,不用重复配置:checkpointer 挂在你编译的图上,嵌进去的 agent 循环自动享受同一套存档恢复;thread 语义不变,前后端按 thread_id 交互的接口不受影响;流式事件照常产生,只是子图部分要记得第 10 篇讲的透传开关和命名空间过滤。换句话说,下沉改变的是「你控制多少」,不改变「平台能力有哪些」,这也是敢按需下沉的底气所在。

判断该不该搬的信号也很具体:你在用字符串拼 JSON 往消息里塞结构化数据(状态结构该下沉了);你在 agent 循环里用计数器模拟并行(控制流该下沉了);你在日志里翻半天找不到「这笔单子停在哪个业务步骤」(恢复粒度该下沉了)。

高层抽象怎么保持可见性

高层抽象的常见抱怨是「不知道它在干什么」。这个抱怨一半是抽象的固有代价,一半是可以用工程手段消掉的。三个手段:

第一,把内部推进接进你的观测体系。create_agent 内部就是节点,updates 模式的流式事件、LangSmith 之类的 tracing(第 14 篇)都能看到它每一轮做了什么。上线前把「模型收到什么、调了什么工具、返回什么」这三件事的日志通路打通,之后一半的「玄学行为」都有据可查。

第二,给默认行为立规矩。deepagents 的文件产物、write_todos 的清单、记忆文件的内容,都做成可检查的对象:产物有格式约定,清单定期导出,记忆有审查入口。默认值不等于不可见,不可见是放弃检查的结果。

第三,测试策略跟分层走。工具函数和 middleware 单测(输入输出确定性高),agent 循环做轨迹级评测(第 15 篇的方法),下沉的图做集成测试。每一层的「对」标准不同,混在一起测,哪个层坏了都查不清。另外把第 13 篇讲的版本纪律按层落实:三层的发版节奏不同,高层迭代快、底层稳,锁版本要按包锁,升级要按层灰度,一次全升等于把三层的风险叠在一起引爆。

一张表选层

你写的 它决定的 适合
StateGraph 全部:状态、节点、控制流 执行与存档机制 业务流程明确的系统
create_agent + middleware 工具、提示词、钩子配置 agent 循环本身 标准 ReAct 循环 + 少量定制
deepagents 任务描述、顶层约束 完整工作环境与执行策略 open-ended 的通用助手类任务

表里的「适合」列是必要条件不是充分条件:落在某一行的任务,用那一行大概率能做;但选层的经济性还要看另一半,团队要维护它。新人多、流转快的团队,中层和顶层的低代码量是真金白银;核心逻辑长期演进、出问题要快速定位的系统,底层的显式性是真金白银。第 01 篇的判断线回答「要不要框架」,这篇的三层回答「框架用到多深」,两道题都答完,选型才算闭环。

成本维度也值得单独看一眼,三层的 token 账单结构不同。底层的账单你全权决定:状态放什么、上下文留多少,都在你自己写的节点里。中层的账单受循环形态影响:工具清单每次循环都发一遍,工具越多固定开销越大,摘要配置直接决定长对话的成本曲线。顶层的账单由 harness 的默认行为主导:子 agent 各自的上下文、任务清单、记忆文件都在消耗预算,好用但不是免费的。评估成本时按层问「谁在决定上下文大小」,答案在哪层,优化就要去哪层做。

收拢成三个问题,选层就能在半小时内定下来:你的需求里有没有那三个控制点的影子(有就往下沉一层);你要改的行为在现成 middleware 清单里吗(在就用现成的,不在就评估自定义或下沉);出问题时你希望调试的颗粒度是节点级还是行为级(前者下层,后者上层)。三问都有明确答案的项目,选层不是难题;三问都含糊的,说明需求还没定型,从中层起步、保留下沉路径,是那个阶段唯一不会错的答案。

延伸阅读


原文出处:91ai / LangGraph 生产实战系列

评论

0
登录后可以参与评论和讨论。

还没有评论

欢迎留下第一条评论,帮助这篇内容更快形成讨论。