使用 LangChain 构建 AI 代理:自动化创建 React TodoList 应用

0 阅读6分钟

在前端开发领域,随着 AI 技术的快速发展,我经常探索如何将 AI 集成到日常开发流程中。今天,我想分享一个实际案例:使用 LangChain 框架构建一个 AI 代理,通过自定义工具来自动化创建和管理一个 React TodoList 应用。这个代理可以处理文件读写、命令执行和目录操作等任务,让开发过程更高效、智能。

LangChain 是一个强大的开源框架,用于构建基于大型语言模型(LLM)的应用。它允许我们定义“工具”(Tools),这些工具可以被 LLM 调用来执行具体操作,从而实现代理(Agent)式的交互。代理的核心在于循环迭代:LLM 思考、规划、使用工具、调整,直到任务完成。这比单纯的提示工程更智能,能处理复杂、多步任务。

在本文中,我将基于提供的代码实例逐步讲解。首先介绍四个核心工具的实现,然后说明如何将它们与 OpenAI 模型绑定,形成一个代理。最后,通过一个完整示例演示如何自动化创建 React TodoList 应用,包括项目初始化、代码编写、样式添加、动画实现和依赖安装。代码中有些小错误我会指正,以确保分享的知识准确可靠。所有示例都使用 Node.js 环境,依赖如 @langchain/core/tools、zod 等。

核心工具的实现

代理需要工具来与外部世界交互。我们定义了四个工具:读取文件、写入文件、执行命令和列出目录。这些工具使用 LangChain 的 tool 函数创建,每个工具包括异步执行逻辑、名称、描述和 Zod schema 用于参数验证。

首先是读取文件工具(readFileTool):

JavaScript

const readFileTool = tool(
  async ({filePath}) => {
    try {
      const content = await fs.readFile(filePath, 'utf-8');
      console.log(`[工具调用] read_file("${filePath}") 成功读取 ${content.length} 字节`);
      return `文件内容:\n${content}`;
    } catch (error) {
      console.log(`工具调用 read_file("${filePath}") 失败:${error.message}`);
      return `错误:${error.message}`;
    }
  },
  {
    name: 'read_file',
    description: '读取指定文件的内容',
    schema: z.object({
      filePath: z.string().describe('文件路径')
    })
  }
);

这个工具使用 Node.js 的 fs/promises 读取文件内容,返回 UTF-8 编码的字符串。如果失败,返回错误消息。日志记录有助于调试。

接下来是写入文件工具(writeFileTool):

JavaScript

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 (error) {
      console.log(`工具调用 write_file("${filePath}") 失败:${error.message}`);
      return `写入文件失败:${error.message}`;
    }
  },
  {
    name: "write_file",
    description: '向指定路径写入文件内容,自动创建目录',
    schema: z.object({
      filePath: z.string().describe('文件路径'),
      content: z.string().describe('要写入的文件内容')
    })
  }
);

这个工具先创建目录(使用 recursive: true 确保嵌套目录),然后写入内容。

第三个是执行命令工具(executeCommandTool),代码中名为 executeCommanTool,有拼写错误,应改为 executeCommandTool:

JavaScript

const executeCommandTool = tool(
  async ({command, workingDirectory}) => {
    const cwd = workingDirectory || process.cwd();
    console.log(`[工具调用] execute_command("${command}", 在目录 ${cwd} 执行命令`);
    return new Promise((resolve, reject) => {
      const [cmd, ...args] = command.split(' ');
      const child = spawn(cmd, args, {
        cwd,
        stdio: 'inherit',
        shell: true
      });
      let errorMsg = '';
      child.on('error', (error) => {
        errorMsg = error.message;
      });
      child.on('close', (code) => {
        if (code === 0) {
          console.log(`[工具调用] execute_command("${command}") 命令执行成功,子进程退出`);
          const cwdInfo = workingDirectory ?
            `\n\n重要提示:命令在目录"${workingDirectory}"中执行成功。
            如果需要在这个项目目录中继续执行命令,请使用 workingDirectory
            "${workingDirectory}" 参数,不要使用 cd 命令`
            : ``;
          resolve(`命令执行成功, ${command} ${cwdInfo}`);
        } else {
          if (errorMsg) {
            console.error(`错误:${errorMsg}`);
          }
          reject(`命令执行失败,退出码:${code}`);
        }
      });
    });
  },
  {
    name: 'execute_command',
    description: '执行系统命令,支持指定工作目录,实时显示输出',
    schema: z.object({
      command: z.string().describe('要执行的命令'),
      workingDirectory: z.string().optional().describe('指定工作目录,默认当前目录')
    })
  }
);

这个工具使用 child_process.spawn 执行命令,支持 workingDirectory 参数,避免手动 cd。Promise 结构处理异步,成功时返回提示信息,提醒后续命令使用 workingDirectory 而非 cd。

最后一个是列出目录工具(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(f => `- ${f}`).join('\n')}`;
    } catch (error) {
      console.log(`工具调用 list_directory("${directoryPath}") 失败:${error.message}`);
      return `列出目录失败:${error.message}`;
    }
  },
  {
    name: 'list_directory',
    description: "列出指定目录的所有文件和文件夹",
    schema: z.object({
      directoryPath: z.string().describe('要列出的目录路径'),
    })
  }
);

简单高效,使用 fs.readdir 返回文件列表,以 Markdown 格式输出,便于阅读。

这些工具导出后,可在代理中使用。

构建 AI 代理

现在,我们将这些工具与 LLM 绑定。使用 @langchain/openai 的 ChatOpenAI,配置模型、API 密钥和 baseURL(从 .env 加载)。

JavaScript

const model = new ChatOpenAI({
  modelName: process.env.MODEL_NAME,
  apiKey: process.env.OPENAI_API_KEY,
  configuration: {
    baseURL: process.env.OPENAI_API_BASE_URL,
  }
});

const tools = [
  readFileTool,
  writeFileTool,
  executeCommandTool,
  listDirectoryTool,
];

const modelWithTools = model.bindTools(tools);

代理函数 runAgentWithTools 接受查询,初始化消息数组,包括系统提示:

JavaScript

const messages = [
  new SystemMessage(`
    你是一个项目管理助手,使用工具完成任务。
    当前工作目录:${process.cwd()}
    
    工具:
    1. read_file: 读取文件
    2. write_file: 写入文件
    3. execute_command: 执行命令(支持workingDirectory参数)
    4. list_directory: 列出目录
    
    重要规则 - execute_command:
    - workingDirectory 参数会自动切换到指定目录
    - 当使用workingDirectory参数时,不要在command中使用cd命令
    - 错误示例: { command: "cd react-todo-app && pnpm install", workingDirectory: "react-todo-app" }
    这是错误的!因为 workingDirectory 已经在 react-todo-app 目录了,再 cd react-todo-app 会找不到目录
    - 正确示例: { command: "pnpm install", workingDirectory: "react-todo-app" }
    这样就对了!workingDirectory 已经切换到 react-todo-app,直接执行命令即可

    回复要简洁,只说做了什么
  `),
  new HumanMessage(query),
];

系统提示定义角色、工具和规则,确保代理高效。循环迭代(最多 30 次):

  • 调用 modelWithTools.invoke(messages),获取响应。
  • 如果无 tool_calls,任务完成,返回内容。
  • 否则,执行每个 tool_call,添加 ToolMessage 到 messages。

使用 chalk 彩色日志增强可读性。

示例:自动化创建 React TodoList 应用

让我们看一个实际查询(case1):

JavaScript

const case1 = `
创建一个功能丰富的 React TodoList 应用:

1. 创建项目:echo -e "n\nn" | pnpm create vite react-todo-app --template react-ts
2. 修改 src/App.tsx,实现完整功能的 TodoList:
 - 添加、删除、编辑、标记完成
 - 分类筛选(全部/进行中/已完成)
 - 统计信息显示
 - localStorage 数据持久化
3. 添加复杂样式:
 - 渐变背景(蓝到紫)
 - 卡片阴影、圆角
 - 悬停效果
4. 添加动画:
 - 添加/删除时的过渡动画
 - 使用 CSS transitions
5. 列出目录确认

注意:使用 pnpm,功能要完整,样式要美观,要有动画效果

之后在 react-todo-app 项目中:
1. 使用 pnpm install 安装依赖
2. 使用 pnpm run dev 启动服务器
`;

代理接收这个查询,启动循环。LLM 先规划:使用 execute_command 执行项目创建命令(echo -e "n\nn" | pnpm create vite react-todo-app --template react-ts)。成功后,切换 workingDirectory 到 react-todo-app。

然后,读取 src/App.tsx(read_file),修改内容(write_file),实现 TodoList 逻辑。代理会生成 React 代码,包括 useState、管理 todos、localStorage 持久化、筛选过滤器、统计(如完成数)、CSS 样式(background: linear-gradient(to right, blue, purple); box-shadow, border-radius, :hover 效果)和过渡动画(transition: opacity 0.3s;)。

例如,代理可能生成的 App.tsx 片段(简化):

tsx

import React, { useState, useEffect } from 'react';
import './App.css';

interface Todo {
  id: number;
  text: string;
  completed: boolean;
}

function App() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [input, setInput] = useState('');
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
  // ... 添加、删除、编辑、标记完成逻辑
  useEffect(() => {
    const stored = localStorage.getItem('todos');
    if (stored) setTodos(JSON.parse(stored));
  }, []);
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);
  // 筛选和统计
  const filteredTodos = todos.filter(/* 根据 filter */);
  const completedCount = todos.filter(t => t.completed).length;
  return (
    <div className="app">
      {/* UI 组件,带动画 class */}
    </div>
  );
}

添加 App.css 以渐变背景、阴影等。

代理继续:list_directory 确认文件,使用 execute_command 安装依赖(pnpm install, workingDirectory: "react-todo-app"),然后 pnpm run dev 启动。

整个过程代理自动迭代,处理错误(如目录不存在),直到完成。输出如 "项目创建成功,代码修改完成,服务器启动"。

结语

通过 LangChain 构建 AI 代理,我们能自动化前端开发任务,如创建 React TodoList,提升效率。