从零手写一个简单的AI编程助手Agent:深入理解ReAct工作流
前言
最近Claude Code、Cursor等AI编程助手火遍全网,它们能自动理解需求、编写代码、运行命令,甚至帮你调试bug。你有没有好奇过,这些工具背后的核心原理是什么?
今天,我们就来手写一个简化版的Claude Code Agent,通过实践深入理解AI Agent的工作机制。相信我,这比你想象的要简单得多!
整体思路
我们的目标是创建一个能自动执行编程任务的Agent,比如让它“创建一个React + Vite的TodoList应用”。
整体架构分为三层:
-
LLM(大语言模型):负责理解和规划任务
-
Tools(工具集):让LLM能够操作文件系统、执行命令
-
Agent循环:协调LLM和Tools,完成复杂任务
用户需求 → LLM思考 → 调用工具 → 获取结果 → LLM再思考 → ... → 完成任务
技术选型
-
TypeScript:类型安全,开发体验好
-
LangChain:LLM应用开发框架,统一各家模型接口
-
OpenAI:使用GPT-4作为推理引擎
-
Node.js:运行环境
核心概念解析
1. Message体系
在LangChain中,对话由不同类型的消息组成:
// SystemMessage: 设定AI的角色和能力边界
new SystemMessage("你是一个编程助手,可以读写文件、执行命令...")
// HumanMessage: 用户输入
new HumanMessage("创建一个React + Vite的TodoList")
// AIMessage: AI的思考和回答
new AIMessage("我来帮你创建项目,首先使用Vite初始化...")
// ToolMessage: 工具执行结果
new ToolMessage({ content: "项目创建成功", toolCallId: "xxx" })
2. Tool机制
工具是LLM能力的延伸,让AI能够“动手干活”:
const writeFileTool = {
name: "write_file",
description: "写入文件到磁盘",
schema: z.object({
path: z.string(),
content: z.string()
}),
async execute({ path, content }) {
await fs.writeFile(path, content);
return "文件写入成功";
}
}
3. ReAct工作流
ReAct = Reason + Act,即“推理 + 行动”:
-
Reason(推理):LLM分析当前状态,决定下一步行动
-
Act(行动):执行选定的工具
-
Observe(观察):获取执行结果
-
循环上述步骤直到任务完成
代码实现
第一步:环境配置
import { ChatOpenAI } from "@langchain/openai";
import { SystemMessage, HumanMessage, AIMessage, ToolMessage } from "@langchain/core/messages";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import fs from "fs/promises";
import { exec } from "child_process";
import util from "util";
const execPromise = util.promisify(exec);
// 初始化LLM
const model = new ChatOpenAI({
modelName: "gpt-4",
temperature: 0,
apiKey: process.env.OPENAI_API_KEY
});
第二步:定义工具集
// 1. 写入文件工具
const writeFileTool = tool(
async ({ path, content }) => {
await fs.writeFile(path, content, "utf-8");
return `✅ 文件 ${path} 写入成功`;
},
{
name: "write_file",
description: "将内容写入指定路径的文件",
schema: z.object({
path: z.string().describe("文件路径"),
content: z.string().describe("要写入的内容")
})
}
);
// 2. 读取文件工具
const readFileTool = tool(
async ({ path }) => {
const content = await fs.readFile(path, "utf-8");
return content;
},
{
name: "read_file",
description: "读取指定路径的文件内容",
schema: z.object({
path: z.string().describe("文件路径")
})
}
);
// 3. 执行命令工具
const execCommandTool = tool(
async ({ command }) => {
try {
const { stdout, stderr } = await execPromise(command);
return stdout || stderr || "命令执行完成";
} catch (error) {
return `❌ 命令执行失败: ${error.message}`;
}
},
{
name: "execute_command",
description: "在终端执行shell命令",
schema: z.object({
command: z.string().describe("要执行的shell命令")
})
}
);
// 工具列表
const tools = [writeFileTool, readFileTool, execCommandTool];
const toolsByName = Object.fromEntries(
tools.map(t => [t.name, t])
);
第三步:实现Agent主循环
async function runAgent(userInput: string) {
// 消息历史
const messages = [
new SystemMessage(`你是一个智能编程助手,能够通过工具完成各种编程任务。
可用工具:
- write_file: 写入文件
- read_file: 读取文件
- execute_command: 执行shell命令
请按以下步骤思考:
1. 理解用户的完整需求
2. 规划实现步骤
3. 逐步执行,每次只调用一个工具
4. 遇到错误要分析原因并尝试修复`),
new HumanMessage(userInput)
];
// 绑定工具到模型
const modelWithTools = model.bindTools(tools);
let maxIterations = 20;
while (maxIterations-- > 0) {
console.log(`\n🔄 第 ${20 - maxIterations} 轮思考...`);
// 调用LLM
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 检查是否有工具调用
const toolCalls = response.additional_kwargs?.tool_calls || [];
if (toolCalls.length === 0) {
// 没有工具调用,任务完成
console.log("✅ 任务完成!");
console.log(response.content);
return response.content;
}
// 执行工具调用
for (const toolCall of toolCalls) {
const toolName = toolCall.function.name;
const toolArgs = JSON.parse(toolCall.function.arguments);
const toolId = toolCall.id;
console.log(`🔧 调用工具: ${toolName}`);
console.log(`📝 参数:`, toolArgs);
try {
// 执行工具
const tool = toolsByName[toolName];
if (!tool) {
throw new Error(`未知工具: ${toolName}`);
}
const result = await tool.invoke(toolArgs);
// 添加工具执行结果到消息历史
messages.push(new ToolMessage({
content: result,
toolCallId: toolId
}));
console.log(`✅ 工具执行成功`);
console.log(`📄 结果:`, result.slice(0, 200) + "...");
} catch (error) {
// 错误处理
messages.push(new ToolMessage({
content: `❌ 工具执行失败: ${error.message}`,
toolCallId: toolId
}));
console.log(`❌ 工具执行失败:`, error.message);
}
}
}
return "任务执行超时,请检查是否陷入死循环";
}
第四步:启动Agent
// 入口函数
async function main() {
const userRequest = process.argv[2] || "创建一个React + Vite的TodoList应用";
console.log("🚀 启动AI编程助手...");
console.log(`📋 任务: ${userRequest}\n`);
try {
const result = await runAgent(userRequest);
console.log("\n🎉 最终结果:");
console.log(result);
} catch (error) {
console.error("💥 发生错误:", error);
}
}
main();
运行演示
假设我们让Agent创建TodoList应用,它会这样工作:
🚀 启动AI编程助手...
📋 任务: 创建一个React + Vite的TodoList应用
🔄 第 1 轮思考...
🔧 调用工具: execute_command
📝 参数: { command: "npm create vite@latest todo-app -- --template react" }
✅ 工具执行成功
🔄 第 2 轮思考...
🔧 调用工具: execute_command
📝 参数: { command: "cd todo-app && npm install" }
✅ 工具执行成功
🔄 第 3 轮思考...
🔧 调用工具: write_file
📝 参数: { path: "todo-app/src/App.jsx", content: "..." }
✅ 工具执行成功
... (继续编写代码、运行项目)
✅ 任务完成!
🎉 你的TodoList应用已创建完成,运行 npm run dev 即可启动!
核心要点总结
1. LLM的局限性
大语言模型本身是无状态的,不能直接操作外部世界。它只能:
-
理解文本
-
生成文本
-
规划步骤
通过Tool机制,我们赋予LLM“手脚”,让它能真正干活。
2. 消息历史的重要性
Agent的“记忆”就是messages数组。每次交互都追加新消息:
-
LLM的思考和工具调用 → AIMessage
-
工具执行结果 → ToolMessage
这保证了Agent能“记住”之前做过的所有事情。
3. 错误处理机制
Agent必须能处理失败:
-
命令执行失败 → 分析错误 → 尝试修复
-
文件写入冲突 → 调整策略 → 重新执行
4. 工具设计原则
-
单一职责:每个工具只做一件事
-
清晰描述:让LLM理解工具的用途和参数
-
错误返回:返回详细的错误信息,帮助LLM调试
进阶优化方向
-
并行工具调用:使用
Promise.all同时执行多个独立操作 -
Token优化:压缩工具返回结果,避免上下文过长
-
安全控制:限制危险命令,添加用户确认环节
-
多模态支持:集成图像生成、代码可视化等能力
-
记忆持久化:保存对话历史,支持断点续传
结语
通过这篇文章,我们亲手实现了一个简化版的AI编程助手,揭开了Claude Code、Cursor等工具的神秘面纱。
核心原理并不复杂:
-
LLM负责思考和规划
-
Tools负责执行具体操作
-
Agent循环协调两者完成复杂任务
这只是一个开始。有了这个基础框架,你可以添加更多工具(如Git操作、API调用、数据库查询等),构建出更强大的AI助手。
技术改变世界,而AI正在改变技术本身。 希望这篇文章能帮助你更好地理解AI Agent的原理,甚至开发出自己的AI工具!
更多推荐


所有评论(0)