从零到一:打造属于自己的AI Agent,这一篇就够了
从零到一:打造属于自己的 AI Agent,这一篇就够了
不是调个 API 那么简单——真正值钱的 Agent 开发,底层逻辑全在这里。
一、你写的那个 API 调用,根本不算 Agent
绝大多数开发者第一次接触大模型,都是从这样一段代码开始的:
javascript
const 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 请求、操作数据库……一个工具由三部分组成:
javascript
const 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 —— 第三方工具的通用协议
MCP(Model 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 + LangGraph:Agent 的两层抽象
LangChain 解决的是"单个 Agent 怎么工作"的问题:
统一 LLM 接口(无论是 OpenAI、DeepSeek 还是 Claude,都用 model.invoke())
封装 Tool 定义(函数 + 描述 + zod 约束,一行搞定)
管理消息历史(HumanMessage / AIMessage / ToolMessage 统一抽象)
提供 Memory、RAG、Chain 等开箱即用的能力
javascript
- LangChain 里定义工具 + 绑定 LLM,就一个动作const modelWithTools = model.bindTools(tools);
LangGraph 解决的是"多个 Agent 怎么协作"的问题:
Planner(规划节点)
/ \
Coder Agent Searcher Agent ← 并行执行
\ /
Reviewer(审查节点)
↓
人工确认节点(条件分支)
↓
部署执行节点
LangGraph 把流程建模成图(Node + Edge),天然支持分支、循环、并行、人工暂停等复杂编排。
| 对比项 | LangChain | LangGraph |
|---|---|---|
| 模式 | 线性链条 | 有向图 |
| 适用场景 | 单 Agent | 多 Agent 协作 |
| 核心概念 | Chain、Tool、Memory | State、Node、Edge、Conditional Edge |
五、Agent 里那个容易被忽视的性能问题
如果一个任务需要调用 10 次工具,每次串行等待:
调工具1(等2s)→ 调工具2(等2s)→ 调工具3(等2s)→ ... 总计 20s
但如果工具 1、2、3 之间没有依赖关系,完全可以并行执行。这时就该用 Promise.all:
javascript
// 串行:一个一个来
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 系统?
text
NestJS(后端框架) + 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 的代码骨架
javascript
import { 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);
学会了吗?先跑通这段代码,再回头重读这篇文章的每一个概念。
你会发现,你理解的深度完全不一样了
更多推荐

所有评论(0)