基于LangChain和LangGraph构建前端开发AI智能体,具备多轮对话记忆、工具调用(代码格式化、文件读取、数据转换)、自主决策(ReAct模式)及状态追踪能力,使用阿里云百炼qwen-plus模型,提供完整源码实现,支持多种前端开发场景,提升开发效率。
前端开发完整LangChain AI智能体实战(附源码)
时间:2026-06-07
浏览:480
文章目录
前置知识
开工之前,得先确认几样基础能力已经到位。如果你已经熟悉了LangChain的几个核心概念,那接下来的内容会顺畅很多。

- LangChain 基础:ChatOpenAI、PromptTemplate 的基本用法——这是最底层的能力,相当于砖块和水泥。
- Memory 机制:对话记忆的原理与实现——知道怎么让AI记住“你是谁”,而不是每轮都像第一次见面。
- Tool 封装:自定义工具的开发规范——工具就像是AI的“手”,让它能做点什么实在事。
- Agent 基础:ReAct 模式的理解——说白了就是“先想、再动”的决策循环。
- LangGraph 入门:节点、边、状态的基本概念——这是整个系统的“骨架”,把各个模块串联起来。
如果上述知识点还不太牢固,建议先花点时间回顾一下。准备好了?那我们直接开始搭建一个完整的前端开发助手智能体。
项目整体架构设计
项目概述
这次实战的目标很明确:做一个能真正干活的前端开发助手。它不能只会聊天,还要会操作代码、读写文件、转换数据格式。具体能力如下:
- 多轮对话记忆(跨会话持久化)——AI能记住之前的对话内容,哪怕你隔天再回来问,它也不会失忆。
- 多种前端工具调用(代码格式化、文件读取、数据转换)——这些是前端开发最频繁的“小操作”,AI能替你搞定。
- 自主决策与任务规划(ReAct 模式)——AI会分析你的需求,自己决定该调用哪个工具、怎么执行。
- 可观测的执行流程(LangGraph 状态追踪)——每一步都有迹可循,方便调试和优化。
整体架构图
先看一张架构图,了解整体脉络:
graph TDA[用户输入] --> B[LangGraph 工作流]subgraph B [LangGraph 工作流]C[agent
推理] --> D[tools
执行]D --> E[should_continue
条件路由]E -->|有工具调用| CendB --> G[核心能力层]subgraph G [核心能力层]H[持久化记忆
JSON存储] -.-I[工具集
3个业务工具] -.-J[基础模型
阿里云百炼] -.-K[状态管理
LangGraph]end
模块划分
整个项目按职责拆成五个模块,每个模块负责一块独立的能力:
| 模块 | 文件 | 职责 |
|---|---|---|
| 配置模块 | config.ts |
环境变量、模型初始化 |
| 记忆模块 | memory.ts |
对话历史的持久化存储与加载 |
| 工具模块 | tools/ |
前端专用工具集(代码格式化、文件读取、数据转换) |
| 工作流模块 | workflow.ts |
LangGraph 状态图定义与编译 |
| 入口模块 | index.ts |
交互式命令行界面 |
第一步:项目初始化与环境配置
1.1 创建项目
先搭建脚手架。这部分比较机械,但每一步都不能少。
# 创建项目文件夹
mkdir ai-frontend-assistant
cd ai-frontend-assistant
# 初始化 npm 项目
npm init -y
# 安装依赖
npm install @langchain/openai @langchain/core @langchain/langgraph dotenv zod
npm install -D typescript @types/node tsx
# 创建源代码目录
mkdir src src/tools
1.2 环境变量配置
创建 .env 文件,把API Key、模型地址等敏感信息放进去:
# .env
DASHSCOPE_API_KEY=你的API Key
DASHSCOPE_API_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
# 记忆文件存储路径
MEMORY_FILE_PATH=./memory.json
# 最大迭代次数
MAX_AGENT_ITERATIONS=5
这里要注意,API Key千万不要硬编码在代码里,.gitignore里记得加上。
1.3 TypeScript 配置
TypeScript 配置直接贴过来,没什么悬念:
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
第二步:核心功能模块实现
2.1 配置模块
配置模块负责初始化模型和统一管理各种参数。选用阿里云百炼的qwen-plus版本,在性能和成本之间取了个平衡。温度设置为0.3,目的是让AI的决策更稳定——通俗点说,就是让它在同一问题上不会每次蹦出不同的答案。
// src/config.ts
import { ChatOpenAI } from "@langchain/openai";
import dotenv from "dotenv";
dotenv.config();
// 模型配置
export const MODEL_CONFIG = {
apiKey: process.env.DASHSCOPE_API_KEY!,
baseURL: process.env.DASHSCOPE_API_URL!,
model: "qwen-plus", // 使用 plus 版本,平衡性能和成本
temperature: 0.3, // 低温度,提高决策稳定性
maxTokens: 2048,
};
// 创建模型实例
export function createModel() {
return new ChatOpenAI({
apiKey: process.env.DASHSCOPE_API_KEY,
configuration: {
baseURL: process.env.DASHSCOPE_API_URL,
},
model: MODEL_CONFIG.model,
temperature: MODEL_CONFIG.temperature,
maxTokens: MODEL_CONFIG.maxTokens,
});
}
// Agent 配置
export const AGENT_CONFIG = {
maxIterations: parseInt(process.env.MAX_AGENT_ITERATIONS || "5"),
systemPrompt: `你是一个专业的前端开发助手,具备以下能力:
1. 代码格式化:使用 code_formatter 工具
2. 文件读取:使用 file_reader 工具
3. 数据转换:使用 data_converter 工具
请遵循 ReAct 模式:
- 先分析用户需求,决定是否需要调用工具
- 需要时调用合适的工具,等待结果后再继续
- 完成任务后用友好的方式回复用户
注意:最多调用 5 次工具,避免无限循环。`,
};
// 记忆配置
export const MEMORY_CONFIG = {
filePath: process.env.MEMORY_FILE_PATH || "./memory.json",
maxHistoryTokens: 4000, // 最大历史 tokens
maxMessages: 20, // 最大消息条数
};
系统提示词写得尽量清晰直接,明确告诉AI它有哪三个工具可用,以及该遵守什么样的工作流程。这是在给AI设定行为边界,免得它天马行空。
2.2 记忆模块(持久化)
记忆模块是整个项目中最容易被低估却至关重要的部分。一个没有记忆的AI助手,每次对话都是一张白纸——那和刚安装好还没配置的软件有什么区别?
代码实现了一个MemoryManager类,内部维护着一个Map
- load():从JSON文件加载之前保存的全部会话,并恢复为BaseMessage实例。
- save():将当前所有会话序列化后写入本地文件。
- 滑动窗口机制:当单个会话的消息数超过上限(20条)时,会裁剪掉最早的记录——这既是性能考量,也是为了让AI的上下文保持在合理的“视野”之内。
完整代码如下:
// src/memory.ts
import { BaseMessage, HumanMessage, AIMessage } from "@langchain/core/messages";
import fs from "fs/promises";
import path from "path";
import { MEMORY_CONFIG } from "./config";
// 会话状态接口
export interface SessionState {
sessionId: string;
messages: BaseMessage[];
createdAt: number;
updatedAt: number;
metadata: Record;
}
// 记忆管理器类
export class MemoryManager {
private sessions: Map = new Map();
private filePath: string;
constructor(filePath: string = MEMORY_CONFIG.filePath) {
this.filePath = path.resolve(filePath);
}
// 加载持久化数据
async load(): Promise {
try {
const content = await fs.readFile(this.filePath, "utf-8");
const data = JSON.parse(content);
// 恢复会话数据
for (const session of data.sessions || []) {
// 将存储的普通对象转换回 BaseMessage 实例
const messages = (session.messages || []).map((msg: any) => {
if (msg.type === "human") {
return new HumanMessage(msg.content);
}
return new AIMessage(msg.content);
});
this.sessions.set(session.sessionId, { ...session, messages });
}
console.log(` 已加载 ${this.sessions.size} 个会话`);
} catch (error) {
// 文件不存在,使用空状态
console.log(" 未找到记忆文件,将创建新会话");
}
}
// 保存到持久化存储
async save(): Promise {
const data = {
sessions: Array.from(this.sessions.values()).map(session => ({
...session,
messages: session.messages.map(msg => ({
type: msg._getType(),
content: msg.content,
})),
})),
lastUpdated: Date.now(),
};
await fs.writeFile(this.filePath, JSON.stringify(data, null, 2));
console.log(`? 已保存 ${this.sessions.size} 个会话`);
}
// 获取或创建会话
getOrCreateSession(sessionId: string): SessionState {
if (this.sessions.has(sessionId)) {
return this.sessions.get(sessionId)!;
}
const newSession: SessionState = {
sessionId,
messages: [],
createdAt: Date.now(),
updatedAt: Date.now(),
};
this.sessions.set(sessionId, newSession);
return newSession;
}
// 添加消息到会话
addMessage(sessionId: string, message: BaseMessage): void {
const session = this.getOrCreateSession(sessionId);
session.messages.push(message);
session.updatedAt = Date.now();
// 限制消息数量(滑动窗口)
if (session.messages.length > MEMORY_CONFIG.maxMessages) {
session.messages = session.messages.slice(-MEMORY_CONFIG.maxMessages);
}
}
// 获取会话历史
getHistory(sessionId: string): BaseMessage[] {
const session = this.getOrCreateSession(sessionId);
return [...session.messages];
}
// 清空会话
async clearSession(sessionId: string): Promise {
this.sessions.delete(sessionId);
await this.save();
}
// 获取所有会话列表
listSessions(): { sessionId: string; createdAt: number; updatedAt: number; messageCount: number }[] {
return Array.from(this.sessions.values())map(session => ({
sessionId: session.sessionId,
createdAt: session.createdAt,
updatedAt: session.updatedAt,
messageCount: session.messages.length,
}));
}
}
2.3 工具模块实现
工具 1:代码格式化
代码格式化工具其实是三个工具里最“有门槛”的。示例中提供了一个最基本的JavaScript和JSON格式化实现,生产环境可以集成Prettier等专业工具。这里最重要的是Zod校验逻辑——参数类型、默认值、校验规则都定义清楚,AI在调用时才能少犯错。
// src/tools/code-formatter.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
// 模拟代码格式化(生产环境可集成 prettier)
function formatJavaScript(code: string, indentSize: number = 2): string {
const indent = " ".repeat(indentSize);
return code
.split("n")
.map(line => line.trim())
.map(line => {
if (line.match(/^[})]]/)) return line;
if (line.match(/[({[]$/)) return indent + line;
return indent + line;
})
.join("n");
}
function formatJSON(code: string, indentSize: number = 2): string {
try {
const obj = JSON.parse(code);
return JSON.stringify(obj, null, indentSize);
} catch {
return "错误:无效的 JSON 格式";
}
}
export const codeFormatter = tool(
async ({ code, language, indentSize = 2 }) => {
try {
if (!code || code.trim().length === 0) {
return "错误:代码内容不能为空";
}
let formattedCode: string;
switch (language) {
case "javascript":
case "typescript":
formattedCode = formatJavaScript(code, indentSize);
break;
case "json":
formattedCode = formatJSON(code, indentSize);
break;
default:
return `暂不支持的语言: ${language},支持:javascript, typescript, json`;
}
return ` 代码格式化完成:n\`\`\`${language}n${formattedCode}n\`\`\``;
} catch (error) {
return `格式化失败:${error instanceof Error error.message : String(error)}`;
}
},
{
name: "code_formatter",
description: `格式化前端代码,使代码更美观易读。使用场景:用户提供未格式化的代码、粘贴的代码排版混乱时。支持的语言:javascript, typescript, json`,
schema: z.object({
code: z.string().describe("需要格式化的原始代码"),
language: z.enum(["javascript", "typescript", "json"]).describe("代码语言类型"),
indentSize: z.number().min(2).max(8).default(2).describe("缩进空格数,默认2"),
}),
}
);
工具 2:文件读取
文件读取工具在安全上做了额外约束——只能读取项目目录内的文件,且默认限制了1MB大小。这是一个很必要的安全防护:你不希望AI能随意读取系统敏感文件。
// src/tools/file-reader.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import fs from "fs/promises";
import path from "path";
export const fileReader = tool(
async ({ filePath, maxSize = 1024 * 1024 }) => {
try {
// 安全检查:防止路径遍历攻击
const resolvedPath = path.resolve(filePath);
if (!resolvedPath.startsWith(process.cwd())) {
return ` 错误:无法访问项目目录外的文件:${filePath}`;
}
// 检查文件是否存在
const stats = await fs.stat(resolvedPath).catch(() => null);
if (!stats) {
return ` 错误:文件不存在:${filePath}`;
}
// 检查文件大小
if (stats.size > maxSize) {
return ` 错误:文件过大(${(stats.size / 1024).toFixed(2)} KB),超过限制(${maxSize / 1024} KB)`;
}
// 读取文件
const content = await fs.readFile(resolvedPath, "utf-8");
// 获取文件扩展名用于语法高亮
const ext = path.extname(filePath).slice(1) || "text";
return ` 文件内容(${filePath}):n\`\`\`${ext}n${content}n\`\`\``;
} catch (error) {
return `读取文件失败:${error instanceof Error error.message : String(error)}`;
}
},
{
name: "file_reader",
description: `读取本地文件内容。使用场景:用户需要查看代码文件、阅读文档、分析配置文件时。限制:只能读取项目目录内的文件,默认最大 1MB。`,
schema: z.object({
filePath: z.string().describe("文件路径,支持相对路径(如 ./src/index.ts)"),
maxSize: z.number().optional().describe("最大文件大小(字节),默认 1MB"),
}),
}
);
工具 3:数据转换
数据转换工具支持JSON与CSV之间的互转。CSV解析部分写了个简易实现,如果生产环境需要处理复杂CSV(比如带换行符的字段),建议换成专业的csv-parse库。
// src/tools/data-converter.ts
import { tool } from "@langchain/core/tools";
import { z } from "zod";
export const dataConverter = tool(
async ({ input, fromFormat, toFormat }) => {
try {
let result: string;
// JSON 转 CSV
if (fromFormat === "json" && toFormat === "csv") {
const data = JSON.parse(input);
if (!Array.isArray(data) || data.length === 0) {
return "错误:JSON 必须是非空数组";
}
const headers = Object.keys(data[0]);
const rows = data.map(obj =>
headers.map(header => {
const value = obj[header];
if (value === undefined || value === null) return "";
if (typeof value === "object") return JSON.stringify(value);
const str = String(value);
return str.includes(",") || str.includes('"') ? `"${str.replace(/"/g, '""')}"` : str;
}).join(",")
);
result = [headers.join(","), ...rows].join("n");
return ` JSON 转 CSV 成功:n\`\`\`csvn${result}n\`\`\``;
}
// CSV 转 JSON
if (fromFormat === "csv" && toFormat === "json") {
const lines = input.trim().split("n");
const headers = lines[0].split(",").map(h => h.trim().replace(/^"|"$/g, ""));
const records = lines.slice(1).map(line => {
// 简单的 CSV 解析(生产环境建议使用专业库)
const values: string[] = [];
let current = "";
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === "," && !inQuotes) {
values.push(current.trim().replace(/^"|"$/g, ""));
current = "";
} else {
current += char;
}
}
values.push(current.trim().replace(/^"|"$/g, ""));
return headers.reduce((obj, header, idx) => {
obj[header] = values[idx] || "";
return obj;
}, {} as Record);
});
result = JSON.stringify(records, null, 2);
return ` CSV 转 JSON 成功:n\`\`\`jsonn${result}n\`\`\``;
}
return ` 不支持的转换类型 ${fromFormat} -> ${toFormat},支持:json->csv 或 csv->json`;
} catch (error) {
if (error instanceof SyntaxError) {
return `错误:JSON 格式解析失败 - ${error.message}`;
}
return `转换失败:${error instanceof Error error.message : String(error)}`;
}
},
{
name: "data_converter",
description: `数据格式转换工具,支持 JSON 和 CSV 之间的互相转换。使用场景:用户需要将 JSON 数据导出为表格格式,或将 CSV 数据转换为 JSON 结构。`,
schema: z.object({
input: z.string().describe("需要转换的原始数据内容"),
fromFormat: z.enum(["json", "csv"]).describe("源数据格式"),
toFormat: z.enum(["json", "csv"]).describe("目标数据格式"),
}),
}
);
三个工具写完后,通过统一的入口文件导出,方便工作流模块集中调用。
// src/tools/index.ts
import { codeFormatter } from "./code-formatter";
import { fileReader } from "./file-reader";
import { dataConverter } from "./data-converter";
// 所有工具列表
export const ALL_TOOLS = [codeFormatter, fileReader, dataConverter];
// 工具名称到工具的映射
export const TOOLS_BY_NAME = Object.fromEntries(
ALL_TOOLS.map(tool => [tool.name, tool])
);
// 按类别获取工具
export const getToolsByCategory = (category: "code" | "file" | "data") => {
switch (category) {
case "code":
return [codeFormatter];
case "file":
return [fileReader];
case "data":
return [dataConverter];
default:
return ALL_TOOLS;
}
};
2.4 工作流模块(LangGraph 整合)
这是整个项目的核心骨架。工作流模块负责把Agent、Tools、Memory串接成一个闭环的、可观测的执行流程。
实现思路是这样的:
- Agent节点:负责推理决策。它接收用户的输入、当前的记忆历史,然后决定是直接回复还是调用某个工具。
- 工具节点:直接使用LangGraph预置的ToolNode,它会根据Agent返回的tool_calls自动调度对应的工具。
- 条件路由:shouldContinue函数判断是继续(还有工具调用)还是结束(直接回复或达到最大迭代次数)。
这个循环结构的意思是:AI先“想”一下,如果需要工具就去调用,拿到结果后再回来“想”下一步。整个过程最多重复5次,避免无限循环。
// src/workflow.ts
import { StateGraph, START, END } from "@langchain/langgraph";
import { ToolNode } from "@langchain/langgraph/prebuilt";
import { AIMessage, BaseMessage } from "@langchain/core/messages";
import { createModel, AGENT_CONFIG } from "./config";
import { ALL_TOOLS } from "./tools";
import { MemoryManager } from "./memory";
// 扩展状态类型
type gentState = {
messages: BaseMessage[];
intent?: "technical" | "billing" | "general";
resolution?: string;
sessionId: string;
iterationCount: number;
}
// 创建 Agent 工作流
export async function createAgentWorkflow(memoryManager: MemoryManager) {
const model = createModel();
// 绑定工具到模型
const modelWithTools = model.bindTools(ALL_TOOLS);
// Agent 节点:推理决策
async function agentNode(state: AgentState) {
// 注入系统提示词
const systemMessage = {
role: "system",
content: AGENT_CONFIG.systemPrompt,
};
// 获取历史消息
const history = memoryManager.getHistory(state.sessionId);
// 构建消息列表
const messages = [
systemMessage,
...history,
...state.messages,
];
const response = await modelWithTools.invoke(messages);
// 保存 AI 回复到记忆
memoryManager.addMessage(state.sessionId, response);
return {
messages: [response],
iterationCount: (state.iterationCount || 0) + 1,
};
}
// 工具节点(使用 LangGraph 预置)
const toolNode = new ToolNode(ALL_TOOLS);
// 条件路由:决定是否继续循环
function shouldContinue(state: AgentState) {
const lastMessage = state.messages[state.messages.length - 1] as AIMessage;
// 检查是否达到最大迭代次数
if (state.iterationCount >= AGENT_CONFIG.maxIterations) {
console.log(" 达到最大迭代次数,强制终止");
return END;
}
// 如果模型调用了工具,继续到工具节点
if (lastMessage.tool_calls && lastMessage.tool_calls.length > 0) {
return "tools";
}
// 否则结束
return END;
}
// 构建状态图
const workflow = new StateGraph({
channels: {
messages: { value: (a, b) => [...(a || []), ...(b || [])] },
sessionId: { value: (_, b) => b },
iterationCount: { value: (_, b) => b 0 },
},
})
.addNode("agent", agentNode)
.addNode("tools", toolNode)
.addEdge(START, "agent")
.addConditionalEdges("agent", shouldContinue)
.addEdge("tools", "agent"); // 工具执行完后回到 agent
return workflow.compile();
}
2.5 主入口与交互界面
最后是CLI交互界面。它支持几个实用命令:/new新建会话、/list查看会话列表、/clear清空当前会话、/exit退出。输入普通文本时,会调用工作流进行对话处理。
值得一提的是SIGINT信号的处理——按Ctrl+C时,程序会先保存记忆再退出,避免数据丢失。
// src/index.ts
import { HumanMessage } from "@langchain/core/messages";
import { createAgentWorkflow } from "./workflow";
import { MemoryManager } from "./memory";
import readline from "readline";
// 创建命令行交互界面
function createReadlineInterface() {
return readline.createInterface({
input: process.stdin,
output: process.stdout,
});
}
// 打印欢迎信息
function printWelcome() {
console.log("n" + "=".repeat(60));
console.log(" ? 前端开发助手 AI 智能体");
console.log("=".repeat(60));
console.log("可用工具:");
console.log(" 代码格式化 (code_formatter)");
console.log(" 文件读取 (file_reader)");
console.log(" 数据转换 (data_converter)");
console.log("指令:");
console.log(" /new - 开始新会话");
console.log(" /list - 查看会话列表");
console.log(" /clear - 清空当前会话");
console.log(" /exit - 退出程序");
console.log("=".repeat(60) + "n");
}
// 主函数
async function main() {
// 初始化记忆管理器
const memoryManager = new MemoryManager();
await memoryManager.load();
// 创建工作流
const app = await createAgentWorkflow(memoryManager);
// 当前会话 ID
let currentSessionId = `session_${Date.now()}`;
printWelcome();
console.log(`? 当前会话: ${currentSessionId}n`);
const rl = createReadlineInterface();
// 处理用户输入
const processInput = async (input: string) => {
const trimmed = input.trim();
// 处理命令
if (trimmed === "/exit") {
await memoryManager.save();
console.log("n? 再见!n");
rl.close();
process.exit(0);
return;
}
if (trimmed === "/new") {
currentSessionId = `session_${Date.now()}`;
console.log(`n 已创建新会话: ${currentSessionId}n`);
rl.prompt();
return;
}
if (trimmed === "/list") {
const sessions = memoryManager.listSessions();
console.log("n? 会话列表:");
for (const session of sessions) {
const marker = session.sessionId === currentSessionId ? "? " : " ";
console.log(`${marker}${session.sessionId} (${session.messageCount} 条消息)`);
}
console.log("");
rl.prompt();
return;
}
if (trimmed === "/clear") {
await memoryManager.clearSession(currentSessionId);
console.log("n? 当前会话已清空n");
rl.prompt();
return;
}
// 空输入处理
if (trimmed === "") {
rl.prompt();
return;
}
// 正常对话处理
console.log("n? AI 思考中...n");
const startTime = Date.now();
try {
// 保存用户消息到记忆
const userMessage = new HumanMessage(trimmed);
memoryManager.addMessage(currentSessionId, userMessage);
// 执行工作流
const result = await app.invoke({
messages: [userMessage],
sessionId: currentSessionId,
iterationCount: 0,
});
// 获取最终回复
const lastMessage = result.messages[result.messages.length - 1];
const responseTime = ((Date.now() - startTime) / 1000).toFixed(1);
console.log(` 回复 (${responseTime}s):n`);
console.log(lastMessage.content);
console.log(`n? 迭代次数: ${result.iterationCount}n`);
} catch (error) {
console.error(`n 错误: ${error}n`);
}
rl.prompt();
};
rl.on("line", processInput);
rl.prompt();
}
// 优雅退出处理
process.on("SIGINT", async () => {
console.log("nn 正在保存记忆...");
const memoryManager = new MemoryManager();
await memoryManager.save();
console.log(" 记忆已保存n");
process.exit(0);
});
main();实用工具推荐
绿化、压缩、噪点和虚拟水印,自由调节做旧程度。
选择画质和输出格式,预览压缩前后的大小。