这段时间我写了一个 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 个节点顺序执行,逐步细化需求:
- intentNode:识别用户意图(新建项目/修改代码/询问问题)
- capabilityNode:分析需要哪些功能模块
- uiNode:设计 UI 结构和页面布局
- componentNode:拆解出需要哪些组件
- structureNode:设计文件目录结构
- dependencyNode:分析依赖关系
- 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 |
| 数组追加 | concat | messages |
| 对象合并 | 自定义 | 复杂状态对象 |
// 自定义对象合并 Reducer
const mergeReducer = (x, y) => ({ ...x, ...y });
7.2 节点设计原则
- 单一职责:每个节点只做一件事
- 纯函数优先:节点应该是纯函数,只依赖输入状态
- 错误隔离:每个节点都应该有错误处理
7.3 子图使用建议
- 用于封装可复用的复杂逻辑
- 子图内部状态与主图隔离
- 通过输入/输出接口与主图交互
7.4 性能优化
- 预编译 Agent,避免重复编译
- 合理使用 Checkpointer,避免状态过大
- 适配器的
canHandle要轻量快速
7.5 常见踩坑
- Reducer 不生效:确保使用
Annotation.Root定义状态 - 子图状态传递:明确输入/输出接口,避免隐式依赖
- SSE 连接超时:使用
keep-alive和X-Accel-Buffering: no
八、总结
通过 LangGraph,我们构建了一个 17 步的 AI 代码生成流水线,实现了:
- 可观测:每个步骤的执行过程清晰可见
- 可控制:支持条件路由和中断恢复
- 可扩展:子图封装复杂逻辑,适配器模式支持灵活扩展
核心设计思想
适配器层(外部输入标准化)
↓
工作流层(内部执行稳定化)
↓
SSE 流(过程可见化)
技术选型对比
| 需求 | LangGraph 方案 | 优势 |
|---|---|---|
| 多步骤流程 | 图结构编排 | 支持分支和循环 |
| 状态管理 | 内置 State | 自动合并,可持久化 |
| 复杂逻辑 | 子图嵌套 | 封装复用 |
| 实时反馈 | SSE 流式输出 | 用户体验好 |