基于Node.js与LangChain构建AI编程Agent:从任务分解到工具调用

📅 2026/8/13 5:46:45
基于Node.js与LangChain构建AI编程Agent:从任务分解到工具调用
1. 从“代码补全”到“任务驱动”为什么我们需要一个AI编程Agent如果你最近也在用Cursor或者Trae这类AI编程工具大概率已经体验过那种“动动嘴皮子代码自己写”的爽快感。它们不再是简单的代码补全工具而更像是一个坐在你旁边的、不知疲倦的资深程序员搭档。你只需要用自然语言描述需求比如“给我写一个用户登录的API用FastAPI需要JWT鉴权”它就能在几秒内生成一个结构清晰、可直接运行的代码文件。这背后就是“AI编程Agent”在发挥作用。但用久了你可能会发现一些“痒点”。比如Trae的积分消耗得飞快复杂任务需要反复沟通Cursor的免费额度有限且对中文指令的理解有时会“跑偏”。更重要的是这些工具像是一个黑盒你无法定制它的工作流也无法让它深度集成到你自己的开发环境中。于是一个念头自然产生我能不能自己造一个轮子一个能理解我的项目上下文、能按我的规则执行任务、并且完全受我控制的AI编程Agent这正是我们今天要探讨的核心。一个真正的AI编程Agent其核心能力远不止是调用大模型的API生成代码。它需要具备任务分解、上下文理解、工具调用、代码执行与验证等一系列复杂能力。简单来说它需要像一个真正的程序员一样去“思考”和“行动”。本文将带你从零开始基于Node.js和LangChain生态一步步拆解并复现这类工具的核心能力。我们不会止步于一个简单的聊天机器人而是要构建一个能真正理解项目结构、调用命令行工具、读写文件、并持续迭代直至完成复杂编程任务的智能体。2. 核心架构拆解一个AI编程Agent的四大支柱要复现Trae或Cursor的核心体验我们不能只盯着“生成代码”这一个环节。我们需要构建一个完整的系统。这个系统可以抽象为四个相互协作的核心模块它们共同构成了Agent的“大脑”和“四肢”。2.1 大脑中枢LLM与提示工程这是Agent的“思考”部分。我们选择一个大语言模型作为核心推理引擎。对于个人开发者或实验项目开源模型如DeepSeek-V3、Qwen2.5-Coder或性价比极高的闭源API如OpenAI的GPT-4o、Anthropic的Claude 3.5 Sonnet都是不错的选择。选择的关键在于模型的代码理解与生成能力、长上下文支持以及成本。但仅仅调用API是不够的。提示工程是让LLM按照我们意图工作的关键。一个高效的编程Agent提示词通常包含以下几个部分角色定义明确告诉模型“你是一个资深的软件开发工程师”并设定其专长领域如全栈、Python后端等。任务约束规定其输出格式必须用JSON、必须包含思考过程、代码规范遵循PEP 8、使用TypeScript等、安全边界不允许执行危险命令。上下文注入这是难点。我们需要把当前项目的关键信息如package.json、相关目录的文件列表、特定文件的代码片段作为上下文喂给模型。这涉及到检索增强生成技术。思维链要求模型“逐步思考”先分析需求再规划步骤最后生成代码或执行命令。这能显著提升复杂任务的成功率。一个基础的提示词模板可能长这样你是一个经验丰富的Node.js全栈工程师。请协助用户完成编程任务。 当前项目根目录/Users/me/my-project 相关文件 - package.json (依赖: express, axios) - src/index.js (主入口文件) 用户请求{user_request} 请你按照以下步骤工作 1. 分析用户请求的最终目标。 2. 检查现有项目结构和代码判断需要修改或创建哪些文件。 3. 如果需要执行终端命令请生成准确的命令。 4. 如果需要编写代码请生成完整、可运行的代码片段并注明文件名。 5. 每一步请先输出你的思考过程格式为 THOUGHT: ...然后再输出行动。 请用JSON格式回复包含 thought 和 action 字段其中 action 可以是 command 或 write_file 等。2.2 记忆与感知项目上下文的获取与管理Agent不能是“健忘”的。它必须知道自己在哪个项目里项目有什么文件刚刚修改了什么。这就是上下文管理。文件系统感知Agent需要能列出目录、读取文件内容。我们可以使用Node.js的fs模块来实现这些基础能力并将其封装成工具函数供LLM调用。向量检索当项目很大时把全部代码塞进提示词是不可能的有上下文长度限制。这时我们需要一个“记忆库”。我们可以将项目中的所有代码文件进行切片、嵌入使用如OpenAIEmbeddings或TensorFlow.js的嵌入模型存入向量数据库如Chroma、LanceDB或简单的Faiss内存索引。当用户提出需求时先根据需求文本检索最相关的代码片段再将它们作为上下文注入提示词。这就是RAG在编程场景下的应用。对话历史Agent需要记住本次会话中用户说过什么、自己做过什么。这可以通过维护一个简单的消息数组来实现并在每次调用LLM时将最近N条历史消息作为上下文传入。2.3 行动与执行工具调用框架这是Agent的“双手”。LLM想出了方案但最终修改文件、安装依赖、运行测试等操作需要由Agent代表用户去执行。这就是工具调用。我们需要为Agent定义一套它可以使用的“工具”。每个工具都是一个函数有明确的名称、描述和参数。例如readFile: 读取指定路径的文件内容。writeFile: 将内容写入指定路径的文件可覆盖或追加。runCommand: 在项目根目录下执行一个shell命令如npm install、git add。searchFiles: 根据关键词在项目中搜索文件。applyDiff: 应用一个统一的diff补丁到代码上更高级、更安全。LangChain提供了强大的Tool类和AgentExecutor可以非常优雅地将这些工具封装起来并让LLM根据需求自动决定调用哪个工具、传入什么参数。这是构建可行动Agent的关键。2.4 循环与规划任务分解与执行流对于“帮我搭建一个博客系统”这样的复杂指令LLM很难一步到位。因此Agent需要具备任务分解和循环执行的能力。规划LLM首先将大任务拆解成一系列有序的子任务。例如① 初始化项目② 创建数据库模型③ 实现用户认证API④ 实现文章CRUD API⑤ 编写前端页面。执行Agent开始逐个执行子任务。对于每个子任务它可能又会调用多个工具读文件、写代码、运行命令。观察与调整执行一个工具后会得到结果如命令输出、文件内容。这个结果会被反馈给LLMLLM据此判断子任务是否完成或是否需要调整后续计划。这个过程循环往复直到所有子任务完成或遇到无法解决的错误。LangChain的LangGraph库是构建这种有状态、可循环的工作流的绝佳选择。它允许你以图的形式定义Agent的工作流程明确节点执行动作和边根据条件流转非常适合实现复杂的规划-执行-观察循环。3. 实战构建用Node.js LangChain打造你的第一个Agent理论说再多不如动手。让我们从一个最简单的“文件读写助手”开始逐步添加能力最终形成一个可以执行简单编程任务的Agent。3.1 环境搭建与基础依赖首先确保你安装了Node.js建议版本18。然后初始化项目并安装核心依赖mkdir my-ai-agent cd my-ai-agent npm init -y npm install langchain langchain/core dotenv我们还需要一个大模型。这里以OpenAI API为例你需要准备一个API Keynpm install langchain/openai创建.env文件存放密钥OPENAI_API_KEYsk-your-key-here3.2 构建核心Agent从聊天到工具调用我们先不搞复杂的规划而是构建一个能调用readFile和writeFile工具的简单Agent。第一步定义工具// tools/fileTools.js import fs from fs/promises; import path from path; /** * 读取文件内容 * param {string} filePath - 相对于项目根目录的文件路径 */ async function readFile(filePath) { try { const fullPath path.resolve(process.cwd(), filePath); const content await fs.readFile(fullPath, utf-8); return 文件 ${filePath} 的内容\n\\\\n${content}\n\\\; } catch (error) { return 读取文件 ${filePath} 失败${error.message}; } } /** * 写入文件内容 * param {string} filePath - 相对于项目根目录的文件路径 * param {string} content - 要写入的内容 */ async function writeFile(filePath, content) { try { const fullPath path.resolve(process.cwd(), filePath); // 简单安全检查防止写入项目根目录之外 if (!fullPath.startsWith(process.cwd())) { return 错误尝试写入安全路径之外${filePath}; } await fs.writeFile(fullPath, content, utf-8); return 成功写入文件 ${filePath}; } catch (error) { return 写入文件 ${filePath} 失败${error.message}; } } export { readFile, writeFile };第二步创建Agent执行器// agent/simpleAgent.js import { ChatOpenAI } from langchain/openai; import { DynamicStructuredTool } from langchain/core/tools; import { AgentExecutor, createOpenAIFunctionsAgent } from langchain/agents; import { pull } from langchain/hub; import { ChatPromptTemplate } from langchain/core/prompts; import { readFile, writeFile } from ../tools/fileTools.js; import dotenv/config; // 1. 初始化LLM const llm new ChatOpenAI({ modelName: gpt-4o-mini, // 或 gpt-4o temperature: 0.1, // 低随机性保证代码稳定 }); // 2. 将工具函数包装成LangChain Tool const tools [ new DynamicStructuredTool({ name: read_file, description: 读取指定路径文件的内容。输入必须是包含filePath字段的对象。, schema: { type: object, properties: { filePath: { type: string, description: 要读取的文件路径如 src/index.js, }, }, required: [filePath], }, func: async ({ filePath }) readFile(filePath), }), new DynamicStructuredTool({ name: write_file, description: 将内容写入指定路径的文件。如果文件存在则覆盖。输入必须是包含filePath和content字段的对象。, schema: { type: object, properties: { filePath: { type: string, description: 要写入的文件路径如 src/newFile.js, }, content: { type: string, description: 要写入的文本内容, }, }, required: [filePath, content], }, func: async ({ filePath, content }) writeFile(filePath, content), }), ]; // 3. 从LangChain Hub拉取一个适合函数调用的提示词模板或自定义 const prompt await pull(hwchase17/openai-functions-agent); // 4. 创建Agent const agent await createOpenAIFunctionsAgent({ llm, tools, prompt, }); // 5. 创建执行器 const agentExecutor new AgentExecutor({ agent, tools, verbose: true, // 打印详细执行过程调试时非常有用 }); // 6. 运行Agent async function runAgent(userInput) { console.log(用户: ${userInput}); const result await agentExecutor.invoke({ input: userInput, }); console.log(Agent: ${result.output}); } // 测试 await runAgent(请读取当前目录下的package.json文件看看里面有什么依赖。); // Agent会调用read_file工具并返回文件内容。 await runAgent(在根目录创建一个名为test.js的文件内容是一个简单的Hello World HTTP服务器使用Node.js的http模块。); // Agent会思考然后调用write_file工具生成代码。运行这个脚本你会看到Agent在verbose模式下详细的思考过程它先分析你的指令决定调用哪个工具生成工具调用的参数执行工具最后把结果返回给你。这就是一个最基础的、具备“感知”读文件和“行动”写文件能力的AI编程助手。注意这里我们使用了DynamicStructuredTool并定义了严格的schema这能极大地提高LLM调用工具时的参数准确性。直接使用非结构化的工具描述容易导致参数格式错误。3.3 增强能力集成命令执行与代码检索仅有文件读写是不够的。一个真正的编程助手需要能运行命令如npm install和从大型代码库中查找信息。添加命令执行工具// tools/commandTools.js import { exec } from child_process; import { promisify } from util; const execAsync promisify(exec); /** * 在项目根目录执行shell命令 * param {string} command - 要执行的命令 */ async function runCommand(command) { // 安全警告这是一个非常危险的工具必须严格限制 // 在实际应用中你需要一个强大的允许命令列表Allow List或沙箱环境。 const dangerousPatterns [/rm\s-rf/, /mkfs/, /dd/, //dev\/sda/, /sudo/]; for (const pattern of dangerousPatterns) { if (pattern.test(command)) { return 拒绝执行潜在危险命令: ${command}; } } try { const { stdout, stderr } await execAsync(command, { cwd: process.cwd() }); if (stderr) { console.warn(命令 stderr: ${stderr}); } return 命令执行成功。输出\n${stdout}; } catch (error) { return 命令执行失败 (${error.code})${error.message}\n${error.stderr}; } } export { runCommand };将这个工具添加到之前的tools数组中Agent就获得了在项目里运行npm init、git add等命令的能力。添加简单的关键词文件搜索工具// tools/searchTools.js import fs from fs/promises; import path from path; /** * 在项目中搜索包含特定关键词的文件 * param {string} keyword - 搜索关键词 * param {string} [fileExtension.js] - 文件扩展名过滤 */ async function searchFiles(keyword, fileExtension .js) { const results []; async function searchDir(dir) { const items await fs.readdir(dir, { withFileTypes: true }); for (const item of items) { const fullPath path.join(dir, item.name); if (item.isDirectory() !item.name.startsWith(.)) { await searchDir(fullPath); // 递归搜索子目录 } else if (item.isFile() fullPath.endsWith(fileExtension)) { const content await fs.readFile(fullPath, utf-8); if (content.includes(keyword)) { results.push(fullPath.replace(process.cwd(), .)); } } } } await searchDir(process.cwd()); return results.length 0 ? 找到包含 ${keyword} 的文件\n${results.join(\n)} : 未找到包含 ${keyword} 的${fileExtension}文件。; } export { searchFiles };这个工具虽然简单但已经能让Agent回答“我们项目里哪里用到了express”这样的问题。对于更复杂的语义搜索就需要引入前面提到的向量检索RAG了。4. 进阶用LangGraph构建具备规划能力的智能工作流前面的AgentExecutor已经能处理单轮工具调用。但对于“创建一个Express服务器并添加用户路由”这样的多步骤任务我们需要更可控的流程。LangGraph允许我们以图的形式定义Agent的状态和决策逻辑。下面是一个简化版的“编码任务执行图”// workflows/codingWorkflow.js import { StateGraph, END } from langchain/langgraph; import { BaseMessage, HumanMessage } from langchain/core/messages; import { ChatOpenAI } from langchain/openai; import { ToolNode } from langchain/langgraph/prebuilt; import { tool } from langchain/core/tools; import { z } from zod; // 1. 定义状态结构 const AgentState { messages: { value: (x, y) x.concat(y), default: () [], }, // 可以添加更多状态如当前任务列表、已完成步骤等 }; // 2. 定义规划节点让LLM分析任务并生成步骤 async function plannerNode(state) { const llm new ChatOpenAI({ modelName: gpt-4o-mini }); const planPrompt 你是一个项目规划师。请将以下用户请求分解为具体的、可执行的编程任务步骤。 用户请求{user_input} 请输出一个JSON数组每个元素是一个步骤描述。 例如[初始化项目创建package.json并安装express, 创建主文件app.js并设置基础服务器, 添加一个GET /users路由] ; const humanMessage new HumanMessage(planPrompt.replace({user_input}, state.messages[state.messages.length - 1].content)); const response await llm.invoke([humanMessage]); // 这里简化处理实际应解析JSON并存入state const plan JSON.parse(response.content); state.messages.push(new HumanMessage(任务计划${JSON.stringify(plan)}。现在开始执行第一步。)); return state; } // 3. 定义执行节点调用工具执行当前步骤 // 我们需要一个更强大的工具集 const codeGenTool tool( async ({ instruction }) { const llm new ChatOpenAI({ modelName: gpt-4o-mini }); const response await llm.invoke(请根据以下指令生成代码${instruction}。只返回代码块不要解释。); return response.content; }, { name: generate_code, description: 根据自然语言指令生成代码片段。, schema: z.object({ instruction: z.string().describe(具体的编码指令如创建一个使用Express的Hello World服务器), }), } ); // 将工具包装成节点 const toolNode new ToolNode([codeGenTool, /* 之前定义的 readFile, writeFile, runCommand 工具 */]); // 4. 定义路由逻辑判断下一步该做什么 function router(state) { const lastMessage state.messages[state.messages.length - 1].content; // 简单逻辑如果最后一条消息包含“完成”或“错误”则结束否则继续执行。 if (lastMessage.includes(任务完成) || lastMessage.includes(无法继续)) { return END; } else { return execute_step; // 指向执行节点 } } // 5. 构建图 const workflow new StateGraph(AgentState) .addNode(plan, plannerNode) .addNode(execute_step, toolNode) .addEdge(plan, execute_step) .addConditionalEdges(execute_step, router); // 执行后根据条件路由 const app workflow.compile(); // 6. 运行工作流 async function runWorkflow(userRequest) { const initialState { messages: [new HumanMessage(userRequest)] }; const finalState await app.invoke(initialState); console.log(最终消息记录:, finalState.messages.map(m ${m._getType()}: ${m.content}).join(\n---\n)); } await runWorkflow(帮我创建一个简单的Node.js API有一个/health检查端点。);这个例子勾勒了如何使用LangGraph构建一个“规划-执行-判断”循环。在实际项目中状态会更复杂路由逻辑会更智能例如判断当前步骤是否成功是否需要回溯或重试。5. 避坑指南与性能优化从Demo到可用产品构建一个能玩的Demo和构建一个真正可用的Agent之间隔着无数个坑。以下是我在实践中的一些关键经验1. 工具调用的稳定性是最大挑战LLM生成的工具参数格式经常出错。解决方案使用结构化工具如前文所示用zod定义严格的schema这能极大提升调用准确率。提供示例在提示词中给出工具调用的具体JSON示例。后置校验与重试在工具函数内部对参数进行二次校验如果失败可以将错误信息反馈给LLM让它重试。LangChain的AgentExecutor已经内置了错误处理和重试机制。2. 上下文管理不当导致成本飙升与效果下降无节制地将所有文件内容塞进上下文会快速耗尽Token并让模型混淆。解决方案实现智能检索必须引入RAG。只检索与当前任务最相关的代码片段。可以按文件类型、目录结构、近期修改等维度建立索引。压缩上下文对检索到的长代码进行摘要用另一个小模型只把摘要和关键部分送入主LLM。分层记忆维护短期记忆本次对话、中期记忆本次会话中读写的文件、长期记忆向量数据库中的项目知识。3. 命令执行的安全性问题这是重中之重。一个不受控的runCommand工具是灾难。解决方案沙箱环境在Docker容器或安全沙箱中运行所有命令与主机隔离。命令白名单只允许执行预定义的安全命令列表如npm install packagegit add file。人工确认对于高风险操作如rm,git push设置“人工确认”环节让用户批准后再执行。4. 代码生成的质量与风格控制生成的代码可能风格不一或存在低级错误。解决方案强化提示词在系统提示中明确代码规范、项目使用的框架和库版本。后置格式化与检查生成代码后自动用Prettier、ESLint对于JS/TS或black、isort对于Python进行格式化。甚至可以运行简单的语法检查如node -c。迭代优化让Agent自己运行生成的代码如果报错将错误信息反馈给它要求其修复。这模仿了“编码-测试-调试”的循环。5. 成本控制频繁调用GPT-4级别的模型账单会很快增长。解决方案模型分级简单的文件操作、命令执行判断可以用小模型如GPT-4o-mini复杂的代码生成和规划再用大模型。缓存对相同的提示词和工具调用结果进行缓存避免重复计算。设置预算与限额在Agent执行前预估Token消耗对单次会话或每日使用设置硬性上限。构建一个成熟的AI编程Agent是一个系统工程它融合了提示工程、RAG、工具调用、工作流编排等多个AI工程化领域的技术。从零开始复刻Trae或Cursor的全部能力极具挑战但通过本文拆解的核心模块和实战步骤你已经掌握了构建自己专属AI编程伙伴的钥匙。你可以从满足自己某个特定工作流开始比如自动生成API文档、修复某类BUG逐步迭代最终打造出一个深度理解你个人编码习惯和项目风格的超级助手。