LangChain AI智能体前端开发实战(附完整源码)
摘要
基于LangChain和LangGraph构建前端开发AI智能体,具备多轮对话记忆、工具调用(代码格式化、
前置知识
动手前,先确认基础能力就绪。如果你已掌握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-assistantcd ai-frontend-assistant# 初始化 npm 项目npm init -y# 安装依赖npm install @langchain/openai @langchain/core @langchain/langgraph dotenv zodnpm install -D typescript @types/node tsx# 创建源代码目录mkdir src src/tools
1.2 环境变量配置
创建 .env 文件,将API Key、模型地址等敏感信息注入其中:
# .envDASHSCOPE_API_KEY=你的API KeyDASHSCOPE_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.tsimport { 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, // 最大历史 tokensmaxMessages: 20,// 最大消息条数};
系统提示词写得清晰直接,明确告诉AI它拥有三个可用工具以及应遵守的工作流程。这是在为AI设定行为边界,避免其天马行空。
2.2 记忆模块(持久化)
记忆模块是整个项目中最容易被低估却至关重要的部分。一个没有记忆的AI助手,每次对话都从白纸开始——那和刚安装还没配置的软件没有区别。
代码实现了一个MemoryManager类,内部维护着一个Map
- load():从JSON文件加载之前保存的全部会话,并恢复为BaseMessage实例。
- save():将当前所有会话序列化后写入本地文件。
- 滑动窗口机制:当单个会话的消息数超过上限(20条)时,裁剪掉最早的记录。这既是性能考量,也是为了让AI的上下文保持在合理范围内。
完整代码如下:
// src/memory.tsimport { 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<string, any>;}// 记忆管理器类export class MemoryManager {private sessions: Map<string, SessionState> = new Map();private filePath: string;constructor(filePath: string = MEMORY_CONFIG.filePath) {this.filePath = path.resolve(filePath);}// 加载持久化数据async load(): Promise<void> {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<void> {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<void> {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.tsimport { 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.tsimport { 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.tsimport { tool } from "@langchain/core/tools";import { z } from "zod";export const dataConverter = tool(async ({ input, fromFormat, toFormat }) => {try {let result: string;// JSON 转 CSVif (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 转 JSONif (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
三个工具完成后,通过统一的入口文件导出,方便工作流模块集中调用。
// src/tools/index.tsimport { 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.tsimport { 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 AgentState = {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<AgentState>({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");// 工具执行完后回到 agentreturn workflow.compile();}
2.5 主入口与交互界面
最后是CLI交互界面。支持几个实用命令:/new新建会话、/list查看会话列表、/clear清空当前会话、/exit退出。输入普通文本时,会调用工作流进行对话处理。
值得一提的是SIGINT信号的处理——按Ctrl+C时,程序会先保存记忆再退出,避免数据丢失。
来源:互联网// src/index.tsimport { 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);// 当前会话 IDlet 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);});//
本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。