LangGraph实战:从ReAct范式到复杂Agent工作流构建

📅 2026/8/7 6:24:13
LangGraph实战:从ReAct范式到复杂Agent工作流构建
1. 项目概述为什么我们需要深入理解Agent与LangGraph如果你最近在折腾大语言模型应用尤其是想让它不只是个“聊天机器人”而是能帮你自动处理一些复杂任务那么“Agent”这个词你一定不陌生。它就像一个智能的“执行者”能理解你的意图规划步骤调用工具最终完成任务。而LangChain作为构建这类应用最流行的框架之一其Agent模块无疑是核心中的核心。但当你真正上手时可能会发现简单的Agent调用API还行一旦任务流程复杂起来涉及到多步骤决策、状态管理、循环或并行执行代码就会变得一团乱麻难以维护和调试。这正是“LangGraph”出现的背景。它不是要取代LangChain而是LangChain生态中一个专门用于构建复杂、有状态、多智能体工作流的库。你可以把它理解为给Agent加上了“流程图”和“中央控制器”。今天我们就抛开那些官方文档里晦涩的定义从一个实际开发者的角度掰开揉碎了讲讲Agent的底层架构思想以及LangGraph是如何解决这些痛点的。无论你是刚接触LangChain的新手还是已经用过Agent但总觉得不够得心应手的老手这篇文章都会带你从“会用”到“懂原理”再到“能设计”。2. Agent架构深度解析从ReAct范式到工程实践在直接敲代码之前我们必须先搞清楚Agent到底是怎么“思考”和“行动”的。这决定了我们后续使用LangGraph时每一个节点和边应该承载什么逻辑。2.1 核心心智模型ReActReason ActReAct是目前最主流的Agent推理框架。它的思想非常直观让模型像人一样交替进行“思考”和“行动”。Reason思考分析当前状况决定下一步要做什么。例如“用户想查天气我需要知道城市。我应该先问用户所在城市。”Act行动执行思考后决定的动作。这可能是调用一个工具如搜索API也可能是直接给用户一个回复如“请问您在哪个城市”。这个过程会循环进行直到任务完成或达到停止条件。在代码层面LangChain的Agent本质上就是一个循环在每一步将当前对话历史、工具描述、任务目标拼接到一个Prompt中交给LLM。LLM输出一个格式化的字符串其中包含了“思考”和“要调用的工具及输入”。Agent解析这个输出调用对应的工具。将工具执行的结果作为新的“观察”加入到对话历史中。回到第1步继续循环。注意很多初学者会混淆“Agent”和“Chain”。简单来说Chain是确定性的流水线A-B-C而Agent引入了基于LLM的决策点路径是不确定的。一个复杂的Agent内部可能包含多个Chain。2.2 关键组件拆解Agent并非铁板一块一个完整的Agent系统通常由以下几个部分构成理解它们对后续使用LangGraph至关重要LLM核心负责推理和决策的大脑。它的Prompt工程直接决定了Agent的“性格”和能力边界。你需要精心设计Prompt告诉它有哪些工具、应该以什么格式输出、遵循什么规则。工具集Agent的手和脚。可以是搜索引擎、计算器、数据库查询、API调用甚至是另一个函数或Chain。工具的定义需要清晰包括名称、描述、参数schema这样LLM才能正确理解和使用它们。代理类型在LangChain中AgentType如ZERO_SHOT_REACT_DESCRIPTION,OPENAI_FUNCTIONS本质上是预定义的Prompt模板和输出解析器的组合。它规定了LLM与工具交互的“协议”。记忆Agent的短期工作记忆和长期知识。这不仅仅是聊天历史还包括在任务执行过程中产生的中间状态、变量等。简单的Agent可能只用ConversationBufferMemory而复杂工作流需要更精细的状态管理——这正是LangGraph的强项。输出解析器负责将LLM那自由奔放的文本输出解析成程序能理解的结构化数据比如下一个要执行的动作AgentAction或最终答案AgentFinish。实操心得在构建自己的Agent时我强烈建议先从最简单的ZERO_SHOT_REACT_DESCRIPTION代理类型和少数几个工具开始。不要一上来就追求全自动。先手动模拟几次ReAct循环观察LLM在每一步的输入和输出你会发现很多问题都出在Prompt描述不清或工具定义模糊上。比如工具描述里最好包含清晰的示例LLM的模仿能力很强。2.3 经典Agent的局限性当你用LangChain的标准Agent跑通一个简单例子后可能会想“这很棒但我的需求更复杂。” 这时你就会撞上传统Agent架构的几堵墙状态管理混乱复杂任务有多个阶段每个阶段会产生不同的数据。这些数据如何在循环中传递、更新、持久化用全局变量那会很快失控。控制流单一主要是while循环难以实现“如果条件A成立则跳转到步骤X否则继续步骤Y”这样的分支逻辑更别提并行执行了。调试困难当Agent卡住或出错时你很难定位问题出在哪个环节是LLM理解错了工具返回异常还是状态被意外覆盖了多智能体协作如何让多个Agent各司其职相互通信共同完成一个任务用标准Agent组合通信和协调逻辑会散落在各处。这些痛点呼唤着一个更强大、更结构化的编排框架。于是LangGraph登场了。3. LangGraph核心概念用“图”来编排智能体LangGraph的思想非常巧妙将工作流抽象为一个有向图。图中的节点代表一个执行单元可以是调用LLM、运行工具、执行函数边代表执行路径和条件。这种模型天然适合描述带有分支、循环、并行和状态的工作流。3.1 核心抽象StateGraph这是LangGraph最核心的类。你需要定义一个State状态它是一个字典或Pydantic模型包含了工作流运行过程中所有需要传递和修改的数据。然后你创建一个StateGraph实例并为其添加节点和边。from langgraph.graph import StateGraph, END from typing import TypedDict, Annotated from langchain_core.messages import HumanMessage import operator # 1. 定义状态。这是工作流的“共享内存”。 class AgentState(TypedDict): messages: Annotated[list, operator.add] # 对话消息列表operator.add表示追加 user_query: str # 用户原始问题 has_searched: bool # 是否已执行搜索 search_result: str # 搜索结果 # 2. 创建图 workflow StateGraph(AgentState)Annotated[list, operator.add]这个语法是LangGraph状态更新的关键。它指定了对messages字段的更新方式是“追加”而不是覆盖。这确保了对话历史能完整保留。3.2 节点与边构建工作流逻辑节点是一个普通的函数它接收当前完整的State执行一些操作然后返回一个包含要更新字段的字典。def search_node(state: AgentState): 搜索节点调用搜索工具 query state[‘user_query‘] # 假设我们有一个搜索工具 result call_search_api(query) # 返回要更新的状态部分 return {“search_result“: result, “has_searched“: True} def llm_node(state: AgentState): LLM节点分析结果并生成回复 if state[‘has_searched‘]: context state[‘search_result‘] else: context “无额外信息“ prompt f“基于以下信息回答问题{context}\n问题{state[‘user_query‘]}“ # 调用LLM response call_llm(prompt) # 将回复添加到消息历史 new_message AIMessage(contentresponse) return {“messages“: [new_message]} # 将函数添加为节点 workflow.add_node(“search“, search_node) workflow.add_node(“generate“, llm_node)边决定了节点执行的顺序。分为两种起始边workflow.set_entry_point(“search”)指定工作流从哪个节点开始。普通边workflow.add_edge(“search“, “generate“)表示search节点执行完后无条件地进入generate节点。条件边这是实现分支逻辑的关键。它连接一个节点到一个函数该函数根据State决定下一步去哪个节点。def should_search(state: AgentState) - str: “““决策函数判断是否需要搜索”“” # 这里可以是一些简单的规则也可以是另一个LLM调用 if “天气“ in state[‘user_query‘] or “新闻“ in state[‘user_query‘]: return “search“ # 需要搜索前往search节点 else: return “generate“ # 直接生成前往generate节点 # 设置起始节点 workflow.set_entry_point(“router“) # 添加一个路由节点可能只是个空节点或决策节点 workflow.add_node(“router“, router_node) # 从router节点出发根据should_search函数的返回值决定去向 workflow.add_conditional_edges( “router“, should_search, # 决策函数 { “search“: “search“, # 如果返回“search”跳转到search节点 “generate“: “generate“ # 如果返回“generate”跳转到generate节点 } ) # 设置其他边 workflow.add_edge(“search“, “generate“) workflow.add_edge(“generate“, END) # END是一个特殊的终止节点3.3 编译与运行让图动起来定义好图和状态后需要将其编译成一个可执行的对象。# 编译图 app workflow.compile() # 运行图传入初始状态 initial_state AgentState( messages[HumanMessage(content“你好今天北京天气怎么样“)], user_query“今天北京天气怎么样“, has_searchedFalse, search_result““ ) final_state app.invoke(initial_state) print(final_state[“messages“][-1].content)app.invoke()会从入口点开始根据边的定义和状态依次执行节点直到到达END节点。你可以通过app.get_graph().draw_mermaid()生成流程图可视化你的工作流这对于复杂流程的调试和沟通至关重要。注意事项在定义状态时务必想清楚哪些数据是“只增不减”的如消息历史哪些是会被覆盖的如临时计算结果。Annotated注解是保证数据合并行为符合预期的关键用错了会导致状态混乱。4. 实战构建一个带审核流程的客服Agent理论说再多不如动手。我们设计一个稍微复杂的场景一个客服Agent它需要先判断用户问题类型如果是技术问题先查知识库再生成回答并且生成的回答需要经过一个“审核节点”检查是否包含敏感词或不准确信息审核不通过则返回重写。4.1 定义状态与节点from typing import TypedDict, Annotated, Literal import operator from langchain_core.messages import HumanMessage, AIMessage, SystemMessage from langgraph.graph import StateGraph, END class CustomerServiceState(TypedDict): messages: Annotated[list, operator.add] user_input: str problem_type: Literal[“technical“, “billing“, “general“, None] # 问题类型 kb_result: str # 知识库查询结果 draft_answer: str # 草稿回答 needs_review: bool # 是否需要审核 review_feedback: str # 审核反馈 final_answer: str # 最终答案 step_log: Annotated[list, operator.add] # 步骤日志用于调试 def classify_problem_node(state: CustomerServiceState): “““问题分类节点”“” log f“步骤1对用户输入‘{state[‘user_input‘]}‘进行分类“ # 这里简化处理实际中可能用一个小型LLM或分类模型 input_lower state[‘user_input‘].lower() if “error“ in input_lower or “bug“ in input_lower or “安装“ in input_lower: p_type “technical“ elif “fee“ in input_lower or “charge“ in input_lower or “账单“ in input_lower: p_type “billing“ else: p_type “general“ log f“判断为‘{p_type}‘类型“ return {“problem_type“: p_type, “step_log“: [log]} def query_kb_node(state: CustomerServiceState): “““查询知识库节点仅技术问题触发”“” log f“步骤2查询知识库关键词‘{state[‘user_input‘]}‘“ # 模拟知识库查询 kb_data { “error 404“: “请检查网络连接和服务地址。“, “安装失败“: “请确保系统满足最低要求并查看详细日志文件。“ } result kb_data.get(state[‘user_input‘], “未找到相关解决方案。“) log f“结果‘{result[:50]}...‘“ return {“kb_result“: result, “step_log“: [log]} def generate_draft_node(state: CustomerServiceState): “““生成回答草稿节点”“” log “步骤3生成回答草稿“ context state.get(‘kb_result‘, ““) prompt f“你是一个客服助手。用户问题{state[‘user_input‘]}\n“ if context: prompt f“参考知识库信息{context}\n“ prompt “请生成专业、友好的回答。“ # 模拟LLM调用 draft f“[模拟生成] 关于‘{state[‘user_input‘]}‘建议您{context if context else ‘联系我们的技术支持团队获取进一步帮助。’}“ # 假设生成长回答或包含特定关键词时需要审核 needs_rev len(draft) 100 or “联系“ in draft log f“草稿长度{len(draft)}标记需审核{needs_rev}“ return {“draft_answer“: draft, “needs_review“: needs_rev, “step_log“: [log]} def review_answer_node(state: CustomerServiceState): “““审核节点”“” log “步骤4审核回答草稿“ draft state[‘draft_answer‘] feedback ““ # 简单的规则审核 if “抱歉“ in draft and “无能为力“ in draft: feedback “语气过于消极请提供更积极的解决方案或转接途径。“ log “审核不通过语气问题“ elif len(draft) 10: feedback “回答过于简略请补充更多有用信息。“ log “审核不通过内容过简“ else: feedback “审核通过。“ log “审核通过“ return {“review_feedback“: feedback, “step_log“: [log]} def finalize_answer_node(state: CustomerServiceState): “““最终处理节点根据审核反馈决定是发布还是重写”“” log “步骤5最终处理“ if “审核通过“ in state[‘review_feedback‘]: final_answer state[‘draft_answer‘] log “采用草稿作为最终答案“ else: # 模拟根据反馈重写 final_answer f“[根据审核意见重写] 我们理解您遇到的问题‘{state[‘user_input‘]}‘。这是一个常见情况通常可以通过检查基础配置解决。若需帮助可提供更多细节。“ log “根据反馈重写答案“ # 将最终答案添加到消息历史 final_msg AIMessage(contentfinal_answer) return {“final_answer“: final_answer, “messages“: [final_msg], “step_log“: [log]}4.2 构建条件路由与图# 创建图 workflow StateGraph(CustomerServiceState) # 添加所有节点 workflow.add_node(“classify“, classify_problem_node) workflow.add_node(“query_kb“, query_kb_node) workflow.add_node(“generate“, generate_draft_node) workflow.add_node(“review“, review_answer_node) workflow.add_node(“finalize“, finalize_answer_node) # 设置入口 workflow.set_entry_point(“classify“) # 添加条件边分类后决定是否查询知识库 def route_after_classify(state: CustomerServiceState) - str: if state[‘problem_type‘] “technical“: return “query_kb“ else: return “generate“ # 非技术问题直接生成 workflow.add_conditional_edges( “classify“, route_after_classify, {“query_kb“: “query_kb“, “generate“: “generate“} ) # 添加普通边查询知识库后去生成草稿 workflow.add_edge(“query_kb“, “generate“) # 添加条件边生成草稿后决定是否需要审核 def route_after_generate(state: CustomerServiceState) - str: if state[‘needs_review‘]: return “review“ else: return “finalize“ # 无需审核直接终审 workflow.add_conditional_edges( “generate“, route_after_generate, {“review“: “review“, “finalize“: “finalize“} ) # 添加普通边审核后去最终处理 workflow.add_edge(“review“, “finalize“) # 最终处理节点连接到END workflow.add_edge(“finalize“, END) # 编译图 app workflow.compile()4.3 运行与调试# 测试1技术问题需要知识库和审核 print(“测试1技术问题“) state1 CustomerServiceState( messages[], user_input“程序报错error 404怎么办“, problem_typeNone, kb_result““, draft_answer““, needs_reviewFalse, review_feedback““, final_answer““, step_log[] ) result1 app.invoke(state1) print(f“最终回答{result1[‘final_answer‘]}“) print(f“步骤日志{result1[‘step_log‘]}“) # 测试2普通问题直接生成无需审核 print(“\n测试2普通问题“) state2 CustomerServiceState( messages[], user_input“你们的办公时间是什么“, problem_typeNone, kb_result““, draft_answer““, needs_reviewFalse, review_feedback““, final_answer““, step_log[] ) result2 app.invoke(state2) print(f“最终回答{result2[‘final_answer‘]}“) print(f“步骤日志{result2[‘step_log‘]}“)通过这个例子你可以清晰地看到状态是共享和演进的每个节点读取和修改State的一部分所有更改在流程中累积。控制流清晰可见通过add_conditional_edges我们实现了基于问题类型和草稿长度的分支决策。模块化每个节点功能单一易于单独测试和修改。比如我们可以轻易替换review_answer_node改用更复杂的LLM进行审核。可观测性我们通过step_log记录了每一步的操作这在调试复杂工作流时是救命稻草。5. 高级模式与最佳实践掌握了基础构建后我们来看看LangGraph更强大的能力以及如何用好它。5.1 多智能体协作与子图LangGraph允许你将一个图作为节点嵌入到另一个图中这就是“子图”。这是实现多智能体协作的优雅方式。from langgraph.graph import StateGraph, START, END # 定义一个专门负责搜索的Agent子图 def create_search_agent_graph(): def search_agent_node(state): # ... 搜索Agent的复杂逻辑可能自己也是一个ReAct循环 return {“search_agent_result“: “搜索结果“} search_graph StateGraph(...) search_graph.add_node(“search_agent“, search_agent_node) search_graph.add_edge(“search_agent“, END) return search_graph.compile() # 在主图中引用子图 main_workflow StateGraph(...) search_agent_app create_search_agent_graph() # 将子图作为一个“宏节点”添加到主图 main_workflow.add_node(“search_agent“, search_agent_app)这样主工作流只需要调用search_agent节点而无需关心其内部复杂的实现。你可以用同样的方式创建“写作Agent”、“审核Agent”、“决策Agent”等让它们在主图的调度下协同工作。5.2 持久化与中断恢复对于长时间运行的工作流如处理一个需要等待外部API响应的任务LangGraph支持将状态持久化到数据库如Redis、PostgreSQL并为每个运行实例分配一个唯一的thread_id。这样你可以暂停工作流稍后根据thread_id恢复执行。这是构建生产级异步、长周期Agent系统的关键。from langgraph.checkpoint.sqlite import SqliteSaver memory SqliteSaver.from_conn_string(“:memory:“) # 使用SQLite内存数据库示例 app workflow.compile(checkpointermemory) config {“configurable“: {“thread_id“: “user-123-session-1“}} # 第一次调用会创建检查点 initial_state {...} result1 app.invoke(initial_state, config) # 假设在这里工作流因为等待而暂停... # 稍后根据同一个thread_id恢复传入新的输入或事件 new_input_state {...} result2 app.invoke(new_input_state, config) # 会从上次暂停的节点继续5.3 错误处理与边界情况在实际应用中节点中的代码如调用外部API可能会失败。LangGraph提供了几种处理方式节点级Try-Catch在节点函数内部进行细致的异常捕获和处理返回一个表示错误的状态。全局错误处理通过装饰器或包装节点为整个图设置统一的错误处理逻辑。超时与重试对于网络调用务必设置超时并考虑实现重试机制可以使用tenacity等库将重试逻辑封装在节点函数内。一个常见的模式是在状态中设置一个error或status字段。当节点失败时更新这个字段并通过条件边将流程导向一个专门的“错误处理节点”该节点可以记录日志、通知用户或尝试降级方案。实操心得在定义状态时我习惯预留一个metadata字典字段用于存放各种运行时信息如错误详情、重试次数、开始时间等。这比散落多个字段更清晰。另外为每个重要的节点编写单元测试至关重要模拟各种输入和可能的异常状态能极大减少集成时的调试时间。6. 常见问题与排查技巧实录即使理解了概念在实际开发中还是会踩坑。下面是我总结的一些典型问题和解决方法。问题现象可能原因排查步骤与解决方案状态更新不符合预期某些字段被覆盖而不是追加。State中字段的Annotated注解使用错误。例如对于列表应该用operator.add对于字符串可能直接用str覆盖或自定义合并函数。1. 仔细检查TypedDict中每个字段的注解。2. 在节点函数中打印传入的state和返回的更新字典。3. 查阅LangGraph文档理解reduce参数的作用。条件边不生效流程总是走默认分支或报错。决策函数返回的值与add_conditional_edges中映射的键不匹配。或者决策函数本身逻辑有误。1. 在决策函数中打印state和返回值。2. 确保返回值是字符串且完全匹配你定义的映射键如“search“。3. 使用app.get_graph().draw_mermaid()生成图检查条件边的连接是否正确。图编译或运行时报类型错误。State的类型定义TypedDict与节点函数实际接收和返回的数据类型不匹配。1. 使用mypy或pyright进行静态类型检查。2. 确保节点函数返回的字典中的键其值的类型与State中定义的完全一致。工作流陷入无限循环。图中存在环但没有设置合适的终止条件。例如一个节点处理后条件边又把它指回了自己或前驱节点。1. 可视化你的图寻找循环路径。2. 在状态中设置一个计数器如loop_count在节点中递增并在条件边决策函数中检查超过阈值则导向END。3. 确保业务逻辑上每个循环都有退出条件。多智能体协作时消息混乱。不同Agent的消息都追加到同一个messages列表导致上下文混乱。1. 为不同的Agent使用不同的消息列表字段如user_messages,search_agent_messages。2. 或者在状态中维护一个current_agent字段在构建Prompt时只选取相关消息。3. 考虑使用RunnableWithMessageHistory来更精细地管理对话历史。子图调用后主图状态未更新。子图节点修改的状态字段可能没有正确映射回主图的状态结构。1. 明确子图的输入输出状态结构。2. 确保在主图中子图节点返回的更新字典包含了主图State需要的字段。可能需要一个适配函数来转换格式。调试技巧善用可视化app.get_graph().draw_mermaid()是理解复杂工作流结构的最佳工具。将生成的Mermaid代码贴到在线编辑器里一目了然。打印中间状态在关键的节点函数开头和结尾打印state的内容。LangGraph的运行是确定的这能帮你跟踪状态是如何一步步变化的。简化复现当遇到问题时尝试创建一个最小可复现例子剥离无关的业务逻辑只保留导致问题的核心图结构这样更容易定位。版本管理你的工作流图会随着需求迭代。记得为图的定义代码和编译后的app对象如果序列化做好版本管理。7. 性能优化与生产部署考量当你的LangGraph应用从原型走向生产时以下几点需要重点考虑1. 节点粒度与复用 节点不是越细越好。过细的节点会增加序列化和状态传递的开销。将紧密相关的操作合并到一个节点中。同时设计可复用的节点比如一个“调用LLM并解析”的通用节点通过状态中的参数来决定具体的Prompt和解析方式。2. 异步支持 LangGraph天然支持异步节点。如果节点涉及大量I/O操作网络请求、数据库查询务必将其定义为async def并在图中使用异步调用可以大幅提升吞吐量。async def async_search_node(state: State): result await async_call_search_api(state[‘query‘]) return {“result“: result}3. 流式输出 对于需要实时向用户反馈的场景可以利用LangGraph的流式接口app.astream()在每个节点执行后逐步输出结果而不是等待整个工作流完成。4. 监控与可观测性 在生产环境中你需要监控工作流的执行时长、成功率、每个节点的耗时等。可以在节点函数中添加装饰器来自动记录指标或者利用LangGraph的Checkpoint机制在持久化时记录元数据。5. 测试策略单元测试单独测试每个节点函数模拟各种输入状态。集成测试测试整个图对于典型输入是否能产生正确输出。属性测试使用hypothesis等库生成随机但符合规范的状态输入测试工作流的健壮性确保不会崩溃或产生非法状态。从我自己的经验来看LangGraph最大的价值在于它提供了一种清晰、可维护、可测试的方式来构建复杂的AI智能体工作流。它迫使你将混乱的Agent逻辑拆解成一个个明确定义的步骤和状态转换这本身就是一个极大的进步。开始可能会觉得有些繁琐但一旦适应了这种“图思维”开发效率和代码质量都会有质的提升。