☰
从Vibe Coding到LangGraph:LLM应用状态编排实战
2026/9/24 22:46:11 网站建设 项目流程

1. 从补全代码到描述意图:编程范式正在发生什么变化

过去两年,我身边做开发的朋友分成了很明显的两拨。一拨还在纠结 Copilot 的补全准不准、Tabnine 的索引快不快,另一拨已经彻底换了工作方式——他们不再一行行敲代码,而是用自然语言把意图"说"给模型听,让模型生成整个函数、整个模块,甚至整个项目骨架。后面这拨人嘴里经常挂着一个词:Vibe Coding。

这个词最早由 Andrej Karpathy 提出来,核心意思很直白:你不再逐字逐句地写代码,而是凭"感觉"和"意图"去描述你想要什么,让 LLM 把代码吐出来,你负责判断、调整、验收。听起来像是偷懒,但真正用过一段时间的人会发现,它改变的其实不是"写代码的速度",而是"人和代码之间的关系"。以前你是代码的作者,现在你更像是一个导演,模型是演员,你负责给方向、给反馈、给边界。

但 Vibe Coding 走到今天,暴露出的问题也越来越明显。单轮对话生成一个函数很爽,可一旦任务变成"帮我做一个能查数据库、能调外部接口、能根据结果决定下一步动作的智能体",纯靠聊天窗口就彻底失控了。模型会忘上下文、会重复劳动、会在关键分支上做出你完全没预期的选择。这时候,LangGraph这类专门为 LLM 应用设计的状态编排框架就登场了。

我写这篇东西,不是要给你科普概念,而是想把这两年里我自己从"纯 Vibe"到"用 LangGraph 重构"的完整心路和踩坑记录摊开讲。如果你正在用 LLM 做产品、做内部工具,或者只是想让自己的 AI 编程方式更靠谱一点,这篇应该能帮你少走不少弯路。全文会围绕 Vibe Coding 的边界、LangGraph 的核心机制、LangChain 与 LangGraph 的区别、以及实际落地时的工程细节展开,尽量说人话,也尽量给能直接抄的配置和代码。

2. Vibe Coding 到底是什么:别被名字骗了

2.1 它解决的不是"写代码慢",而是"启动成本高"

很多人第一次听到 Vibe Coding,第一反应是"这不就是让 AI 帮我写代码吗,有什么新鲜的"。但真正用起来你会发现,它和传统的代码补全有本质区别。

传统补全的逻辑是:你写for i in range(,它帮你补10):。你仍然是作者,AI 只是加速器。而 Vibe Coding 的逻辑是:你说"帮我写一个函数,输入是一个用户 ID 列表,输出是这些用户最近 7 天的活跃天数,按活跃天数降序排列,如果用户不存在就跳过",模型直接给你一整段可运行的代码。你从"写"变成了"描述"。

这个转变带来的最大价值,是启动成本被压到了极低。以前你要写一个脚本处理 CSV,得先想好用 pandas 还是 csv 模块,要不要处理编码,异常怎么捕获,写下来至少十几分钟。现在你一句话,十秒钟出结果,跑一下不对再改描述。对于探索性任务、一次性脚本、原型验证,这种方式的效率提升是数量级的。

但这里有个很多人忽略的点:Vibe Coding 的效率优势,只在"任务边界清晰、验收标准明确"的时候才成立。一旦任务变成多步骤、有状态、需要根据中间结果做决策,纯 Vibe 就会迅速崩盘。

2.2 纯 Vibe 的三个致命边界

我自己在项目里踩过的坑,总结下来主要是三类。

第一类是上下文漂移。你在一个对话窗口里聊了二十轮,前面定义的变量名、数据结构、业务规则,模型到后面就开始记混。你让它改一个函数,它顺手把你之前定好的字段名改了,你还得回头去核对。窗口越长,这种漂移越严重。

第二类是状态丢失。LLM 本身是无状态的,每次调用都是独立的。你让它先查数据库、再根据结果调接口、再根据接口返回决定要不要发通知,这一串动作之间的状态,模型自己是记不住的。你得手动把上一步的输出塞进下一步的 prompt 里,塞着塞着就乱了。

第三类是分支不可控。真实业务里大量逻辑是"如果 A 就做 X,否则做 Y"。纯 Vibe 模式下,你只能靠 prompt 里写"如果……那么……",但模型会不会真的按你的分支走,全靠运气。它可能把两个分支都执行了,也可能跳过条件直接给你一个看似合理但完全错误的结果。

这三类问题的本质是一样的:Vibe Coding 擅长生成"点",不擅长编排"线"和"面"。而真实应用恰恰是由大量的线和面构成的。这就是为什么我们需要 LangGraph。

2.3 一个生活化的类比

你可以把 Vibe Coding 想象成"点外卖"。你想吃什么,说一句,外卖送到,很方便。但如果你要办一场二十人的家宴,需要先买菜、再洗切、再分几道菜同时开火、中间还要根据火候调整,你不可能靠"点外卖"完成。你需要的是一个厨房调度系统:谁负责哪道菜、什么时候下锅、哪道菜好了先上、哪道菜糊了要重做。

LangGraph 就是这个调度系统。它不负责"做菜"(那是 LLM 的事),它负责"决定谁在什么时候做什么、做完之后下一步去哪"。把 Vibe Coding 的生成能力和 LangGraph 的编排能力结合起来,才是 AI 时代编程范式真正完整的样子。

3. LangGraph 的核心机制:把 LLM 应用当成状态机来写

3.1 为什么是"图"而不是"链"

在 LangGraph 出现之前,大家用 LangChain 的 Chain 来串 LLM 调用。Chain 的本质是线性流水线:A 的输出给 B,B 的输出给 C,一路到底。这在简单场景下够用,但一旦遇到循环、条件分支、并行执行,Chain 就非常别扭。

LangGraph 换了个思路:把整个应用建模成一张有向图。图里有节点(Node)和边(Edge)。节点是一个个执行单元,可以是一次 LLM 调用、一次工具调用、一段普通 Python 函数;边决定了执行完一个节点之后去哪个节点。最关键的是,LangGraph 支持条件边和循环,这意味着你可以表达"如果结果不满足要求就回到上一步重做"这种真实业务里极其常见的逻辑。

我自己的理解是:Chain 是"流水线",Graph 是"状态机"。流水线只能往前走,状态机可以根据当前状态决定下一步去哪,甚至可以绕回来。这个差别看起来小,实际用起来是天壤之别。

3.2 State:整个图的"共享内存"

LangGraph 里最重要的概念是State。你可以把它理解成整张图的共享内存,所有节点都能读它、写它。State 通常用一个 TypedDict 或者 Pydantic 模型来定义,里面放的就是你这个应用需要流转的所有数据。

举个我实际项目里的例子。做一个客服工单处理智能体,State 大概长这样:

from typing import TypedDict, Annotated from langgraph.graph import add_messages class TicketState(TypedDict): messages: Annotated[list, add_messages] ticket_id: str category: str priority: str resolved: bool retry_count: int

这里messages用了add_messages这个 reducer,意思是每次节点往 messages 里写东西,是"追加"而不是"覆盖"。其他字段默认是覆盖语义。这个 reducer 机制是 LangGraph 很精髓的地方,它让你能精确控制每个字段的合并方式,避免多节点并发写同一个字段时互相覆盖。

提示:State 的字段设计直接决定了你后面写节点顺不顺手。我的经验是,凡是需要跨节点传递的信息,全部放进 State,不要试图靠闭包或者全局变量传。全局变量在并发场景下会出大问题。

3.3 Node 和 Edge:执行单元与控制流

Node 就是一个普通函数,签名是def node_name(state: State) -> dict。它接收当前 State,返回一个字典,字典里的键会被合并回 State。Node 里可以干任何事:调 LLM、查数据库、发 HTTP 请求、做数据清洗。

Edge 分两种。普通边add_edge("a", "b")表示 a 执行完无条件去 b。条件边add_conditional_edges("a", router_func, mapping)表示 a 执行完后,调用router_func(state)得到一个字符串,再根据 mapping 决定去哪个节点。

这个设计的好处是,控制流和业务逻辑彻底解耦。你的节点只管"做事",路由函数只管"判断下一步",两者互不干扰。这比把所有 if-else 塞进一个大函数里清晰太多了。

3.4 Checkpointer:让状态可以持久化和恢复

LangGraph 还有一个我觉得被严重低估的能力:Checkpointer。它能在每一步执行后自动把 State 存下来,支持内存、SQLite、Postgres 等多种后端。这意味着什么?

意味着你的智能体可以中断后恢复。用户聊到一半关掉页面,下次回来还能接着聊。意味着你可以做人工审核节点:流程走到某一步暂停,等人确认后再继续。意味着你可以做时间旅行调试:回放任意一步的 State,看看到底是哪一步出了问题。

我在做长流程任务(比如自动生成报告、多轮数据核对)的时候,Checkpointer 几乎是必开的。没有它,一旦中间某步失败,整个流程就得从头再来,浪费大量 token 和时间。

4. LangChain 和 LangGraph 到底什么关系:别再搞混了

4.1 一句话说清区别

网上关于"LangChain 和 LangGraph 的区别"的搜索量一直很高,说明很多人确实被绕晕了。我用一句话概括:LangChain 是工具箱,LangGraph 是编排引擎。

LangChain 提供的是各种组件:LLM 封装、Prompt 模板、输出解析器、向量库接口、各种工具(搜索、计算、数据库)。它解决的是"单个环节怎么做"的问题。LangGraph 提供的是把这些环节组织起来的方式,它解决的是"多个环节怎么串、怎么分支、怎么循环"的问题。

两者不是替代关系,是互补关系。你完全可以在 LangGraph 的节点里调用 LangChain 的组件。实际上这也是最常见的用法。

4.2 用一张表看清差异

维度LangChainLangGraph
核心抽象Chain、Agent、ToolGraph、Node、Edge、State
控制流线性为主,Agent 内部隐式循环显式图结构,支持任意分支和循环
状态管理靠 Memory 组件,较弱一等公民,State + Reducer + Checkpointer
可调试性链路长时较难追踪每步状态可查、可回放
适合场景单轮问答、简单 RAG、工具调用多步骤智能体、复杂工作流、需要人工介入
学习曲线组件多,容易迷路概念少,但需要理解状态机思维

4.3 什么时候该用哪个

我的判断标准很简单:如果你的流程能用一张流程图画出所有分支和循环,就用 LangGraph;如果只是"输入→处理→输出"一条直线,LangChain 的 Chain 就够了。

具体来说,下面这些场景我强烈建议直接上 LangGraph:

  • 需要根据中间结果决定下一步做什么(条件分支)
  • 需要反复尝试直到满足某个条件(循环重试)
  • 需要多个步骤之间共享和累积状态
  • 需要人工审核或中断恢复
  • 需要并行执行多个子任务再汇总

而下面这些场景,LangChain 的简单 Chain 完全够用,没必要上 LangGraph:

  • 单轮问答
  • 简单的 RAG(检索→拼接→生成)
  • 一次性的文本处理流水线

注意:不要为了用而用。我见过有人把三行代码能搞定的线性流程硬套 LangGraph,结果多了一堆样板代码,维护成本反而更高。工具是拿来解决问题的,不是拿来炫技的。

4.4 关于 LangChain4j 和 RRF 去重的一个小插曲

搜索热词里出现了"langchain 和 langchain4j 的默认 rrf 实现去重逻辑存在缺陷",这个点挺有意思,值得单独说两句。LangChain4j 是 Java 生态的对应实现,RRF(Reciprocal Rank Fusion)是多路检索结果融合的常用算法。它的核心思想是:对每一路检索结果,按排名给分,排名越靠前分越高,然后把多路分数加起来重新排序。

去重逻辑的缺陷通常出在"同一个文档在不同路里 ID 不一致"或者"分数相同但顺序不稳定"这两种情况。如果你在做混合检索(比如向量检索 + 关键词检索),融合前一定要确保两路的文档 ID 是同一套体系,否则去重会失效,同一个文档会出现两次。这个坑我在做本地知识库问答的时候踩过,后来统一用文档内容的 hash 作为 ID 才解决。

5. 从 Vibe 到 LangGraph:一个完整重构案例

5.1 需求背景:一个自动化工单分类与处理流程

假设我们要做一个工单处理智能体,需求是这样的:

  1. 接收一条用户工单文本
  2. 判断工单类别(技术问题、账单问题、投诉、其他)
  3. 根据类别决定处理路径
  4. 技术问题:检索知识库,生成回复
  5. 账单问题:查询订单系统,生成回复
  6. 投诉:标记高优先级,转人工
  7. 其他:生成通用回复
  8. 回复生成后,做一次质量检查,不合格就重做(最多重试 2 次)

如果用纯 Vibe Coding,你会在一个对话窗口里跟模型来回聊,让它写一个巨大的函数,里面塞满 if-else 和 LLM 调用。能跑,但一旦某类工单处理出问题,你很难定位是哪一步的 prompt 有问题,也很难单独测试某个分支。

用 LangGraph 重构之后,整个流程变成一张清晰的图,每个节点职责单一,可以单独测试,可以单独调 prompt。

5.2 状态设计

from typing import TypedDict, Literal, Annotated from langgraph.graph import add_messages class TicketState(TypedDict): messages: Annotated[list, add_messages] raw_ticket: str category: Literal["tech", "billing", "complaint", "other"] | None draft_reply: str | None quality_score: float | None retry_count: int final_reply: str | None

这里category用 Literal 限定取值范围,避免模型返回意料之外的类别。retry_count用来控制重试次数,防止无限循环。

5.3 节点实现

分类节点:

def classify_node(state: TicketState) -> dict: prompt = f"""判断以下工单的类别,只返回一个词: tech / billing / complaint / other 工单内容:{state['raw_ticket']}""" result = llm.invoke(prompt).content.strip().lower() if result not in ["tech", "billing", "complaint", "other"]: result = "other" return {"category": result}

技术问题处理节点:

def tech_node(state: TicketState) -> dict: docs = retriever.invoke(state["raw_ticket"]) context = "\n".join([d.page_content for d in docs]) prompt = f"""根据以下知识库内容回答工单: 知识库: {context} 工单:{state['raw_ticket']} 请给出专业、简洁的回复。""" reply = llm.invoke(prompt).content return {"draft_reply": reply}

质量检查节点:

def quality_check_node(state: TicketState) -> dict: prompt = f"""评估以下回复的质量,从 0 到 1 打分,只返回数字: 工单:{state['raw_ticket']} 回复:{state['draft_reply']}""" score = float(llm.invoke(prompt).content.strip()) return {"quality_score": score, "retry_count": state["retry_count"] + 1}

5.4 路由函数

def route_by_category(state: TicketState) -> str: return state["category"] def route_by_quality(state: TicketState) -> str: if state["quality_score"] >= 0.7: return "accept" if state["retry_count"] >= 2: return "accept" # 重试超限,接受当前结果 return "retry"

5.5 组装图

from langgraph.graph import StateGraph, START, END builder = StateGraph(TicketState) builder.add_node("classify", classify_node) builder.add_node("tech", tech_node) builder.add_node("billing", billing_node) builder.add_node("complaint", complaint_node) builder.add_node("other", other_node) builder.add_node("quality_check", quality_check_node) builder.add_edge(START, "classify") builder.add_conditional_edges("classify", route_by_category, { "tech": "tech", "billing": "billing", "complaint": "complaint", "other": "other", }) for node in ["tech", "billing", "other"]: builder.add_edge(node, "quality_check") builder.add_edge("complaint", END) builder.add_conditional_edges("quality_check", route_by_quality, { "accept": END, "retry": "tech", # 简化处理,实际应根据类别回到对应节点 }) graph = builder.compile(checkpointer=memory_saver)

这段代码看起来比一个大函数长,但它的价值在于:每个节点可以独立测试,每条边可以独立调整,整个流程可视化。当线上出问题时,你能一眼看出是哪一步卡住了。

5.6 重构前后的对比

维度纯 Vibe 版本LangGraph 版本
代码组织一个几百行的大函数多个小节点,职责单一
调试难度出问题只能看日志猜每步 State 可查可回放
重试逻辑靠 prompt 里写"如果不合格就重做"显式条件边,可控
新增类别改大函数,容易引入 bug加一个节点和一条边
人工介入很难实现加一个中断节点即可
Token 消耗上下文越来越长,浪费严重每步只传必要状态

6. 实操中的坑与排查技巧

6.1 无限循环:最常见也最致命

LangGraph 支持循环,这是它的优势,也是它最容易出事的地方。我见过最典型的情况是:质量检查一直不通过,重试节点一直跑,token 烧了一大堆,流程还没结束。

解决办法有两个。一是在 State 里加计数器,像上面例子里的retry_count,超过阈值就强制走 accept 分支。二是设置图的递归上限,graph.compile()时可以传recursion_limit参数,默认是 25,超过就抛异常。我一般两个都加,双保险。

提示:递归上限不要设太大。我见过有人设成 1000,结果一个 bug 导致流程跑了半小时才报错,账单直接爆了。25 到 50 之间是比较合理的范围。

6.2 状态字段被意外覆盖

前面提到过,State 字段默认是覆盖语义。如果你有两个节点并发执行,都往同一个字段写,后写的会覆盖先写的。解决办法是给这个字段加 reducer,比如Annotated[list, operator.add]表示追加,或者自定义一个合并函数。

我踩过的具体坑是:并行执行三个检索节点,每个都往documents字段写结果,结果只有最后一个节点的结果被保留。后来改成Annotated[list, operator.add]才正常。

6.3 LLM 返回格式不稳定

这是所有 LLM 应用的通病。你让它返回 JSON,它有时候返回带 markdown 代码块的 JSON,有时候返回纯 JSON,有时候还给你加一句"好的,以下是结果"。在 LangGraph 里,这种不稳定会直接导致路由函数解析失败。

我的做法是:所有需要结构化输出的地方,都用 Pydantic 模型 + 结构化输出能力。LangChain 提供了with_structured_output方法,能强制模型按 schema 返回。如果模型不支持,就退而求其次,用正则把 JSON 抠出来,再加一层 try-except 兜底。

from pydantic import BaseModel class Classification(BaseModel): category: Literal["tech", "billing", "complaint", "other"] structured_llm = llm.with_structured_output(Classification) result = structured_llm.invoke(prompt)

6.4 密钥泄露:一个必须重视的安全问题

搜索热词里有"使用 llm 时如何防止密钥等鉴权信息泄露",这个问题在 LangGraph 项目里尤其重要,因为你的图里可能有很多节点,每个节点都可能调外部服务。

我的几条硬性规则:

  • 密钥一律走环境变量,绝不硬编码在代码里
  • 用.env文件管理本地开发密钥,.env必须进.gitignore
  • 生产环境用密钥管理服务,不要用明文环境变量
  • 日志里打印 State 时,先过滤掉敏感字段
  • 如果 State 里需要存用户凭证,用单独的加密字段,不要混在普通字段里

注意:LangGraph 的 Checkpointer 会把 State 持久化到数据库。如果你的 State 里有敏感信息,数据库就成了泄露点。要么加密存储,要么在持久化前脱敏。

6.5 常见问题速查表

问题现象可能原因排查方向
流程卡住不动条件边路由函数返回了未映射的值检查 router 返回值是否在 mapping 里
状态字段丢失并发写同一字段,被覆盖加 reducer 或改为串行
无限循环重试条件永远不满足加计数器 + recursion_limit
LLM 输出解析失败返回格式不稳定用结构化输出 + 兜底解析
流程恢复后状态错乱Checkpointer 配置不一致确认 thread_id 和 checkpointer 后端一致
Token 消耗异常高上下文累积过多精简 State,只传必要字段

7. 我对 AI 编程范式演进的一点个人判断

写到这里,我想跳出具体技术,聊聊我对这个方向的理解。

Vibe Coding 和 LangGraph 看起来是两个层面的东西,一个偏"交互方式",一个偏"工程架构",但它们其实指向同一个趋势:程序员的核心能力正在从"写代码"转向"定义问题、设计流程、验收结果"。

以前我们花大量时间在语法、API、边界条件上,现在这些越来越多地被模型接管。但这不意味着程序员变得不重要了,恰恰相反,能把一个模糊需求拆解成清晰的节点和边、能设计出合理的状态流转、能预判哪里会出问题并加上兜底,这些能力变得比以前更值钱。

LangGraph 这类框架的价值,不在于它让你少写代码,而在于它给了你一套表达复杂逻辑的清晰语言。当你的智能体从"一问一答"进化到"多步骤协作",当你的应用从"demo"进化到"生产可用",这套语言就是你和团队沟通、和未来的自己沟通的基础。

我个人的建议是:先用 Vibe Coding 快速验证想法,一旦发现流程开始出现分支、循环、状态累积,立刻切换到 LangGraph 重构。不要等到代码烂成一团再动手,那时候重构成本会高得多。

最后分享一个我一直在用的小技巧:每次设计一个新的 LangGraph 流程,先在纸上把图画出来,标清楚每个节点的输入输出和每条边的条件。图画清楚了,代码基本就是照着抄。图没画清楚就动手写代码,十有八九要返工。这个习惯帮我省下的时间,比我学任何框架都多。

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

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

立即咨询