选框架最常见的错误不是选错,而是流程还没想清楚就选了。三家都能跑起 demo,差别全在第三天:加人工审批、断点续跑、拆可观测步骤、让 Java 后端复用同一套工具时,摩擦面完全不同。不谈"谁更强",只拆分工边界与判据。
1. 框架替你解决的三类问题
框架的价值必须能写成"不用它我要自己写什么",否则就是负债。
| 问题域 | 框架提供什么 | 自写的真实成本 | 是否值得买 |
|---|---|---|---|
| 编排与状态 | 节点/边、循环、条件分支、状态合并、检查点 | 状态机 + 序列化 + 恢复,典型量级几百到两千行 | 值得,尤其检查点 |
| 工具与模型适配 | schema 生成、多供应商切换、结构化输出、重试 | 每家 SDK 一层适配器,接口变动跟着改 | 部分值得,抽象层会漏字段 |
| 可观测与部署 | 轨迹、token 计量、托管运行时、流式 | OTel 手埋点 + 自建 trace 表 | 值得,但别指望开箱即全 |
反过来:如果流程是"读一条消息 → 调一次模型 → 写一个字段",这三类问题都不成立,自己写两百行更省。框架的成本不在学 API,在于它拿走了控制流的所有权。先问:三个月内会加几个分支、几个人工审批点? 都是 0,就别上框架。
2. 三种定位:图、角色、生命周期
中心对象不同,决定了思考的单位。
| 维度 | LangGraph | CrewAI | Google ADK |
|---|---|---|---|
| 中心对象 | 图(节点 + 状态) | 角色 + 任务 | Agent 生命周期 + 工具 |
| 控制流来源 | 显式画出 | Process(顺序/层级) | runner 驱动 + 回调 |
| 状态单位 | TypedDict / Pydantic state | Task 输出向下传 | Session / State |
| 循环表达 | 边回跳到前节点 | 靠重试或层级委派 | 组合器(LoopAgent 类) |
| 人工介入 | interrupt + 检查点 |
需自己包一层 | 工具确认回调 |
2.1 LangGraph:图是唯一真相
状态显式、边显式,"为什么走到这里"永远可回答。
class OrderState(TypedDict):
order_id: str
retries: int
approved: bool
g = StateGraph(OrderState)
g.add_node("plan", plan)
g.add_node("execute", execute)
g.add_node("human", wait_for_approval)
g.add_conditional_edges("plan", route, {"execute": "execute", "human": "human"})
g.add_edge("execute", END)
app = g.compile(checkpointer=checkpointer) # 恢复点从这里来
代价是你得先把流程想清楚;流程还在变时,画图会变成改图。
2.2 CrewAI:角色与任务,控制流是副产品
researcher = Agent(role="条款检索", goal="找到规范原文条款", backstory="熟悉内部编号体系")
auditor = Agent(role="合规核对", goal="把条款转成检查项", backstory="做过落地检查")
t1 = Task(description="检索 ORDER_SPEC 第 3 章", expected_output="原文 + 出处", agent=researcher)
t2 = Task(description="输出检查清单 JSON", expected_output="JSON 数组", agent=auditor, context=[t1])
Crew(agents=[researcher, auditor], tasks=[t1, t2], process=Process.sequential).kickoff()
写起来最快,语义贴近"分工"。但需要"根据中间结果跳回第三步"时表达就绕:只能把循环塞进 Task 描述让模型自己决定重做与否——确定性交给了概率。
2.3 ADK:生命周期与工具体系是重点
order_agent = LlmAgent(
name="order_agent",
model=MODEL_ID,
instruction="只处理订单状态类问题;其他意图转交 supervisor,不要自行猜测。",
tools=[FunctionTool(query_order)], # docstring 即工具描述
)
强项是工具与回调:调用前后都能挂钩子,适合审计、脱敏、限流;弱项是图表达力,复杂分支要靠组合器拼。
3. 状态与控制的表达力对照
对着自己的流程逐行打勾,勾不满的就是将来要手写补的部分。
| 控制结构 | LangGraph | CrewAI | ADK |
|---|---|---|---|
| 条件分支 | 原生 conditional edge | 需在 Task 内描述 | 靠 flow/组合器 |
| 循环与重试上限 | 原生(回跳 + 计数入 state) | 弱,通常靠 max_iter | 组合器 + 计数 |
| 并行扇出 | 原生(多边 + 合并节点) | 部分 | 原生 ParallelAgent |
| 人工介入(停数小时) | 检查点 + interrupt |
需外部队列拼 | 回调 + 长任务 |
| 状态可读性 | 高,state 就是 dict | 中,输出藏在 Task 里 | 中,session 对象 |
三者都能循环,但只有 LangGraph 把循环画在同一张图上——评审时不用在脑子里重建控制流。
4. 可观测与持久化的内建程度
POC 最易被忽略、上线后最痛的一块。
| 能力 | LangGraph | CrewAI | ADK |
|---|---|---|---|
| 检查点 | 内建,可换后端 | 无内建持久化 | Session service |
| 断点续跑 | 原生(同 thread_id) | 需自己存任务输出 | 需自己接 |
| 轨迹 | 与 LangSmith 生态紧耦合 | 事件监听 + 第三方 | Cloud Trace / OTel 路线 |
| 单独重跑某一步 | 支持(从检查点 fork) | 不支持 | 部分 |
config = {"configurable": {"thread_id": "order-8821"}}
app.invoke({"order_id": "8821"}, config) # 跑到人工节点暂停
app.invoke({"approved": True}, config) # 从断点继续,不重跑前面节点
没有检查点时,"等审批四小时"意味着四小时后整链重跑,前面所有工具副作用重来。这才是"隐式重试导致重复副作用"的根源——不是模型的问题,是架构的问题。
5. 多模型适配的抽象层次
| 层次 | 表现 | 风险 |
|---|---|---|
| 只换 model 字符串(LiteLLM 类网关) | 三家都常见 | 供应商特有字段被丢弃 |
| 厂商 SDK 直连 | ADK 对 Gemini 路线最顺 | 跨供应商要另加适配层 |
| 自建网关 | 统一计费/限流/审计 | 多一层运维,流式与工具调用要透传 |
换模型时先验的不是回答质量,而是工具调用是否还稳定:描述与 schema 相同,命中率也可能差一截。
6. 和既有后端的集成边界
现实里 agent 很少孤立,典型是 Java 主站 + Python 侧车。
| 集成方式 | 适用 | 代价 |
|---|---|---|
| Spring AI 直接做 | 流程简单、团队只有 Java | 图编排弱于 LangGraph |
| Python 侧车暴露 HTTP | 职责清晰 | 要处理超时、幂等、鉴权 |
| 通过 MCP 共享工具层 | 工具被多客户端复用 | 需维护协议与版本 |
| 消息队列异步 | 长任务、可容忍延迟 | 需补偿与状态查询接口 |
MCP 是最值得投入的一层:工具定义一次,Python agent、Java 服务、IDE 客户端共用同一份 schema。
params = StdioServerParameters(command="python", args=["-m", "order_mcp"],
env={"LOG_LEVEL": "warn"})
async with stdio_client(params) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
Spring AI 侧对应 MCP client 自动配置,把同一批工具注册成 ToolCallback。边界划在这里,换框架时工具层不用重写。
7. 选型判据清单
按顺序回答,前面的答案会否决后面的选项。
| 判据 | 问题 | 指向 |
|---|---|---|
| 流程确定性 | 分支和顺序现在能画出来吗 | 能 → 图型;不能 → 先原型再谈框架 |
| 人工介入 | 有停几小时等审批的环节吗 | 有 → 必须有检查点,排除无持久化方案 |
| 断点续跑 | 失败能否接受从头再来 | 不能 → 同上 |
| 团队语言栈 | 主力 Java 还是 Python | Java → Spring AI / MCP 桥;Python → 三者皆可 |
| 可观测要求 | 需要逐步复盘吗 | 需要 → 查检查点与 OTel 完整度 |
| 迭代节奏 | 三个月内会大改吗 | 会 → 选约束少的,别选图 |
把这张表做成打分卡:每项按能力给 0–3 分,低于阈值直接出局,别用"感觉更顺"做决策。
8. 迁移成本:耦合长在哪
迁移成本不看行数,看编排逻辑有多少长在框架 API 上。
| 耦合点 | 迁移难度 | 缓解办法 |
|---|---|---|
| 状态定义 | 低 | 纯数据类,别继承框架基类 |
| 节点函数签名 | 中 | 内部写成纯函数,外头套薄壳 |
| 路由函数 | 中 | 只读 state,不读框架上下文 |
| 检查点格式 | 高 | 只存业务状态,不存框架内部对象 |
| 工具注册方式 | 中 | 走 MCP 或自建注册表,别用装饰器直绑 |
def execute_refund(state: dict) -> dict:
return refund_service.apply(state["order_id"], state["amount"]) # 纯业务
def node_execute_refund(state: OrderState) -> dict:
return execute_refund(dict(state)) # 只有这一层知道框架
坚持这层薄壳,换框架时改壳不改业务。
9. 排错:四个高频坑
| 现象 | 根因 | 处理 |
|---|---|---|
| 简单流程也"模型自己选下一步",结果飘 | 为"agent"把确定性流程做成自主决策 | 能写 if 的地方不给模型;只把真需判断的环节交出去 |
| DB 业务状态与框架 state 不一致 | 两套状态各写各的,没有单一写入点 | 业务状态以 DB 为准,state 只存游标与临时结果 |
| 一次失败后下游收到两笔退款 | 隐式重试 + 工具不幂等 | 工具带幂等键;重试次数显式配置,不留默认值 |
| 半年后框架停更,升级卡死 | 选了不活跃项目且深度耦合 | 看提交频率与发版节奏;耦合点按第 8 节收敛 |
框架活跃度是选型指标,不是八卦:查最近几个月的 release 与 breaking change 频率,比 star 数有用。
10. 边界:这些情况本就别用框架
| 场景 | 更合适的做法 | 理由 |
|---|---|---|
| 单轮抽取/分类 | SDK + 结构化输出 | 框架只多一层间接 |
| 流程完全确定、无模型判断 | 普通工作流引擎 | 不需要概率决策 |
| 只要工具复用 | MCP server + 简单 client | 工具层与编排层可分开 |
| 强审计、每步留证 | 自建编排 + 显式状态表 | 框架 trace 未必满足审计 |
| 极致延迟敏感 | 单次调用 + 缓存 | 抽象有开销 |
一句话:框架买的是"状态 + 恢复 + 轨迹",不是智能。 这三样都不需要,它就是纯成本。
11. 自测
- 你的流程里"下一步做什么"由代码决定还是由模型决定,分界线画在哪?
- 没有检查点时,一次要等人工审批四小时的流程会发生什么?
- LangGraph 的 conditional edge 与 CrewAI 的 Task 描述,在可评审性上差在哪?
- 团队主力是 Java,走 MCP 共享工具层能省掉哪部分重复工作?
- 隐式重试在什么条件下会产生重复副作用,你打算怎么堵?
- 判断一个框架是否活跃,你会看哪三个客观信号?