从零到一搞懂 AI Agent:原理、工作流程、LangChain Tool Use 与实战
=
文章目录
- 一、不是直接调用大模型接口——LLM 的五大短板
- 二、Agent 的工作流程
- 三、Agent 开发框架 LangChain
- 3.1 为什么需要框架?
- 3.2 LangChain 核心抽象
- (1)LLM 统一接口
- (2)Tool 工具定义
- (3)Tool 调用的底层机制——LLM 的"自知之明"
- 四、LLM Tool 调用性能优化
- 4.1 Promise 基础回顾
- 4.2 实战:并行执行多个 Tool
- 五、LangChain 框架总结
- 六、全文总结
- 核心结论
- 核心知识点复盘
- 常见问题 / 避坑指南
最值钱的 Agent 开发技能,从概念、原理到 LangChain 实战,一次性讲透。
一、不是直接调用大模型接口——LLM 的五大短板
很多人以为 AI 应用就是调一下大模型 API,把 prompt 丢进去拿结果。实际生产环境中,裸调 LLM 会遇到五个绕不开的问题:
| 短板 | 问题描述 | 解决方案 |
|---|---|---|
| 无记忆 | 上周聊过的内容,LLM 完全不记得(Stateless) | Memory 模块(数据库 / Redis / 前端存储) |
| 不会动手 | 让它访问网页、操作文件,它只能给你思路 | Tool Use 模块 |
| 不懂内部知识 | 公司内部文档、私有数据,LLM 训练时没见过 | RAG(检索增强生成) |
| 信息滞后 | 最新的新闻、实时数据,不在预训练语料中 | MCP / Function Call |
| 做不了复杂任务 | 做 PPT、分析股市自动买卖等长链路操作 | Skills(技能编排 / 蒸馏) |
核心认知:Agent 就是给 LLM 装上"大脑(Memory)+ 手脚(Tool)+ 知识库(RAG)+ 技能包(Skills)"。
Claude Code、Cursor、Manus、小龙虾这些明星产品,本质都是LLM + 能力扩展的 Agent。
二、Agent 的工作流程
一个完整的 Agent 处理任务的过程如下:
User Prompt(用户提出复杂任务) ↓ ┌─────────────────────────┐ │ LLM Planning/Reasoning │ ← 规划 & 推理:拆解任务步骤 └──────────┬──────────────┘ ↓ ┌─────────────────────────┐ │ 需要加载 Memory 吗? │ ← 回忆历史上下文 └──────────┬──────────────┘ ↓ ┌─────────────────────────┐ │ 需要调用 Tool 吗? │ ← 分步骤调用多个工具(读文件/写文件/执行命令) └──────────┬──────────────┘ ↓ ┌─────────────────────────┐ │ RAG 检索 + Prompt 模板 │ ← 从知识库查询相关内容,拼入 Prompt └──────────┬──────────────┘ ↓ ┌─────────────────────────┐ │ Response → 返回给用户 │ ← 任务完成 └─────────────────────────┘整个流程的核心驱动力是LLM 自身的推理能力(Reasoning),它自主判断每一步需要什么、调用什么、何时结束。我们开发者要做的,就是把 Memory、Tool、RAG 这些能力"挂载"上去。
三、Agent 开发框架 LangChain
3.1 为什么需要框架?
直接调 OpenAI 接口也能做 Tool Call,但问题是:
- 各家 LLM 接口不统一:OpenAI 的 tool_calls 格式和 DeepSeek、文心一言都不一样
- 工具管理繁琐:参数校验、结果回传、异常处理都要手写
- 多 Agent 协作困难:多个 Agent 之间的消息路由、状态同步是地狱级复杂度
LangChain 就是来解决这些问题的。技术栈推荐:
Node.js (NestJS) + LangChain (单 Agent) + LangGraph (多 Agent)3.2 LangChain 核心抽象
(1)LLM 统一接口
@langchain/openai的ChatOpenAI类兼容所有 OpenAI-API 兼容的模型(DeepSeek、通义千问等),按需切换:
// llm-demo.mjs — LangChain 统一 LLM 调用import'dotenv/config';import{ChatOpenAI}from'@langchain/openai';constmodel=newChatOpenAI({modelName:'deepseek-v4-flash',// 模型名称apiKey:process.env.DEEPSEEK_API_KEY,// API Keytemperature:0,// 温度:0=确定性输出,适合工具调用configuration:{baseURL:'https://api.deepseek.com/v1'// 兼容 OpenAI 接口的 Base URL}});// 一行 invoke,等价于 openai.chat.completions.createconstresponse=awaitmodel.invoke('用一句话介绍 Agent 是什么');console.log(response.content);关键设计:
ChatOpenAI封装了底层的 HTTP 调用细节,换模型只需改baseURL和modelName,业务代码零改动。
(2)Tool 工具定义
Tool 是 Agent 的"手脚",让 LLM 从"想"变成"做"。LangChain 使用@langchain/core/tools的tool()(把js变成LLM的tool) 函数定义工具,配合zod做参数校验。
一个 Tool 由两部分组成:
| 组成部分 | 说明 |
|---|---|
| 处理函数(异步) | 真正的执行逻辑,如fs.readFile() |
| 描述对象 | name(工具名)、description(告诉 LLM 什么时候用)、schema(zod 参数约束) |
// tool.mjs — 定义文件读取工具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'}from'@langchain/core/messages';importfsfrom'node:fs/promises';import{z}from'zod';// zod 提供运行时类型约束constmodel=newChatOpenAI({modelName:'deepseek-v4-flash',apiKey:process.env.DEEPSEEK_API_KEY,temperature:0,configuration:{baseURL:'https://api.deepseek.com/v1'}});// ==================== 定义 Tool ====================constreadFileTool=tool(// 第一部分:异步处理函数 —— 真正干活的代码async({filePath})=>{constcontent=awaitfs.readFile(filePath,'utf-8');// 每次工具调用都应输出反馈,Agent 任务可能很耗时console.log(`[工具调用] read_file(${filePath}) 成功读取${content.length}字节`);returncontent;// 返回值会作为 ToolMessage 喂回 LLM},// 第二部分:描述对象 —— LLM 据此判断何时调用、传什么参数{name:'read_file',// 工具名称,LLM 通过它引用工具description:`用此工具来读取文件内容。当用户要求读取文件、查看代码、 分析文件内容时,调用此工具。输入文件路径(可以是相对路径或绝对路径)。`,schema:z.object({filePath:z.string().describe('要读取的文件路径'),// zod 参数约束})});// ==================== 绑定工具到模型 ====================consttools=[readFileTool];constmodelWithTools=model.bindTools(tools);// LangChain 提供的抽象// ==================== 构建对话 ====================constmessages=[newSystemMessage(`你是一个代码助手,可以使用工具读取文件并解释代码。 工作流程: 1. 用户要求读取文件时,立即调用 read_file 工具。 2. 等待工具返回文件内容。 3. 基于文件内容进行分析和解释。`),newHumanMessage('请读取 tool.mjs 文件内容并解释代码'),];// ==================== 第一轮:LLM 决定调用工具 ====================letresponse=awaitmodelWithTools.invoke(messages);messages.push(response);// 把 AIMessage(含 tool_calls)加入历史// 此时 response.tool_calls 包含:// [{ id: "call_xxx", name: "read_file", arguments: { filePath: "tool.mjs" } }](3)Tool 调用的底层机制——LLM 的"自知之明"
这是最容易踩坑的核心原理,必须彻底理解:
LLM 知道自己不会读文件。当它判断需要调用工具时,不会硬编一个答案,而是"停下来"返回
tool_calls数组。
一个tool_calls条目的结构:
// response.tool_calls[0] 的结构{id:"call_abc123",// 唯一 ID,关联后续的 ToolMessagename:"read_file",// 要调用的工具名args:{filePath:"tool.mjs"// 参数 —— 完全由 schema 约束}}id是关键:后续执行工具后,返回的ToolMessage必须带上相同的tool_call_id,LLM 才能把工具结果和之前的调用请求对应起来。
// ==================== 第二轮:执行工具 & 回传结果 ====================// 遍历 LLM 返回的 tool_calls,逐一执行for(consttoolCallofresponse.tool_calls){if(toolCall.name==='read_file'){constresult=awaitreadFileTool.invoke(toolCall.args);// 构造 ToolMessage,id 必须匹配,这样才能组成完整上下文messages.push(newToolMessage({content:result,tool_call_id:toolCall.id// ← 关键!关联 LLM 的调用请求}));}}// ==================== 第三轮:LLM 基于工具结果生成最终回复 ====================constfinalResponse=awaitmodelWithTools.invoke(messages);console.log(finalResponse.content);// 输出:对文件内容的解释和分析...一句话总结:
tool_calls.id是 LLM 和 Tool 之间的"快递单号",丢了就关联不上。
四、LLM Tool 调用性能优化
复杂任务中,LLM 可能调用多个不同的 Tool,或对同一 Tool多次调用。如果串行执行,总耗时 = 每次调用耗时之和。
4.1 Promise 基础回顾
| 概念 | 说明 |
|---|---|
| Promise | ES6 异步语法,三种状态:Pending→Fulfilled/Rejected,且不可逆 |
| await | ES8 语法糖,让异步代码看起来像同步,最优雅的写法 |
| Promise.all() | 并行执行多个 Promise,全部完成才返回,结果顺序与输入一致 |
4.2 实战:并行执行多个 Tool
// ==================== 并行执行多个工具调用 ====================// LLM 返回了多个 tool_calls(如同时读3个文件)// ❌ 错误写法:串行 await,慢for(consttoolCallofresponse.tool_calls){constresult=awaitexecuteTool(toolCall);// 一个接一个等messages.push(newToolMessage({content:result,tool_call_id:toolCall.id}));}// 总耗时 = t1 + t2 + t3// ✅ 正确写法:Promise.all 并行执行consttoolResults=awaitPromise.all(response.tool_calls.map(async(toolCall)=>{constresult=awaitexecuteTool(toolCall);// 每个独立运行return{result,tool_call_id:toolCall.id};}));// 一次性 push 所有结果for(const{result,tool_call_id}oftoolResults){messages.push(newToolMessage({content:result,tool_call_id}));}// 总耗时 ≈ max(t1, t2, t3) —— 接近最慢的那个适用场景:多个工具调用之间没有依赖关系时才可并行。如果 Tool B 的输入依赖 Tool A 的输出,必须串行。
五、LangChain 框架总结
LangChain 比 OpenAI 的 Transformer 架构还早诞生,它解决的核心问题是:
| 能力 | LangChain 模块 | 说明 |
|---|---|---|
| 统一 LLM 调用 | @langchain/openai的ChatOpenAI | 兼容所有 OpenAI-API 格式的模型 |
| 工具定义 & 校验 | @langchain/core/tools的tool()+zod | 声明式定义工具,自动生成 JSON Schema |
| 消息管理 | HumanMessage/SystemMessage/ToolMessage/AIMessage | 类型化的消息对象,避免手动拼 JSON |
| Tool 绑定 | model.bindTools(tools) | 自动将 Tool Schema 注入请求,LLM 按需返回tool_calls |
一句话理解 LangChain:它把"调 LLM"从"手写 fetch + 拼 JSON"升级为"类型安全的工程化开发",让 Agent 开发从 demo 级别走向生产级别。
六、全文总结
核心结论
- Agent = LLM + Memory + Tool + RAG + Skills。裸调 LLM 只是"聊天机器人",加上这些扩展才是能自动干活的"智能体"。
- Agent 工作流是一个闭环:用户提需求 → LLM 规划 → 调用工具 → 获取结果 → 继续推理 → 输出答案。
- LangChain 是 Agent 开发的事实标准框架,核心就三件事:统一 LLM 接口、管理 Tool 生命周期、编排消息流转。
- Tool 机制的本质是 LLM 的"自知之明"——它知道自己不会读文件,所以"停下来"返回
tool_calls,而不是瞎编。
核心知识点复盘
| 知识点 | 一句话总结 |
|---|---|
| Agent 定义 | LLM + 能力扩展(Memory/Tool/RAG/Skills) |
| Agent 工作流 | Planning → Memory → Tool → RAG → Response |
| LangChain 价值 | 统一 LLM 接口 + Tool 管理 + 消息编排 |
| Tool 两部分 | 处理函数(干活)+ 描述对象(告诉 LLM 怎么用) |
tool_calls.id | “快递单号”,关联工具请求和结果 |
Promise.all | 无依赖的多个 Tool 可以并行执行 |
常见问题 / 避坑指南
Q1:LLM 调用了不存在的工具怎么办?
在
toolMap中查不到时,返回明确的错误信息给 LLM(如未知工具: xxx),LLM 会自行调整策略。
Q2:tool_calls 的 id 可以随便写吗?
不可以。必须用 LLM 返回的原始
id原封不动填到ToolMessage.tool_call_id。写错了 LLM 无法关联,会导致上下文错乱。
Q3:什么时候用Promise.all,什么时候串行?
多个 Tool 调用互不依赖时用
Promise.all并行加速;如果 Tool B 需要 Tool A 的输出作为输入,必须串行await。
Q4:temperature 为什么要设为 0?
temperature = 0让 LLM 输出确定性的tool_calls,避免同样的 prompt 每次调用不同工具。工具调用场景强烈建议设为 0。
本文基于 LangChain.js + DeepSeek 实战编写,代码完整可运行,适合作为 Agent 开发的入门到进阶参考。