1. 从补全代码到描述意图:编程范式正在发生什么变化
过去两年,我身边做开发的朋友分成了很明显的两拨。一拨人还在纠结“Copilot 补全的代码能不能直接用”,另一拨人已经开始对着编辑器说“帮我把这个页面的加载状态改成骨架屏,顺便把错误重试加上”,然后看着 AI 一口气改完五六个文件。后面这种工作方式,圈子里给了它一个挺形象的名字——Vibe Coding。这个词最早由 Andrej Karpathy 提出来,核心意思就是:你不再逐行敲代码,而是用自然语言描述你想要的感觉和结果,让大模型去生成、修改、串联整个实现,你负责判断“对不对味”。
但 Vibe Coding 只解决了“写”的问题。当你真的想做一个能自己查资料、自己调工具、自己决定下一步干什么的 AI 应用时,光靠对话式生成就不够了。你会遇到状态怎么保存、多个步骤怎么编排、出错怎么回滚、循环怎么控制这些工程问题。这时候 LangGraph 就进入了视野。它是 LangChain 团队推出的一个专门用来构建有状态、多步骤 AI 工作流的框架,把整个流程建模成一张图,节点是动作,边是流转条件,状态在整张图里共享和传递。
这篇文章我想聊的就是这条演进线:从 Vibe Coding 这种“意图驱动”的编码方式,到 LangGraph 这种“图驱动”的 AI 应用编排框架,中间到底发生了什么,为什么会有这个转变,以及一个普通开发者该怎么上手。不管你是刚接触 LLM 应用开发的新手,还是已经用 LangChain 写过一些链式调用的老手,都能从里面找到可以直接抄作业的东西。我会尽量把原理讲透,把踩过的坑摊开,把能复现的步骤写清楚。
2. Vibe Coding 到底是什么:意图驱动编码的底层逻辑
2.1 从“写代码”到“描述意图”的转变
传统编程里,开发者的大脑里要先有一棵完整的逻辑树:输入是什么、分支怎么走、异常怎么处理、数据结构怎么设计,然后把这些翻译成具体的语法。Vibe Coding 把这个过程倒过来了。你只需要描述“我想要什么”,大模型负责把它翻译成可运行的代码。这背后依赖的是 LLM 对大量代码语料的学习,它见过足够多的模式,所以能根据你的意图补全出合理的实现。
我自己的体验是,用这种方式写前端页面和写脚本特别快。比如你说“做一个带搜索过滤的表格,数据从本地 JSON 读,支持按名称和日期排序”,它几秒钟就能给你一个能跑的版本。你不需要记住某个 API 的具体参数,也不需要查文档,描述清楚就行。这种效率提升是实打实的,尤其是做原型和验证想法的时候。
但这里有个关键前提:你得能判断它写出来的东西对不对。Vibe Coding 不是让你放弃理解代码,而是把精力从“怎么写”转移到“写什么”和“对不对”上。如果你完全看不懂生成的代码,那出了问题你也没法修,这就很危险。
2.2 Vibe Coding 的适用边界与典型场景
Vibe Coding 最适合的场景有几个共同特征:逻辑相对独立、边界清晰、不需要复杂的状态管理。比如写一个数据清洗脚本、做一个静态页面、生成一些测试用例、把一段旧代码翻译成新语言,这些都很适合。它的优势在于快速试错,你可以在几分钟内看到好几个不同版本的实现,然后挑一个最顺眼的继续改。
但一旦涉及到多步骤的 AI 工作流,Vibe Coding 就开始吃力了。举个例子,你想做一个“自动调研助手”:先让模型理解用户问题,然后决定要不要搜索,搜索完再总结,总结完再判断信息够不够,不够就换个关键词再搜。这个流程里有条件分支、有循环、有状态累积,你很难用一句“帮我写一个调研助手”就让模型生成一个稳定可靠的实现。就算它生成了,你也很验证它每一步到底在干什么。
提示:Vibe Coding 生成的代码一定要跑一遍再信。我见过太多看起来没问题、一跑就报错的例子,尤其是涉及异步和状态的部分。
2.3 为什么 Vibe Coding 会自然过渡到 LangGraph
当你用 Vibe Coding 的方式去构建 AI 应用时,你会发现一个规律:简单的单轮对话很快就能搞定,但稍微复杂一点的需求就会逼着你去设计流程。而一旦你开始设计流程,你就会想要一个东西来帮你管理流程。这个东西就是 LangGraph。
LangGraph 的出现不是为了替代 Vibe Coding,而是承接了 Vibe Coding 搞不定的那部分。你可以继续用 Vibe Coding 的方式快速生成节点内部的逻辑,但节点之间怎么连、状态怎么传、循环怎么停,这些交给 LangGraph 来管。两者其实是互补的:Vibe Coding 负责“写”,LangGraph 负责“编排”。
3. LangGraph 核心概念拆解:图、状态与节点
3.1 为什么是“图”而不是“链”
LangChain 最早的核心抽象是 Chain,也就是把多个步骤串成一条线。这个模型很简单,A 的输出给 B,B 的输出给 C,一路走到底。但现实中的 AI 工作流很少是一条直线。你可能需要根据模型的输出决定下一步走哪个分支,可能需要回到之前的步骤重新来一遍,可能需要在多个步骤之间共享一份不断更新的状态。这些用 Chain 来表达就很别扭。
LangGraph 换了一个思路:把整个工作流建模成一张有向图。图里有节点和边,节点代表一个具体的操作,边代表节点之间的流转关系。最关键的是,这张图可以带环,也就是说你可以从节点 A 走到节点 B,再从节点 B 走回节点 A,形成循环。这个特性让 LangGraph 天然适合表达“反复思考直到满意”这类 AI 工作流。
我刚开始用的时候也有点不习惯,觉得图比链复杂。但用多了就发现,图的表达能力确实强很多。你可以在图上清晰地看到整个流程长什么样,哪个节点负责什么,什么条件下会走哪条边。这种可视化带来的可维护性提升,在项目变复杂之后特别明显。
3.2 State:整个工作流的共享记忆
LangGraph 里最重要的概念是 State。你可以把它理解成整个工作流共享的一块内存,所有节点都能读它、写它。State 通常是一个字典或者一个带类型定义的对象,里面放着工作流运行过程中需要传递的所有信息。
举个例子,如果你做一个客服机器人,State 里可能包含:用户原始问题、对话历史、当前意图分类、检索到的知识库片段、最终回复。每个节点负责更新 State 里的某一部分,下一个节点读取更新后的 State 继续处理。这种设计让节点之间解耦了,每个节点只需要关心自己那部分逻辑,不需要知道上游是谁、下游是谁。
State 的定义方式直接决定了工作流的清晰度。我踩过的一个坑是:一开始把所有东西都塞进一个巨大的字典里,结果节点之间互相覆盖,调试起来非常痛苦。后来改成用 TypedDict 明确定义每个字段的类型和用途,并且约定好哪个节点负责写哪个字段,问题就少了很多。
3.3 Node 与 Edge:动作与流转规则
Node 就是图里的一个执行单元。它可以是调用一次 LLM、执行一次检索、跑一段 Python 函数、调用一个外部 API,基本上任何你想做的事情都可以封装成一个节点。每个节点接收当前的 State,执行自己的逻辑,然后返回一个更新后的 State(通常是部分更新)。
Edge 定义了节点之间怎么走。最简单的 Edge 是固定边,A 执行完直接去 B。更常用的是条件边,也就是根据 State 里的某个值决定下一步去哪个节点。比如你有一个“意图判断”节点,它输出“咨询”就去检索节点,输出“投诉”就去转人工节点。这种条件分支用 LangGraph 表达非常自然。
还有一个很实用的概念叫入口点和终点。入口点是工作流开始的地方,终点是结束的地方。你可以设置多个终点,比如“成功结束”和“失败结束”走不同的节点。整个图跑起来就是从入口点出发,沿着边一路走到某个终点。
4. 从零搭建一个 LangGraph 工作流:完整实操
4.1 环境准备与依赖安装
先把环境搭起来。我习惯用 conda 建一个独立环境,避免和系统里的其他包冲突。Python 版本建议 3.10 以上,因为 LangGraph 用到了一些较新的类型语法。
conda create -n langgraph-demo python=3.11 conda activate langgraph-demo pip install langgraph langchain langchain-openai如果你用的是其他模型提供商,把langchain-openai换成对应的包就行。LangGraph 本身不绑定模型,它只负责编排,模型调用是通过 LangChain 的接口来的。
注意:API 密钥不要硬编码在代码里。用环境变量或者
.env文件管理,提交代码前检查一下有没有把密钥带上去。我见过不止一次因为密钥泄露导致账单暴涨的案例。
4.2 定义 State 结构
我们做一个简单的例子:一个能判断问题类型并给出不同回复的助手。先定义 State:
from typing import TypedDict, Literal class AssistantState(TypedDict): user_input: str category: Literal["question", "complaint", "other"] response: str这里user_input是用户输入,category是分类结果,response是最终回复。用Literal限定取值范围,这样类型检查能帮你提前发现一些错误。
4.3 编写节点函数
接下来写三个节点:分类节点、问答节点、投诉节点。
def classify_node(state: AssistantState) -> dict: user_input = state["user_input"] # 实际项目中这里调用 LLM 做分类 if "怎么" in user_input or "如何" in user_input: category = "question" elif "投诉" in user_input or "不满" in user_input: category = "complaint" else: category = "other" return {"category": category} def answer_node(state: AssistantState) -> dict: return {"response": f"关于「{state['user_input']}」,我的回答是..."} def complaint_node(state: AssistantState) -> dict: return {"response": "非常抱歉给您带来不便,我们会尽快处理。"}每个节点接收 State,返回一个字典表示要更新的字段。LangGraph 会自动把这些更新合并到全局 State 里。
4.4 构建图并编译运行
现在把节点和边组装起来:
from langgraph.graph import StateGraph, START, END builder = StateGraph(AssistantState) builder.add_node("classify", classify_node) builder.add_node("answer", answer_node) builder.add_node("complaint", complaint_node) builder.add_edge(START, "classify") def route_by_category(state: AssistantState) -> str: if state["category"] == "question": return "answer" elif state["category"] == "complaint": return "complaint" else: return "answer" builder.add_conditional_edges("classify", route_by_category) builder.add_edge("answer", END) builder.add_edge("complaint", END) graph = builder.compile() result = graph.invoke({"user_input": "这个功能怎么用?"}) print(result["response"])跑一下就能看到输出。这个例子虽然简单,但把 LangGraph 的核心要素都用到了:State 定义、节点函数、条件边、入口和终点。
4.5 加入循环:让工作流自己决定要不要继续
上面那个例子是一条路走到黑。LangGraph 真正强大的地方是支持循环。我们改一下,加一个“质量检查”节点,如果回复质量不达标就回到生成节点重新生成。
class LoopState(TypedDict): question: str draft: str quality: str attempts: int def generate_node(state: LoopState) -> dict: attempts = state.get("attempts", 0) + 1 return {"draft": f"第{attempts}版草稿", "attempts": attempts} def check_node(state: LoopState) -> dict: if state["attempts"] >= 3: return {"quality": "pass"} return {"quality": "retry"} builder = StateGraph(LoopState) builder.add_node("generate", generate_node) builder.add_node("check", check_node) builder.add_edge(START, "generate") builder.add_edge("generate", "check") def should_continue(state: LoopState) -> str: if state["quality"] == "pass": return END return "generate" builder.add_conditional_edges("check", should_continue) graph = builder.compile() result = graph.invoke({"question": "测试", "attempts": 0})这个循环最多跑三次,每次生成后检查,不通过就回去重新生成。这种模式在 AI 工作流里非常常见,比如让模型反复修改直到满足某个条件。
5. LangChain 与 LangGraph 的关系:不是替代,是分工
5.1 两者定位的差异
很多人搞不清楚 LangChain 和 LangGraph 到底什么关系,网上搜“langchain和langgraph的区别”也是高频问题。我的理解是:LangChain 是一套工具箱,提供了和 LLM 交互的各种组件,比如模型封装、提示词模板、输出解析器、检索器、工具调用接口。LangGraph 是在这些组件之上的一层编排框架,负责把这些组件按某种流程组织起来。
打个比方,LangChain 像是厨房里的各种食材和厨具,LangGraph 像是菜谱和烹饪流程。你可以只用 LangChain 做一道简单的菜,但如果要做一桌宴席,就需要 LangGraph 来安排先后顺序和协调。
5.2 什么时候用 LangChain,什么时候用 LangGraph
如果你的需求是单轮问答、简单的 RAG 检索、一次性的文本处理,用 LangChain 的 Chain 就够了,没必要上 LangGraph。LangGraph 的价值在于流程复杂到一定程度之后:有多个步骤、有条件分支、有循环、需要维护状态、需要人工介入。
我自己的判断标准是:如果你画流程图的时候发现需要画箭头回指,那就该用 LangGraph 了。如果是一条直线走到底,LangChain 的 Chain 更简单直接。
5.3 实际项目中的混合使用方式
在实际项目里,两者通常是混着用的。节点内部的逻辑用 LangChain 的组件来实现,比如在某个节点里调用 LLM、解析输出、检索向量库。节点之间的流转用 LangGraph 来管理。这样既享受了 LangChain 丰富的组件生态,又获得了 LangGraph 强大的编排能力。
提示:不要为了用 LangGraph 而用 LangGraph。如果你的流程真的很简单,硬套图结构只会增加复杂度。
6. 常见问题与排查技巧实录
6.1 State 更新不生效或互相覆盖
这是新手最容易遇到的问题。LangGraph 默认对 State 的更新是合并式的,但如果你在多个节点里同时更新同一个字段,后面的会覆盖前面的。解决办法是明确每个字段的写入责任,或者使用 reducer 来定义合并逻辑。比如你可以给某个字段指定一个operator.add作为 reducer,这样多个节点的更新会累加而不是覆盖。
6.2 条件边返回值不匹配导致流程卡住
条件边函数必须返回一个目标节点的名称或者 END。如果你返回了一个不存在的节点名,图会报错。我建议把节点名定义成常量,条件边函数里引用常量而不是硬编码字符串,这样改名字的时候不容易漏。
6.3 循环没有终止条件导致死循环
带环的图一定要有明确的退出条件。我见过有人写了一个“反思-改进”循环,但退出条件写错了,结果跑了几百轮还没停。建议在循环里加一个最大迭代次数,达到上限就强制退出,避免意外消耗。
6.4 调试时看不到中间状态
LangGraph 支持流式输出,你可以用graph.stream()来逐步查看每个节点的输出。调试的时候这个功能特别好用,能看到 State 在每个节点之后变成了什么样。另外,LangGraph 还支持持久化,可以把每一步的状态存下来,方便回放和排查。
| 问题现象 | 可能原因 | 排查方向 |
|---|---|---|
| 流程提前结束 | 条件边返回了 END | 检查条件判断逻辑 |
| 节点没被执行 | 边没连对 | 打印图的边结构确认 |
| State 字段丢失 | 节点返回了空字典 | 确认每个节点都返回了更新 |
| 循环停不下来 | 退出条件永远不满足 | 加最大迭代次数兜底 |
6.5 关于密钥和鉴权信息泄露的防范
使用 LLM 时,密钥管理是个必须重视的问题。我的做法是:本地开发用.env文件,并且把.env加到.gitignore里;生产环境用环境变量或者密钥管理服务。另外,在日志里打印请求信息时要注意脱敏,不要把完整的请求头打出来。还有一点容易被忽略:如果你把代码分享给别人或者发到网上,先检查一下有没有不小心把密钥带进去。
7. 这套东西还能怎么扩展
把 LangGraph 跑通之后,你会发现很多之前觉得麻烦的事情变得可行了。比如你可以做一个多智能体协作的系统,每个智能体是一个子图,它们之间通过消息传递来协调。也可以做一个带人工审核的流程,在关键节点暂停,等人确认后再继续。还可以把持久化打开,让工作流支持断点续跑,这在处理长任务时特别有用。
我个人的体会是,LangGraph 最大的价值不是让你写出更复杂的流程,而是让你能把复杂流程写清楚。当一张图摆在面前,每个节点的职责、每条边的条件都一目了然的时候,维护和迭代就变成了一件可控的事情。这比在一堆嵌套的 if-else 里找 bug 要舒服太多了。
最后分享一个小技巧:刚开始用 LangGraph 的时候,先用纸把流程图草图画出来,标清楚每个节点干什么、什么条件下走哪条边、State 里需要哪些字段。图画清楚了,代码就是翻译工作,会快很多。如果图画不清楚,那说明你对流程的理解还不够,这时候写代码大概率会返工。