尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

从零手写AI智能体:基于LangChain与DeepSeek的项目自动化实践

从零手写AI智能体:基于LangChain与DeepSeek的项目自动化实践 1. 项目概述为什么我们要“手写”一个智能体最近“智能体”这个词火得不行感觉一夜之间从技术社区到产品发布会不提两句Agent都不好意思说自己在搞AI。但说实话很多文章和教程要么是高高在上的概念科普要么是直接甩给你一个Dify、LangFlow这样的可视化平台让你拖拖拽拽就“组装”出一个智能体。这当然很高效但对于想真正理解其内在机理、想拥有定制和调试能力的开发者来说总觉得隔了一层纱。所以我决定动手从零开始用代码“手写”一个能自主建项目的Mini Cursor。这个想法源于一个很实际的痛点我经常需要初始化一些前端项目每次都是npm create vitelatest然后回答一堆配置问题再安装一堆依赖流程固定且重复。为什么不让AI来干这个更进一步为什么不让AI不仅能理解我的指令还能像人一样自己思考、规划步骤、调用工具、检查结果直到把项目完整地搭建起来这就是我们这期要实现的“智能体”。这个项目不是简单的函数调用而是一个具备规划、执行、反思能力的自主Agent。我们将使用Node.js作为运行环境LangChain作为核心框架来组织AI的“思维链条”并接入DeepSeek的最新模型作为“大脑”。通过这个过程你会彻底明白一个智能体是如何被“组装”起来的从接收用户指令到拆解任务到调用终端命令再到验证结果。这比使用任何现成平台都更能让你获得对智能体开发的“手感”和“掌控感”。2. 核心架构与工具选型为什么是LangChain DeepSeek在开始敲代码之前我们必须为智能体选择一个合适的“骨架”和“大脑”。市面上框架很多比如LangChain、LangGraph、CrewAI等。这里我选择LangChain原因很直接它生态成熟、文档丰富并且其AgentExecutor的设计哲学与我们想要构建的“规划-执行”循环高度契合。它不像一些更高级的框架如LangGraph那样一开始就引入复杂的状态图而是提供了一个清晰、直接的起点非常适合我们理解第一性原理。至于“大脑”也就是大语言模型我选择了DeepSeek。原因有几个首先它的API性价比极高对于个人开发者和小项目非常友好其次在代码生成和理解方面DeepSeek的表现有目共睹非常适合我们这种需要精确生成命令行和代码的项目最后其最新的V4 Flash模型在响应速度和长上下文处理上表现优异。当然你也可以替换为OpenAI的GPT-4或Claude 3.5核心架构是通用的。我们的智能体需要与外界交互最主要的就是操作本地文件系统。因此我们需要给AI“安装手臂”。在LangChain中这通过工具来实现。我们将创建一个最核心的工具ExecuteCommandTool。这个工具允许AI在得到用户授权后在安全的子进程中执行Shell命令如mkdir,npm init,git等。这里有一个至关重要的安全考量绝不能允许AI无限制地执行任何命令。我们的设计是AI必须将其思考后决定要执行的命令以结构化的格式如JSON输出由我们的主程序解析并显式地请求用户确认后才会真正执行。这是手写Agent与使用黑盒平台最大的区别之一——安全可控。整个系统的数据流大致如下用户输入自然语言指令如“创建一个Vue 3 TypeScript Pinia的项目名叫my-app”。LangChain Agent接收指令其背后的DeepSeek模型开始“思考”将复杂任务拆解为步骤。对于需要执行命令的步骤Agent会调用ExecuteCommandTool并生成一个待执行的命令对象。我们的主程序拦截这个命令对象打印出来并询问用户“AI建议执行npm create vuelatest my-app是否继续(y/N)”。用户确认后程序在子进程中执行该命令并捕获输出和错误。执行结果成功输出或错误信息被反馈给Agent作为下一步“思考”的上下文。Agent根据反馈决定下一步行动继续执行下一个命令或判断任务已完成。这个“观察-思考-行动-反馈”的循环就是智能体自主性的核心。3. 手把手实现构建Mini Cursor智能体3.1 环境准备与初始化首先确保你的系统已经安装了Node.js建议版本18以上和npm。然后我们创建一个新的项目目录并初始化。mkdir mini-cursor-agent cd mini-cursor-agent npm init -y接下来安装我们所需的核心依赖npm install langchain langchain/core dotenv npm install --save-dev typescript types/node ts-nodelangchainlangchain/core: LangChain框架的核心库。dotenv: 用于管理环境变量我们将把DeepSeek的API Key放在.env文件中。typescript等我们将使用TypeScript来获得更好的类型提示和代码可靠性。初始化TypeScript配置npx tsc --init在生成的tsconfig.json中确保target是ES2020或更高并且module是commonjs。创建项目结构mini-cursor-agent/ ├── src/ │ ├── tools/ │ │ └── executeCommand.ts # 自定义命令执行工具 │ ├── agents/ │ │ └── projectCreator.ts # 智能体核心逻辑 │ └── index.ts # 程序入口 ├── .env # 环境变量记得加入.gitignore ├── package.json └── tsconfig.json在.env文件中添加你的DeepSeek API KeyDEEPSEEK_API_KEYyour_api_key_here3.2 实现核心工具安全命令执行这是整个智能体的“手”必须既强大又安全。我们在src/tools/executeCommand.ts中实现。import { Tool } from langchain/core/tools; import { exec } from child_process; import { promisify } from util; const execAsync promisify(exec); // 定义工具输入参数的Zod Schema用于让LLM理解如何调用此工具 import { z } from zod; const executeCommandInputSchema z.object({ command: z.string().describe(The exact shell command to execute.), cwd: z.string().optional().describe(The working directory where the command should run.), }); // 工具类定义 export class ExecuteCommandTool extends Tool { name execute_command; description Executes a shell command on the local machine. Use this to create directories, run npm, git, or any other shell operations. IMPORTANT: The command will NOT be executed automatically. It will be presented to the user for confirmation first.; schema executeCommandInputSchema; // 这是LangChain框架调用工具时执行的方法 protected async _call(arg: z.infertypeof executeCommandInputSchema): Promisestring { const { command, cwd process.cwd() } arg; // 注意这里我们并不真正执行命令而是将命令信息返回。 // 真正的执行逻辑在调用者Agent Executor那里它会拦截这个结果并请求用户确认。 return JSON.stringify({ action: REQUEST_COMMAND_EXECUTION, command: command, cwd: cwd, // 可以添加一个唯一ID用于后续跟踪 id: cmd_${Date.now()}, }); } }关键点解析安全第一_call方法没有直接调用execAsync而是返回了一个结构化的JSON对象。这强制将“决策”AI思考和“执行”真实系统调用分离。清晰的描述description字段非常重要它直接引导LLM何时以及如何调用这个工具。我们明确说明了工具的用途和限制。结构化输入使用Zod Schema定义了输入格式这能帮助LLM更准确地生成调用参数减少格式错误。3.3 构建智能体核心逻辑接下来在src/agents/projectCreator.ts中我们将组装智能体。import { ChatDeepSeek } from langchain/deepseek; import { AgentExecutor, createReactAgent } from langchain/agents; import { ExecuteCommandTool } from ../tools/executeCommand; import * as readline from readline/promises; // 用于用户交互确认 // 创建与DeepSeek模型对话的实例 const model new ChatDeepSeek({ apiKey: process.env.DEEPSEEK_API_KEY, modelName: deepseek-chat, // 或使用 deepseek-coder 如果更侧重代码 temperature: 0.1, // 较低的温度使输出更确定、更少随机性适合执行任务 }); // 创建工具数组目前只有我们的命令执行工具 const tools [new ExecuteCommandTool()]; // 使用LangChain的React Agent范式创建智能体 // React Agent是一种流行的Agent模式代表“Reasoning Acting” const agent await createReactAgent({ llm: model, tools, }); // 创建Agent执行器它将管理Agent的运行循环 const agentExecutor new AgentExecutor({ agent, tools, // 设置最大迭代次数防止AI陷入死循环 maxIterations: 10, // 设置为true让执行过程输出详细的思考步骤便于调试 verbose: true, }); // 这是我们的核心函数运行智能体并处理与用户的交互 export async function runProjectCreatorAgent(userPrompt: string): Promisevoid { const rl readline.createInterface({ input: process.stdin, output: process.stdout, }); console.log( 智能体启动任务: ${userPrompt}\n); // 关键我们重写AgentExecutor的_streamEvents以拦截工具调用 const streamEvents agentExecutor.streamEvents( { input: userPrompt }, { version: v2 } ); for await (const event of streamEvents) { const eventType event.event; if (eventType on_chain_start event.name Agent) { console.log( AI正在思考...\n); } // 当工具被调用时我们拦截它 if (eventType on_tool_start) { const toolName event.name; const toolInput event.data.input; if (toolName execute_command) { // 解析工具返回的请求对象 let execRequest; try { execRequest JSON.parse(toolInput.command); } catch { // 如果解析失败可能AI直接返回了命令字符串理论上不会因为我们的工具定义很清晰 execRequest { action: REQUEST_COMMAND_EXECUTION, command: toolInput.command, cwd: toolInput.cwd }; } if (execRequest.action REQUEST_COMMAND_EXECUTION) { console.log(\n⚠️ AI建议执行命令:); console.log( 目录: ${execRequest.cwd || process.cwd()}); console.log( 命令: ${execRequest.command}); // 关键交互请求用户确认 const answer await rl.question(❓ 是否允许执行(y/N): ); if (answer.toLowerCase() y) { console.log( 执行中...); try { // 真正执行命令 const { stdout, stderr } await execAsync(execRequest.command, { cwd: execRequest.cwd }); if (stdout) console.log(✅ 输出:\n${stdout}); if (stderr) console.log(⚠️ 标准错误:\n${stderr}); // 将执行结果构造成一个“观察”反馈给Agent让它继续思考 // 这里需要模拟工具调用的返回格式 const result Command executed successfully in ${execRequest.cwd}. Output: ${stdout || (no output)} ${stderr ? Errors: ${stderr} : }; // 注意这里需要将结果注入回事件流LangChain的底层API处理起来较复杂。 // 更简单的方式是我们直接使用非流式执行的executor并在外部控制循环。 // 因此我们调整一下架构。 } catch (error: any) { console.error(❌ 命令执行失败: ${error.message}); // 同样将错误信息反馈给Agent } } else { console.log(⏹️ 用户取消了命令执行。); // 反馈给Agent用户取消了操作 const result User denied the execution of command: ${execRequest.command}. Please adjust the plan or ask the user for an alternative.; // 需要反馈给Agent } } } } // 当Agent最终给出答案时 if (eventType on_chain_end event.name Agent) { const finalOutput event.data?.output; if (finalOutput finalOutput.output) { console.log(\n 任务完成最终结果:\n${finalOutput.output}); } break; } } rl.close(); }上面的代码展示了核心思路但直接重写streamEvents来处理交互式确认在LangChain中较为复杂。更清晰、更推荐的做法是使用回调函数Callbacks或者采用手动控制每一步Step-wise的执行方式。让我们调整一下采用更直观的“步进”模式。3.4 重构实现步进式执行与用户确认我们修改projectCreator.ts放弃自动执行器手动驱动Agent的每一步。import { ChatDeepSeek } from langchain/deepseek; import { createReactAgent } from langchain/agents; import { ExecuteCommandTool } from ../tools/executeCommand; import { HumanMessage } from langchain/core/messages; import * as readline from readline/promises; import { exec } from child_process; import { promisify } from util; const execAsync promisify(exec); const model new ChatDeepSeek({ apiKey: process.env.DEEPSEEK_API_KEY, modelName: deepseek-chat, temperature: 0.1, }); const tools [new ExecuteCommandTool()]; const agent await createReactAgent({ llm: model, tools }); export async function runProjectCreatorAgentStepwise(userPrompt: string): Promisevoid { const rl readline.createInterface({ input: process.stdin, output: process.stdout, }); console.log( 智能体启动步进模式任务: ${userPrompt}\n); // 初始化Agent状态 let agentState { input: userPrompt, chatHistory: [], // 存储对话历史 intermediateSteps: [], // 存储已执行的步骤工具调用和结果 }; const maxSteps 15; let currentStep 0; while (currentStep maxSteps) { currentStep; console.log(\n--- 第 ${currentStep} 步 ---); // 调用Agent传入当前状态获取下一步动作 const action await agent.invoke({ input: agentState.input, chatHistory: agentState.chatHistory, intermediateSteps: agentState.intermediateSteps, }); // 检查Agent返回的是最终答案还是工具调用 if (action.returnValues?.output) { // 是最终答案 console.log(\n 智能体报告任务完成:); console.log(action.returnValues.output); break; } // 如果是工具调用 const toolCall action.toolCalls?.[0]; if (toolCall) { const toolName toolCall.name; const toolArgs toolCall.args; console.log( 智能体决定调用工具: ${toolName}); console.log( 参数: ${JSON.stringify(toolArgs)}); if (toolName execute_command) { // 解析命令请求 let execRequest; try { // 注意这里toolArgs已经是对象无需再解析JSON execRequest toolArgs; } catch (e) { console.error(解析工具参数失败:, e); break; } console.log(\n⚠️ 建议执行命令:); console.log( 目录: ${execRequest.cwd || process.cwd()}); console.log( 命令: ${execRequest.command}); const answer await rl.question(❓ 是否允许执行(y/N): ); let toolResult; if (answer.toLowerCase() y) { console.log( 执行中...); try { const { stdout, stderr } await execAsync(execRequest.command, { cwd: execRequest.cwd || process.cwd() }); const output stdout || (无输出); const errorMsg stderr ? \n标准错误: ${stderr} : ; console.log(✅ 成功输出: ${output}${errorMsg}); toolResult Command executed successfully. Output: ${output}${errorMsg}; } catch (error: any) { console.error(❌ 执行失败: ${error.message}); toolResult Command failed with error: ${error.message}. Stderr: ${error.stderr || None}; } } else { console.log(⏹️ 用户取消了执行。); toolResult User denied execution of the command: ${execRequest.command}. Please adjust the plan or ask the user for guidance.; } // 将本次工具调用和结果记录到历史中供Agent下一步思考 agentState.intermediateSteps.push({ action: { tool: toolName, toolInput: toolArgs, log: Called ${toolName} }, observation: toolResult, }); // 将用户的取消或执行结果也作为一条消息加入历史提供更多上下文 agentState.chatHistory.push(new HumanMessage(User responded to command execution request. Result: ${toolResult})); } else { console.log(未知工具: ${toolName}); break; } } else { console.log(智能体返回了无法处理的响应:, action); break; } // 防止长时间运行 if (currentStep maxSteps) { console.log(\n⚠️ 达到最大步数限制(${maxSteps})强制终止。); break; } } rl.close(); console.log(\n 智能体运行结束。); }这个“步进式”版本更加清晰。它手动控制着Agent的思考循环调用Agent - 获取动作工具调用或最终答案- 如果是工具调用则与用户交互 - 将结果反馈给Agent - 继续下一步。这种方式让我们对整个过程有完全的控制权便于插入安全确认和调试日志。3.5 主程序入口最后在src/index.ts中我们创建一个简单的CLI来启动智能体。import { config } from dotenv; import { runProjectCreatorAgentStepwise } from ./agents/projectCreator; // 加载环境变量 config(); async function main() { // 从命令行参数获取用户指令如果没有则使用默认示例 const userPrompt process.argv.slice(2).join( ) || 请帮我创建一个名为my-vue-app的Vue 3项目使用TypeScript和Vite并安装Pinia进行状态管理。; console.log(.repeat(50)); console.log(Mini Cursor 智能体 - 项目创建助手); console.log(.repeat(50)); try { await runProjectCreatorAgentStepwise(userPrompt); } catch (error) { console.error(智能体运行出错:, error); } } main();现在你可以运行你的智能体了npx ts-node src/index.ts或者为了更直观你可以传递自定义指令npx ts-node src/index.ts 初始化一个Node.js项目名字叫server使用Express框架并安装mongoose和dotenv4. 实战演示与效果评估让我们运行一个真实案例。输入指令“创建一个React 18 TypeScript项目使用Vite构建项目名称为react-ts-demo并安装Tailwind CSS和React Router DOM。”智能体运行日志节选: 智能体启动步进模式任务: “创建一个React 18 TypeScript项目...” --- 第 1 步 --- 智能体决定调用工具: execute_command 参数: {command:npm create vitelatest react-ts-demo -- --template react-ts,cwd:.} ⚠️ 建议执行命令: 目录: . 命令: npm create vitelatest react-ts-demo -- --template react-ts ❓ 是否允许执行(y/N): y 执行中... ✅ 成功输出: (无输出) # 实际会有一系列交互式提示这里假设全部默认 --- 第 2 步 --- 智能体决定调用工具: execute_command 参数: {command:cd react-ts-demo,cwd:.} ⚠️ 建议执行命令: 目录: . 命令: cd react-ts-demo ❓ 是否允许执行(y/N): y 执行中... ✅ 成功输出: (无输出) --- 第 3 步 --- 智能体决定调用工具: execute_command 参数: {command:npm install -D tailwindcss postcss autoprefixer,cwd:./react-ts-demo} ... --- 第 4 步 --- 智能体决定调用工具: execute_command 参数: {command:npx tailwindcss init -p,cwd:./react-ts-demo} ... --- 第 5 步 --- 智能体决定调用工具: execute_command 参数: {command:npm install react-router-dom,cwd:./react-ts-demo} ... --- 第 6 步 --- 智能体报告任务完成: 已成功创建项目‘react-ts-demo’。依次执行了以下操作 1. 使用Vite模板创建了ReactTypeScript项目。 2. 进入项目目录。 3. 安装了Tailwind CSS及其依赖。 4. 初始化了Tailwind配置。 5. 安装了React Router DOM。 项目已就绪你可以运行 npm run dev 启动开发服务器。效果评估规划能力智能体成功将复杂指令拆解为顺序正确的步骤创建 - 进入目录 - 安装依赖A - 配置A - 安装依赖B。工具使用准确使用了execute_command工具并生成了正确的命令和参数如正确的cwd。上下文感知第二步的cd命令是基于第一步成功创建了react-ts-demo目录的假设。在实际流中AI需要根据上一步的输出来判断是否成功我们的代码通过将上一步结果toolResult放入intermediateSteps实现了这一点。任务完成判断在安装完所有依赖后智能体判断核心任务已完成给出了总结性输出。这个Mini Cursor智能体已经具备了基础的项目创建自动化能力。它比简单的脚本更灵活因为它能理解自然语言描述的需求并动态规划步骤。5. 深度优化与进阶思考一个基础的、能跑的智能体已经完成了。但要让它真正可靠、强大我们还需要解决很多问题。5.1 增强工具能力与安全性目前的工具只有一个“执行命令”这很强大但也危险。我们可以创建更精细、更安全的工具ReadFileTool/WriteFileTool允许AI读取和修改特定类型的配置文件如package.json,tailwind.config.js而不是直接执行echo或cat命令。这可以施加更严格的路径和内容校验。CheckDirectoryExistsTool让AI先检查目录是否存在再决定是创建还是报错。RunNPMScriptTool专门用于运行package.json中定义的脚本如dev,build比直接执行npm run更可控。安全策略升级命令白名单在ExecuteCommandTool中维护一个允许的命令前缀白名单如npm,npx,git clone,mkdir等禁止rm -rf、curl | bash等危险命令。沙盒环境考虑在Docker容器或临时目录中执行命令实现环境隔离。权限分级区分“查询类”命令如ls,pwd和“修改类”命令如npm install,git init后者需要更高级别的用户确认。5.2 引入记忆与反思机制当前的Agent是“无状态”的每次调用只基于当前输入和最近几步的历史。为了处理更复杂的、可能失败的任务我们需要引入记忆和反思。短期记忆ConversationBufferMemory使用LangChain提供的内存模块让Agent能记住更长的对话历史避免重复提问。反思ReAct模式的高级应用当工具执行失败如npm install网络超时时我们不应简单地让Agent停止或重试。可以设计一个“反思”步骤将错误信息反馈给Agent并提示它“分析失败原因并提出修正方案”。例如Agent可能会反思“网络错误建议重试一次”或“依赖包名可能写错建议检查拼写”。这需要更精细的Prompt工程和可能的多轮Agent调用。5.3 处理复杂依赖与条件逻辑创建项目时步骤间常有依赖关系。例如“安装Tailwind CSS”必须在“进入项目目录”之后“初始化Tailwind配置”又必须在“安装Tailwind”之后。我们的简单步进循环能处理线性依赖但面对分支选择就力不从心了。场景用户说“创建一个项目如果用Vue就装Element Plus如果用React就装Ant Design”。解决方案这需要Agent具备更强的推理能力。我们可以在Prompt中明确要求AI先做出选择再规划后续步骤。或者引入LangGraph这样的框架将工作流显式地定义为有向图节点代表检查点或决策点边代表条件分支。这将是智能体从“脚本”升级为“工作流引擎”的关键一步。5.4 提升可靠性的工程化实践结构化输出Structured Output强制要求DeepSeek模型以严格的JSON格式输出其“思考过程”和“工具调用请求”。这能极大减少解析错误。最新的LangChain和DeepSeek API都对此有良好支持。超时与重试为每个工具调用和AI思考过程设置超时。网络请求失败或AI响应缓慢时自动重试1-2次。完整的日志系统不仅记录到控制台还将每一步的AI思考、工具调用、用户确认、执行结果持久化到文件或数据库。这对于调试复杂任务和复盘故障至关重要。配置化管理将模型参数temperature, maxTokens、工具列表、允许的命令白名单、最大步数等提取为配置文件便于不同场景下的调整。6. 踩坑实录与避坑指南在开发这个Mini Cursor的过程中我遇到了不少坑这里分享出来希望能帮你节省时间。坑一AI的“幻觉”与命令生成错误现象AI有时会生成不存在的命令标志或者错误的包名如npm create react-appinstead ofnpm create vite。根因LLM的训练数据可能过时或者它只是在模仿常见的模式而非精确记忆。解决方案在工具描述中提供范例在ExecuteCommandTool的description里明确写出“例如创建Vue项目使用npm create vuelatest创建React项目使用npm create vitelatest”。使用更专精的模型尝试切换modelName为deepseek-coder它在代码和命令生成上通常更准确。后置校验在执行命令前可以添加一个简单的校验层比如检查npm install后面的包名是否在npm registry中存在可通过一个快速的API调用检查。坑二上下文丢失与无限循环现象Agent执行了几步后似乎忘记了最初的目标或者在一个步骤上反复尝试失败陷入循环。根因intermediateSteps历史可能太长或被截断导致关键目标信息丢失或者Agent缺乏“任务已完成”的明确判断标准。解决方案优化Prompt在每次调用Agent的input中除了当前用户消息反复强调核心任务目标。例如“你的最终目标是${initialGoal}。当前已完成的步骤有...。请根据当前情况决定下一步。”设置明确的终止条件除了最大步数可以定义一些成功信号。例如如果检测到目标目录中存在package.json且关键依赖已安装可以主动向Agent发送“任务似乎已完成请确认并总结”的提示。使用有状态的执行器LangChain的AgentExecutor本身会管理步骤历史比我们手动的步进循环更健壮。可以回归使用它并通过callbacks来拦截工具调用进行确认。坑三用户交互体验割裂现象每次确认命令都需要用户输入在创建包含十几个步骤的大型项目时非常繁琐。解决方案实现批处理模式提供一个启动参数如--yes自动批准所有低风险命令如mkdir,cd仅对高风险命令如rm, 写入系统文件进行确认。提供预览模式Agent先规划出所有步骤并列出用户一次性审核批准后再按顺序自动执行。分级确认将命令分为“安全”、“警告”、“危险”等级别不同级别采用不同的确认策略。坑四错误处理不够健壮现象命令执行失败如网络错误后Agent收到的错误信息过于原始无法做出有效应对。解决方案在工具执行层execAsync的catch块中对常见错误进行归类和处理生成对AI更友好的错误描述。例如将npm ERR! code E404转换为“NPM包未找到请检查包名拼写”将Command failed with signal SIGTERM转换为“进程被终止可能是超时或资源不足”。手写一个智能体的过程就像在教一个极其聪明但缺乏常识和手眼协调能力的孩子完成一件复杂的手工。你需要为它设计清晰的动作指令工具在它每次行动前检查安全性用户确认并在它困惑或犯错时给予明确的反馈错误处理与上下文管理。这个过程充满挑战但一旦跑通你对AI如何思考、如何与真实世界交互的理解会远超过仅仅调用一个API。这个Mini Cursor只是一个起点你可以沿着这个框架为它添加更多工具连接数据库、调用云API、操作Docker赋予它更强大的能力去自动化你工作流中任何重复、枯燥的部分。
返回列表