阿里本地通
101.9MB · 2026-02-28
在 LangChain、AutoGen 等框架层出不穷的今天,为什么还要坚持“手搓”?
因为框架封装了细节,也屏蔽了灵魂。
当你调用 agent.run() 时,你看到的只是一个结果。你看不见大模型是如何决定调用工具的,看不见工具执行失败后它是如何“反思”的,更看不见那个让 AI 从“聊天机器人”进化为“智能体”的核心引擎——ReAct 循环(Reasoning + Acting) 。
手搓 Agent 的意义在于:
while 循环中的消息流转,才能真正理解 Agent 的“思考”过程。今天,我们就用不到 200 行核心代码,揭开 AI Agent 的神秘面纱。
一个完整的 Agent 系统由三部分组成:
在 Node.js 环境中,最强大的能力莫过于操作文件系统和执行 Shell 命令。我们利用 node:fs/promises 和 node:child_process 模块,结合 LangChain 的 tool 函数,定义了四个核心工具。
try-catch 中。AI 可能会尝试读取不存在的文件,或者写入权限不足的目录,工具必须优雅地返回错误信息,而不是让进程崩溃。write_file 工具中,我们使用了 fs.mkdir(dir, { recursive: true })。这意味着 AI 可以直接写入 /src/components/Button.tsx,即使中间目录不存在,系统也会自动创建。这对代码生成场景至关重要。// 核心逻辑示意:写入文件工具
const writeFileTool = tool(
async ({ filePath, content }) => {
const dir = path.dirname(filePath);
// 递归创建目录,确保路径存在
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(filePath, content, 'utf-8');
return `文件写入成功: ${filePath}`;
},
{
name: 'write_file',
description: '向指定路径写入文件内容,自动创建目录',
schema: z.object({
filePath: z.string().describe('文件路径'),
content: z.string().describe('要写入的文件内容'),
}),
}
);
在实现 execute_command 时,初学者常犯两个致命错误:
process.exit:如果命令执行失败(如 npm install 报错),直接退出进程会导致 Agent 死亡。正确的做法是将错误输出作为字符串返回给 LLM,让它自行分析并尝试修复。workingDirectory 参数显式指定 cwd,并在 System Prompt 中严厉禁止 AI 在命令中混用 cd,避免了路径混乱。有了工具,如何让大模型知道它们的存在?
LangChain 提供了 model.bindTools(tools) 方法。这行代码看似简单,实则完成了复杂的“神经连接”:
tool_calls 结构,而不仅仅是普通文本。const modelWithTools = model.bindTools(tools);
此时,modelWithTools 不再是一个普通的聊天模型,而是一个随时准备调用工具的智能体代理。
这是整个 Agent 最核心的部分。如果没有这个循环,AI 只能调用一次工具就停止,无法完成多步骤任务。
我们在 runAgentWithTools 函数中实现了一个经典的 ReAct 循环:
for (let i = 0; i < maxIterations; i++) {
// 1. 思考 (Reasoning)
const response = await modelWithTools.invoke(messages);
messages.push(response);
// 2. 判断是否结束
if (!response.tool_calls || response.tool_calls.length === 0) {
return response.content; // 没有工具调用,说明任务完成,返回最终回复
}
// 3. 行动 (Acting) & 观察 (Observation)
for (const toolCall of response.tool_calls) {
const toolResult = await foundTool.invoke(toolCall.args);
// 将工具执行结果作为“观察”反馈给模型
messages.push(new ToolMessage(toolResult, toolCall.id));
}
}
messages 数组(包含用户指令、历史对话、之前的工具执行结果)。它分析现状,决定下一步该做什么。如果需要工具,它会输出一个包含 tool_calls 的消息。tool_calls,遍历每一个调用,找到对应的工具函数并执行。这是 AI 真正“动手”的时刻——文件被创建,命令被运行。ToolMessage,再次推入 messages 数组。这个循环不断迭代,直到模型认为任务完成(不再调用工具)或达到最大迭代次数。
理论讲得再多,不如看它干点实事。我们给 Agent 下达了一个复杂指令:
第一轮思考:
execute_command,执行 pnpm create vite react-todo-app --template react-ts。react-todo-app 已创建。第二轮思考:
workingDirectory: "react-todo-app" 参数,而没有在命令里写 cd。execute_command,执行 pnpm install。第三轮思考:
src/App.tsx 实现业务逻辑。read_file 读取原有代码(了解结构),然后调用 write_file 写入包含完整逻辑(State 管理、LocalStorage、筛选逻辑)的新代码。第四轮思考:
write_file 修改 App.css,加入渐变背景、卡片阴影和 Keyframes 动画。第五轮思考:
list_directory 确认文件无误,最后调用 execute_command 执行 pnpm run dev。最终回复:
整个过程无需人工干预,Agent 自主完成了环境搭建、编码、样式设计、服务启动的全流程。
虽然基础版已经能跑通,但在生产环境中,我们还需要考虑更多:
目前的 execute_command 拥有执行任意命令的权限,这是危险的。
pnpm, npm, ls, cat 等安全命令;或者使用 Docker 容器运行 Agent,限制其文件系统访问权限。随着循环次数增加,messages 数组会越来越长,可能超出模型的 Token 限制。
目前的 Agent 遇到未知错误可能会陷入死循环。
通过手搓这个 Agent,我们不仅实现了一个自动写代码的工具,更深刻理解了AI 2.0 时代的交互范式转变:
这段代码只是一个起点。你可以在此基础上扩展更多工具(如数据库操作、API 调用、浏览器自动化),将其进化为一个全能的 DevOps 助手。
技术栈是死的,但赋予 AI“行动力”的思想是活的。 当你看着终端里自动跳动的日志,看着一个个文件被自动生成,你会意识到:我们不再是代码的搬运工,我们是智能体的架构师。