LangChain 打造第一个 Agent:LLM 大脑与 Tool 手脚的完整闭环
会聊天的 AI 到处都是,能干活的 AI 才值钱。Agent(智能体)就是让大模型从"只能嘴上说说"变成"真的能动手做事"的关键架构。本文从一个最简 Agent 的定义出发,用 LangChain 框架一步步实现 LLM + Tool 的完整闭环------从理解"为什么要做 Agent"到写出几十行能跑通的 Agent 代码。
目录
- 一、直接调大模型接口,有什么问题
- [1.1 LLM 的五个天生短板](#1.1 LLM 的五个天生短板 "#11-llm-%E7%9A%84%E4%BA%94%E4%B8%AA%E5%A4%A9%E7%94%9F%E7%9F%AD%E6%9D%BF")
- [1.2 Agent 就是给 LLM 打补丁](#1.2 Agent 就是给 LLM 打补丁 "#12-agent-%E5%B0%B1%E6%98%AF%E7%BB%99-llm-%E6%89%93%E8%A1%A5%E4%B8%81")
- [二、Agent 的能力公式和工作流程](#二、Agent 的能力公式和工作流程 "#%E4%BA%8Cagent-%E7%9A%84%E8%83%BD%E5%8A%9B%E5%85%AC%E5%BC%8F%E5%92%8C%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B")
- [2.1 公式:Agent = LLM + Memory + Tool + RAG + MCP + Skills](#2.1 公式:Agent = LLM + Memory + Tool + RAG + MCP + Skills "#21-%E5%85%AC%E5%BC%8Fagent--llm--memory--tool--rag--mcp--skills")
- [2.2 Agent 的执行流程](#2.2 Agent 的执行流程 "#22-agent-%E7%9A%84%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B")
- [2.3 你已经见过的 Agent 产品](#2.3 你已经见过的 Agent 产品 "#23-%E4%BD%A0%E5%B7%B2%E7%BB%8F%E8%A7%81%E8%BF%87%E7%9A%84-agent-%E4%BA%A7%E5%93%81")
- [三、LangChain:为 Agent 而生的开发框架](#三、LangChain:为 Agent 而生的开发框架 "#%E4%B8%89langchain%E4%B8%BA-agent-%E8%80%8C%E7%94%9F%E7%9A%84%E5%BC%80%E5%8F%91%E6%A1%86%E6%9E%B6")
- [3.1 比 OpenAI 还早的 LLM 框架](#3.1 比 OpenAI 还早的 LLM 框架 "#31-%E6%AF%94-openai-%E8%BF%98%E6%97%A9%E7%9A%84-llm-%E6%A1%86%E6%9E%B6")
- [3.2 LangChain 的模块分工](#3.2 LangChain 的模块分工 "#32-langchain-%E7%9A%84%E6%A8%A1%E5%9D%97%E5%88%86%E5%B7%A5")
- [3.3 用 LangChain 连接 DeepSeek](#3.3 用 LangChain 连接 DeepSeek "#33-%E7%94%A8-langchain-%E8%BF%9E%E6%8E%A5-deepseek")
- [四、Tool:让 LLM 动手的能力](#四、Tool:让 LLM 动手的能力 "#%E5%9B%9Btool%E8%AE%A9-llm-%E5%8A%A8%E6%89%8B%E7%9A%84%E8%83%BD%E5%8A%9B")
- [4.1 Tool 的两个核心部分](#4.1 Tool 的两个核心部分 "#41-tool-%E7%9A%84%E4%B8%A4%E4%B8%AA%E6%A0%B8%E5%BF%83%E9%83%A8%E5%88%86")
- [4.2 手写一个读文件工具](#4.2 手写一个读文件工具 "#42-%E6%89%8B%E5%86%99%E4%B8%80%E4%B8%AA%E8%AF%BB%E6%96%87%E4%BB%B6%E5%B7%A5%E5%85%B7")
- [4.3 description 和 schema:LLM 理解工具的唯一渠道](#4.3 description 和 schema:LLM 理解工具的唯一渠道 "#43-description-%E5%92%8C-schemallm-%E7%90%86%E8%A7%A3%E5%B7%A5%E5%85%B7%E7%9A%84%E5%94%AF%E4%B8%80%E6%B8%A0%E9%81%93")
- [4.4 把工具"绑"到模型上](#4.4 把工具"绑"到模型上 "#44-%E6%8A%8A%E5%B7%A5%E5%85%B7%E7%BB%91%E5%88%B0%E6%A8%A1%E5%9E%8B%E4%B8%8A")
- [五、Messages 体系:Agent 的记忆载体](#五、Messages 体系:Agent 的记忆载体 "#%E4%BA%94messages-%E4%BD%93%E7%B3%BBagent-%E7%9A%84%E8%AE%B0%E5%BF%86%E8%BD%BD%E4%BD%93")
- [5.1 四种消息角色](#5.1 四种消息角色 "#51-%E5%9B%9B%E7%A7%8D%E6%B6%88%E6%81%AF%E8%A7%92%E8%89%B2")
- [5.2 SystemMessage:给 Agent 定规矩](#5.2 SystemMessage:给 Agent 定规矩 "#52-systemmessage%E7%BB%99-agent-%E5%AE%9A%E8%A7%84%E7%9F%A9")
- [5.3 消息流转的完整过程](#5.3 消息流转的完整过程 "#53-%E6%B6%88%E6%81%AF%E6%B5%81%E8%BD%AC%E7%9A%84%E5%AE%8C%E6%95%B4%E8%BF%87%E7%A8%8B")
- [六、高性能 Tool 调用:Promise.all 并行执行](#六、高性能 Tool 调用:Promise.all 并行执行 "#%E5%85%AD%E9%AB%98%E6%80%A7%E8%83%BD-tool-%E8%B0%83%E7%94%A8promiseall-%E5%B9%B6%E8%A1%8C%E6%89%A7%E8%A1%8C")
- [6.1 串行 vs 并行:2500ms vs 2000ms 的差距](#6.1 串行 vs 并行:2500ms vs 2000ms 的差距 "#61-%E4%B8%B2%E8%A1%8C-vs-%E5%B9%B6%E8%A1%8C2500ms-vs-2000ms-%E7%9A%84%E5%B7%AE%E8%B7%9D")
- [6.2 Promise 三态与 async/await](#6.2 Promise 三态与 async/await "#62-promise-%E4%B8%89%E6%80%81%E4%B8%8E-asyncawait")
- [6.3 Promise.all 的工程意义](#6.3 Promise.all 的工程意义 "#63-promiseall-%E7%9A%84%E5%B7%A5%E7%A8%8B%E6%84%8F%E4%B9%89")
- [七、完整 Agent 代码复盘](#七、完整 Agent 代码复盘 "#%E4%B8%83%E5%AE%8C%E6%95%B4-agent-%E4%BB%A3%E7%A0%81%E5%A4%8D%E7%9B%98")
- 八、总结与关键概念速查
- 感谢阅读
一、直接调大模型接口,有什么问题
我一开始也觉得,AI Agent 听起来很玄乎,不就是调个大模型 API 吗?但真动手做需求的时候就发现问题了------LLM 能做的事情,比你想象中少得多。
1.1 LLM 的五个天生短板
短板一:没有记忆(Stateless)
你跟 LLM 聊天,上一轮说过的内容,下一轮它全忘了。LLM 本身是纯函数------同样的输入得到同样的输出,不保存任何状态。你问它"我刚才说了什么",它只能瞎编。想让 LLM 记住东西,你需要一个记忆模块(Memory)------把对话历史存起来,每次请求时一起发过去。存哪?数据库、Redis、前端 localStorage,都行。
短板二:无法与外部世界交互
让 LLM 帮你访问一个网页?它只能告诉你"你可以用 fetch 发一个 GET 请求"------它给的是思路,不是行动。它没有手,不会真的去发 HTTP 请求、不会去操作浏览器、不会去读你硬盘上的文件。想让 LLM 真的"做事",你需要一个工具调用模块(Tool Use)------告诉 LLM 有哪些工具可用,让它决定调哪个,然后代码去执行。
短板三:不知道你的私有数据
你公司内部的文档、产品手册、设计规范------LLM 的训练数据里没有这些。你直接问它"我们公司报销流程是什么",它只能瞎编。想让 LLM 了解你的私有知识,你需要 RAG(检索增强生成)------把内部文档向量化存起来,用户提问时先检索相关内容,再连同问题一起发给 LLM。
短板四:缺乏实时信息
LLM 的训练数据有截止日期。你问它今天的新闻、昨天的股价、最新版框架的 API------全部超纲。想让 LLM 获取实时信息,你需要接入外部 API 或搜索引擎------这些在 MCP 和 Tool 的范畴内解决。
短板五:不会处理复杂多步任务
"帮我分析这只股票并自动买卖"------这涉及查实时行情、读财报、做技术分析、评估风险、执行交易,不是一个 prompt 能搞定的。想让 LLM 处理这种复杂任务,你需要技能编排(Skills)------把大任务拆成小步骤,每一步调用不同的工具,最后拼成完整结果。
1.2 Agent 就是给 LLM 打补丁
上面这五个短板,每一个都有对应的解决方案。而 Agent(智能体)就是把这些解决方案系统性地组装到 LLM 身上。LLM 本身能思考、能规划、能推理,但它缺手脚、缺记忆、缺信息来源。Agent 就是给这个"大脑"装上手脚、配上记忆、接上情报网络。
LLM 本身其实不复杂------给它扩展 Tool 就能做事情,加上 Memory 就能记住东西,挂上 RAG 就能查内部知识库。这样一个知道内部知识、能思考规划、能动手做事的增强版大模型,就是一个 Agent。
二、Agent 的能力公式和工作流程
2.1 公式:Agent = LLM + Memory + Tool + RAG + MCP + Skills
把这个公式展开,每个模块解决一个问题:
| 模块 | 解决的问题 | 实现手段 |
|---|---|---|
| LLM | 推理和生成能力 | DeepSeek / Claude / GPT / 豆包 |
| Memory | LLM 记不住东西 | 数据库、Redis、前端存储、消息历史 |
| Tool | LLM 不会动手做事 | 函数封装 + schema 声明 + Tool 调用 |
| RAG | LLM 不知道私有知识 | 向量数据库 + 文档检索 + Prompt 注入 |
| MCP | 外部工具的标准化接入 | MCP Server + Client + 协议通信 |
| Skills | 需要多步复杂技能 | 任务拆分 + 工具编排 + 技能蒸馏 |
这个公式不是让你把所有模块都怼上去------实际上,一个最简单的 Agent,可能只需要 LLM + Tool,跑起来之后再按需加 Memory、RAG。重要的是理解每个模块解决的是什么问题,这样你遇到具体需求时知道该用什么。
Claude Code 和 Codex 是 Coding Agent(编程智能体),小龙虾(Manus)是自动化任务 Agent------它们底层都是这个公式的不同组装方式。
2.2 Agent 的执行流程
Agent 接到用户任务后的完整执行流程:
markdown
用户以 Prompt 形式提出复杂任务
↓
交给 Agent
↓
LLM 进行 Planning / Reasoning(规划/推理)
↓
┌─ 要不要加载 Memory?──→ 加载历史对话和记忆
│ ↓
├─ 要不要调用 Tool? ──→ 分步骤调用多个工具
│ ↓
├─ 要不要查 RAG? ──→ 检索内部知识库,注入 Prompt Template
│ ↓
└─→ 整合所有上下文,LLM 生成最终 Response
↓
返回给用户(任务完成)
这个流程的核心在于:LLM 不是在"执行任务",而是在"做决策"。它决定查不查记忆、调不调工具、检索什么知识------然后代码负责真正的执行。LLM 是大脑,代码是手脚,Agent 框架是连接两者的神经系统。
2.3 你已经见过的 Agent 产品
市面上很多看起来是"AI 产品"的东西,本质都是 Agent 的不同包装:
| 产品 | LLM(大脑) | Tool(手脚) | Memory | 本质 |
|---|---|---|---|---|
| Claude Code | Claude | 读文件、写代码、执行命令、搜索 | 对话历史 | Coding Agent |
| Cursor | Claude/GPT | 读写文件、终端、搜索代码库 | 对话历史 | Coding Agent |
| Manus(小龙虾) | Claude | 浏览器操作、文件处理、API 调用 | 任务状态管理 | 通用任务 Agent |
不一样的品牌、不一样的模型、不一样的工具,但骨架一模一样:LLM 推理 + Tool 执行 + Memory 存储。理解了这套骨架,换一个模型厂商、换一套工具,模式不变。
三、LangChain:为 Agent 而生的开发框架
3.1 比 OpenAI 还早的 LLM 框架
LangChain 的历史比很多人想象的要早------它比 OpenAI 的 Transformer API 和 Generative API 更早诞生。在 LLM 还没大规模流行的时候,LangChain 已经在做"如何把语言模型和外部世界连接起来"这件事了。
它的核心思路就一条:LLM 有很多家,各家接口不一样,LangChain 提供了一个统一的抽象层 。你用同一个 ChatOpenAI 类,换一下 baseURL 和 apiKey,就能无缝切换 DeepSeek、OpenAI、豆包、月之暗面------不需要改业务代码。
这跟 MCP 的思路异曲同工,只不过 LangChain 解决的是"怎么调用 LLM"的标准化问题,而 MCP 解决的是"怎么调用外部工具"的标准化问题。
LangChain 生态的两个核心库:
langchain:单智能体开发框架。定义 Tool、管理 Messages、编排 LLM 调用流程。你笔记里的 hello-langchain 项目用的就是这套。langgraph:多智能体协作框架。当单个 Agent 不够用、需要多个 Agent 协作完成复杂任务时(比如一个 Agent 负责查资料、一个 Agent 负责写代码、一个 Agent 负责测试),用 langgraph 来编排。
结合后端技术(Node.js / NestJS),你可以用 LangChain + LangGraph 开发 AI 全栈 Agent 产品,通过 Harness Engineering(工程化落地) 把 AI 技术转化为商业价值。
3.2 LangChain 的模块分工
回到我们项目里实际用到的包:
json
{
"dependencies": {
"@langchain/core": "^1.2.1",
"@langchain/openai": "^1.5.3",
"dotenv": "^17.4.2",
"zod": "^4.4.3"
}
}
| 包 | 提供什么 | 对应概念 |
|---|---|---|
@langchain/openai |
ChatOpenAI 类------统一封装各家 LLM 调用 |
LLM 大脑 |
@langchain/core |
tool() 函数、SystemMessage/HumanMessage/AIMessage/ToolMessage 四种消息类 |
Tool + Messages |
zod |
z.string()、z.object() 等声明式 schema |
参数校验 |
dotenv |
读 .env 文件,管理 API Key |
配置管理 |
这里面 @langchain/core 是最关键的------它提供了定义 Tool 的 tool() 函数、管理对话的 Messages 体系,以及 bindTools() 方法把工具注册到模型上。
3.3 用 LangChain 连接 DeepSeek
最简单的调用,几行代码就能跑通:
javascript
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',
},
});
const response = await model.invoke('棍王杯台球比赛应该设什么奖励?');
console.log(response.content);
注意几个细节:
- 类名是
ChatOpenAI,但实际调的是 DeepSeek------因为 DeepSeek 兼容 OpenAI 接口格式。configuration.baseURL把请求路由到了 DeepSeek 的服务器。 model.invoke()是 LangChain 的统一调用入口。不管你用的是 OpenAI、DeepSeek 还是豆包,invoke 的用法不变,底层自动适配。- 返回的
response不是纯字符串,而是一个AIMessage对象。.content拿到的是 LLM 回复的文本,后面 Tool 调用时.tool_calls会用到。
四、Tool:让 LLM 动手的能力
这是 Agent 最核心、也最实用的部分。LLM 再聪明,没有 Tool 只能"口嗨"------能分析问题、给出思路,但不能执行。Tool 补上了行动力的空白。
4.1 Tool 的两个核心部分
LangChain 中定义一个 Tool,需要两部分:
第一部分:处理函数(异步)
这就是工具的实际逻辑------读文件、调 API、查数据库,总之是做具体事情的代码。必须是异步函数,因为文件 I/O、网络请求都是耗时操作。
第二部分:函数描述对象
包含三个关键信息:
| 字段 | 作用 | 注意事项 |
|---|---|---|
name |
工具的名称,LLM 用它来指定调用哪个工具 | 语义清晰,建议用 snake_case |
description |
详细描述工具功能、覆盖场景、参数需求 | 这是 LLM 理解工具的唯一依据,越详细越好 |
schema |
参数的类型和约束 | 用 zod 定义,LLM 必须提供 schema 约定的参数才能调用 |
4.2 手写一个读文件工具
你笔记里的 tool.mjs 实现了一个完整的读文件工具,拆开来看:
javascript
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import fs from 'node:fs/promises';
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({
filePath: z.string().describe('要读取的文件路径')
})
}
);
这里面藏着一个工程上的好习惯:在工具函数里打 log 反馈进度。Agent 的任务可能非常复杂(调用多个工具、每个工具多次调用),执行时间可能很长。如果用户等了 30 秒什么反馈都没有,他会以为程序卡死了,直接 Ctrl+C。每隔一段时间打一个进度 log,告诉用户"我在做事、做到哪了",能大幅降低用户的焦虑感。
4.3 description 和 schema:LLM 理解工具的唯一渠道
description 和 schema 是 LLM 决定"要不要调这个工具、调的时候传什么参数"的全部依据。写得好不好,直接影响 Agent 的行为质量。
description 要写什么?
- 这个工具是干什么的(读文件)
- 什么场景下应该用它(用户要求读取文件、查看代码、分析文件内容时)
- 参数需要什么格式(文件路径,可以是相对路径或绝对路径)
这不是在写技术文档,是在给 LLM 写"使用说明书"。LLM 会根据这段描述判断"用户现在的要求匹配不匹配这个工具的能力",描述越清楚越具体,匹配就越准确。
schema 要写什么?
用 zod 声明参数类型和含义:
javascript
schema: z.object({
filePath: z.string().describe('要读取的文件路径')
})
z.object({...}) 定义了一个对象参数,里面有一个 filePath 字段,类型是 z.string()(字符串),描述是"要读取的文件路径"。
对比一下手写 JSON Schema 的版本:
json
{
"type": "object",
"properties": {
"filePath": {
"type": "string",
"description": "要读取的文件路径"
}
},
"required": ["filePath"]
}
zod 版只有一行,JSON Schema 版十几行------功能完全一样,但 zod 的写法同时完成了类型声明和运行时校验。当 LLM 传了参数进来,zod 先校验------filePath 是不是字符串?不是就抛错,杜绝非法参数进入处理函数。
4.4 把工具"绑"到模型上
定义了工具之后,需要告诉 LLM"你可以用这些工具"。LangChain 的做法是 bindTools():
javascript
const tools = [readFileTool];
const modelWithTools = model.bindTools(tools);
model.bindTools(tools) 生成了一个新的 model 实例,它内部自动完成了两件事:
- 把
tools数组转换成 OpenAI 兼容的 Tool Definition 格式 - 每次调用时,把工具列表和用户消息一起发给 LLM
之后所有 Agent 调用都通过 modelWithTools,而不是原始的 model。当你调用 modelWithTools.invoke(messages) 时,LLM 会自己判断------这个任务需要调工具吗?不需要就直接生成文本;需要就返回 tool_calls。
LLM 有一个"自知之明"的设计:当它判断需要调用工具时,它不会继续生成文本,而是停下来,在返回结果里标记 tool_calls,告诉代码"我需要调这些工具" 。tool_calls 是一个数组,包含了 LLM 要求调用的所有工具信息:
javascript
// LLM 返回的 AIMessage 中包含 tool_calls
{
tool_calls: [
{
id: "call_xxx", // 工具调用 ID,用于后续关联结果
name: "read_file", // 要调用的工具名
args: { // LLM 生成的参数
filePath: "tool.mjs"
}
}
]
}
id 这个字段值得单独说一下。为什么要一个 ID?因为一个 Agent 任务可能涉及多次工具调用------先读文件 A,再分析,再读文件 B。每次调用的结果需要对应到具体哪次调用,LLM 通过 ID 来关联"这个结果来自哪个工具调用请求"。这是 LLM 基于自然语言处理复杂任务的基础机制。
五、Messages 体系:Agent 的记忆载体
5.1 四种消息角色
LLM 本身没有状态,Agent 的"记忆"靠的是把整个对话历史打包在 messages 数组里。每一轮对话都加上新消息,然后把整个数组发给 LLM。
LangChain 封装了四种消息类型,对应 OpenAI 的四种 role:
| LangChain 类 | OpenAI role | 什么时候用 |
|---|---|---|
SystemMessage |
system |
对话开始时设置角色、规则、边界 |
HumanMessage |
user |
用户发言 |
AIMessage |
assistant |
LLM 的回复(包括 tool_calls) |
ToolMessage |
tool |
工具执行后的返回结果 |
你在代码里用 LangChain 的消息类,它们内部会自动转成 OpenAI 兼容的 JSON 格式发给 API。这样做的好处是类型安全------你不会把 "role": "usre" 这种拼写错误传给 API,因为 LangChain 帮你约束了。
5.2 SystemMessage:给 Agent 定规矩
SystemMessage 是整个对话的第一条消息,它的作用是给 LLM 设定"人设"和行为边界:
javascript
new SystemMessage(`
你是一个代码助手,可以使用工具读取文件并解释代码。
工作流程:
1. 用户要求读取文件时,立即调用 read_file 工具。
2. 等待工具返回文件内容。
3. 基于文件内容进行分析和解释。
可用工具:
- read_file: 读取文件内容(使用此工具来获取文件内容)
`)
这段 System Prompt 做了三件事:
- 角色定义:"你是一个代码助手"------设定身份
- 行为指引:明确的工作流程(1→2→3),告诉 LLM 遇到什么情况该做什么
- 能力清单:列出可用工具及说明,强化 LLM 对工具的认知
System Prompt 不是可有可无的东西------对于 Agent 来说,一个好的 System Prompt 能大幅降低 LLM"瞎操作"的概率。你给 Agent 接了五个工具,如果不写清楚什么时候该用哪个,LLM 可能在你问"今天天气如何"时去调用文件读取工具------因为它不知道这个问题的正确答案应该是"我不适合回答"。
5.3 消息流转的完整过程
一条消息从用户发出到 Agent 回答,messages 数组经历了这些变化:
scss
初始化:
messages = [ SystemMessage("你是一个代码助手..."), HumanMessage("请读取 tool.mjs 文件内容并解释代码") ]
第一次 modelWithTools.invoke(messages):
LLM 返回 AIMessage,其中包含 tool_calls: [{ name: "read_file", args: { filePath: "tool.mjs" } }]
→ messages.push(AIMessage)
代码执行工具:
fs.readFile("tool.mjs") → 拿到文件内容
→ messages.push(ToolMessage(文件内容, tool_call_id))
第二次 modelWithTools.invoke(messages):
LLM 拿到 ToolMessage,看到文件内容,生成自然语言解释
→ messages.push(AIMessage(解释文本))
每一步都把新的消息追加到数组末尾,LLM 每次都看到完整的对话历史。这就是 Agent 的"记忆"------不是存在某个魔法变量里,就是每次把历史消息完整传回去。
这也解释了为什么长对话的 token 消耗会越来越大:每轮对话都要重新发送之前的所有消息,100 轮对话就是 100 条消息打包发过去。
六、高性能 Tool 调用:Promise.all 并行执行
6.1 串行 vs 并行:2500ms vs 2000ms 的差距
你笔记里 1.html 这个 demo 非常直观地展示了串行和并行的性能差异。场景很简单:同时获取天气数据和推文,两个请求互不依赖。
串行写法(用两个 await):
javascript
async function main() {
console.time("my-operation");
const weatherData = await getWeather(); // 等 2000ms
const tweetsData = await getTweets(); // 再等 500ms
console.log(weatherData);
console.log(tweetsData);
console.timeEnd("my-operation"); // 总耗时 ≈ 2500ms
}
scss
时间线:
0ms ───────────────── 2000ms ── 2500ms
│ getWeather() 等待中... │ getTweets() │ 完成
两个 await 是串行的:必须先等到第一个完成,才能开始第二个。总耗时 = 2000ms + 500ms = 2500ms。
并行写法(用 Promise.all):
javascript
const main = async () => {
console.time("my-operation");
const [weatherData, tweetsData] =
await Promise.all([getWeather(), getTweets()]);
console.timeEnd("my-operation"); // 总耗时 ≈ 2000ms
}
scss
时间线:
0ms ──────────────── 2000ms
│ getWeather() (2000ms) │
│ getTweets() (500ms) │ 完成
两个 Promise 同时启动,总耗时取决于最慢的那个------2000ms。节省了 500ms,在复杂 Agent 场景下这个差距会非常惊人:如果你的 Agent 需要同时调用 5 个工具,每个 2 秒,串行要 10 秒,并行只要 2 秒多一点。
6.2 Promise 三态与 async/await
要理解 Promise.all,先得理解 Promise 本身的状态机制。
Promise 是 ES6 提供的异步编程语法,有三种状态:
scss
┌──→ Pending(等待中)
│ ↓
├──→ resolve() → Fulfilled(已成功)
│ ↓
└──→ reject() → Rejected(已失败)
一个 Promise 只能从 Pending 变成 Fulfilled 或 Rejected,一旦变了就再也不会变。这个"单次转换"特性保证了 Promise 的行为是可预测的------不会出现"先成功又失败"的情况。
async/await 是 ES8 提供的最优雅的异步写法:
javascript
// 不用 await(回调地狱)
getWeather().then(weather => {
getTweets().then(tweets => {
console.log(weather, tweets);
});
});
// 用 await(像同步代码一样写异步逻辑)
const weather = await getWeather();
const tweets = await getTweets();
console.log(weather, tweets);
await 让异步代码看起来像同步代码------代码从上往下读,逻辑清晰。但代价就是上面说的串行问题:每个 await 都会"卡住"当前函数,直到它等的那件事做完。
Promise.all 在"不牺牲可读性"的前提下解决了串行问题:
javascript
const [weatherData, tweetsData] = await Promise.all([getWeather(), getTweets()]);
Promise.all 接收一个 Promise 数组,返回一个新 Promise。这个新 Promise 的规则是:等数组里所有 Promise 都 Fulfilled 了,它才 Fulfilled;任意一个 Rejected 了,它立刻 Rejected。返回结果是一个数组,顺序和输入的 Promise 顺序一致(不是按完成时间排序)。
6.3 Promise.all 的工程意义
回到 Agent 开发的具体场景:LLM 任务复杂时,一次可能需要调用多个工具,或者同一个工具调用多次。比如用户说"帮我读一下 A 文件、B 文件、C 文件的内容",LLM 会返回三个 tool_calls。
串行的话:
javascript
for (const toolCall of toolCalls) {
const result = await executeTool(toolCall); // 一个接一个执行
messages.push(result);
}
并行的话:
javascript
const results = await Promise.all(
toolCalls.map(toolCall => executeTool(toolCall)) // 同时执行
);
results.forEach(result => messages.push(result));
三个文件各 1 秒,串行 3 秒,并行 1 秒------三倍的性能差距。而且这些工具调用之间互不依赖(读 A 文件和读 B 文件没有顺序要求),完全应该并行。
这就是你笔记里说的"即将打造高性能的第一个 Agent"的含义------Agent 的性能,很大程度上取决于你用什么方式调度 Tool 调用。Promise.all 是最基础但最有效的优化手段。
七、完整 Agent 代码复盘
把上面的所有概念串起来,项目的目录结构是这样的:
bash
hello-langchain/
├── package.json ← 依赖声明
├── pnpm-lock.yaml ← 锁文件
├── .env ← API Key 和模型配置
├── index.mjs ← 最简 LLM 调用(model.invoke)
├── tool.mjs ← Agent 核心:Tool 定义 + Messages + bindTools
└── 1.html ← Promise.all 并行演示
index.mjs 是最简版本------只演示了怎么用 LangChain 调 LLM:
javascript
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',
},
});
const response = await model.invoke('棍王杯台球比赛应该设什么奖励?');
console.log(response.content);
tool.mjs 是 Agent 的完整实现。从头到尾理一遍它的执行链路:
- 初始化模型 :
new ChatOpenAI({...})连上 DeepSeek - 定义工具 :
tool(处理函数, 描述对象)创建一个read_file工具 - 绑定工具到模型 :
model.bindTools(tools)让 LLM 知道有工具可用 - 构建消息数组 :
SystemMessage(定规矩)+HumanMessage(用户请求) - 第一次 invoke :LLM 分析任务,判断需要调
read_file,返回AIMessage包含tool_calls - 执行工具 :代码根据
tool_calls调fs.readFile(),拿到文件内容 - 追加消息 :把
AIMessage(含 tool_calls)和工具结果 push 到 messages - 第二次 invoke:LLM 拿到文件内容,生成最终的解释回复
这个循环可以无限扩展------加更多工具、处理更多轮调用、用 Promise.all 并行执行多个工具------但核心骨架不变:LLM 决策 → 代码执行 → 结果反馈 → LLM 生成。
在完整 Agent 开发中,你还可以接入:
- Memory 模块:把消息数组持久化到数据库,跨会话保持记忆
- RAG 模块:在 System Prompt 和 HumanMessage 之间插入检索到的知识片段
- MCP Client:通过 MCP 协议接入第三方工具,而不用自己手写 Tool 函数
八、总结与关键概念速查
| 概念 | 一句话解释 | 对应代码 |
|---|---|---|
| Agent | LLM + 外部能力的增强版 AI 系统 | 整个 tool.mjs |
| LangChain | 为 Agent 开发的框架,封装 LLM/Tool/Message | @langchain/core + @langchain/openai |
| ChatOpenAI | LangChain 的统一 LLM 调用类,不限于 OpenAI | new ChatOpenAI({...}) |
| tool() | LangChain 定义的函数:工具 = 处理函数 + 描述对象 | tool(async({...}), {name, description, schema}) |
| zod | 声明式 schema 校验,替代手写 JSON Schema | z.object({ filePath: z.string() }) |
| bindTools() | 把工具列表注册到模型,让 LLM 知道有哪些工具 | model.bindTools(tools) |
| SystemMessage | 对话第一条消息,设定角色和行为边界 | new SystemMessage("你是代码助手...") |
| Messages 数组 | Agent 的"记忆"载体,每次把完整历史发给 LLM | messages = [SystemMsg, HumanMsg, ...] |
| tool_calls | LLM 返回的工具调用指令数组 | AIMessage.tool_calls |
| Promise.all | 并行执行多个异步任务,等全部完成才返回 | Promise.all([p1, p2, p3]) |
| await | ES8 异步语法,让异步代码看起来像同步 | const result = await promise |
搞懂了这篇文章的内容,你就拿到了 Agent 开发的"最小必要知识"。剩下的东西------多 Agent 协作、RAG 记忆系统、MCP 标准化接入、LangGraph 工作流编排------都是在这个基础上做的扩展。Agent 的骨架不会变:LLM 负责想,Tool 负责做,Messages 负责记,Promise.all 负责快。
下一步的动手建议:在 tool.mjs 里多加几个工具(比如一个写文件的工具、一个调 API 的工具),然后让 LLM 在一次回答里同时用到多个工具。你会发现多工具协作时,System Prompt 的描述质量和 Promise.all 的并行调度,是两个最影响体验的变量------一个决定 LLM 能不能选对工具,一个决定工具执行快不快。
感谢阅读。如果这篇文章帮你理清了 Agent 的核心骨架,麻烦点赞支持一下。关注我,后续会持续更新 LangGraph 多 Agent 协作、MCP 协议对接、RAG 记忆系统实战等系列------都是先写代码跑通再写文章,不空谈架构。