从零搭建 AI Agent:LLM + Tool + 异步并发的实战之路
大家好,今天想和大家分享我最近在学习 AI Agent 开发过程中的一些思考和实战代码。我们从最简单的异步概念开始,一步步走到一个能调用工具的 LangChain Agent。
一、为什么 LLM 本身不够用?
很多人以为 AI Agent 就是直接调用大模型接口,但实际上,裸的 LLM 有几个「硬伤」:
| 问题 | 解决方案 |
|---|---|
| 无状态,记不住上次聊了什么 | Memory 模块(数据库、Redis 等) |
| 不能访问网页、操作文件 | Tool Use 模块 |
| 不知道企业内部私有文档 | RAG 模块 |
| 不知道预训练之后的新信息 | MCP / Tool |
| 复杂任务需要多步骤执行 | Skills(技能编排) |
所以,Agent = LLM + Memory + Tool + RAG + MCP + Skills。就是给大模型装上「手和脚」,让它不再只是一个「嘴炮选手」。
二、磨刀不误砍柴工:理解异步并发
在动手写 Agent 之前,有一个绕不开的基础概念——异步并发。Agent 经常需要同时调用多个工具,如果串行执行,用户体验会非常差。
来看例子:
function getWeather() {
return new Promise((resolve) => {
setTimeout(() => {
resolve({ temp: 38, conditions: 'Sunny with Clouds' })
}, 2000) // 模拟 2 秒的网络请求
})
}
function getTweets() {
return new Promise((resolve) => {
setTimeout(() => {
resolve(['I like cake', 'BBQ is good too!'])
}, 500) // 模拟 0.5 秒的网络请求
})
}
❌ 串行写法(总耗时 ≈ 2500ms)
const weatherData = await getWeather(); // 等 2000ms
const tweetsData = await getTweets(); // 再等 500ms
// 总耗时:2000 + 500 = 2500ms
✅ 并行写法(总耗时 ≈ 2000ms)
const [weatherData, tweetsData] =
await Promise.all([getWeather(), getTweets()]);
// 总耗时:max(2000, 500) = 2000ms,节省了 500ms
核心知识点:
Promise有三种状态:pending→fulfilled或rejected,一旦确定就不可逆Promise.all()并行执行多个 Promise,等所有都完成才返回,结果的顺序和传入顺序一致await是 ES8 提供的语法糖,让异步代码看起来像同步代码
这个基础很重要,因为后面 Agent 调用多个 Tool 时,我们会用 Promise.all 来并发执行它们,大幅提升性能。
三、Hello LangChain:第一行代码
LangChain 是一个 LLM 开发框架,它的核心价值是统一兼容各家大模型。来看看最简版的调用:
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);
虽然底层用的是 DeepSeek 的 API,但通过 LangChain 的 ChatOpenAI 统一接口,调用方式和 OpenAI 完全一致——这就是 LangChain 的兼容层价值。
四、给 LLM 装上「手」:Tool 工具调用
这是整个 Agent 开发中最核心的一步。来看看完整实现:
4.1 定义一个工具
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
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({ // ③ Zod 参数约束
filePath: z.string().describe('要读取的文件路径')
})
}
);
一个 Tool 由两部分组成:
- 处理函数:实际执行逻辑(读文件、调 API、写数据库……)
- 描述对象:告诉 LLM「我是什么、我能干什么、你需要给我什么参数」
4.2 绑定工具到模型
const modelWithTools = model.bindTools(tools);
这一行代码把工具列表注册到模型中。之后当用户的问题需要用到工具时,LLM 不会直接生成答案,而是在响应中返回 tool_calls:
{
"tool_calls": [
{
"id": "call_abc123",
"name": "read_file",
"arguments": { "filePath": "tool.mjs" }
}
]
}
关键设计:LLM 有「自知之明」——当它判断需要调用工具时,会停下来,把工具调用信息返回给开发者,由开发者实际执行工具函数,再把结果返回给 LLM 继续处理。这个 id 字段就是用来关联「哪个工具调用」对应「哪个执行结果」的。
4.3 多步对话流程
const messages = [
new SystemMessage(`你是一个代码助手,可以使用工具读取文件并解释代码。...`),
new HumanMessage('请读取 tool.mjs 文件内容并解释代码'),
];
let response = await modelWithTools.invoke(messages);
messages.push(response); // 将 LLM 响应(含 tool_calls)加入历史
// 这里还需要:
// 1. 解析 response.tool_calls
// 2. 并发执行工具(Promise.all!)
// 3. 将工具结果以 ToolMessage 形式加入 messages
// 4. 再次调用 LLM 生成最终回答
五、Agent 的工作全流程
把上面的内容串起来,一个完整的 Agent 工作流程是这样的:
用户提出任务(Prompt)
↓
LLM 规划/推理(Planning & Reasoning)
↓
┌─ 要不要加载 Memory? ──→ 加载历史对话上下文
├─ 要不要调用 Tool? ──→ 并行/串行执行工具(Promise.all 加速)
├─ 要不要查 RAG? ──→ 检索内部知识库 → 注入 Prompt Template
└─ 要不要用 Skill? ──→ 执行多步骤技能编排
↓
综合所有信息,生成最终回复
↓
返回给用户
六、技术栈全景图
我们选择的是一套 JavaScript/TypeScript 全栈 AI Agent 技术栈:
| 层级 | 技术 | 作用 |
|---|---|---|
| 后端框架 | NestJS | 提供企业级架构、依赖注入 |
| Agent 开发 | LangChain.js | 单智能体开发(LLM + Tool + Memory) |
| 多智能体 | LangGraph | 多智能体编排、状态图 |
| 工具协议 | MCP | 第三方工具标准化接入 |
| 知识检索 | RAG | 向量数据库 + 私有知识库 |
| 参数校验 | Zod | Tool 参数类型约束 |
七、总结
回顾整个学习路径:
- 异步基础:
Promise+Promise.all是 Agent 并发调用工具的性能基石 - LangChain 统一接口:一套代码兼容 OpenAI、DeepSeek 等各家模型
- Tool 机制:
tool()函数定义 +model.bindTools()注册,让 LLM 从「能说」变成「能做」 - Agent 思维:LLM 负责思考和规划,Tool 负责执行,Memory 负责记忆,RAG 负责知识——各司其职
一个知道内部知识、能思考规划、能帮你做事情的「增强版大模型」,就是 Agent。而 LangChain 让这个过程变得工程化、可维护。
学完这些,我们即将打造自己的第一个高性能 Agent! 🚀