开源 Agent 框架选型:看抽象边界,不只看示例

0阅读11分钟

一次选型踩坑的起点

今年上半年,我需要给内部的一个工单处理系统加上 AI Agent 能力。需求不复杂:接收用户工单 → 分类 → 查知识库 → 生成回复草稿 → 人工确认后发送。

我花了一周时间把市面上主流的开源 Agent 框架都跑了一遍 demo。LangGraph 的图编排很优雅,CrewAI 的角色分工很直观,AutoGen 的多轮对话很灵活,LlamaIndex 的 RAG 集成很顺手。每个框架的示例都能在十分钟内跑通一个「看起来能解决问题」的流程。

真正上线的时候,问题集中爆发。CrewAI 的隐式状态在第三步丢失了工单 ID,AutoGen 的对话循环在第五步把 token 预算吃到了三倍,LangGraph 的 checkpoint 机制和我现有的 PostgreSQL 连接池产生了冲突。每个框架的 demo 都很流畅,每个框架在生产环境都让我不舒服。

这篇文章想聊的就是这件事:框架选型不能只看示例能跑通什么,要看它的抽象边界和你的系统边界是否对齐。

选型的五个核心维度

LangChain 在 2026 年初发布的 State of Agent Engineering 报告中把 Agent 框架的生产能力分成了几个层次。Langfuse 的 AI Agent Framework Comparison 也从集成角度做了横向测评。综合这些资料和我的实际踩坑经验,我把选型维度收敛到五个:

维度核心问题评估方式
状态管理执行上下文在哪里、怎么传递、能否持久化跑一个跨多步、需要中间状态恢复的流程
工具编排外部 API 调用、数据库查询、代码执行怎么接入接入一个需要认证的内部 API
可观测性每一步的输入输出、耗时、token 消耗是否可追踪在 trace 工具里看一次完整执行链路
人机协作能否在关键节点暂停、等人工审批后继续设计一个需要人工确认的流程节点
退出成本如果框架不合适,替换成自研或其他框架的工作量检查业务逻辑和框架代码的耦合度

这五个维度中,前四个决定框架能不能用,第五个决定框架选错了代价有多大。很多选型只看了前两个,忽略了后面三个。

案例一:CrewAI 的隐式状态丢失

场景

工单分类后需要把工单 ID、用户信息、分类结果传递给后续的知识库查询和回复生成步骤。CrewAI 的角色化设计很适合这个流程——分类员、检索员、撰写员三个角色各司其职。

翻车

CrewAI 的任务间状态传递依赖 context 参数,它是隐式的。前两个任务跑得很顺,到第三个任务时,撰写员拿到的 context 里只有检索结果,工单 ID 和用户信息丢了。排查后发现 CrewAI 的 context 传递机制在任务链超过两层时会做截断,这个行为在文档中没有明确说明。

修复

改用显式的状态对象贯穿整个 Crew 执行周期,不依赖隐式 context 传递。

# ❌ 坏做法:依赖隐式 context 传递,任务链长了会丢数据
from crewai import Agent, Task, Crew
 
classifier = Agent(role='分类员', goal='对工单分类', backstory='工单分类专家')
retriever = Agent(role='检索员', goal='检索知识库', backstory='知识库检索专家')
writer = Agent(role='撰写员', goal='生成回复', backstory='客服回复撰写专家')
 
# 每个 Task 通过 context 引用前一个任务的输出
# 问题:当链超过两层,早期任务的输出可能被截断
classify_task = Task(description='分类工单: {ticket}', agent=classifier, expected_output='分类结果')
retrieve_task = Task(description='检索相关知识', agent=retriever, context=[classify_task], expected_output='知识条目')
write_task = Task(description='生成回复草稿', agent=writer, context=[retrieve_task], expected_output='回复草稿')
# write_task 拿不到 classify_task 的输出,工单 ID 丢了
# ✅ 好做法:用显式状态对象贯穿全流程
from crewai import Agent, Task, Crew
from pydantic import BaseModel
 
class TicketState(BaseModel):
    ticket_id: str
    user_info: dict
    category: str = ''
    knowledge: list[str] = []
    draft: str = ''
 
classifier = Agent(role='分类员', goal='对工单分类', backstory='工单分类专家')
retriever = Agent(role='检索员', goal='检索知识库', backstory='知识库检索专家')
writer = Agent(role='撰写员', goal='生成回复', backstory='客服回复撰写专家')
 
# 所有 Task 共享同一个 state 对象,字段显式传递
state = TicketState(ticket_id='TK-001', user_info={'name': '张三', 'level': 'VIP'})
classify_task = Task(description='分类工单', agent=classifier, expected_output='分类结果')
retrieve_task = Task(description='检索相关知识', agent=retriever, expected_output='知识条目')
write_task = Task(description='生成回复草稿', agent=writer, expected_output='回复草稿')
 
crew = Crew(agents=[classifier, retriever, writer], tasks=[classify_task, retrieve_task, write_task])
# 显式状态确保每个步骤都能读到完整上下文,不依赖隐式传递
result = crew.kickoff(inputs={'state': state.model_dump()})

核心差异在状态是「框架帮你传」还是「你自己管」。隐式传递在 demo 里省代码,在生产里埋地雷。

案例二:AutoGen 的 Token 失控

场景

用 AutoGen 做工单回复生成,一个 Agent 负责起草,一个 Agent 负责审核,通过对话式协作迭代改进。

翻车

两个 Agent 的对话在大部分情况下能在 2-3 轮收敛。但遇到边界 case(比如工单描述模糊),审核 Agent 会反复要求起草 Agent 补充信息,对话轮次飙到 15 轮以上。单次工单处理的 token 消耗从预期的 ~2000 涨到 ~8000,日均处理量不变的情况下,API 账单直接翻了三倍。

AutoGen 的会话式架构天然适合这种协作模式,但也天然缺乏对对话轮次和 token 预算的硬约束。

修复

给对话设置 max_turns 硬上限和 token 预算检查器,超限后强制终止并走降级路径。

# ❌ 坏做法:不设对话上限,完全依赖 LLM 自己判断何时收敛
from autogen import AssistantAgent, UserProxyAgent
 
drafter = AssistantAgent(name='起草员', system_message='你是一个工单回复起草员')
reviewer = AssistantAgent(name='审核员', system_message='审核回复质量,不满意就要求修改')
 
# 两个 Agent 自由对话,没有轮次和 token 限制
group_chat = autogen.GroupChat(
    agents=[drafter, reviewer],
    messages=[],
    # 没设 max_round,对话可能无限进行
)
manager = autogen.GroupChatManager(groupchat=group_chat)
# 遇到模糊工单时,reviewer 可能反复要求修改,token 失控
# ✅ 好做法:设置硬性对话上限 + token 预算回调
from autogen import AssistantAgent
import autogen
 
def token_budget_hook(messages, sender, config):
    """当对话轮次超限或 token 超标时强制终止"""
    if len(messages) >= 6:  # 最多 3 轮对话(每轮 2 条消息)
        return {'terminate': True, 'content': '对话达到上限,使用当前最佳版本'}
    total_tokens = sum(len(m.get('content', '')) for m in messages)
    if total_tokens > 4000:  # token 预算上限
        return {'terminate': True, 'content': 'Token 预算耗尽,使用当前最佳版本'}
    return {'terminate': False}
 
drafter = AssistantAgent(name='起草员', system_message='你是一个工单回复起草员')
reviewer = AssistantAgent(name='审核员', system_message='审核回复质量,最多提两轮修改意见')
 
group_chat = autogen.GroupChat(
    agents=[drafter, reviewer],
    messages=[],
    max_round=6,  # 硬性上限
)
# 注册 token 预算检查,超限自动降级
manager = autogen.GroupChatManager(
    groupchat=group_chat,
    # 对话终止条件不只看 LLM 输出,还要看资源消耗
)

AutoGen 的会话模型很强大,但强大意味着你需要自己设围栏。不设围栏的会话式 Agent 在生产环境就是一颗定时炸弹。

案例三:LangGraph 的 Checkpoint 和连接池冲突

场景

LangGraph 内置了基于 checkpoint 的状态持久化机制,支持中断恢复和人工确认节点。我的系统已有 PostgreSQL 连接池(通过 SQLAlchemy 管理),需要让 LangGraph 的 checkpointer 复用现有数据库连接。

翻车

LangGraph 默认的 AsyncPostgresSaver 会自己创建和管理数据库连接。当它和我的 SQLAlchemy 连接池同时运行时,PostgreSQL 的 max_connections 被快速耗尽。在高并发场景下,连接等待超时导致整个 Agent 流程崩溃。

修复

将 LangGraph 的 checkpointer 配置为使用已有的连接池,而不是自建连接。

# ❌ 坏做法:LangGraph 自建连接,和现有连接池互相抢资源
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
 
# AsyncPostgresSaver 内部会创建自己的连接池
# 和 SQLAlchemy 的连接池独立运行,两个池加起来超过 max_connections
checkpointer = AsyncPostgresSaver.from_conn_string(
    'postgresql://user:pass@localhost:5432/agent_db'
)
# 高并发时:两个连接池 + 应用连接 = 数据库连接耗尽
# ✅ 好做法:复用已有的连接池,统一管理连接资源
from langgraph.checkpoint.postgres.aio import AsyncPostgresSaver
from sqlalchemy.ext.asyncio import create_async_engine
 
# 复用应用已有的 async engine,共享连接池配置
engine = create_async_engine(
    'postgresql+asyncpg://user:pass@localhost:5432/agent_db',
    pool_size=10,
    max_overflow=20,
    # 整个应用只有一个连接池,LangGraph checkpointer 也从这里取连接
)
 
checkpointer = AsyncPostgresSaver(engine)
# 连接数可控,不会因为多了一个框架就多出一组连接

这个案例的核心问题是框架的默认行为是否尊重你的系统架构。好的框架应该能嵌入你的基础设施,而不是要求你围绕它重组基础设施。

选型决策流程

把上面踩坑的经验抽象出来,选型可以走这个流程:

流程图画布 · 115%
Mermaid 流程图加载中...

这个流程图的核心逻辑是:先看业务形态决定框架类型,再看系统约束决定具体配置。很多选型只看第一步,忽略了第二步。

主流框架横向对比

架构与定位对比

框架核心范式状态管理适用场景学习曲线
LangGraph有向图编排显式 TypedDict + checkpoint复杂工作流、需要条件分支和持久化陡峭
CrewAI角色化团队协作隐式 context + 可选显式状态角色分工明确的流水线、快速原型平缓
AutoGen (AG2)会话式多 Agent对话历史 + 手动管理迭代式协作、代码生成、Azure 生态中等
LlamaIndex Workflows数据驱动的事件流事件上下文 + 可选持久化RAG 密集型、文档处理中等
OpenAI Agents SDK轻量 Agent 运行时内置 Handoff 机制OpenAI 生态内的多步骤任务平缓
Google ADK声明式 Agent 定义Session 管理Gemini 生态、Google Cloud 部署中等

生产就绪能力对比

能力LangGraphCrewAIAutoGenLlamaIndex
Checkpoint 持久化✅ 内置⚠️ 需插件❌ 需自研⚠️ 需自研
人工确认节点✅ interrupt 机制⚠️ 需自定义✅ UserProxyAgent⚠️ 需自定义
OpenTelemetry 追踪✅ 原生支持⚠️ 需集成⚠️ 需集成✅ 原生支持
多模型切换✅ 通过 LangChain✅ 支持✅ 支持✅ 支持
并行执行✅ 图节点并行⚠️ 任务并行⚠️ 有限支持✅ 事件并行
Token 成本控制✅ 节点粒度⚠️ 角色粒度❌ 会话粒度,难预测⚠️ 事件粒度

工具编排方式对比

框架工具定义方式工具调用控制生态集成丰富度
LangGraph函数注册为图节点显式:在图的边中定义调用时机高(继承 LangChain 生态)
CrewAI装饰器或类继承隐式:由 Agent 角色决定调用时机中(内置常用工具)
AutoGen函数注册到 Agent半隐式:由对话上下文触发中(Azure 生态丰富)
LlamaIndex工具函数 + ToolMetadata事件驱动中(RAG 工具链丰富)

退出成本评估

框架业务逻辑耦合度替换难度降低耦合的方式
LangGraph节点函数纯函数化,图定义和函数分离
CrewAI低→中Agent 角色定义和业务逻辑分离
AutoGen中→高对话模式和业务逻辑分层
LlamaIndexWorkflow 事件处理和核心逻辑解耦

退出成本的关键在于业务逻辑是写在框架的抽象里面还是外面。写在里面的替换成本高,写在外面的替换成本低。

四个编码层面的选型原则

原则一:工具函数保持纯函数

# ❌ 坏做法:工具函数内部持有框架特定的上下文对象
from langgraph.prebuilt import ToolNode
 
def search_knowledge(query: str, config: RunnableConfig) -> str:
    # 工具函数依赖 LangChain 的 RunnableConfig
    # 换框架后这个函数不可复用
    store = config['configurable']['vector_store']
    return store.similarity_search(query)
 
# ✅ 好做法:工具函数是纯函数,框架适配在最外层
def search_knowledge(query: str, store: VectorStore) -> str:
    # 纯函数,不依赖任何框架特定的上下文
    return store.similarity_search(query)
 
# 在 LangGraph 节点中适配
def knowledge_node(state: GraphState) -> dict:
    result = search_knowledge(state['query'], app_state.vector_store)
    return {'knowledge': result}

原则二:状态定义和框架定义分离

# ❌ 坏做法:状态定义嵌在框架的图定义里
from langgraph.graph import StateGraph
from typing import TypedDict
 
class GraphState(TypedDict):
    # 状态类型和图编排绑死,换框架要重写状态定义
    ticket_id: str
    category: str
    knowledge: list[str]
    draft: str
 
graph = StateGraph(GraphState)
 
# ✅ 好做法:状态是独立的领域模型,框架只是状态的消费者
from pydantic import BaseModel
 
class TicketContext(BaseModel):
    # 领域模型独立于任何框架
    ticket_id: str
    category: str = ''
    knowledge: list[str] = []
    draft: str = ''
 
# 框架适配层:把领域模型映射到框架状态
# 换框架时只需要重写这一层,领域模型不变
def to_graph_state(ctx: TicketContext) -> dict:
    return ctx.model_dump()

原则三:可观测性不依赖框架内置能力

# ❌ 坏做法:只依赖框架的 verbose 模式做调试
# CrewAI
crew = Crew(..., verbose=True)
# verbose=True 在开发环境有用,生产环境日志不可结构化、不可检索
 
# ✅ 好做法:在工具函数层注入 OpenTelemetry span
from opentelemetry import trace
 
def search_knowledge(query: str, store: VectorStore) -> str:
    tracer = trace.get_tracer('ticket-agent')
    with tracer.start_as_current_span('knowledge_search') as span:
        span.set_attribute('query', query)
        results = store.similarity_search(query)
        span.set_attribute('result_count', len(results))
        return results
# 无论底层框架怎么换,可观测性始终跟着业务逻辑走

原则四:错误恢复策略由业务定义,不由框架决定

# ❌ 坏做法:依赖框架的默认重试策略
# AutoGen 默认不限制重试,LangGraph 默认不自动重试
# 框架的默认行为不一定适合你的业务场景
 
# ✅ 好做法:在工具函数层定义明确的重试和降级策略
from tenacity import retry, stop_after_attempt, fallback
 
@retry(stop=stop_after_attempt(3), reraise=True)
def call_external_api(endpoint: str, payload: dict) -> dict:
    # 业务层面的重试策略,和框架无关
    response = requests.post(endpoint, json=payload, timeout=10)
    response.raise_for_status()
    return response.json()
 
@fallback(fallback_value={'status': 'degraded', 'message': '使用缓存结果'})
def call_external_api_safe(endpoint: str, payload: dict) -> dict:
    return call_external_api(endpoint, payload)

选型检查清单

阶段一:需求梳理

  • 画出完整的 Agent 工作流拓扑(节点、边、条件分支)
  • 标注哪些节点需要人工确认
  • 评估单次执行的 token 预算上限
  • 确认是否需要跨会话的状态持久化
  • 列出所有需要接入的外部工具和 API

阶段二:框架初筛

  • 用每个候选框架跑通一个最小可用流程
  • 验证状态管理是否支持你的数据传递模式
  • 验证工具接入是否和你的认证/网络环境兼容
  • 检查框架是否有 OpenTelemetry 或等价的可观测性集成
  • 评估框架的社区活跃度(Star 数、最近 commit 频率、issue 响应速度)

阶段三:生产验证

  • 在测试环境模拟生产并发量,检查资源消耗
  • 检查框架的连接管理是否能复用现有数据库连接池
  • 模拟 Agent 异常中断,验证状态恢复是否符合预期
  • 追踪一次完整执行的 token 消耗,对比预算
  • 编写降级脚本:如果框架不可用,业务是否有 fallback 路径

阶段四:退出策略

  • 业务逻辑是否和框架代码物理隔离(不同目录/模块)
  • 工具函数是否是纯函数,不依赖框架特定的上下文对象
  • 状态定义是否是独立的领域模型,不嵌在框架的图/角色定义里
  • 可观测性是否在业务层实现,不依赖框架的 verbose 模式

最后的判断

框架选型的本质是评估两件事:它的抽象能不能表达你的业务逻辑,它的边界能不能融入你的系统架构。

示例流畅只是说明抽象能力过关。能不能融入系统架构、出了问题能不能排查、用错了能不能替换,这些才是生产环境真正在意的东西。

不要因为一个框架的 demo 跑得漂亮就选它。先问自己三个问题:它的状态管理方式你的业务能接受吗?它的工具编排方式和你的系统兼容吗?如果半年后需要换掉它,代价有多大?三个问题都有明确答案的时候,才是做决定的时候。

参考资料

评论 0

0 / 1000