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

0 阅读8分钟

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

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

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

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

1.1 我们要解决什么问题

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

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

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

1.2 核心功能

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

用户输入:"帮我生成一个简历优化工具的首页"
                    ↓
  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 整体架构图

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: 输入分析

// 分析用户输入,决定后续流程
.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: 代码生成(子图调用)

.addNode("componentSubgraph", runComponentGraph)  // 批量生成组件
.addNode("pageSubgraph", runPageGraph)            // 批量生成页面

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

Phase 6: 集成与组装

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


四、核心实现详解

4.1 状态定义:使用 Annotation.Root

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

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 构建工作流

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 中封装复杂逻辑的重要机制。以组件生成为例:

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 容错机制

// 降级组件生成
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 实现代码

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 前端对接

// 前端通过 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 适配器设计

// 适配器接口
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 设计价值

价值一:职责分离

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

价值二:易于扩展

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

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

// 3. 主流程零改动 ✅

七、最佳实践与踩坑总结

7.1 状态 Reducer 的选择

场景推荐 Reducer示例
简单覆盖默认 (LastValue)textPrompt
数组追加concatmessages
对象合并自定义复杂状态对象
// 自定义对象合并 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-alive 和 X-Accel-Buffering: no

八、总结

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

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

核心设计思想

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

技术选型对比

需求LangGraph 方案优势
多步骤流程图结构编排支持分支和循环
状态管理内置 State自动合并,可持久化
复杂逻辑子图嵌套封装复用
实时反馈SSE 流式输出用户体验好