ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

LangGraph智能体编排实战:从问答机器到AI Agent的进化之路

LangGraph智能体编排实战:从问答机器到AI Agent的进化之路

LangGraph:让AI从"问答机器"进化为"智能体"

做了大半年AI应用开发,我最大的感受是:把大模型接上一个聊天窗口,那只是入门;真正让它"干活",才算入了门。这个"干活"的过程,在今年几乎被同一个词概括——智能体(Agent)。而在我实际把智能体落地到业务项目之后,我的结论是:LangGraph 是目前少有的、能把"AI干活"这件事讲清楚、做扎实的框架。

先说一个我自己的真实经历。之前我用纯 LangChain 搭过一个客服助手,本质上是"大模型 + Prompt + 检索",用户问一句,模型答一句,看似智能,一到需要查订单状态、调内部接口、跨多轮确认信息的场景就露馅。因为它没有一个清晰的"状态"概念,也没法让模型自主决定"下一步该调用什么工具"。后来我切换到 LangGraph 重写了一遍核心流程,整个系统的行为一下子从"有一搭没一搭聊天"变成了"按流程办事"。这篇文章我就想把这套思路完整拆开:为什么要用图来编排智能体、LangGraph 的核心机制是什么、工具调用如何打通、以及和低代码平台相比,用代码写的智能体到底强在哪里。

适合谁来读?如果你已经能熟练调用大模型 API,但面对"Agent 到底怎么设计""多工具协作怎么编排""状态怎么管理"这些问题还比较模糊,那这篇文章应该能给你一个清晰的起点。我会尽量把原理讲得直白,同时给出的代码可以直接复制去改。

1. 为什么大模型需要一张"工作流地图"

要说清楚 LangGraph 的价值,得先回答一个问题:纯靠 Prompt 的大模型,离"智能体"到底差了什么?

1.1 问答机器与智能体的本质区别

一个普通的问答模型,输入是"用户问题 + 上下文",输出是"一段文字"。这叫stateless,无状态。你问它"帮我查一下昨天上海到北京的航班",它能给出通用回答,但如果你接着说"那帮我订最早那班",它压根不记得刚才说的是哪个航班——准确来说,它没有边界的记忆机制,所有信息要么塞进上下文窗口,要么就丢了。

智能体则完全不同。它被设计成stateful,有状态,而且这个状态是显式的、可持久化的。它不只是"回复",而是"行动":解析用户意图、决定调用哪个工具、根据工具返回结果判断下一步动作、多轮迭代直到任务完成。要做到这一点,就需要一个东西来承载"流程控制"——告诉模型当前走到了哪一步、下一步有哪些选项、什么时候停。

我曾经用一句话给同事解释:**大模型是大脑,LangGraph 是神经回路。**大脑本身不知道手脚该按什么顺序动,是神经回路把"看到杯子→伸手→握住→送回来"串成了动作链。LangGraph 做的事情,就是为 AI 定义这样一组可执行、可回退、可控的神经回路。

1.2 为什么不是 LangChain 的 Chain,也不是单纯的函数调用

你可能要问:LangChain 里不早就有 Chain 和 Agent 了吗?为什么要换?

Chain 的问题是它太"死"。它把调用顺序写死成 DAG(有向无环图),比如"先检索、再生成、再格式化",一旦中间需要根据结果动态决定走哪条分支,代码就开始绕。而传统 Agent 的问题恰恰相反,它太"活"——没有固定流程,只能依赖 ReAct 循环反复调用 LLM 决定下一步,状态全部靠字符串拼接,调试起来极其痛苦。

LangGraph 站在中间:它保留图的结构化(你定义节点和边),但允许环的存在(循环是智能体的灵魂),同时把状态对象提升为一等公民。你既可以用它写确定性的流程,也可以写完全由模型自主决策的 ReAct 循环,还可以写"先走固定流程、遇到异常再让模型接管"的混合模式。这种灵活性是 Chain 和原生 Agent 都做不到的。

提示:我个人的建议是,不要把 LangGraph 当成"新框架"学,而是把它当成"状态机 + 图执行引擎"来理解。一旦你接受这个心智模型,后面的所有 API 都只是表达方式的问题。

1.3 一张图的抽象能力

LangGraph 的核心抽象其实很简单,就三样东西:

  • State:整个智能体的"共享内存",可以在节点之间传递和更新。它可以是 TypedDict、Pydantic Model 或任意自定义对象。
  • Node:一个函数或可调用对象,接收当前的 State,做一些处理(可能调 LLM、调工具、算个值),然后返回 State 的增量更新。
  • Edge:节点之间的连接,决定执行流向。分为普通边(固定流向)、条件边(根据 State 内容动态选择流向)、以及特殊的 START 和 END 节点。

就这么简单。但三个概念组合起来,就能表达任意复杂的工作流:顺序执行、分支、循环、人机协作、并行、超时控制……这也是为什么我用它一段时间后,感觉像拿到了编程语言级的控制力,而不是被某一个"Agent 模板"框住。

2. LangGraph 的四个核心支柱:State、Node、Edge、Checkpoint

从入门到真正能把 LangGraph 用好,我建议先把四个核心概念吃透。它们分别对应了系统的记忆、行为、路由和持久化。

2.1 State 不只是变量,是"唯一事实来源"

我最早犯的错误是把 State 当成普通 dict,随便往里面丢数据,结果节点之间传递越来越乱。后来才意识到,State 的设计直接决定了智能体的上限。

实际项目中,我会把 State 分成几类字段:

from typing import TypedDict, Annotated, List import operator from langgraph.graph import StateGraph, START, END class AgentState(TypedDict): # 输入输出 user_input: str final_answer: str # 中间记忆 messages: Annotated[List[dict], operator.add] # 自动累加 tool_results: dict # 控制字段 step_count: int need_human_review: bool

关键点是那个Annotated[List[dict], operator.add]。在 LangGraph 里,节点返回的更新不是覆盖整个 State,而是按字段做"归约"。operator.add意味着所有节点返回的 messages 都会自动拼接,而不是互相覆盖。这个机制非常实用:每个节点只需要返回自己新增的消息,不用担心搞乱全局历史。

我的建议是:把"会话消息历史"做成自动累加字段,把"工具结果"做成可覆盖字段,把"控制标记"做成普通字段。这样状态流转的任何异常,你都能通过打印 State 快速定位。

2.2 Node 的设计原则:一个节点只做一件事

LangGraph 鼓励你写小函数节点。我见过很多人一个节点里塞几十行代码:调 LLM、解析 JSON、调工具、再调 LLM……结果复用了好几次,改一处动全身。我自己现在坚持一个原则:一个节点只做一类操作,节点的名字要能直接看出来它是干嘛的。

async def call_llm_node(state: AgentState) -> AgentState: """调用 LLM 生成下一步决策或回复""" messages = state["messages"] response = await llm.ainvoke(messages) return {"messages": [response], "step_count": state["step_count"] + 1} async def execute_tool_node(state: AgentState) -> AgentState: """解析模型输出的工具调用并执行""" ai_message = state["messages"][-1] for tool_call in ai_message.tool_calls: result = await tools_by_name[tool_call["name"]].ainvoke(tool_call["args"]) state["tool_results"][tool_call["id"]] = result return {"tool_results": state["tool_results"]}

这样每个函数都能单独测试,出问题也能准确定位。其实这就是软件工程里的"单一职责",只不过换到了智能体场景。

2.3 Edge 的两种形态:确定性路由与模型决策路由

Edge 看似简单,但它是智能体"智能"的来源。LangGraph 支持两种路由:一种是写死的,A 执行完必须去 B;另一种是条件路由,根据 State 的某个字段在运行时决定下一步去哪。

典型的模型决策路由长这样:

def route_after_llm(state: AgentState) -> str: """根据模型输出决定下一步,是执行工具还是直接结束""" last_message = state["messages"][-1] if last_message.tool_calls: return "tools" # 有工具调用请求,去执行工具 return END # 没有工具调用,直接结束 graph = StateGraph(AgentState) graph.add_node("llm", call_llm_node) graph.add_node("tools", execute_tool_node) graph.add_edge(START, "llm") graph.add_conditional_edges("llm", route_after_llm, {"tools": "tools", END: END}) graph.add_edge("tools", "llm") # 关键:执行完工具后回到 LLM,形成循环 app = graph.compile()

注意最后这行graph.add_edge("tools", "llm"),它把流程变成了一个环:LLM → 工具 → LLM → 工具 → LLM → 结束。这就是 ReAct 循环的图表达。只要模型认为还需要调用工具,这个环就会继续转;一次不调用,就自然结束。

这里我提一个经验:**条件路由不要搞太多分支,会显著增加调试难度。**我一般控制在三种以内:去工具、结束、去人工审核。如果你发现路由分支超过五个,通常说明 State 设计出了问题,或者流程本身需要拆成多个子图。

2.4 Checkpoint:让智能体"断电后还能想起来"

如果说 State 是运行时的记忆,Checkpoint 就是持久化的记忆。LangGraph 内置了基于 Thread ID 的检查点机制,会在每个节点执行后自动保存状态快照。

from langgraph.checkpoint.memory import MemorySaver checkpointer = MemorySaver() app = graph.compile(checkpointer=checkpointer) # 第一次调用:新会话 config = {"configurable": {"thread_id": "user_123"}} result = await app.ainvoke({"user_input": "查一下昨天订单"}, config) # 第二次调用:同一会话,AI 记得上下文 result2 = await app.ainvoke({"user_input": "那我要退款"}, config)

Checkpoint 的价值不只是"记忆"。它意味着你可以随时中断一个智能体的执行、保存现场、几小时后再恢复;也意味着你可以在另一个进程里加载同一个线程状态继续跑。这在做长耗时任务、人工审批环节时几乎是刚需。生产环境中,建议把 MemorySaver 换成 PostgresSaver 或 RedisSaver,不然进程一重启记忆就没了。

3. 让 AI 真正"下地干活":工具调用的完整链路

智能体区别于聊天机器人的核心能力,就是它能够调用外部工具。LangGraph 的工具调用链路设计得相当精巧,值得仔细拆解。

3.1 从"想要"到"做到":tool_calls 的完整旅程

很多人第一次接触工具调用时都会困惑:模型怎么知道有哪些工具?调用参数怎么填?其实完整链路是这样的:

第一步,注册工具清单。你要把工具的"说明书"(函数名、参数、描述)传给模型。OpenAI 系模型支持tools参数,LangChain 的bind_tools()会自动把函数签名转成 JSON Schema。

第二步,模型输出结构化意图。当模型认为需要调用工具时,它不在回答里写文字,而是输出一个结构化的tool_calls字段,包含工具名和参数。这一步的关键是:决定权在模型,执行权在你。模型只负责"我想要查订单",不负责真的去数据库里查——这层安全边界非常重要。

第三步,你的代码执行工具并返回结果。LangGraph 中execute_tool_node拿到tool_calls后,调用真实函数,把结果包装成ToolMessage追加到消息历史。

第四步,模型根据工具结果决定下一步。工具结果作为上下文的一部分回到模型,模型继续推理:结果够了吗?够了就组织最终回答;不够就再调下一个工具。

下面是一个完整的工具注册和执行链路的简化示例:

from langchain_core.tools import tool from langchain_openai import ChatOpenAI @tool def get_order_status(order_id: str) -> str: """查询订单状态。参数 order_id 是用户订单号。""" # 这里实际是查数据库或调用内部 API return f"订单 {order_id} 状态为:已发货,预计3天后送达" tools = [get_order_status] llm = ChatOpenAI(model="gpt-4o", temperature=0) llm_with_tools = llm.bind_tools(tools)

这一步我犯过很多傻,最蠢的一次是忘了@tool装饰器,结果模型虽然"知道"有工具,执行时却找不到对应的 Python 函数。后来我把规则写死在代码规范里:任何需要模型自主调用的能力,第一步永远是@tool装饰器声明,第二步是在函数 docstring 里写清楚用途和参数,这两行决定了模型能不能正确理解工具。

3.2 工具调用的两个大坑:幻觉参数与错误吞掉

工具调用做得多了,你会发现两个高频问题。

第一个坑是模型会"编造"工具参数。比如用户说"查一下我昨天买的东西",模型可能猜一个订单号 "20240612" 就传进来了,而现实里订单号根本不是这个格式。解决思路有两个:一是把工具参数设计成"模糊可查",比如只传用户 ID,让工具内部去模糊匹配;二是在工具校验失败时返回一个"参数错误解释"给模型,让它基于错误信息重新推理。

第二个坑是工具执行异常被静默吞掉。早期我写的执行节点只处理成功结果,一旦工具内部抛异常,整个智能体直接崩掉,而用户只看到"服务开小差"。后来我改成这样:

async def safe_execute_tool(tool_call): try: result = await tools_by_name[tool_call["name"]].ainvoke(tool_call["args"]) return {"role": "tool", "name": tool_call["name"], "content": result} except Exception as e: return { "role": "tool", "name": tool_call["name"], "content": f"工具执行失败: {str(e)}。请尝试其他方式。" }

关键点:把错误信息变成一条正常格式的 ToolMessage 返回给模型,而不是中断整个图。模型收到错误信息后,会自己想办法,比如换参数、换工具,或者向用户解释没有查到。这比"直接崩掉"或"假装成功"都靠谱得多。

3.3 工具的"权限边界"设计:能查的不一定能删

工具调用的能力越强,风险越大。我给客户的智能体做工具清单时,强制要求每个工具标注"操作级别":

  • 只读型:查订单、搜知识库、查询天气——可以做,但要审计
  • 写入门槛型:提交表单、发消息——必须用户二次确认
  • 高危操作型:删除、退款、修改密码——需要额外的人工会签

LangGraph 里实现这个很简单:在条件路由判断里加一个need_human_review字段。一旦检测到高危工具被调用,不走"tools"边,而是走"human_review"节点,挂起等待人工确认再继续。这也是智能体落地时最容易被忽视的一点:技术上跑通不等于业务上敢用,权限边界一定是上线前要过的最后一道关。

4. 从零搭一个带记忆的实战智能体:订单客服 Agent

铺垫了这么多原理,是时候来一个完整的实战了。我拿一个非常常见的场景举例:电商订单客服智能体,要求它既能回答"我的订单到哪里了",又能处理"我要改收货地址",还要记住整个对话过程。

4.1 需求拆解与图设计

这个智能体的核心能力拆解如下:

  • 能识别用户意图(查订单 / 改地址 / 闲聊)
  • 查订单需要调用订单查询工具
  • 改地址需要先确认用户身份,再调用修改工具,且需要用户明确确认
  • 全程保持对话记忆
  • 多轮调用工具时(查完订单之后用户紧接着说改地址),能正确衔接

图结构设计如下:

START → 意图识别节点 → 条件路由 - 需要工具 → 执行工具节点 → 回到记忆增强 LLM → ... - 纯闲聊 → 直接回复 → END - 高危操作 → 人工确认节点 → 执行 → END

这个设计的好处是:普通问答很快,一次节点就结束;工具类需求会走循环;高危操作有专属挂起节点。这是 LangGraph 的典型优势——你可以对不同类型的请求给不同的处理路径。

4.2 核心代码实现

首先定义状态和工具:

from typing import TypedDict, List, Annotated import operator from langgraph.graph import StateGraph, START, END from langgraph.checkpoint.memory import MemorySaver from langchain_core.tools import tool from langchain_openai import ChatOpenAI class OrderState(TypedDict): messages: Annotated[List[dict], operator.add] need_human: bool pending_action: str order_db = { "1001": {"status": "已发货", "eta": "3天后", "address": "北京市朝阳区xx路1号"}, } @tool def query_order(order_id: str) -> str: """查询订单物流状态。参数 order_id 为订单号。""" info = order_db.get(order_id) if not info: return f"未找到订单 {order_id}" return f"订单{order_id}当前状态:{info['status']},预计{info['eta']}送达" @tool def update_address(order_id: str, new_address: str) -> str: """修改订单收货地址。参数 order_id 为订单号,new_address 为新地址。""" if order_id not in order_db: return f"未找到订单 {order_id}" order_db[order_id]["address"] = new_address return f"订单 {order_id} 的地址已修改为:{new_address}"

然后是节点:

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0) tools = [query_order, update_address] llm_with_tools = llm.bind_tools(tools) def manager_node(state: OrderState): """核心决策节点:决定是调工具、直接回复,还是需要人工""" result = llm_with_tools.invoke(state["messages"]) # 检查是否动了高危工具 if result.tool_calls and any(tc["name"] == "update_address" for tc in result.tool_calls): return {"messages": [result], "need_human": True, "pending_action": "update_address"} return {"messages": [result], "need_human": False} def tool_node(state: OrderState): last = state["messages"][-1] responses = [] for tc in last.tool_calls: resp = None if tc["name"] == "query_order": resp = query_order.invoke(tc["args"]) elif tc["name"] == "update_address": resp = update_address.invoke(tc["args"]) responses.append({"role": "tool", "name": tc["name"], "content": resp, "tool_call_id": tc["id"]}) return {"messages": responses} def human_confirm_node(state: OrderState): """人工确认后执行地址修改""" last = state["messages"][-1] if state.get("pending_action") == "update_address" and state.get("confirmed"): for tc in last.tool_calls: if tc["name"] == "update_address": resp = update_address.invoke(tc["args"]) return {"messages": [{"role": "tool", "name": "update_address", "content": resp, "tool_call_id": tc["id"]}]} return {"messages": [{"role": "assistant", "content": "已取消地址修改操作"}]}

最后是路由与组装:

def route_after_manager(state: OrderState) -> str: last = state["messages"][-1] if state.get("need_human"): return "human_confirm" if last.tool_calls: return "tools" return END builder = StateGraph(OrderState) builder.add_node("manager", manager_node) builder.add_node("tools", tool_node) builder.add_node("human_confirm", human_confirm_node) builder.add_edge(START, "manager") builder.add_conditional_edges("manager", route_after_manager, { "human_confirm": "human_confirm", "tools": "tools", END: END, }) builder.add_edge("tools", "manager") builder.add_edge("human_confirm", END) app = builder.compile(checkpointer=MemorySaver())

4.3 实测效果与调优记录

跑通以后,我拿几组真实对话来验证:

第一组:查订单

User: 帮我看看订单 1001 到哪了 Assistant: 订单 1001 当前状态:已发货,预计3天后送达。

链路:manager 识别 tool_call → 路由到 tools → 执行 query_order → 回到 manager → 无 tool_call,生成最终回答 → END。一次工具调用,干净利落。

第二组:后续追问

User: 那能帮我改到 2 号收吗? Assistant: 修改收货地址属于重要操作,需要您确认一下:确认将订单 1001 的地址修改为其他地址吗?

这里need_human被置为 True,流程挂起等人工确认。确认后,智能体才执行 update_address。

这组测试让我意识到一个调优点:高危操作不仅仅是"确认后执行",还需要在确认消息里把它要做的事明确复述一遍。一开始我的实现只是提示"是否确认修改",用户很容易误点。后来改成"确认将地址修改为 xxx,回复确认以继续",歧义就小多了。

第三组:闲聊

User: 你好,你们是谁? Assistant: 你好!我是订单助手,可以帮你查询订单状态或修改收货地址。

链路:manager 无 tool_calls → 直接 END,一个节点搞定。这种"想走流程就走流程,不想走就一句回完"的体验,是老式 Chain 很难做到的。

调优过程中我还遇到过一个问题:模型在工具返回后,偶尔会把工具结果重复输出给用户(比如把数据库里所有字段原样念一遍)。解决方式是在 system prompt 里写了一句"工具结果是给操作使用的,请用自然语言概括后回复用户"。这个细节虽然小,但对体验影响很大。

5. 落地时的工程化改造:从 Demo 到可上线的服务

图结构跑通,只是万里长征第一步。把 LangGraph 智能体接到真实业务里,还有一堆工程问题要处理。这里分享几个我实际踩过的坑和对应方案。

5.1 和 FastAPI 集成:注意流式输出与时间轮询

LangGraph 本身没有绑定任何 Web 框架,配合 FastAPI 非常自然。最简单的集成方式是把编译好的app对象放到 FastAPI 的路由里直接调用:

from fastapi import FastAPI from pydantic import BaseModel web_app = FastAPI() class ChatRequest(BaseModel): message: str thread_id: str @web_app.post("/chat") async def chat(req: ChatRequest): config = {"configurable": {"thread_id": req.thread_id}} result = await app.ainvoke({"messages": [{"role": "user", "content": req.message}]}, config) return {"reply": result["messages"][-1]["content"]}

看起来很顺,但生产环境里要注意一个关键问题:LLM 生成经常要 5-30 秒,HTTP 请求等不住。我实际用的是异步任务加前端轮询(或 SSE)的方案:请求进来先返回一个 task_id,后台 worker 跑智能体,前端轮询拿结果。LangGraph 的 Checkpoint 机制在这里帮了大忙——即使进程中途重启,只要 thread_id 还在,就能从最新检查点继续跑。

另一个小坑是并发。MemorySaver 只适合单进程调试,用 FastAPI 跑多 worker 时,我换成了 PostgresSaver。踩过一次"用户 A 的对话出现在用户 B 的消息里"的血泪之后,我彻底理解了为什么官方文档反复强调"生产环境务必用持久化 Checkpoint"。

5.2 记忆的两种类型:会话内记忆 vs 长期用户画像

LangGraph 的 Checkpoint 解决的是会话内记忆(对话历史),但更复杂的智能体还需要"长期记忆"——记住用户上次买过什么、偏好什么风格。前者是thread_id维度,后者是user_id维度。

我的做法是把长期记忆和短期记忆分开存:

  • 短期记忆:LangGraph 消息列表,存在 Checkpoint,按thread_id隔离
  • 长期记忆:存在数据库或向量库,按user_id关联,每次会话开始前注入到 system prompt

这个拆分在实用层面帮了我大忙。之前我一股脑把所有历史都塞给模型,token 消耗巨大,响应还慢。后来只把"用户画像摘要"(比如偏好、历史订单量)注入,对话历史的最近几轮走 Checkpoint,效果反而更好。你在设计自己的 Agent 时,一定要想清楚"哪些该记在会话里,哪些该记在用户档案里",这个决策直接影响成本和体验。

5.3 测试智能体的方法论:光靠"好不好用"是不行的

智能体没法像传统程序那样断言输出,但也没到"只能靠感觉"的地步。我给自己定了一套测试分级:

  • 单元级:单独测试每个工具函数,输入输出确定性强,按普通单测写
  • 图级:给固定的输入序列,断言"是否调用了某个工具"、"是否走到了 human_confirm"、"消息数量的走向是否符合预期"
  • 回归级:维护一组典型对话样本,每次改完图结构后全跑一遍,人工看关键节点输出,确保老的流程没被改坏

举个例子,"图级"测试可以这样写:

def test_query_order_flow(): result = app.invoke( {"messages": [{"role": "user", "content": "查订单 1001"}]}, config={"configurable": {"thread_id": "test-001"}} ) # 消息应该包含用户消息、AI tool_call、工具返回、最终回答,至少4条 assert len(result["messages"]) >= 4 # 最终回答中应包含订单状态关键词 final = result["messages"][-1]["content"] assert "已发货" in final

这套测试虽然不能保证"模型永远答对",但能保证"流程永远是按设计走的"。只要流程不出错,回答的质量更多取决于模型和 Prompt,而不是框架本身。

6. 平台型智能体 vs 代码型智能体:我的选型思考

最近"扣子""Dify"这类低代码智能体平台很火,热搜词里也频繁出现"平台搭建的智能体和用 Python 搭建的有什么不同"。这个问题的答案,我是在实际对比后才真正想明白的。两者定位完全不同。

6.1 平台的好处和天花板

低代码平台的价值在于快。我见过一个运营同事用扣子半天搭出一个客服 bot,连接了知识库、表格和几个 API,效果已经相当能打。它内置了常见节点:LLM、知识库检索、代码块、HTTP 请求、条件分支,所见即所得,不需要会写代码。

但平台的天花板也很明显:

  • 复杂逻辑难表达:循环嵌套、递归回溯、动态路由、多人协作状态机,拖拽界面很容易变成一团乱麻
  • 状态和持久化不可控:平台帮你管了 Checkpoint,但你拿不到底层接口,做不了精细化的会话恢复
  • 锁死风险:工作流在平台里,后期要迁移到自研系统,只能重写
  • 模型与工具受限:想接开源模型、私有化部署,或者调一些平台没封装的底层参数,基本没门

6.2 什么时候该选 LangGraph 这类代码框架

我的判断标准很简单:

  • 如果你的智能体是"知识问答 + 简单工具",用户量不大,用平台最快,别折腾
  • 如果你要做面向业务关键路径的智能体(比如支付流程、订单系统、企业内部审批),涉及复杂状态、要和现有系统深度集成、需要精细的权限控制和审计日志——选 LangGraph 这类代码框架

还有一点很多人没提:团队的长期能力积累。用代码框架写智能体,每一次踩坑、每一段封装,都沉淀成团队的代码资产和调试方法论。用平台搭,沉淀的是平台的操作经验——这个经验跟着平台走,不跟着你走。

我自己现在的分工是:快速原型用平台验证,正式系统的核心流程用 LangGraph 写,两者不冲突。但从"让智能体真正成为业务的一部分"这个目标来看,代码框架的掌控力是不可替代的。

7. 生产环境里那些文档没写透的注意点

文章最后,我把在生产环境里踩过的一些零碎坑集中列一下。这些点单独看都不大,组合起来却直接决定一个智能体项目能不能顺利上线。

第一,设置超时和最大迭代次数。ReAct 循环理论上可以无限转,模型一旦陷入"工具结果不如预期→换工具→再试"的死循环,你的 token 账单会很难看。LangGraph 里给递归限制很简单:app.invoke(..., {"recursion_limit": 15})。这个值根据你的流程复杂度设,我一般设 10-20。配合超时控制,基本能杜绝失控。

第二,所有工具调用的出入都要写日志。传统 API 的日志记录大家都会做,但工具调用的日志很容易被忽略。我的做法是:在tool_node里统一记录工具名、入参、出参、耗时。不要嫌麻烦——出了线上事故,这份日志就是你唯一的取证手段。尤其是涉及用户数据的工具,日志更是底线。

第三,谨慎对待"模型自主性"。由模型决定一切听起来很 AI,但真实业务里你往往需要"确定性"。比如用户说"退款",你肯定不希望模型自己决定退多少。我的原则是:模型负责理解意图和拆解任务,规则负责兜底重要参数和行为边界。能用规则写死的地方,不用模型自由发挥;模型的自由度只留给"理解"和"表达"这两个环节。

第四,关注 Prompt 和工具描述的迭代,而不是死磕框架。LangGraph 本身只负责编排,真正决定智能体聪明程度的,一半在模型,一半在你的工具和 Prompt。我花了最多时间优化的,是工具 docstring 的写法:从"修改订单地址"改成"修改订单收货地址,参数 order_id 必须是纯数字字符串,new_address 必须是完整详细的地址",模型的工具调用准确率立刻涨了一截。很多人跑来问我说你框架怎么配的,其实他们的问题是工具描述太含糊。

第五,用一个元信息字段标识每次输出的来源。我的 State 里始终有一个final_answer_source字段,记录这轮回答到底是来自"知识库检索"、"工具结果"、"模型直接生成"还是"兜底话术"。前端收到后可以展示不同的样式,更重要的是——当你需要分析用户满意度、改进回答质量时,这份来源数据能帮你看清楚模型在哪些场景下表现最差。这不是框架要求,但我觉得这是无数细节里最值回票价的一个。

结语:从"会说话"到"会办事",还差一个编排层

回看这大半年的实践,我的核心体会是:大模型的能力边界已经足够广,真正拉开差距的是你如何编排它。LangGraph 提供的图执行模型,恰好给了开发者一个平衡点——既不像纯代码那样什么都要自己写,又不像低代码平台那样被束缚住。

Quoting the title of my article: AI 从"问答机器"进化到"智能体",关键不是模型变聪明了多少,而是你给它修了一条什么样的"路"。有人用提示词修路,有人用平台修路,有人用 LangGraph 修路。我试了一圈之后,选择在关键业务上亲自修路,因为路得握在自己手里,车才能开得放心。

如果你是第一次接触 LangGraph,建议从本文第二节的四个核心概念开始,然后直接把第四节的项目抄下来跑一遍。跑通之后再去看官方文档的 advanced 部分(比如子图、并行节点、Human-in-the-loop),你会发现自己已经能读懂那些内容了。如果过程中遇到什么问题,欢迎随时交流——智能体这个方向,值得每个做 AI 应用的人投入时间。

返回列表