从LangChain到LangGraph:构建可控有状态Agent的工程实践

📅 2026/8/7 4:23:13
从LangChain到LangGraph:构建可控有状态Agent的工程实践
1. 项目概述从链式编排到图式编排的Agent进化如果你在过去一年里折腾过基于大语言模型的应用开发那么“LangChain”这个名字对你来说一定不陌生。它几乎成了快速搭建一个具备检索增强生成RAG或简单工具调用能力应用的“脚手架”代名词。我自己也用它做过不少原型从简单的文档问答到稍复杂一些的客服机器人LangChain的链Chain式思维确实让早期开发变得直观。但当你真的想把一个原型推进成能处理复杂、多步骤、有状态交互的“智能体”Agent时链式结构的局限性就暴露无遗了状态管理混乱、执行路径僵化、错误处理笨拙调试起来像在迷宫里找路。这正是“LangGraph”出现的背景也是我这次实践的核心。LangGraph不是LangChain的替代品而是它的“超级赛亚人”形态。它把我们从线性的“链”思维解放到了更符合复杂业务逻辑的“图”思维。简单说LangChain帮你快速搭好积木而LangGraph则让你能设计并运行一个精密的、带反馈循环的自动化流水线。这次我就以构建一个“可控的、具备长期记忆与任务分解能力的客服工单处理Agent”为例带你完整走一遍从LangChain传统Agent模式升级到基于LangGraph构建健壮Stateful Agent的工程实践。你会发现当你的思维从“下一步做什么”转变为“当前处于什么状态根据状态决定流向哪个节点”时你对Agent的控制力将得到质的飞跃。2. 核心理念剖析为什么是LangGraph在深入代码之前我们必须先统一思想为什么传统的LangChain Agent在复杂场景下会“力不从心”而LangGraph的图模型是更优解这关乎你对Agent本质的理解。2.1 LangChain Agent的经典模式与瓶颈LangChain的经典Agent模式其核心是“AgentExecutor”驱动一个“Action-ActionInput-Observation”的循环。Agent根据当前对话历史和工具列表决定下一个工具调用Action及其参数Input执行后得到结果Observation再将这个结果拼接到历史中进入下一轮循环。这个过程听起来很合理但在工程实践中会遇到几个硬伤状态管理黑盒整个循环的状态对话历史、中间变量都糅杂在一个冗长的字符串或消息列表里。你想在循环中间插入一个状态检查、修改某个中间变量或者实现一个“暂停-继续”机制非常困难你需要去解析和重构那段历史。流程刚性执行路径本质上是线性的尽管工具选择有分支很难实现复杂的流程控制比如循环直到满足某个条件、并行执行多个工具、或者实现一个真正的“规划-执行-反思”工作流。虽然可以通过自定义Chain和Router来模拟但代码会迅速变得难以维护。调试与可观测性差当Agent出错或陷入死循环时你看到的是一长串交互历史很难一眼定位问题出在哪一个决策节点。缺乏清晰的状态快照和流程可视化。2.2 LangGraph的图状态机模型LangGraph引入了“状态图”StateGraph的概念。在这里一切围绕“状态”State和“节点”Node展开。状态State一个定义清晰的Pydantic模型或TypedDict。它明确规定了Agent运行过程中需要维护的所有数据例如messages对话历史intermediate_steps工具调用记录current_task当前子任务knowledge_base从数据库查询的结果等。状态是结构化的不再是混沌的字符串。节点Node一个执行单元是一个函数。它接收当前完整的“状态”作为输入对其进行读写操作并返回一个更新后的“状态”。一个节点可以是一个LLM调用、一个工具调用、一个条件判断函数或者任何你需要的业务逻辑。边Edge决定执行流程的方向。分为“条件边”conditional edge和“普通边”。条件边根据当前状态的某些属性值决定下一个执行哪个节点普通边则固定流向下一个节点。这种模型的优势是降维打击式的显式状态管理所有数据都在明面上读写清晰极易实现持久化长期记忆和注入从外部恢复会话。灵活的流程控制你可以轻松设计循环一个节点执行完后根据条件边再次指向自己、分支根据LLM的决策或工具结果走不同路径、并行虽然原生是顺序但节点内可并发调用和子图封装复杂子流程。卓越的可观测性因为每个节点输入输出都是结构化的状态你可以轻松地在每个节点前后打日志、做监控、甚至插入审核节点。LangGraph还内置了可视化功能能生成你Agent的流程图一目了然。简单类比LangChain Agent像是一份手写的、步骤连续的烹饪清单而LangGraph Agent则像一张厨房的流程图清晰地标明了备菜区、炒锅、烤箱、调味台以及根据食物生熟度决定下一步该去哪的决策点。3. 工程实践构建可控客服工单处理Agent理论说再多不如动手。我们的目标是构建一个Agent它能理解用户模糊的客服请求如“我的订单没收到而且页面还扣款了”自动分解任务查询相关订单和支付数据综合分析给出解决方案并记住对话上下文。3.1 定义清晰的状态结构这是LangGraph设计中最关键的一步决定了整个系统的数据流。我们使用Pydantic来获得类型提示和验证。from typing import Annotated, List, Optional, TypedDict, Union from pydantic import BaseModel, Field from langgraph.graph.message import add_messages import operator # 定义核心状态模型 class AgentState(TypedDict): # 消息历史LangGraph内置的注解能自动合并消息列表 messages: Annotated[List[Union[HumanMessage, AIMessage, ToolMessage]], add_messages] # 当前解析出的用户意图如“查询订单状态”、“投诉支付问题” user_intent: str # 从外部系统查询到的原始数据如订单列表、支付记录 raw_data: dict # 经过分析后的结构化信息如“订单12345状态为已发货支付单67890状态为已扣款未确认” analyzed_info: str # 标记当前流程是否结束 is_finished: bool # 用于控制流程转向的指令如“need_more_info”, “can_resolve”, “escalate_to_human” next_step: str为什么这么设计messages是对话引擎的核心。user_intent和next_step是控制流程的关键“开关”。raw_data和analyzed_info将工具执行结果与LLM分析结果分离便于调试和复用。is_finished是循环终止的条件。3.2 实现核心功能节点节点就是普通的Python函数它操作State。我们实现几个关键节点。节点1意图识别与任务分解节点这个节点接收用户最新消息调用LLM分析用户意图并初步分解任务。from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI llm ChatOpenAI(modelgpt-4-turbo-preview, temperature0) intent_prompt ChatPromptTemplate.from_messages([ (system, 你是一个高级客服分析助手。请分析用户的输入完成以下任务 1. 识别核心意图单点如查询订单、投诉支付、咨询退货。 2. 判断是否需要以及需要查询哪些外部信息如订单号、支付流水号、用户ID。 3. 输出一个简明的下一步指令用于驱动系统。 用户可能表达多个问题请聚焦于最紧急的一个。), (placeholder, {messages}), ]) def intent_analysis_node(state: AgentState) - AgentState: 分析用户意图初始化任务状态 # 1. 调用LLM进行意图分析 chain intent_prompt | llm analysis_result chain.invoke({messages: state[messages]}) # 2. 解析LLM返回的结构化内容这里简化处理实际应用建议让LLM返回JSON # 假设我们通过一个简单的解析函数来提取意图和下一步指令 # 例如解析结果为“意图支付问题投诉。需查询订单号、支付流水。下一步query_payment” parsed_intent parse_intent_from_llm_output(analysis_result.content) # 自定义解析函数 parsed_next_step parse_next_step_from_llm_output(analysis_result.content) # 3. 更新状态 return { user_intent: parsed_intent, next_step: parsed_next_step, raw_data: {}, # 清空旧数据 analyzed_info: , is_finished: False, messages: state[messages] [AIMessage(contentf已理解您的问题属于【{parsed_intent}】。正在为您处理...)] }实操心得在intent_analysis_node中让LLM返回结构化数据如JSON远比返回一段文本再解析要稳健。可以使用LangChain的with_structured_output功能或提示词工程强制输出JSON这能极大减少后续节点处理的复杂度。节点2数据查询节点这是一个“工具执行”节点根据next_step或user_intent来决定调用哪个外部API或数据库查询。# 模拟一些工具函数 def query_order_system(order_id: str) - dict: # 模拟订单查询 return {order_id: order_id, status: shipped, product: Book} def query_payment_system(payment_id: str) - dict: # 模拟支付查询 return {payment_id: payment_id, amount: 100, status: completed} def data_query_node(state: AgentState) - AgentState: 根据意图和上下文查询外部数据 # 这里可以根据 state[user_intent] 或 state[next_step] 来路由查询逻辑 # 例如如果 next_step 是 query_order if state[next_step] query_order: # 从对话历史中提取订单号这里简化实际可用另一个LLM调用或正则提取 order_id extract_order_id(state[messages]) if order_id: data query_order_system(order_id) state[raw_data].update({order: data}) state[next_step] analyze_data # 查询完成后指示下一步分析 else: state[next_step] need_more_info # 没找到订单号需要追问用户 state[messages].append(AIMessage(content请问您的订单号是多少)) elif state[next_step] query_payment: payment_id extract_payment_id(state[messages]) if payment_id: data query_payment_system(payment_id) state[raw_data].update({payment: data}) state[next_step] analyze_data else: state[next_step] need_more_info state[messages].append(AIMessage(content请提供支付流水号或订单号。)) # ... 其他查询类型 else: state[next_step] analyze_data # 默认进入分析 return state注意事项工具节点是Agent与真实世界交互的桥梁必须做好错误处理。网络超时、API限流、数据格式异常等情况都要考虑并在状态中设置相应的next_step如query_failed来触发降级处理或人工接管流程。节点3数据分析与决策节点这个节点接收raw_data调用LLM进行综合分析、判断并生成最终回复或下一步决策。analysis_prompt ChatPromptTemplate.from_messages([ (system, 你是一名客服专家。请根据以下系统查询到的原始数据和分析任务进行综合判断 原始数据{raw_data} 用户意图{user_intent} 你的任务 1. 分析数据是否足以解决问题。 2. 如果足以解决生成对用户友好、准确的答复并标记问题已解决is_finishedTrue。 3. 如果数据不足或发现矛盾如订单显示发货但用户未收到生成需要进一步操作的指令next_step例如“need_more_info”、“escalate_to_human”或触发另一个查询“query_logistics”。 4. 将你的分析逻辑简要总结到‘analyzed_info’字段。 请以JSON格式输出包含字段analysis, response, is_finished, next_step, analyzed_info。 ), ]) def analysis_and_decision_node(state: AgentState) - AgentState: 分析数据做出决策生成回复 chain analysis_prompt | llm.with_structured_output( schema{ analysis: str, response: str, is_finished: bool, next_step: str, analyzed_info: str } ) try: result chain.invoke({ raw_data: state[raw_data], user_intent: state[user_intent] }) except Exception as e: # LLM调用失败降级处理 result { analysis: 分析过程出错, response: 系统暂时无法处理请稍后再试或联系人工客服。, is_finished: False, next_step: escalate_to_human, analyzed_info: fLLM分析失败: {e} } # 用LLM的决策结果更新状态 state[is_finished] result[is_finished] state[next_step] result[next_step] state[analyzed_info] result[analyzed_info] state[messages].append(AIMessage(contentresult[response])) return state节点4信息追问节点与人工接管节点这是处理边界情况的节点。def need_more_info_node(state: AgentState) - AgentState: 当数据不足时向用户追问信息 # 可以根据 analyzed_info 或一个更精细的规则来生成追问话术 # 这里简单处理LLM在analysis节点应该已经把追问内容放到了response中 # 我们只需要确保状态正确流转 state[next_step] wait_for_user_input return state def escalate_to_human_node(state: AgentState) - AgentState: 将对话升级给人工客服 state[messages].append(AIMessage(content您的问题已超出我的处理范围正在为您转接高级客服专员请稍候。)) state[is_finished] True # 或设置为等待人工接入的状态 state[next_step] human_in_loop # 这里可以触发一个通知到工单系统或客服坐席平台 # trigger_escalation_notification(state) return state3.3 组装图并定义流程逻辑现在我们将节点组装成图并定义它们之间的流转逻辑。from langgraph.graph import StateGraph, END # 1. 创建图构建器 workflow StateGraph(AgentState) # 2. 添加节点 workflow.add_node(intent_analysis, intent_analysis_node) workflow.add_node(data_query, data_query_node) workflow.add_node(analysis_decision, analysis_and_decision_node) workflow.add_node(need_more_info, need_more_info_node) workflow.add_node(escalate_to_human, escalate_to_human_node) # 3. 设置入口点 workflow.set_entry_point(intent_analysis) # 4. 添加边定义流程 # 意图分析后总是先去查询数据 workflow.add_edge(intent_analysis, data_query) # 数据查询后根据其设置的 next_step 决定去向 workflow.add_conditional_edges( data_query, # 这是一个路由函数根据state决定下一个节点名 lambda state: state.get(next_step, analysis_decision), { need_more_info: need_more_info, analysis_decision: analysis_decision, escalate_to_human: escalate_to_human, } ) # 分析决策后同样根据 next_step 和 is_finished 路由 def route_after_analysis(state: AgentState) - str: if state.get(is_finished): return END # LangGraph内置的结束标识 next_step state.get(next_step) if next_step need_more_info: return need_more_info elif next_step escalate_to_human: return escalate_to_human elif next_step data_query: # 可能需要新一轮查询 return data_query else: # 默认情况可能是等待用户输入或结束 return END workflow.add_conditional_edges(analysis_decision, route_after_analysis) # 需要更多信息节点执行后流程应该暂停等待用户输入。 # 在LangGraph中我们通常通过将图编译成“可中断”的图来实现。 # 这里我们让它直接回到入口点或一个等待节点但实际中我们会在这里暂停。 workflow.add_edge(need_more_info, END) # 简化处理实际应用需要更精细的控制 # 人工接管后流程结束 workflow.add_edge(escalate_to_human, END) # 5. 编译图 app workflow.compile()3.4 运行与持久化实现长期记忆LangGraph的State本身是一个字典持久化非常方便。我们可以使用LangGraph的Checkpointer机制或者简单地将其序列化如JSON存储到数据库。import json from langgraph.checkpoint.sqlite import SqliteSaver # 方式一使用内置的SQLite检查点适用于开发和小型应用 memory SqliteSaver.from_conn_string(:memory:) # 或你的数据库路径 app_with_memory workflow.compile(checkpointermemory) # 运行一个会话 config {configurable: {thread_id: user_123_session_1}} initial_state {messages: [HumanMessage(content我的订单没收到页面显示扣款成功了)]} # 第一次运行 result_state app_with_memory.invoke(initial_state, config) print(result_state[messages][-1].content) # 查看AI回复 # 假设用户稍后回复了订单号我们继续这个会话 new_state_input {messages: [HumanMessage(content订单号是 ORD-2024-1001)]} # 注意我们使用相同的 config (thread_id)app会从上次中断的状态继续执行 continued_state app_with_memory.invoke(new_state_input, config) print(continued_state[messages][-1].content) # 方式二手动序列化状态更灵活可集成到现有存储系统 def save_state_to_db(thread_id: str, state: dict): # 将state序列化后存入数据库 serialized json.dumps(state, defaultstr) # 注意处理不可序列化对象 # db.execute(INSERT OR REPLACE INTO agent_sessions VALUES (?, ?), (thread_id, serialized)) pass def load_state_from_db(thread_id: str) - dict: # 从数据库加载并反序列化 # serialized db.query(...) # return json.loads(serialized) if serialized else {} pass核心技巧Checkpointer是LangGraph实现“长期记忆”和“可恢复对话”的利器。thread_id是会话的唯一键。在生产环境中你可以将其与用户的会话ID绑定。当用户再次发起对话时只需用相同的thread_id调用app.invokeAgent就能从上次结束的状态继续运行实现了真正的有状态对话。4. 高级模式与性能优化基础图搭建完成后我们可以探索更强大的模式来提升Agent的智能和效率。4.1 实现规划-执行-反思ReAct循环ReAct是Agent的经典范式。在LangGraph中我们可以轻松地将其建模为一个包含“规划节点”、“工具执行节点”和“反思节点”的子图并将这个子图作为一个超级节点嵌入主图。from langgraph.graph import StateGraph as SubStateGraph # 定义子图的状态可以是主状态的一部分 class ReActState(TypedDict): plan: List[str] last_action: dict observation: str reflection: str # ... 其他字段 def planner_node(state: ReActState): # 根据目标和大纲生成下一步具体行动指令 pass def actor_node(state: ReActState): # 执行 planner 指定的工具 pass def reflector_node(state: ReActState): # 评估执行结果判断是否继续、修改计划或结束 pass # 构建ReAct子图 react_workflow SubStateGraph(ReActState) react_workflow.add_node(planner, planner_node) react_workflow.add_node(actor, actor_node) react_workflow.add_node(reflector, reflector_node) react_workflow.set_entry_point(planner) react_workflow.add_edge(planner, actor) react_workflow.add_edge(actor, reflector) react_workflow.add_conditional_edges(reflector, lambda s: planner if not s.get(done) else END) react_subgraph react_workflow.compile() # 在主图中可以将这个子图作为一个节点使用 # workflow.add_node(complex_task_solver, react_subgraph)4.2 并行与异步执行优化默认情况下LangGraph节点是顺序执行的。但如果你的data_query_node需要同时查询多个不相关的数据源你可以在节点内部使用异步并发。import asyncio async def parallel_data_query_node(state: AgentState) - AgentState: 并行查询订单和支付信息 tasks [] if should_query_order(state): tasks.append(query_order_system_async(state)) # 假设是异步函数 if should_query_payment(state): tasks.append(query_payment_system_async(state)) # 并发执行 results await asyncio.gather(*tasks, return_exceptionsTrue) # 处理结果合并到 state[raw_data] for i, result in enumerate(results): if not isinstance(result, Exception): # 正常合并 pass else: # 错误处理记录日志可能更新state[next_step]为部分失败状态 pass return state性能提示对于I/O密集型的工具调用网络请求、数据库查询务必使用异步。LangGraph本身支持异步节点只需将节点函数定义为async def并在调用时使用ainvoke即可。这能显著提升Agent处理多个外部依赖时的响应速度。4.3 可视化与调试LangGraph提供了出色的可视化工具对于理解复杂流程和调试至关重要。# 生成流程图的PNG图片 from IPython.display import Image, display try: display(Image(app.get_graph().draw_mermaid_png())) except: # 如果环境不支持可以输出Mermaid文本粘贴到Mermaid编辑器中查看 print(app.get_graph().draw_mermaid())通过生成的流程图你可以清晰地看到所有节点和条件边快速定位流程设计中的逻辑漏洞或死循环。5. 避坑指南与生产级考量在实际部署中我踩过不少坑这里总结几个关键点。1. 状态设计的膨胀与精简初期容易把所有想到的数据都塞进State导致State变得臃肿影响序列化效率和节点函数签名。务必遵循“最小必要”原则只保留驱动流程和生成回复所必需的数据。对于中间计算结果考虑是否真的需要跨节点持久化。2. 条件边的路由逻辑过于复杂路由函数lambda state: ...应该保持简单。如果路由逻辑变得复杂说明你的节点职责可能不清晰。考虑将复杂判断封装成一个专用的“路由节点”Router Node该节点只做判断并设置next_step然后由简单的条件边根据next_step的值进行分发。3. LLM调用不稳定性的处理LLM可能宕机、超时或返回无法解析的内容。在每个调用LLM的节点必须用try...except包裹并在异常时提供一个降级的next_step如llm_failed引导流程走向安全节点如直接请求澄清或转人工。重试策略也要谨慎使用避免无限循环。4. 工具执行的安全与权限工具节点是安全重灾区。任何执行外部命令、访问数据库、调用API的工具都必须进行严格的输入验证和权限控制。例如查询数据库时禁止直接拼接用户输入生成SQL调用系统命令时必须白名单化可接受的参数。最好在工具层和节点层都做校验。5. 循环与超时控制LangGraph图可能因为条件边设置不当而陷入死循环。务必在关键循环路径上设置“循环计数器”并在State中记录。可以在一个“监督节点”中检查计数器超过阈值则强制跳转到结束或人工接管节点。也可以利用编译图时的interrupt_before/after参数在外部设置超时中断。6. 测试策略不要只测试整个图。要对每个节点进行单元测试模拟各种输入State。对条件边进行集成测试验证所有可能的分支路径。使用LangGraph的stream模式进行端到端测试观察State的完整变化流这比只看最终输出更有助于发现问题。从LangChain到LangGraph不仅仅是工具的升级更是开发范式的转变。它迫使你以更工程化、更结构化的方式去思考Agent的“状态”和“流程”。当你习惯了这种思维你会发现构建可控、可观测、可维护的复杂智能体不再是一件令人头疼的事情。这套实践下来最深的体会是清晰的状态定义是可控性的基石而可视化的图结构则是团队沟通和迭代优化最好的蓝图。