Skip to content
Charles Shao
Go back

什么时候不必上 Agent 框架:三条边界,四笔成本

–views

上一篇把这一套拆成上下两层:LangGraph 在底下跑循环、管 checkpoint,create_agent 是日常入口。那篇第二节有一句一带而过的话:任务短、没有工具、一次请求就结束,官方自己也说,大概用不上任何框架。选型走到这里,这句话值得单独展开,因为它回答的问题比「选哪个框架」更靠前——什么样的任务,根本不该承担框架的成本。

回答这个问题,我们手里有一个具体的参照。手搓系列已经把循环、护栏、审计和续跑从零写过一遍,哪些几十行就能写完、哪些越写越像在重造运行时,心里有数。所以这篇不做功能对比,而是把收益和成本逐项摆出来:先看官方自己划的线,把它拆成三条能落地的边界;再对照手搓,看清框架到底替你做了什么、又要你付出哪几笔成本;最后给出按层采用的判断顺序。

Table of contents

Open Table of contents

一、官方自己划的那条线

LangChain 在 2025 年 9 月发过一篇设计回顾 Building LangGraph,上一篇说的「可控」「耐用」两条理念,和慢、会失败、输入输出开放那三条前提,都出自这里。三条前提各引出两项能力:一次 run 按秒、按分钟计,很快会按小时计,所以要压延迟;跑到第九分钟崩掉,从头再来又慢又贵,所以重试要更少、更便宜;同一个 prompt 隔天可能得到不同的结果,开发时的测试也覆盖不了用户的真实用法,所以要能停下来让人参与,也要看得清线上发生了什么。合起来,就是他们认定生产环境最常用到的六项能力:

能力解决什么
并行(Parallelization)互不依赖的步骤同时执行,压缩真实延迟
流式(Streaming)边跑边展示进度或逐字输出,压缩体感延迟
任务队列(Task queue)让 run 与触发它的请求解耦,减少重试次数
检查点(Checkpointing)保存中间状态快照,降低每次重试的代价
人工介入(Human-in-the-loop)随时停住,等人确认或修改后接着跑
追踪(Tracing)看清输入、轨迹和输出,知道用户实际怎么用

清单之后紧跟着一句:如果你要做的 agent 用不上其中大部分——比如它很短、没有工具、只有一个 prompt——那你可能不需要 LangGraph,也不需要任何别的框架。上一篇引过的「最大的竞争对手是不用框架」,原文前面还有半句设计原则:框架对开发者代码提出的每一条要求,都必须由一项真正高价值的能力来抵偿,否则干脆不用框架的吸引力就太强了。任何代码框架最大的竞争对手,永远是不用框架。

模型厂商那边的结论方向一致。Anthropic 在 2024 年底发布的 Building effective agents 里写道,框架确实简化了调用模型、定义和解析工具、串联多次调用这些底层工作;但它们往往多出几层抽象,把底下真实的 prompt 和响应遮住,排障变难,还容易诱使人在简单方案已经够用时继续堆复杂度。他们的建议是先直接调用模型 API,很多模式几行代码就能实现;真要用框架,务必弄懂它底下的代码,对底层行为的错误假设是客户出问题的常见来源。

一家做框架的公司,一家卖模型的公司,结论指向同一处:框架值不值得上,取决于那六项能力你真正用得上几项。 这个标准比「社区大不大」「demo 起得快不快」更可操作,后面几节都用它来衡量。

二、三条边界:单次请求、无工具、短任务

LangChain 那句话里的三个条件,落到订房这条业务线上,可以拆成三条彼此独立的边界。每一条都要同时看两件事:为什么此时用不上框架,以及出现什么信号时,这条边界就不再成立。

第一条,一次请求就能给出结果。 用户说「西安 10 月 1 日住两晚,下雨订 suite,否则订 standard」,流程的第一步往往只是把这句话抽成城市、入住日期、晚数和房型规则几个字段。这是一次模型调用加一段结构化输出:没有循环,没有需要模型决定的「下一步」,也就没有中间状态可言。框架在这里能提供的只是便利,比如换 provider 时少写几行适配代码,而不是那六项能力里的任何一项。

第二条,没有工具。 订单成功后给用户发一条确认短信,先按模板起草、再按规则检查一遍措辞,是按固定顺序调一两次模型。按 Anthropic 的分法,这属于工作流里的提示链(prompt chaining):路径由代码预先写死,模型只负责其中几步的生成。没有工具就没有 tool_calls,循环无事可判;更重要的是没有副作用,没有哪个动作需要停下来等人确认。它就是几次普通的函数调用。

第三条,短任务。 「西安现在天气怎么样」要走 search_location 和 get_current_weather 两轮工具,已经是一个真正的循环;但它全是只读调用,几秒钟内在一次 HTTP 请求里跑完,中途失败了整段重来也只多花几秒。这正是从零手搓那一版:一个带轮数上限和 token 预算的 while,每轮打一行轨迹日志。LangChain 把检查点定位成「降低每次重试的代价」,可这类任务的重试代价本来就低,每一步落盘的快照不会有人去读。

边界订房线上的例子为什么用不上框架越界信号
单次请求把一句话抽成订房参数没有循环,也没有中间状态开始需要模型自己决定下一步
无工具按模板起草确认短信路径由代码写死,也没有副作用接上了订房、扣款这类写操作
短任务两轮只读工具查天气失败了整段重跑也便宜要等人确认、要跨请求续跑、单次 run 按分钟计

最右一列比前三列更值得记住。三条边界不是给任务贴一次就算的标签,而是会随需求移动的阈值:查天气的 agent 哪天接上了 book_hotel,第三条边界就被写操作打破了——订单不能整段重跑,确认之前也不能执行。手搓系列从工具设计开始一路加上白名单、护栏、幂等键、审计和续跑,本质上就是在一条条越过这些边界。

三、框架真正卖的是什么:对照手搓

越过边界,也不等于立刻非上框架不可。手搓系列其实已经把六项能力里的四项做出了粗粒度版本,关键是看清它们做到哪一步开始吃力:

能力手搓系列怎么做的做到哪开始吃力LangGraph 给的
并行手搓上线用线程池并发一轮内的 tool_calls,回灌仍按请求顺序配对并行的不再是几次工具调用,而是几段各有中间状态的多步流程Send 运行时扇出,reducer 合并,结果与完成先后无关
流式没做,每轮结束才打一行日志用户要看逐字输出和工具进度values、updates、messages、custom 等 stream mode
任务队列没做run 要和请求解耦,失败要公平地重试Python 库里同样没有,要靠他们的部署平台或自己接队列
检查点每轮把 messages 和记忆按 run_id 写成 JSON状态不止 messages;要从一轮中间恢复;要换机器、隔几天再续每个 superstep 落一份可移植的 checkpoint,按 thread_id 取回
人工介入终答里问确认后停住;--confirm 追加一条 user「确认」,让模型再推理一轮确认后模型可能换了参数;同时挂起的 run 越来越多interrupt() 停在节点里,Command(resume=...) 把人的答复交回那个节点,不必重新问模型
追踪每轮打一行带 run_id 的轨迹日志,轨迹检查读落盘的 messages要按 run 检索、对比,做线上评测执行步骤本身就是结构化的,可接 LangSmith、Langfuse 或 OTEL

这张表里最容易被低估的是检查点那一行。全链路那一版的续跑之所以便宜——读写审计的 audit.py 不到 30 行有效代码,loop.py 里再多一段 if resume——是因为那个循环只有一个恢复点,就是下一轮的开头;状态也只有两样,messages 和记忆,都能直接写成 JSON。LangChain 在同一篇回顾里说,把 agent 写成「一个函数加一个大 while」就做不了检查点和人工介入,因为函数执行到一半的状态存不成可移植的格式,没法换一台机器、隔一段时间再恢复。放在手搓系列面前,这句话要加一个限定:全链路那一版的续跑同样可移植,JSON 换一台机器也读得回来,因为它已经按「轮」切出了离散的步骤,只是切得粗,恢复点只能落在一轮的开头。要在一轮中间的任意一步停住、再从那一步原样恢复,步骤就得切得更细,这正是 LangGraph 按节点和 superstep 做的事。

框架的价值从三个地方开始显现:恢复点挪进一轮的中间,比如 tools 节点里、订房写库之前;状态从 messages 长成一组带合并规则的字段,并行分支还各有各的中间结果;同时挂起的 run 从几个变成成百上千,隔几天才有人回复,还要能随时翻看和回溯。到了这一步还坚持手搓,写出来的就是一个简陋的运行时。LangChain 在那篇回顾里也提到,LangGraph 的前身因为没有明确的执行算法,并发节点出现过不确定的行为,后来才选定 BSP / Pregel 这套按 superstep 推进的模型。自己写运行时,迟早也要回答同一个问题。

顺带澄清一点:框架省下来的主要不是代码行数。@tool 从函数签名和 docstring 推出 schema,房型写成 Literal 就成了 enum;ToolNode 执行并按 id 回灌。省掉的是手写 JSON schema、dispatch 和配对回灌那几十行;工具函数本体、提示词,以及 hotel_id 白名单、日期格式、这家酒店有没有这个房型这些业务校验,一行都省不掉。

四、上了框架要付的四笔成本

左边是框架替你做的六项能力:并行、检查点、人工介入、追踪在手搓系列里已有粗粒度版本,流式没做,任务队列两边都没有。右边是一上框架就开始计的四笔成本:版本耦合、执行语义、状态契约、可见性。

能力是按需取用的,成本却从引入框架那一刻起就开始计。下面四笔在后面几篇都会真实遇到,这里先对上号。

第一笔,版本耦合。 按 2026 年 9 月在 Python 3.12 下用 pip 解析的结果,只装 openai 连同传递依赖是 14 个包,只装 langgraph 是 38 个,配套仓库 part2 用到的四个包(langchain、langchain-openai、langgraph、langgraph-checkpoint-sqlite)合起来是 47 个。包的数量本身不是要害——part1 为 MCP 多装一个 fastmcp,总数就到了 72——要害在耦合:langchain 1.4.2 把 langgraph 锁在 >=1.2.11,<1.3.0,langchain-openai 1.6.5 把 openai 限在 >=2.45.0,<4.0.0,两者又同时约束着 langchain-core 的版本,升级只能成组地升。再往上是 API 本身的演进,0.x 到 1.x 之间,Chain、AgentExecutor 和 create_react_agent 都收进了 create_agent;框架的 bug 也会原样继承,比如 update_state 之后 next 变空,到 langgraph 1.0.3 才修掉(issue #6433)。

第二笔,执行语义。 手搓循环里,恢复从哪一行开始是你自己写的;上了框架,就得接受它的规则。interrupt() 恢复时节点会从第一行重新执行,所以写操作必须放在 interrupt() 之后并带上幂等键,否则人还没点头,订单就已经下了——这是中断续跑的核心约束。reducer 必须是纯函数,因为 checkpoint 恢复和状态回放都可能把它再算一遍(见状态契约)。recursion_limit 数的是 superstep,不是工具调用次数(见条件边与子图)。Anthropic 说的「对底层的错误假设」,落到这里就是节点重跑导致的重复下单。

第三笔,状态契约。 凡是要跨过一次暂停保留下来的东西,都得进 State,而且要能序列化:checkpoint 会把 State 编码后写进存储,默认用 MsgPack。进程内用得好好的模块级变量,比如框架系列前两层那份 seen_hotel_ids 白名单,一换成跨进程续跑就会丢,确认之后反而回灌「不在本次搜索结果里」,必须提升进 State(见关了进程再续和 create_agent 的排障表)。没声明 reducer 的字段每次更新都整体覆盖,第一次 invoke 不给初值就读到 None;模型客户端、数据库连接这类对象,本来就不该放进 State。

第四笔,可见性。 create_agent 默认不把图画给你看。停住时 get_state 告诉你 next=('HumanInTheLoopMiddleware.after_model',),这个节点名来自中间件,不是你写的代码;真正发给模型的消息也可能被中间件改写过,比如压缩把旧消息换成了摘要。手搓时 messages 就是你手里的一个列表,打印出来就是真相;上了框架,想看真实的 prompt 就得接追踪。这正是 Anthropic 所说的「把底下的 prompt 和响应遮住」。

四笔之外还有一项运行时开销:挂上 checkpointer 之后,每个 superstep 都要序列化并写一次存储。默认的 durability="async" 在下一步执行的同时异步落盘;"exit" 只在图退出时写,包括停在 interrupt() 的那一刻,更省,但进程中途崩溃就没法恢复(见官方文档 Durability modes)。一次请求里就能跑完的短任务,干脆别挂 checkpointer。

五、按层采用:不必一次全上

这一套本来就是分层的,采用也可以分层。按上一篇的划分,从不用框架到用足高层入口,有三个位置可以停:

只用模型 SDK。 适合前面三条边界之内的任务。手搓系列 part1 就停在这一层,调模型的依赖只有 openai 一个包。

只用 LangGraph 运行时。 需要停住、续跑、落盘和并行分支,又不想要高层封装时,停在这一层。langgraph 依赖的是 langchain-core 里的消息等基础类型,并不依赖 langchain 这个包;节点就是普通的 Python 函数,在里面照样可以直接调 OpenAI SDK。checkpointer、thread_id、interrupt() 这些真正值钱的能力,都在这一层。

用 create_agent。 当你的循环就是标准的「问模型、有 tool_calls 就执行、回灌」,又需要人确认、压缩、换模型时,用它最省事。它编译出来的仍是 CompiledStateGraph,默认那张图不够用时,随时可以退回上一层自己画。

四个问题依次往下问:一次请求就能给出结果吗、需要工具吗、要停住等确认或跨进程续跑吗、下一步是否只看有没有 tool_calls。前三个问题分别答能、不需要、都没有时,落在不用框架:直接调 API、按固定顺序调模型、手搓 while;第四个问题答是落到 create_agent,答否落到自己画 StateGraph。

把三层串起来,就得到一套判断顺序:

  1. 一次请求就能给出结果吗?能,就直接调模型 API,需要结构就用结构化输出。
  2. 需要工具吗?不需要,就不写循环,多步也只是按固定顺序调几次模型。
  3. 有写操作要等人确认、要跨进程续跑,或者单次 run 按分钟计吗?都没有,手搓那个 while 就够,加上轮数上限、token 预算和轨迹日志。
  4. 有,就上 LangGraph 运行时。下一步只看「有没有 tool_calls」时直接用 create_agent;要按 State 分流、并行扇出或嵌套子图,就自己画 StateGraph。

这套顺序敢从「不用」问起,是因为每一步都能回退,也能随时往下走。手搓系列迁到框架系列时已经演示过:订房工具还是那几个普通函数,照样读本地假库存;schema 从手写 JSON 换成 @tool 从签名推出,白名单和日期、房型校验依旧留在函数体里。只要守住两条——工具写成签名清楚、docstring 写明前置条件的普通函数,状态保持显式——换层就只是换外面那层包装。「先不上,撞到边界再上」是一个代价很低的策略。

六、常见误判

常见说法实际情况
「先上框架,以后总会用到」以后再迁移并不贵;现在就上,版本耦合和执行语义的成本现在就开始付
「用了框架就不用懂循环」框架替你写 while,不替你承担语义:节点重跑、reducer 纯净、recursion_limit 都得懂
「手搓也能续跑,所以用不上框架」手搓只在一轮开头续跑,状态只有 messages 和记忆;恢复点一旦挪进一轮中间,就是在重写运行时
「短任务也挂上 checkpointer,更保险」每一步都要序列化、写存储,却没有人读;用不上续跑就不挂
「装了 langchain 就等于用了 LangGraph 和 LangSmith」langchain 依赖 langgraph,反过来不成立;LangSmith 是另外的观测、评测与部署产品
「框架写出来的代码更短」省下的是手写 schema、dispatch 和配对回灌;工具、提示词和业务校验一行不少

接下来

边界划清之后,就可以动手了。下一篇从底下那层开始:用 StateGraph 把手搓那个 while 画成两个节点,看清上面那些执行语义最初从哪里来。


–views
Share this post on:

Previous Post
LangGraph 时光旅行:回溯状态、断点重跑与分支分叉
Next Post
LangGraph:条件边、Send 与子图如何接成一张图