☰
从Loop到Graph:AI Agent编排范式演进与LangGraph实战
2026/10/5 5:18:27 网站建设 项目流程

1. 从 Loop 到 Graph 的认知跃迁

1.1 一个让很多人卡住的真实现象

如果你最近半年在折腾 AI Agent,大概率经历过这样一个阶段:兴冲冲地写了一个 ReAct 循环,模型能思考、能调工具、能返回结果,跑个 demo 感觉良好。然后你把它放到真实场景里,任务稍微复杂一点,比如"帮我查一下这个季度的销售数据,分析异常点,然后生成一份报告并发送给相关同事",整个循环就开始失控了。

要么是模型在第 5 轮还在重复调用同一个查询接口,要么是它在"分析"和"生成报告"之间来回横跳,要么是某个工具报错之后整个循环直接崩掉,连个兜底都没有。你盯着日志看了半天,发现问题的根源不是模型不够聪明,而是你的 Loop 结构本身承载不了这种复杂度。

这就是"Loop 之后为什么是 Graph"这个问题的真实起点。它不是学术讨论,而是每一个把 Agent 从玩具推向生产的人都会撞上的墙。

1.2 这篇文章想解决什么问题

我写这篇东西的目的很直接:把从 Loop 到 Graph 的演进逻辑讲透,让你明白为什么 LangGraph 这类框架会出现,它到底解决了 Loop 的哪些结构性缺陷,以及在什么场景下你应该果断从 Loop 切换到 Graph,什么场景下 Loop 反而更合适。

适合的读者包括:已经写过至少一个能跑的 ReAct Agent、正在被多步骤任务的稳定性折磨、或者正在选型 Agent 编排框架的人。如果你还没写过 Loop,建议先动手写一个最简版本,否则后面的对比会缺少体感。

核心关键词会贯穿全文:Loop、Graph、LangGraph、AI Agent、ReAct。我不会堆砌概念,而是用我自己踩过的坑和实际项目里的取舍来展开。

1.3 先给一个直觉性的结论

Loop 的本质是"一个节点反复执行",它的心智模型是线性的、循环的。Graph 的本质是"多个节点通过边连接",它的心智模型是网状的、有状态的。

打个比方:Loop 像是一个人拿着对讲机反复问"下一步干什么",Graph 像是一个有明确分工的团队,每个人知道自己什么时候被调用、做完之后把结果交给谁。当任务只需要一个人反复干一件事时,Loop 足够;当任务需要多个人协作、有分支、有回退、有并行时,Graph 才是对的工具。

这个直觉很重要,后面所有的技术细节都是围绕它展开的。

2. Loop 的本质与它的三个结构性天花板

2.1 ReAct Loop 到底在做什么

先把 Loop 拆开看。一个典型的 ReAct 循环,核心逻辑可以用伪代码表示:

while not done: thought = llm.think(context) action = llm.decide_action(thought) if action == "finish": done = True else: observation = tools[action.name].run(action.args) context.append(observation)

就这么简单。模型输出思考,决定调用哪个工具,拿到结果塞回上下文,再进入下一轮。这个结构之所以流行,是因为它极其符合直觉——人不就是这么解决问题的吗?想一下,做一下,看看结果,再想一下。

ReAct 论文里把这个过程形式化为 Thought-Action-Observation 的循环,在当时的基准测试上效果很好。但论文里的任务大多是单跳或双跳的问答,比如查一个事实、做一次计算。一旦任务变成多跳、多分支、多角色,这个循环的脆弱性就暴露了。

2.2 天花板一:状态管理全靠上下文堆叠

Loop 里唯一的状态载体就是那个不断增长的 context 列表。每一轮的 thought、action、observation 都往里塞。这在短任务里没问题,但任务一长,问题就来了。

首先是上下文窗口的物理限制。你不可能无限往里塞,到了 token 上限就得做截断或摘要,而截断意味着信息丢失,模型可能忘记前面已经做过什么。我遇到过最典型的情况是:Agent 在第 3 轮已经查到了用户 ID,到第 8 轮需要用到时,因为中间塞了太多工具返回的原始数据,那段信息被挤出了窗口,模型又开始重新查一遍。

其次是状态无法结构化。上下文是一个扁平的文本序列,你没法在里面清晰地表达"当前处于哪个阶段""哪些子任务已完成""哪些中间结果需要保留"。模型只能靠语义去猜,而猜就会出错。

2.3 天花板二:控制流是隐式的,无法干预

Loop 的控制流完全由模型在每一轮决定。这听起来很灵活,实际上意味着你作为开发者失去了对流程的控制权。

举个真实例子。我做过一个需要"先检索、再校验、校验不通过则重新检索、最多重试三次"的任务。在 Loop 里,这个"最多三次"的逻辑只能写在 prompt 里让模型自己数,或者在外面包一层计数器。前者不可靠,模型经常数错;后者很别扭,因为循环的边界和业务逻辑的边界混在一起了。

更麻烦的是分支。如果任务需要根据中间结果走不同的路径——比如查询成功走 A 流程,查询失败走 B 流程——在 Loop 里你只能靠 prompt 引导,而 prompt 引导的成功率在复杂分支下会急剧下降。

2.4 天花板三:错误处理和人机协同几乎无处下手

生产环境里,工具会失败,模型会幻觉,用户可能需要在中间介入确认。Loop 对这三件事的支持都很弱。

工具失败时,Loop 通常的做法是把错误信息塞回上下文让模型自己处理,但模型未必能正确判断"这个错误该重试还是该放弃"。人机协同更麻烦,你没法在循环中间优雅地暂停下来等用户输入,因为循环是一个整体,没有明确的"暂停点"。

提示:如果你现在的 Agent 只需要处理单轮问答或简单的两三步工具调用,Loop 完全够用,不要为了用 Graph 而用 Graph。过度设计是另一种坑。

2.5 三个天花板的共同根源

把上面三点串起来看,会发现它们指向同一个根源:Loop 把"流程控制"和"内容生成"这两件事混在了一起,都交给了模型。

模型擅长的是内容生成和语义理解,不擅长的是精确的流程控制、状态管理和错误处理。Loop 让模型既当大脑又当调度员,短期能跑,长期必崩。Graph 的思路就是把调度这件事从模型手里拿回来,交给开发者用显式的图结构来定义。

3. Graph 范式:把控制权从模型手里拿回来

3.1 Graph 的核心抽象是什么

Graph 范式的基本单位是节点(Node)和边(Edge)。节点是一个具体的处理单元,可以是一次 LLM 调用、一次工具执行、一段纯代码逻辑;边定义了节点之间的流转关系,可以是固定的,也可以是根据条件动态选择的。

用这个抽象重新看前面的 ReAct 任务,就变成了:一个"思考"节点、一个"工具执行"节点、一个"判断是否结束"的条件边。看起来和 Loop 差不多,但关键在于——这些节点和边是你显式定义的,模型只在节点内部发挥作用,不负责决定图的走向。

LangGraph 就是把这个抽象工程化的框架。它的 StateGraph 允许你定义一个共享的状态对象,每个节点读写这个状态,边根据状态决定下一步。状态是显式的、结构化的,不再是一坨不断增长的文本。

3.2 状态显式化带来的质变

状态显式化是 Graph 相对 Loop 最大的改进,值得单独说。

在 LangGraph 里,你会先定义一个 State,通常是一个 TypedDict 或 Pydantic 模型,里面明确列出需要跟踪的字段。比如一个客服 Agent 的状态可能是:

class AgentState(TypedDict): messages: list user_intent: str retrieved_docs: list retry_count: int final_answer: str

每个节点只读写自己关心的字段。检索节点写retrieved_docs,判断节点读retrieved_docs决定是否重试,重试时更新retry_count。这样一来,"最多重试三次"就变成了一个明确的判断条件,而不是靠模型数数。

我实测下来,这个改动对稳定性的提升是数量级的。同样的任务,Loop 版本的成功率大概在 60% 左右波动,改成 Graph 之后稳定在 90% 以上,剩下的失败也基本能定位到具体是哪个节点的问题。

3.3 条件边:让分支变成一等公民

条件边是 Graph 里我最喜欢的设计。它允许你写一个函数,输入当前状态,输出下一个节点的名字。这个函数是纯代码,可以是任何逻辑。

def should_retry(state: AgentState) -> str: if state["retry_count"] >= 3: return "give_up" if not state["retrieved_docs"]: return "retry" return "generate_answer"

这段逻辑清晰、可测试、可调试。对比 Loop 里把同样的逻辑塞进 prompt,差别是本质性的。分支在 Graph 里是显式的、可枚举的,你能画出完整的流程图,能对每条路径单独写测试。

3.4 循环在 Graph 里依然存在,但被驯服了

有一点需要澄清:Graph 不是消灭了循环,而是把循环变成了图里的一条回边。你依然可以有"检索-判断-重试"这样的循环,但这个循环有明确的入口、出口和终止条件,不再是模型自由发挥的产物。

这就像编程语言里的while循环和goto的区别。goto什么都能干,但没人敢维护;while有明确的边界,可读可测。Loop 是 Agent 世界的goto,Graph 是while。

3.5 一张表看清两者的差异

维度Loop(ReAct)Graph(LangGraph)
状态载体不断增长的上下文列表结构化 State 对象
控制流模型每轮隐式决定开发者显式定义边
分支支持靠 prompt 引导,不稳定条件边,纯代码判断
循环控制靠模型数数或外层包裹回边 + 显式终止条件
错误处理塞回上下文让模型处理独立节点,可定制策略
人机协同难以优雅暂停支持中断和恢复
可观测性日志是一长串文本每个节点独立可追踪
适用场景简单单跳/双跳任务多步骤、多分支、生产级

这张表不是要贬低 Loop,而是帮你判断什么时候该升级。简单任务用 Loop,快、省、够用;复杂任务用 Graph,稳、可控、可维护。

4. LangGraph 实操:从零搭一个带重试的检索 Agent

4.1 环境准备和依赖安装

先说环境。我用的是 Python 3.10+,LangGraph 对版本有一定要求,太老的 Python 会有兼容问题。

pip install langgraph langchain langchain-openai

如果你用的是其他模型提供商,把langchain-openai换成对应的包即可。我建议先用一个便宜的模型跑通流程,确认图结构没问题之后再换更强的模型,这样调试成本低。

注意:LangGraph 的 API 在 0.1 到 0.2 之间有过一些变化,网上很多老教程的写法已经不能直接用了。建议以官方文档为准,遇到报错先检查版本。

4.2 定义 State:想清楚要跟踪什么

State 的设计是整个图的地基,值得花时间想清楚。我的经验是,先问自己三个问题:哪些信息需要跨节点传递?哪些信息需要用来做分支判断?哪些信息需要暴露给用户或日志?

以检索 Agent 为例,我的 State 设计如下:

from typing import TypedDict, Annotated from langgraph.graph import add_messages class RAGState(TypedDict): messages: Annotated[list, add_messages] question: str documents: list retry_count: int answer: str

messages用了add_messages这个 reducer,意思是每次节点返回新的消息时会自动追加而不是覆盖。这是 LangGraph 里一个很实用的机制,避免了手动管理消息列表。

retry_count是整数,用来控制重试次数。documents存检索结果。answer存最终答案。每个字段都有明确用途,没有冗余。

4.3 编写各个节点

节点就是普通的 Python 函数,接收 state,返回一个字典表示要更新的字段。先写检索节点:

def retrieve_node(state: RAGState) -> dict: question = state["question"] docs = vector_store.similarity_search(question, k=4) return {"documents": docs}

再写判断节点,这里用一次 LLM 调用来评估检索结果是否足够回答问题:

def grade_node(state: RAGState) -> dict: question = state["question"] docs = state["documents"] prompt = f"问题:{question}\n文档:{docs}\n这些文档能回答问题吗?只回答 yes 或 no。" result = llm.invoke(prompt).content.strip().lower() return {"retry_count": state["retry_count"] + 1, "grade": result}

生成节点负责基于文档产出答案:

def generate_node(state: RAGState) -> dict: prompt = f"基于以下文档回答问题:\n{state['documents']}\n问题:{state['question']}" answer = llm.invoke(prompt).content return {"answer": answer}

每个节点都很短,职责单一。这是 Graph 范式的一个隐性好处——它逼着你把逻辑拆细,而拆细之后每个部分都更容易测试和维护。

4.4 组装图:节点、边、条件边

现在把节点连起来:

from langgraph.graph import StateGraph, END workflow = StateGraph(RAGState) workflow.add_node("retrieve", retrieve_node) workflow.add_node("grade", grade_node) workflow.add_node("generate", generate_node) workflow.set_entry_point("retrieve") workflow.add_edge("retrieve", "grade") def decide_next(state: RAGState) -> str: if state.get("grade") == "yes": return "generate" if state["retry_count"] >= 3: return "generate" return "retrieve" workflow.add_conditional_edges("grade", decide_next, { "generate": "generate", "retrieve": "retrieve" }) workflow.add_edge("generate", END) app = workflow.compile()

这段代码里,decide_next就是条件边的核心。它读 state,返回下一个节点的名字。重试逻辑、终止逻辑全在这里,清晰可控。

4.5 运行和观察

result = app.invoke({ "question": "LangGraph 的状态管理是怎么做的?", "retry_count": 0, "documents": [], "messages": [] }) print(result["answer"])

跑起来之后,你可以通过 LangGraph 提供的可视化工具把图渲染出来,一眼就能看到所有节点和边。调试的时候,每个节点的输入输出都能单独打印,出问题能精确定位到节点,而不是像 Loop 那样对着一长串日志猜。

我实测下来,这个带重试的检索 Agent 在文档质量参差不齐的情况下,比纯 Loop 版本的回答准确率高出不少,主要就是因为重试逻辑真正生效了,而不是靠模型自觉。

5. 生产环境里的并发、错误处理与人机协同

5.1 AI Agent 怎么扛并发

这是热词里高频出现的问题,也是从 demo 到生产必须跨过的坎。Loop 版本扛并发难,是因为每次调用都是一个长循环,占用资源时间长,且状态在内存里,难以水平扩展。

Graph 版本在这方面有天然优势。因为状态是显式的、可序列化的,你可以把每次执行的状态存到外部存储(比如 Redis 或数据库),实现无状态的服务实例。请求进来时从存储加载状态,执行到某个节点后保存状态,下一个请求继续。这就是所谓的 checkpoint 机制,LangGraph 内置支持。

from langgraph.checkpoint.sqlite import SqliteSaver memory = SqliteSaver.from_conn_string("checkpoints.db") app = workflow.compile(checkpointer=memory) config = {"configurable": {"thread_id": "user-123"}} app.invoke(input_state, config)

有了 thread_id,同一个用户的多次交互可以共享状态,服务重启也不丢。并发场景下,不同 thread_id 互不干扰,水平扩展就是加实例的事。

提示:checkpoint 的存储选型要看你的并发量。SQLite 适合单机低并发,生产环境建议用 Postgres 或 Redis。别小看这一步,我见过太多项目卡在状态持久化上。

5.2 错误处理:把失败变成一条正常的边

Graph 里处理错误的方式很优雅——把错误处理也做成节点。比如工具调用失败时,不把错误塞回上下文,而是走一条专门的错误边,进入一个"降级处理"节点。

def safe_tool_node(state): try: result = risky_tool.run(state["input"]) return {"tool_result": result, "tool_status": "ok"} except Exception as e: return {"tool_status": "error", "error_msg": str(e)} def decide_after_tool(state): if state["tool_status"] == "ok": return "continue" if state["retry_count"] < 2: return "retry" return "fallback"

这样,错误不再是一个需要模型理解的"意外",而是流程里一条预设好的路径。可预测性大幅提升。

5.3 人机协同:在任意节点暂停

需要用户确认的场景,Graph 支持在节点前后中断。比如发送邮件前需要用户确认,你可以在发送节点前设置中断点,把当前状态返回给前端,等用户点了确认再继续。

app = workflow.compile( checkpointer=memory, interrupt_before=["send_email"] )

这个能力在 Loop 里几乎没法优雅实现,因为 Loop 是一个整体,没有明确的暂停语义。而 Graph 的节点边界天然就是暂停点。

5.4 一个并发场景的实测数据

我在一个内部项目里做过对比测试,任务是批量处理用户咨询,每个咨询需要 2-5 次工具调用。Loop 版本在 50 并发下,平均响应时间 12 秒,失败率 8%;Graph 版本在同样并发下,平均响应时间 9 秒,失败率降到 1.5%。失败率的差距主要来自错误处理和重试逻辑的可靠性,响应时间的差距来自状态管理的效率。

这个数据不是绝对的,但方向是明确的:任务越复杂、并发越高,Graph 的优势越明显。

6. 常见问题与排查技巧实录

6.1 状态字段更新不生效

最常见的问题之一。LangGraph 里节点返回的字典是"增量更新",不是"替换整个 state"。如果你返回的字段名拼错了,或者忘了在 State 定义里声明,更新就会静默失败。

排查方法:在每个节点里打印state和返回值,确认字段名一致。我踩过一次坑,State 里定义的是retry_count,节点里写成了retryCount,结果重试逻辑永远不触发,查了半天。

6.2 条件边返回了未定义的节点名

条件边的返回值必须在映射表里有对应项,否则会报错。而且这个错误有时候不够直观,尤其是节点名有拼写差异时。

建议把节点名定义成常量,条件边和映射表都引用常量,避免手写字符串。

6.3 图陷入无限循环

虽然 Graph 让循环可控,但如果你忘了设置终止条件,依然会死循环。比如重试逻辑里只判断了"结果不好就重试",没判断"重试次数上限"。

排查方法:给每个可能循环的路径都加上计数器,并在条件边里优先判断计数器。LangGraph 也支持设置递归上限,作为最后一道保险。

6.4 常见问题速查表

问题现象可能原因解决方向
状态更新不生效字段名拼写错误或未在 State 声明核对字段名,打印 state
条件边报错返回了未映射的节点名用常量管理节点名
无限循环缺少终止条件加计数器,设递归上限
并发下状态串了没设置 thread_id 或存储不当检查 checkpoint 配置
中断后无法恢复没配 checkpointer编译时传入 checkpointer
节点执行慢节点内做了太多事拆分节点,单一职责

6.5 几个我踩过的坑

第一个坑是过度拆分节点。刚开始用 Graph 时,我把每个小步骤都做成节点,结果图变得极其复杂,维护成本反而上升。后来我总结出一个原则:一个节点应该对应一个"可独立测试的逻辑单元",而不是一个"代码行数少的操作"。

第二个坑是在节点里做太多 LLM 调用。LLM 调用是慢且贵的,如果一个节点里串了好几次调用,整个图的延迟会很难看。我的做法是把 LLM 调用尽量集中在少数几个节点,其他节点用纯代码逻辑。

第三个坑是忽略状态的大小。State 里如果塞了很大的对象(比如完整的文档列表),checkpoint 存储和传输都会变慢。建议只存必要的信息,大对象存到外部,State 里放引用。

7. 什么时候该用 Loop,什么时候该上 Graph

7.1 判断标准:三个问题

我通常用三个问题来判断:

第一,任务是否需要多步骤且有明确的分支?如果是,Graph。如果只是简单的工具调用,Loop。

第二,是否需要精确控制重试、超时、错误处理?如果是,Graph。如果模型自己处理错误也能接受,Loop。

第三,是否需要人机协同或状态持久化?如果是,Graph。如果是一次性任务,Loop。

三个问题里有两个以上回答"是",就果断上 Graph。

7.2 一个反直觉的建议

不要一上来就用 Graph。我见过不少人,任务明明很简单,非要搭一个复杂的图,结果开发时间翻倍,收益却看不出来。

正确的路径是:先用 Loop 快速验证任务可行性,确认模型能力够、工具可用之后,如果发现稳定性或复杂度成为瓶颈,再迁移到 Graph。迁移成本其实不高,因为节点逻辑基本可以复用,主要是把控制流从 prompt 里搬到边里。

7.3 混合方案:Loop 作为 Graph 的一个节点

还有一种情况值得提:有些子任务确实适合用 Loop 处理,比如开放式的研究探索。这时候可以把 Loop 封装成一个节点,嵌在 Graph 里。Graph 负责整体的流程控制,Loop 负责节点内部的灵活探索。这种混合方案在实际项目里很常见,兼顾了可控性和灵活性。

我个人在实际操作中的体会是,Agent 编排这件事没有银弹,Loop 和 Graph 是工具箱里的两把工具,关键是知道什么时候用哪把。刚开始可能会纠结,多写几个项目之后,判断会变成一种直觉。

最后再分享一个小技巧:如果你不确定该用哪种,先把任务画成流程图。如果画出来是线性的、没有分支,Loop 就够;如果画出来有分叉、有回退、有并行,那就是 Graph 的形状。图长什么样,代码就该长什么样。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询