LangChain 到底帮你干了什么活?------Agent 开发框架完全解读
摘要:Agent 开发很火,但手写一个 Agent 需要处理循环调度、记忆管理、输出解析等大量底层逻辑。LangChain 把这些脏活累活全包了。本文从 Agent 的核心组件出发,拆解 LangChain 如何标准化 Tool 编写、封装 ReAct 循环,并深入异步并发优化多 Tool 调用性能。
📑 目录
- 为什么需要 Agent?LLM 的五大缺陷
- Agent 是什么?一个"能干活"的 LLM
- 手写 Agent 有多麻烦?
- LangChain:Agent 开发的"标准化引擎"
- Tool 编写实战:用 LangChain 封装文件读取工具
- Tool 的返回格式与 ID 关联机制
- 异步并发:Agent 高性能的关键
- Promise 状态与并发控制
- 一点总结
- 互动讨论
为什么需要 Agent?LLM 的五大缺陷
大模型很强,但它有几个天生的缺陷,如果不解决,它永远只能当"聊天机器人",干不了正事:
| LLM 缺陷 | 问题描述 |
|---|---|
| 无状态 | 每次对话结束就忘光,记不住项目规范、用户偏好 |
| 无法操作外部世界 | 只能生成文字,不能读文件、不能执行命令、不能操作浏览器 |
| 预训练知识不含私有数据 | 公司的内部文档、产品的技术架构,LLM 一概不知 |
| 不知道最新信息 | 训练数据有截止日期,最新发生的事情它不知道 |
| 复杂任务无法自动完成 | 只能一问一答,不能自主规划多步骤任务 |
Agent = LLM + Memory + Tool + RAG + MCP + Skills
Agent 就是围绕以上问题,给 LLM 加上记忆模块、工具调用能力、RAG、MCP、Skills 等组件,让 LLM 从一个"只会说话"的模型,进化成一个"能干活"的智能体。
Agent 是什么?一个"能干活"的 LLM
Agent(智能体)的工作流程是这样的:
text
markdown
用户以 Prompt 形式提出一个任务(复杂的)
↓
LLM 进行规划/推理(Planning/Reasoning)
↓
要不要加载 Memory?
↓
需不需要调用工具?(可分步骤调用多个工具)
↓
要不要查询 RAG?
↓
生成 Response 返回给用户
在这个过程中,LLM 需要:
- 理解用户的复杂任务
- 拆解成可执行的步骤
- 决定调用哪些工具、按什么顺序调用
- 根据工具返回的结果继续推理
- 最终生成答案
看起来很简单,但手写一个 Agent 非常麻烦。
手写 Agent 有多麻烦?
如果不用任何框架,自己实现一个 Agent,你需要处理:
| 需要手写的内容 | 复杂度 |
|---|---|
手写 while 循环管理"思考→行动→观察"迭代 |
容易出 bug,难以控制迭代次数 |
| 手动拼接 System Prompt,包含工具描述 | Prompt 模板需要反复调优 |
| 用正则从 LLM 文本中提取工具调用指令 | 脆弱,LLM 输出格式稍有偏差就解析失败 |
| 手动管理对话历史,防止上下文溢出 | 记忆管理复杂 |
| 处理多个 Tool 的并发调用 | 需要手动管理 Promise 和错误处理 |
这就是 LangChain 的用武之地。
LangChain:Agent 开发的"标准化引擎"
LangChain 是一个 LLM 应用开发框架,比 OpenAI 的 SDK 更早诞生。它的核心价值不是"简化代码",而是标准化 Agent 的开发流程。
LangChain 封装的复杂内容
| 原生实现(自己造轮子) | LangChain 提供的标准化组件 |
|---|---|
| 手写循环管理迭代 | AgentExecutor 内置标准循环,自动处理最大迭代次数、提前终止 |
| 手动拼接 System Prompt | 自动生成经过验证的 Prompt 模板 |
| 正则提取工具调用 | 自带 OutputParser,精准解析成结构化对象 |
| 手动管理对话历史 | 内置多种记忆机制(BufferMemory / SummaryMemory) |
| 手动处理多 Tool 并发 | 配合 Promise.all 轻松实现并行调用 |
LangChain 的兼容性
LangChain 提供了兼容各家大模型的抽象层。无论你用 OpenAI、DeepSeek、Claude 还是其他模型,都可以用同样的 API:
javascript
arduino
import { ChatOpenAI } from '@langchain/openai';
import 'dotenv/config';
const model = new ChatOpenAI({
modelName: process.env.DEEPSEEK_MODEL_FLASH,
apiKey: process.env.DEEPSEEK_API_KEY,
configuration: {
baseURL: process.env.DEEPSEEK_API_BASE_URL,
}
});
const response = await model.invoke('简单介绍一下足球比赛的规则');
console.log(response.content);
你只需要换一个 modelName 和 baseURL,其他代码完全不用改。
Tool 编写实战:用 LangChain 封装文件读取工具
LangChain 把 Tool 的编写抽象为两个紧密相连的部分,代码可读性更强,编写效率也更高。
javascript
javascript
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
import { tool } from '@langchain/core/tools';
import { HumanMessage, SystemMessage } from '@langchain/core/messages';
import { z } from 'zod';
import fs from 'node:fs/promises';
// 1. 实例化模型
const model = new ChatOpenAI({
modelName: process.env.DEEPSEEK_MODEL_FLASH,
apiKey: process.env.DEEPSEEK_API_KEY,
temperature: 0,
configuration: {
baseURL: process.env.DEEPSEEK_API_BASE_URL,
}
});
// 2. 用 LangChain 的 tool() 函数封装
const readFileTool = tool(
async ({ FilePath }) => { // ① 处理函数:实际干活的
const content = await fs.readFile(FilePath, 'utf-8');
console.log(`[工具调用] read_file(${FilePath}) 成功读取 ${content.length}字节`);
return content;
},
{ // ② 描述对象:告诉 LLM 这个工具是干嘛的
name: 'read_file',
description: '此工具用来读取文件内容,当用户要求读取文件、查看代码、分析文件内容时,调用此工具。',
schema: z.object({
FilePath: z.string().describe('需要读取的文件的文件地址')
})
}
);
// 3. 多个工具打包给 LLM
const tools = [readFileTool];
// 4. 绑定工具到模型
const modelWithTools = model.bindTools(tools);
两个部分缺一不可:
- 处理函数:实际执行业务逻辑,读取文件、查询数据库、调用 API。
- 描述对象 :包含
name、description、schema,让 LLM 知道这个工具能做什么、需要什么参数。
LangChain 内部会自动将 Zod Schema 转换成 LLM 能理解的 JSON Schema,你不需要手写冗长的 JSON 格式。
Tool 的返回格式与 ID 关联机制
当 LLM 决定调用工具时,它会返回一个 tool_calls 数组:
json
bash
{
"tool_calls": [
{
"id": "call_abc123",
"function": {
"name": "read_file",
"arguments": "{"FilePath": "./tool.mjs"}"
}
}
]
}
多个工具调用时,每个调用都有一个唯一的 id。由于工具函数是异步的,执行顺序和返回顺序可能不一致,必须通过 id 来关联请求和响应,才能组成完整、正确的上下文。
javascript
php
// 将 LLM 的响应加入对话历史
messages.push(response);
// 执行工具调用
for (const toolCall of response.tool_calls) {
const result = await executeTool(toolCall.function.name, toolCall.function.arguments);
// 用 tool_call_id 关联结果
messages.push(new ToolMessage({
content: result,
tool_call_id: toolCall.id
}));
}
只有这样,LLM 才能在后续对话中知道"哪个结果对应哪个请求",从而正确地将返回结果通过对话告知用户。
异步并发:Agent 高性能的关键
Agent 任务复杂,可能需要调用多个 Tool,或同一个 Tool 多次。如果串行执行,总耗时是所有 Tool 耗时之和。
顺序执行(串行)
javascript
csharp
const weather = await getWeather(); // 等待 2s
const tweets = await getTweets(); // 再等待 0.5s
// 总耗时:2.5s
并发执行(并行)
javascript
scss
const [weather, tweets] = await Promise.all([
getWeather(), // 发起请求,不等待
getTweets() // 立即发起另一个请求
]);
// 总耗时:约 2s(最慢的那个)
在 Agent 中的实际应用
当 Agent 需要调用多个互不影响、无依赖关系 的 Tool 时,使用 Promise.all 能显著提升响应速度:
javascript
javascript
// Agent 需要同时读取多个文件(互不依赖)
async function readMultipleFiles(paths) {
const contents = await Promise.all(
paths.map(path => readFileTool({ FilePath: path }))
);
return contents; // 顺序与 paths 一致
}
💡 注意 :
Promise.all是"一荣俱荣,一损俱损"------任何一个 Promise 失败,整体都会失败。如果某个 Tool 偶尔会挂掉,应使用Promise.allSettled,它会等待所有 Tool 完成(无论成功或失败)。
Promise 状态与并发控制
理解 Promise 的三个状态,是理解异步并发的关键:
| 状态 | 含义 | 触发时机 |
|---|---|---|
pending(待定) |
操作尚未完成 | Promise 刚创建,请求已发出但未回复 |
fulfilled(已成功) |
操作成功完成 | 触发 .then() 或 await 后的下一行 |
rejected(已失败) |
操作失败 | 网络断开、404、代码报错,触发 .catch() |
状态不可逆 :一旦从 pending 变为 fulfilled 或 rejected,就永久固定。这就是"承诺"(Promise)的含义------一旦兑现或失败,不能再改变。
并发执行的底层原理
JavaScript 是单线程,但 Node.js 底层通过 libuv 实现"宏观并发":
- 网络 I/O (
fetch、http.request):利用操作系统内核事件轮询(Linux:epoll/ Windows:IOCP) - 文件 I/O (
fs.readFile):利用 C++ 线程池(默认 4 个线程)在后台读取
主线程发起 I/O 后立即交给 libuv,自己继续执行。libuv 拿到结果后通过事件循环推回主线程。这就是 Promise.all 能实现并行等待的根本原因。
async/await只是写法上的语法糖,让异步代码看起来像同步,并没有改变其非阻塞本质。
一点总结
通过这次学习,我理解了:
- Agent 解决了 LLM 的五大缺陷:无状态、无法操作外部世界、无私有知识、无最新信息、无法自动完成复杂任务。
- Agent 的工作流程:规划/推理 → 加载 Memory → 调用 Tool → 查询 RAG → 生成 Response。
- LangChain 是 Agent 开发的标准化引擎:封装了循环调度、记忆管理、输出解析等复杂逻辑,并提供兼容各家大模型的抽象层。
- LangChain 的 Tool 抽象:处理函数 + 描述对象(name、description、schema),代码可读性强,编写效率高。
tool_calls的 ID 关联机制 :异步调用多 Tool 时,通过id关联请求和响应,保证上下文完整。Promise.all是 Agent 性能优化的利器:互不依赖的 Tool 调用可以并行执行,大幅缩短响应时间。
互动讨论
- 手写 Agent 时,最让你头疼的是哪个部分? 循环控制、Prompt 拼接还是输出解析?
- LangChain 的
bindTools()和直接在ChatOpenAI构造器中传入tools有什么区别? Promise.all的"一荣俱荣"特性在 Agent 开发中有什么风险? 如何规避?- 如果你要设计一个"自动写代码并运行测试"的 Agent,需要哪些 Tool? 它们的调用顺序应该如何编排?
tool_calls的id为什么能保证异步调用的顺序关联? 如果不用id会有什么问题?
📌 一点心得:LangChain 的核心价值不是"简化代码",而是"标准化流程"。它把 Agent 开发中重复、繁琐的底层逻辑封装好,让你专注于业务逻辑和 Tool 的实现。