
为什么你调的 LLM 接口不值钱——从零理解 Agent 开发的正确姿势2026年7月6日 · 学习总结 · 实战教学向一、开篇一个让你重新思考的问题来先看一段代码constmodelnewChatOpenAI({modelName:deepseek-v4-flash,/* ... */});constresponseawaitmodel.invoke(你好推荐一款蛋白粉);console.log(response.content);三行代码一个 AI 对话就跑起来了。你看着终端里流淌出来的文字心里暗爽——“就这AI 开发也太简单了吧。”然后老板找到你“小王做个智能客服能查我们内部的订单系统、读公司的产品文档、自动帮用户退换货。下班前出个方案。”你盯着那三行代码陷入了沉默。问题出在哪那三行代码调的是裸 LLM——一个被困在 API 后面的大脑。它智商很高但它记不住你上一条消息说了什么无状态没法真的去查你的订单数据库不会执行完全不知道你们公司有什么产品文档无私有知识回答不了今天股价多少训练数据过时处理不了帮我做一份 Q3 分析 PPT这种多步骤任务不会编排你花十分钟学会的那三行代码只解决了让模型说话。而值钱的部分——让模型干活——一行都还没开始写。今天这篇文章就是要把说话到干活中间缺的每一块拼图掰开揉碎了讲清楚。读完你会得到一个公式和把这个公式落地的全部知识Agent LLM Memory Tool RAG MCP Skills二、裸 LLM 的五宗罪及解药咱们一个一个来。每一点都遵循同一个模式先看清问题再给出解法最后落到代码层面的关键动作。第一宗健忘症Stateless问题场景你跟 LLM 聊了两个小时把项目背景、技术选型、团队分工全交代清楚了。第二天打开新对话——“你好请问有什么可以帮您” 它全忘了。不是它不想记是它压根没有记忆这个能力。LLM 每一次调用都是无状态的上一轮的输出不会自动变成下一轮的输入。解法Memory记忆模块最简单的记忆就是你手动把历史消息拼回去constmessages[newSystemMessage(你是一个代码助手),newHumanMessage(帮我读 tool.mjs),];constresponseawaitmodel.invoke(messages);messages.push(response);// ← 这一行就是最朴素的 Memory进阶一点把关键信息存到 Redis、数据库、或者前端 localStorage在需要的时候检索出来注入上下文。核心思想就一句话该记住的东西持久化下次对话塞回去。第二宗手残No Tool Use问题场景你问 LLM “帮我看看这个项目的代码结构”它能给你写五百字方法论——用什么命令、看哪些文件、怎么分析——但它永远不会真的执行ls或者readFile。它能告诉你怎么做不能替你做。解法Tool Use工具调用给它装上手。在 LangChain 里一个 Tool 长这样constreadFileToolnewTool(async({filePath}){// ① 干活的函数constcontentawaitfs.readFile(filePath,utf-8);returncontent;},{// ② 描述对象给 LLM 看的说明书name:read_file,description:读取文件内容。当用户要求查看代码、分析文件时调用此工具。,schema:z.object({filePath:z.string().describe(要读取的文件路径),}),});constmodelWithToolsmodel.bindTools([readFileTool]);// 注册工具注意这个设计Tool 有两个部分——函数负责执行描述对象负责让 LLM 理解什么时候该用我。两者职责分离谁也别越界。第三宗知识盲区No Private Knowledge问题场景你们公司有 300 篇内部 Wiki、500 个 Confluence 页面。随便一个新人都能翻到的信息LLM 却一无所知——这些私有数据从来没进过训练集。你让它按内部规范写接口它只能瞎编一个。解法RAGRetrieval-Augmented Generation检索增强生成流程三步走索引把内部文档切片 → 向量化 → 存入向量数据库检索用户提问时从向量库检索最相关的文档片段生成把检索结果拼进 Prompt交给 LLM 生成回答本质上是给 LLM 配了一位内部档案馆馆长——你问问题馆长先去翻档案把找到的资料连同问题一起递给 LLM。第四宗信息滞后No Real-time Data问题场景“今天深圳天气怎么样”“比特币最新价格”“刚结束的欧洲杯谁夺冠了” LLM 的训练数据有截止日期在此之后的事它一问三不知。解法MCPModel 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 → 生成信息滞后没有实时数据MCPTool 的标准化协议不会编排处理不了复杂任务Skills任务流程固化复用五个问题五个解药。把它们装到一起——一个知道内部知识、能动手干活、有记忆、能联网、能编排复杂任务的大模型——这就是 Agent。三、解剖一个 ToolLLM 是怎么长出手的理论讲完上硬菜。我们逐行拆解今天写的tool.mjs这是理解 Agent 工作机制最核心的代码。3.1 Tool 的定义两段式结构constreadFileToolnewTool(// 第一部分执行函数 async({filePath}){constcontentawaitfs.readFile(filePath,utf-8);console.log([工具调用] read_file (${filePath}) 成功读取${content.length}字节);returncontent;},// 第二部分描述对象 {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()注册给模型后对话流程变成了这样用户: 读取 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 的短期记忆constmessages[newSystemMessage(你是一个代码分析助手...),newHumanMessage(请读取 tool.mjs 文件的内容并解释代码),];letresponseawaitmodelWithTools.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 性能优化的前提Promise 状态机 pending ──→ fulfilled (resolved) └──────→ rejected 规则状态只能从 pending 出发一旦到达 fulfilled 或 rejected永久定型不可逆转。类比你下单了一个快递。下单那一刻是pending快递到了是fulfilled丢件了是rejected。不管是到了还是丢了这单的状态就此锁定不会今天到了明天又变回运输中。4.2 await 的陷阱你以为在并行其实在排队// ❌ 反模式串行等待console.time(serial);constweatherawaitgetWeather();// 等 2000msconsttweetsawaitgetTweets();// 再等 500msconsole.timeEnd(serial);// → 约 2500ms这段代码的问题在于getWeather()和getTweets()之间没有任何依赖关系——天气数据不需要推文作为输入推文也不需要等天气结果。但你用await把它们写成了串行——第二个操作干等第一个操作完成白白浪费了 500ms。4.3 Promise.all让独立任务一起跑// ✅ 正解并行执行console.time(parallel);const[weather,tweets]awaitPromise.all([getWeather(),// 2000msgetTweets(),// 500ms —— 和上面同时跑]);console.timeEnd(parallel);// → 约 2000ms以最慢的为准// 节省 500ms性能提升 20%Promise.all的语义接收一个 Promise 数组同时启动所有 Promise不是排队是一起发车等所有Promise 都完成才返回返回的结果顺序与传入的数组顺序一致不是按完成先后4.4 推广到 Agent这才是真正的性能优化回到 Agent 场景。假设你需要同时读取 5 个代码文件来分析项目结构// ❌ 串行5 × 200ms 1000msfor(constfileoffiles){constcontentawaitreadFileTool.invoke({filePath:file});}// ✅ 并行max(200, 200, 200, 200, 200) 200msconstresultsawaitPromise.all(files.map(freadFileTool.invoke({filePath:f})));800ms 的差距。而这还是一个保守的估算。实际场景中Tool 可能包括数据库查询、HTTP 请求、文件 I/O——这些操作的耗时差异更大并行的收益也就更大。Agent 性能优化的第一原则凡是彼此间没有数据依赖的 Tool 调用一律用 Promise.all 并行执行。不需要更快的硬件不需要更复杂的算法只需要用对并发模式。这就是工程素养——同样的工具不同的人用出来性能天差地别。五、两个 Demo 串起来的完整学习链路今天写了两个 Demo它们合在一起构成了从调接口到做 Agent的完整学习路径Demo 1index.mjs — 统一接口的价值import{ChatOpenAI}fromlangchain/openai;constmodelnewChatOpenAI({modelName:deepseek-v4-flash,apiKey:process.env.DEEPSEEK_API_KEY,configuration:{baseURL:https://api.deepseek.com/v1},});constresponseawaitmodel.invoke(你好推荐一款蛋白粉);console.log(response.content);虽然调的是 DeepSeek用的是langchain/openai的类。为什么能这样因为 DeepSeek 的 API 兼容 OpenAI 规范。而 LangChain 的核心价值之一就是统一抽象——换模型厂商就像换数据库驱动上层的 Agent 逻辑一行不用改。这种面向接口编程的思路在微服务和 AI 开发中同样适用。Demo 2tool.mjs — 从说话到干活的跃迁这是今天的重头戏。完整流程定义 Toolnew Tool(fn, { name, description, schema })注册 Toolmodel.bindTools([readFileTool])构建消息SystemMessageHumanMessage调用模型modelWithTools.invoke(messages)处理 tool_calls执行工具函数返回ToolMessage管理历史messages.push(response)继续推理再次invokeLLM 拿到工具结果后生成最终答案这七个步骤就是 Agent 运行的最小闭环。掌握了它后面学 LangGraph 多智能体编排、NestJS 工程化封装、生产环境部署都是在这个闭环上做加法。六、结语今天迈出的这一步是最值钱的一步回头再看开篇那个公式Agent LLM Memory Tool RAG MCP Skills今天你学到的不只是一个公式而是公式里每一项落到代码层面长什么样Memorymessages.push(response) 数据库持久化Toolnew Tool(async fn, { name, description, schema: z.object({...}) })model.bindTools()RAG 向量检索 Prompt 注入下一步深入MCP 标准化 Tool 协议下一步深入Skills LangGraph 多智能体任务编排下一步深入而贯穿始终的性能基石——Promise.all——是你今天掌握的最具性价比的优化手段。接下来的学习路线也很清晰LangChain单 Agent → 今天已完成 ↓ NestJS把 Agent 封装成 API 服务 ↓ LangGraph多 Agent 协作编排 ↓ MCP / RAG / Skills能力拼图补齐 ↓ 生产环境部署性能、监控、容错从model.invoke(你好)到打造一个能自主思考、规划、执行的生产级 Agent——这中间隔的不是代码量是思维层次。而今天你已经跨过了第一道门槛。