公司动态
LangGraph生产实践:构建可观测、有状态的AI智能体工作流
1. 从LangChain到LangGraph为什么我们需要一个“状态机”如果你在过去一年里折腾过AI应用开发尤其是基于大语言模型LLM的智能体Agent那么“LangChain”这个名字你一定不陌生。它像一把瑞士军刀帮你把各种工具、模型、记忆模块组装起来。但当你真的想把一个Agent丢到生产环境让它7x24小时处理真实、复杂、带状态的业务流程时你可能会发现用LangChain写出的代码逻辑开始变得像一团纠缠的意大利面。循环、条件判断、状态维护的代码散落在各处调试一个多轮对话的流程堪比在迷宫里找出口。这就是我三个月前面临的困境也是我决定将核心服务迁移到LangGraph上的根本原因。LangGraph不是来替代LangChain的它是来补上那块最关键的拼图复杂、有状态的工作流编排。你可以把它理解为一个专为AI智能体设计的可视化状态机State Graph。在LangGraph的世界里你的整个Agent被建模成一个“图”Graph节点Node是一个个执行单元比如调用LLM、执行工具、查询数据库边Edge则定义了节点之间的流转条件。所有的会话状态、中间结果、历史记录都被封装在一个中心化的State对象里随着流程在图中有序传递。这带来的最直观好处是心智负担的极大降低。你不再需要手动写一堆if-else来控制流程分支也不需要费心维护一个全局变量来记录对话到了哪一步。你只需要定义好状态的结构、每个节点的函数、以及决定下一个节点是谁的规则。剩下的LangGraph的引擎会帮你搞定。这种声明式的编程模型让Agent的可维护性和可观测性提升了不止一个档次。当你的老板问“为什么这个订单处理卡住了”时你可以清晰地看到当前状态卡在“人工审核”节点而不是在一堆日志里大海捞针。2. 生产环境架构选型为什么是LangGraph在决定引入任何新技术栈之前尤其是在生产环境这种“牵一发而动全身”的地方技术选型的评估必须苛刻。我们当时的候选方案并不少继续深度优化基于LangChain的Custom Agent、尝试其他新兴框架如AutoGen、或者自己从零搭建一套状态管理逻辑。最终LangGraph胜出主要基于以下几个维度的考量2.1 与LangChain生态的无缝集成这是我们选择LangGraph的基石。我们的基础能力如模型调用通过ChatOpenAI、ChatAnthropic、工具调用通过tool装饰器、以及向量存储检索都已经深度依赖LangChain。LangGraph被设计为LangChain的一部分其State对象可以天然地容纳LangChain的各种对象如Messages、Document。这意味着迁移成本是最低的我们不需要重写已有的工具函数或模型交互层只需要用LangGraph的图结构把它们重新“编织”起来。这种“渐进式重构”的能力对于已在线上运行的系统至关重要。2.2 对复杂、有状态流程的天然亲和力我们的核心业务场景是“智能客服工单处理”这绝不是一个简单的问答机器人。一个工单的生命周期可能涉及1意图识别与分类2自动查询知识库解答3如需人工则转交并等待4人工处理后自动生成回复摘要并通知用户5超时或升级处理。这个流程包含条件分支、循环等待人工响应、并行同时通知多个渠道以及持久化状态。用传统代码写会充斥着标志位和回调。而用LangGraph我们可以用“图”清晰地定义一个“路由”节点根据意图决定走自动解答分支还是人工分支。一个“等待”节点挂起流程直到人工系统回调。一个“并行”节点同时执行发送邮件和更新内部工单系统的操作。 这种可视化、结构化的表达让业务逻辑一目了然新同事也能快速理解整个系统脉络。2.3 内置的持久化与检查点Checkpoint机制这是LangGraph打入生产环境的“王牌功能”。在langgraph库中你可以为图配置一个Checkpointer。这意味着图执行的任意中间状态都可以被持久化到数据库如PostgreSQL、MySQL或内存中。当流程因为服务重启、网络波动或长时间等待而中断时可以从最后一个检查点无损恢复继续执行。这对于需要处理长时间运行任务如等待人工介入可能长达数小时的Agent来说是保证可靠性和数据一致性的生命线。我们不必自己再去实现一套复杂的状态恢复逻辑。2.4 优秀的可调试性与可观测性LangGraph提供了详细的执行日志你可以清晰地看到开始执行节点: classify_intent 输入状态: {“messages”: [用户消息…]} 输出状态: {“intent”: “complaint”, …} 下一节点: route_to_handler结合像LangSmith这样的LLM应用监控平台你可以追踪每一次LLM调用、工具执行的输入输出、耗时和成本。这为我们定位生产环境下的诡异问题比如为什么LLM突然把“投诉”理解成了“咨询”提供了强大的工具支持。在微服务架构下这种可观测性是无价的。3. 实战部署踩坑、优化与稳定性保障把LangGraph从Demo环境搬到生产环境这中间有相当长的路要走。下面分享我们过去三个月里在部署、运维和优化过程中积累的一些核心经验与踩过的坑。3.1 状态State设计的艺术State是LangGraph中流动的血液设计好它的结构是第一步也是最容易出错的一步。from typing import TypedDict, Annotated, List from langgraph.graph.message import add_messages import operator class AgentState(TypedDict): # 消息历史必须使用Annotated和add_messagesLangGraph才能正确处理消息累加 messages: Annotated[List[BaseMessage], add_messages] # 用户输入的原始问题 user_query: str # 经过路由节点解析后的意图 intent: str # 从知识库检索到的相关文档列表 retrieved_docs: List[Document] # 工单系统返回的工单ID可能为空 ticket_id: str # 标志位是否需要人工介入 needs_human: bool # 本次对话的会话ID用于持久化关联 session_id: str注意messages字段必须使用Annotated[List[BaseMessage], add_messages]来声明。add_messages是一个归约器Reducer它告诉LangGraph如何合并新旧状态中的消息列表通常是追加而不是简单地覆盖。这是新手最容易忽略导致消息历史丢失的点。另一个坑是状态爆炸。初期我们喜欢把一切中间结果都塞进State导致State对象越来越庞大不仅影响序列化/反序列化性能在持久化时也占用大量存储。后来我们遵循一个原则只存储影响路由决策和最终输出所必需的数据。例如检索到的文档内容本身可能很大如果后续节点只需要文档ID那就只存ID需要时再按ID查询。3.2 图Graph的编译与配置定义好State和节点函数后需要将它们组装成图并编译。from langgraph.graph import StateGraph, END # 1. 创建图构建器 workflow StateGraph(AgentState) # 2. 添加节点 workflow.add_node(“classify_intent”, classify_intent_node) workflow.add_node(“retrieve_knowledge”, retrieve_knowledge_node) workflow.add_node(“generate_response”, generate_response_node) workflow.add_node(“create_ticket”, create_ticket_node) # 3. 设置入口点 workflow.set_entry_point(“classify_intent”) # 4. 添加条件边Conditional Edge def route_after_intent(state: AgentState): if state[“intent”] “complex_issue”: return “create_ticket” else: return “retrieve_knowledge” workflow.add_conditional_edges( “classify_intent”, route_after_intent, {“create_ticket”: “create_ticket”, “retrieve_knowledge”: “retrieve_knowledge”} ) # 5. 添加普通边 workflow.add_edge(“retrieve_knowledge”, “generate_response”) workflow.add_edge(“generate_response”, END) workflow.add_edge(“create_ticket”, END) # 6. 编译图并配置持久化检查点 from langgraph.checkpoint.sqlite import SqliteSaver checkpointer SqliteSaver.from_conn_string(“sqlite:///checkpoints.db”) app workflow.compile(checkpointercheckpointer)这里的关键点在于条件边的使用。它让图具备了动态路由能力是实现复杂业务逻辑的核心。确保你的路由函数如route_after_intent逻辑清晰且健壮对所有可能的State情况都有明确的返回。3.3 持久化与并发处理在生产环境中我们使用PostgreSQL作为检查点存储替代了开发时用的SQLite。from langgraph.checkpoint.postgres import PostgresSaver import asyncpg async def get_app(): conn await asyncpg.create_pool(dsn“postgresql://user:passlocalhost/dbname”) checkpointer PostgresSaver(conn) app workflow.compile(checkpointercheckpointer) return app重要提示LangGraph的检查点机制默认是线程/协程安全的但在高并发场景下对同一session_id的并发操作需要谨慎。我们的策略是对于一个新的用户会话生成一个唯一的session_id如UUID。任何针对该会话的请求都使用这个session_id来调用图。LangGraph会基于检查点实现“恰好一次”或“至少一次”的语义避免重复执行。如果你的应用允许用户从多个客户端同时操作同一会话可能需要在外层加锁或采用乐观锁机制。3.4 性能监控与成本控制Agent的每次运行都可能涉及昂贵的LLM API调用。我们做了以下几件事来保障性能和成本超时与重试对所有LLM调用和外部工具调用如数据库查询、第三方API包裹了超时和指数退避重试逻辑。这能有效应对临时的网络抖动或上游服务不稳定。缓存对于频繁且结果固定的查询如某些通用知识库检索在State节点之前加入Redis缓存层。如果同样的问题被再次问起直接返回缓存结果跳过LLM和检索步骤。流式输出对于需要长时间生成响应的节点使用LangGraph和FastAPI结合支持流式Server-Sent Events返回tokens极大提升用户体验。LangSmith集成将所有LLM调用链路接入LangSmith。通过其Dashboard我们可以清晰地看到每个环节的耗时、Token使用量和成本快速定位性能瓶颈和异常昂贵的调用。4. 与微服务及现有基础设施的集成我们的系统并非一个孤立的LangGraph应用它需要嵌入到现有的Java/Go微服务生态中并与SkyWalking、Prometheus等监控体系打通。4.1 服务化封装我们没有直接让前端调用Python的LangGraph应用而是用FastAPI将其包装成一个独立的HTTP服务即一个Python微服务。from fastapi import FastAPI, HTTPException from pydantic import BaseModel import asyncio app FastAPI(title“Agent-Orchestrator”) graph_app None # 在启动时初始化 class ChatRequest(BaseModel): session_id: str message: str user_id: str app.post(“/chat”) async def chat_endpoint(request: ChatRequest): try: # 准备初始状态 initial_state { “messages”: [HumanMessage(contentrequest.message)], “session_id”: request.session_id, “user_id”: request.user_id } # 异步执行图 config {“configurable”: {“thread_id”: request.session_id}} async for event in graph_app.astream(initial_state, configconfig, stream_mode“values”): # 处理流式事件如可以实时返回部分结果 if “messages” in event and len(event[“messages”]) 0: latest_msg event[“messages”][-1] if isinstance(latest_msg, AIMessage): # 这里可以流式返回AI消息内容 pass # 返回最终结果 final_state await graph_app.ainvoke(initial_state, configconfig) return {“response”: final_state[“messages”][-1].content} except Exception as e: raise HTTPException(status_code500, detailstr(e))这样其他语言的服务如Java的订单服务只需要通过RESTful API或gRPC与这个Agent编排服务交互即可。4.2 接入SkyWalking实现分布式追踪为了让LangGraph服务的调用链能在整个微服务链路中可见我们接入了SkyWalking的Python Agent。安装skywalking-python包。在服务启动脚本中初始化SkyWalking。from skywalking import agent, config config.init(collector_address‘skywalking-oap:11800’, service_name‘python-agent-orchestrator’) agent.start()利用FastAPI的中间件或手动埋点将关键的LangGraph节点执行如classify_intent、call_llm作为Span上报到SkyWalking。这样在SkyWalking UI上你就可以看到一个用户请求从前端到网关再到Java订单服务最后调用Python LangGraph服务的完整调用链包括每个LLM调用的耗时对于排查跨服务延迟问题至关重要。4.3 指标暴露与Prometheus监控我们使用prometheus-fastapi-instrumentator来让FastAPI服务自动暴露Prometheus指标如请求数、延迟、错误率。此外我们还自定义了一些业务指标agent_execution_duration_seconds记录每次图执行的总耗时。llm_invocation_total按模型类型统计LLM调用次数。agent_node_transitions_total统计各个节点被触发的次数用于分析业务流量路径。 这些指标被Prometheus抓取并在Grafana中制成仪表盘用于实时监控服务健康度和业务趋势。5. 遇到的典型问题与排查心法在生产环境运行三个月不可能一帆风顺。下面记录几个有代表性的问题及其解决思路。5.1 状态持久化失败导致流程“失忆”现象用户进行到多轮对话的某一步服务重启后Agent忘记了之前的对话历史从头开始。排查首先检查检查点数据库PostgreSQL的连接和表结构是否正常。langgraph会创建checkpoints和checkpoint_blobs等表。确认在编译图compile时checkpointer参数是否正确传入。最关键的一步确认每次调用app.invoke或app.astream时传入的config参数中包含了正确的thread_id通常用session_id。检查点是通过configurable.thread_id来唯一标识和恢复一个会话流程的。如果每次调用都使用新的thread_idLangGraph就会认为是一个全新的会话不会加载历史状态。检查节点函数是否对State进行了不符合预期的修改导致序列化异常。解决确保前端或调用方在连续对话中传递稳定的session_id并在服务端将其设置为config{configurable: {thread_id: session_id}}。5.2 条件路由逻辑出现死循环或无法到达END现象某个流程一直卡住日志显示在几个节点间来回跳转或者永远无法结束。排查可视化你的图。LangGraph提供了app.get_graph().draw_mermaid_png()方法需要安装pygraphviz可以将图结构输出为图片。这是排查逻辑错误的神器一眼就能看出节点和边的连接关系是否如你所想。在路由函数add_conditional_edges指定的函数中增加详细的日志打印出做出路由决策时的完整State信息。经常发现是因为某个字段为None或类型不符合预期导致路由判断出错。检查是否有边指向了不存在的节点名或者条件边返回的字符串与add_conditional_edges中映射的键不匹配。解决为路由函数增加更健壮的异常处理和默认返回值。例如确保所有分支都有返回值并最终都能导向一个已知节点包括END。5.3 LLM调用超时或不稳定导致整个流程阻塞现象在call_llm节点由于OpenAI API偶尔响应慢或网络问题导致请求超时整个图执行被卡住进而拖累整个服务线程。排查这不是LangGraph的问题而是外部服务依赖问题。需要监控LLM API的响应时间P95 P99和错误率。观察LangGraph的执行线程/协程是否被长时间挂起。解决为节点函数设置超时使用asyncio.wait_for或threading模块的定时器为包含LLM调用的节点函数包装超时逻辑。超时后可以将状态标记为失败并路由到一个“降级处理”节点如返回一个预定义的提示。使用异步与并发控制确保你的app.invoke是在异步环境中调用如使用ainvoke。对于高并发场景可以考虑使用asyncio.Semaphore限制同时进行的LLM调用数量防止瞬间打爆上游API或耗尽本地资源。实现断路器模式如果某个LLM提供商持续超时或报错暂时熔断对该节点的调用快速失败并走备用流程。5.4 内存缓慢增长潜在内存泄漏现象服务运行一段时间后内存使用率持续缓慢上升。排查首先使用内存 profiling 工具如filprofiler对Python服务进行分析定位是哪些对象在累积。在LangGraph场景下常见的怀疑对象是缓存中没有正确设置TTL的LLM响应、在State中不断追加且从未清理的大型历史数据如完整的对话历史如果一直无限制追加、或者检查点版本快照积累过多。解决定期修剪State实现一个“清理”节点在对话轮数超过一定数量后只保留最近N轮的关键消息摘要将完整历史归档到外部存储。配置检查点保留策略研究你所用的Checkpointer是否支持配置自动清理旧的检查点。如果不支持需要写定时任务清理过期的checkpoint记录。审查工具函数确保所有自定义的工具函数或节点函数没有意外地持有对大对象的全局引用。6. 对团队协作与长期维护的思考引入LangGraph不仅仅是一次技术升级也对团队的开发模式带来了积极影响。6.1 设计文档即代码由于图的结构本身具有很好的可读性和可视化潜力我们现在要求每个新的Agent工作流都必须先绘制出状态图可以用Mermaid语法写在README里。这张图成为了产品经理、后端工程师、AI算法工程师之间的通用沟通语言。大家对“流程在何处分支”、“哪些节点可能失败”、“状态包含哪些信息”达成了共识减少了后续开发中的误解。6.2 测试策略的转变测试一个LangGraph应用我们将其分为三个层次单元测试节点测试单独测试每个节点函数。Mock掉LLM调用和外部依赖验证给定输入State函数是否返回正确的输出State。集成测试子图测试对于由几个节点组成的、逻辑紧密的子图例如“检索-生成”链将其编译成一个独立的子图进行测试验证数据流是否正确。端到端测试全图测试使用真实的开发环境配置但可能使用便宜的模型如gpt-3.5-turbo针对关键业务路径进行测试。这里大量使用app.invoke并断言最终的输出State和消息。6.3 版本管理与演进当业务逻辑需要变更时例如增加一个新的处理节点修改LangGraph图比修改传统面条代码要清晰得多。通常只需要1定义新节点函数2将其添加到图中3调整相关节点的边指向它。然后通过对比新旧图的Mermaid渲染图可以非常直观地审查变更影响范围。我们将图的定义StateGraph的构建代码视为重要的业务逻辑代码纳入Git版本控制并进行Code Review。7. 总结LangGraph是否适合你经过三个月的生产环境锤炼我对LangGraph的评价是它是一个非常专注于解决“复杂AI工作流编排”这一特定问题的优秀框架但并非银弹。你应该考虑使用LangGraph如果你的应用核心是多步骤、有状态、带条件分支的LLM调用流程。你需要可视化、可维护的方式来管理日益复杂的Agent逻辑。你对流程的可观测性、持久化中断恢复有强需求。你已经在使用LangChain希望平滑演进。你可能需要再考虑一下如果你的应用仅仅是简单的单轮问答QA没有复杂状态。直接用LangChain Chain甚至直接调用LLM API更简单。你的团队对Python异步编程和状态机概念不熟悉学习曲线可能会成为短期障碍。你的流程极度简单且稳定引入一个新的抽象层带来的复杂度超过其收益。对我而言LangGraph带来的最大价值是控制感的提升。当业务逻辑被清晰地定义在图中当任何一次对话的状态都可以被持久化和追溯当调试问题可以从跟踪“图执行路径”入手时我对运行在生产线上的AI智能体有了前所未有的信心。它让“AI应用工程化”向前迈出了扎实的一步。当然它依然是一个年轻的项目社区和最佳实践还在快速演进中但目前的稳定性和能力已经足以支撑起严肃的生产级应用了。