一个后端开发者的 Agent 学习笔记,Day 1。
为什么开始学 Agent?
我每天用 Claude Code 写代码,它帮我读文件、写代码、跑命令------一个对话窗口完成整套开发流程。这让我很好奇:它背后的原理到底是什么?
拆开来看其实不复杂:Claude Code 本质就是 LLM + 读写文件的工具 + 执行命令的工具。LLM 负责思考和规划,工具负责干活,两者组合起来就是一个能独立完成任务的智能体。
这个"组合"就是 Agent。
所以我决定从零开始,用 LangChain 框架亲手搭建一个 Agent。今天是第一天,这篇文章记录我学到的所有东西。
一、Agent 到底是什么?
直接调用大模型 API 有几个硬伤:
- 记不住:每次对话都是全新的,LLM 不记得你们上次聊了什么(无状态)
- 不会动手:它只能告诉你"怎么做",没法亲自去读文件、调 API、跑命令
- 不知道内部知识:公司内部的文档、代码库,预训练数据里没有
- 不知道最新信息:训练数据有截止日期,今天的新闻它不知道
Agent 就是给 LLM 装上各种"外挂",补上这些短板:
ini
Agent = LLM + Memory + Tool + RAG + MCP + Skills
| 能力短板 | Agent 的解决方案 |
|---|---|
| 记不住上下文 | Memory 模块(数据库 / Redis) |
| 没法动手操作 | Tool Use(调用 API、读写文件、执行命令) |
| 不懂内部知识 | RAG(检索增强生成) |
| 拿不到最新信息 | MCP / Tool(外部工具协议) |
| 搞不定复杂任务 | Skills(技能编排) |
Agent 的工作流程
markdown
用户丢来一个复杂任务
│
▼
LLM 规划推理:这任务需要什么?
│
├──► 需要记忆? → 查 Memory
├──► 需要动手? → 调用 Tool(可能多个、可能多轮)
├──► 需要查内部文档? → RAG 检索
│
▼
把以上所有信息组装进上下文 → LLM 生成最终回复
核心思想一句话:LLM 是大脑,工具是手脚,记忆是海马体。组合起来才是一个完整的人。
二、LangChain:统一的大模型调用框架
LangChain 比 OpenAI 公司还早诞生。它的核心价值就一个:用同一套接口调用不同厂商的模型。
最简调用:5 行代码
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", // 兼容 OpenAI 接口
},
});
const response = await model.invoke("你觉得什么水果好吃?");
console.log(response.content);
DeepSeek 提供了 OpenAI 兼容的 API,所以用 @langchain/openai 就能直接调。换成通义千问、月之暗面,改一下 baseURL 和 apiKey 就行,代码完全不用动。
三、核心重点:Tool 工具机制
这是今天最有收获的部分。Agent 能"干活",全靠 Tool。
3.1 Tool 的两个组成部分
在 LangChain 中定义一个 Tool,需要两样东西:
ini
Tool = 异步处理函数(干活) + 描述对象(告诉 LLM 什么时候用它)
以读文件工具为例:
javascript
const readFileTool = tool(
// 第一部分:干活函数
async ({ filePath }) => {
const content = await fs.readFile(filePath, "utf8");
console.log(`[工具调用] 读取 ${filePath},共 ${content.length} 字节`);
return content;
},
// 第二部分:描述对象
{
name: "read_file",
description: `读取文件内容。当用户要求查看代码、分析文件时调用。`,
schema: z.object({
filePath: z.string().describe("要读取的文件路径"),
}),
}
);
这里有一个很精妙的设计:LLM 并不会真的执行你的函数代码。它只是在推理时"决定"要调用这个工具,然后停下来,把工具名和参数返回给你。你在外部执行完,再把结果传回去。
这个过程中三个关键字段各司其职:
name:LLM 用它来指认"我要调这个工具"description:告诉 LLM 什么场景下该用这个工具------描述越详细,LLM 判断越准schema(Zod 校验):约束 LLM 必须传什么参数、参数是什么类型
3.2 Tool 调用的完整循环
matlab
第 1 轮:LLM 推理
→ "用户要我读 tool.mjs,我得调 read_file 工具"
→ 返回 tool_calls: [{name: "read_file", arguments: {filePath: "tool.mjs"}}]
→ 注意:LLM 此时不生成文本内容!
执行工具(外部)
→ fs.readFile("tool.mjs") → 返回文件内容
第 2 轮:LLM 再次推理
→ "拿到文件内容了,我来分析一下..."
→ 生成文本回复给用户
这个循环可能有多轮------比如"先读文件分析,再写分析结果到新文件",就需要两轮工具调用。
3.3 Agent 主循环代码
javascript
// 注册工具
const modelWithTools = model.bindTools([readFileTool, writeFileTool]);
// Agent 主循环
while (true) {
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 没有 tool_calls → 任务完成,输出回复
if (!response.tool_calls?.length) {
console.log(response.content);
break;
}
// 有 tool_calls → 并行执行所有工具
const toolMessages = await Promise.all(
response.tool_calls.map(async (tc) => {
const tool = toolList.find(t => t.name === tc.name);
const result = await tool.invoke(tc.args);
return new ToolMessage({
content: result,
tool_call_id: tc.id, // 通过 id 关联,LLM 知道谁的结果
});
})
);
messages.push(...toolMessages);
// 回到循环开头,让 LLM 基于结果继续推理
}
四、性能关键:Promise.all 并行执行
4.1 为什么需要 Promise
LLM 调用和 Tool 执行都是异步操作(网络请求、文件读写)。JavaScript 通过 Promise 管理异步。
Promise 有三种状态,且不可逆:
scss
Pending ──resolve()──► Fulfilled(成功)
Pending ──reject()───► Rejected(失败)
async/await 是语法糖,让异步代码写起来像同步:
javascript
async function getWeather() {
return new Promise((resolve) => {
setTimeout(() => resolve({ temp: 38 }), 2000);
});
}
const data = await getWeather(); // 等 2 秒,拿到 { temp: 38 }
4.2 串行 vs 并行
javascript
// ❌ 串行:等天气(2s) → 再等推文(0.5s) → 总耗时 2.5s
const weather = await getWeather();
const tweets = await getTweets();
// ✅ 并行:同时发起 → 等最慢的那个(2s) → 总耗时 2s
const [weather, tweets] = await Promise.all([
getWeather(),
getTweets(),
]);
4.3 在 Agent 中的落地
LLM 一次可能返回多个互不依赖的 tool_calls。比如用户问"帮我读一下这三个文件",LLM 会一次性返回 3 个 read_file 调用。这时候用 Promise.all 并行执行,耗时 = 最慢的那个文件,而不是三个文件加起来。
判断标准很简单:工具之间没有数据依赖 → 并行;后一个依赖前一个的输出 → 串行。
这就是高性能 Agent 的关键优化点。
五、回头看 Claude Code
学完这一天,我终于理解了 Claude Code 的底层模型:
ini
Claude Code = LLM + fs(文件读写)+ cli(命令执行)
当我对它说"创建一个 React Todo 项目",它的 Planning 大概是:
- 调 cli →
npm create vite@latest - 调 fs → 写入 App.jsx 代码
- 调 cli →
npm run dev
每一步都是 LLM 决定调哪个工具,外部执行完后把结果传回去,LLM 再决定下一步。
我自己在写的 Agent 也是这个模式------LLM + Tool 列表 + 循环推理。只是我的 Tool 还只有读写文件,后面还可以加 HTTP 请求、数据库查询、发邮件......扩展空间无限。
六、总结
今天从零开始,搞清楚了四件事:
- Agent 为什么存在------补 LLM 的能力短板(记忆、动手、知识、时效)
- LangChain 怎么用 ------统一接口调模型,
bindTools注册工具 - Tool 机制是核心------LLM 决定用什么工具,外部执行,结果返回,循环往复
- Promise.all 是性能关键------互不依赖的工具调用必须并行
知识地图
javascript
Day 1 已覆盖 ✅
├── 理论基础:Agent 定义、工作流程
├── LangChain:LLM 调用、Tool 定义/注册/调用循环
└── JS 异步:Promise 三态 → async/await → Promise.all 并行
待学习 ⬜
├── Memory --- 让 Agent 记住历史对话
├── RAG --- 检索内部知识库
├── MCP --- 标准化工具协议
├── Skills --- 复杂任务编排
├── LangGraph --- 多智能体协作
└── NestJS --- Agent 产品化部署
最后
第一天下来最大的感受:Agent 概念不复杂,复杂的是工程落地。
把 LLM + Tool + Memory 组合起来,原理上就几十行代码。但要做到生产可用------工具的错误处理、并行调度的最优策略、多轮对话的状态管理、安全权限控制------每一样都是硬仗。
不过这就是 "Harness Engineering" 的意义:把 AI 能力驯服,装进产品里,创造真实的商业价值。
Day 1 结束,明天继续。
日期:2026-07-06