LangGraph 实战:从 0 到 1 构建 AI 代码生成工作流

这段时间我写了一个 AI 代码生成项目,核心目标是根据用户的自然语言描述或设计稿,自动生成完整的前端应用代码。在这个过程中,我深入使用了 LangGraph 来编排复杂的代码生成工作流,也积累了一些关于工作流设计、状态管理、子图编排、流式响应等方面的实战经验。

这篇文章会从一个真实的项目场景出发,带你了解:

  • 为什么需要 LangGraph 这样的工作流框架
  • 如何设计一个多步骤的代码生成流水线
  • 状态管理、子图编排、流式响应的具体实现
  • 适配器模式如何让系统具备良好的扩展性

一、项目背景:AI 代码生成器

1.1 我们要解决什么问题

传统的 AI 代码生成工具,通常是一次性输入 prompt,然后等待大模型返回完整代码。这种方式有几个明显的痛点:

  • 缺乏过程感:用户不知道生成到哪一步了,只能干等
  • 难以控制中间结果:无法在某个环节进行人工干预或调整
  • 职责混乱:所有的生成逻辑揉在一个大函数里,维护困难

我们的目标是构建一个 可观测、可分步控制、可扩展 的代码生成系统。

1.2 核心功能

用户通过聊天界面输入需求,系统经过多阶段分析后,生成可直接运行的前端应用代码:

text 复制代码
用户输入:"帮我生成一个简历优化工具的首页"
                    ↓
  17 步工作流:分析 → 规划 → 生成组件 → 生成页面 → 组装
                    ↓
  输出:完整的 React + TypeScript 项目代码

二、为什么选择 LangGraph

2.1 什么是 LangGraph

LangGraph 是 LangChain 团队推出的一个图工作流编排框架,专门用于构建有状态的、多步骤的 AI Agent 应用。它把复杂的 AI 流程拆解成 节点(Node)边(Edge) ,让开发者可以像搭积木一样组织逻辑。

2.2 核心概念速览

概念 说明 类比
StateGraph 状态图的构建器 工厂流水线的设计图纸
State 节点间传递的数据 流水线上的半成品
Node 执行单元,接收状态返回更新 每个工位上的工人
Edge 节点之间的连接 工位间的传送带
Conditional Edge 条件路由 质检分拣口
Subgraph 子图,封装复杂逻辑 一个独立的车间
Checkpointer 状态持久化 保存生产进度

2.3 为什么不用其他方案

方案 优点 缺点
直接调用 LLM 简单直接 难以控制流程,没法做分步
LangChain Chain 链式调用 只支持线性流程,不支持分支
自定义状态机 完全可控 重复造轮子,维护成本高
LangGraph 图结构、状态管理、可恢复 学习曲线陡峭

对于需要 17 个步骤、有条件分支、有子图嵌套 的场景,LangGraph 是最合适的选择。


三、工作流设计:17 步代码生成流水线

3.1 整体架构图

text 复制代码
START
  ↓
┌─────────────────────────────────────────────────────┐
│  Phase 1: 输入分析 (Analysis)                      │
│  analysisNode → 判断是否跳过生成                    │
└─────────────────────────────────────────────────────┘
  ↓ (条件路由: skipGeneration? END : continue)
┌─────────────────────────────────────────────────────┐
│  Phase 2: 意图与规划 (Planning)                    │
│  intentNode → capabilityNode → uiNode → componentNode │
└─────────────────────────────────────────────────────┘
  ↓
┌─────────────────────────────────────────────────────┐
│  Phase 3: 架构设计 (Architecture)                  │
│  structureNode → dependencyNode → typeNode          │
└─────────────────────────────────────────────────────┘
  ↓
┌─────────────────────────────────────────────────────┐
│  Phase 4: 代码准备 (Preparation)                   │
│  utilsNode → mockDataNode → serviceNode → hooksNode │
└─────────────────────────────────────────────────────┘
  ↓
┌─────────────────────────────────────────────────────┐
│  Phase 5: 代码生成 (Generation)                    │
│  componentSubgraph → pageSubgraph                   │
│  (调用子图批量生成组件和页面)                      │
└─────────────────────────────────────────────────────┘
  ↓
┌─────────────────────────────────────────────────────┐
│  Phase 6: 集成与组装 (Assembly)                    │
│  layoutNode → styleGenNode → appGenNode             │
│  → assembleNode → postProcessNode                   │
└─────────────────────────────────────────────────────┘
  ↓
END

3.2 各阶段详解

Phase 1: 输入分析

typescript 复制代码
// 分析用户输入,决定后续流程
.addNode("analysisNode", analysisNode)
.addConditionalEdges("analysisNode", routeAfterAnalysis, ["intentNode", END])

const routeAfterAnalysis = (state) => {
  if (state.skipGeneration) return END;  // 不需要生成代码,直接结束
  return "intentNode";                   // 继续执行
};

Phase 2-3: 规划与架构

这 7 个节点顺序执行,逐步细化需求:

  1. intentNode:识别用户意图(新建项目/修改代码/询问问题)
  2. capabilityNode:分析需要哪些功能模块
  3. uiNode:设计 UI 结构和页面布局
  4. componentNode:拆解出需要哪些组件
  5. structureNode:设计文件目录结构
  6. dependencyNode:分析依赖关系
  7. typeNode:生成 TypeScript 类型定义

Phase 4: 代码准备

生成辅助代码,为后续组件生成做准备:

  • 工具函数、模拟数据、API 服务、自定义 Hooks

Phase 5: 代码生成(子图调用)

typescript 复制代码
.addNode("componentSubgraph", runComponentGraph)  // 批量生成组件
.addNode("pageSubgraph", runPageGraph)            // 批量生成页面

这里使用了子图来封装复杂的批量生成逻辑。

Phase 6: 集成与组装

最后 6 个节点完成代码的组装和优化。


四、核心实现详解

4.1 状态定义:使用 Annotation.Root

LangGraph 的状态管理是它的核心特性之一。我们需要明确定义节点间传递的数据结构:

typescript 复制代码
import { Annotation } from "@langchain/langgraph";

const GraphState = Annotation.Root({
  // 输入消息
  messages: Annotation<T_Graph["messages"]>({
    reducer: (x, y) => x.concat(y),  // 数组追加
    default: () => [],
  }),
  
  // 用户输入
  textPrompt: Annotation<T_Graph["textPrompt"]>(),
  
  // 控制流标记
  skipGeneration: Annotation<T_Graph["skipGeneration"]>(),
  
  // 各阶段的中间产物
  intent: Annotation<T_Graph["intent"]>(),
  capabilities: Annotation<T_Graph["capabilities"]>(),
  structure: Annotation<T_Graph["structure"]>(),
  types: Annotation<T_Graph["types"]>(),
  
  // 最终代码产物
  componentsCode: Annotation<T_Graph["componentsCode"]>(),
  pagesCode: Annotation<T_Graph["pagesCode"]>(),
  files: Annotation<T_Graph["files"]>(),
});

关键点

  • reducer 定义了状态合并策略:x.concat(y) 表示数组追加
  • 默认策略是 LastValue(后者覆盖前者)
  • Annotation.Root 是 LangGraph JS 中推荐的状态定义方式

4.2 构建工作流

typescript 复制代码
import { StateGraph, START, END } from "@langchain/langgraph";

export function buildTraditionalAgent() {
  const builder = new StateGraph(GraphState)
    // 添加所有节点
    .addNode("analysisNode", analysisNode)
    .addNode("intentNode", intentNode)
    // ... 添加其余 15 个节点
    
    // 编排流程
    .addEdge(START, "analysisNode")
    .addConditionalEdges("analysisNode", routeAfterAnalysis, ["intentNode", END])
    .addEdge("intentNode", "capabilityNode")
    .addEdge("capabilityNode", "uiNode")
    // ... 连接所有节点
    .addEdge("postProcessNode", END);

  // 编译并返回
  return builder.compile({
    checkpointer: new MemorySaver(),  // 状态持久化
  });
}

4.3 子图设计

子图是 LangGraph 中封装复杂逻辑的重要机制。以组件生成为例:

typescript 复制代码
const runComponentGraph = async (state: typeof GraphState.State) => {
  // 1. 从主图状态提取需要生成的组件列表
  const allFiles = state.structure?.files || [];
  const componentsToGenerate = allFiles.filter(
    (f: any) => f.path.includes("/components/") && f.path.endsWith(".tsx")
  );

  // 2. 构造子图输入
  const subgraphInput = {
    componentsToGenerate,
    context: {
      hooks: state.hooks,
      types: state.types,
      service: state.service,
    },
  };

  // 3. 调用子图
  let result;
  try {
    result = await componentGraph.invoke(subgraphInput);
  } catch (error) {
    // 降级:生成简单组件
    result = {
      componentsCode: componentsToGenerate.map(createFallbackComponentFile),
    };
  }

  // 4. 返回结果(由主图 reducer 合并)
  return { componentsCode: result.componentsCode };
};

子图的价值

  • 封装了批量生成的复杂逻辑
  • 主图只关心"调用"和"接收结果"
  • 子图可以独立测试和迭代

4.4 容错机制

typescript 复制代码
// 降级组件生成
function createFallbackComponentFile(file: any) {
  const componentName = toComponentName(file.path, "Component");
  return {
    path: file.path,
    content: `import React from 'react';

export default function ${componentName}() {
  return (
    <section className="rounded-lg border border-slate-200 bg-white p-5">
      <h2 className="text-lg font-semibold">${componentName}</h2>
      <p className="mt-2 text-sm">${file.description || "组件内容"}</p>
    </section>
  );
}`,
    description: file.description || "Fallback component",
  };
}

即使子图执行失败,系统也能生成可用的降级组件,保证流程完整性。


五、SSE 流式响应

5.1 为什么需要 SSE

17 个步骤的执行过程,如果一次性返回,用户需要等待很长时间。通过 SSE (Server-Sent Events),我们可以把每个节点的输出实时推送给前端,让用户看到"AI 正在做什么"。

5.2 实现代码

typescript 复制代码
import express from "express";

router.post("/", async (req: Request, res: Response) => {
  // 设置 SSE 响应头
  res.setHeader("Content-Type", "text/event-stream");
  res.setHeader("Cache-Control", "no-cache, no-transform");
  res.setHeader("Connection", "keep-alive");
  res.setHeader("X-Accel-Buffering", "no");  // 禁用 Nginx 缓冲
  
  // 发送 keep-alive 防止连接超时
  res.write(": keep-alive\n\n");

  // 启动 Agent 流式执行
  const stream = await agent.stream(input, {
    configurable: { thread_id: projectId },
    streamMode: "updates",
  });

  // 遍历每个节点的输出
  for await (const chunk of stream) {
    const nodeName = Object.keys(chunk)[0];
    const output = chunk[nodeName];
    
    // 策略模式处理不同节点
    const handler = NODE_HANDLERS[nodeName];
    if (handler) {
      res.write(`data: ${JSON.stringify({
        type: handler.type,
        data: output[handler.key]
      })}\n\n`);
    }
  }

  // 发送结束信号
  res.write(`data: ${JSON.stringify({ type: "done" })}\n\n`);
  res.end();
});

5.3 前端对接

javascript 复制代码
// 前端通过 EventSource 接收流式数据
const eventSource = new EventSource('/api/chat', {
  method: 'POST',
  body: JSON.stringify({ messages: [...] })
});

eventSource.onmessage = (event) => {
  const { type, data } = JSON.parse(event.data);
  
  switch(type) {
    case 'analysis':    // 显示分析结果
    case 'intent':      // 显示意图识别
    case 'components':  // 显示生成的组件
    case 'done':        // 完成
  }
};

六、适配器模式:灵活扩展的入口设计

6.1 问题背景

系统虽然入口统一是聊天接口,但用户实际发来的请求类型并不一样:

  • 普通 prompt:直接根据需求生成项目
  • Figma 链接:走设计稿直连链路
  • 修改请求:基于已有上下文继续调整

如果这些差异都直接堆在主流程里,代码会越来越臃肿。

6.2 适配器设计

typescript 复制代码
// 适配器接口
interface RouteInputAdapter {
  name: string;
  priority: number;
  canHandle: (input: any) => boolean;
  adapt: (input: any) => Promise<{ flow: string; input: any; meta?: any }>;
}

// Figma 适配器
export const figmaRouteAdapter: RouteInputAdapter = {
  name: "figma-route",
  priority: 100,
  canHandle: ({ messages }) => !!extractFigmaUrl(messages),
  adapt: async ({ messages }) => {
    const figmaUrl = extractFigmaUrl(messages)!;
    return {
      flow: "figma",
      input: { messages, figmaUrl },
      meta: { figmaUrl },
    };
  },
};

// 注册表
const ROUTE_ADAPTERS = [figmaRouteAdapter, traditionalRouteAdapter];

export const resolveRouteAdapter = async (input) => {
  // 按优先级排序,找到第一个能处理的适配器
  for (const adapter of ROUTE_ADAPTERS.sort((a, b) => b.priority - a.priority)) {
    if (adapter.canHandle(input)) {
      return await adapter.adapt(input);
    }
  }
  return { flow: "traditional", input };
};

6.3 设计价值

价值一:职责分离

  • 入口层只做分流 → 很薄
  • 适配器层处理识别/转换 → 可扩展
  • 工作流层专注执行 → 稳定

价值二:易于扩展

typescript 复制代码
// 新增一种类型,只需三步:
// 1. 新建适配器
export const imageAdapter: RouteInputAdapter = { /* ... */ };

// 2. 注册到列表
ROUTE_ADAPTERS.push(imageAdapter);

// 3. 主流程零改动 ✅

七、最佳实践与踩坑总结

7.1 状态 Reducer 的选择

场景 推荐 Reducer 示例
简单覆盖 默认 (LastValue) textPrompt
数组追加 concat messages
对象合并 自定义 复杂状态对象
typescript 复制代码
// 自定义对象合并 Reducer
const mergeReducer = (x, y) => ({ ...x, ...y });

7.2 节点设计原则

  1. 单一职责:每个节点只做一件事
  2. 纯函数优先:节点应该是纯函数,只依赖输入状态
  3. 错误隔离:每个节点都应该有错误处理

7.3 子图使用建议

  • 用于封装可复用的复杂逻辑
  • 子图内部状态与主图隔离
  • 通过输入/输出接口与主图交互

7.4 性能优化

  1. 预编译 Agent,避免重复编译
  2. 合理使用 Checkpointer,避免状态过大
  3. 适配器的 canHandle 要轻量快速

7.5 常见踩坑

  1. Reducer 不生效 :确保使用 Annotation.Root 定义状态
  2. 子图状态传递:明确输入/输出接口,避免隐式依赖
  3. SSE 连接超时 :使用 keep-aliveX-Accel-Buffering: no

八、总结

通过 LangGraph,我们构建了一个 17 步的 AI 代码生成流水线,实现了:

  1. 可观测:每个步骤的执行过程清晰可见
  2. 可控制:支持条件路由和中断恢复
  3. 可扩展:子图封装复杂逻辑,适配器模式支持灵活扩展

核心设计思想

text 复制代码
适配器层(外部输入标准化)
    ↓
工作流层(内部执行稳定化)
    ↓
SSE 流(过程可见化)

技术选型对比

需求 LangGraph 方案 优势
多步骤流程 图结构编排 支持分支和循环
状态管理 内置 State 自动合并,可持久化
复杂逻辑 子图嵌套 封装复用
实时反馈 SSE 流式输出 用户体验好
相关推荐
用户298698530142 小时前
在 React 中使用 JavaScript 合并 Excel 文件
前端·javascript·react.js
烬羽2 小时前
面试官:聊聊 LocalStorage 和 this 指向?看这篇就够了
面试·程序员
橘子星2 小时前
JavaScript this 指向全解实战指南
前端·javascript
何出无名之师2 小时前
AIDL的一次调用链路追踪之二,如何和驱动打交道
前端
weedsfly2 小时前
JS垃圾回收:从原理到项目实战,彻底根治内存泄漏
前端·javascript·面试
Jcc2 小时前
虚拟 DOM 是什么?从 Snabbdom 理解 Vue 的 DOM 更新机制
前端
user62229864925813 小时前
Vue 常用技术知识全景:从响应式到组件通信的系统理解
前端
feiyu_gao3 小时前
一个人 + AI:246 commits 做出设计系统 CLI 的故事
前端·ai编程·交互设计
奶油mm3 小时前
从 0 到 1 搭建高可用 Redis Cluster:踩坑、优化与生产实践
前端