1. 从手动实践到框架开发:为什么必须跨过这道坎
如果你正在做大模型应用开发,或者刚接触智能体(Agent)这个方向,大概率经历过这样一个阶段:一开始用最原始的方式,手动拼接提示词、手动管理对话历史、手动调用工具函数、手动解析模型返回结果。一个简单的任务跑通之后,你会觉得“好像也没那么难”。但当你试图把两三个工具串起来、加上条件判断、再加上失败重试和状态记录的时候,代码就会迅速膨胀成一团乱麻。这个从手动实践到框架开发的转折点,几乎是每个Agent开发者都会遇到的瓶颈。
我自己在这个阶段卡了很久。最早做的一个小项目是帮团队内部做一个文档问答助手,需求很简单:用户提问,模型判断是否需要检索,如果需要就调用检索工具,拿到结果后再生成回答。手动写的时候,我用了一个while循环加一堆if-else,状态存在一个字典里,工具调用靠正则表达式从模型输出里抠JSON。刚开始只有两个工具,勉强能跑。后来加了第三个工具、第四个工具,还要支持多轮对话和上下文压缩,代码直接失控——一个文件八百多行,改一处逻辑要花半小时确认不会影响其他地方。这就是典型的“手动实践的天花板”。
框架开发解决的正是这个问题。它把Agent运行过程中反复出现的模式抽象出来:状态管理、节点编排、条件路由、工具注册、记忆持久化、错误处理。你不再需要自己造轮子,而是把精力放在业务逻辑本身。目前主流的Agent开发框架里,LangGraph和AutoGen是两个绕不开的选择。LangGraph走的是图编排路线,把Agent的执行流程建模成状态图,节点是计算步骤,边是流转条件,特别适合需要精细控制流程的场景。AutoGen则更偏向多智能体对话协作,让多个Agent通过消息传递来协同完成任务。两者各有侧重,后面我会详细拆解。
这篇文章适合谁看?如果你已经用Python写过一些大模型调用代码,对提示词工程有基本了解,但还没系统使用过Agent框架,那这篇内容就是为你准备的。我会从手动实践的痛点讲起,一步步拆解框架开发的核心思路,然后用LangGraph和AutoGen分别给出可运行的代码示例,最后分享我在实际项目中踩过的坑和排查技巧。整篇内容基于我自己的项目经验,代码都可以直接复制运行,环境依赖也会写清楚。
2. 手动实践阶段的核心痛点拆解
2.1 状态管理为什么最容易失控
手动做Agent开发,第一个绕不过去的问题就是状态管理。一个Agent在执行任务时,需要维护的状态包括:对话历史、当前步骤、已调用的工具及结果、中间变量、错误信息、重试次数等等。刚开始你可能用一个字典就搞定了,但随着逻辑变复杂,这个字典会变成什么都能往里塞的“垃圾堆”。更麻烦的是,当你要在多个函数之间传递这个状态时,Python的字典是可变对象,某个函数不小心修改了它,另一个函数读到的就是被污染的数据。这种bug排查起来极其痛苦,因为错误现场和错误原因往往隔了好几个调用层级。
我遇到过一个典型场景:Agent在调用检索工具失败后,应该把错误信息写入状态并触发重试逻辑。但因为状态字典在工具函数里被直接修改了,重试逻辑读到的错误信息是上一次的残留值,导致重试一直失败。这种问题在手动实践中非常普遍,根本原因是你没有对状态变更做集中管理。框架开发的做法是定义一个明确的状态结构(比如TypedDict或Pydantic模型),所有状态变更都通过框架提供的机制进行,每次变更都有迹可循。
2.2 流程编排的复杂度爆炸
手动写流程编排,本质上是在用代码模拟一个状态机。你会有各种条件分支:如果模型返回了工具调用请求,就走工具执行分支;如果工具执行成功,就走结果整合分支;如果失败,就走重试或降级分支。这些分支用if-else写出来,短流程还能看,一旦超过五个节点,代码的可读性就急剧下降。更致命的是,你很难在代码层面直观地看到整个流程的全貌——哪个节点连哪个节点,什么条件下走哪条边,全靠脑补。
LangGraph这类框架的价值就在这里。它让你用图的方式定义流程:节点是函数,边是流转规则,条件边用函数返回值来决定走向。整个Agent的执行逻辑变成一张可视化的图,你一眼就能看出流程从哪里开始、经过哪些节点、在哪里分叉、在哪里结束。这种表达方式不仅让代码更清晰,也让团队协作变得容易——新人拿到代码,看图就能理解逻辑,不用逐行读if-else。
2.3 工具注册与调用的重复劳动
手动实践中,每加一个工具,你都要做几件事:写工具函数、写工具描述(给模型看的schema)、在提示词里告诉模型有哪些工具可用、解析模型返回的工具调用请求、根据工具名分发到对应的函数、处理工具返回结果并塞回对话历史。这些步骤在每个工具上都要重复一遍,而且很容易出错。比如工具描述的格式写错了,模型就识别不了;工具名和函数名对不上,分发逻辑就找不到对应函数。
框架开发通常提供工具注册机制,你只需要用装饰器标注一个函数是工具,框架自动生成schema、自动处理调用分发、自动把结果写回状态。这省下来的不仅是代码量,更是心智负担。你可以把注意力放在工具本身的逻辑上,而不是工具和模型之间的胶水代码上。
2.4 记忆与上下文管理的困境
Agent需要记忆,这是共识。但记忆怎么存、存多少、什么时候压缩、什么时候检索,手动实现起来非常琐碎。最简单的做法是把所有对话历史都塞进上下文,但模型有token限制,对话一长就爆了。你需要实现滑动窗口、摘要压缩、关键信息提取等策略。每种策略都有自己的适用场景和副作用,手动实现和调试的成本很高。
框架开发一般会提供记忆管理的抽象层。比如LangGraph支持checkpointer机制,可以自动持久化每一步的状态,支持从任意检查点恢复执行。AutoGen则有对话历史管理和上下文窗口控制的内置逻辑。这些机制不能完全替代你对记忆策略的思考,但至少把基础设施搭好了,你只需要配置参数和选择策略。
3. 框架开发的核心思路与选型考量
3.1 LangGraph的图编排模型解析
LangGraph的核心思想是把Agent的执行流程建模成一个有向图。图由节点和边组成,节点是计算步骤(可以是调用模型、执行工具、处理数据等),边定义了节点之间的流转关系。状态在节点之间传递,每个节点接收当前状态,返回状态更新。这种模型非常贴近人类对流程的直觉认知,也便于调试和可视化。
LangGraph的几个关键概念需要理解清楚。首先是StateGraph,它是图的容器,你通过add_node和add_edge往里添加节点和边。其次是状态定义,通常用TypedDict或Pydantic模型来定义,每个字段代表流程中需要维护的一项数据。第三是条件边,通过add_conditional_edges添加,它接收一个函数,函数的返回值决定下一步走哪个节点。最后是checkpointer,用于持久化状态,支持中断恢复和人工介入。
LangGraph和LangChain的关系经常被问到。简单说,LangChain是一个大而全的LLM应用开发框架,提供了模型调用、提示词模板、工具、检索器等组件。LangGraph是LangChain生态中的一个子项目,专注于Agent的流程编排。你可以单独使用LangGraph,也可以和LangChain的组件配合使用。LangGraph比LangChain更底层、更灵活,适合需要精细控制流程的场景。LangChain的AgentExecutor虽然也能用,但它的流程是固定的,你很难插入自定义逻辑。LangGraph则把流程的控制权完全交给你。
3.2 AutoGen的多智能体协作模式
AutoGen的出发点和LangGraph不同。它关注的是多个Agent之间的对话协作。在AutoGen里,你可以定义多个角色,每个角色有自己的系统提示词和工具集,它们通过消息传递来协同完成任务。比如一个典型的模式是:用户代理负责理解需求,助手代理负责生成方案,批评者代理负责审查方案,三者循环对话直到达成共识。
AutoGen的优点是上手快,多Agent协作的样板代码很少,你定义好角色和初始消息,它就能自动跑起来。但它的缺点也在这里——流程控制相对粗放,你很难精确指定“在什么条件下切换到哪个Agent”。AutoGen适合那些任务边界清晰、可以通过对话自然推进的场景,比如代码生成加审查、多轮辩论、任务分解与执行。如果你的流程需要严格的状态机和条件分支,LangGraph会更合适。
3.3 选型对比与决策依据
选LangGraph还是AutoGen,我的经验是看三个维度。第一,流程是否需要精细控制。如果你的Agent有明确的状态流转和条件分支,选LangGraph。如果任务可以通过多轮对话自然推进,选AutoGen。第二,是否需要多Agent协作。单Agent任务用LangGraph更简洁,多Agent协作场景AutoGen更省事。第三,团队的技术背景。LangGraph的概念更多,学习曲线稍陡,但一旦理解就非常灵活。AutoGen上手快,但深入定制时可能会遇到框架限制。
实际项目中,两者也可以结合使用。比如用LangGraph做外层流程编排,在某个节点里调用AutoGen的多Agent对话来完成子任务。这种混合模式在复杂项目中很常见。
| 对比维度 | LangGraph | AutoGen |
|---|---|---|
| 核心模型 | 状态图编排 | 多智能体对话 |
| 流程控制 | 精细,支持条件边和循环 | 相对粗放,靠对话推进 |
| 状态管理 | 显式状态定义,支持持久化 | 对话历史管理 |
| 学习曲线 | 中等偏陡 | 较平缓 |
| 适用场景 | 复杂流程、需要精确控制 | 多Agent协作、对话式任务 |
| 与LangChain关系 | 同生态,可配合使用 | 独立框架 |
4. 实操过程与核心环节实现
4.1 环境准备与依赖安装
先把环境搭起来。我用的Python版本是3.11,LangGraph和AutoGen都支持3.9以上。建议用虚拟环境,避免依赖冲突。
python -m venv agent-env source agent-env/bin/activate # Windows用 agent-env\Scripts\activate pip install langgraph langchain-openai autogen-agentchat autogen-ext[openai]LangGraph的核心包是langgraph,配合langchain-openai来调用模型。AutoGen的新版本拆成了autogen-agentchat和autogen-ext,安装时注意版本。我实测下来,LangGraph 0.2.x和AutoGen 0.4.x的API比较稳定,建议锁定版本。
pip install langgraph==0.2.60 langchain-openai==0.2.14 pip install autogen-agentchat==0.4.5 autogen-ext[openai]==0.4.5模型方面,我用的是OpenAI的接口,你也可以换成其他兼容OpenAI协议的服务。需要设置环境变量:
export OPENAI_API_KEY="你的密钥" export OPENAI_BASE_URL="你的接口地址"注意:不要把密钥硬编码在代码里,用环境变量或配置文件管理。我见过有人把密钥提交到代码仓库,结果被扫到后产生意外费用,这个坑一定要避开。
4.2 用LangGraph搭建一个带工具调用的Agent
先定义一个简单的场景:Agent可以调用两个工具,一个是查询天气,一个是计算数学表达式。用户提问后,Agent判断是否需要调用工具,如果需要就调用,拿到结果后生成最终回答。
第一步,定义状态结构。我用TypedDict来定义,包含消息列表和当前步骤计数。
from typing import Annotated, TypedDict from langgraph.graph.message import add_messages class AgentState(TypedDict): messages: Annotated[list, add_messages] step_count: int这里messages字段用了add_messages注解,这是LangGraph提供的消息合并机制,新消息会追加到列表而不是覆盖。step_count用来记录执行步数,防止无限循环。
第二步,定义工具函数。用LangChain的tool装饰器来标注。
from langchain_core.tools import tool @tool def get_weather(city: str) -> str: """查询指定城市的天气""" weather_data = { "北京": "晴,25度", "上海": "多云,28度", "深圳": "阵雨,30度" } return weather_data.get(city, f"暂时没有{city}的天气数据") @tool def calculate(expression: str) -> str: """计算数学表达式,支持加减乘除""" try: result = eval(expression, {"__builtins__": {}}, {}) return f"计算结果:{result}" except Exception as e: return f"计算失败:{str(e)}"注意:eval有安全风险,生产环境要用更安全的表达式解析库,比如asteval或sympy。这里为了演示简化了。
第三步,绑定工具到模型。
from langchain_openai import ChatOpenAI llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) tools = [get_weather, calculate] llm_with_tools = llm.bind_tools(tools)第四步,定义节点函数。两个节点:一个调用模型,一个执行工具。
from langchain_core.messages import ToolMessage def call_model(state: AgentState): messages = state["messages"] response = llm_with_tools.invoke(messages) return {"messages": [response], "step_count": state.get("step_count", 0) + 1} def call_tools(state: AgentState): messages = state["messages"] last_message = messages[-1] tool_messages = [] for tool_call in last_message.tool_calls: tool_name = tool_call["name"] tool_args = tool_call["args"] if tool_name == "get_weather": result = get_weather.invoke(tool_args) elif tool_name == "calculate": result = calculate.invoke(tool_args) else: result = f"未知工具:{tool_name}" tool_messages.append(ToolMessage(content=str(result), tool_call_id=tool_call["id"])) return {"messages": tool_messages}第五步,定义路由函数,决定模型调用后是走工具执行还是结束。
def should_continue(state: AgentState): messages = state["messages"] last_message = messages[-1] if hasattr(last_message, "tool_calls") and last_message.tool_calls: return "tools" return "end"第六步,组装图。
from langgraph.graph import StateGraph, START, END workflow = StateGraph(AgentState) workflow.add_node("agent", call_model) workflow.add_node("tools", call_tools) workflow.add_edge(START, "agent") workflow.add_conditional_edges("agent", should_continue, {"tools": "tools", "end": END}) workflow.add_edge("tools", "agent") app = workflow.compile()第七步,运行测试。
result = app.invoke({"messages": [("user", "北京天气怎么样?顺便帮我算一下 23*47")], "step_count": 0}) for msg in result["messages"]: print(f"{msg.type}: {msg.content}")跑下来你会看到,Agent先调用get_weather查北京天气,再调用calculate算23*47,最后整合两个结果生成回答。整个流程在图上清晰可见:START -> agent -> tools -> agent -> END。如果模型判断不需要工具,直接走agent -> END。
4.3 用AutoGen实现多Agent协作
同样的场景,用AutoGen的多Agent模式来实现。定义三个Agent:用户代理负责发起请求,助手代理负责调用工具,执行代理负责实际执行工具。
from autogen_agentchat.agents import AssistantAgent from autogen_agentchat.teams import RoundRobinGroupChat from autogen_agentchat.conditions import MaxMessageTermination from autogen_ext.models.openai import OpenAIChatCompletionClient model_client = OpenAIChatCompletionClient(model="gpt-4o-mini") async def get_weather_func(city: str) -> str: weather_data = {"北京": "晴,25度", "上海": "多云,28度", "深圳": "阵雨,30度"} return weather_data.get(city, f"暂时没有{city}的天气数据") async def calculate_func(expression: str) -> str: try: result = eval(expression, {"__builtins__": {}}, {}) return f"计算结果:{result}" except Exception as e: return f"计算失败:{str(e)}" assistant = AssistantAgent( name="assistant", model_client=model_client, tools=[get_weather_func, calculate_func], system_message="你是一个助手,可以查询天气和计算数学表达式。需要时调用工具。" ) termination = MaxMessageTermination(max_messages=10) team = RoundRobinGroupChat([assistant], termination_condition=termination) async def main(): result = await team.run(task="北京天气怎么样?顺便帮我算一下 23*47") for msg in result.messages: print(f"{msg.source}: {msg.content}") import asyncio asyncio.run(main())AutoGen的代码量明显更少,但流程控制也更粗放。它靠对话轮次和终止条件来推进,适合任务边界清晰的场景。如果你需要精确控制每一步的流转条件,LangGraph更合适。
4.4 状态持久化与中断恢复
LangGraph的checkpointer机制是生产环境必备的。它可以把每一步的状态存到数据库或内存中,支持从任意检查点恢复执行。这在长流程Agent中非常有用——如果某一步失败了,不用从头重跑。
from langgraph.checkpoint.memory import MemorySaver memory = MemorySaver() app = workflow.compile(checkpointer=memory) config = {"configurable": {"thread_id": "user-001"}} result = app.invoke({"messages": [("user", "北京天气怎么样?")], "step_count": 0}, config)用thread_id来区分不同用户的会话。生产环境可以把MemorySaver换成SqliteSaver或PostgresSaver,状态就持久化了。我实测下来,SqliteSaver在中小规模场景完全够用,配置也简单。
from langgraph.checkpoint.sqlite import SqliteSaver with SqliteSaver.from_conn_string("checkpoints.db") as memory: app = workflow.compile(checkpointer=memory) config = {"configurable": {"thread_id": "user-001"}} result = app.invoke({"messages": [("user", "北京天气怎么样?")], "step_count": 0}, config)注意:checkpointer存储的是完整状态,包括所有消息。对话长了之后,数据库会膨胀。建议定期清理旧会话,或者实现状态压缩逻辑。
5. 常见问题与排查技巧实录
5.1 模型不调用工具怎么办
这是最常见的问题。模型返回的响应里没有tool_calls,直接给了文本回答。原因通常有三个:工具描述不够清晰、提示词没有引导模型使用工具、模型本身能力不足。
排查步骤:先打印模型的原始响应,看它到底返回了什么。如果响应里有工具调用但格式不对,检查工具schema是否正确。如果响应里完全没有工具调用意图,优化工具描述和系统提示词。我一般会在系统提示词里明确写“当用户询问天气或需要计算时,必须调用对应工具,不要自己编造答案”。
还有一个坑是工具名冲突。如果你定义了两个同名工具,或者工具名和模型内置的某些标识符冲突,模型可能会混淆。工具名用下划线命名法,保持唯一性。
5.2 无限循环怎么破
Agent在agent和tools之间来回跳,停不下来。原因通常是模型反复调用同一个工具,或者工具返回的结果让模型认为还需要继续调用。解决办法有两个:一是加步数限制,在状态里记录step_count,超过阈值就强制结束;二是在路由函数里加判断,如果连续两次调用同一个工具且参数相同,就终止。
def should_continue(state: AgentState): if state.get("step_count", 0) >= 10: return "end" messages = state["messages"] last_message = messages[-1] if hasattr(last_message, "tool_calls") and last_message.tool_calls: return "tools" return "end"提示:步数阈值不要设太小,否则复杂任务跑不完。我一般设10到15步,根据任务复杂度调整。
5.3 状态字段丢失或类型错误
LangGraph的状态更新是合并式的,节点返回的字典会合并到当前状态。如果你在节点里返回了一个状态里没有的字段,它会被忽略。如果返回的字段类型和定义的不一致,可能会报错。排查时先检查状态定义,确认所有字段都声明了。然后用print大法,在每个节点入口打印当前状态,看数据是否符合预期。
另一个常见问题是messages字段没有用add_messages注解,导致新消息覆盖旧消息。这个错误很隐蔽,因为流程能跑通,但模型看不到历史对话。一定要确认messages字段加了Annotated[list, add_messages]。
5.4 AutoGen的终止条件配置
AutoGen如果不配终止条件,对话会一直进行下去。常见的终止条件有MaxMessageTermination(最大消息数)、TextMentionTermination(提到特定词终止)、TokenUsageTermination(token用量超限终止)。我一般组合使用:
from autogen_agentchat.conditions import MaxMessageTermination, TextMentionTermination termination = MaxMessageTermination(max_messages=20) | TextMentionTermination("TERMINATE")用|操作符组合多个条件,满足任一就终止。TextMentionTermination让Agent在任务完成时输出TERMINATE,这样能自然结束。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 模型不调用工具 | 工具描述不清、提示词未引导 | 打印原始响应 | 优化描述和提示词 |
| 无限循环 | 模型反复调用同一工具 | 查看消息历史 | 加步数限制和重复检测 |
| 状态字段丢失 | 未在状态中声明 | 检查状态定义 | 补全字段声明 |
| 消息覆盖 | messages未加add_messages | 检查注解 | 添加Annotated注解 |
| AutoGen不终止 | 未配终止条件 | 检查termination | 组合多个终止条件 |
5.5 性能优化的几个实操心得
第一个心得是减少不必要的模型调用。在路由函数里能判断的逻辑,不要交给模型。比如工具返回结果后,如果结果明确表示失败,可以直接走错误处理分支,不用再让模型判断。
第二个心得是控制上下文长度。对话历史长了之后,每次模型调用的token消耗很大。我一般会保留最近10轮对话,更早的做摘要压缩。LangGraph可以在节点里实现这个逻辑,AutoGen有内置的上下文窗口管理。
第三个心得是工具执行加超时。外部API调用可能卡住,给工具函数加超时机制,避免整个Agent挂起。Python的signal模块或concurrent.futures都可以实现。
import signal def timeout_handler(signum, frame): raise TimeoutError("工具执行超时") def call_tool_with_timeout(tool_func, args, timeout=10): signal.signal(signal.SIGALRM, timeout_handler) signal.alarm(timeout) try: result = tool_func.invoke(args) signal.alarm(0) return result except TimeoutError: return "工具执行超时,请稍后重试"注意:signal只在主线程有效,如果你在多线程环境里跑,要用concurrent.futures的ThreadPoolExecutor加timeout参数。
6. 从框架开发再往前一步的思考
框架开发解决了手动实践的很多痛点,但它不是银弹。用了框架之后,你依然需要理解Agent的基本原理——模型怎么决策、工具怎么调用、状态怎么流转。框架只是把这些原理封装成了更易用的接口,底层的逻辑没有变。我见过一些人上来就用框架,结果遇到问题完全不知道从哪里排查,因为他不理解框架背后做了什么。
我的建议是,先手动写一遍最简单的Agent循环,理解每一步在干什么。然后再用框架重写一遍,对比两者的差异。这样你既有了底层认知,又有了工程效率。LangGraph和AutoGen都是很好的工具,但它们不是唯一的选择。随着这个领域的快速发展,新的框架和模式会不断出现。重要的是掌握核心概念——状态、节点、边、工具、记忆、路由——这些概念在不同框架里是相通的。
另外,Agent的安全性和可控性值得持续关注。工具调用的权限控制、敏感操作的二次确认、输出内容的过滤,这些在生产环境里都是必须考虑的。框架提供了一些机制,但最终的责任还是在开发者身上。我在项目里一般会给工具调用加一层权限校验,高风险操作需要人工确认后才执行。这个逻辑可以用LangGraph的中断机制实现——在关键节点前暂停,等待人工输入后再继续。
最后分享一个我常用的调试技巧:把Agent的每一步执行都打上日志,包括输入状态、模型响应、工具调用、输出状态。日志用结构化格式(比如JSON),方便后续分析和回放。LangGraph的checkpointer天然支持这个,AutoGen也有消息历史记录。有了完整的执行日志,排查问题会快很多。