手搓 AI Agent:从零构建能自动写代码、跑命令的“数字员工”

62 阅读9分钟

导读:当大模型还在和你“纸上谈兵”时,真正的 AI Agent 已经挽起袖子,在终端里敲下 pnpm install,创建项目结构,甚至修复编译错误。本文将带你深入 LangChain 核心,不依赖任何黑盒框架,仅用 Node.js 原生能力 + LangChain 基础组件,手搓一个具备文件读写、目录浏览、命令执行能力的自主智能体(Agent)。我们将剖析“思考 - 行动 - 观察”循环的本质,并实战打造一个能独立开发 React 应用的 AI 助手。


一、为什么我们需要“手搓”Agent?

在 LangChain、AutoGen 等框架层出不穷的今天,为什么还要坚持“手搓”?

因为框架封装了细节,也屏蔽了灵魂

当你调用 agent.run() 时,你看到的只是一个结果。你看不见大模型是如何决定调用工具的,看不见工具执行失败后它是如何“反思”的,更看不见那个让 AI 从“聊天机器人”进化为“智能体”的核心引擎——ReAct 循环(Reasoning + Acting)

手搓 Agent 的意义在于:

  1. 掌控力:完全控制工具的边界、错误处理逻辑和上下文传递机制。
  2. 理解深度:亲手实现 while 循环中的消息流转,才能真正理解 Agent 的“思考”过程。
  3. 定制化:你可以轻松加入日志染色、执行沙箱、权限控制等企业级需求,而不受框架限制。

今天,我们就用不到 200 行核心代码,揭开 AI Agent 的神秘面纱。


二、核心架构:赋予 AI“手脚”与“大脑”

一个完整的 Agent 系统由三部分组成:

  • 大脑(LLM) :负责推理、规划和决策。
  • 手脚(Tools) :负责执行具体操作(读写文件、运行命令)。
  • 神经系统(Loop) :负责连接大脑与手脚,形成“感知 - 决策 - 行动”的闭环。

1. 打造“手脚”:定义原生工具

在 Node.js 环境中,最强大的能力莫过于操作文件系统和执行 Shell 命令。我们利用 node:fs/promisesnode:child_process 模块,结合 LangChain 的 tool 函数,定义了四个核心工具。

关键设计点:

  • Zod Schema 校验:每个工具都通过 Zod 定义严格的输入参数 schema。这不仅防止了 AI 生成错误的参数格式,还为 LLM 提供了清晰的“工具说明书”。
  • 防御性编程:所有文件操作都包裹在 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 时,初学者常犯两个致命错误:

  1. 直接 process.exit:如果命令执行失败(如 npm install 报错),直接退出进程会导致 Agent 死亡。正确的做法是将错误输出作为字符串返回给 LLM,让它自行分析并尝试修复。
  2. 忽略工作目录:很多命令依赖于当前路径。我们通过 workingDirectory 参数显式指定 cwd,并在 System Prompt 中严厉禁止 AI 在命令中混用 cd,避免了路径混乱。

2. 激活“大脑”:绑定工具与上下文

有了工具,如何让大模型知道它们的存在?

LangChain 提供了 model.bindTools(tools) 方法。这行代码看似简单,实则完成了复杂的“神经连接”:

  • 它将工具的元数据(名称、描述、参数 Schema)注入到模型的 System Prompt 中。
  • 它修改了模型的输出解析器,使其能够识别并生成特殊的 tool_calls 结构,而不仅仅是普通文本。
const modelWithTools = model.bindTools(tools);

此时,modelWithTools 不再是一个普通的聊天模型,而是一个随时准备调用工具的智能体代理


三、灵魂引擎:手写 ReAct 循环

这是整个 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));
  }
}

循环的三个阶段详解:

  1. 思考 (Reasoning)
    模型接收当前的 messages 数组(包含用户指令、历史对话、之前的工具执行结果)。它分析现状,决定下一步该做什么。如果需要工具,它会输出一个包含 tool_calls 的消息。
  2. 行动 (Acting)
    主程序检测到 tool_calls,遍历每一个调用,找到对应的工具函数并执行。这是 AI 真正“动手”的时刻——文件被创建,命令被运行。
  3. 观察 (Observation)
    工具执行完毕后,返回结果(无论是成功的内容还是失败的报错)。我们将这些结果封装成 ToolMessage,再次推入 messages 数组。
    关键点:这一步相当于把“眼睛”看到的结果喂回给“大脑”。模型看到报错信息后,会在下一轮循环中调整策略(例如:“安装失败了,可能是源的问题,我换个源再试一次”)。

这个循环不断迭代,直到模型认为任务完成(不再调用工具)或达到最大迭代次数。


四、实战演练:从 0 到 1 开发 React 应用

理论讲得再多,不如看它干点实事。我们给 Agent 下达了一个复杂指令:

“创建一个功能丰富的 React TodoList 应用:使用 pnpm 创建 vite 项目,实现增删改查、分类筛选、本地存储,添加渐变背景和动画,最后启动服务器。”

Agent 的执行流实录:

  1. 第一轮思考

    • 决策:需要先创建项目骨架。
    • 行动:调用 execute_command,执行 pnpm create vite react-todo-app --template react-ts
    • 观察:命令成功,目录 react-todo-app 已创建。
  2. 第二轮思考

    • 决策:进入项目目录安装依赖。注意,这里它严格遵守了规则,使用 workingDirectory: "react-todo-app" 参数,而没有在命令里写 cd
    • 行动:调用 execute_command,执行 pnpm install
    • 观察:依赖安装完成。
  3. 第三轮思考

    • 决策:需要修改 src/App.tsx 实现业务逻辑。
    • 行动:先调用 read_file 读取原有代码(了解结构),然后调用 write_file 写入包含完整逻辑(State 管理、LocalStorage、筛选逻辑)的新代码。
    • 观察:文件写入成功。
  4. 第四轮思考

    • 决策:添加 CSS 样式和动画。
    • 行动:调用 write_file 修改 App.css,加入渐变背景、卡片阴影和 Keyframes 动画。
    • 观察:样式更新成功。
  5. 第五轮思考

    • 决策:验证项目结构并启动。
    • 行动:调用 list_directory 确认文件无误,最后调用 execute_command 执行 pnpm run dev
    • 观察:服务器启动成功,输出 Localhost 地址。
  6. 最终回复

    • 模型检测到无需更多操作,输出:“React TodoList 应用已创建并启动,访问 http://localhost:5173 查看。功能包含...(简述)”。

整个过程无需人工干预,Agent 自主完成了环境搭建、编码、样式设计、服务启动的全流程。


五、进阶优化:让 Agent 更稳健

虽然基础版已经能跑通,但在生产环境中,我们还需要考虑更多:

1. 安全性沙箱

目前的 execute_command 拥有执行任意命令的权限,这是危险的。

  • 解决方案:在工具内部建立白名单机制,只允许执行 pnpm, npm, ls, cat 等安全命令;或者使用 Docker 容器运行 Agent,限制其文件系统访问权限。

2. 上下文窗口管理

随着循环次数增加,messages 数组会越来越长,可能超出模型的 Token 限制。

  • 解决方案:实现滑动窗口机制,只保留最近的 N 轮对话;或者对早期的工具执行结果进行摘要压缩。

3. 错误自愈能力

目前的 Agent 遇到未知错误可能会陷入死循环。

  • 解决方案:在 System Prompt 中加入“最大重试次数”指令,或者引入“人类介入”机制,当连续失败 3 次时暂停并请求人工帮助。

六、结语:Agent 时代的开发者新范式

通过手搓这个 Agent,我们不仅实现了一个自动写代码的工具,更深刻理解了AI 2.0 时代的交互范式转变:

  • 从 Prompt Engineering 到 Agent Engineering:未来的核心竞争力不再是写出完美的提示词,而是设计出能自主规划、可靠执行的任务代理。
  • 从“辅助编程”到“自主交付” :AI 不再仅仅是补全代码片段,而是能够独立交付完整的功能模块甚至小型应用。

这段代码只是一个起点。你可以在此基础上扩展更多工具(如数据库操作、API 调用、浏览器自动化),将其进化为一个全能的 DevOps 助手。

技术栈是死的,但赋予 AI“行动力”的思想是活的。 当你看着终端里自动跳动的日志,看着一个个文件被自动生成,你会意识到:我们不再是代码的搬运工,我们是智能体的架构师。


互动话题:如果你有一个全能的 AI Agent,你最想让它帮你完成什么重复性工作?欢迎在评论区留言讨论!