从零理解 Agent 架构,到用 Node.js + LangChain 写出你的第一个智能体
前言
最近大家都在聊 Agent,Claude Code 能帮你写代码,Manus 能帮你自动完成复杂任务。你可能会想:这不就是调大模型接口吗,有什么神奇的?
说实话,我一开始也这么想。直到上周的课上跟着老师走了一遍,才发现——纯大模型和 Agent,根本是两个物种。
这篇文章是我整理的课堂笔记和代码实践,从 Agent 的核心概念讲起,到用 LangChain 亲手写一个能读文件、能调用工具的简易 Agent。希望能帮你少走弯路。
一、纯大模型到底差在哪?
你可能会问:GPT、DeepSeek 这么强了,为什么还要 Agent?
我们来看看纯 LLM 在实际场景中有哪些硬伤:
| 场景 | 纯 LLM 的表现 | 你真正需要的 |
|---|---|---|
| "帮我记住上周聊过的需求" | 😅 我是无状态的,记不住 | 有 Memory,跨会话持久记忆 |
| "帮我把这个网页的内容爬下来" | 🤔 你应该用 requests + BeautifulSoup... | 直接调用工具,自己动手 |
| "公司内部的接口文档怎么用?" | 🫤 我不知道你们内部的东西 | RAG 检索内部知识库 |
| "昨晚世界杯谁赢了?" | 😬 我的训练数据截止到... | 接入搜索工具,获取实时信息 |
| "帮我做一份 Q2 复盘 PPT" | 📝 我给你大纲,你手动做 | Skills 编排多步骤自动完成 |
发现了吗?大模型就像一个知识渊博但困在聊天框里的顾问——它能告诉你思路,但不能替你干活。
Agent 要做的,就是打破这个聊天框。
二、Agent 到底是什么?
一句话公式:
Agent = LLM + Memory + Tool + RAG + MCP + Skills
每个模块解决一个具体问题:
- Memory(记忆):LLM 是无状态的,每次对话都是"初次见面"。Memory 用数据库/Redis/前端存储把历史对话持久化,让 Agent 能记住你说过的话。
- Tool(工具):让 Agent 能读写文件、发 HTTP 请求、操作数据库,从"动嘴"变成"动手"。
- RAG(检索增强生成):把企业内部文档、PDF、代码库喂给 Agent,让它具备私有领域知识。
- MCP(模型上下文协议):标准化的第三方工具接入协议,像给 Agent 装"App Store"。
- Skills(技能):把多个工具调用编排成可复用的技能,比如"做 PPT"就是一个组合技能。
Agent 的工作流程
用户提出复杂任务(Prompt)
→ Agent 接收
→ LLM 规划与推理(Planning/Reasoning)
→ 需要查记忆吗?(Memory)
→ 需要动手操作吗?(Tool,可能分多步、多工具)
→ 需要查内部知识吗?(RAG → 拼入 Prompt)
→ 整合结果 → 返回用户
关键点在于:LLM 不只是生成答案,它在做规划、决策、调度。 这就是 Agent 和"调接口"的本质区别。
三、LangChain:Agent 开发的脚手架
LangChain 是目前最主流的 Agent 开发框架之一,Python 和 JS 生态都有。它的核心价值就两个:
- 统一 LLM 接口:不管底层是 OpenAI、DeepSeek 还是其他模型,一套代码全兼容。
- 标准化 Tool 机制:定义工具 → 注册到模型 → LLM 自动决策调用,全流程封装。
技术栈推荐:
Node.js (NestJS) + LangChain(单智能体) + LangGraph(多智能体编排)
3.1 三步接入大模型
import { ChatOpenAI } from '@langchain/openai';
import 'dotenv/config';
const model = new ChatOpenAI({
modelName: process.env.DEEPSEEK_MODEL,
apiKey: process.env.DEEPSEEK_API_KEY,
configuration: {
baseURL: process.env.DEEPSEEK_BASE_URL, // 兼容任意 OpenAI 格式接口
}
});
const response = await model.invoke('棍王杯台球比赛应该设置什么奖励?');
console.log(response);
@langchain/openai 不止支持 OpenAI,只要接口兼容 OpenAI 格式,换个 baseURL 就能切模型。这意味着你的代码和模型提供商解耦了——今天用 DeepSeek,明天换别的,一行配置的事。
3.2 Tool:让 LLM 长出手来
这是 Agent 最核心的能力。Tool 由两部分组成:
① 执行函数(真正干活的代码):
const readFileTool = tool(
async ({ filePath }) => {
const content = await fs.readFile(filePath, 'utf-8');
console.log(`[工具调用] read_file(${filePath}) 成功读取 ${content.length} 字节`);
return content;
},
// ...描述对象见下方
);
② 描述对象(给 LLM 看的"使用说明书"):
{
name: 'read_file',
description: `用此工具来读取文件内容。当用户要求读取文件、
查看代码、分析文件内容时,调用此工具。`,
schema: z.object({
filePath: z.string().describe('要读取的文件路径')
})
}
注意这里的设计:description 不是写给人看的注释,是写给 LLM 看的 Prompt。LLM 根据这段描述来判断"现在该不该用这个工具"。描述越清晰,LLM 的工具选择越准确。
schema 由 Zod 做参数校验——LLM 如果要调用此工具,必须按照 schema 定义的格式提供参数,否则直接报错。
3.3 注册工具并发起调用
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
const modelWithTools = model.bindTools(tools);
const messages = [
new SystemMessage(`
你是一个代码助手,可以使用工具读取文件并解释代码。
工作流程:
1. 用户要求读取文件时,立即调用 read_file 工具
2. 等待工具返回文件内容
3. 基于文件内容进行分析和解释
`),
new HumanMessage('请读取 tool.mjs 文件内容并解释代码'),
];
let response = await modelWithTools.invoke(messages);
messages.push(response); // 把 AI 回复追加到对话历史
四种消息类型构成了完整的对话上下文:
| 类型 | 角色 | 何时使用 |
|---|---|---|
SystemMessage | system | 设定 Agent 人设和行为规范 |
HumanMessage | user | 用户每次的输入 |
AIMessage | assistant | LLM 生成回复或返回 tool_calls |
ToolMessage | tool | 工具函数执行后的返回值 |
3.4 Tool 调用的内部机制
当 LLM 判断"这事我得用工具"时,它会停止生成文字,转而返回 tool_calls:
[
{
"id": "call_abc123",
"name": "read_file",
"arguments": { "filePath": "tool.mjs" }
}
]
然后你的代码执行工具,拿到结果后带着 ToolMessage 再次请求 LLM,LLM 才能基于工具结果继续生成最终回复。注意这里:一次完整的 Agent 调用,背后可能是多轮 LLM 请求。每轮都可能产生新的 tool_calls,循环直到 LLM 觉得任务完成。
四、性能优化:别让工具排队浪费时间
真实场景中,Agent 可能一次调用 3-5 个工具。如果串行执行(一个等一个),3 个 2 秒的工具总耗时就是 6 秒——用户早就关页面了。
Promise 三种状态(基础但必须懂)
Pending(等待中)
→ Fulfilled(成功)
→ Rejected(失败)
状态不可逆:一旦从 Pending 变为 Fulfilled/Rejected,就定型了。
串行 vs 并行,差距有多大?
// ❌ 串行(2500ms)
const weather = await getWeather(); // 等 2000ms
const tweets = await getTweets(); // 再等 500ms
// 总计 2500ms
// ✅ 并行(2000ms)
const [weather, tweets] = await Promise.all([
getWeather(), // ┐
getTweets() // ┤ 同时起飞
]); // ┘ 等最慢的那个
// 总计 2000ms
Promise.all 的核心规则:
- 并行启动所有 Promise
- 等全部完成才返回
- 结果顺序 = 入参数组顺序
- 适用于互不依赖的独立操作
在 Agent 场景中,如果你需要同时读 3 个文件、调 2 个 API,用 Promise.all 总耗时 = 最慢那个,而不是全部加起来。这对用户体验是质的提升。
五、实战思维:手写一个简易 Claude Code
课堂上的核心思路:Claude Code 的本质 = LLM + 文件工具 + 命令行工具。
以"创建一个 React+Vite 的 TodoList"为例,Agent 的 Planning 会拆成三步:
| 步骤 | 操作 | 使用的 Tool |
|---|---|---|
| 1 | 创建项目骨架 | CLI Tool(npm create vite) |
| 2 | 写出 TodoList 代码 | 文件写入 Tool(生成 .jsx/.css) |
| 3 | 跑起来看看效果 | CLI Tool(npm run dev) |
这三步的核心依赖就两个工具:文件系统 Tool + CLI Tool。加上 LLM 的编程能力和 Planning 能力,就是一个小型的 Coding Agent。
理解了这一点,你就知道 Agent 开发并不是什么黑魔法——本质上是把 LLM 的推理能力和工程系统的执行能力连接起来。
六、你的第一个 Agent 项目需要哪些依赖?
{
"dependencies": {
"@langchain/core": "^1.2.1",
"@langchain/openai": "^1.5.3",
"dotenv": "^17.4.2",
"zod": "^4.4.3"
}
}
四个包,各司其职:
- @langchain/core:工具定义、消息类型、链式调用等核心抽象
- @langchain/openai:统一的 LLM 调用入口(兼容所有 OpenAI 格式接口)
- dotenv:API Key 放
.env,代码和配置分离 - zod:Tool 入参校验,LLM 参数不规范直接拒掉
七、学习路线图
如果你也想入门 Agent 开发,建议按这个顺序来:
- 理解 Agent 概念:搞懂 LLM 的局限性,以及 Memory/Tool/RAG/MCP/Skills 各自解决什么问题
- 掌握 LangChain 基础:LLM 调用 → Tool 定义 → 消息类型 → bindTools 注册
- 吃透异步编程:Promise → async/await → Promise.all 并行优化(这决定了你 Agent 的性能)
- 动手写一个简易 Agent:先做一个只有 1-2 个 Tool 的小 Agent,跑起来
- 逐步加模块:加 Memory 让它记住对话 → 加 RAG 让它会查文档 → 加更多 Tool
- 进阶:LangGraph 多智能体编排、MCP 协议接入第三方工具
写在最后
Agent 没那么神秘。回顾一下公式:
Agent = LLM + Memory + Tool + RAG + MCP + Skills
它就是一个会思考、有记忆、能动手、懂内部知识、可扩展技能的大模型。每一项能力都是一个独立的模块,你不需要一次全加上——从最简单的 "LLM + 一个 Tool" 开始,逐步迭代。
那些让你觉得惊艳的 Agent 产品(Claude Code、Manus),底层逻辑逃不出这个框架。关键在于工程落地的细节:Tool 的粒度怎么切、Memory 的策略怎么选、Planning 失败怎么兜底……
希望这篇文章能帮你迈出第一步。共勉 💪