从Vibe Coding到LangGraph:LLM应用状态编排实战
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 的输出给 BB 的输出给 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 生态的对应实现RRFReciprocal Rank Fusion是多路检索结果融合的常用算法。它的核心思想是对每一路检索结果按排名给分排名越靠前分越高然后把多路分数加起来重新排序。去重逻辑的缺陷通常出在同一个文档在不同路里 ID 不一致或者分数相同但顺序不稳定这两种情况。如果你在做混合检索比如向量检索 关键词检索融合前一定要确保两路的文档 ID 是同一套体系否则去重会失效同一个文档会出现两次。这个坑我在做本地知识库问答的时候踩过后来统一用文档内容的 hash 作为 ID 才解决。5. 从 Vibe 到 LangGraph一个完整重构案例5.1 需求背景一个自动化工单分类与处理流程假设我们要做一个工单处理智能体需求是这样的接收一条用户工单文本判断工单类别技术问题、账单问题、投诉、其他根据类别决定处理路径技术问题检索知识库生成回复账单问题查询订单系统生成回复投诉标记高优先级转人工其他生成通用回复回复生成后做一次质量检查不合格就重做最多重试 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 retry5.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(checkpointermemory_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_limitLLM 输出解析失败返回格式不稳定用结构化输出 兜底解析流程恢复后状态错乱Checkpointer 配置不一致确认 thread_id 和 checkpointer 后端一致Token 消耗异常高上下文累积过多精简 State只传必要字段7. 我对 AI 编程范式演进的一点个人判断写到这里我想跳出具体技术聊聊我对这个方向的理解。Vibe Coding 和 LangGraph 看起来是两个层面的东西一个偏交互方式一个偏工程架构但它们其实指向同一个趋势程序员的核心能力正在从写代码转向定义问题、设计流程、验收结果。以前我们花大量时间在语法、API、边界条件上现在这些越来越多地被模型接管。但这不意味着程序员变得不重要了恰恰相反能把一个模糊需求拆解成清晰的节点和边、能设计出合理的状态流转、能预判哪里会出问题并加上兜底这些能力变得比以前更值钱。LangGraph 这类框架的价值不在于它让你少写代码而在于它给了你一套表达复杂逻辑的清晰语言。当你的智能体从一问一答进化到多步骤协作当你的应用从demo进化到生产可用这套语言就是你和团队沟通、和未来的自己沟通的基础。我个人的建议是先用 Vibe Coding 快速验证想法一旦发现流程开始出现分支、循环、状态累积立刻切换到 LangGraph 重构。不要等到代码烂成一团再动手那时候重构成本会高得多。最后分享一个我一直在用的小技巧每次设计一个新的 LangGraph 流程先在纸上把图画出来标清楚每个节点的输入输出和每条边的条件。图画清楚了代码基本就是照着抄。图没画清楚就动手写代码十有八九要返工。这个习惯帮我省下的时间比我学任何框架都多。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →