ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零构建AI编程助手:基于LangGraph与Node.js的智能代码Agent实战

从零构建AI编程助手:基于LangGraph与Node.js的智能代码Agent实战 1. 项目概述为什么我们要亲手打造一个AI编程助手最近AI编程助手的风头正劲无论是风靡全球的Cursor还是后起之秀Trae都让开发者们体验到了前所未有的编码效率。它们能理解你的意图、自动补全代码、重构函数甚至帮你调试Bug。但作为一个有十多年经验的老码农我总在想这些工具的核心到底是什么它们是如何工作的如果有一天这些服务收费了、网络受限了或者我想定制一个完全符合我个人编码习惯的“专属助手”该怎么办于是我决定动手从零开始复刻一个具备Trae和Cursor核心能力的AI编程Agent。这不仅仅是一个技术实践更是一次深入理解AI如何与编程工作流深度融合的探索。通过这个项目你将亲手搭建一个能够理解自然语言需求、分析项目上下文、生成或修改代码、并能自主执行简单任务的智能体。整个过程会涉及Node.js后端、LangChain框架、大模型LLM的集成与调用以及一个交互式命令行界面CLI的设计。无论你是想深入AI应用开发还是希望为自己的工具链增加一个“超级大脑”这个项目都将为你提供一个清晰的路线图和可运行的代码库。我们不会止步于简单的API调用而是会深入Agent的思维链条ReAct、工具调用Tool Calling和状态管理让你真正掌握构建智能体的核心方法论。2. 核心架构设计拆解AI编程Agent的“大脑”与“四肢”一个功能完备的AI编程Agent远不止是“调用大模型生成代码”那么简单。它需要具备感知、思考、决策和执行的能力。参考Trae和Cursor的设计理念我们可以将整个系统拆解为以下几个核心模块。2.1 系统模块划分与职责一个典型的AI编程Agent架构可以分为四层交互层Interface Layer负责与用户沟通接收自然语言指令。在我们的复刻项目中为了简化并从核心能力入手我们将首先实现一个命令行界面CLI。这相当于Agent的“耳朵”和“嘴巴”。推理与规划层Reasoning Planning Layer这是Agent的“大脑”。它接收用户指令结合当前的项目上下文如文件结构、已有代码决定需要采取哪些步骤来完成任务。例如用户说“在utils.js里加一个计算数组平均值函数”大脑需要解析出目标文件是utils.js动作是“添加函数”函数功能是“计算数组平均值”。更复杂的任务如“重构登录模块”大脑则需要规划出先读取相关文件、分析代码结构、再生成重构方案等子步骤。我们将使用LangChain的Agent和LangGraph来构建这个具备规划能力的大脑。工具层Tool Layer这是Agent的“双手”。大脑规划出步骤后需要具体的工具去执行。对于编程Agent核心工具包括文件读写工具读取文件内容、写入新代码。代码搜索工具在项目目录中查找相关函数、类或变量。终端命令执行工具运行npm install、git命令或测试脚本。代码分析工具调用ESLint、Prettier进行代码风格检查与格式化。网络搜索工具可选当遇到未知API或错误时自动搜索解决方案。上下文与记忆层Context Memory Layer这是Agent的“短期记忆”和“工作台”。它需要维护当前会话的对话历史以及在整个任务执行过程中收集到的项目信息如已读取的文件内容、工具执行的结果。这能确保Agent在多轮对话和多步骤任务中保持连贯性。2.2 技术栈选型背后的思考为什么选择这些技术每一个选择都经过了实战考量。Node.js作为后端运行时其异步非阻塞特性非常适合处理AI Agent这种需要频繁进行I/O操作文件读写、网络请求的场景。npm生态丰富能轻松集成各种工具。同时我们最终要构建CLI工具Node.js是这方面的天然选择。LangChain/LangGraph这是构建Agent的“脚手架”和“调度中心”。LangChain提供了标准化的大模型调用、提示词模板、链Chain和工具Tool的抽象让我们不必从零开始处理与大模型的复杂交互。而LangGraph是其更高级的部分它引入了“状态图”的概念能让我们以可视化、可调试的方式定义Agent复杂的多步骤工作流和循环逻辑这对于实现Trae/Cursor那种能自主规划、执行、检查、再执行的“思考-行动”循环至关重要。大模型LLM这是Agent的“智慧源泉”。我们将主要使用OpenAI的GPT-4或GPT-3.5-Turbo的API因为它们对代码的理解和生成能力目前最为出色。为了复刻Trae/Cursor的体验我们需要选择支持“函数调用”Function Calling或“工具调用”Tool Calling的模型版本这是实现Agent自主使用工具的关键。后续也可以探索集成开源的DeepSeek-Coder等模型。向量数据库可选用于高级RAG如果我们希望Agent能深度理解大型代码库仅仅读取当前文件是不够的。我们可以将整个项目的代码片段函数、类嵌入成向量存入如ChromaDB或LanceDB中。当用户提问时先进行语义搜索找到最相关的代码片段作为上下文喂给LLM这能极大提升代码理解的准确性。这对应着Cursor中“引用项目其他部分代码”的能力。注意在项目初期为了聚焦核心流程我们可以暂不引入向量数据库而是采用基于文件路径和关键词的简单搜索。先让Agent“跑起来”再考虑优化其“记忆力”。3. 环境搭建与核心依赖安装工欲善其事必先利其器。让我们从搭建一个干净的项目环境开始。3.1 初始化项目与包管理首先创建一个新的项目目录并初始化package.json。我强烈推荐使用pnpm作为包管理器它的速度快、磁盘空间利用效率高非常适合这种依赖较多的项目。mkdir my-ai-code-agent cd my-ai-code-agent pnpm init -y接下来安装核心依赖。我们将它们分为几类AI与LangChain核心langchain,langchain-openai,langgraph。注意langchain是一个元包我们还需要安装针对OpenAI的集成包langchain-openai。langgraph用于构建有状态的、可循环的工作流。OpenAI SDKopenai。虽然LangChain封装了调用但直接使用SDK有时在处理流式响应或复杂配置时更灵活。命令行交互与美化commander用于构建CLI的命令和参数解析inquirer用于交互式问答chalk用于终端输出着色ora用于显示加载动画。文件与系统操作fs-extra提供了比原生fs模块更友好、功能更全的API。代码解析与处理可选但推荐babel/parser和babel/traverse用于解析JavaScript/TypeScript代码为AST抽象语法树这对于实现精准的代码定位和修改至关重要。执行安装命令pnpm add langchain langchain-openai langgraph openai commander inquirer chalk ora fs-extra pnpm add -D babel/parser babel/traverse types/node typescript ts-node nodemon因为我们使用TypeScript以获得更好的类型安全和开发体验所以需要安装TypeScript相关开发依赖并初始化配置。npx tsc --init编辑生成的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, dist] }3.2 配置大模型API密钥安全地管理API密钥是第一步。永远不要将密钥硬编码在代码中或提交到版本控制系统。创建一个.env文件在项目根目录OPENAI_API_KEYsk-your-actual-openai-api-key-here # 后续可以添加其他配置如 # OPENAI_BASE_URLhttps://api.openai.com/v1 # 或者你的代理地址 # MODEL_NAMEgpt-4-turbo-preview然后安装dotenv来加载环境变量pnpm add dotenv在项目的入口文件例如src/index.ts开头加载配置import * as dotenv from dotenv; dotenv.config(); if (!process.env.OPENAI_API_KEY) { throw new Error(OPENAI_API_KEY is not set in .env file); }实操心得除了.env对于CLI工具更专业的做法是支持从系统密钥链如macOS的KeychainWindows的Credential Manager或像keytar这样的库中读取密钥。但对于快速原型和大多数开发场景.env文件配合.gitignore是最高效安全的方式。务必确保.gitignore文件中包含.env和node_modules。4. 构建Agent的“双手”核心工具集实现工具是Agent能力的延伸。我们将实现几个最基础但最核心的工具。4.1 文件读写工具这是所有操作的基础。我们需要一个能安全读取和写入文件的工具。// src/tools/fileTools.ts import { Tool } from langchain/core/tools; import * as fs from fs-extra; import * as path from path; export class ReadFileTool extends Tool { name read_file; description Read the contents of a file. Input should be a valid file path.; protected async _call(arg: string): Promisestring { const filePath arg.trim(); try { // 简单的路径安全检查防止读取系统文件 if (path.isAbsolute(filePath) !filePath.startsWith(process.cwd())) { return Error: For security,只能读取当前工作目录(${process.cwd()})及其子目录下的文件。; } const content await fs.readFile(filePath, utf-8); return Successfully read file: ${filePath}\nContent:\n\\\\n${content}\n\\\; } catch (error: any) { return Error reading file ${filePath}: ${error.message}; } } } export class WriteFileTool extends Tool { name write_file; description Write content to a file. Input should be a JSON string like {path: file.js, content: console.log(1)}.; protected async _call(arg: string): Promisestring { try { const { path: filePath, content } JSON.parse(arg); // 同样进行路径安全检查 const safePath path.resolve(process.cwd(), filePath); if (!safePath.startsWith(process.cwd())) { return Error: For security,只能写入当前工作目录(${process.cwd()})及其子目录。; } // 确保目录存在 await fs.ensureDir(path.dirname(safePath)); await fs.writeFile(safePath, content, utf-8); return Successfully wrote to file: ${filePath}; } catch (error: any) { return Error writing file: ${error.message}. Please ensure input is a valid JSON with path and content fields.; } } }为什么这样设计输入验证WriteFileTool要求输入是JSON字符串这强制LLM以结构化的方式思考减少了歧义。直接传递文件路径和内容字符串容易出错。安全限制通过process.cwd()限制文件操作范围这是一个基本的安全措施防止Agent意外或被恶意指令引导修改系统关键文件。错误处理工具必须返回清晰的字符串结果无论是成功还是失败供LLM和后续逻辑判断。4.2 终端命令执行工具让Agent能运行npm install、git status或测试命令是实现自动化工作流的关键。// src/tools/shellTool.ts import { Tool } from langchain/core/tools; import { exec } from child_process; import { promisify } from util; const execAsync promisify(exec); export class ShellTool extends Tool { name execute_shell; description Execute a shell command and return its output. Use this for running tests, installing packages, or git operations. Input is the command string.; protected async _call(arg: string): Promisestring { const command arg.trim(); // **极其重要的安全警告**这里存在命令注入风险 // 在生产环境中必须建立严格的命令白名单机制。 const dangerousPatterns [/rm\s-rf/, /format\s[cC]:/, /shutdown/, /:(){ :|: };:/]; // 示例禁止递归删除、格式化、关机、fork炸弹等 for (const pattern of dangerousPatterns) { if (pattern.test(command)) { return Error: Command rejected by security policy: ${command}; } } console.log([Agent] Executing: ${command}); try { const { stdout, stderr } await execAsync(command, { cwd: process.cwd() }); let result Command executed: ${command}\n; if (stdout) result STDOUT:\n${stdout}\n; if (stderr) result STDERR:\n${stderr}\n; // 如果只有stderr但命令可能成功了如npm warn我们仍返回结果让LLM判断 return result.trim(); } catch (error: any) { return Error executing command ${command}: ${error.message}\n${error.stderr || }; } } }⚠️ 重大安全警示ShellTool是威力最大也最危险的工具。上述简单的正则过滤是远远不够的。在真实的、面向开放环境的Agent中你必须实现一个命令白名单。例如只允许运行npm run下的特定脚本如test,build、git的只读或安全命令pull,status,log、以及ls,cat等。绝对禁止直接执行用户提供的任意命令。这里的实现仅为演示切勿直接用于生产环境。4.3 代码搜索与简单分析工具为了给LLM提供项目上下文我们需要一个能快速定位代码的工具。// src/tools/searchTool.ts import { Tool } from langchain/core/tools; import * as fs from fs-extra; import * as path from path; import * as glob from glob; // 需要安装 pnpm add glob types/glob import { promisify } from util; const globAsync promisify(glob); export class SearchCodeTool extends Tool { name search_code; description Search for files or code snippets in the current project. Input can be a filename pattern (e.g., *.js) or a text to search in files (e.g., function calculateAverage).; protected async _call(arg: string): Promisestring { const query arg.trim(); let results: string[] []; // 情况1按文件名/模式搜索 if (query.includes(*) || query.endsWith(.js) || query.endsWith(.ts) || query.endsWith(.json)) { try { const files await globAsync(**/${query}, { ignore: node_modules/**, cwd: process.cwd() }); results files.slice(0, 10); // 限制返回数量 if (files.length 10) results.push(... and ${files.length - 10} more files.); } catch (error) { return Error in glob search: ${error}; } return results.length 0 ? Found files:\n${results.join(\n)} : No files found matching pattern: ${query}; } // 情况2全文件内容文本搜索简单grep // 注意对于大项目这很慢。生产环境应用ripgrep等专业工具。 try { const allFiles await globAsync(**/*.{js,ts,json}, { ignore: node_modules/**, cwd: process.cwd() }); const matches: string[] []; for (const file of allFiles.slice(0, 50)) { // 限制搜索文件数量 const content await fs.readFile(path.join(process.cwd(), file), utf-8); if (content.includes(query)) { matches.push(file); } if (matches.length 5) break; // 限制匹配结果数量 } return matches.length 0 ? Found ${query} in:\n${matches.join(\n)} : No files contain the text: ${query}; } catch (error: any) { return Error during content search: ${error.message}; } } }这个工具比较简单但对于中小型项目初步定位代码位置已经足够。它的设计体现了渐进式增强的思路先实现一个可用的版本后续可以替换为基于ripgreprg命令的快速搜索或者集成上文提到的向量搜索实现真正的语义化代码检索。5. 组装Agent的“大脑”使用LangGraph构建工作流有了工具我们需要一个“大脑”来协调它们。这就是LangGraph的用武之地。我们将构建一个具备ReActReasoning Acting思维的Agent。5.1 定义Agent状态与模型首先定义我们的Agent在整个工作流中需要维护哪些状态。// src/agent/state.ts import { BaseMessage } from langchain/core/messages; // 定义Agent的状态结构 export interface AgentState { // 用户输入的问题或指令 input: string; // 对话历史消息用于让LLM记住上下文 messages: BaseMessage[]; // 从工具执行中收集到的所有中间结果或最终答案 collectedOutputs: string[]; // 当前步骤的工具调用结果可选用于更精细的控制 lastToolResult?: string; }接下来初始化我们的LLM和工具集。// src/agent/setup.ts import { ChatOpenAI } from langchain/openai; import { ReadFileTool, WriteFileTool, ShellTool, SearchCodeTool } from ../tools; import { Tool } from langchain/core/tools; import { pull } from langchain/hub; import { ChatPromptTemplate } from langchain/core/prompts; export async function setupAgent() { // 1. 初始化LLM // 使用 gpt-3.5-turbo-0125 或 gpt-4-turbo-preview它们支持工具调用且成本/性能平衡 const llm new ChatOpenAI({ modelName: process.env.MODEL_NAME || gpt-3.5-turbo-0125, temperature: 0.1, // 低温度让代码生成更确定、更少“创意” openAIApiKey: process.env.OPENAI_API_KEY, }); // 2. 绑定工具 const tools: Tool[] [ new ReadFileTool(), new WriteFileTool(), new ShellTool(), new SearchCodeTool(), ]; const llmWithTools llm.bindTools(tools); // 3. 从LangChain Hub拉取一个预设的Agent提示词或自定义 // 这里我们自定义一个更贴合编程场景的提示词 const prompt ChatPromptTemplate.fromMessages([ [system, 你是一个专业的AI编程助手擅长理解和修改代码。 你的目标是根据用户请求使用提供的工具完成任务。 你拥有以下工具{tool_names}。 请遵循以下规则 1. 仔细分析用户请求明确需要达成的目标。 2. 规划步骤一次只使用一个工具。 3. 观察工具返回的结果判断是否完成任务或需要下一步。 4. 如果用户请求模糊主动询问澄清例如要创建的文件名、函数的具体行为。 5. 操作文件时优先读取现有内容理解结构后再修改。 6. 生成代码时确保语法正确、风格一致。 当前工作目录{cwd} 现在开始处理请求。], [placeholder, {messages}], ]); return { llm: llmWithTools, tools, prompt }; }5.2 使用LangGraph定义工作流节点LangGraph的核心是定义节点函数和边条件转移形成一个状态机。// src/agent/graph.ts import { StateGraph, END } from langchain/langgraph; import { AgentState } from ./state; import { BaseMessage, HumanMessage, AIMessage, ToolMessage } from langchain/core/messages; import { setupAgent } from ./setup; export async function createAgentWorkflow() { const { llm, prompt } await setupAgent(); // 定义工作流中的各个节点函数 // 节点1调用LLM决定下一步行动调用工具或结束 async function callModel(state: AgentState): PromisePartialAgentState { console.log(\n--- [Agent Thinking] ---); const cwd process.cwd(); const messages state.messages; // 将提示词模板与当前消息结合 const formattedPrompt await prompt.invoke({ tool_names: llm.bindTools ? read_file, write_file, execute_shell, search_code : None, cwd, messages, }); // 调用LLM const response await llm.invoke([...formattedPrompt.toChatMessages(), ...messages]); // 将LLM的响应添加到消息历史中 const newMessages [...messages, response]; return { messages: newMessages, lastToolResult: undefined }; } // 节点2执行工具调用 async function executeTools(state: AgentState): PromisePartialAgentState { const lastMessage state.messages[state.messages.length - 1]; if (!lastMessage || !(tool_calls in lastMessage) || lastMessage.tool_calls?.length 0) { // 如果最后一条消息没有工具调用直接返回 return { lastToolResult: No tool calls to execute. }; } const toolCalls lastMessage.tool_calls!; const toolMessages: ToolMessage[] []; for (const toolCall of toolCalls) { console.log([Agent Action] Calling tool: ${toolCall.name} with args: ${JSON.stringify(toolCall.args)}); // 这里需要根据toolCall.name找到对应的工具并执行 // 为了简化我们假设setupAgent返回的tools数组可以通过名字匹配 // 在实际中你需要一个工具名到工具实例的映射 const toolMap: Recordstring, any {}; // 应从setup中传入 const tool toolMap[toolCall.name]; if (!tool) { toolMessages.push(new ToolMessage({ tool_call_id: toolCall.id, content: Error: Tool ${toolCall.name} not found., })); continue; } try { // 执行工具 const result await tool.invoke(toolCall.args); toolMessages.push(new ToolMessage({ tool_call_id: toolCall.id, content: String(result), // 确保结果是字符串 })); console.log([Tool Result] ${toolCall.name}: ${result.slice(0, 200)}...); // 日志截断 } catch (error: any) { toolMessages.push(new ToolMessage({ tool_call_id: toolCall.id, content: Error executing ${toolCall.name}: ${error.message}, })); } } // 将工具执行结果也加入消息历史 const newMessages [...state.messages, ...toolMessages]; const combinedResult toolMessages.map(m m.content).join(\n); return { messages: newMessages, lastToolResult: combinedResult, collectedOutputs: [...(state.collectedOutputs || []), combinedResult], }; } // 节点3判断是否应该继续LLM是否想继续调用工具 function shouldContinue(state: AgentState): tools | END { const lastMessage state.messages[state.messages.length - 1]; // 如果最后一条消息是AIMessage并且包含工具调用则转到工具执行节点 if (lastMessage tool_calls in lastMessage lastMessage.tool_calls?.length 0) { return tools; } // 否则结束工作流 return END; } // 构建图 const workflow new StateGraphAgentState({ channels: { input: null, // 由节点提供 messages: { default: () [] }, collectedOutputs: { default: () [] }, lastToolResult: { default: () undefined }, } }) .addNode(agent, callModel) // “思考”节点 .addNode(action, executeTools) // “执行”节点 .addEdge(agent, shouldContinue) // 从“思考”节点根据条件决定下一步 .addConditionalEdges(agent, shouldContinue, { tools: action, // 如果需要工具去“执行”节点 END: END, // 否则结束 }) .addEdge(action, agent); // 执行完工具后回到“思考”节点进行下一轮分析 // 设置入口点 workflow.setEntryPoint(agent); const app workflow.compile(); return app; }这个图定义了一个经典的ReAct循环Agent思考LLM分析当前状态用户问题历史消息上次工具结果决定下一步是回答问题还是调用工具。条件判断检查LLM的输出。如果包含工具调用转到action节点如果不包含直接结束。Action执行执行LLM指定的工具并将结果作为ToolMessage添加到历史中。循环执行完成后自动跳回agent节点LLM将看到工具执行结果并决定下一步行动。如此循环直到任务完成。5.3 包装成可用的CLI命令最后我们需要一个入口点来启动这个Agent工作流并处理用户的命令行输入。// src/cli/index.ts import { Command } from commander; import { createAgentWorkflow } from ../agent/graph; import { HumanMessage } from langchain/core/messages; import chalk from chalk; const program new Command(); program .name(my-aicode-agent) .description(An AI coding assistant that can read, write, and execute code.) .version(0.1.0); program .argument(prompt, Your instruction for the AI agent) .option(-v, --verbose, output extra debugging information) .action(async (prompt, options) { console.log(chalk.blue( AI Code Agent starting...)); console.log(chalk.gray(Instruction: ${prompt})); console.log(chalk.gray(Working directory: ${process.cwd()}\n)); try { // 1. 初始化Agent工作流 const app await createAgentWorkflow(); // 2. 准备初始状态 const initialState { input: prompt, messages: [new HumanMessage(prompt)], collectedOutputs: [], }; // 3. 运行工作流 const finalState await app.invoke(initialState); // 4. 输出最终结果 console.log(chalk.green(\n--- ✅ Task Completed ---)); const finalAIMessage finalState.messages .filter(m m._getType() ai) .pop(); if (finalAIMessage !(tool_calls in finalAIMessage)) { console.log(chalk.bold(Final Answer:)); console.log(finalAIMessage.content); } if (options.verbose finalState.collectedOutputs.length 0) { console.log(chalk.gray(\n--- Tool Execution Log ---)); finalState.collectedOutputs.forEach((output, i) { console.log(chalk.gray([Step ${i 1}] ${output.slice(0, 150)}...)); }); } } catch (error: any) { console.error(chalk.red(❌ Agent execution failed:), error.message); if (options.verbose) { console.error(error); } process.exit(1); } }); program.parse();现在一个具备核心能力的AI编程Agent的骨架就搭建完成了。你可以通过ts-node运行它npx ts-node src/cli/index.ts 帮我看看src目录下有没有叫index.js的文件或者在package.json中配置一个脚本后使用pnpm start。6. 实战演练让Agent完成一个真实任务让我们用一个完整的例子看看这个Agent是如何工作的。假设我们有一个简单的Node.js项目里面有一个math.js文件// math.js function add(a, b) { return a b; } function subtract(a, b) { return a - b; } module.exports { add, subtract };现在我们在项目根目录下运行Agentnpx ts-node src/cli/index.ts 在math.js文件中添加一个计算数组平均值的函数函数名叫做average然后运行node -e \const m require(./math); console.log(m.average([1,2,3,4]))\来测试它Agent的思考与执行过程模拟第一轮思考LLM收到指令。它分析出需要a) 读取math.jsb) 修改它c) 运行测试命令。它决定先调用read_file工具。第一轮执行read_file工具被调用参数为math.js。工具返回文件内容。第二轮思考LLM看到文件内容理解了现有结构。它规划生成新的函数代码并决定调用write_file工具。它需要构造JSON输入{path: math.js, content: 完整的新文件内容包含average函数}。第二轮执行write_file工具被调用成功写入。第三轮思考LLM知道文件已修改下一步需要执行测试命令。它决定调用execute_shell工具。第三轮执行execute_shell工具被调用参数为node -e const m require(./math); console.log(m.average([1,2,3,4]))。工具执行并返回输出应该是2.5。第四轮思考LLM看到测试命令成功执行并输出了结果。它判断所有任务已完成生成最终回复“已成功在math.js中添加average函数并运行测试得到结果2.5。”工作流结束。在这个过程中Agent自动完成了读取、分析、修改、验证的全流程无需人工干预每一步。这就是Trae/Cursor核心的自动化能力。7. 性能优化、安全加固与扩展方向我们有了一个能跑起来的原型但要达到生产可用还有很长的路要走。以下是一些关键的优化和扩展点。7.1 性能优化减少Token消耗与延迟与大模型交互Token就是金钱延迟影响体验。上下文管理我们的messages数组会无限增长。需要实现一个滑动窗口或摘要机制。例如只保留最近10轮对话或者将过长的历史消息总结成一段摘要再喂给LLM。LangChain提供了ConversationSummaryBufferMemory等内存类。流式输出对于代码生成这种可能较长的输出使用OpenAI的流式响应streaming可以极大提升用户体验让用户看到生成过程。这需要在CLI中处理llm.stream()的调用。工具结果压缩工具尤其是read_file返回的结果可能很长。可以在将结果放入messages前先让另一个LLM或简单的文本处理进行摘要只保留关键信息。例如只返回被修改函数周围的代码而不是整个文件。并行工具调用如果任务中的多个工具调用没有依赖关系例如同时读取两个不相关的文件可以支持并行调用以节省时间。这需要更复杂的图结构设计。7.2 安全加固构建可信的执行沙箱安全是重中之重尤其是当Agent能执行Shell命令和写文件时。命令白名单如前所述ShellTool必须实现严格的白名单。定义一个允许的命令列表如[npm run test, npm run build, git status, git log --oneline -5]并在执行前进行匹配。文件系统沙箱将Agent限制在一个特定的项目子目录如/tmp/agent_workspace内运行而不是整个process.cwd()。使用chroot或容器技术是更彻底的方案。代码静态分析在写入文件前可以对生成的代码进行简单的静态分析如使用eslint或babel解析检查是否有明显的恶意代码模式如eval、Function构造函数、访问process.env敏感变量等。用户确认机制对于高风险操作如删除文件、运行npm install、向package.json添加依赖可以让Agent暂停并通过CLI交互询问用户“是否继续”。权限模型为不同的工具定义权限等级。例如“只读工具”read_file,search_code可以随时使用“写入工具”需要确认“执行工具”需要更高的权限或特定的安全上下文。7.3 能力扩展从原型到产品要让我们的Agent更像Trae/Cursor可以添加以下功能交互式聊天模式当前的CLI是单次命令。可以修改为持续的聊天会话保持记忆允许用户进行多轮对话和追问。项目范围理解RAG集成向量数据库如ChromaDB。在会话开始时或按需将项目关键文件package.jsonREADME.md 主要的.js/.ts文件切片并向量化存储。当用户提问时先进行语义检索将相关代码片段作为上下文提供给LLM。这能显著提升对大型代码库的理解。更精准的代码编辑目前的write_file是覆盖整个文件。可以实现基于AST的精准编辑工具如insert_code在指定行后插入、replace_function替换特定函数、rename_variable重命名变量。这需要更复杂的babel操作。集成开发环境IDE插件将Agent的核心能力封装成Language Server ProtocolLSP服务器就可以被VSCode、Neovim等编辑器集成实现类似Cursor的沉浸式体验。多模型支持与降级策略除了OpenAI可以集成Claude、DeepSeek-Coder等模型。并设置降级策略复杂任务用GPT-4简单补全用GPT-3.5或本地模型以控制成本。持久化记忆与学习将成功的操作和用户反馈存储下来用于微调小模型或优化提示词让Agent越来越了解你的项目和编码风格。8. 常见问题与调试技巧实录在开发和测试过程中我踩过不少坑这里分享一些典型的排查思路。8.1 Agent陷入循环或行为异常症状Agent不停地调用同一个工具或者生成无意义的工具调用参数。排查启用详细日志在callModel和executeTools函数中打印详细的输入输出。观察LLM接收到的messages历史是否完整、清晰。检查提示词Prompt提示词是Agent的“宪法”。如果指令不清晰LLM就会迷路。确保你的系统提示词明确规定了停止条件如“当你认为任务已完成时直接给出最终答案不要调用工具”。审查工具描述工具的description字段至关重要。LLM完全依赖这个描述来决定是否以及如何调用工具。确保描述清晰、准确并说明了输入格式。例如write_file的描述明确要求JSON字符串输入。限制循环次数在LangGraph中可以设置一个最大循环次数interruptAfter。在workflow.compile()时添加配置防止无限循环消耗大量API费用。8.2 工具调用参数格式错误症状LLM想调用工具但传递的参数无法被JSON.parse解析或者缺少必要字段。解决强化工具描述在工具描述中使用非常明确的例子。例如“Input must be a JSON string with path and content keys. Example input: {path: test.js, content: console.log(1)}”。使用结构化工具LangChain支持使用Zod模式定义工具输入这能强制LLM生成符合模式的结构化参数比自由文本描述更可靠。在工具内部做兼容性处理如果LLM偶尔还是传错了格式可以在_call方法开头尝试多种解析方式如先尝试JSON.parse如果不是JSON再尝试当作纯字符串路径处理并返回清晰的错误信息引导LLM。8.3 处理大型项目时速度慢或Token超限症状读取大文件或搜索整个项目时响应缓慢或者上下文太长导致API调用失败。优化实现分页读取read_file工具可以增加参数如startLine和endLine让LLM只读取关心的部分。使用更高效的搜索用ripgrep(rg) 替代基于glob和fs.readFile的简单搜索速度有数量级的提升。上下文窗口管理这是核心挑战。除了之前提到的摘要和滑动窗口还可以选择性上下文只将与当前任务最相关的文件内容放入上下文。这需要Agent具备“元认知”知道自己需要什么信息。分层摘要对于大型文件先读取目录结构、函数/类名列表通过AST解析如果LLM需要细节再根据名称去读取具体内容。8.4 生成的代码质量不高或不符合规范症状代码有语法错误或者风格与项目现有代码不一致。提升在上下文中提供范例在系统提示词或初始消息中插入几段项目中的典型代码作为风格范例。后置处理在write_file工具执行后自动调用一个format_code工具使用项目的Prettier或ESLint配置对文件进行格式化。迭代改进实现一个code_review工具。让Agent在写入代码后再调用一次LLM或另一个专门的“审查模型”对生成的代码进行审查检查语法和潜在问题根据反馈进行修改。这模拟了人类的“写代码-审查-修改”流程。构建一个成熟的AI编程Agent是一个持续迭代的过程。从今天这个可以运行的原型出发你可以根据自己的需求像搭积木一样不断添加新的工具、优化工作流、强化安全边界。最重要的是你通过亲手实践彻底理解了这些炫酷工具背后的运作机制。下次再使用Cursor或Trae时你看到的将不再是一个黑盒而是一个由清晰模块组成的、你可以掌控和复现的智能系统。
返回列表