从零到一:打造属于自己的AI Agent,这一篇就够了

发布时间:2026/7/22 11:03:19
从零到一:打造属于自己的AI Agent,这一篇就够了 从零到一打造属于自己的 AI Agent这一篇就够了不是调个 API 那么简单——真正值钱的 Agent 开发底层逻辑全在这里。一、你写的那个 API 调用根本不算 Agent绝大多数开发者第一次接触大模型都是从这样一段代码开始的javascriptconst response await model.invoke(帮我写一个贪吃蛇游戏); console.log(response.content);这叫什么这叫单次 API 调用——你把问题丢过去它返回一段文字然后呢没了。真实世界的任务长这样吗当然不是。你说帮我部署一个新项目它需要拉代码 → 安装依赖 → 配置环境 → 构建 → 部署 → 通知你结果大模型自己做不到。它只能给你一段部署思路的文字。为什么因为 LLM 有三大致命缺陷缺陷问题后果无状态Stateless每次调用独立不记得上一句说过什么没法跨会话工作无手脚No Tools只能输出文字不能执行操作只会说不会做无私有知识No Private Data只知道训练数据里的东西不知道你的业务、你的代码、公司政策Agent 的本质就是给 LLM 装上记忆手脚和知识库的系统。这篇文章就是带你把这套系统从零搭出来。二、Agent 的五个核心模块先记下这个公式后面全在讲它Agent LLM Memory Tool RAG MCP Skills我们一个一个拆开看。2.1 Memory 记忆模块 —— 解决转头就忘LLM 本质上是一个纯函数输入 token输出 token。函数返回之后什么都不留下。javascript第一次调用model.invoke(我叫张三); → 好的张三第二次调用—— 它根本不记得model.invoke(我叫什么); → 我不知道你没说过你感受到的所有对话感都是前端在替你作弊——每次请求都把整段历史重新塞给模型。生产级的 Agent 需要三层记忆架构层级存储位置用途工作记忆当前消息数组当前任务的每一步决策上下文短期记忆Redis / 内存跨步骤的任务状态长期记忆数据库 向量库跨会话的用户偏好、历史经验2.2 Tool 工具模块 —— 解决只会说不会做工具让 LLM 能调用外部能力读文件、写代码、执行命令、发 HTTP 请求、操作数据库……一个工具由三部分组成javascriptconst readFileTool tool( async ({ file_path }) { // ① 实际执行的函数 return await fs.readFile(file_path, utf-8); }, { name: read_file, // ② LLM 看到的工具名 description: 用此工具读取文件内容..., // ③ LLM 靠这段描述决定是否调用 schema: z.object({ // ④ 参数约束zod 验证 file_path: z.string().describe(要读取的文件路径), }), } );当 LLM 觉得需要读文件时它不会继续输出文字而是返回一个 tool_call——告诉后端去执行 read_file参数是 xxx。后端执行完把结果塞回对话历史LLM 继续推理。这就是 Agent 循环的核心机制。2.3 RAG 检索增强生成 —— 解决不知道你的东西LLM 不知道你公司的内部规定、最新的新闻、你私有代码库里的业务逻辑。RAG 的做法很简单在问 LLM 之前先去知识库里搜相关资料一起喂给 LLM。用户问题 → 向量化 → 在知识库检索相关文档 → 拼到 Prompt 里 → 发给 LLM举个例子问公司年假几天→ 先去内部知识库搜假期政策文档 → 把文档内容和问题一起给 LLM → 得到精准回答。2.4 MCP —— 第三方工具的通用协议MCPModel Context Protocol是 Anthropic 推出的标准协议让 LLM 能通过统一接口调用任何第三方工具。以前你想让 Agent 操作 GitHub Slack 数据库每个都要单独写集成代码。MCP 把这统一了——就像 USB 协议让任何设备都能插上电脑MCP 让任何工具都能被 LLM 调用。2.5 Skills —— 技能蒸馏把一段复杂的操作流程封装成一个可复用的技能。比如代码审查部署上线生成周报每个都是一段固定的工作流模板Agent 遇到对应任务时直接加载执行。三、Agent 的工作流程 —— 一个完整的循环用户提出复杂任务Prompt↓LLM 规划/推理Planning Reasoning↓是否需要加载 Memory ──→ 是 → 检索相关记忆↓ 否是否需要调用 Tool ──→ 是 → 执行工具 → 结果返回↓ 否是否需要查 RAG ──→ 是 → 检索知识库 → 拼入 Prompt↓ 否LLM 生成最终响应↓返回给用户任务完成这个循环会一直迭代直到 LLM 认为任务完成或达到最大步数限制。四、LangChain LangGraphAgent 的两层抽象LangChain解决的是单个 Agent 怎么工作的问题统一 LLM 接口无论是 OpenAI、DeepSeek 还是 Claude都用 model.invoke()封装 Tool 定义函数 描述 zod 约束一行搞定管理消息历史HumanMessage / AIMessage / ToolMessage 统一抽象提供 Memory、RAG、Chain 等开箱即用的能力javascriptLangChain 里定义工具 绑定 LLM就一个动作const modelWithTools model.bindTools(tools);LangGraph解决的是多个 Agent 怎么协作的问题Planner规划节点/ \Coder Agent Searcher Agent ← 并行执行\ /Reviewer审查节点↓人工确认节点条件分支↓部署执行节点LangGraph 把流程建模成图Node Edge天然支持分支、循环、并行、人工暂停等复杂编排。对比项LangChainLangGraph模式线性链条有向图适用场景单 Agent多 Agent 协作核心概念Chain、Tool、MemoryState、Node、Edge、Conditional Edge五、Agent 里那个容易被忽视的性能问题如果一个任务需要调用 10 次工具每次串行等待调工具1等2s→ 调工具2等2s→ 调工具3等2s→ ... 总计 20s但如果工具 1、2、3 之间没有依赖关系完全可以并行执行。这时就该用 Promise.alljavascript// 串行一个一个来const result1 await toolA();const result2 await toolB();const result3 await toolC();// 耗时 A B C// 并行一起上const [result1, result2, result3] await Promise.all([toolA(),toolB(),toolC(),]);// 耗时 max(A, B, C)这是 Agent 从能跑到高性能的关键一步。回顾一下 Promise 的三种状态状态含义pending等待中fulfilled已完成成功rejected已失败状态只能从 pending 变为 fulfilled 或 rejected且一旦变化不可逆转。而 async/await 只是 Promise 的语法糖——让你用同步的写法写异步的逻辑。六、技术栈选型怎么搭一套生产级 Agent 系统textNestJS后端框架 LangChain单 Agent 能力LLM 调用、工具定义、记忆管理、RAG LangGraph多 Agent 编排工作流、条件分支、人工审批 MCP / RAG / Skills扩展能力层为什么选 NestJS因为 Agent 系统本质上是几个独立模块的集合编排器、记忆管理、RAG 检索、工具执行、权限控制。NestJS 的 Module 依赖注入天然适合这种架构——每个模块独立开发、独立测试通过 DI 容器灵活组装。七、写给想入门的你Agent 开发的门槛其实不在技术在理解。理解 LLM 不是一个有记忆的脑子而是一个无状态的推理函数。理解 Agent 不是在调 API而是在建一套让无状态的大脑变得能记、能做、能查的系统。当你真正理解了这句话Agent LLM Memory Tool RAG MCP Skills你就已经跨过最难的坎了。剩下的框架、代码、调优都是工程问题——而工程问题恰恰是最可解决的问题。彩蛋一个最简 Agent 的代码骨架javascriptimport { ChatOpenAI } from langchain/openai;import { tool } from langchain/tools;import { HumanMessage, ToolMessage, AIMessage} from langchain/core/messages;import fs from node:fs/promises;import { z } from zod; // 1. 定义工具const readFileTool tool( async ({ file_path }) await fs.readFile(file_path, utf-8), { name: read_file, description: 读取文件内容, schema: z.object({ file_path: z.string().describe(文件路径), }), } );const runCommandTool tool( async ({ command }) { const { execSync } await import(node:child_process);return execSync(command, { encoding: utf-8 });},{name: run_command, description: 执行终端命令, schema: z.object({ command: z.string().describe(要执行的命令), }), } ); // 2. 初始化 LLM 绑定工具const model new ChatOpenAI({ modelName: deepseek-v4-flash, apiKey: process.env.DEEPSEEK_API_KEY, configuration: { baseURL: https://api.deepseek.com/v1 },});const modelWithTools model.bindTools([readFileTool, runCommandTool]); // 3. Agent 循环async function agent(task) { const messages [new HumanMessage(task)];const maxSteps 20; const toolsMap { read_file: readFileTool, run_command: runCommandTool };for (let step 0; step maxSteps; step) { const response await modelWithTools.invoke(messages);messages.push(response); const toolCalls response.tool_calls || []; if (toolCalls.length 0) { return response.text; // 任务完成} // 并行执行所有工具调用 const toolResults await Promise.all( toolCalls.map(async (tc) { const result await toolsMap[tc.name](tc.args); console.log([工具] ${tc.name}: ${result.slice(0, 100)}...); return new ToolMessage({ content: result, tool_call_id: tc.id,});})); messages.push(...toolResults); } } // 4. 跑起来const answer await agent(检查 package.json 里有什么依赖);console.log(answer);学会了吗先跑通这段代码再回头重读这篇文章的每一个概念。你会发现你理解的深度完全不一样了