开篇:一个经常被忽略的真相
你可能已经用过大模型聊天,也感受过它在文案、翻译、代码解释上的能力。但如果把问题换成下面这些,它通常就不太行了:
- 它还记得你们上周聊过什么吗?
- 它能主动打开网页抓取内容吗?
- 它能访问你公司内部的私有文档吗?
- 它知道今天刚发生的新闻吗?
- 它能自动分析数据、生成 PPT,甚至帮你下单买卖吗?
大多数时候,答案都是否定的。
原因并不复杂:大模型本质上只是一个无状态的文本生成器。每次你发来一条消息,它都会基于当前输入重新生成回答,而不是像人一样持续记住上下文、调用工具、查阅资料、完成动作。
这也是为什么,单独一个 LLM 很聪明,却很难真正"干活"。
这篇文章想讲清楚一件事:怎么给 LLM 装上记忆、工具、知识和执行能力,把它升级成一个真正能工作的 Agent 智能体。
为什么不能直接调大模型接口
先看几个最常见的问题:
| 问题 | 根本原因 |
|---|---|
| 不记得上周聊过什么 | LLM 是无状态的,每轮对话都像"重新开始" |
| 只能给建议,不能执行 | 它本质是文本生成器,不能直接操作网页、文件、系统 |
| 不知道公司内部文档 | 训练数据里没有你的私有知识 |
| 不知道最新新闻 | 模型知识存在时间截止 |
| 不能做 PPT、炒股、下单 | 缺少工具系统和执行能力 |
一句话总结:裸 LLM 擅长生成答案,但不擅长完成任务。
而 Agent 的出现,就是为了解决这个问题。
Agent 到底是什么
如果把 LLM 比作大脑,那一个完整的 Agent 至少还需要补上几类能力:
Agent = LLM + Memory + Tool + RAG + MCP + Skill
每一部分都在补 LLM 的一个硬伤:

- LLM:负责理解任务、规划步骤、做推理决策。
- Memory:负责保存历史对话、长期偏好和任务上下文。
- Tool:负责把"想法"变成"动作",比如读文件、调 API、执行命令。
- RAG:负责补充模型训练数据之外的知识,尤其是你的私有文档和业务资料。
- MCP:负责标准化接入第三方服务,比如地图、网盘、数据库、企业系统。
- Skill:负责沉淀某一类可复用能力,比如做 PPT、分析股票、代码审查等。
所以,Agent 并不是一个更大的模型,而是一个带外部能力系统的 LLM 运行框架。
Agent 和 Chatbot 的本质区别
很多人第一次接触 Agent 时,会把它理解成"更高级的聊天机器人"。但两者其实不是一回事。
Chatbot 的典型模式是:
用户提问 → 模型直接回答
而 Agent 的典型模式是:
用户提任务 → 模型规划 → 判断是否调用工具 / 检索知识 / 读取记忆 → 执行动作 → 观察结果 → 再推理 → 输出最终结果
也就是说,Agent 不是一次性作答,而是在一个循环里持续完成任务。
Agent 的工作流程
把这个流程展开,大致是下面这样:

这个流程里有几个特别关键的判断点:
- 当前任务要不要加载历史记忆?
- 当前问题要不要查知识库?
- 当前目标能不能靠工具直接执行?
- 如果工具执行失败,要不要换一种方案?
这就是 Agent 真正有价值的地方:它不是在"回答问题",而是在"完成任务"。
一套实用的技术栈
如果你想从零开始搭一个 Agent,下面这套组合非常适合入门和工程化落地:
nestjs 后端框架,适合企业级项目和模块化架构 langchain 单 Agent 开发框架,统一模型与工具抽象 langgraph 多 Agent 协作和流程编排框架 MCP 标准化工具协议,用来接第三方服务
这里顺手说一个很容易被忽略的点:LangChain 的价值,不只是"封装大模型调用",而是它提供了一套统一抽象。
同样一段业务代码,你可以:
- 用 OpenAI 模型跑
- 换成 DeepSeek 跑
- 换成阿里云兼容接口继续跑
很多时候,真的只需要换 baseURL 和 API Key。
这也是为什么在 Agent 场景里,LangChain 依然很有价值。
实战:搭建第一个 Tool Agent
接下来进入最关键的部分:让 LLM 真正学会调用工具。
项目结构可以先从一个很小的 Demo 开始:
hello-langchain/ ├── package.json ├── .env ├── index.mjs ├── tool.mjs └── 1.html
其中:
- index.mjs:最基础的 LLM 调用
- tool.mjs:让 LLM 能调用工具读文件
- 1.html:演示 Promise 并行优化
第一步:最小 LLM 调用
index.mjs 的核心代码如下:
arduino
import { ChatOpenAI } from "@langchain/openai";
import "dotenv/config";
const model = new ChatOpenAI({
modelName: "deepseek-v4-flash",
apiKey: process.env.DEEPSEEK_API_KEY,
configuration: {
baseURL: "https://api.deepseek.com/v1",
},
});
// client.chat.completions.create
const res = await model.invoke("棍王杯台球比赛应该有什么奖励");
console.log(res.content);
这段代码虽然简单,但已经体现了 LangChain 的核心抽象:
- ChatOpenAI 是统一模型接口
- model.invoke() 是统一调用方式
- 不同模型厂商只需要换配置,不需要重写业务逻辑
从使用体验上看,它和直接调用模型 SDK 很像;但从可扩展性看,它更适合后面继续接 Tool、Memory、RAG。
第二步:让 LLM 学会调用 Tool
真正让 Agent "活起来"的,是工具系统。
read_file 工具的核心实现:
javascript
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("要读取的文件路径名"), }), } );
这段代码可以拆成两层理解:
1. 真正执行任务的是处理函数
第一部分是一个异步函数:
dart
async ({ filePath }) => {
const content = await fs.readFile(filePath, "utf-8");
return { content };
}
它才是真正"干活"的部分。这里的动作非常明确:读取文件内容并返回。
2. 决定模型会不会调用它的是工具描述
第二部分是工具元信息:
- name:工具名
- description:告诉模型什么时候该用它
- schema:告诉模型参数应该怎么传
这部分非常关键,因为 LLM 并不是靠"猜"来调用工具,而是靠读取工具描述做决策。
如果 description 写得太模糊,模型就很可能不会调,或者调错。
如果 schema 不清楚,模型就不知道参数该怎么构造。
所以在 Agent 开发里,一个常见经验是:
Tool 的描述质量,往往直接决定 Tool Agent 的调用效果。
3. 执行过程要有反馈
你在工具内部加的这段日志非常好:
javascript
console.log(`[工具调用] read_file (${filePath}) ...`)
这是很多初学者会忽略的细节。
现实里的 Agent 任务通常不是一两秒结束的。它可能要:
- 连续调用多个工具
- 读多个文件
- 检索知识库
- 多轮推理
如果整个过程中没有反馈,用户很容易以为它卡死了。
所以,一个可用的 Agent,不只是"能调用工具",还要让执行过程可感知、可观察。
4. 把工具绑定给模型
接下来是这一步:
ini
const tools = [readFileTool];
const modelWithTools = model.bindTools(tools);
这可以理解成:告诉模型,你现在不仅能聊天,还拥有了这些额外能力。
绑定之后,模型在推理时就会多一个选择:
- 直接回答
- 或者先请求调用某个工具
5. 用消息历史驱动完整对话
你的消息上下文写法也很标准:
go
const messages = [
new SystemMessage(`
你是一个代码助手,可以使用工具读取文件并解释代码
工作流程:
1. 用户要求读取文件时,立即调用 read_file 工具
2. 等待工具返回文件内容
3. 基于文件内容进行分析和解释
`),
new HumanMessage("请读取文件 index.js 并解释代码"),
];
这里的重点不是"SystemMessage 要写得多长",而是要明确三件事:
- 模型现在扮演什么角色
- 它应该遵循什么流程
- 它有哪些可以调用的能力
有了这些信息,模型才更像一个稳定执行任务的 Agent,而不是一个随机发挥的聊天模型。
6. Tool Agent 的运行本质
当你执行下面这句时:
ini
let response = await modelWithTools.invoke(messages);
messages.push(response);
模型不会立刻产出最终答案。它通常会先返回一个 tool_calls 结构,里面包含:
- 要调用哪个工具
- 这个工具的参数是什么
- 调用 ID 是多少
也就是说,它会先说:
"这个问题我需要先调用 read_file,等拿到结果再继续。"
这个过程可以用一张图看得更清楚:

这就是 Tool Agent 和普通聊天的本质区别:
- 普通聊天:模型直接输出文本
- Tool Agent:模型先判断要不要行动,再根据行动结果继续输出
一个很容易被低估的问题:性能
当 Agent 开始变复杂之后,性能问题会很快暴露出来。
比如一个任务可能同时需要:
- 查天气
- 查社交平台数据
- 读文件
- 调数据库
如果这些请求一个一个串行执行,体验会非常差。
你在笔记里单独整理 Promise 优化,其实抓得很准,因为 Tool 调用性能往往是 Agent 落地时第一批真正的瓶颈。
Promise 三状态,为什么重要
Promise 只有三种状态:
pending → fulfilled
pending → rejected
一旦从 pending 变成成功或失败,就不会再逆转。
这个机制决定了,我们可以把多个互不依赖的异步任务一起发出去,最后统一等待结果。
串行和并行的差距
你在 1.html 里演示的例子很典型。
如果串行执行:
csharp
const weatherData = await getWeather(); // 2000ms
const tweetsData = await getTweets(); // 再等 500ms
总耗时大约是 2500ms。
如果并行执行:
scss
const [weatherData, tweetsData] =
await Promise.all([getWeather(), getTweets()]);
总耗时就会降到大约 2000ms,由最慢的那个任务决定。
可视化之后非常直观:

所以这里可以提炼出一个很实用的经验:
只要多个 Tool 之间没有依赖关系,就应该尽量并发执行,而不是串行等待。
这在 Agent 场景里非常重要,因为一次复杂任务往往会触发很多独立的外部调用。
如果继续往前走,可以做到什么程度
你在笔记里提到一个很有意思的目标:
手写一个简单版的 Claude Code Agent:LLM + Tool(fs + cli)
这个方向完全成立,而且思路已经很清晰了。
比如用户说:
创建一个 React + Vite 的 TodoList
一个编程型 Agent 完整可以拆成下面几步:
objectivec
第一步:用 CLI Tool 创建项目
第二步:用 File Tool 写入代码
第三步:用 CLI Tool 运行项目
第四步:根据运行结果继续修复和调整
换句话说,只要你把 Tool 能力从"读文件"扩展到:
- 读文件
- 写文件
- 执行命令
再加上一层循环控制,实际上就已经具备了一个简化版代码 Agent 的雏形。
最后,用一张图把 Agent 看透
回到最开始的问题:Agent 到底神秘吗?
其实并不神秘。
你完全可以把它理解成这样一个系统:
- LLM 负责思考
- Tool 负责行动
- Memory 负责记住
- RAG 负责查资料
- MCP / Skill 负责连接外部世界和复用能力
于是,一个原本只会生成文本的大模型,就被升级成了一个能在"思考 → 行动 → 观察 → 再思考"循环里持续完成任务的智能体。
这也是我对 Agent 最喜欢的一种理解:
Agent 不是什么玄学,它只是一个装上了手脚、记忆和知识系统的 LLM。