为什么你调的 LLM 接口不值钱?------从零理解 Agent 开发的正确姿势
2026年7月6日 · 学习总结 · 实战教学向
一、开篇:一个让你重新思考的问题
来,先看一段代码:
javascript
const model = new ChatOpenAI({ modelName: 'deepseek-v4-flash', /* ... */ });
const response = await model.invoke('你好,推荐一款蛋白粉');
console.log(response.content);
三行代码,一个 AI 对话就跑起来了。你看着终端里流淌出来的文字,心里暗爽------"就这?AI 开发也太简单了吧。"
然后老板找到你:"小王,做个智能客服,能查我们内部的订单系统、读公司的产品文档、自动帮用户退换货。下班前出个方案。"
你盯着那三行代码,陷入了沉默。
问题出在哪?
那三行代码调的是裸 LLM------一个被困在 API 后面的"大脑"。它智商很高,但它:
- 记不住你上一条消息说了什么(无状态)
- 没法真的去查你的订单数据库(不会执行)
- 完全不知道你们公司有什么产品文档(无私有知识)
- 回答不了"今天股价多少"(训练数据过时)
- 处理不了"帮我做一份 Q3 分析 PPT"这种多步骤任务(不会编排)
你花十分钟学会的那三行代码,只解决了"让模型说话"。而值钱的部分------让模型干活------一行都还没开始写。
今天这篇文章,就是要把"说话"到"干活"中间缺的每一块拼图,掰开揉碎了讲清楚。读完你会得到一个公式,和把这个公式落地的全部知识:
Agent = LLM + Memory + Tool + RAG + MCP + Skills
二、裸 LLM 的"五宗罪"及解药
咱们一个一个来。每一点都遵循同一个模式:先看清问题,再给出解法,最后落到代码层面的关键动作。
第一宗:健忘症(Stateless)
问题场景 :你跟 LLM 聊了两个小时,把项目背景、技术选型、团队分工全交代清楚了。第二天打开新对话------"你好,请问有什么可以帮您?" 它全忘了。不是它不想记,是它压根没有"记忆"这个能力。LLM 每一次调用都是无状态的,上一轮的输出不会自动变成下一轮的输入。
解法:Memory(记忆模块)
最简单的记忆就是你手动把历史消息拼回去:
javascript
const messages = [
new SystemMessage('你是一个代码助手'),
new HumanMessage('帮我读 tool.mjs'),
];
const response = await model.invoke(messages);
messages.push(response); // ← 这一行,就是最朴素的 Memory
进阶一点,把关键信息存到 Redis、数据库、或者前端 localStorage,在需要的时候检索出来注入上下文。核心思想就一句话:该记住的东西持久化,下次对话塞回去。
第二宗:手残(No Tool Use)
问题场景 :你问 LLM "帮我看看这个项目的代码结构",它能给你写五百字方法论------用什么命令、看哪些文件、怎么分析------但它永远不会真的执行 ls 或者 readFile。它能"告诉你怎么做",不能"替你做"。
解法:Tool Use(工具调用)
给它装上"手"。在 LangChain 里,一个 Tool 长这样:
javascript
const readFileTool = new Tool(
async ({ filePath }) => { // ① 干活的函数
const content = await fs.readFile(filePath, 'utf-8');
return content;
},
{ // ② 描述对象(给 LLM 看的说明书)
name: 'read_file',
description: '读取文件内容。当用户要求查看代码、分析文件时调用此工具。',
schema: z.object({
filePath: z.string().describe('要读取的文件路径'),
}),
}
);
const modelWithTools = model.bindTools([readFileTool]); // 注册工具
注意这个设计:Tool 有两个部分------函数 负责执行,描述对象负责让 LLM 理解"什么时候该用我"。两者职责分离,谁也别越界。
第三宗:知识盲区(No Private Knowledge)
问题场景:你们公司有 300 篇内部 Wiki、500 个 Confluence 页面。随便一个新人都能翻到的信息,LLM 却一无所知------这些私有数据从来没进过训练集。你让它"按内部规范写接口",它只能瞎编一个。
解法:RAG(Retrieval-Augmented Generation,检索增强生成)
流程三步走:
- 索引:把内部文档切片 → 向量化 → 存入向量数据库
- 检索:用户提问时,从向量库检索最相关的文档片段
- 生成:把检索结果拼进 Prompt,交给 LLM 生成回答
本质上是给 LLM 配了一位"内部档案馆馆长"------你问问题,馆长先去翻档案,把找到的资料连同问题一起递给 LLM。
第四宗:信息滞后(No Real-time Data)
问题场景:"今天深圳天气怎么样?""比特币最新价格?""刚结束的欧洲杯谁夺冠了?" LLM 的训练数据有截止日期,在此之后的事,它一问三不知。
解法:MCP(Model Context Protocol,模型上下文协议)
你可以把 MCP 理解为"Tool 的 USB 接口标准"------不管你接入的是天气 API、股票数据源、还是新闻 RSS,只要遵循 MCP 协议,LLM 就能即插即用。它解决了 Tool 的标准化问题:不用每次接入新数据源都写一套胶水代码。
第五宗:不会编排(No Task Orchestration)
问题场景:"帮我做一份 Q3 业务分析 PPT------从数据库拉数据,用 ECharts 画图,导出 PDF"------这是一个多步骤任务,涉及数据查询、可视化、文档生成三个环节。裸 LLM 只能一段段给你文字建议,它不会自动拆解任务、按顺序执行、处理中间异常、交付最终成品。
解法:Skills(技能蒸馏)
把经过验证的复杂任务流程固化,变成可复用的"技能包"。一次打磨,反复调用。这背后是 LangGraph 这类多智能体编排框架的用武之地------每个子任务分给一个专门的 Agent,由一个"调度者"统一协调。
一张图收束
| 缺陷 | 症状 | 解药 | 一句话 |
|---|---|---|---|
| 无状态 | 记不住对话 | Memory | 持久化 + 塞回上下文 |
| 不会执行 | 只能给思路 | Tool Use | 函数干活,描述对象给 LLM 看 |
| 不知内档 | 不懂私有文档 | RAG | 检索 → 注入 Prompt → 生成 |
| 信息滞后 | 没有实时数据 | MCP | Tool 的标准化协议 |
| 不会编排 | 处理不了复杂任务 | Skills | 任务流程固化复用 |
五个问题,五个解药。把它们装到一起------一个知道内部知识、能动手干活、有记忆、能联网、能编排复杂任务的大模型------这就是 Agent。
三、解剖一个 Tool:LLM 是怎么"长出手"的?
理论讲完,上硬菜。我们逐行拆解今天写的 tool.mjs,这是理解 Agent 工作机制最核心的代码。
3.1 Tool 的定义:两段式结构
javascript
const readFileTool = new 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({
filePath: z.string().describe('要读取的文件路径'),
}),
}
);
这里有三个值得停下来想一想的细节:
细节一:description 是写给 LLM 看的,不是写给人看的。 你写的 description 会直接影响到 LLM 的 tool selection 准确率。写得敷衍------"读取文件"------LLM 可能不知道该什么时候用。写得详细------"当用户要求读取文件、查看代码、分析文件内容时调用"------LLM 就能精准命中场景。把 description 当成 API 文档来写,消费者是 LLM。
细节二:schema 用 Zod 做运行时类型校验。 z.object({ filePath: z.string().describe('...') }) 这行代码的价值在于------LLM 虽然强大,但它生成的参数格式偶尔也会出错。Zod 在你的函数执行之前做了一层验证,参数不对直接报错,不会让错误的数据流进业务逻辑。永远不要信任 LLM 生成的参数格式。
细节三:工具函数里的 console.log。 这不是调试代码,是用户体验设计------Agent 的任务可能很耗时(读大文件、调慢接口),如果界面长时间没有任何反馈,用户会焦虑、会怀疑是不是卡死了、会直接关掉页面。每隔几秒汇报一下进度,是 Agent 开发的基本素养。
3.2 tool_calls 的流转:LLM 怎么告诉你"我要用工具了"
当你把 Tool 通过 bindTools() 注册给模型后,对话流程变成了这样:
arduino
用户: "读取 tool.mjs 的内容并解释"
↓
LLM 推理: "我需要读文件,用 read_file 工具"
↓
LLM 停止生成文字,返回 tool_calls:
[{
id: "call_abc123", // 唯一标识,关联后续结果
name: "read_file", // 工具名
arguments: { // 参数(由 schema 约束)
filePath: "tool.mjs"
}
}]
↓
你的代码执行 readFileTool("tool.mjs")
↓
返回 ToolMessage:
{
tool_call_id: "call_abc123", // ← 靠这个 id 关联!
content: "<文件内容>"
}
↓
把 ToolMessage 追加到 messages 数组
↓
LLM 拿到文件内容,继续推理 → 生成代码解释
关键设计:id 字段的作用 。Agent 可能同时调用多个工具------比如读 3 个文件------每个工具的执行耗时不同,返回顺序可能和调用顺序不一致。id 就是用来把"哪个返回结果对应哪个调用请求"关联起来的,保证 LLM 拿到的是正确的上下文。就像快递单号------你同时下了三单,到货时间不一样,但每个包裹上都有单号,你不会搞混。
3.3 消息历史:Agent 的"短期记忆"
javascript
const messages = [
new SystemMessage('你是一个代码分析助手...'),
new HumanMessage('请读取 tool.mjs 文件的内容并解释代码'),
];
let response = await modelWithTools.invoke(messages);
messages.push(response); // ← 把 LLM 的响应(可能包含 tool_calls)推进去
// 如果 response 里有 tool_calls,执行工具后:
// messages.push(new ToolMessage(result, tool_call_id));
// 然后再次 invoke(messages),LLM 就能看到工具执行结果
messages 数组就是对话的"记忆带"。每一次 invoke() 都带着完整的消息历史,LLM 才能理解上下文。这里有一个工程上的trade-off:消息历史越长,token 消耗越大、响应越慢。生产环境中你需要在"保留足够上下文"和"控制 token 成本"之间找平衡------裁剪旧消息、摘要压缩、只保留关键轮次,都是常用手段。
四、Promise.all:决定 Agent 是"能用"还是"好用"的分水岭
4.1 先复习 Promise 的三种状态
这是今天 1.html 里讲的基础,但它是理解 Agent 性能优化的前提:
javascript
Promise 状态机:
pending ──→ fulfilled (resolved)
└──────→ rejected
规则:状态只能从 pending 出发,一旦到达 fulfilled 或 rejected,永久定型,不可逆转。
类比:你下单了一个快递。下单那一刻是 pending,快递到了是 fulfilled,丢件了是 rejected。不管是到了还是丢了,这单的状态就此锁定,不会今天到了明天又变回"运输中"。
4.2 await 的陷阱:你以为在并行,其实在排队
javascript
// ❌ 反模式:串行等待
console.time('serial');
const weather = await getWeather(); // 等 2000ms
const tweets = await getTweets(); // 再等 500ms
console.timeEnd('serial'); // → 约 2500ms
这段代码的问题在于:getWeather() 和 getTweets() 之间没有任何依赖关系 ------天气数据不需要推文作为输入,推文也不需要等天气结果。但你用 await 把它们写成了串行------第二个操作干等第一个操作完成,白白浪费了 500ms。
4.3 Promise.all:让独立任务一起跑
javascript
// ✅ 正解:并行执行
console.time('parallel');
const [weather, tweets] = await Promise.all([
getWeather(), // 2000ms
getTweets(), // 500ms ------ 和上面同时跑!
]);
console.timeEnd('parallel'); // → 约 2000ms(以最慢的为准)
// 节省 500ms,性能提升 20%
Promise.all 的语义:
- 接收一个 Promise 数组
- 同时启动所有 Promise(不是排队,是一起发车)
- 等所有 Promise 都完成才返回
- 返回的结果顺序 与传入的数组顺序一致(不是按完成先后)
4.4 推广到 Agent:这才是真正的性能优化
回到 Agent 场景。假设你需要同时读取 5 个代码文件来分析项目结构:
javascript
// ❌ 串行:5 × 200ms = 1000ms
for (const file of files) {
const content = await readFileTool.invoke({ filePath: file });
}
// ✅ 并行:max(200, 200, 200, 200, 200) = 200ms
const results = await Promise.all(
files.map(f => readFileTool.invoke({ filePath: f }))
);
800ms 的差距。 而这还是一个保守的估算。实际场景中,Tool 可能包括数据库查询、HTTP 请求、文件 I/O------这些操作的耗时差异更大,并行的收益也就更大。
Agent 性能优化的第一原则:凡是彼此间没有数据依赖的 Tool 调用,一律用 Promise.all 并行执行。 不需要更快的硬件,不需要更复杂的算法,只需要用对并发模式。这就是"工程素养"------同样的工具,不同的人用出来,性能天差地别。
五、两个 Demo 串起来的完整学习链路
今天写了两个 Demo,它们合在一起构成了从"调接口"到"做 Agent"的完整学习路径:
Demo 1:index.mjs --- 统一接口的价值
javascript
import { ChatOpenAI } from '@langchain/openai';
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,用的是 @langchain/openai 的类。为什么能这样?因为 DeepSeek 的 API 兼容 OpenAI 规范。而 LangChain 的核心价值之一就是统一抽象------换模型厂商就像换数据库驱动,上层的 Agent 逻辑一行不用改。这种"面向接口编程"的思路,在微服务和 AI 开发中同样适用。
Demo 2:tool.mjs --- 从"说话"到"干活"的跃迁
这是今天的重头戏。完整流程:
- 定义 Tool (
new Tool(fn, { name, description, schema })) - 注册 Tool (
model.bindTools([readFileTool])) - 构建消息 (
SystemMessage+HumanMessage) - 调用模型 (
modelWithTools.invoke(messages)) - 处理 tool_calls (执行工具函数,返回
ToolMessage) - 管理历史 (
messages.push(response)) - 继续推理 (再次
invoke,LLM 拿到工具结果后生成最终答案)
这七个步骤,就是 Agent 运行的最小闭环。掌握了它,后面学 LangGraph 多智能体编排、NestJS 工程化封装、生产环境部署,都是在这个闭环上做加法。
六、结语:今天迈出的这一步,是最值钱的一步
回头再看开篇那个公式:
Agent = LLM + Memory + Tool + RAG + MCP + Skills
今天你学到的不只是一个公式,而是公式里每一项落到代码层面长什么样:
- Memory =
messages.push(response)+ 数据库持久化 - Tool =
new Tool(async fn, { name, description, schema: z.object({...}) })+model.bindTools() - RAG = 向量检索 + Prompt 注入(下一步深入)
- MCP = 标准化 Tool 协议(下一步深入)
- Skills = LangGraph 多智能体任务编排(下一步深入)
而贯穿始终的性能基石------Promise.all------是你今天掌握的最具性价比的优化手段。
接下来的学习路线也很清晰:
markdown
LangChain(单 Agent → 今天已完成)
↓
NestJS(把 Agent 封装成 API 服务)
↓
LangGraph(多 Agent 协作编排)
↓
MCP / RAG / Skills(能力拼图补齐)
↓
生产环境部署(性能、监控、容错)
从 model.invoke("你好") 到打造一个能自主思考、规划、执行的生产级 Agent------这中间隔的不是代码量,是思维层次。而今天,你已经跨过了第一道门槛。