前置知识
在开始之前,我们需要掌握之前所学的核心技能:
- LangChain 基础:ChatOpenAI、PromptTemplate 基本用法
- Memory 机制:对话记忆的原理与实现
- Tool 封装:自定义工具的开发规范
- Agent 基础:ReAct 模式的理解
- LangGraph 入门:节点、边、状态的基本概念
如果对以上任一知识点还不熟悉,可以查看我之前相关的几篇文章。
项目整体架构设计
项目概述
本次实战项目是一个完整的前端开发助手智能体,具备以下能力:
- ✅ 多轮对话记忆(跨会话持久化)
- ✅ 多种前端工具调用(代码格式化、文件读取、数据转换)
- ✅ 自主决策与任务规划(ReAct 模式)
- ✅ 可观测的执行流程(LangGraph 状态追踪)
整体架构图
graph TD
A[用户输入] --> B[LangGraph 工作流]
subgraph B [LangGraph 工作流]
C[agent<br/>推理] --> D[tools<br/>执行]
D --> E[should_continue<br/>条件路由]
E -->|有工具调用| C
end
B --> G[核心能力层]
subgraph G [核心能力层]
H[持久化记忆<br/>JSON存储] -.-
I[工具集<br/>3个业务工具] -.-
J[基础模型<br/>阿里云百炼] -.-
K[状态管理<br/>LangGraph]
end
模块划分
| 模块 | 文件 | 职责 |
|---|---|---|
| 配置模块 | config.ts |
环境变量、模型初始化 |
| 记忆模块 | memory.ts |
对话历史的持久化存储与加载 |
| 工具模块 | tools/ |
前端专用工具集(代码格式化、文件读取、数据转换) |
| 工作流模块 | workflow.ts |
LangGraph 状态图定义与编译 |
| 入口模块 | index.ts |
交互式命令行界面 |
第一步:项目初始化与环境配置
1.1 创建项目
bash
# 创建项目文件夹
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 文件:
bash
# .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
1.3 TypeScript 配置
json
// 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 配置模块
typescript
// 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, // 最大消息条数
};
2.2 记忆模块(持久化)
typescript
// 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<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:代码格式化
typescript
// 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:文件读取
typescript
// 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:数据转换
typescript
// 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\`\`\`csv\n${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<string, string>);
});
result = JSON.stringify(records, null, 2);
return `✅ CSV 转 JSON 成功:\n\`\`\`json\n${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("目标数据格式"),
}),
}
);
工具统一导出
typescript
// 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 整合)
typescript
// 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 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"); // 工具执行完后回到 agent
return workflow.compile();
}
2.5 主入口与交互界面
typescript
// 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("\n\n🔄 正在保存记忆...");
const memoryManager = new MemoryManager();
await memoryManager.save();
console.log("✅ 记忆已保存\n");
process.exit(0);
});
// 启动
main().catch(console.error);
第三步:功能测试与效果展示
测试用例 1:代码格式化
text
📝 用户:帮我格式化这段代码 function test(){console.log("hello")}
🤖 AI 思考中...
✅ 回复 (2.3s):
我来帮您格式化这段 JavaScript 代码:
function test(){
console.log("hello")
}
格式化后的代码添加了适当的缩进,结构更清晰了。
📊 迭代次数: 1
测试用例 2:多轮对话记忆
text
📝 用户:我叫小明,是一名前端开发
🤖 回复:很高兴认识你小明!作为一名前端开发,有什么我可以帮你的吗?
📝 用户:我叫什么名字?
🤖 回复:你刚才说你叫小明。有什么需要帮助的吗?
测试用例 3:复杂任务(多工具调用)
text
📝 用户:帮我读取当前目录的 package.json,然后把依赖信息转换成 CSV 格式
🤖 AI 思考中...
📊 迭代次数: 3
✅ 回复:
我已经完成了以下操作:
1. 读取了 package.json 文件
2. 提取了 dependencies 信息
3. 转换成了 CSV 格式
依赖信息 CSV:
name,version
@langchain/openai,^0.2.0
@langchain/core,^0.3.0
dotenv,^16.0.0
zod,^3.22.0
项目拓展思路
1. 增加更多工具
typescript
// 可拓展的工具方向
- HTTP 请求工具(调用外部 API)
- Git 操作工具(提交、分支管理)
- 数据库查询工具
- 图片处理工具
2. 多 Agent 协作
typescript
// 使用 LangGraph 实现多 Agent 协作
const multiAgentGraph = new StateGraph(...)
.addNode("code_writer", codeWriterAgent)
.addNode("code_reviewer", codeReviewerAgent)
.addNode("test_generator", testGeneratorAgent)
.addEdge("code_writer", "code_reviewer")
.addConditionalEdges("code_reviewer", (state) =>
state.passed ? "test_generator" : "code_writer"
);
3. Human-in-the-loop
typescript
// 人工审批节点
async function humanApproval(state) {
const needApproval = state.requiresApproval;
if (needApproval) {
// 中断执行,等待人工输入
return interrupt("请审批以下操作...");
}
return state;
}
开发难点与解决方案
| 难点 | 解决方案 |
|---|---|
| 状态类型定义复杂 | 使用 MessagesAnnotation 预设类型,按需扩展 |
| 工具描述不够清晰 | 详细描述使用场景、参数格式、输出示例 |
| Agent 陷入循环 | 设置最大迭代次数,添加循环检测逻辑 |
| 记忆持久化格式 | 统一消息序列化格式,支持恢复 |
| 工具调用参数错误 | 使用 Zod 严格校验,返回友好错误信息 |
结语
通过本次实战,我们从零到一构建了一个完整的 AI 智能体。这个项目整合了 LangChain 生态的核心组件:Memory(记忆)、Tool(工具)、Agent(智能体)、LangGraph(工作流)。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!