从零手写 AI Agent:LLM + Tool + LangChain,打造能干活的大模型
为什么 ChatGPT 只能聊天,而 Claude Code 能写代码、改文件、执行命令?答案就是 Agent 架构。本文从 Promise 异步基础出发,用 LangChain 框架手把手构建一个能读取文件、分析代码的 AI Agent,深入理解 Agent = LLM + Memory + Tool + RAG + MCP + Skills 的完整公式。
前言
ChatGPT 很强大,但当你让它"帮我创建一个 React + Vite 的 TodoList 项目"时,它只能给你步骤说明------它不能真的帮你创建文件、运行命令。
而 Claude Code、Cursor Agent、Manus 这些产品,本质上都是在 LLM 外面套了一层Agent 架构:给它装上 Memory(记忆)、Tool(工具)、RAG(知识检索)、MCP(协议连接)、Skills(技能蒸馏),让 LLM 从"只会说话"变成"能干活"。
本文将带你:
- 理解 Agent 的本质与组成公式
- 掌握 Promise 三种状态与
Promise.all并行执行 - 入门 LangChain 框架的 LLM 兼容与 Tool 注册
- 手写一个完整的
read_fileAgent
一、Agent 的本质:LLM 的"超进化"
1.1 LLM 的五大局限
| 局限 | 说明 | Agent 解决方案 |
|---|---|---|
| 无状态 | 每次对话结束,什么都不记得 | Memory 记忆模块 |
| 无法访问网页 | 只能告诉你思路,不能自己做 | Tool Use 工具调用 |
| 无法访问私有文档 | 预训练数据中没有你的内部资料 | RAG 检索增强生成 |
| 不知道最新消息 | 世界杯新闻、股价实时数据 | MCP 第三方工具协议 |
| 无法执行复杂任务 | 做 PPT、分析股市并自动买卖 | Skills 技能蒸馏 |
1.2 Agent 的组成公式
ini
Agent = LLM + Memory + Tool + RAG + MCP + Skills
| 组件 | 作用 | 类比 |
|---|---|---|
| LLM | 大脑:思考、规划、推理 | 人的大脑 |
| Memory | 记忆:记住对话历史、用户偏好 | 人的记忆 |
| Tool | 双手:读写文件、调用 API、执行命令 | 人的双手 |
| RAG | 知识库:查询内部文档、私有数据 | 人的专业知识 |
| MCP | 连接器:标准化接入第三方服务 | 人的社交关系 |
| Skills | 技能:做 PPT、数据分析、写代码 | 人的职业技能 |
1.3 Agent 的工作流程
markdown
用户提出复杂任务
↓
LLM Planning / Reasoning(规划/推理)
↓
要不要加载 Memory?(获取历史上下文)
↓
要不要调用 Tool?(分步骤调用多个工具)
↓
要不要查询 RAG?(检索内部知识库)
↓
生成 Response → 返回给用户
↓
任务完成
一个具体例子:"创建一个 React + Vite 的 TodoList"
arduino
步骤1:调用文件写入 Tool → 创建项目结构
步骤2:调用 LLM 编程 Tool → 生成组件代码
步骤3:调用 CLI 命令 Tool → vite create + npm install
步骤4:调用运行命令 Tool → npm run dev
二、Promise 基础:Agent 异步执行的基石
Agent 的核心是异步:调用工具、查询数据库、请求 API------这些操作都不能阻塞主线程。Promise 是理解 Agent 代码的前提。
2.1 Promise 的三种状态
javascript
const p = new Promise((resolve, reject) => {
// pending(等待中)...
if (成功) {
resolve(结果); // pending → fulfilled(成功)
} else {
reject(错误); // pending → rejected(失败)
}
});
// 状态只能变一次,且不可逆
// pending → fulfilled 或 pending → rejected
scss
┌─────────┐ resolve() ┌───────────┐
│ pending │ ──────────────→ │ fulfilled │
└─────────┘ reject() └───────────┘
──────────────→ ┌───────────┐
│ rejected │
└───────────┘
2.2 模拟异步操作
javascript
function getWeather() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ temp: 38, conditions: 'Sunny with Clouds' });
}, 2000);
});
}
function getTweets() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(['I like cake', 'BBQ is good too!']);
}, 500);
});
}
2.3 串行 vs 并行:await 的陷阱
串行执行(慢):
javascript
const main = async () => {
console.time('serial');
const weatherData = await getWeather(); // 等 2s
const tweetsData = await getTweets(); // 等 0.5s
console.log(weatherData, tweetsData);
console.timeEnd('serial'); // 约 2500ms
};
问题 :getWeather 和 getTweets 之间没有依赖关系,但串行执行白白浪费了时间。
并行执行(快):
javascript
const main = async () => {
console.time('parallel');
// 两个 Promise 同时启动,等待全部完成
const [weatherData, tweetsData] = await Promise.all([
getWeather(), // 2s
getTweets() // 0.5s
]);
console.log(weatherData, tweetsData);
console.timeEnd('parallel'); // 约 2000ms(取最慢的)
};
Promise.all 的核心价值:
| 场景 | 执行方式 | 时间 |
|---|---|---|
| 串行 await | 一个接一个 | 总和(2s + 0.5s = 2.5s) |
Promise.all |
同时启动 | 最大值(max(2s, 0.5s) = 2s) |
Agent 中经常需要同时调用多个工具(如同时查询天气和股价),
Promise.all是性能优化的关键。
2.4 Promise.all 的注意事项
javascript
// 结果顺序与 Promise 数组顺序一致
const [a, b, c] = await Promise.all([promiseA, promiseB, promiseC]);
// 如果一个失败,全部失败
await Promise.all([
Promise.resolve(1),
Promise.reject('error'), // 这里 reject
Promise.resolve(3)
]);
// 整体抛出错误,不会返回任何结果
三、LangChain:Agent 开发的瑞士军刀
3.1 为什么需要 LangChain?
| 痛点 | LangChain 解决 |
|---|---|
| LLM 厂商众多(OpenAI、DeepSeek、Claude) | 统一兼容层:一套代码切换不同模型 |
| Tool 定义格式不统一 | 标准化 Tool 注册:函数 + Schema + 描述 |
| 对话历史管理复杂 | Message 抽象:System / Human / AI / Tool Message |
| 多步骤任务编排困难 | Chain / Graph:可视化任务流 |
LangChain 比 OpenAI SDK 更早诞生,它的设计哲学是**"模型无关"**------你今天用 DeepSeek,明天可以无缝切换到 Claude,代码几乎不用改。
3.2 LangChain 的核心模块
bash
LangChain 架构
├── @langchain/core
│ ├── messages ← SystemMessage, HumanMessage, AIMessage, ToolMessage
│ ├── tools ← tool() 函数注册工具
│ └── prompts ← PromptTemplate
├── @langchain/openai ← ChatOpenAI(兼容 OpenAI 格式)
├── @langchain/anthropic
├── @langchain/deepseek
└── langgraph ← 多智能体编排
四、实战:手写一个 read_file Agent
4.1 项目依赖
json
{
"type": "module",
"dependencies": {
"@langchain/openai": "latest",
"@langchain/core": "latest",
"zod": "latest",
"dotenv": "latest"
}
}
4.2 完整代码
tool.mjs:
javascript
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { tool } from '@langchain/core/tools';
import {
HumanMessage, // user 角色
SystemMessage, // system 角色
ToolMessage, // tool 返回结果
AIMessage // AI 回复
} from '@langchain/core/messages';
import fs from 'node:fs/promises';
import { z } from 'zod';
// 1. 创建 LLM 实例(兼容 DeepSeek API)
const model = new ChatOpenAI({
modelName: 'deepseek-v4-flash',
apiKey: process.env.DEEPSEEK_API_KEY,
temperature: 0, // 确定性输出,适合工具调用
configuration: {
baseURL: 'https://api.deepseek.com/v1',
}
});
// 2. 定义 read_file 工具
const readFileTool = tool(
// 第一部分:异步处理函数
async ({ filePath }) => {
const content = await fs.readFile(filePath, 'utf-8');
// 时刻反馈 Agent 执行消息
console.log(`[工具调用] read_file(${filePath}) 成功读取 ${content.length} 字节`);
return content;
},
// 第二部分:描述对象(LLM 靠这个来决定是否调用)
{
name: 'read_file',
description: `用此工具来读取文件内容,当用户要求读取文件、查看代码、分析文件内容时,调用此工具。输入文件路径(可以是相对路径或绝对路径)`,
schema: z.object({ // 注意:schema 是小写 s
filePath: z.string().describe('要读取的文件路径')
})
}
);
// 3. 工具列表
const tools = [readFileTool];
// 4. 将工具绑定到模型
const modelWithTools = model.bindTools(tools);
// 5. 构建消息上下文
const messages = [
new SystemMessage(`
你是一个代码助手,可以使用工具读取文件并解释代码。
工作流程:
1. 用户要求读取文件时,立即调用 read_file 工具
2. 等待工具返回文件内容
3. 基于文件内容进行分析和解释
可用工具:
- read_file:读取文件内容(使用此工具来获取文件内容)
`),
new HumanMessage('请读取 tool.mjs 文件内容并解释代码')
];
// 6. 调用模型(LLM 会分析并决定调用工具)
let response = await modelWithTools.invoke(messages);
messages.push(response);
// response 中可能包含 tool_calls → 需要执行工具 → 将结果再次传入模型
4.3 代码逐层解析
第一步:LLM 实例化
javascript
const model = new ChatOpenAI({
modelName: 'deepseek-v4-flash',
apiKey: process.env.DEEPSEEK_API_KEY,
temperature: 0,
configuration: { baseURL: 'https://api.deepseek.com/v1' }
});
ChatOpenAI 是 LangChain 的兼容层 。虽然名字叫 OpenAI,但通过配置 baseURL 可以接入任何兼容 OpenAI 格式的 API(DeepSeek、通义千问、本地模型等)。
temperature: 0 表示确定性输出------每次相同输入得到相同输出,这对工具调用场景非常重要。
第二步:Tool 的定义(双结构)
LangChain 的 tool() 函数要求传入两个参数:
| 参数 | 类型 | 作用 |
|---|---|---|
| 参数1 | 异步函数 | 执行逻辑:接收校验后的参数,执行操作,返回结果 |
| 参数2 | 描述对象 | 元信息:name、description、schema,供 LLM 决策使用 |
javascript
tool(
async ({ filePath }) => { ... }, // ← 执行逻辑
{ // ← 元信息
name: 'read_file',
description: '...',
schema: z.object({ filePath: z.string() })
}
);
为什么需要 description?
LLM 不是靠函数名来决策的------它根本看不到你的代码。它靠的是 description 中的文字描述来判断"当前这个任务,应不应该调用这个工具"。
第三步:Schema 校验
javascript
schema: z.object({
filePath: z.string().describe('要读取的文件路径')
})
Zod Schema 有两个作用:
- 运行时校验:确保 LLM 传来的参数类型正确
- 自动生成 JSON Schema:LangChain 内部将 Zod Schema 转换为 JSON Schema 发给 LLM,LLM 据此生成正确的参数
⚠️ 重要提醒 :schema 的键名是小写 s 。如果写成 Schema(大写),LangChain 可能无法正确识别,导致参数校验失败。
第四步:绑定工具
javascript
const modelWithTools = model.bindTools(tools);
bindTools 将工具列表注册到模型上。之后调用 modelWithTools.invoke() 时,LLM 会知道"我有这些工具可用"。
第五步:Message 类型
javascript
const messages = [
new SystemMessage('系统角色设定...'), // 设定 Agent 的身份和能力
new HumanMessage('用户的提问...') // 用户的实际需求
];
LangChain 定义了四种核心消息类型:
| 消息类型 | 角色 | 用途 |
|---|---|---|
SystemMessage |
system | 设定 Agent 的身份、能力、工作流程 |
HumanMessage |
user | 用户的输入/提问 |
AIMessage |
assistant | AI 的回复(可能包含 tool_calls) |
ToolMessage |
tool | 工具执行后的返回结果 |
第六步:工具调用的完整循环
javascript
// 第一轮:LLM 分析 → 决定调用 read_file
let response = await modelWithTools.invoke(messages);
// response 包含:content(文字)+ tool_calls(工具调用请求)
messages.push(response); // 将 AI 的回复加入历史
// 如果 response 包含 tool_calls:
// 1. 提取 tool_calls[0].function.name = "read_file"
// 2. 提取 arguments = { "filePath": "tool.mjs" }
// 3. 执行 readFileTool({ filePath: "tool.mjs" })
// 4. 将结果包装为 ToolMessage,加入 messages
// 5. 再次调用 modelWithTools.invoke(messages)
// 6. LLM 基于文件内容生成最终回答
五、用户体验优化:给 Agent 加上"进度条"
Agent 执行任务可能很耗时(读取大文件、调用多个工具),用户如果太久没收到反馈,可能会认为程序卡死了。
javascript
const readFileTool = tool(
async ({ filePath }) => {
const content = await fs.readFile(filePath, 'utf-8');
// ✅ 时刻反馈 Agent 执行消息
console.log(`[工具调用] read_file(${filePath}) 成功读取 ${content.length} 字节`);
return content;
},
{ name: 'read_file', description: '...', schema: z.object({...}) }
);
在工具函数中添加 console.log,让用户看到 Agent 正在执行什么操作------这是 Agent 产品化的关键细节。
六、从单工具到多工具:扩展 Agent 能力
当前 Agent 只有一个 read_file 工具。要让它更像 Claude Code,可以扩展更多工具:
javascript
// 写文件工具
const writeFileTool = tool(
async ({ filePath, content }) => {
await fs.writeFile(filePath, content, 'utf-8');
console.log(`[工具调用] write_file(${filePath}) 写入成功`);
return `文件 ${filePath} 写入成功`;
},
{
name: 'write_file',
description: '写入文件内容',
schema: z.object({
filePath: z.string().describe('文件路径'),
content: z.string().describe('文件内容')
})
}
);
// 执行命令工具
const execCommandTool = tool(
async ({ command }) => {
const { execSync } = await import('child_process');
const result = execSync(command, { encoding: 'utf-8' });
console.log(`[工具调用] exec(${command}) 执行成功`);
return result;
},
{
name: 'exec_command',
description: '执行 CLI 命令(如 npm install、git commit)',
schema: z.object({
command: z.string().describe('要执行的命令')
})
}
);
const tools = [readFileTool, writeFileTool, execCommandTool];
const modelWithTools = model.bindTools(tools);
有了这三个工具,Agent 就能完成"创建项目 → 写代码 → 运行项目"的完整工作流。
知识树
javascript
从零手写 AI Agent
├── Agent 本质
│ ├── LLM 五大局限(无状态/无手/无知识/无时效/无技能)
│ ├── Agent 公式:LLM + Memory + Tool + RAG + MCP + Skills
│ └── 工作流程:Planning → Memory → Tool → RAG → Response
├── Promise 基础
│ ├── 三种状态:pending → fulfilled / rejected
│ ├── await:异步变同步
│ └── Promise.all:并行执行(取最慢的时间)
├── LangChain 框架
│ ├── ChatOpenAI:统一兼容层(DeepSeek/Claude/OpenAI)
│ ├── tool():双结构(执行函数 + 描述对象)
│ ├── bindTools():将工具注册到模型
│ └── Message 类型:System / Human / AI / Tool
├── 实战:read_file Agent
│ ├── LLM 实例化(temperature: 0)
│ ├── Tool 定义(name + description + schema)
│ ├── SystemMessage 设定工作流程
│ └── 工具调用循环(invoke → tool_calls → ToolMessage → 再 invoke)
├── 扩展方向
│ ├── write_file(写文件)
│ ├── exec_command(执行 CLI)
│ └── Promise.all 并行调用多个工具
└── 审查纠正
├── 代码截断补全
├── schema 键名大小写
└── 并发工具优化
结语
Agent 的本质,是给 LLM 装上"记忆"和"双手",让它从"会说话"变成"能干活"。
LangChain 的价值在于抽象和统一 :不管底层是 DeepSeek、Claude 还是 GPT,你都用同样的 ChatOpenAI 接口;不管工具是读文件、查数据库还是发邮件,你都用同样的 tool() 函数注册。
理解了这个模式,你就具备了构建真正 AI 产品的能力:
- 分析 LLM 的局限 → 确定需要哪些组件
- 用 LangChain 注册工具 → 给 LLM 装上"手"
- 用 Memory 管理对话 → 给 LLM 装上"记忆"
- 用 RAG 接入知识库 → 给 LLM 装上"专业知识"
- 用 MCP 连接外部服务 → 给 LLM 装上"社交关系"
Agent 开发的核心不是写更复杂的代码,而是设计更好的系统。LLM 已经会思考了,你的工作是让它的思考能够落地。
参考与拓展阅读:
- LangChain 官方文档(langchain.com)
- Zod 官方文档(zod.dev)
- 《MCP 协议深度解析》------ Agent 工具层的标准化协议
- 《LLM Tool Calling 实战》------ 底层 Function Calling 原理
- 《Harness Engineering 深度解析》------ Agent 工程化的系统架构
- 《Promise 与异步编程》------ JS 异步机制完全指南
如果本文帮你理解了 Agent 的构建逻辑和 LangChain 的实战用法,欢迎点赞 + 收藏。动手写一个你自己的 Agent,欢迎在评论区分享你的创意 👇
#AIAgent #LangChain #Promise #ToolCalling #Node.js #掘金技术社区