从零打造你的第一个智能体(Agent):当大模型学会“动手干活”
别再只当“聊天机器人”了,让 LLM 拥有记忆、工具和知识库,真正成为你的自动化助手。
作为一名全栈开发者,你可能已经调过无数次大模型接口,给个 prompt 就能得到一段文案或代码。但当你问它“帮我读取项目里的 tool.mjs 并解释代码”时,它只会告诉你“你可以用 fs.readFile 这样读”,却不会真的帮你读。当你问它“上周我们聊过的那个设计文档还记得吗”,它一脸茫然——因为 LLM 天生是无状态的(stateless)。
这些痛点正是 Agent(智能体) 要解决的问题。本文会从 LLM 的先天不足出发,一步步拆解 Agent 的核心模块,然后用 LangChain 手写一个能读文件、能解释代码的简易 Agent,并深挖工具调用中的并发性能优化。读完你会明白:Agent = LLM + Memory + Tool + RAG + MCP + Skills 不只是公式,更是落地方案。
一、LLM 很好,但还“不够”
大模型(LLM)本质是一个概率生成器,它根据输入的 tokens 预测下一个 token。这种设计让它博学多才,但也带来了几个硬伤:
| 问题 | 表现 | 根因 |
|---|---|---|
| 无记忆 | 每次对话都是全新的,它不记得你 5 分钟前说了什么 | 无状态(stateless),每次请求独立 |
| 不会执行 | 只会告诉你“应该怎么做”,不会真的去操作文件、调 API | 没有执行能力,只输出文本 |
| 知识陈旧 | 不知道最新的世界杯冠军,也不知道你公司的内部文档 | 训练数据有截止日期,且私有数据从未见过 |
| 无法规划复杂任务 | 面对“帮我创建一个 React + Vite 的 TodoList”这种多步骤任务,容易跳步或遗漏 | 缺乏任务分解和工具调用编排能力 |
正因为如此,单纯的 LLM 接口(比如你直接调 ChatOpenAI)只能当“顾问”,不能当“员工”。而 Agent 的目标,就是给 LLM 装上手脚(Tool)、记忆(Memory)、知识库(RAG)和规划能力(Planning),让它变成一个能闭环执行任务的智能体。
二、Agent 核心组件拆解
对照笔记中的公式:
Agent = LLM + Memory + Tool + RAG + MCP + Skills
我们逐个解释,并思考它们如何协同工作。
2.1 Memory(记忆模块)
- 作用:让 Agent 记住历史对话、用户偏好、任务上下文。
- 实现:可以用数据库、Redis 或前端存储(如 localStorage)持久化消息历史。在 LangChain 中,
BaseChatMessageHistory和Memory类提供了标准抽象。 - 思考:记忆不仅仅是“存消息”,还要考虑总结压缩(当历史太长时,用 LLM 做摘要)、检索性记忆(只取相关片段)等,这是长期记忆和短期记忆的工程化落地。
2.2 Tool(工具调用)
- 作用:让 LLM 能够执行现实世界操作,比如读写文件、调用 API、执行命令行、发送邮件。
- 关键机制:LLM 本身不直接执行工具,而是生成结构化的工具调用请求(如
tool_calls字段),由宿主程序(你的代码)来执行,再将结果返回给 LLM。 - 笔记中强调了:工具要有详细描述(description)和参数 schema(使用 Zod 等库约束),这样 LLM 才知道什么时候该用、该传什么参数。
2.3 RAG(检索增强生成)
- 作用:让 Agent 能访问外部知识库(如公司文档、数据库、网页),解决“不知道”的问题。
- 流程:用户提问 → 向量检索(Embedding)→ 相关文档片段 → 拼入 prompt 上下文 → LLM 生成答案。
- 注意:RAG 和 Tool 可以结合——Agent 可以决定先调用“检索工具”去查知识库,再基于返回结果回答。
2.4 MCP(Model Context Protocol)
- 一个新兴的标准化协议,让 Agent 可以无缝调用第三方工具(如做 PPT、分析股市),类似“工具市场的统一接口”。笔记中把它单独列出,说明它未来会成为 Agent 生态的重要桥梁。
2.5 Skills(技能蒸馏)
- 将常见复杂任务固化为可复用的“技能”,比如“写周报”、“生成代码项目”。本质是把一系列工具调用和 prompt 模板打包,让 Agent 一触即发。
三、Agent 的工作流程(一次完整的任务执行)
笔记中给出了一个清晰的宏观流程:
User Prompt(复杂任务)
↓
LLM Planning / Reasoning
↓
是否需要加载 Memory? → 是 → 获取历史上下文
↓
是否需要调用 Tool? → 是 → 分步骤调用多个工具(可能并发)
↓
是否需要 RAG? → 是 → 查询知识库,拼入 prompt
↓
生成最终 Response
↓
返回 User
这个过程不是线性的,而是一个循环——Agent 可能先调用一个工具得到结果,再根据结果决定下一步调用什么工具,直到任务完成。这种“思考-行动-观察”的循环(ReAct 模式)是 Agent 的核心。
我们可以用 Mermaid 画出更详细的流程图:
注意,图中存在循环,因为一个复杂任务可能需要多次工具调用。而并发调用(如 Promise.all)是提升性能的关键,我们后面会深入。
四、LangChain:Agent 开发的“脚手架”
LangChain 是一个专门用于构建 LLM 应用的框架,它比 OpenAI 的原生 SDK 诞生更早,并且抽象了模型统一接口、工具绑定、消息管理和链式调用。对于 Node.js 生态,我们使用 @langchain/openai 和 @langchain/core。
笔记中提到了技术栈:
- NestJS:作为后端框架,提供依赖注入、模块化,适合构建生产级 Agent 服务。
- LangChain:单智能体开发框架,提供
ChatOpenAI、tool、bindTools等核心 API。 - LangGraph:多智能体开发框架,用于构建多个 Agent 协作的图工作流(本篇暂不展开)。
- MCP / RAG / Skill:作为扩展模块集成。
我们先从一个最简单的例子开始:用 LangChain 调用 DeepSeek 模型(index.mjs)。
import { ChatOpenAI } from '@langchain/openai';
import 'dotenv/config';
const model = new ChatOpenAI({
modelName: 'deepseek-v4-flash', // 兼容 OpenAI 接口的模型
apiKey: process.env.DEEPSEEK_API_KEY,
temperature: 0,
configuration: {
baseURL: 'https://api.deepseek.com/v1', // 自定义 baseURL
},
});
const response = await model.invoke('棍王杯台球比赛应该设什么奖励?');
console.log(response.content);
这段代码展示了 LangChain 的统一接口——无论你用 OpenAI、DeepSeek 还是其他兼容模型,只需要改 modelName 和 baseURL,invoke 方法都是一样的。这大大降低了切换模型的成本。
五、手写一个能读文件的 Tool(核心)
现在我们要让 LLM 真正“动手”读取文件。笔记中的 tool.mjs 是精华所在,我们逐段剖析。
5.1 定义 Tool 的两大要素
LangChain 的 tool 函数接收两个部分:
- 异步处理函数:实际的业务逻辑,比如读文件、写文件、调 API。
- 描述对象:包含
name、description、schema(参数约束)。
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
const readFileTool = tool(
async ({ filePath }) => { // ① 处理函数
const content = await fs.readFile(filePath, 'utf-8');
console.log(`[工具调用] read_file(${filePath}) 成功读取 ${content.length} 字节`);
return content;
},
{
name: 'read_file', // ② 工具元信息
description: `用此工具来读取文件内容,当用户要求读取文件、查看代码、分析文件内容时,调用此工具。`,
schema: z.object({
filePath: z.string().describe('要读取的文件路径')
})
}
);
为什么要有 schema?
因为 LLM 在决定调用工具时,需要知道应该传什么参数,参数的类型和含义。Zod 提供了运行时校验,同时 LangChain 会将其转换为 OpenAI 的 function 参数描述,让模型按照格式生成 arguments JSON。
5.2 将 Tools 绑定到 Model
const tools = [readFileTool];
const modelWithTools = model.bindTools(tools);
bindTools 会把工具的定义自动填入每次请求的 tools 参数中,这样 LLM 就知道当前有哪些可用工具。
5.3 构建对话消息
const messages = [
new SystemMessage(`
你是一个代码助手,可以使用工具读取文件并解释代码。
工作流程:
1. 用户要求读取文件时,立即调用 read_file 工具。
2. 等待工具返回文件内容。
3. 基于文件内容进行分析和解释。
`),
new HumanMessage('请读取 tool.mjs 文件内容并解释代码'),
];
系统消息(SystemMessage)用来设定 Agent 的角色和行为规则,非常重要——它告诉 LLM “什么时候该用工具”。笔记中提到了“LLM 有自知之明,当要调用 tool 的时候,不生成,停下来告诉用户”,这其实是因为模型在训练时学习了函数调用(function calling)的特殊 token,当它判定需要工具时,会输出 tool_calls 而不是普通文本。
5.4 处理 tool_calls 并获取结果
let response = await modelWithTools.invoke(messages);
messages.push(response); // 将 AI 的响应(可能包含 tool_calls)加入历史
此时 response 可能包含 tool_calls 字段,例如:
{
"tool_calls": [
{
"id": "call_abc123",
"name": "read_file",
"arguments": "{\"filePath\": \"tool.mjs\"}"
}
]
}
我们需要手动执行这些工具(LangChain 不会自动执行,这是设计上的自由度),然后将结果以 ToolMessage 的形式追加到消息列表,再调用一次模型,让它基于工具结果生成最终回答。
笔记中还强调了一个细节:多个工具调用可能并发执行。比如用户说“读取 a.js 和 b.js 并比较”,Agent 可能会同时发起两个 read_file 调用,这时我们可以用 Promise.all 并行执行,大幅减少总耗时。这就是我们接下来要重点优化的性能点。
六、深入并发:Promise.all 与 Agent 性能
在 1.html 中,我们看到了两个简单的异步函数 getWeather() 和 getTweets(),分别模拟了 2000ms 和 500ms 的延迟。
串行执行(错误示范):
const weatherData = await getWeather(); // 等待 2000ms
const tweetsData = await getTweets(); // 再等 500ms
// 总耗时 2500ms
两个任务互不依赖,却串行等待,浪费了并行机会。
并行执行(正确姿势):
const [weatherData, tweetsData] = await Promise.all([
getWeather(),
getTweets()
]);
// 总耗时 ≈ max(2000, 500) = 2000ms
Promise.all 接收一个 Promise 数组,并发启动所有任务,并等待全部 resolve 后返回结果数组,顺序与输入一致。如果其中任何一个 reject,则整体 reject(可以用 allSettled 来容错)。
在 Agent 场景中,假设用户要求“读取 file1、file2、file3 并合并内容”,如果串行读,总耗时是三个文件读取时间之和;而并行读,总耗时约等于最大的那个文件读取时间。对于 I/O 密集型任务(文件、网络请求),这种优化是数量级的。
但要注意:并非所有工具调用都能并行。如果第二个工具依赖第一个工具的输出,就必须串行。Agent 的规划层(LLM)需要生成正确的调用顺序,而我们的执行层则需要根据依赖关系决定是否并发。LangGraph 等框架提供了更精细的控制,但即便在简单场景中,我们也可以利用 Promise.all 批量执行无依赖的工具。
在 tool.mjs 的结尾,注释写着“多个工具 await read await write 并发?”,这正是指出了下一步优化方向——在我们的执行循环中,可以收集所有待执行的 tool_calls,区分出可并行的批次,用 Promise.all 一次性发出。
七、从 Demo 到真实 Agent:规划与执行
笔记中的 readme2.md 提到了一个实际场景:
创建一个 React + Vite 的 TodoList
要完成这个任务,Agent 需要:
- 调用
write_file工具,生成package.json、vite.config.js、App.jsx等文件。 - 调用
execute_command工具,运行npm install和npm run dev。
这不再是单一工具调用,而是多步骤规划。LLM 需要先写出所有文件,再执行安装命令。每一步都可能产生新的文件,而下一步命令需要这些文件存在。
我们可以把这种“计划-执行-观察”循环封装成一个通用的 Agent 循环:
async function runAgent(userMessage) {
const messages = [new HumanMessage(userMessage)];
let maxIterations = 10;
while (maxIterations-- > 0) {
const response = await modelWithTools.invoke(messages);
messages.push(response);
if (!response.tool_calls || response.tool_calls.length === 0) {
// 没有工具调用,直接返回最终回答
return response.content;
}
// 并行执行所有工具(注意依赖问题,此处简化)
const toolResults = await Promise.all(
response.tool_calls.map(async (tc) => {
const tool = tools.find(t => t.name === tc.name);
const args = JSON.parse(tc.arguments);
const result = await tool.invoke(args);
return new ToolMessage({
content: result,
tool_call_id: tc.id,
});
})
);
messages.push(...toolResults);
}
return "达到最大迭代次数,任务未完成";
}
注意,这里直接用 Promise.all 并发执行所有 tool_calls,前提是它们之间没有依赖。如果有依赖,需要更复杂的拓扑排序,或者交由 LangGraph 处理。
八、总结与思考
8.1 Agent 本质上是什么?
Agent 就是给 LLM 装上“手脚”和“记忆”,让它从“嘴炮”变成“实干家”。它的核心不在于 LLM 本身有多强,而在于工程化地组织记忆、工具、知识库和规划流程。笔记中那句“Agent 其实也不复杂”是真的——当你理解了 ReAct 模式,剩下的就是编码落地。
8.2 LangChain 带来了什么?
- 统一接口:屏蔽不同模型提供商的差异。
- 工具抽象:标准化的
tool和bindTools,让 LLM 知道能干啥。 - 消息管理:
SystemMessage、HumanMessage、AIMessage、ToolMessage让对话状态清晰。 - 生态丰富:内置 RAG、Memory、Chain 等模块,减少重复造轮子。
8.3 性能是 Agent 落地的生命线
用户不愿等一个 Agent 思考 30 秒才回复。善用 Promise.all 并行 I/O,将串行等待转为并发,可以极大提升用户体验。同时,要设计好反馈机制(如流式输出工具调用状态),让用户感知到进度。
8.4 未来展望
- MCP 会降低第三方工具集成的门槛,Agent 可以即插即用各种 SaaS 能力。
- Skills 让高频任务模板化,减少重复规划开销。
- 多智能体协作(LangGraph)将分解超级复杂任务,各司其职。
最后,回到笔记开头的那个比喻:Agent 就是给 LLM 加上 Memory、Tool、RAG、MCP、Skills 后的增强版大脑。现在你已经掌握了从理论到代码的全链路,不妨动手写一个自己的 Agent,从“读文件解释代码”开始,逐步扩展执行命令、联网搜索、操作数据库……让 AI 真正为你干活。
本文所有代码示例均基于 Node.js + LangChain + DeepSeek 模型,可本地运行。如果你对 LangGraph、RAG 实现或 Skill 蒸馏感兴趣,欢迎在评论区留言,我们下期再聊。