从零开始构建AI Agent —— 基于LangChain的智能体开发实践

摘要:2025-2026年,AI Agent(智能体)已从概念炒作进入工程落地阶段,成为大模型应用开发中最具商业价值的方向。Claude Code、Manus、Codex等明星产品背后都遵循着同一套核心架构。本文从"为什么裸LLM不够用"这一根本问题出发,逐层剖析Agent的六大核心模块(Memory、Tool、RAG、MCP、Skills),深入讲解Agent的工作流程和LLM的"自知之明"机制;随后以LangChain.js为主线,通过一个精心设计的示例项目,手把手带你从环境准备、最简模型调用,一路走到Tool-use Agent的完整实现;最后深入探讨Promise并发模型在Agent Tool调用中的性能优化策略,并给出简易版Claude Code Agent的架构设计思路。阅读本文,你将获得打造自己第一个AI Agent所需的完整知识体系和可运行的代码基础。


目录

  1. [为什么需要Agent?------ 裸LLM的五大局限](#为什么需要Agent?—— 裸LLM的五大局限 "#1-%E4%B8%BA%E4%BB%80%E4%B9%88%E9%9C%80%E8%A6%81agent-%E8%A3%B8llm%E7%9A%84%E4%BA%94%E5%A4%A7%E5%B1%80%E9%99%90")
    • [1.1 问题一:LLM是无状态的](#1.1 问题一:LLM是无状态的 "#11-%E9%97%AE%E9%A2%98%E4%B8%80llm%E6%98%AF%E6%97%A0%E7%8A%B6%E6%80%81%E7%9A%84")
    • [1.2 问题二:LLM无法与外部世界交互](#1.2 问题二:LLM无法与外部世界交互 "#12-%E9%97%AE%E9%A2%98%E4%BA%8Cllm%E6%97%A0%E6%B3%95%E4%B8%8E%E5%A4%96%E9%83%A8%E4%B8%96%E7%95%8C%E4%BA%A4%E4%BA%92")
    • [1.3 问题三:LLM无法访问私有/内部知识](#1.3 问题三:LLM无法访问私有/内部知识 "#13-%E9%97%AE%E9%A2%98%E4%B8%89llm%E6%97%A0%E6%B3%95%E8%AE%BF%E9%97%AE%E7%A7%81%E6%9C%89%E5%86%85%E9%83%A8%E7%9F%A5%E8%AF%86")
    • [1.4 问题四:LLM不知道训练截止日期之后的事](#1.4 问题四:LLM不知道训练截止日期之后的事 "#14-%E9%97%AE%E9%A2%98%E5%9B%9Bllm%E4%B8%8D%E7%9F%A5%E9%81%93%E8%AE%AD%E7%BB%83%E6%88%AA%E6%AD%A2%E6%97%A5%E6%9C%9F%E4%B9%8B%E5%90%8E%E7%9A%84%E4%BA%8B")
    • [1.5 问题五:LLM无法执行复杂的多步骤技能](#1.5 问题五:LLM无法执行复杂的多步骤技能 "#15-%E9%97%AE%E9%A2%98%E4%BA%94llm%E6%97%A0%E6%B3%95%E6%89%A7%E8%A1%8C%E5%A4%8D%E6%9D%82%E7%9A%84%E5%A4%9A%E6%AD%A5%E9%AA%A4%E6%8A%80%E8%83%BD")
  2. Agent的核心公式:六大模块解析
    • [2.1 LLM ------ 大脑](#2.1 LLM —— 大脑 "#21-llm--%E5%A4%A7%E8%84%91")
    • [2.2 Memory ------ 记忆](#2.2 Memory —— 记忆 "#22-memory--%E8%AE%B0%E5%BF%86")
    • [2.3 Tool ------ 双手](#2.3 Tool —— 双手 "#23-tool--%E5%8F%8C%E6%89%8B")
    • [2.4 RAG ------ 检索增强生成](#2.4 RAG —— 检索增强生成 "#24-rag--%E6%A3%80%E7%B4%A2%E5%A2%9E%E5%BC%BA%E7%94%9F%E6%88%90")
    • [2.5 MCP ------ 模型上下文协议](#2.5 MCP —— 模型上下文协议 "#25-mcp--%E6%A8%A1%E5%9E%8B%E4%B8%8A%E4%B8%8B%E6%96%87%E5%8D%8F%E8%AE%AE")
    • [2.6 Skills ------ 技能编排](#2.6 Skills —— 技能编排 "#26-skills--%E6%8A%80%E8%83%BD%E7%BC%96%E6%8E%92")
    • [2.7 现实中的Agent产品剖析](#2.7 现实中的Agent产品剖析 "#27-%E7%8E%B0%E5%AE%9E%E4%B8%AD%E7%9A%84agent%E4%BA%A7%E5%93%81%E5%89%96%E6%9E%90")
  3. Agent的工作流程:从Prompt到任务完成
    • [3.1 完整执行流程](#3.1 完整执行流程 "#31-%E5%AE%8C%E6%95%B4%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B")
    • [3.2 LLM的"自知之明":tool_calls机制](#3.2 LLM的"自知之明":tool_calls机制 "#32-llm%E7%9A%84%E8%87%AA%E7%9F%A5%E4%B9%8B%E6%98%8Etool_calls%E6%9C%BA%E5%88%B6")
    • [3.3 一个具体的执行示例](#3.3 一个具体的执行示例 "#33-%E4%B8%80%E4%B8%AA%E5%85%B7%E4%BD%93%E7%9A%84%E6%89%A7%E8%A1%8C%E7%A4%BA%E4%BE%8B")
  4. [LangChain ------ 为什么选择它作为Agent开发框架](#LangChain —— 为什么选择它作为Agent开发框架 "#4-langchain--%E4%B8%BA%E4%BB%80%E4%B9%88%E9%80%89%E6%8B%A9%E5%AE%83%E4%BD%9C%E4%B8%BAagent%E5%BC%80%E5%8F%91%E6%A1%86%E6%9E%B6")
    • [4.1 LangChain的历史定位](#4.1 LangChain的历史定位 "#41-langchain%E7%9A%84%E5%8E%86%E5%8F%B2%E5%AE%9A%E4%BD%8D")
    • [4.2 核心能力一:统一的LLM抽象层](#4.2 核心能力一:统一的LLM抽象层 "#42-%E6%A0%B8%E5%BF%83%E8%83%BD%E5%8A%9B%E4%B8%80%E7%BB%9F%E4%B8%80%E7%9A%84llm%E6%8A%BD%E8%B1%A1%E5%B1%82")
    • [4.3 核心能力二:标准化的Tool抽象](#4.3 核心能力二:标准化的Tool抽象 "#43-%E6%A0%B8%E5%BF%83%E8%83%BD%E5%8A%9B%E4%BA%8C%E6%A0%87%E5%87%86%E5%8C%96%E7%9A%84tool%E6%8A%BD%E8%B1%A1")
    • [4.4 核心能力三:消息类型体系](#4.4 核心能力三:消息类型体系 "#44-%E6%A0%B8%E5%BF%83%E8%83%BD%E5%8A%9B%E4%B8%89%E6%B6%88%E6%81%AF%E7%B1%BB%E5%9E%8B%E4%BD%93%E7%B3%BB")
    • [4.5 全栈Agent技术架构](#4.5 全栈Agent技术架构 "#45-%E5%85%A8%E6%A0%88agent%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84")
  5. [动手实践:Hello LangChain 项目全解析](#动手实践:Hello LangChain 项目全解析 "#5-%E5%8A%A8%E6%89%8B%E5%AE%9E%E8%B7%B5hello-langchain-%E9%A1%B9%E7%9B%AE%E5%85%A8%E8%A7%A3%E6%9E%90")
    • [5.1 项目初始化与环境配置](#5.1 项目初始化与环境配置 "#51-%E9%A1%B9%E7%9B%AE%E5%88%9D%E5%A7%8B%E5%8C%96%E4%B8%8E%E7%8E%AF%E5%A2%83%E9%85%8D%E7%BD%AE")
    • [5.2 级别一:最简模型调用](#5.2 级别一:最简模型调用 "#52-%E7%BA%A7%E5%88%AB%E4%B8%80%E6%9C%80%E7%AE%80%E6%A8%A1%E5%9E%8B%E8%B0%83%E7%94%A8indexmjs")
    • [5.3 级别二:Tool-use Agent](#5.3 级别二:Tool-use Agent "#53-%E7%BA%A7%E5%88%AB%E4%BA%8Ctool-use-agenttoolmjs")
    • [5.4 Tool调用的消息流转全流程](#5.4 Tool调用的消息流转全流程 "#54-tool%E8%B0%83%E7%94%A8%E7%9A%84%E6%B6%88%E6%81%AF%E6%B5%81%E8%BD%AC%E5%85%A8%E6%B5%81%E7%A8%8B")
  6. Tool性能优化:Promise并发模型深度解析
    • [6.1 为什么Agent需要并发?](#6.1 为什么Agent需要并发? "#61-%E4%B8%BA%E4%BB%80%E4%B9%88agent%E9%9C%80%E8%A6%81%E5%B9%B6%E5%8F%91")
    • [6.2 Promise基础:三种状态与不可变性](#6.2 Promise基础:三种状态与不可变性 "#62-promise%E5%9F%BA%E7%A1%80%E4%B8%89%E7%A7%8D%E7%8A%B6%E6%80%81%E4%B8%8E%E4%B8%8D%E5%8F%AF%E5%8F%98%E6%80%A7")
    • [6.3 串行 vs 并行:实战对比](#6.3 串行 vs 并行:实战对比 "#63-%E4%B8%B2%E8%A1%8C-vs-%E5%B9%B6%E8%A1%8C1html-%E5%AE%9E%E6%88%98%E5%AF%B9%E6%AF%94")
    • [6.4 async/await:异步变同步的语法糖](#6.4 async/await:异步变同步的语法糖 "#64-asyncawait%E5%BC%82%E6%AD%A5%E5%8F%98%E5%90%8C%E6%AD%A5%E7%9A%84%E8%AF%AD%E6%B3%95%E7%B3%96")
    • [6.5 Promise.all在Agent中的实战应用](#6.5 Promise.all在Agent中的实战应用 "#65-promiseall%E5%9C%A8agent%E4%B8%AD%E7%9A%84%E5%AE%9E%E6%88%98%E5%BA%94%E7%94%A8")
  7. [架构实战:手写一个简易版Claude Code Agent](#架构实战:手写一个简易版Claude Code Agent "#7-%E6%9E%B6%E6%9E%84%E5%AE%9E%E6%88%98%E6%89%8B%E5%86%99%E4%B8%80%E4%B8%AA%E7%AE%80%E6%98%93%E7%89%88claude-code-agent")
    • [7.1 核心公式](#7.1 核心公式 "#71-%E6%A0%B8%E5%BF%83%E5%85%AC%E5%BC%8F")
    • [7.2 Demo场景:创建一个React+Vite的TodoList](#7.2 Demo场景:创建一个React+Vite的TodoList "#72-demo%E5%9C%BA%E6%99%AF%E5%88%9B%E5%BB%BA%E4%B8%80%E4%B8%AAreactvite%E7%9A%84todolist")
    • [7.3 完整架构设计](#7.3 完整架构设计 "#73-%E5%AE%8C%E6%95%B4%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1")
    • [7.4 关键设计决策](#7.4 关键设计决策 "#74-%E5%85%B3%E9%94%AE%E8%AE%BE%E8%AE%A1%E5%86%B3%E7%AD%96")
  8. 总结与展望
    • [8.1 核心知识体系回顾](#8.1 核心知识体系回顾 "#81-%E6%A0%B8%E5%BF%83%E7%9F%A5%E8%AF%86%E4%BD%93%E7%B3%BB%E5%9B%9E%E9%A1%BE")
    • [8.2 从本文你可以带走的6项能力](#8.2 从本文你可以带走的6项能力 "#82-%E4%BB%8E%E6%9C%AC%E6%96%87%E4%BD%A0%E5%8F%AF%E4%BB%A5%E5%B8%A6%E8%B5%B0%E7%9A%846%E9%A1%B9%E8%83%BD%E5%8A%9B")
    • [8.3 下一步学习路线图](#8.3 下一步学习路线图 "#83-%E4%B8%8B%E4%B8%80%E6%AD%A5%E5%AD%A6%E4%B9%A0%E8%B7%AF%E7%BA%BF%E5%9B%BE")

1. 为什么需要Agent?------ 裸LLM的五大局限

很多初学者以为"做AI应用 = 调用大模型API",于是写下这样的代码:

javascript 复制代码
const response = await openai.chat.completions.create({
    model: 'gpt-4',
    messages: [{ role: 'user', content: '帮我分析一下公司的财务报表' }],
});
console.log(response.choices[0].message.content);

跑完之后发现:模型回复头头是道,但实际上什么都没做成------它既没有读到你的财务报表文件,也没有记住你上周告诉它的财务背景,更没有去网上下载最新的行业对比数据。

这就是裸LLM(未经扩展的大语言模型)的根本局限。让我们逐一深入剖析。

1.1 问题一:LLM是无状态的

核心矛盾:你上周和它聊过的信息,这周它还能记住吗?

答案是:不能 。每次API调用都是独立的、无状态的(Stateless)。HTTP请求本身是无状态的,LLM API也是如此------每一次 POST /v1/chat/completions 都是一个全新的会话。

这意味着什么? 你必须在每次请求时把整个对话历史重新发送一遍:

javascript 复制代码
// 第1次对话
const messages1 = [{ role: 'user', content: '我叫张三,我是架构师' }];
// LLM回复: "你好张三,有什么可以帮助你的?"

// 第2次对话 ------ 如果不带上历史,LLM就忘了你是谁
const messages2 = [{ role: 'user', content: '我叫什么名字?' }];
// LLM: "抱歉,我不知道你的名字......"  ← 它真的不知道!

解决方案 ------ Memory模块

存储方案 适用场景 优点 缺点
数据库(MySQL/PostgreSQL) 持久化存储,长期记忆 可靠、可查询 速度较慢
Redis 会话缓存,短期记忆 极快 重启丢失
前端 localStorage 用户偏好、设置 零服务端成本 容量有限

Memory模块的核心逻辑是:在每次发给LLM的消息列表中,自动拼接相关的历史记忆。这就是"LLM + 后端"组合的起点。

1.2 问题二:LLM无法与外部世界交互

核心矛盾:你让LLM帮你访问一个网页、读一个文件、发一封邮件------它只能给你思路,没法自己动手。

LLM是一个"缸中之脑":它能生成文本,但它的输出只是一串字符。它不能:

  • 🌐 访问网页(没有HTTP客户端)
  • 📁 读写文件(没有文件系统权限)
  • 💻 执行命令(没有Shell)
  • 📧 发送邮件(没有SMTP客户端)
  • 🗄️ 查询数据库(没有数据库连接)

这就是Tool Use模块要解决的问题 。Tool本质上是一个函数桥接器 ------把LLM的"意图"("我想读文件A")转化为实际的系统调用(fs.readFile('A')),然后把结果返回给LLM。

💡 类比:LLM像一个瘫痪的天才------大脑完好但四肢不能动。Tool就是给它装上的机械外骨骼,让它能实际作用于世界。

1.3 问题三:LLM无法访问私有/内部知识

核心矛盾:你问它公司内部的API文档、产品规格书、客户合同------它完全不知道。

LLM的知识来自于公开训练数据(网页、书籍、论文等)。这些数据中不包含:

  • 你公司的内部Wiki
  • 你的Notion文档
  • 你的GitLab代码仓库
  • 你的客户沟通记录

解决方案 ------ RAG(Retrieval-Augmented Generation,检索增强生成)

RAG的工作流程分为两个阶段:

scss 复制代码
【离线阶段】文档 → 切分(Chunking) → 向量化(Embedding) → 向量数据库
【在线阶段】用户问题 → 向量化 → 相似检索 → 拼接Prompt → LLM生成回答

核心思想:先检索,再生成。不靠LLM的记忆,而是把相关文档"喂"给它。

1.4 问题四:LLM不知道训练截止日期之后的事

核心矛盾:你问最新的世界杯新闻、今天的股价、上周发布的框架文档------训练数据里没有。

所有LLM都有一个知识截止日期(Knowledge Cutoff),模型预训练完成后,其知识就"冻结"了。对于2026年的我们来说,如果你用的模型知识截止于2024年,那么:

  • 2025年发布的LangChain 1.0 稳定版 → ❌ 不知道
  • 2026年世界杯预选赛结果 → ❌ 不知道
  • 今天的天气 → ❌ 不知道

解决方案 ------ MCP(Model Context Protocol)+ 实时Tool

MCP是Anthropic提出的一种标准化的第三方Tool协议,可以理解为"Tool的USB接口"------任何服务只要实现了MCP Server,Agent就能通过统一的MCP Client来调用它。比如:

  • 天气MCP Server → Agent获取实时天气
  • 新闻MCP Server → Agent获取最新资讯
  • GitHub MCP Server → Agent操作代码仓库

1.5 问题五:LLM无法执行复杂的多步骤技能

核心矛盾:你让它"做一份Q2财报PPT,分析股市并自动买卖"------单次对话远远不够。

这类任务需要:

  1. 多步骤编排:先查数据 → 再分析 → 再生成图表 → 再插入PPT → 再发送
  2. 条件判断:如果股价跌破阈值 → 自动卖出;如果涨超5% → 提醒用户
  3. 错误处理:API挂了怎么办?数据格式不对怎么办?
  4. 长时间运行:可能需要几分钟甚至几小时

解决方案 ------ Skills(技能)

Skills是将多个Tool和Memory操作"蒸馏"(组合封装)成一个可复用的技能单元。比如一个"股市分析Skill"内部可能组合了:

yaml 复制代码
Skill: 股市分析
├── Tool: 获取实时股价 (MCP/API)
├── Tool: 获取历史K线数据 (API)
├── Tool: 技术指标计算 (本地函数)
├── Memory: 记住用户的持仓和风险偏好
└── Tool: 执行交易 (券商API)

2. Agent的核心公式:六大模块解析

围绕以上五大问题,Agent的本质就清晰了:给LLM装上各种"外挂",让它从一个只会"说话"的模型,变成一个能"做事"的智能体。

ini 复制代码
Agent = LLM + Memory + Tool + RAG + MCP + Skills

下面我们逐一深入理解每个模块的定位和作用。

2.1 LLM ------ 大脑

维度 说明
角色 Agent的中央处理器
职责 理解用户意图、分解任务、制定计划、推理判断、生成回复
关键能力 Planning(规划)+ Reasoning(推理)
类比 人的大脑皮层

LLM在Agent中不只是"生成文字"的工具,它是决策中心。面对一个复杂任务(比如"帮我搭建一个博客"),LLM需要:

  1. 理解意图:用户想要什么类型的博客?静态还是动态?有哪些约束?
  2. 分解任务:这个任务应该拆成哪些子步骤?
  3. 选择工具:每一步需要调用哪个Tool?
  4. 处理结果:Tool返回的结果怎么用?下一步是什么?
  5. 生成回复:最终如何向用户汇报?

2.2 Memory ------ 记忆

维度 说明
角色 Agent的记忆系统
职责 存储和检索用户偏好、历史对话、任务上下文
关键能力 持久化存储 + 上下文注入
类比 人的海马体 + 前额叶

Memory模块解决的核心问题是:让LLM的每次调用都能"看到"相关的历史信息。实现上通常分为两个层面:

  • 短期记忆:当前会话的对话历史(存储在Redis或内存中,TTL较短)
  • 长期记忆:用户偏好、重要事实、项目背景(存储在数据库中,持久化)

2.3 Tool ------ 双手

维度 说明
角色 Agent的行动能力
职责 执行具体操作(读写文件、调用API、运行命令、查询数据库)
关键能力 函数调用 + 参数校验 + 结果返回
类比 人的双手和工具

Tool是Agent从"说"到"做"的桥梁。每一个Tool包含两部分:

  1. 处理函数(async function):实际干活的代码
  2. 描述对象:告诉LLM这个工具的用途、参数格式和适用场景

🔑 关键设计:LLM不是被硬编码"何时调用哪个工具"的------它通过Tool的描述对象自己判断。这意味着你可以随时添加新工具,LLM会自动学会使用它们!

2.4 RAG ------ 检索增强生成

维度 说明
角色 Agent的知识获取系统
职责 从外部知识库检索相关信息,注入到LLM的上下文中
关键能力 文档切分 + 向量化 + 相似检索 + Prompt拼接
类比 人的"查资料"能力

RAG的Pipeline:

bash 复制代码
用户问题 → Embedding模型 → 查询向量
                                    ↓
                            向量数据库(Milvus/Pinecone/pgvector)
                                    ↓
                            返回Top-K相关文档片段
                                    ↓
用户问题 + 文档片段 → Prompt Template → LLM → 回答

💡 注意:RAG和Tool的区别在于------RAG是"被动检索"(查资料),Tool是"主动执行"(做事情)。

2.5 MCP ------ 模型上下文协议

维度 说明
角色 标准化的第三方Tool协议
职责 提供统一的Tool接入标准,让任何服务都能被Agent调用
关键能力 Client-Server架构 + 工具发现 + 标准化调用
类比 USB协议(插上就能用)

MCP的价值在于标准化。没有MCP时,每接入一个新服务(天气、新闻、GitHub、数据库......),开发者都要手写适配代码。有了MCP,只需对接一次协议,所有MCP Server都能直接使用。

arduino 复制代码
Agent (MCP Client) ←──MCP协议──→ MCP Server A (天气服务)
                                → MCP Server B (GitHub)
                                → MCP Server C (数据库)
                                → MCP Server D (任意服务...)

2.6 Skills ------ 技能编排

维度 说明
角色 多能力的组合编排
职责 将多个Tool + Memory操作"蒸馏"成可复用的技能单元
关键能力 工作流编排 + 条件逻辑 + 错误处理
类比 人的专业技能(如"做财务分析")

Skills是更高层次的抽象。如果说Tool是"锤子"和"钉子",那Skill就是"做一张桌子"的完整工艺。

2.7 现实中的Agent产品剖析

产品 Agent公式映射
Claude Code LLM(Claude)+ Tool(fs + cli + grep + ...) + Memory(对话历史) + MCP(第三方工具) + Skills(/review, /fix等)
Codex Coding Agent LLM + Tool(代码编辑 + 终端) + Memory(项目上下文) + RAG(文档检索)
Manus LLM + Tool(浏览器操作 + API调用) + Memory(任务状态) + Skills(自动化流程)

💡 核心理念 :Agent其实也不复杂------LLM本身也可以思考、规划,给它用Tool扩展能力,它就能自己做事情了;用Memory管理记忆,它就可以记住你要它记住的东西;还可以用RAG查询内部知识库来获取知识。这样一个知道内部知识、能思考规划、能够帮你做事情的扩展后的大模型,就是一个Agent。


3. Agent的工作流程:从Prompt到任务完成

3.1 完整执行流程

一个典型的Agent任务执行过程是一个循环决策链

markdown 复制代码
用户以Prompt形式提出一个复杂任务
            ↓
    ┌──────────────────────────────┐
    │  LLM Planning / Reasoning    │
    │  (大模型进行规划和推理)       │
    │                              │
    │  判断需要调用哪些模块:        │
    │  ├─ 需要加载 Memory 吗?      │
    │  │  是 → 检索相关历史/偏好    │
    │  ├─ 需要调用 Tool 吗?       │
    │  │  是 → 分步骤调用多个工具   │
    │  └─ 需要走 RAG 检索吗?      │
    │     是 → 查询向量数据库      │
    └──────────────────────────────┘
            ↓
    各模块返回结果,组装上下文
            ↓
    LLM 基于所有上下文生成最终响应
            ↓
    返回 Response 给用户(任务完成)

3.2 LLM的"自知之明":tool_calls机制

这是Agent区别于传统程序最核心的特征 :LLM自己判断"什么时候该调用什么工具",而不是由开发者用 if-else 硬编码。

传统程序的做法(硬编码):

javascript 复制代码
if (userMessage.includes('读文件')) {
    const content = fs.readFileSync(extractPath(userMessage));
    // ...
} else if (userMessage.includes('天气')) {
    const weather = await fetchWeather(extractCity(userMessage));
    // ...
}
// 每新增一个能力,都要加一个 if-else......

Agent的做法(LLM自主决策):

javascript 复制代码
// 只需要注册工具列表,LLM自己判断何时调用、如何调用
const modelWithTools = model.bindTools([readFileTool, weatherTool, emailTool]);
const response = await modelWithTools.invoke(messages);
// LLM的response中要么是文本回复,要么是tool_calls列表

当LLM判断需要调用工具时,它不生成文本回复 ,而是停下来返回 tool_calls

json 复制代码
{
    "role": "assistant",
    "content": null,           // ← 不生成文本!
    "tool_calls": [
        {
            "id": "call_abc123",
            "name": "read_file",
            "arguments": {
                "filePath": "tool.mjs"
            }
        },
        {
            "id": "call_abc124",
            "name": "search_web",  // ← 可能同时调用多个工具!
            "arguments": {
                "query": "LangChain tool use best practices"
            }
        }
    ]
}

3.3 一个具体的执行示例

以用户请求 "请读取 tool.mjs 文件内容并解释代码" 为例:

css 复制代码
Round 1:
  用户消息: "请读取tool.mjs文件内容并解释代码"
      ↓
  LLM推理: 我需要先读到文件内容,才能解释代码
      ↓
  LLM返回 tool_calls: [{ name: "read_file", arguments: { filePath: "tool.mjs" } }]
      ↓
  Agent执行 Tool: fs.readFile("tool.mjs") → 返回代码文本
      ↓
  将 ToolMessage(id="call_abc123", content=代码文本) 追加到消息列表

Round 2:
  消息列表: [SystemMessage, HumanMessage, AIMessage(tool_calls), ToolMessage(代码)]
      ↓
  LLM推理: 我现在有代码内容了,可以开始解释
      ↓
  LLM返回: "这段代码实现了......它首先导入了......然后定义了......"
      ↓
  返回给用户

🔑 关键机制tool_calls 中每个调用的 id 字段用于将工具执行结果(ToolMessage)与调用请求关联。因为Agent可能同时发起多个Tool调用,LLM需要通过 id 知道"哪个结果对应哪个调用",从而组装完整的任务上下文。这正是 Promise.all 并发执行多个Tool的基础。


4. LangChain ------ 为什么选择它作为Agent开发框架

4.1 LangChain的历史定位

LangChain 是一个比 OpenAI 官方 SDK 诞生还早的 LLM 开发框架。当时 OpenAI 还没有推出 function calling,LangChain 就已经在探索"如何让 LLM 调用外部工具"这个问题了。

它的核心价值在于:在LLM应用开发的"蛮荒时代"建立了一套标准化的抽象层,让开发者不需要为每家模型厂商、每种Tool格式重复造轮子。

版本现状(截至项目创建时):LangChain.js v1.x 稳定版,核心包分为:

  • @langchain/core:核心抽象层(Tool、Message、Callback等)
  • @langchain/openai:OpenAI兼容的ChatModel实现
  • @langchain/community:社区贡献的各种集成
  • langgraph:多Agent编排框架

4.2 核心能力一:统一的LLM抽象层

市面上有几十家大模型厂商(OpenAI、DeepSeek、通义千问、文心一言、智谱......),每家API格式都不一样。LangChain 的 ChatOpenAI 类提供统一的接入方式:

javascript 复制代码
import { ChatOpenAI } from '@langchain/openai';

// 切换到 DeepSeek
const model = new ChatOpenAI({
    modelName: 'deepseek-v4-flash',         // 模型名称 → 一行切换
    apiKey: process.env.DEEPSEEK_API_KEY,
    configuration: {
        baseURL: 'https://api.deepseek.com/v1',  // API地址 → 一行切换
    }
});

// 同样的代码,换到通义千问只需修改配置
const qianwenModel = new ChatOpenAI({
    modelName: 'qwen-turbo',
    apiKey: process.env.QWEN_API_KEY,
    configuration: {
        baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
    }
});

设计哲学 :按需加载。你不用的模型,对应的包根本不会被打包进项目。@langchain/openai 就是这样一个"适配器包"。

4.3 核心能力二:标准化的Tool抽象

在 LangChain 诞生之前,手写 OpenAI function calling 的 Tool 定义非常繁琐:

javascript 复制代码
// 原始 OpenAI function calling 格式 ------ 手写JSON Schema
const tools = [{
    type: 'function',
    function: {
        name: 'read_file',
        description: '读取文件内容...',
        parameters: {
            type: 'object',
            properties: {
                filePath: { type: 'string', description: '要读取的文件路径' }
            },
            required: ['filePath']
        }
    }
}];

LangChain 通过 @langchain/core/tools 配合 Zod 将其大大简化:

javascript 复制代码
import { tool } from '@langchain/core/tools';
import { z } from 'zod';

const readFileTool = tool(
    async ({ filePath }) => { /* 实现逻辑 */ },
    {
        name: 'read_file',
        description: '读取文件内容',
        schema: z.object({
            filePath: z.string().describe('要读取的文件路径'),
        }),
    }
);

Zod在这里扮演了"类型契约"的角色------它既是运行时校验,又自动生成了LLM所需的JSON Schema。

4.4 核心能力三:消息类型体系

LangChain 定义了清晰的消息类型体系(来自 @langchain/core/messages):

消息类型 role 方向 用途
HumanMessage user 用户 → LLM 用户的自然语言输入
SystemMessage system 系统 → LLM 系统级指令(角色设定、规则约束)
AIMessage assistant LLM → 用户 LLM的文本回复或tool_calls
ToolMessage tool Tool → LLM 工具执行结果,需要关联 tool_call_id

这种类型体系让消息管理变得类型安全且语义清晰 ------你不再需要手写 { role: 'user', content: '...' } 这样的裸对象。

4.5 全栈Agent技术架构

把 Agent 开发放到一个完整的生产级技术栈中来看:

层次 技术选型 用途 说明
🖥️ 后端框架 NestJS 企业级API服务 依赖注入、模块化、可测试
🤖 单Agent LangChain.js 单智能体开发 Tool/Memory/RAG一站式
🕸️ 多Agent LangGraph 多Agent协作 有状态图、条件分支、循环
🔌 工具协议 MCP 标准化工具接入 Client-Server,即插即用
📚 知识检索 RAG 内部知识库 向量检索 + Prompt注入
🎯 技能编排 Skills 复杂能力组合 多Tool的"蒸馏"封装

🎯 目标 :结合后端技术,开发AI全栈Agent产品,让AI技术通过 Harness Engineering 落地,实现AI技术的商业价值(FDE: Full-Stack Development Engineering)


5. 动手实践:Hello LangChain 项目全解析

理论讲得够多了,下面我们通过 hello-langchain/ 项目来动手实践。这个项目的设计非常精巧------它用最少的代码覆盖了从"裸LLM调用"到"Tool-use Agent"的完整演进路径。

5.1 项目初始化与环境配置

项目结构

bash 复制代码
hello-langchain/
├── .env              # 环境变量(API Key配置)
├── package.json      # 项目元信息与依赖声明
├── pnpm-lock.yaml    # 依赖版本锁文件
├── index.mjs         # 级别一:最简模型调用
├── tool.mjs          # 级别二:Tool-use Agent
└── 1.html            # 基础知识:Promise并发演示

依赖解析

项目的 package.json 声明了四个核心依赖:

json 复制代码
{
    "name": "hello-langchain",
    "version": "1.0.0",
    "dependencies": {
        "@langchain/core": "^1.2.1",     // LangChain核心抽象
        "@langchain/openai": "^1.5.3",   // OpenAI兼容适配器
        "dotenv": "^17.4.2",             // 环境变量加载
        "zod": "^4.4.3"                  // Schema校验
    }
}
依赖 版本 为什么需要它?
@langchain/core ^1.2.1 提供 tool()HumanMessageSystemMessageToolMessageAIMessage 等核心抽象。所有LangChain生态的基础。
@langchain/openai ^1.5.3 提供 ChatOpenAI 类。虽然叫"openai",但它兼容所有OpenAI API格式的服务(DeepSeek、通义千问等)。
dotenv ^17.4.2 .env 文件中的 KEY=VALUE 自动加载到 process.env。避免API Key硬编码在源码中。
zod ^4.4.3 提供 z.object({...}) 式的Schema定义,既是TypeScript类型推断的基础,也是Tool参数校验的运行时保障。

.env 环境配置

.env 文件只包含一行:

ini 复制代码
DEEPSEEK_API_KEY=sk-6220b65165a14b20b0f78272ef13947e

⚠️ 安全提示 :API Key 不应硬编码在源码或 .env 文件中提交到 Git 仓库。生产环境中应使用环境变量注入(如 k8s Secrets、CI/CD Variables)。这里的 Key 仅供本地学习使用。

pnpm-lock.yaml 中的关键传递依赖

less 复制代码
@langchain/openai@1.5.3
  └── openai@6.45.0          ← OpenAI Node.js SDK(HTTP客户端)
        └── ...               ← 实际的API通信层

@langchain/core@1.2.1
  ├── @cfworker/json-schema@4.1.1   ← JSON Schema校验
  ├── js-tiktoken@1.0.21           ← Token计数
  ├── langsmith@0.7.15             ← 调试追踪(LangSmith平台)
  ├── p-queue@6.6.2                ← 并发队列管理
  └── zod@4.4.3                    ← Schema定义

💡 注意 p-queue:这是LangChain内部用于管理并发Tool执行的队列库------和我们要讲的 Promise.all 天然关联。

5.2 级别一:最简模型调用

这是Agent开发的"Hello World"------用最少的代码验证整个链路是否通畅:

javascript 复制代码
import { ChatOpenAI } from '@langchain/openai';
import 'dotenv/config';        // ← 副作用导入,自动执行dotenv.config()

const model = new ChatOpenAI({
    modelName: 'deepseek-v4-flash',           // 使用DeepSeek的快速模型
    apiKey: process.env.DEEPSEEK_API_KEY,      // 从.env加载
    configuration: {
        baseURL: 'https://api.deepseek.com/v1', // DeepSeek的OpenAI兼容端点
    }
});

const response = await model.invoke('台球赛比赛奖励是什么?');

console.log(response.content);

🎯 这个文件的价值:它验证了整个技术栈------Node.js运行时 → npm依赖安装正确 → API Key有效 → 网络能连通DeepSeek → 模型能正常响应。在开始复杂的Agent开发之前,确保这个"Hello World"能跑通是最佳的调试策略。

5.3 级别二:Tool-use Agent

这是整个项目的核心,它展示了如何给LLM装上"双手",让它能真正干活。

分段深度解析

第一段:导入区
javascript 复制代码
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { tool } from '@langchain/core/tools';
import {
    HumanMessage,    // role: 'user'     → 用户的输入
    SystemMessage,   // role: 'system'   → 系统指令/角色设定
    ToolMessage,     // role: 'tool'     → 工具执行结果
    AIMessage        // role: 'assistant' → LLM的回复(文本或tool_calls)
} from '@langchain/core/messages';
import fs from 'node:fs/promises';
import { z } from 'zod';

这里导入了四种消息类型------它们共同构成了Agent对话的完整消息生命周期

css 复制代码
SystemMessage ──→ 设定角色,一次性注入(不参与多轮对话的用户输入)
HumanMessage  ──→ 用户问题,每次对话的起点
AIMessage     ──→ LLM回复:要么是文本(content),要么是tool_calls
ToolMessage   ──→ 工具执行结果,通过tool_call_id关联到具体的AIMessage.tool_calls[i].id

fsnode:fs/promises 导入,注意 node: 前缀------这是Node.js内置模块的标准写法,确保不会被npm包劫持。fs/promises 提供Promise-based的文件API(readFilewriteFile等),返回Promise,天然支持 await

第二段:模型初始化
javascript 复制代码
const model = new ChatOpenAI({
    modelName: 'deepseek-v4-flash',
    apiKey: process.env.DEEPSEEK_API_KEY,
    temperature: 0,                    // ← 新增!设为0让输出更确定
    configuration: {
        baseURL: 'https://api.deepseek.com/v1',
    }
});

这里多了 temperature: 0 参数:

temperature值 效果 适用场景
0 确定性输出,每次结果基本相同 Tool调用(需要稳定性和可预测性)
0.5 适度的创造性 一般对话
1.0 高随机性 创意写作

在Tool-use场景中,temperature: 0 是最佳实践------你希望LLM稳定地选择正确的工具和参数,而不是每次随机选不同的工具。

第三段:Tool定义 ------ 本文最核心的代码
javascript 复制代码
const readFileTool = tool(
    // ─────────── 第一部分:异步处理函数 ───────────
    async ({ filePath }) => {
        // fs.readFile是异步的,返回Promise<string>
        const content = await fs.readFile(filePath, 'utf-8');

        // 实时反馈给用户------Agent任务可能很复杂、很耗时
        // 用户如果太久没看到反馈,可能会以为程序卡死了
        console.log(`[工具调用] read_file (${filePath})
            成功读取${content.length}字节`);

        return content;  // 返回值会作为ToolMessage的content
    },

    // ─────────── 第二部分:函数描述对象 ───────────
    {
        name: 'read_file',          // 工具的唯一标识名
        description: `用此工具来读取文件内容,
        当用户要求读取文件、查看代码、分析文件内容时,
        调用此工具。输入文件路径(可以是相对路径或绝对路径)`,
        schema: z.object({
            filePath: z.string().describe('要读取的文件路径'),
        }),
    }
);

这段代码展示了LangChain Tool定义的两部分结构,这也是所有Tool的本质:

部分 类型 面向谁? 作用
处理函数 async (args) => result 面向运行时 实际执行操作,返回结果
描述对象 { name, description, schema } 面向LLM 告诉LLM"我能做什么、何时用、需要什么参数"

处理函数的细节

javascript 复制代码
async ({ filePath }) => {    // ← 解构参数,参数名必须和schema中定义的一致
    const content = await fs.readFile(filePath, 'utf-8');
    console.log(`[工具调用] ...`);  // ← 这是Agent开发的重要实践
    return content;
}
  • console.log 不是可有可无的装饰------它是用户反馈机制。Agent执行复杂任务时可能需要几十秒甚至几分钟,用户如果看不到任何进度,会以为程序崩溃了
  • return content 的值将作为 ToolMessagecontent 字段传回给LLM

描述对象的设计艺术

javascript 复制代码
{
    name: 'read_file',      // 简短、准确、英文(与LLM的训练数据一致)
    description: `用此工具来读取文件内容,
    当用户要求读取文件、查看代码、分析文件内容时,调用此工具。
    输入文件路径(可以是相对路径或绝对路径)`,
    schema: z.object({
        filePath: z.string().describe('要读取的文件路径'),
    }),
}

description 的写法至关重要------它相当于给LLM看的API文档。一个好的 description 应该包含:

  1. 功能说明:这个工具做什么?
  2. 触发条件:什么时候应该调用它?("当用户要求读取文件、查看代码、分析文件内容时")
  3. 参数说明 :需要什么参数?格式是什么?(通过schema中的 .describe() 补充)

description 写得好不好,直接影响LLM能否正确判断"是否该用这个工具"。如果 description 过于模糊,LLM可能在本该调用工具时生成文本回复;如果过于宽泛,LLM可能在不该调用时也触发工具。

Zod Schema的角色

javascript 复制代码
schema: z.object({
    filePath: z.string().describe('要读取的文件路径'),
})

LangChain内部会将这个Zod schema转换为OpenAI function calling所需的JSON Schema格式:

json 复制代码
{
    "type": "object",
    "properties": {
        "filePath": { "type": "string", "description": "要读取的文件路径" }
    },
    "required": ["filePath"]
}

Zod在这里提供了双重价值

  • 编译时:TypeScript类型推断
  • 运行时:参数校验(如果LLM传入的参数不符合schema,Zod会抛出明确的错误)
第四段:Tool注册------bindTools
javascript 复制代码
// 将工具放入数组------可以放多个
const tools = [
    readFileTool
    // 未来可以添加更多工具:
    // writeFileTool,
    // executeCommandTool,
    // searchWebTool,
];

// bindTools 是LangChain提供的核心方法
// 它将Tool列表转换为LLM能理解的function calling格式
const modelWithTools = model.bindTools(tools);

model.bindTools(tools) 是LangChain的魔法所在。它做了什么?

  1. 遍历 tools 数组中的每个Tool
  2. 将每个Tool的描述对象转换为OpenAI function calling格式
  3. 将所有工具定义注入到后续每次 invoke() 请求的 tools 参数中

效果 :之后每次调用 modelWithTools.invoke(messages),LLM在生成回复前都会"看到"所有可用工具的列表,并自行判断是否需要调用工具。

bindTools 返回的是一个新的模型实例 (包装后的),原始的 model 不受影响。这意味着你可以:

  • model.invoke() → 不带工具的纯对话
  • modelWithTools.invoke() → 带工具的Agent模式
第五段:消息构建
javascript 复制代码
const messages = [
    new SystemMessage(`
        你是一个代码助手,可以使用工具读取文件并解释代码。

        工作流程:
        1. 用户要求读取文件时,立即调用read_file工具。
        2. 等待工具返回文件内容。
        3. 基于文件内容进行分析和解释。

        可用工具:
        - read_file: 读取文件内容(使用此工具来读取文件内容)
    `),
    new HumanMessage('请读取tool.mjs文件内容并解释代码'),
];

SystemMessage(系统消息):这是Agent的"角色设定"。它告诉LLM:

  • 你是什么角色(代码助手)
  • 你有哪些能力(可以读取文件)
  • 你的工作流程是什么(先读文件,再分析)
  • 有哪些工具可用(read_file)

SystemMessage的写法没有标准答案,但有几个经验法则:

  • 明确角色:"你是一个XXX" 开头
  • 说明能力边界:能做什么、不能做什么
  • 规定工作流程:分步骤说明
  • 列出可用工具:方便LLM快速判断

HumanMessage(用户消息):"请读取tool.mjs文件内容并解释代码"------这是一个典型的"需要Tool"的请求,因为LLM不读文件就看不到代码。

第六段:执行区
javascript 复制代码
let response = await modelWithTools.invoke(messages);
messages.push(response);  // 将AI回复加入对话历史
// 多个工具 执行 例如:await read  await write 并发?

最后一行注释是项目作者留下的思考------如何处理多个Tool的并发执行?

这正是下一节要深入探讨的问题。如果LLM返回了两个 tool_calls(读文件A和读文件B),我们是否应该串行 await?还是用 Promise.all 并发执行?答案是显而易见的------这就是高性能Agent的关键。

5.4 Tool调用的消息流转全流程

为了让你彻底理解消息在Agent系统中的流转,下面是完整的时序图:

css 复制代码
时间线 →

① 用户发起请求
   messages = [SystemMessage, HumanMessage("请读取tool.mjs...")]
                    ↓
② modelWithTools.invoke(messages)
   LangChain内部:将tools定义+ messages一起发给DeepSeek API
                    ↓
③ LLM推理:我需要调read_file工具
   LLM返回 AIMessage {
       content: null,
       tool_calls: [{
           id: "call_abc123",
           name: "read_file",
           arguments: { filePath: "tool.mjs" }
       }]
   }
                    ↓
④ messages.push(AIMessage) → 对话历史现在有3条消息
                    ↓
⑤ Agent检查 response.tool_calls
   发现需要调用工具 → 执行 readFileTool({ filePath: "tool.mjs" })
   工具返回: "import 'dotenv/config'; ...(代码全文)..."
                    ↓
⑥ 创建 ToolMessage {
       role: "tool",
       content: "import 'dotenv/config'; ...(代码全文)...",
       tool_call_id: "call_abc123"   ← 关联到第③步的id
   }
   messages.push(ToolMessage) → 对话历史现在有4条消息
                    ↓
⑦ modelWithTools.invoke(messages) → 第二次调用LLM
   LLM现在看到了完整的上下文:
   - SystemMessage(角色设定)
   - HumanMessage(用户问题)
   - AIMessage(之前的tool_calls)
   - ToolMessage(工具返回的文件内容)  ← LLM现在可以读代码了!
                    ↓
⑧ LLM生成回复 AIMessage {
       content: "这段代码实现了一个基于LangChain的Tool-use Agent。
                它首先导入了ChatOpenAI模型适配器..."
   }
                    ↓
⑨ 返回给用户

🔑 关键洞察 :整个过程中,LLM被调用了两次(第②步和第⑦步),但用户只感知到一次交互。这就是Agent的"自主循环"------它自己判断是否需要继续调用工具,直到能给出最终回答。


6. Tool性能优化:Promise并发模型深度解析

6.1 为什么Agent需要并发?

在真实的Agent应用场景中,LLM经常需要同时调用多个Tool

  • 用户:"对比一下两个文件的区别" → 需要同时读2个文件
  • 用户:"搜索关于LangChain的最新文章并总结" → 需要同时搜索多个关键词
  • 用户:"查一下这三个城市的天气" → 需要同时调用3次天气API

如果串行执行,比如读3个文件(每个500ms):

scss 复制代码
读文件A (500ms) → 读文件B (500ms) → 读文件C (500ms) = 1500ms

如果使用并发:

scss 复制代码
Promise.all([读文件A, 读文件B, 读文件C]) = max(500ms, 500ms, 500ms) = 500ms

3倍速度提升,且Tool数量越多,收益越大。

6.2 Promise基础:三种状态与不可变性

在深入实战代码之前,先回顾Promise的核心概念。

Promise 是ES6(ECMAScript 2015)引入的异步编程基础设施。它代表一个未来会完成的操作,有三种互斥的状态:

scss 复制代码
                    ┌─────────────┐
                    │   Pending   │  初始状态:操作进行中
                    │   (等待中)   │
                    └──────┬──────┘
                           │
              ┌────────────┼────────────┐
              │                         │
              ▼                         ▼
      ┌──────────────┐          ┌──────────────┐
      │  Fulfilled   │          │   Rejected   │
      │   (已成功)    │          │   (已失败)    │
      └──────────────┘          └──────────────┘
        resolve()                  reject()

Promise不可变性的核心保证

⚠️ Promise状态只能从Pending转换到Fulfilled或Rejected之一 ,一旦转换就永远锁死,不能再变。

这意味着:

  • 你不能"取消"一个已经成功的Promise
  • 你不能"修复"一个已经失败的Promise
  • 你只能在Promise完成之后 通过 .then() / .catch() / await 来处理结果

这不是Bug,而是设计特性------它保证了异步操作的确定性,避免了"回调地狱"中的状态混乱。

6.3 串行 vs 并行:实战对比

下面用两个模拟的异步任务直观展示串行和并行的性能差异。这些代码可以直接在浏览器中运行验证。

模拟函数定义

javascript 复制代码
// 模拟天气查询 ------ 耗时2000ms(模拟慢速API)
function getWeather() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve({ temp: 38, conditions: 'Sunny with Clouds' });
        }, 2000);
    });
}

// 模拟推文查询 ------ 耗时500ms(模拟快速API)
function getTweets() {
    return new Promise((resolve) => {
        setTimeout(() => {
            resolve(['I like cake', 'BBQ is good too!']);
        }, 500);
    });
}

这两个函数的设计非常巧妙:

  • getWeather() 耗时2000ms → 模拟慢速Tool(如大文件读取、远程API调用)
  • getTweets() 耗时500ms → 模拟快速Tool(如缓存查询、本地文件读取)

它们的耗时差异(2000ms vs 500ms)让性能对比更加直观。

方式一:串行执行(❌ 反模式)

javascript 复制代码
async function main() {
    console.time("my-operation");

    // 第一个await:启动getWeather()的Promise,等待2000ms
    const weatherData = await getWeather();
    // ↑ 上面这一行不完成,永远不会执行下面这行
    const tweetsData = await getTweets();
    // ↑ 再等500ms

    console.log(weatherData);   // { temp: 38, conditions: 'Sunny with Clouds' }
    console.log(tweetsData);    // ['I like cake', 'BBQ is good too!']
    console.timeEnd("my-operation");  // 输出: my-operation: ~2500ms
}

串行执行的时间线

ini 复制代码
0ms ──────────────────────── 2000ms ──────── 2500ms
│                              │               │
└─ getWeather() 开始           └─ getWeather 完成
                               └─ getTweets 开始  └─ getTweets 完成
                                                     └─ 打印结果

总耗时 = 2000ms + 500ms = 2500ms

问题很明显:getTweets() 完全不依赖 getWeather() 的结果,但它必须干等2000ms才能开始。这是对时间的纯粹浪费

方式二:并行执行(✅ 最佳实践)

javascript 复制代码
const main = async () => {
    console.time("my-operation");

    // Promise.all 同时启动两个Promise
    const [weatherData, tweetsData] =
        await Promise.all([getWeather(), getTweets()]);
    //    ↑ 解构赋值,顺序与数组中的顺序一致

    console.log(weatherData);   // { temp: 38, conditions: 'Sunny with Clouds' }
    console.log(tweetsData);    // ['I like cake', 'BBQ is good too!']
    console.timeEnd("my-operation");  // 输出: my-operation: ~2000ms
};

main();

并行执行的时间线

scss 复制代码
0ms ───── 500ms ──────────────────────── 2000ms
│           │                              │
├─ getWeather() 开始 ─────────────────────┘ getWeather 完成
└─ getTweets() 开始 ──┘ getTweets 完成     └─ 打印结果(等getWeather也完成)

总耗时 = max(2000ms, 500ms) = 2000ms

性能对比

执行方式 耗时 节省
串行(await逐个) 2500ms -
并行(Promise.all) 2000ms 500ms(20%)

两个Tool就节省了20%,如果你有5个Tool(每个500ms),串行需要2500ms,并行只需要500ms------5倍差距

关于代码风格

值得注意的细节------main函数的两种写法:

javascript 复制代码
// 写法一:async function 声明
async function main() {
    const weatherData = await getWeather();
    const tweetsData = await getTweets();
}

// 写法二:箭头函数 + const
const main = async () => {
    const [weatherData, tweetsData] =
        await Promise.all([getWeather(), getTweets()]);
};

这两种写法在功能上等价,但社区偏好箭头函数 + const 的组合------更简洁、更符合现代JavaScript风格。

6.4 async/await:异步变同步的语法糖

async/await 是ES8(ECMAScript 2017)引入的语法,它是目前最优雅的异步编程方式

javascript 复制代码
// 没有 async/await 的时代 ------ "回调地狱"
getWeather()
    .then(weatherData => {
        return getTweets().then(tweetsData => {
            console.log(weatherData, tweetsData);
        });
    });

// 有了 async/await ------ 看起来像同步代码,但本质仍是异步
const weatherData = await getWeather();
const tweetsData = await getTweets();
console.log(weatherData, tweetsData);

await 的语义:

  • 等待一个Promise完成(Fulfilled或Rejected)
  • 如果Fulfilled:返回resolve的值(赋给 = 左边)
  • 如果Rejected:抛出一个异常(可以用 try/catch 捕获)
  • 不阻塞JavaScript主线程------它只暂停当前async函数的执行,事件循环继续运转

⚠️ 常见误区await 让代码"看起来"同步了,但Promise的异步本质没变。await 只是语法糖------底层仍是 .then() 回调。

6.5 Promise.all在Agent中的实战应用

回到Agent场景,当LLM返回多个 tool_calls 时:

javascript 复制代码
// LLM的回复中有两个tool_calls
const response = await modelWithTools.invoke(messages);
// response.tool_calls = [
//     { id: 'call_1', name: 'read_file', arguments: { filePath: 'tool.mjs' } },
//     { id: 'call_2', name: 'read_file', arguments: { filePath: 'index.mjs' } },
// ]

// 串行执行(❌ 低效)
for (const tc of response.tool_calls) {
    const result = await executeTool(tc.name, tc.arguments);
    messages.push(new ToolMessage({
        content: result,
        tool_call_id: tc.id,
    }));
}

// 并行执行(✅ 高效)
const toolResults = await Promise.all(
    response.tool_calls.map(tc =>
        executeTool(tc.name, tc.arguments).then(result => ({
            id: tc.id,        // 必须保留id!LLM用它关联结果
            content: result,
        }))
    )
);
// 按id将结果组装成ToolMessage
toolResults.forEach(({ id, content }) => {
    messages.push(new ToolMessage({ content, tool_call_id: id }));
});

// 所有工具执行完毕后,再次调用LLM
const finalResponse = await modelWithTools.invoke(messages);

这和我们之前讲的 Promise.all 完全一致:

  • 各个Tool调用独立,互不依赖 → 可以并行
  • Promise.all 等待所有完成 → 结果顺序与 tool_calls 数组一致
  • 通过 id 字段保持结果与调用的关联 → LLM能正确组装上下文

🚀 这就是打造高性能Agent的关键技术------用并发替代串行,让多个独立工具同时执行,显著缩短总响应时间。


7. 架构实战:手写一个简易版Claude Code Agent

7.1 核心公式

理解了以上所有概念后,我们就可以设计一个简化版的Claude Code Agent:

ini 复制代码
简易Agent = LLM + Tool(fs + cli)
  • fs(文件系统):读文件、写文件、列目录、删除文件等
  • cli(命令行):执行任意Shell命令、启动服务、安装依赖等

这已经是Claude Code的核心能力子集。Claude Code在此基础上还有grep搜索、LSP代码分析、Git操作等更多Tool,但架构模式完全一样

7.2 Demo场景:创建一个React+Vite的TodoList

以用户输入 "创建一个React+Vite的TodoList" 为例,Agent需要做Planning(规划)

yaml 复制代码
用户输入: "创建一个React+Vite的TodoList"
            ↓
    ┌──────────────────────────────┐
    │      LLM Planning 规划        │
    │                              │
    │  判断这是一个编程任务,分解为: │
    │                              │
    │  Step 1: 用Vite创建项目骨架   │
    │    → Tool: CLI (npm create vite)│
    │                              │
    │  Step 2: 生成TodoList代码      │
    │    → Tool: File (writeFile)  │
    │    → 需要使用编程能力强的模型    │
    │                              │
    │  Step 3: 启动项目验证          │
    │    → Tool: CLI (npm run dev) │
    └──────────────────────────────┘

这个Planning过程体现了LLM的推理能力------它理解"创建React+Vite项目"意味着需要执行哪些命令行操作,并且知道步骤之间的依赖关系(必须先创建项目 → 才能写代码 → 才能启动)。

7.3 完整架构设计

yaml 复制代码
┌──────────────────────────────────────────────────────┐
│                    简易 Claude Code Agent              │
│                                                      │
│  ┌──────────────────────────────────────────────┐    │
│  │              LLM (DeepSeek/Claude)            │    │
│  │                                               │    │
│  │  • Planning: 分解任务为Tool调用序列             │    │
│  │  • Reasoning: 判断Tool结果,决策下一步          │    │
│  │  • Code Generation: 基于上下文编写代码          │    │
│  └──────────────────┬───────────────────────────┘    │
│                     │                                │
│         ┌───────────┼───────────┐                    │
│         │                       │                    │
│         ▼                       ▼                    │
│  ┌─────────────┐        ┌─────────────┐             │
│  │  FS Tools   │        │  CLI Tools  │             │
│  ├─────────────┤        ├─────────────┤             │
│  │ readFile    │        │ execCommand │             │
│  │ writeFile   │        │ spawnProcess│             │
│  │ listDir     │        │ killProcess │             │
│  │ deleteFile  │        │             │             │
│  └─────────────┘        └─────────────┘             │
│         │                       │                    │
│         ▼                       ▼                    │
│  ┌──────────────────────────────────────────────┐    │
│  │           Operating System                   │    │
│  │         (真实的文件系统和Shell)                │    │
│  └──────────────────────────────────────────────┘    │
│                                                      │
│  ┌──────────────────────────────────────────────┐    │
│  │              Message History                  │    │
│  │  [SystemMsg, HumanMsg, AIMsg, ToolMsg, ...]  │    │
│  │         ↑ Memory: 管理对话上下文               │    │
│  └──────────────────────────────────────────────┘    │
└──────────────────────────────────────────────────────┘

7.4 关键设计决策

在设计Agent系统时,有几个架构决策值得深入思考:

决策一:何时用串行,何时用并行?

javascript 复制代码
// 用例1:互相独立的Tool → Promise.all 并行
// 用户:"同时读A.mjs和B.mjs"
Promise.all([readFile('A.mjs'), readFile('B.mjs')]);

// 用例2:有依赖关系的Tool → 必须串行
// Step 1 的输出是 Step 2 的输入
await writeFile('App.jsx', generatedCode);  // 先写
await execCommand('npm run dev');            // 再启动(依赖文件存在)

决策二:任务反馈如何设计?

javascript 复制代码
// 工具执行时实时反馈
console.log(`[工具调用] read_file (${filePath}) 成功读取${content.length}字节`);
console.log(`[工具调用] write_file (${filePath}) 已写入${code.length}字符`);
console.log(`[工具调用] exec_command (${cmd}) 正在执行...`);

// 原因:Agent任务可能很复杂、很耗时,用户太久没看到反馈会退出

决策三:如何管理消息列表?

javascript 复制代码
const messages = [systemMessage, ...historyMessages, userMessage];

while (true) {
    const response = await modelWithTools.invoke(messages);
    messages.push(response);  // 维护完整的对话历史

    if (response.tool_calls && response.tool_calls.length > 0) {
        // 并行执行所有工具调用
        const results = await Promise.all(
            response.tool_calls.map(tc => executeTool(tc))
        );
        // 将结果追加到消息列表
        results.forEach(r => messages.push(r));
        // 继续循环,让LLM处理结果
    } else {
        // 没有tool_calls → LLM给出了最终回答
        return response.content;
    }
}

这个 while 循环就是Agent的核心执行引擎------它会自动循环,直到LLM不再请求工具调用。


8. 总结与展望

8.1 核心知识体系回顾

scss 复制代码
                            ┌──────────────────────────────────┐
                            │          AI Agent 全景图           │
                            │                                   │
                            │    ┌─────────────────────────┐    │
                            │    │   LLM (大脑/决策中心)     │    │
                            │    │   • 理解意图              │    │
                            │    │   • 规划任务 (Planning)    │    │
                            │    │   • 推理判断 (Reasoning)   │    │
                            │    │   • 生成回复 (Generation)  │    │
                            │    └────────────┬────────────┘    │
                            │                 │                 │
                            │     ┌───────────┼───────────┐     │
                            │     │           │           │     │
                            │     ▼           ▼           ▼     │
                            │  ┌──────┐  ┌──────┐  ┌──────┐    │
                            │  │Memory│  │ Tool │  │ RAG  │    │
                            │  │ 记忆  │  │ 工具  │  │ 检索  │   │
                            │  └──────┘  └──────┘  └──────┘    │
                            │  ┌──────┐  ┌──────┐              │
                            │  │ MCP  │  │Skills│              │
                            │  │ 协议  │  │ 技能  │              │
                            │  └──────┘  └──────┘              │
                            │                                   │
                            │  开发框架: LangChain + LangGraph    │
                            │  后端框架: NestJS                   │
                            │  并发优化: Promise.all              │
                            └──────────────────────────────────┘

8.2 从本文你可以带走的6项能力

# 能力 具体收获
1 Agent核心概念 深刻理解 Agent = LLM + Memory + Tool + RAG + MCP + Skills,每个模块解决LLM的一个根本局限
2 Agent工作流程 掌握 用户Prompt → LLM规划推理 → 模块调用 → 组装上下文 → 响应的完整执行链路
3 tool_calls机制 理解LLM的"自知之明"------它自己判断何时调用工具、如何调用,而非硬编码
4 LangChain框架 掌握 ChatOpenAI统一接口 + tool()抽象 + bindTools注册 + 四种消息类型的完整用法
5 Promise并发优化 理解Promise三状态模型、串行vs并行的性能差异、Promise.all在Agent Tool调用中的实战应用
6 架构思维 能设计简易Claude Code Agent,理解LLM + Tool(fs+cli) + 并发执行的底层原理

8.3 下一步学习路线图

掌握了单Agent的开发基础后,你可以沿着以下路线继续深入:

arduino 复制代码
当前阶段:单Agent开发 (LangChain.js)
    │
    ├──→ LangGraph:多Agent协作编排
    │     • 有状态图的Agent工作流
    │     • 条件分支、循环、并行
    │     • Agent间的消息传递
    │
    ├──→ MCP协议深入:标准化工具生态
    │     • 自己实现一个MCP Server
    │     • 接入社区MCP工具市场
    │     • 理解MCP的Client-Server架构
    │
    ├──→ RAG实战:企业知识库
    │     • Embedding模型选型
    │     • 向量数据库(Milvus/Pinecone/pgvector)
    │     • Chunking策略与检索优化
    │
    └──→ NestJS集成:生产级Agent服务
          • Agent能力封装为REST API
          • 用户认证与多租户
          • 监控、日志、限流

🚀 Agent开发是当下最值钱的AI工程化方向 。结合后端技术(NestJS),开发AI全栈Agent产品,让AI技术通过 Harness Engineering 落地,实现AI技术的商业价值(FDE)


相关推荐
前端双越老师2 小时前
我多年积累的 100+ 简历优化经验,整理成了 Skill
面试·agent
先吃饱再说2 小时前
LangChain 到底帮你干了什么活?——Agent 开发框架完全解读
langchain·llm·agent
用户2930750976692 小时前
拆解 AI Agent:用 LangChain 构建你的第一个智能工具
agent
Darling噜啦啦2 小时前
从零手写 AI Agent:LLM + Tool + LangChain,打造能干活的大模型
langchain·llm·agent
_冷眸_2 小时前
用 OpenAI SDK 统一调用 Claude、Gemini、DeepSeek:聊聊 AI API 聚合网关的实践
人工智能·ai·llm·agent
梦想4812 小时前
三次迭代,把一个本地检索引擎的排序准确率从 77.8% 做到 100%
agent
文言一心2 小时前
业务数据对话查询智能体系统
langchain·agent
武子康2 小时前
调查研究-217 Fortress:当浏览器 Agent 开始被网站识别,开源 Chromium 反拦截引擎来了
人工智能·爬虫·agent
新知图书3 小时前
多模态智能体开发的核心挑战与解决方案
人工智能·agent·多模态·ai agent·智能体·langgraph