从Vibe Coding到LangGraph:LLM流程编排与状态管理实战
1. 从补全代码到描述意图编程范式正在发生什么变化过去两年我身边做开发的朋友分成了很明显的两拨。一拨人还在纠结 Copilot 的补全准不准、Tabnine 的索引快不快另一拨人已经开始对着编辑器说“帮我把这个页面的加载状态改成骨架屏顺便把错误重试加上指数退避”然后看着代码自己长出来。后面这拨人干的这件事圈子里现在有个挺形象的说法叫Vibe Coding——你不再逐行敲代码而是用自然语言描述你想要的“感觉”和“行为”让模型去填充实现细节。这个词最早是 Andrej Karpathy 在 2025 年初提出来的时候很多人以为只是又一个营销噱头。但我实际用下来发现它描述的是一种真实存在的状态切换你的注意力从“语法怎么写”转移到了“意图是什么”。以前写一个带重试的 HTTP 请求你得想requests还是httpx、异常怎么捕获、退避算法用哪个现在你只需要说清楚“失败要重试三次间隔翻倍超时 10 秒”剩下的交给模型。这不是偷懒这是把人的认知带宽从机械劳动里解放出来放到真正需要判断力的地方。但 Vibe Coding 有个绕不过去的天花板它擅长生成片段不擅长维持结构。你让它写一个函数它写得又快又好你让它维护一个跨越十几个文件、有状态流转、有分支判断的完整业务流程它就开始胡言乱语了。这就是为什么我们需要LangGraph这类东西——它把 LLM 从“代码补全器”升级成了“流程编排引擎”让 AI 能在一个有向图里按照你定义的节点和边去执行任务而不是每次都在一个孤立的 prompt 里重新开始。这篇文章我想聊的就是这条演进线从 Vibe Coding 这种“对话式生成”的浅层交互到 LangGraph 这种“图结构编排”的深层控制中间到底发生了什么、为什么要这么变、以及你如果现在想上手具体该怎么落地。不管你是刚听说 LangChain 想入门的新手还是已经在用 LLM 做自动化但被状态管理折磨过的老手下面这些内容应该都能让你少走点弯路。2. Vibe Coding 的真实边界它解决了什么又卡在了哪里2.1 为什么“描述意图”比“编写语法”更高效我先说一个我自己的真实场景。上个月我要给一个内部工具加一个“批量导出 CSV 并压缩成 zip”的功能。按老办法我得查csv模块的DictWriter用法、zipfile的压缩级别参数、文件流什么时候关闭、中文编码怎么处理。这一套下来哪怕我熟也得花二十分钟。但那次我直接对着编辑器说“写一个函数接收一个字典列表导出成 UTF-8 BOM 的 CSV然后用 zip 压缩返回字节流。”模型十秒钟给了我一段能跑的代码我只改了一个地方——它默认用了utf-8而不是utf-8-sig导致 Excel 打开乱码。这个例子里Vibe Coding 的价值不是“快”而是“跳过检索”。人的大脑在工作记忆里同时维持语法细节和业务逻辑是很累的模型帮你把语法细节那一层接过去了你只需要检查业务逻辑对不对。这跟当年从汇编到 C 语言、从 C 到 Python 是同一种性质的进步——抽象层级又往上走了一级。但这里有个关键前提你得能判断它写得对不对。我见过不少新手模型给什么就用什么结果一个 SQL 注入漏洞藏在字符串拼接里或者一个except: pass把真正的错误吞掉了。Vibe Coding 不是让你放弃判断而是让你把判断力从“这行代码语法对不对”转移到“这个实现方案有没有隐患”。后者其实更难但价值也更高。2.2 单轮对话的天花板状态、分支与长流程Vibe Coding 最舒服的场景是“单文件、单函数、无状态”的任务。一旦你开始涉及多步骤流程问题就来了。我举个具体的例子做一个“用户上传合同 PDF → 提取关键条款 → 比对标准模板 → 生成差异报告 → 发送邮件”的流程。你用 Vibe Coding 的方式可能会写五个独立的 prompt每个 prompt 处理一步。但很快你会发现几个要命的问题。第一个问题是状态传递。第一步提取出来的条款怎么传给第二步你可能会说“存到变量里再拼进下一个 prompt”。但合同可能很长条款可能几十条你拼进去之后 prompt 就爆了。而且模型每次都是无状态的它不记得上一步做了什么你得把上下文全部重新喂一遍。第二个问题是分支判断。如果提取出来的条款里没有“违约责任”这一项流程应该走“标记缺失”还是“跳过比对”这种条件分支在纯 prompt 链里很难优雅地表达你只能写一堆 if-else 把 prompt 拼来拼去代码很快就变成意大利面条。第三个问题是错误恢复。第三步比对失败了你想重试但重试的时候不想重新跑第一步和第二步。纯 prompt 链没有“检查点”的概念你只能从头再来浪费 token 也浪费时间。这三个问题归结起来就是一句话Vibe Coding 没有“流程”这个一等公民。它把每次交互都当成独立的、无记忆的对话而真实业务是有状态、有分支、有生命周期的。这就是 LangGraph 要解决的核心问题。2.3 从“对话”到“图”思维方式的转变LangGraph 的思路其实很朴素既然业务流程天然就是一张图——节点是步骤边是流转条件——那我就直接用图来建模。你定义几个节点比如“提取条款”“比对模板”“生成报告”定义它们之间的边比如“比对成功 → 生成报告”“比对失败 → 重试或告警”然后让 LLM 在每个节点里干活。整个图的执行状态由一个共享的 state 对象维护每个节点读写这个 state框架负责调度和持久化。这个转变的意义在于你把“控制流”从 prompt 里抽出来了。以前你得在 prompt 里写“如果你看到 A 就做 B否则做 C”现在你把这些逻辑写在图的边上prompt 只需要专注做好当前节点那一件事。这就像从“一个巨大的 main 函数”重构为“多个小函数加一个调度器”可维护性和可测试性完全不是一个量级。我自己的体会是用 Vibe Coding 写代码你是在跟一个很聪明但没记性的实习生对话用 LangGraph 编排流程你是在给一个团队写 SOP。前者适合快速原型后者适合要上生产的系统。3. LangGraph 核心机制拆解状态、节点与边到底怎么配合3.1 StateGraph 的设计哲学共享状态是唯一的真相源LangGraph 里最核心的抽象叫StateGraph。你可以把它理解成一个“带类型定义的共享白板”。你首先定义一个 state 的结构比如from typing import TypedDict, Annotated from langgraph.graph import StateGraph, START, END import operator class ContractState(TypedDict): pdf_path: str raw_text: str clauses: list[str] missing_items: list[str] report: str retry_count: Annotated[int, operator.add]这个ContractState就是整张图里所有节点共享的数据结构。每个节点是一个函数接收当前 state返回一个字典表示要更新的字段。框架会自动把返回值合并回 state。注意retry_count那个字段用了Annotated[int, operator.add]意思是每次更新是“累加”而不是“覆盖”——这是 LangGraph 处理并发更新和累积逻辑的机制叫 reducer。为什么这么设计因为在复杂流程里最大的 bug 来源就是“状态不一致”。节点 A 以为某个字段是字符串节点 B 把它改成了列表节点 C 读的时候直接崩。LangGraph 用 TypedDict 强制你声明每个字段的类型用 reducer 明确更新语义把这类问题在编译期就暴露出来。我踩过的坑是一开始图省事用了普通 dict结果两个分支同时更新同一个字段后写的把先写的覆盖了排查了半天才发现是 reducer 没配。提示state 的字段设计要遵循“最小必要”原则。不要把整个对话历史都塞进 state那样会迅速膨胀。只放节点之间真正需要传递的结构化数据原始文本可以存到外部存储state 里只放引用。3.2 节点函数的编写规范与常见陷阱节点函数看起来简单但有几个细节不注意就会出问题。先看一个标准写法def extract_clauses(state: ContractState) - dict: text state[raw_text] prompt f从以下合同文本中提取所有条款标题每行一个\n{text[:8000]} response llm.invoke(prompt) clauses [line.strip() for line in response.content.split(\n) if line.strip()] return {clauses: clauses}这里第一个陷阱是返回值必须是字典且键必须在 state 里定义过。我见过有人直接返回一个字符串框架不报错但 state 没更新下游节点拿到旧数据整个流程静默地跑偏。第二个陷阱是节点函数应该是纯函数——同样的输入产生同样的输出不要在里面读写全局变量或做副作用操作比如发邮件。副作用应该单独抽成一个节点这样重试的时候不会重复发邮件。第三个陷阱是关于异常处理。节点里抛异常默认整个图会中断。如果你希望某个节点失败后走另一条边比如“降级处理”你需要用try-except把异常捕获然后返回一个标记字段让条件边去判断。比如def compare_template(state: ContractState) - dict: try: result do_comparison(state[clauses]) return {missing_items: result, compare_ok: True} except Exception as e: return {compare_ok: False, error_msg: str(e)}然后在边上判断compare_ok是 True 还是 False分别指向不同节点。这种“把异常转化为状态”的模式是 LangGraph 里做健壮流程的关键技巧。3.3 条件边与循环让流程真正“活”起来LangGraph 最强大的地方是支持条件边和循环。条件边就是一个函数接收 state返回下一个节点的名字def should_retry(state: ContractState) - str: if state.get(compare_ok): return generate_report if state[retry_count] 3: return extract_clauses return alert_human然后这样连边graph.add_conditional_edges(compare_template, should_retry)这就实现了一个“失败重试最多三次超过就转人工”的逻辑。注意这里的循环是回到extract_clauses意味着整个提取和比对会重跑。如果你只想重跑比对就把边指向compare_template自己。我实际用下来循环是 LangGraph 相比 LangChain 普通 Chain 最大的优势。LangChain 的 Chain 是线性的 DAG你很难表达“回到上一步重做”。而 LangGraph 的图可以有环这让它天然适合实现 Agent 的“思考-行动-观察”循环。很多号称“LLM powered autonomous agents”的项目底层其实就是一张带条件边的图。注意循环一定要有终止条件否则会无限跑下去烧 token。我一般会在 state 里放一个计数器每次循环累加超过阈值强制跳出。另外 LangGraph 有recursion_limit参数默认 25超过会抛异常这个值可以根据流程复杂度调整但别设太大。4. 从零搭一个 LangGraph 流程合同审查机器人的完整实现4.1 环境准备与依赖选型先说环境。LangGraph 是 Python 库装起来很简单pip install langgraph langchain-openai langchain-community pypdf这里我选langchain-openai作为 LLM 接入层因为它兼容大部分主流模型的 API 格式。如果你用的是本地部署的模型可以用langchain-community里的ChatOllama或者自己写一个符合BaseChatModel接口的类。pypdf用来解析 PDF。关于版本LangGraph 迭代很快我建议锁定一个稳定版本比如langgraph0.2.x。我踩过的坑是某次没锁版本自动升级到 0.3 之后StateGraph的导入路径变了整个项目跑不起来。生产环境一定要用requirements.txt或poetry.lock锁死。还有一个容易被忽略的点API 密钥管理。千万不要把 key 硬编码在代码里。我一般用.env文件加python-dotenvfrom dotenv import load_dotenv load_dotenv()然后在.env里写OPENAI_API_KEYsk-xxx并且把.env加进.gitignore。如果你在团队里协作更稳妥的做法是用密钥管理服务或者至少用环境变量注入。我见过有人把 key 提交到公开仓库十分钟后就被刷了几百美元的账单。4.2 定义状态结构与节点函数回到合同审查这个例子。我们先把 state 定义好from typing import TypedDict, Annotated import operator class ReviewState(TypedDict): pdf_path: str raw_text: str clauses: list[str] standard_clauses: list[str] missing: list[str] report: str retry_count: Annotated[int, operator.add] error: str然后写四个节点解析 PDF、提取条款、比对标准模板、生成报告。from pypdf import PdfReader def parse_pdf(state: ReviewState) - dict: reader PdfReader(state[pdf_path]) text \n.join(page.extract_text() or for page in reader.pages) return {raw_text: text} def extract_clauses(state: ReviewState) - dict: prompt ( 你是合同分析助手。从下面的合同文本中提取所有条款标题 每行输出一个不要编号不要解释。\n\n state[raw_text][:10000] ) resp llm.invoke(prompt) clauses [l.strip() for l in resp.content.split(\n) if l.strip()] return {clauses: clauses} def compare_clauses(state: ReviewState) - dict: standard state[standard_clauses] found set(state[clauses]) missing [c for c in standard if c not in found] return {missing: missing} def generate_report(state: ReviewState) - dict: if not state[missing]: report 所有标准条款均已覆盖未发现缺失。 else: report 缺失条款如下\n \n.join(f- {m} for m in state[missing]) return {report: report}这里standard_clauses我假设是从某个配置文件读进来的实际项目里可以放在 state 初始化时传入。注意extract_clauses里我截断了文本到 10000 字符这是为了防止超出模型上下文窗口。真实场景下应该做分块处理但为了示例清晰先这样。4.3 组装图并接入条件分支现在把节点和边连起来from langgraph.graph import StateGraph, START, END builder StateGraph(ReviewState) builder.add_node(parse_pdf, parse_pdf) builder.add_node(extract_clauses, extract_clauses) builder.add_node(compare_clauses, compare_clauses) builder.add_node(generate_report, generate_report) builder.add_edge(START, parse_pdf) builder.add_edge(parse_pdf, extract_clauses) builder.add_edge(extract_clauses, compare_clauses) def check_result(state: ReviewState) - str: if state.get(error): return extract_clauses if state[retry_count] 2 else END return generate_report builder.add_conditional_edges(compare_clauses, check_result) builder.add_edge(generate_report, END) graph builder.compile()这段代码里check_result是一个条件边函数它检查是否有错误。如果有错误且重试次数小于 2就回到extract_clauses重试否则直接结束。正常情况下去generate_report。编译之后你可以这样调用result graph.invoke({ pdf_path: contract.pdf, standard_clauses: [保密条款, 违约责任, 争议解决, 付款方式], retry_count: 0, }) print(result[report])整个流程跑下来从 PDF 解析到报告生成全自动。如果提取环节因为网络问题失败它会自动重试两次。这就是 LangGraph 相比纯 prompt 链的价值——流程控制是显式的、可测试的、可恢复的。4.4 持久化与断点续跑生产环境必备上面这个例子有个问题如果流程跑到一半服务重启了所有状态就丢了。生产环境必须加持久化。LangGraph 提供了 checkpointer 机制from langgraph.checkpoint.sqlite import SqliteSaver memory SqliteSaver.from_conn_string(checkpoints.db) graph builder.compile(checkpointermemory) config {configurable: {thread_id: contract-001}} result graph.invoke(initial_state, config)加上thread_id之后每次执行的状态都会存到 SQLite 里。如果中途失败你可以用同样的thread_id重新调用它会从上次中断的地方继续。这个功能在长流程里太重要了——我有个流程要跑十几分钟中间任何一步失败都从头再来是不可接受的。提示生产环境建议用 Postgres 作为 checkpointer 后端SQLite 适合本地开发和单机部署。LangGraph 官方提供了langgraph-checkpoint-postgres包配置方式和 SQLite 类似。5. 踩坑实录那些文档里不会写的经验5.1 状态字段膨胀与 token 浪费我第一个上生产的 LangGraph 流程state 里塞了完整的对话历史、原始文档、中间结果、日志信息结果每次节点执行都要序列化整个 state内存和延迟都上去了。更糟的是有些节点会把整个 state 拼进 prompt导致 token 消耗是必要量的好几倍。后来我学乖了state 只放结构化的小数据大文本放外部存储state 里只存 key。比如原始 PDF 文本不要放 state放到 Redis 或本地文件state 里只放文件路径。节点需要的时候自己去读。这样 state 序列化快checkpoint 也小。另一个技巧是用Annotated的 reducer 来控制字段更新。比如日志字段用operator.add让它累加而不是每次覆盖。但要注意累加字段如果无限增长也会出问题所以日志最好还是写到外部。5.2 条件边的返回值必须精确匹配节点名这个坑我踩了不止一次。条件边函数返回的字符串必须和add_node时注册的节点名完全一致包括大小写。我有次写了个return Generate_Report但节点注册的是generate_report结果框架直接抛KeyError而且报错信息不太直观排查了好一会儿。更隐蔽的情况是返回了None。如果你的条件边函数在某些分支下没有显式 returnPython 默认返回None框架会报错。所以条件边函数一定要保证所有路径都有返回值最好用if-elif-else写全。5.3 LLM 输出不稳定导致流程抖动LLM 不是确定性系统同样的输入可能产生不同的输出。这在 LangGraph 里会被放大——因为一个节点的输出会影响下游所有节点的走向。我遇到过提取条款时模型偶尔多输出一行“以下是提取结果”导致比对时多出一个不存在的条款报告就错了。解决办法有两个层面。第一是在 prompt 里加约束明确要求“只输出条款标题不要任何额外文字”并且用 few-shot 示例强化格式。第二是在节点函数里做后处理比如用正则过滤掉不符合格式的行或者用 Pydantic 做结构化解析。LangChain 提供了with_structured_output方法可以让模型直接返回符合 schema 的对象比解析自由文本稳定得多。from pydantic import BaseModel class ClauseList(BaseModel): clauses: list[str] structured_llm llm.with_structured_output(ClauseList) result structured_llm.invoke(prompt) clauses result.clauses这样模型输出的格式由 schema 保证流程抖动会小很多。代价是有些模型对 structured output 的支持不完善可能需要多试几个模型。5.4 常见问题速查表问题现象可能原因排查方向图执行到某节点后卡住条件边返回了未注册的节点名检查返回值拼写和节点注册名state 字段更新后下游读到旧值节点返回的字典键名和 state 定义不一致用 TypedDict 类型检查开启严格模式流程无限循环循环没有终止条件或计数器未累加检查 reducer 配置和 recursion_limitcheckpoint 文件过大state 里存了大文本把大对象移到外部存储state 只存引用重试时重复执行副作用副作用写在节点函数里把发邮件、写数据库等操作抽成独立节点LLM 输出格式不稳定prompt 约束不够或模型能力不足用 structured output 或加后处理6. LangChain 与 LangGraph 的关系别把它们当成竞品6.1 两者定位的本质差异很多人第一次接触这两个名字会以为 LangGraph 是 LangChain 的升级版或者替代品。其实不是。LangChain 是一套组件库LangGraph 是一个编排引擎。它们解决的是不同层次的问题。LangChain 提供的是“零件”各种 LLM 的封装、prompt 模板、输出解析器、文档加载器、向量存储接口、工具定义。你用 LangChain 可以快速拼出一个“读取文档 → 切分 → 嵌入 → 检索 → 生成回答”的 RAG 流程。它的抽象是 Chain本质是一条线性的处理管道。LangGraph 提供的是“装配线”你定义节点和边它负责调度、状态管理、持久化、错误恢复。它的抽象是 Graph可以表达分支、循环、并行、子图。LangGraph 并不关心你每个节点里用什么 LLM、什么 prompt它只关心流程怎么走。所以正确的关系是LangGraph 可以调用 LangChain 的组件也可以完全不用 LangChain。你完全可以在 LangGraph 的节点里直接调 OpenAI 的 SDK不经过 LangChain。反过来LangChain 的 Chain 也可以独立使用不需要 LangGraph。6.2 什么时候该用哪个我的判断标准很简单如果你的流程是线性的、无状态的、不需要重试和恢复的用 LangChain 的 Chain 就够了。比如一个简单的“翻译这段文本”或者“总结这篇文章”没必要上 LangGraph杀鸡用牛刀。如果你的流程有分支、有循环、需要维护跨步骤状态、需要断点续跑、需要人工介入节点那就上 LangGraph。典型场景包括多轮对话 Agent、需要调用多个工具的自动化流程、需要人工审核的审批流、需要长时间运行的数据处理管道。还有一个中间地带LCELLangChain Expression Language。它用|操作符把组件串起来比传统 Chain 更灵活支持流式和异步。但 LCEL 本质上还是 DAG不支持循环。所以如果你的流程需要“回到上一步”还是得用 LangGraph。我自己的项目里通常是两者混用用 LangChain 的组件做节点内部的实现用 LangGraph 做节点之间的编排。这样既享受了 LangChain 生态的便利又获得了 LangGraph 的流程控制能力。6.3 从 LangChain 迁移到 LangGraph 的实操建议如果你已经有一个用 LangChain Chain 写的流程想迁移到 LangGraph我的建议是不要一次性重写。先把 Chain 拆成几个逻辑步骤每个步骤包装成一个节点函数然后用 LangGraph 把它们串起来。原来的 Chain 逻辑可以原封不动地放在节点里。迁移过程中最容易出问题的是状态管理。LangChain 的 Chain 通常用Memory或者手动传参来维护上下文而 LangGraph 用统一的 state。你需要把原来散落在各处的上下文变量收敛到 state 里。这个过程虽然有点繁琐但做完之后你会发现代码清晰很多——所有状态都在一个地方定义谁改了什么都一目了然。另一个建议是先不加持久化跑通再说。持久化虽然重要但会增加调试复杂度。先用内存模式把流程跑通确认节点逻辑和边条件都正确再加上 checkpointer。我见过有人一上来就配 Postgres结果流程本身有 bug每次重跑都从 checkpoint 恢复反而干扰了排查。7. 这套东西还能怎么扩展合同审查只是一个引子。LangGraph 能做的事情远不止于此。我最近在琢磨的几个方向也一并分享出来。第一个方向是多 Agent 协作。LangGraph 支持子图你可以把每个 Agent 定义成一张子图然后用一张主图来协调它们。比如一个“研究 Agent”负责搜集资料一个“写作 Agent”负责成文一个“审核 Agent”负责挑错主图控制它们之间的流转。这比单个 Agent 硬扛所有任务要稳定得多因为每个 Agent 的 prompt 可以更专注。第二个方向是人工介入节点。LangGraph 支持interrupt机制可以在某个节点执行前暂停等待人工确认后再继续。这在审批流、高风险操作确认等场景里非常有用。你可以把“发送邮件”这个节点设成需要人工确认避免模型误发。第三个方向是与外部系统集成。节点函数里可以调任何 API、查任何数据库、操作任何文件。LangGraph 不限制你用什么技术栈它只负责流程调度。这意味着你可以用它来编排 RPA 流程、数据处理管道、甚至 IoT 设备的控制逻辑。我个人的体会是LangGraph 最大的价值不是它提供了多少功能而是它把“流程”这个被 LLM 应用长期忽视的东西重新放回了架构的中心。过去我们太关注 prompt 怎么写、模型怎么选却忘了真实业务是有结构的。把结构还给流程把智能留给节点这可能是 AI 应用从 demo 走向生产的关键一步。