用LangChain 打造第一个 Agent:LLM 大脑与 Tool 手脚的完整闭环

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 类,换一下 baseURLapiKey,就能无缝切换 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 理解工具的唯一渠道

descriptionschema 是 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 实例,它内部自动完成了两件事:

  1. tools 数组转换成 OpenAI 兼容的 Tool Definition 格式
  2. 每次调用时,把工具列表和用户消息一起发给 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. 行为指引:明确的工作流程(1→2→3),告诉 LLM 遇到什么情况该做什么
  3. 能力清单:列出可用工具及说明,强化 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 的完整实现。从头到尾理一遍它的执行链路:

  1. 初始化模型new ChatOpenAI({...}) 连上 DeepSeek
  2. 定义工具tool(处理函数, 描述对象) 创建一个 read_file 工具
  3. 绑定工具到模型model.bindTools(tools) 让 LLM 知道有工具可用
  4. 构建消息数组SystemMessage(定规矩)+ HumanMessage(用户请求)
  5. 第一次 invoke :LLM 分析任务,判断需要调 read_file,返回 AIMessage 包含 tool_calls
  6. 执行工具 :代码根据 tool_callsfs.readFile(),拿到文件内容
  7. 追加消息 :把 AIMessage(含 tool_calls)和工具结果 push 到 messages
  8. 第二次 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 记忆系统实战等系列------都是先写代码跑通再写文章,不空谈架构。

相关推荐
小林ixn1 小时前
手写AI编程Agent:从0到1复刻Trae/Cursor的核心能力
人工智能·aigc·ai编程
冬哥聊AI1 小时前
腾讯三面:Claude Code 写书超过上下文长度怎么办?/compact 只是应急,五步架构才是正解
人工智能
饼干哥哥1 小时前
用Claude Code跑通跨境电商的5大场景,顶10个人的团队
人工智能·设计模式·正则表达式
ATA88881 小时前
数据库管理工具的数据安全机制对比:权限管控与审计
数据库·人工智能·数据分析·数据库管理
西西学代码1 小时前
Flutter---底部导航栏(2)
开发语言·javascript·flutter
legendary_1631 小时前
SINK芯片:Type-C统一供电时代的小家电核心方案
c语言·开发语言·人工智能·智能手机
私人珍藏库1 小时前
[Android] 发票制作器-内置多行业专业模板
android·人工智能·app·软件·多功能
桐桐桐1 小时前
深入理解 MCP:AI 连接外部世界的标准协议
人工智能
mit6.8241 小时前
AI不该只预测潜空间
人工智能