选 Agent 框架,先找出系统最不能出错的交接,再决定用角色、状态图还是对话来表达它。
Pickaxe 的 CrewAI、LangGraph、AutoGen 对比提供了一个好记的入口:CrewAI 像分工团队,LangGraph 像流程图,AutoGen 像群聊。这个比喻适合开始学习,却不足以直接得出生产选型结论。
读到它对 LangGraph 恢复能力的描述时,我想验证一个具体问题:如果文章已经写入发布系统,Agent 却在保存成功状态之前崩溃,恢复后会不会再发一次? 本文的实际实验给出的答案是:会。即使用了文件型检查点、同步持久化,仍然可能发生。
因此,我更愿意把这三类框架看成不同的编排入口。它们都处在“用户目标”和“模型、工具、外部系统”之间,帮助我们组织下一步动作。真正需要比较的是四个部件:
| 部件 | 要回答的问题 | 改变它会怎样 | 缺失时先坏在哪里 |
|---|---|---|---|
| 路由 | 谁决定下一步交给谁? | 更多交给模型,分支更灵活,也多出决策成本 | 重复讨论、走错分支 |
| 状态 | 当前进度和证据保存在哪? | 更明确的状态便于恢复,也增加维护工作 | 接手者不知道哪些事已经做完 |
| 提交 | 工具动作何时算真实完成? | 业务幂等键可避免重试重复生效 | 重复发文、重复创建工单 |
| 验收 | 凭什么说目标完成了? | 检查越贴近业务结果,越能发现“看起来完成” | 日志全绿,产物仍然错误 |
本文核验日期为 2026 年 9 月 8 日。框架现状按当前官方资料判断;论文按下文指定版本讨论,不把旧论文实验当成今天的产品排行榜。
先把几个容易混用的词分开
| 术语 | 在本文里的具体含义 |
|---|---|
| Agent | 反复根据观察决定动作的执行单元;一次任务可能调用模型和工具多次 |
| 编排 | 决定执行顺序、交接内容、暂停条件和退出条件的外层逻辑 |
| 状态 / memory | 状态记录“本次做到了哪”;供模型检索的长期记忆不必包含完整执行进度 |
| checkpoint(检查点) | 运行时保存的进度快照,像游戏存档;它不会自动撤销游戏之外发生的事情 |
| 副作用 | 会改变外部世界的动作,例如创建文章;读取文章通常不属于这一类 |
| 幂等键 | 为同一业务动作使用固定编号,让重复请求命中同一结果 |
| 验证器 | 检查结果是否满足条件的程序或评审过程;名字叫 reviewer 的 Agent 不自动具备可靠验证能力 |
关于恢复的更完整背景,可以接着读站内的《Agent 的断点续传》。这篇只用一个具体崩溃窗口,检验框架宣传与实际责任的分界。
原文哪些判断可以留下,哪些需要重新核验
原文的价值在于展示抽象差异;问题在于,有些方便理解的标签被扩大成了能力边界。
CrewAI 不只有角色分工。 当前 Flows 文档包含条件控制、结构化状态和 @persist 持久化。@persist 是给类或方法加上的保存状态标记,不是“记住用户偏好”。因此不能仅凭 memory 一栏判断它没有运行时状态能力。另外,当前 Processes 文档列出的实现是 sequential 和 hierarchical;原文并列提到的 consensual 不能直接当作当前可用枚举值。
AutoGen 与 AG2 要分开确认。 Microsoft AutoGen 仓库当前已声明进入维护模式,并引导新项目使用 Microsoft Agent Framework;AutoGen Studio 是其图形化原型工具,不能把它当成这次路线迁移的目标。AG2则是独立维护的项目,核验时发布列表已有 v1.0.4,原文“仍在等待稳定 1.0”的描述已经不能用于今天的判断。许可证也应按仓库看:Microsoft AutoGen 代码采用 MIT,AG2 的相关修改与新增采用 Apache-2.0,不能合并成同一格。
LangGraph 的持久化有配置与粒度前提。 独立使用时要配置检查点后端;内存型保存器在进程重启后会丢失状态。托管的 Agent Server 可代管这些基础设施,但仍要区分图状态与外部业务系统。官方 Persistence 文档明确区分了 checkpointer 与跨任务的 store。
原文还转述了三组框架完成率,并给出了生产能力排序。我没有在这篇原文中看到能复现该排序所必需的任务集、模型配置、预算、版本和统计过程,因此本文不采用这些百分比。它们不能替代同题、同预算的对照实验。
价格也要分层比较:开源库许可、托管运行、观测平台、模型调用和维护人力是不同账目。把一个项目的自托管费用与另一个项目的商业套餐放在同一行,推不出谁的总成本更低。本文不沿用原文的套餐报价。
四篇论文,把三个比喻拆成可检查的机制
ReAct:先证明一个循环能工作
ReAct,arXiv:2210.03629v3研究的是让推理与动作交替进行:根据已有信息形成下一步,调用环境获得观察,再更新计划。它在问答与交互任务中验证这一方式,而非比较本文三个框架。
这为选型补上了一个经常遗漏的基线:单个 Agent 加工具循环,究竟缺少什么?
以“检索资料并写文章”为例,同一个执行者也能检索、写作、检查链接。拆出研究员和编辑之前,先说明新增边界能带来什么:独立证据来源、更窄的工具权限、更短的上下文,还是可以并行的工作。只有角色名称变化,不能证明产生了新的能力。
AutoGen:对话也是控制程序
AutoGen,arXiv:2308.08155v2的关键设计是 conversable agents 与 conversation programming:参与者可以由模型、工具、人或它们的组合驱动;自然语言和代码共同规定交互方式。
所以“群聊”不意味着放任模型聊天。对话背后仍然有可设计的控制规则:谁能收到消息,什么反馈会触发重做,什么时候停止。
我的工程推论是:如果代码作者把“所有检查通过”发给评审者,评审者却看不到测试输出,那么多一次回复不等于多一次验证。对话是否有价值,要看它引入了什么新观察,而非多少参与者表示同意。
MetaGPT:分工的价值落在交接产物上
MetaGPT,arXiv:2308.00352v6把标准作业流程引入协作,强调结构化中间产物、共享消息池中的发布订阅,以及运行代码获得反馈。其软件任务实验支持这些设计在相应设置下的价值,不构成 CrewAI 的效果证明。
这篇论文让我更关注“角色拿什么交差”。一个研究员输出一段顺滑总结,与输出“判断、来源、原文定位、适用日期、待核实项”,对写作者是完全不同的接口。角色像工位,交接产物才是下一个工位能加工的零件。
把这种思路放进 CrewAI 的任务定义、LangGraph 的状态字段或者 AutoGen 的消息协议,都成立。论文启发的是接口设计,不能反向充当某一品牌的认证。
MAST:加一个 reviewer,并不能抹掉前面的错误
Why Do Multi-Agent LLM Systems Fail?,arXiv:2503.13657v2通过执行轨迹分析建立 MAST 失败分类,将 14 类问题归入规范设计、Agent 间失配和任务验证三大类。本文关注其方法与第 4.3 节,而不搬用跨系统成功率做当前框架排名。
一个值得带走的发现是:验证可能只检查编译或表面质量,却没有检查任务真正要求的行为。错误也可能来自更早的交接,最后一个验证器没发现它,不代表根因只在验证器。
据此,我会把博客工作流的检查放在交接处:检索结束检查来源能否支持判断;成稿后检查引用是否错配;发布后检查公开页面。最后再让一个模型说“整体不错”,不能替代这些动作。
这四篇论文连起来,给出的设计顺序很清楚:先做能获取反馈的循环,再设计交接协议,最后用失败轨迹检查这些协议是否有效。 它们没有提供一张通吃所有任务的框架胜负表。
同一项任务,三种抽象分别把什么放到台前
假设目标是生成一份有引用的行业简报,经人工确认后发布。下表是我根据上述机制设计的映射,不是三个框架的默认模板或实测成绩。
| 入口 | 我会怎样表达任务 | 应重点审查的交接 |
|---|---|---|
| CrewAI 的角色与任务,外接 Flow | 研究员交资料包,作者交草稿,Flow 控制审阅与发布阶段 | 资料包有没有稳定结构;角色完成是否真的满足任务条件 |
| LangGraph 的状态与边 | 状态保存来源、稿件版本和审阅结果;条件决定继续检索、修改或发布 | 哪个节点可以重跑;并行更新如何合并;批准绑定哪一版稿件 |
| AutoGen / AG2 的消息交互 | 作者提出稿件,审查者针对证据反馈,调度规则决定下一位参与者 | 消息是否带证据;循环是否有预算;停止信号有没有外部依据 |
三种入口能组合,也会逐渐接近。一个 Flow 可以调用角色团队;一个图节点可以包住对话团队;对话也可以遵守固定流程。
但每多包一层,都要解释谁拥有最终状态、谁负责取消和超时,以及异常会落在哪一层。没有这类收益证据时,我会先选一个外层编排者,保持提交与验收的所有权清楚。
亲手验证:用了检查点,为什么还是重复发布
我用 Python 3.12.13、LangGraph 1.2.11 与 langgraph-checkpoint-sqlite 3.1.1 跑了一个不调用模型、不访问真实发布服务的实验。
图只有一个 publish 节点。一个 SQLite 文件保存图检查点,另一个模拟独立业务系统里的文章表。节点先提交业务表,然后立刻用 os._exit(23) 退出进程,刻意不给它返回成功结果的机会。随后启动新进程,从同一个任务 ID 恢复。
这里的 23 只是便于断言的故障退出码。测试使用 durability='sync',要求运行时同步保存检查点,以免把异步刷盘的风险混入这个问题。即便如此,业务提交和节点完成之间仍然存在窗口。
| 配置 | 首次进程 / 恢复进程退出码 | 恢复后的文章记录数 | 业务效果 |
|---|---|---|---|
| 每次写入生成新键 | 23 / 0 | 2 | 同一逻辑发布重复生效 |
重试沿用 article-42 | 23 / 0 | 1 | 重试命中已有记录 |
完整原始输出如下。这是两个确定性故障案例,不是通过率估计;每个案例各启动一次故障进程和一次恢复进程。
{"langgraph": "1.2.11", "langgraph-checkpoint-sqlite": "3.1.1"}
{"policy": "naive", "exit_codes": [23, 0], "posts": 2}
{"policy": "idempotent", "exit_codes": [23, 0], "posts": 1}
{"messages": 12, "full_history_tokens": 13200, "last_two_tokens": 4200, "difference": 9000}
最后一行用于下一节的手算验算。下面保存为 replay_probe.py,在安装了 uv 的环境运行:
uv run --python 3.12 --with langgraph==1.2.11 --with langgraph-checkpoint-sqlite==3.1.1 replay_probe.py
关键参数的意思:thread_id 固定为同一任务;恢复时输入 None 表示继续已有执行;INSERT OR IGNORE 配合主键约束,让重复业务键不再新增记录。article-42 只代表一个逻辑动作,生产中必须为不同动作生成不同键。
展开完整复现代码:真实子进程退出、磁盘检查点与结果断言
import json
import os
from pathlib import Path
import sqlite3
import subprocess
import sys
import tempfile
from importlib.metadata import version
from typing import TypedDict
from langgraph.checkpoint.sqlite import SqliteSaver
from langgraph.graph import StateGraph, START, END
class State(TypedDict):
done: bool
def worker(folder, policy, phase):
folder = Path(folder)
def publish(state):
with sqlite3.connect(folder / 'external.db') as db:
db.execute('CREATE TABLE IF NOT EXISTS posts (key TEXT PRIMARY KEY)')
key = 'article-42' if policy == 'idempotent' else os.urandom(8).hex()
db.execute('INSERT OR IGNORE INTO posts VALUES (?)', (key,))
# External transaction committed; node result not returned yet.
if phase == 'crash':
os._exit(23)
return {'done': True}
builder = StateGraph(State)
builder.add_node('publish', publish)
builder.add_edge(START, 'publish')
builder.add_edge('publish', END)
with SqliteSaver.from_conn_string(str(folder / 'checkpoints.db')) as saver:
graph = builder.compile(checkpointer=saver)
config = {'configurable': {'thread_id': 'same-task'}}
result = graph.invoke({'done': False} if phase == 'crash' else None,
config, durability='sync')
assert result['done'] is True
if __name__ == '__main__':
if len(sys.argv) > 1:
worker(*sys.argv[1:])
else:
print(json.dumps({p: version(p) for p in
['langgraph', 'langgraph-checkpoint-sqlite']}))
for policy in ['naive', 'idempotent']:
with tempfile.TemporaryDirectory() as folder:
codes = []
for phase in ['crash', 'resume']:
proc = subprocess.run([sys.executable, __file__, folder, policy, phase])
codes.append(proc.returncode)
with sqlite3.connect(Path(folder) / 'external.db') as db:
count = db.execute('SELECT count(*) FROM posts').fetchone()[0]
assert codes == [23, 0], codes
assert count == (2 if policy == 'naive' else 1), count
print(json.dumps({'policy': policy, 'exit_codes': codes, 'posts': count}))
full = sum(200 * i for i in range(12))
recent = sum(200 * min(i, 2) for i in range(12))
assert (full, recent) == (13200, 4200)
print(json.dumps({'messages': 12, 'full_history_tokens': full,
'last_two_tokens': recent, 'difference': full - recent}))
为什么运行时没有“阻止”第二次执行?因为它可靠掌握的是上一个检查点,而外部业务表已经先行提交。恢复时重跑未完成节点是合理选择;盲目跳过反而可能漏掉尚未提交的动作。
这个实验同时说明两件事:框架确实帮助我们找到了恢复位置;业务动作是否只生效一次,还需要提交端的协议。即便把写入单独做成一个节点,只要外部提交与该节点结果落盘分属两个事务,窗口仍然存在。
这里的共同祖先是分布式系统的重试与去重:投递可以再次发生,接收端用稳定业务标识识别重复。根本约束是:运行时进度与外部动作通常无法在同一个原子事务里提交。 如果两者确实可以放入同一事务,这个具体窗口就能被关闭;用一句“有 checkpoint”却无法改变事务边界。
修复也有条件。示例用数据库主键完成原子去重,不是“先查询有没有,再插入”;后者遇到并发仍可能重复。生产接口还要处理同键不同内容、键的保留期限、超时后查回结果,以及重试时返回原响应。它们没有在本实验中验证。
我没有用该实验测试 CrewAI 或 AG2,也没有测试分布式节点、断电和网络分区。它足以反驳“图能恢复,所以外部动作一定不会重复”的推断,不能据此排出三者可靠性的高低。
再算一笔账:对话成本藏在重复阅读里
“库免费”只说明少了一项许可或平台开销。多 Agent 协作的另一项成本,是后续调用反复读取历史消息。
先做一个简单的加法例子:假设依次发生 4 次模型调用,每次新增一条 200 token 的消息。只算此前的协作消息,四次输入分别重读 0、200、400、600 token,总计 1,200。这里 token 是模型计量文本长度的单位;200 是人为设定,不是对中文字符数的换算。
把调用扩展到 12 次,每次仍只新增一条消息,并且不计系统提示、用户问题、工具结果与输出费用:
| 消息注入策略 | 各轮重复读入量 | 总输入量 |
|---|---|---|
| 每次读全部历史 | 0、200、400,直到 2,200 | 13,200 token |
| 每次只读最近两条 | 0、200,此后每次 400 | 4,200 token |
第二行可以手算为 200 加上 10 次 400,得到 4,200;第一行是 0 到 11 的和 66,再乘 200,得到 13,200。上面的脚本对两项做了断言验算,差值是 9,000 token。
这不是任一框架的账单,也不意味着“只留两条”应该成为默认设置。截断历史会丢掉早期约束;摘要要付出额外调用,也可能压掉关键细节;缓存能改变实际计费与计算成本。
真正的选择是:下一位参与者为了作出正确决定,需要哪一份信息?MetaGPT 的结构化交接和订阅思路在这里提供了方向,但这个加法模型是我构造的,未测真实 tokenizer 或模型质量。
对简报任务,我会尝试保留“任务约束、已接受事实及来源、当前稿件、未解决异议”,而不把所有礼貌回复注入每一次调用。然后用同一组任务观察引用错误、遗漏约束和总 token 是否变化。降低输入量而让关键证据消失,属于退化,不属于优化。
把选型变成一张可执行的验收表
我的初始判断是:已有自然分工、交付物清楚的内容或研究流程,可以从 CrewAI 的 Crews 与 Flows 开始验证;大量条件分支、暂停恢复与显式状态是核心需求时,LangGraph 值得优先做原型;若研究对象本身就是对话协议,则考虑 AG2,或在已有 AutoGen 系统上延续实验。新项目进入 Microsoft 生态时,应把其当前推荐的 Agent Framework 纳入评估。
这只是缩小候选范围。正式采用之前,我会让候选实现完成同一张验收表:
| 测试 | 注入的条件 | 记录什么 | 我会据此拒绝什么 |
|---|---|---|---|
| 单 Agent 基线 | 同一任务、模型、工具和预算 | 结果正确性、耗时、token | 多 Agent 没收益却显著增加成本 |
| 丢失确认 | 外部动作已完成,调用方没拿到确认 | 动作数、查回记录、恢复轨迹 | 重复提交或无依据地宣告成功 |
| 错误交接 | 提供缺少来源或互相矛盾的资料包 | 是否拒收、追问或补证据 | 下游把缺失信息当事实 |
| 过期批准 | 批准之后更换稿件版本 | 批准记录绑定的版本 | 旧批准被用于发布新内容 |
| 超额讨论 | 无法解决的争议持续出现 | 总轮数、花费、退出理由 | 无限循环或预算耗尽才被动失败 |
| 结果验收 | 构建成功但线上正文不对 | 页面内容与预期差异 | 只检查进程退出码 |
表中这些是建议实施的测试,只有前文“丢失确认”对应的局部故障窗口已经在本文实验中实际执行。其他项需要用自己的业务数据补齐。
比较时还要记录框架版本、模型、工具描述、重试策略、上下文裁剪和预算。否则,测到的可能是两套应用配置的区别。《别再把 harness 当实现细节》讨论的正是这一类变量申报问题。
小结:先定义交接,再挑表达方式
角色让分工容易表达,图让状态转移容易检查,对话让反馈协议容易组织。三者最终都要回答:交给下一步什么,失败后相信什么,以及谁来证明结束。
检查点与业务提交分属两个事务时,最早的崩点可以只是一次成功写入后丢失确认,根本不需要“大规模”。 本文的两个进程故障案例把这个边界落到了可复现结果上。
下一步可以很小:有 Python 和 uv、无需 API 密钥,预留约 5 分钟运行上面的脚本,亲眼看见 2 条记录变成 1 条。然后找出自己应用里那个同样的窗口——工具动作已经生效,但运行时还不知道。能回答那里如何恢复,再回到框架对比表,选型会具体得多。
参考来源
工程资料与讨论起点
- Pickaxe:CrewAI vs LangGraph vs AutoGen:本文讨论起点;其排名与报价未作为本文结论依据。
- CrewAI Processes、Flows:流程枚举、条件控制与持久化。
- LangGraph Persistence、Fault tolerance:状态保存与失败处理;能力与版本应一并核对。
- Microsoft AutoGen、AG2:项目身份、维护路线和许可证。
论文
- ReAct: Synergizing Reasoning and Acting in Language Models,v3:推理、动作、环境观察的循环。
- AutoGen: Enabling Next-Gen LLM Applications via Multi-Agent Conversation,v2:可对话执行单元与对话编程。
- MetaGPT: Meta Programming for a Multi-Agent Collaborative Framework,v6:标准流程、结构化交接与可执行反馈,重点见第 3 节。
- Why Do Multi-Agent LLM Systems Fail?,v2:失败分类与验证边界,重点见第 4.1、4.3 节。