引言:为什么需要 Agent?
直接调用大模型(LLM)接口存在以下核心问题:
| 问题 | 解决方案 |
|---|---|
| 无记忆性:LLM 是无状态的,记不住历史对话 | Memory 模块(数据库/Redis/前端存储) |
| 不能干活:LLM 只能告诉你思路,无法真正执行操作 | Tool 使用模块 |
| 知识局限:无法访问内部私有文档 | RAG(检索增强生成) |
| 信息过时:不知道最新的新闻和数据 | MCP(第三方 Tool 协议) |
| 复杂任务:无法自动完成多步骤任务(如做 PPT、买卖股票) | Skills 技能蒸馏 |
Agent = LLM + Memory + Tool + RAG + MCP + Skills
简单来说,Agent 就是给 LLM 装上"记忆"和"手脚",让它能思考、规划,并真正帮你完成任务。
一、技术栈与项目结构
1.1 技术选型
- Node.js + ESM 模块:运行环境
- LangChain.js:LLM 应用开发框架(统一兼容各大模型)
- Zod:参数校验库(用于 Tool 的 Schema 定义)
- Chalk:终端彩色输出
1.2 核心依赖
json
{
"dependencies": {
"@langchain/core": "^1.2.9", // LangChain 核心
"@langchain/openai": "^1.5.10", // 兼容 OpenAI 接口的模型
"dotenv": "^17.4.2", // 环境变量管理
"zod": "^4.4.3", // Schema 校验
"chalk": "^6.0.0" // 终端美化
}
}
1.3 项目结构
bash
hello-langchain/
├── src/
│ ├── index.mjs # 基础 LLM 调用示例
│ ├── tool.mjs # 单 Tool 实现示例
│ ├── all-tools.mjs # 完整工具集实现
│ ├── node_exec.mjs # 子进程命令执行示例
│ └── mini-cursor.mjs # 核心:完整的 Mini-Cursor Agent
├── .env # 环境变量(API Key)
└── package.json
二、LangChain 核心概念
2.1 四种 Message 类型
LangChain 使用消息传递机制构建对话历史:
| Message 类型 | 作用 | 说明 |
|---|---|---|
SystemMessage |
系统提示 | 设置 AI 的角色、能力、行为规范 |
HumanMessage |
用户消息 | 用户的输入请求 |
AIMessage |
AI 回复 | AI 的自然语言回复 |
ToolMessage |
工具结果 | Tool 执行后的返回结果,需绑定 tool_call_id |
2.2 Tool 设计模式
Tool 是 Agent 的"手脚",由两部分组成:
ini
Tool = 异步函数(实现功能) + 描述对象(name + description + schema)
- name:工具唯一标识,LLM 通过此名称调用
- description:详细描述工具功能、使用场景、参数要求
- schema:Zod Schema,约束参数格式,确保 LLM 传递正确的参数
2.3 核心工作流
markdown
用户请求 → SystemMessage + HumanMessage → LLM 推理
↓
LLM 返回 tool_calls(需要调用的工具列表)
↓
执行工具 → ToolMessage(带 tool_call_id)加入消息历史
↓
再次调用 LLM → 循环直到无 tool_calls
↓
返回最终结果
三、实战:创建 Mini-Cursor 编程助手
3.1 初始化 LLM 模型
javascript
import 'dotenv/config';
import { ChatOpenAI } from '@langchain/openai';
const model = new ChatOpenAI({
modelName: 'deepseek-v4-pro',
apiKey: process.env.DEEPSEEK_API_KEY,
temperature: 0, // 设为 0 确保严谨性,因为需要精确调用工具
configuration: {
baseURL: 'https://api.deepseek.com/v1',
}
});
要点 :temperature 设为 0 是关键------工具调用需要确定性输出,不能有随机性。
3.2 实现工具集
3.2.1 读文件工具(readFileTool)
javascript
import { tool } from '@langchain/core/tools';
import fs from 'node:fs/promises';
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({
filePath: z.string().describe('要读取的文件路径')
})
}
);
3.2.2 写文件工具(writeFileTool)
javascript
import path from 'node:path';
const writeFileTool = tool(
async ({ filePath, content }) => {
try {
const dir = path.dirname(filePath);
await fs.mkdir(dir, { recursive: true }); // 自动创建目录
await fs.writeFile(filePath, content, 'utf-8');
console.log(`[工具调用] write_file(${filePath}) 成功写入 ${content.length} 字节`);
return `成功写入 ${filePath}`;
} catch (err) {
return `写入文件失败:${err.message}`; // 容错处理
}
},
{
name: 'write_file',
description: '向指定路径写入文件内容,自动创建目录',
schema: z.object({
filePath: z.string().describe('文件路径'),
content: z.string().describe('要写入的文件内容')
})
}
);
3.2.3 列目录工具(listDirectoryTool)
javascript
const listDirectoryTool = tool(
async ({ directoryPath }) => {
try {
const files = await fs.readdir(directoryPath);
console.log(`[工具调用] list_directory(${directoryPath}) 成功列出 ${files.length} 个文件`);
return `目录内容:\n${files.map(file => file.name).join('\n')}`;
} catch (err) {
return `列出目录失败:${err.message}`;
}
},
{
name: 'list_directory',
description: '列出指定目录下的所有文件和文件夹',
schema: z.object({
directoryPath: z.string().describe('目录路径')
})
}
);
3.2.4 执行命令工具(executeCommandTool)
这是最关键的工具,让 Agent 能执行终端命令:
javascript
import { spawn } from 'node:child_process';
const executeCommandTool = tool(
async ({ command, workingDirectory }) => {
const cwd = workingDirectory || process.cwd();
return new Promise((resolve, reject) => {
const [cmd, ...args] = command.split(' ');
const child = spawn(cmd, args, {
cwd,
stdio: 'inherit', // 继承父进程输入输出,实时显示
shell: true // 启用 Shell 环境
});
let errorMsg = '';
child.on('error', (err) => { errorMsg = err.message; });
child.on('close', (code) => {
if (code === 0) {
resolve(`命令成功执行: ${command}`);
} else {
resolve(`命令执行失败,退出码:${code}\n错误:${errorMsg}`);
}
});
});
},
{
name: 'execute_command',
description: '执行系统命令,支持指定工作目录',
schema: z.object({
command: z.string().describe('要执行的命令'),
workingDirectory: z.string().describe('工作目录(推荐指定)')
})
}
);
3.3 注册工具到 LLM
javascript
const tools = [
readFileTool,
writeFileTool,
listDirectoryTool,
executeCommandTool
];
// 将工具绑定到模型,LLM 就知道自己拥有哪些能力
const modelWithTools = model.bindTools(tools);
3.4 实现 ReAct 循环(核心)
ReAct(Reasoning + Acting)是 Agent 的核心工作模式:
javascript
import { HumanMessage, SystemMessage, ToolMessage } from '@langchain/core/messages';
async function runAgentWithTools(query, maxIterations = 30) {
// 1. 初始化消息历史
const messages = [
new SystemMessage(`你是一个项目管理助手,使用工具完成任务。
当前工作目录:${process.cwd()}
可用工具:read_file, write_file, execute_command, list_directory
重要规则:
- execute_command 的 workingDirectory 会自动切换目录
- 不要在 command 中使用 cd
- 示例正确用法:{ command: "pnpm install", workingDirectory: "react-todo-app" }
回复要简洁,只说做了什么`),
new HumanMessage(query)
];
// 2. ReAct 循环
for (let i = 0; i < maxIterations; i++) {
// Step 1: LLM 推理(Reasoning)
const response = await modelWithTools.invoke(messages);
messages.push(response);
// Step 2: 判断是否需要调用工具
if (!response.tool_calls || response.tool_calls.length === 0) {
// 无工具调用,说明任务完成
return response.content;
}
// Step 3: 并行执行工具调用(Acting)
const toolResults = await Promise.all(
response.tool_calls.map(async (toolCall) => {
const tool = tools.find(t => t.name === toolCall.name);
if (!tool) return `错误:未找到工具 ${toolCall.name}`;
try {
return await tool.invoke(toolCall.args);
} catch (err) {
return `错误:${err.message}`;
}
})
);
// Step 4: 将工具结果加入消息历史(Observation)
response.tool_calls.forEach((toolCall, index) => {
messages.push(new ToolMessage({
content: toolResults[index],
tool_call_id: toolCall.id
}));
});
}
// 超过最大迭代次数,返回最后一条消息
return messages[messages.length - 1].content;
}
3.5 执行任务
javascript
const task = `
创建一个功能丰富的 React TodoList 应用:
1. 使用 pnpm create vite react-todo-app --template react-ts 创建项目
2. 修改 App.tsx 实现完整 TodoList 功能
3. 添加美观的样式和动画
4. 安装依赖并启动开发服务器
`;
try {
const result = await runAgentWithTools(task);
console.log('AI 最终回复:', result);
} catch (err) {
console.error('执行出错:', err.message);
}
四、关键技巧与最佳实践
4.1 SystemMessage 是核心
精心设计的系统提示能显著提升 Agent 的稳定性:
javascript
new SystemMessage(`
你是一个编程助手,擅长使用工具完成开发任务。
当前工作目录:${process.cwd()}
工具使用规则:
1. execute_command 的 workingDirectory 参数会自动切换到指定目录
2. 绝对不要在 command 字符串中使用 cd
3. 需要查看目录结构时,使用 list_directory 而不是 ls/dir
回复风格:简洁、专业、直奔主题
`)
4.2 并行执行提升性能
当 LLM 返回多个 tool_calls 时,使用 Promise.all 并行执行:
javascript
const toolResults = await Promise.all(
response.tool_calls.map(toolCall => tool.invoke(toolCall.args))
);
4.3 容错处理
每个工具都应该有 try-catch,避免单点故障:
javascript
try {
return await tool.invoke(toolCall.args);
} catch (err) {
return `错误:${err.message}`; // 返回错误信息给 LLM,让它决定下一步
}
4.4 进度反馈
Agent 任务可能耗时很长,需要给用户实时反馈:
javascript
for (let i = 0; i < maxIterations; i++) {
console.log(`[第 ${i + 1} 轮] AI 正在思考...`);
// ...
console.log(`[执行工具] ${toolCall.name}(${JSON.stringify(toolCall.args)})`);
}
4.5 超时保护
防止 Agent 陷入无限循环:
javascript
// 超时强制退出
setTimeout(() => {
console.log("超时保护:强制退出进程");
process.exit(0);
}, 1000000); // 约 16 分钟
五、运行项目
5.1 安装依赖
bash
cd hello-langchain
pnpm install
5.2 配置环境变量
创建 .env 文件:
ini
DEEPSEEK_API_KEY=your_api_key_here
5.3 运行 Agent
bash
node src/mini-cursor.mjs
六、Agent 整体架构与底层原理
6.1 Agent 分层架构
objectivec
┌─────────────────────────────────────────────────────┐
│ 用户交互层 │
│ (User / CLI / Web UI) │
├─────────────────────────────────────────────────────┤
│ Agent 编排层 │
│ ReAct 循环 / 任务规划 / 状态管理 │
├─────────────────────────────────────────────────────┤
│ 能力扩展层 │
│ Tool 工具集 / Memory 记忆 / RAG 检索 / MCP │
├─────────────────────────────────────────────────────┤
│ LLM 推理层 │
│ ChatOpenAI / 模型选择 / temperature 调优 │
├─────────────────────────────────────────────────────┤
│ 基础设施层 │
│ 文件系统 / 子进程 / 网络请求 / 数据库 │
└─────────────────────────────────────────────────────┘
6.2 一次完整的 Agent 执行流程(图解)
以「创建 React TodoList」为例,看看底层到底发生了什么:
css
用户: "创建一个 React TodoList 并启动"
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 第1轮:LLM 推理 │
│ │
│ messages = [ │
│ SystemMessage("你是项目管理助手...") │
│ HumanMessage("创建 React TodoList...") │
│ ] │
│ │
│ ↓ modelWithTools.invoke(messages) │
│ │
│ LLM 内部处理: │
│ 1. 解析 SystemMessage 中的规则 │
│ 2. 解析 HumanMessage 中的任务需求 │
│ 3. 查看绑定的 tools 列表(名称+描述+参数约束) │
│ 4. 决策:需要调用 execute_command 创建项目 │
│ │
│ ↓ 返回响应 │
│ AIMessage { │
│ content: "好的,我来创建项目...", │
│ tool_calls: [{ │
│ id: "call_abc123", │
│ name: "execute_command", │
│ args: { │
│ command: "pnpm create vite react-todo-app --template react-ts", │
│ workingDirectory: "." │
│ } │
│ }] │
│ } │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 执行工具:Acting │
│ │
│ 1. 遍历 response.tool_calls │
│ 2. 根据 name 找到对应的 Tool 实例 │
│ 3. 调用 tool.invoke(args) 执行实际操作 │
│ │
│ ↓ 底层执行过程 │
│ executeCommandTool.invoke({ │
│ command: "pnpm create vite react-todo-app --template react-ts",│
│ workingDirectory: "." │
│ }) │
│ ↓ │
│ spawn("pnpm", ["create", "vite", "react-todo-app", ││ "--template", "react-ts"], { │
│ cwd: ".", │
│ stdio: "inherit", │
│ shell: true │
│ }) │
│ ↓ │
│ 子进程执行 pnpm create vite... │
│ 输出实时显示在控制台 │
│ 子进程退出码 = 0 → resolve("命令成功执行") │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 第2轮:加入观察结果,继续推理 │
│ │
│ messages 追加: │
│ AIMessage (第1轮LLM响应) │
│ ToolMessage({ │
│ content: "命令成功执行 pnpm create vite...", │
│ tool_call_id: "call_abc123" ← 关联到第1轮的tool_call │
│ }) │
│ │
│ ↓ modelWithTools.invoke(messages) │
│ │
│ LLM 看到: │
│ - 第1轮的思考和决策(AIMessage) │
│ - 工具执行的结果(ToolMessage) │
│ - 上下文:知道项目已创建成功 │
│ │
│ ↓ 决策:需要使用 writeFileTool 写入 App.tsx │
│ AIMessage { │
│ tool_calls: [{ │
│ id: "call_def456", │
│ name: "write_file", │
│ args: { │
│ filePath: "react-todo-app/src/App.tsx", │
│ content: "import React, { useState } from 'react'..." │
│ } │
│ }] │
│ } │
└─────────────────────────────────────────────────────────────────┘
│
▼
... 循环继续 ...
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 第N轮:LLM 判定任务完成 │
│ │
│ messages 已包含完整的执行历史: │
│ SystemMessage │
│ HumanMessage │
│ AIMessage(1) + ToolMessage(1) │
│ AIMessage(2) + ToolMessage(2) │
│ ... │
│ AIMessage(N) ← tool_calls 为空! │
│ │
│ ↓ 检测到 response.tool_calls.length === 0 │
│ │
│ LLM 返回最终总结: │
│ "已完成 React TodoList 应用的创建: │
│ 1. 使用 Vite 创建了 react-todo-app 项目 │
│ 2. 实现了完整的 TodoList 功能... │
│ 3. 已通过 pnpm run dev 启动开发服务器" │
│ │
│ return response.content ← 任务结束 │
└─────────────────────────────────────────────────────────────────┘
6.3 ReAct 循环底层解析
ReAct = Reason (推理)+ Act(行动)
markdown
┌──────────────┐
│ 启动任务 │
└──────┬───────┘
│
▼
┌─────────────────────────┐
│ Reason(推理阶段) │
│ │
│ 1. 打包 messages 历史 │
│ 2. 调用 LLM API │
│ 3. LLM 返回决策 │
│ - 需要调用工具? │
│ - 还是任务完成? │
└───────────┬─────────────┘
│
┌─────────┴─────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ 有 tool_calls │ │ 无 tool_calls │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────┐
│ Act(行动阶段) │ │ 返回最终结果 │
│ │ │ 任务结束 │
│ 1. 解析参数 │ └──────────────┘
│ 2. 执行工具 │
│ 3. 收集结果 │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Observe(观察) │
│ │
│ 1. 封装 ToolMessage│
│ 2. 加入 messages │
│ 3. 回到 Reason │
└────────┬─────────┘
│
└───────→ 回到 Reason 阶段
核心要点:
- Messages 数组是唯一的状态载体 :所有上下文、决策、结果都通过
messages数组传递 - LLM 无记忆 ≠ Agent 无记忆 :Agent 通过维护
messages数组实现上下文传递 - Tool 是无副作用的执行单元:每次调用独立执行,结果通过 ToolMessage 返回
6.4 LLM API 底层请求/响应
当调用 modelWithTools.invoke(messages) 时,底层实际发生了什么:
swift
┌─ 发送给 LLM 的 HTTP 请求 ──────────────────────────────────────┐
│ │
│ POST https://api.deepseek.com/v1/chat/completions │
│ │
│ { │
│ "model": "deepseek-v4-pro", │
│ "temperature": 0, │
│ "messages": [ │
│ { │
│ "role": "system", │
│ "content": "你是一个项目管理助手..." │
│ }, │
│ { │
│ "role": "user", │
│ "content": "创建一个 React TodoList..." │
│ }, │
│ { │
│ "role": "assistant", │
│ "content": "好的,我来创建项目...", │
│ "tool_calls": [{ │
│ "id": "call_abc123", │
│ "type": "function", │
│ "function": { │
│ "name": "execute_command", │
│ "arguments": "{\"command\":\"pnpm create vite...\"}" │
│ } │
│ }] │
│ }, │
│ { │
│ "role": "tool", │
│ "content": "命令成功执行...", │
│ "tool_call_id": "call_abc123" │
│ } │
│ ], │
│ "tools": [ │
│ { │
│ "type": "function", │
│ "function": { │
│ "name": "read_file", │
│ "description": "读取文件内容...", │
│ "parameters": { │
│ "type": "object", │
│ "properties": { │
│ "filePath": { "type": "string" } │
│ }, │
│ "required": ["filePath"] │
│ } │
│ } │
│ }, │
│ // ... 其他 tools │
│ ] │
│ } │
└─────────────────────────────────────────────────────────────────┘
关键理解:
tools数组会在每次请求中发送给 LLM,告诉它有哪些能力可用messages数组包含完整历史,LLM 通过此理解上下文tool_calls是 LLM 的决策输出,表示"我需要调用这个工具"tool角色消息是工具执行结果 ,通过tool_call_id关联
6.5 Tool 绑定底层机制
scss
model.bindTools(tools)
│
▼
┌─ LangChain 内部处理 ──────────────────────────────────────────┐
│ │
│ 1. 提取每个 tool 的元信息: │
│ - tool.name → 函数名 │
│ - tool.description → 功能描述 │
│ - tool.schema → Zod Schema → JSON Schema 转换 │
│ │
│ 2. 构建 tools 参数: │
│ [{type: "function", function: {name, description, ...}}] │
│ │
│ 3. 生成新的 modelWithTools 实例 │
│ - 内部持有 tools 引用 │
│ - invoke() 时自动附加 tools 参数 │
│ │
└────────────────────────────────────────────────────────────────┘
│
▼
modelWithTools.invoke(messages)
│
▼
内部自动执行:
1. 提取 tools 元数据
2. 构建带 tools 的 API 请求
3. 解析响应中的 tool_calls
4. 返回结构化的 AIMessage
6.6 消息状态流转模型
css
┌─────────────────────────────────────────────────────────────┐
│ Messages 状态机 │
│ │
│ [SystemMessage] ← 初始化时添加,全程不变 │
│ │ │
│ ▼ │
│ [HumanMessage] ← 用户输入 │
│ │ │
│ ▼ │
│ [AIMessage] ← LLM 第1次响应(含 tool_calls) │
│ │ │
│ ▼ │
│ [ToolMessage] ← 工具执行结果(关联 tool_call_id) │
│ │ │
│ ▼ │
│ [AIMessage] ← LLM 第2次响应(可能含 tool_calls) │
│ │ │
│ ▼ │
│ [ToolMessage] ← 第2个工具结果 │
│ │ │
│ ... │
│ │ │
│ ▼ │
│ [AIMessage] ← LLM 最终响应(无 tool_calls) │
│ │ │
│ ▼ │
│ 任务完成 │
│ │
└─────────────────────────────────────────────────────────────┘
6.7 并发 vs 串行执行
javascript
// 串行执行(tool.mjs 中的写法)
for (const toolCall of response.tool_calls) {
const result = await tool.invoke(toolCall.args); // 等一个完成再执行下一个
messages.push(new ToolMessage({...}));
}
// 并行执行(mini-cursor.mjs 中的优化写法)
const toolResults = await Promise.all(
response.tool_calls.map(toolCall => tool.invoke(toolCall.args))
); // 同时执行所有工具,速度更快
选择策略:
- 工具之间无依赖 → 并行(
Promise.all) - 工具之间有先后顺序 → 串行(
for...of+await)
6.8 完整生命周期时序图
scss
时间轴 →
用户 Agent 主循环 LLM API Tool 执行
│ │ │ │
│──query────→ │ │ │
│ │──messages 组装──→ │ │
│ │ │──HTTP POST────────→│
│ │ │ │
│ │ │←──tool_calls[]─────│
│ │←──AIMessage────── │ │
│ │ │ │
│ │──解析 tool_calls──→│ │
│ │ │ │
│ │ ┌───────────────┼───────────────┐ │
│ │ │ │ │ │
│ │ ▼ ▼ ▼ │
│ │ tool1() tool2() tool3() │
│ │ │ │ │ │
│ │ ↓ ↓ ↓ │
│ │ result1 result2 result3 │
│ │ │ │ │ │
│ │ └───────────────┼───────────────┘ │
│ │ │ │
│ │──ToolMessage[]──→ │ │
│ │ │ │
│ │──messages 组装──→ │ │
│ │ │──HTTP POST────────→│
│ │ │ │
│ │ │←──final_response───│
│ │←──最终结果──────── │ │
│←──result──── │ │ │
│ │ │ │
七、总结
Agent 开发核心公式
diff
Agent = LLM + Tools + ReAct 循环
其中:
- LLM:负责推理和决策(选择使用哪个工具、传递什么参数)
- Tools:负责实际执行(读文件、写文件、执行命令)
- ReAct 循环:Reason(推理)→ Act(行动)→ Observe(观察结果)→ 循环
Mini-Cursor 的能力边界
✅ 可以做到:
- 创建项目脚手架
- 读写代码文件
- 执行 CLI 命令
- 多步骤任务规划与执行
❌ 暂时不能:
- 处理 GUI 界面交互
- 访问受权限保护的资源
- 执行长时间运行的后台任务
扩展方向
- Memory 模块:添加对话记忆,支持多轮上下文
- RAG 模块:接入内部知识库,让 Agent 了解项目规范
- MCP 协议:接入第三方工具生态
- 多 Agent 协作:使用 LangGraph 实现多智能体协同
结语
Mini-Cursor 的核心并不复杂:给 LLM 装上几个工具,用 ReAct 循环让它不断推理-执行-观察,直到任务完成。真正的价值在于工程实践------精心设计的 SystemMessage、健壮的错误处理、合理的并行执行,这些才是让 Agent 稳定可靠的关键。
下一步,你可以尝试添加更多工具(如 Git 操作、数据库查询),或者接入 Memory 模块让 Agent 记住你的开发习惯。