从零构建AI智能体:基于Node.js与LangChain实现自主项目创建

发布时间:2026/8/12 9:58:27
从零构建AI智能体:基于Node.js与LangChain实现自主项目创建 1. 项目缘起为什么我们要“手写”一个智能体最近“智能体”这个词火得不行感觉一夜之间从技术社区到产品发布会大家都在谈论AI Agent。但说实话很多教程和框架要么是给你一个黑盒告诉你“调用这个API就行”要么就是概念讲得天花乱坠落地实操却一笔带过。作为一个喜欢刨根问底、动手实践的开发者我总觉得不自己从头“拧一遍螺丝”心里就不踏实。这就好比学开车如果只是坐在副驾看永远不知道换挡的顿挫感和油离配合的微妙。所以我决定启动这个系列目标很明确抛开那些封装过度的框架用最基础的Node.js和LangChain从零开始手把手构建一个能真正“干活”的智能体。我们不给它设定太科幻的目标就做一个最实用、最能体现智能体核心能力的“Mini Cursor”——一个能理解你的自然语言指令并自主创建和管理代码项目的智能助手。你可能会问现在不是有Cursor、Copilot这些现成的工具吗为什么还要自己造轮子我的回答是理解原理才能更好地使用和驾驭工具。通过亲手实现你会彻底明白几个关键问题大模型比如我们用的DeepSeek是如何被“调度”的所谓的“自主规划”和“工具调用”在代码层面是怎么串联起来的智能体在决策时内部状态是如何流转和更新的这些认知能让你在未来使用任何高级框架如LangGraph、Dify时都知其然更知其所以然遇到问题也能快速定位而不是对着报错干瞪眼。这个系列的第三篇我们将进入最激动人心的部分让智能体真正“动”起来完成从指令解析到项目创建的完整闭环。我们会聚焦于智能体的“大脑”与“手脚”的协同即如何让大模型LLM的思考结果转化为操作系统上的实际动作如创建文件、执行命令。我会分享在集成过程中遇到的那些“坑”比如异步流程控制、错误处理、以及如何让智能体的行为更稳定、更可控。2. 智能体的核心架构大脑、记忆与工具在开始敲代码之前我们必须把智能体的“心智模型”搞清楚。一个能自主工作的智能体绝不是简单地把用户问题扔给大模型然后回复就完事了。它需要一个精密的内部架构来支撑其“自主性”。我们可以将其类比为一个经验丰富的项目经理。大脑LLM / 推理核心这就是智能体的“CPU”我们选用DeepSeek模型。它的核心职责是理解和规划。它接收来自外部的信息用户指令、工具执行结果、记忆上下文进行分析、推理然后做出决策“下一步我该做什么是调用某个工具还是直接给出最终答案” 我们之前搭建的Prompt模板和解析逻辑就是在塑造这个大脑的“思维方式”。记忆Memory这是智能体的“RAM”和“硬盘”。它分为两部分短期记忆Conversation Buffer Memory保存当前对话的上下文。当用户说“在刚才创建的项目里再添加一个README文件”时智能体需要记得“刚才创建的项目”是什么、路径在哪。我们通常用一个数组来存储最近的几轮对话用户输入、AI响应、工具调用记录。长期记忆Vector Store理论上智能体可以将重要的执行结果、学到的知识存入向量数据库供未来检索。对于我们这个Mini项目创建器这一步可以简化但架构上需要留出接口。工具Tools这是智能体的“手”和“脚”。大脑想得再好没有工具去执行也是白搭。工具就是一个个封装好的函数智能体可以调用它们来与外部世界交互。对我们来说核心工具就是文件系统和命令行。createFileTool: 根据路径和内容创建文件。runCommandTool: 在指定目录下执行Shell命令如npm init,git init。listDirectoryTool: 列出目录内容帮助智能体了解当前工作环境。控制流Orchestration这是智能体的“神经系统”负责协调大脑、记忆和工具的工作。它决定流程的走向是继续思考还是执行工具或是返回最终结果在LangChain中这通常由AgentExecutor来承担。而在更复杂的场景下我们会用到LangGraph来绘制有状态的工作流图它允许循环、条件分支更能体现智能体的“自主”特性。理解了这个架构再看我们的代码就不会是一团乱麻。每一行代码都是为了实现这个架构中的一个具体环节。接下来我们就进入实战看看如何用Node.js和LangChain把这些模块像乐高一样拼接起来。3. 实战构建项目创建智能体的完整工作流理论说得再多不如一行代码。让我们在项目根目录下创建核心文件agentCore.js。这里我们将把之前散落的模块模型、提示词、解析器、工具整合成一个可运行的智能体。3.1 初始化智能体组装大脑与工具首先我们需要导入必要的模块并初始化智能体的各个组件。// agentCore.js import { ChatDeepSeek } from langchain/deepseek; import { ChatPromptTemplate } from langchain/core/prompts; import { AgentExecutor, createReactAgent } from langchain/agents; import { DynamicStructuredTool } from langchain/core/tools; import { z } from zod; import { MemorySaver } from langchain/langgraph; import { createReactAgentExecutor } from langchain/experimental/react_agent; import fs from fs/promises; import { exec } from child_process; import { promisify } from util; import path from path; const execAsync promisify(exec); // 1. 初始化DeepSeek模型大脑 const llm new ChatDeepSeek({ apiKey: process.env.DEEPSEEK_API_KEY, // 请确保在.env文件中设置 model: deepseek-chat, temperature: 0.1, // 降低随机性让生成更稳定、可预测 }); // 2. 定义工具手脚 // 工具A创建文件 const createFileTool new DynamicStructuredTool({ name: create_file, description: 在指定路径创建或覆盖一个文件并写入内容。, schema: z.object({ filePath: z.string().describe(文件的完整路径例如./myProject/src/index.js), content: z.string().describe(要写入文件的内容), }), func: async ({ filePath, content }) { try { // 确保目录存在 const dir path.dirname(filePath); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(filePath, content, utf-8); return 文件创建成功: ${filePath}; } catch (error) { return 创建文件失败: ${error.message}; } }, }); // 工具B执行Shell命令 const runCommandTool new DynamicStructuredTool({ name: run_command, description: 在指定的工作目录下执行一条Shell命令如 npm init, git init。, schema: z.object({ command: z.string().describe(要执行的命令例如npm init -y), cwd: z.string().describe(命令执行的工作目录路径), }), func: async ({ command, cwd }) { try { const { stdout, stderr } await execAsync(command, { cwd }); if (stderr) { console.warn(命令执行有警告: ${stderr}); } return 命令执行成功。输出\n${stdout}; } catch (error) { // 这里不直接抛出错误而是返回给Agent处理这是关键 return 命令执行失败: ${error.message}; } }, }); // 工具C列出目录 const listDirectoryTool new DynamicStructuredTool({ name: list_directory, description: 列出指定目录下的文件和文件夹。, schema: z.object({ dirPath: z.string().describe(要列出的目录路径), }), func: async ({ dirPath }) { try { const items await fs.readdir(dirPath, { withFileTypes: true }); const list items.map(item ${item.isDirectory() ? [DIR] : [FILE]} ${item.name}).join(\n); return 目录 ${dirPath} 内容\n${list || (空目录)}; } catch (error) { return 列出目录失败: ${error.message}; } }, }); // 将所有工具放入一个数组 const tools [createFileTool, runCommandTool, listDirectoryTool];关键点解析与避坑错误处理策略注意在工具函数中我们捕获了错误但没有throw而是返回了一个格式化的错误字符串。这是智能体工具设计的黄金法则。如果工具抛出异常整个智能体执行链会中断。将错误作为字符串结果返回智能体的“大脑”LLM就能接收到这个失败信息并据此决定下一步行动例如重试、换种方式、或向用户报告错误这体现了智能体的自主纠错能力。Temperature参数将temperature设为较低的0.1是为了减少模型生成的随机性。在需要精确执行指令如生成代码、决定使用哪个工具的场景下稳定性比创造性更重要。DynamicStructuredTool使用LangChain提供的这个工具类配合Zod库定义输入模式可以自动生成清晰的工具描述供LLM理解并验证输入参数非常方便。3.2 设计智能体的“思维链”提示词接下来我们需要给智能体一个清晰的“工作指南”。这个提示词Prompt的质量直接决定了智能体的表现。// 3. 定义智能体的提示词模板 const prompt ChatPromptTemplate.fromMessages([ [system, 你是一个专业的代码项目创建助手。你的目标是根据用户的需求自主规划并执行一系列文件创建和命令操作最终搭建出一个可运行的项目骨架。 你必须严格遵守以下规则 1. **自主规划与执行**用户只会给你一个最终目标如“创建一个React项目”。你需要自己拆解步骤并主动调用工具create_file, run_command, list_directory来完成而不是向用户提问具体步骤。 2. **使用工具**你拥有以上工具。在思考过程中如果你认为需要执行某个操作如创建文件、运行命令、查看目录就直接在“Action”中调用它。 3. **观察结果**每次调用工具后你会得到“Observation”。根据观察结果决定下一步。 4. **项目结构意识**创建文件时注意合理的目录结构如将组件放在src/components下。 5. **安全与确认**如果用户请求的操作可能具有破坏性如删除非项目文件或者你无法确定则停止并询问用户。 当前工作目录是{working_directory} 开始你的最终目标是{input} 请开始你的思考。], [placeholder, {chat_history}], // 这里是记忆对话历史插入的位置 [human, {input}], [placeholder, {agent_scratchpad}], // 这里是Agent思考和执行过程的暂存区 ]);这个系统提示词是智能体行为的“宪法”。它明确了几个要点主动性强调“自主规划”避免智能体变成一问一答的客服。工具使用规范告诉它有什么工具以及如何与工具交互Action - Observation。上下文注入了{working_directory}和{chat_history}让智能体知道自己在哪以及之前说过什么。安全边界设置了一个简单的安全规则虽然基础但很重要。3.3 创建并运行智能体执行器现在我们把大脑LLM、工具Tools和指令Prompt组装起来形成可执行的智能体。// 4. 创建智能体执行器 // 使用LangChain的实验性React Agent实现它基于ReAct范式非常适合工具调用。 const agentExecutor await createReactAgentExecutor({ llm, tools, prompt, }); // 5. 封装一个方便调用的函数 /** * 运行智能体处理用户输入 * param {string} userInput - 用户指令 * param {string} workingDir - 工作目录路径 * returns {Promisestring} - 智能体的最终回复 */ async function runAgent(userInput, workingDir process.cwd()) { console.log(\n 处理指令: ${userInput} ); console.log(工作目录: ${workingDir}); const inputs { input: userInput, working_directory: workingDir, // 在实际应用中这里应该传入一个持久化的聊天历史memory chat_history: , }; try { const stream await agentExecutor.stream(inputs); let finalAnswer ; for await (const chunk of stream) { // 这里可以实时输出Agent的思考过程便于调试 if (chunk.actions) { console.log(\n[思考] ${chunk.messages[chunk.messages.length-1]?.content}); } if (chunk.steps) { const step chunk.steps[0]; if (step.action step.action.tool) { console.log([行动] 调用工具: ${step.action.tool} 输入: ${JSON.stringify(step.action.toolInput)}); } if (step.observation) { console.log([观察] ${step.observation}); } } if (chunk.output) { finalAnswer chunk.output; } } console.log(\n 最终回复 \n${finalAnswer}); return finalAnswer; } catch (error) { console.error(智能体执行出错:, error); return 抱歉处理你的请求时出现了问题: ${error.message}; } } // 导出函数供外部调用 export { runAgent };核心机制剖析 我们使用了createReactAgentExecutor。它实现了ReActReason Act范式这是当前智能体最主流的推理框架之一。其工作流程是一个循环思考ThinkLLM根据当前状态用户输入、历史、上次工具结果分析“现在该怎么办”。行动ActLLM决定调用哪个工具并生成符合工具模式的参数。观察Observe执行工具获取结果成功或失败。再思考LLM根据观察结果决定下一步继续调用工具还是得出最终结论并结束。这个stream方法让我们可以实时看到智能体的“内心戏”思考、行动、观察对于调试和理解其行为逻辑至关重要。4. 从指令到项目智能体实战演示与深度解析理论架构和核心代码都已就位是时候看看我们的Mini Cursor智能体到底能做什么了。让我们创建一个测试文件demo.js来运行几个真实的场景。4.1 场景一创建一个基础的Node.js项目这是最常见的需求。我们看看智能体如何拆解并执行。// demo.js import { runAgent } from ./agentCore.js; async function main() { // 测试指令1创建一个简单的Node.js项目 await runAgent( 在 ./demoProjects 目录下为我创建一个名为 myServer 的Node.js项目。需要包含一个 package.json 文件其中name为my-serverversion为1.0.0并安装express和nodemon作为开发依赖。最后创建一个 src/index.js 文件内容是一个简单的Express服务器监听3000端口。 ); } main();运行node demo.js你会在控制台看到类似以下的输出流 处理指令: “在 ./demoProjects...” 工作目录: /your/current/path [思考] 用户想要创建一个Node.js项目。我需要先创建目录和package.json然后安装依赖最后创建服务器文件。我应该先列出目标目录看看是否存在。 [行动] 调用工具: list_directory 输入: {dirPath:./demoProjects} [观察] 目录 ./demoProjects 内容(空目录) [思考] 目录是空的很好。我需要先创建项目目录。但用户指令中已经指定了路径我可以直接开始创建文件。首先创建 package.json。 [行动] 调用工具: create_file 输入: {filePath:./demoProjects/myServer/package.json,content:{\n \name\: \my-server\,\n \version\: \1.0.0\,\n \main\: \src/index.js\,\n \scripts\: {\n \start\: \node src/index.js\,\n \dev\: \nodemon src/index.js\\n },\n \dependencies\: {\n \express\: \^4.18.0\\n },\n \devDependencies\: {\n \nodemon\: \^3.0.0\\n }\n}} [观察] 文件创建成功: ./demoProjects/myServer/package.json [思考] 文件创建成功。现在需要安装依赖。我应该进入项目目录运行 npm install。 [行动] 调用工具: run_command 输入: {command:npm install,cwd:./demoProjects/myServer} [观察] 命令执行成功。输出(这里会是npm install的详细输出日志)... [思考] 依赖安装成功。现在创建 src/index.js 文件。 [行动] 调用工具: create_file 输入: {filePath:./demoProjects/myServer/src/index.js,content:const express require(express);\nconst app express();\nconst PORT process.env.PORT || 3000;\n\napp.get(/, (req, res) {\n res.send(Hello from my-server!);\n});\n\napp.listen(PORT, () {\n console.log(Server is running on port ${PORT});\n});} [观察] 文件创建成功: ./demoProjects/myServer/src/index.js [思考] 所有步骤都已完成。项目已成功创建在 ./demoProjects/myServer 目录下包含了 package.json、安装的依赖和入口文件。 最终回复 已完成我已经在 ./demoProjects/myServer 目录下成功创建了你的Node.js项目。 - 创建了 package.json 文件并按要求配置了 name 和 version添加了 start 和 dev 脚本。 - 运行了 npm install成功安装了 express 和 nodemon 依赖。 - 创建了 src/index.js 文件内容是一个基本的Express服务器监听3000端口。 你现在可以进入该目录运行 npm run dev 来启动开发服务器。过程深度解析规划能力智能体没有一上来就创建文件。它先调用list_directory工具去“观察”环境确认目录状态。这是一个符合人类直觉的谨慎行为。顺序执行它正确地规划了顺序创建项目定义文件package.json- 安装依赖npm install- 创建应用代码index.js。如果顺序错了比如先创建index.js再安装express代码可能会因找不到模块而无法运行。上下文理解在创建package.json时它不仅填充了用户明确要求的name和version还根据常识补充了main入口、scripts等字段并正确区分了dependencies和devDependencies。这体现了LLM基于训练知识的“常识性补全”能力。错误韧性注意npm install命令的输出可能很长但智能体将其作为“观察”结果接收后能正确判断为成功并继续下一步。如果npm install失败如网络错误工具会返回失败信息智能体就会“观察”到失败并可能尝试重试或报告给用户。4.2 场景二处理更复杂的请求与边界情况让我们测试一下智能体处理模糊指令和应对问题的能力。// 在demo.js中继续添加测试 async function main() { // ... 之前的测试 ... console.log(\n\n--- 测试复杂指令 ---); // 测试指令2一个更模糊的请求 await runAgent( “我想做一个简单的待办事项网页用HTML、CSS和纯JavaScript。把它放在 ./demoProjects/todoApp 里。” ); console.log(\n\n--- 测试错误处理 ---); // 测试指令3可能引发错误的指令 await runAgent( “在 ./demoProjects 里创建一个项目然后运行一个不存在的命令 fakecommand。” ); }对于第二个指令一个优秀的智能体可能会创建index.html包含基本的HTML结构和待办事项输入框、列表。创建style.css添加一些基本样式。创建app.js实现添加、删除待办事项的JavaScript逻辑。可能还会创建一个简单的README.md说明文件。对于第三个指令关键在于观察智能体如何处理工具执行失败run_command工具会返回“命令执行失败: Command failed: fakecommand ...”。智能体接收到这个“观察”后应该能理解任务部分失败。它可能会在最终回复中告诉你“项目目录已创建但执行 ‘fakecommand’ 时失败因为该命令不存在。” 这展示了智能体对部分成功任务的处理能力。4.3 调试与优化让智能体更可靠在实际运行中你可能会遇到智能体“犯傻”的情况比如循环调用不停地在list_directory和思考之间循环无法推进。工具参数错误生成的路径格式不对或者命令语法错误。不理解最终目标完成了几个步骤后就提前宣布结束。调试技巧利用Streaming输出如前所述实时观察[思考]、[行动]、[观察]日志是最重要的调试手段。你能看到智能体的完整决策链精准定位它是在哪一步“想歪了”。优化提示词Prompt Engineering大部分问题可以通过优化系统提示词解决。例如如果智能体总是不主动结束可以在提示词中强调“当你认为已经完成了用户请求的所有核心任务时请给出最终总结并结束。” 如果它总用错工具可以更详细地描述每个工具的用途和适用场景。调整模型参数尝试稍微提高temperature如到0.3可能让智能体在规划时更有创造力或者换用更强大的模型版本如果可用。增加验证步骤在工具函数内部或外部可以增加更严格的输入验证。例如在createFileTool中检查文件路径是否在允许的工作目录范围内防止路径遍历攻击。5. 超越基础引入状态管理与LangGraph我们目前构建的智能体在单次对话中表现良好。但它有一个明显的局限缺乏持久化的状态记忆。每次调用runAgent都是全新的开始它不记得之前的对话。对于多轮、复杂的项目创建任务比如“在刚才的项目里加个路由”这就行不通了。这就是LangGraph大显身手的地方。LangGraph允许你以“图”的形式定义智能体的工作流其中节点可以是LLM调用、工具执行或条件判断边代表状态流转。更重要的是它可以轻松地集成Memory让状态在多次调用间持久化。5.1 为何需要LangGraph想象一个更复杂的场景用户“创建一个React项目。” 智能体创建了项目 用户“在src目录下加一个components文件夹里面放一个Button组件。” 智能体它需要知道当前正在操作哪个项目、项目结构如何才能执行这个操作没有记忆第二个问题就无法被正确处理。LangGraph通过StateGraph和MemorySaver等组件可以优雅地解决这个问题。它将整个对话和执行历史作为“状态”的一部分在图中传递。5.2 快速上手为我们的智能体添加记忆由于篇幅所限这里给出一个简化的概念性代码展示如何将我们现有的Agent改造成一个有状态的LangGraph工作流。// agentWithGraph.js (概念示例) import { StateGraph, END } from langchain/langgraph; import { MemorySaver } from langchain/langgraph; import { BaseMessage } from langchain/core/messages; import { HumanMessage, AIMessage } from langchain/core/messages; // 定义状态的结构 const State { messages: { // 存储完整的对话历史 value: (x: BaseMessage[], y: BaseMessage[]) x.concat(y), default: () [], }, workingDir: { // 当前工作目录 value: (x: string, y: string) y || x, default: () process.cwd(), }, // 可以添加更多状态如当前项目路径、已执行步骤等 }; // 1. 定义节点函数 // 节点调用我们之前构建的agentExecutor async function callAgentExecutor(state) { const { messages, workingDir } state; const latestHumanMessage messages.filter(m m._getType() human).pop(); if (!latestHumanMessage) return { messages: [] }; const inputs { input: latestHumanMessage.content, working_directory: workingDir, chat_history: messages.slice(0, -1), // 传入历史消息 }; const response await agentExecutor.invoke(inputs); // 使用invoke而非stream // 将AI的响应添加到消息历史 return { messages: [new AIMessage(response.output)] }; } // 节点判断是否继续简化版总是继续 function shouldContinue(state) { // 这里可以实现更复杂的逻辑比如检查AI消息中是否有“最终答案”关键词 // 本例中我们假设总是需要用户提供下一个指令所以返回“继续”的边名 return continue; } // 2. 构建图 const workflow new StateGraph(State) .addNode(agent, callAgentExecutor) // 添加agent节点 .addEdge(agent, shouldContinue) // agent节点后执行条件判断 .addConditionalEdges( shouldContinue, { continue: agent, // 如果继续则循环回agent节点 [END]: END, // 如果结束则到达终点 } ) .setEntryPoint(agent); // 设置入口节点 // 3. 添加记忆持久化 const memory new MemorySaver(); // 这会将会话状态存储到内存可配置为数据库 const app workflow.compile({ checkpointer: memory }); // 4. 使用图应用进行多轮对话 async function chatWithAgent(threadId, userMessage) { const config { configurable: { thread_id: threadId } }; const inputs { messages: [new HumanMessage(userMessage)] }; const stream await app.stream(inputs, config); let finalState; for await (const chunk of stream) { // 处理流式输出... finalState chunk; } // finalState 包含了更新后的所有状态包括完整的对话历史 return finalState; }这个示例勾勒了方向我们将单次的智能体调用封装成一个LangGraph的“节点”并将整个对话历史作为“状态”在图中流转。MemorySaver通过thread_id来区分不同的会话线程从而实现记忆的隔离和持久化。带来的好处真正的多轮对话智能体可以记住“我们正在做什么项目”、“刚才创建了哪些文件”。复杂工作流可以轻松地在图中添加分支、循环。例如增加一个“代码质量检查”节点在创建文件后自动运行ESLint。更好的可控性开发者可以像设计流程图一样精确控制智能体的决策路径。当然将完整的ReAct Agent嵌入LangGraph需要更精细的状态设计需要把Agent的agent_scratchpad也纳入状态这涉及到更高级的用法。但上面的示例清晰地指出了从“一次性智能体”迈向“有状态的、可持续交互的智能体”的路径。6. 项目总结与未来展望走到这一步我们已经从零开始构建了一个具备真正“自主行动”能力的项目创建智能体。它不再是简单的聊天机器人而是一个能够理解模糊意图、拆解复杂任务、安全调用系统工具并完成实际工作的数字助手。回顾整个构建过程最关键的收获不在于使用了某个特定的库或API而在于理解了智能体架构的核心思想LLM作为推理引擎工具作为执行器通过一个精心设计的控制流如ReAct循环和状态管理机制如记忆和图将它们有机结合起来。这个模式是通用的无论你是想做一个自动写文档的智能体、一个数据分析智能体还是一个游戏中的NPC其内核都是相通的。在实践过程中我深刻体会到几个决定智能体是否“好用”的细节工具设计的健壮性工具函数必须考虑到所有可能的失败情况并以结构化的方式将成功或失败的信息返回给LLM这是智能体能够“从错误中学习”并调整策略的基础。提示词是方向盘系统提示词定义了智能体的“性格”和能力边界。花时间打磨提示词用清晰、无歧义的语言设定规则比调整模型参数往往更有效。可观测性是生命线一定要让智能体的“思考过程”可视化。LangChain的stream接口或LangGraph的调试工具是排查智能体“诡异行为”不可或缺的利器。这个“Mini Cursor”只是一个起点。在此基础上你可以轻松地进行扩展集成更多工具连接Git API来自动提交代码、连接Docker API来初始化容器环境、连接云服务API直接部署项目。增强规划能力引入更复杂的规划模块让智能体在行动前先输出一个详细的步骤清单Step-by-step Plan并进行校验。加入验证与回滚在工具执行后增加一个验证步骤如检查文件是否创建成功、命令返回值是否正确如果失败可以触发自动回滚或尝试备用方案。构建Web界面用FastAPI或Next.js为你的智能体套上一个聊天界面让它变成一个真正的产品。智能体开发的世界刚刚拉开帷幕充满了挑战和乐趣。亲手搭建一个哪怕功能简单你所获得的关于LLM能力边界、错误处理、人机协同的直觉是任何教程都无法替代的。希望这个系列能成为你探索Agent世界的一块坚实跳板。接下来就基于这个核心去创造属于你自己的、更强大的智能体吧。