Skip to content
陈广亮的技术博客
Go back

AI Agent 前端工作流(一):理解 Agent 与组件自动生成实战

前端开发正在经历一场范式转变。我不是在说又一个新框架,而是工作方式的根本改变——从「写代码」到「指挥 AI 写代码」。但市面上大多数 AI 编程助手本质上还是「智能补全」,你写一行,它猜下一行。真正改变游戏规则的是 AI Agent

AI Agent 不是聊天机器人

很多人第一次接触 AI 编程工具时,会觉得它像个「很懂代码的 ChatGPT」。这种理解差了十万八千里。

传统 AI 编程助手(如早期的 GitHub Copilot)是被动响应型的:

而 AI Agent 是主动执行型的:

举个具体例子。如果你对 Copilot 说「生成一个用户卡片组件」,它会给你一段代码,然后就没了。你要自己复制、粘贴、保存文件、检查类型错误、写样式。

而对一个前端 AI Agent 说同样的话,它会:

  1. 分析项目结构,找到组件存放位置
  2. 检查现有的设计系统和样式规范
  3. 生成符合项目风格的 TypeScript 代码
  4. 自动创建文件并保存
  5. 运行类型检查,如果有错误就修复
  6. 甚至可以生成对应的 Storybook 文档

这就是质的区别。Agent 是能干活的,Assistant 只是能聊天的。

核心差异:工具调用能力

AI Agent 的灵魂是 Function Calling(也叫 Tool Use)。这是 2024 年开始大规模商用的技术,Anthropic、OpenAI 都支持。

简单说,你可以给 AI 定义一组「工具」,比如:

const tools = [
  {
    name: "read_file",
    description: "读取项目文件内容",
    input_schema: {
      type: "object",
      properties: {
        path: { type: "string", description: "文件路径" }
      }
    }
  },
  {
    name: "write_file",
    description: "创建或修改文件",
    input_schema: {
      type: "object",
      properties: {
        path: { type: "string" },
        content: { type: "string" }
      }
    }
  }
]

然后把这些工具和用户请求一起发给 AI。AI 会决定:

  1. 我需要用哪个工具?
  2. 参数是什么?
  3. 工具返回结果后,我下一步该做什么?

这就像给 AI 配了一个工具箱。以前它只能「说」,现在它能「做」。

为什么前端开发特别适合 Agent?

前端开发有几个痛点,天然适合用 Agent 解决:

1. 重复劳动太多
创建一个新页面:建文件夹、写组件、配路由、写样式、加类型、写测试……每一步都很机械,但又不能出错。

2. 上下文切换频繁
你要同时关注业务逻辑、UI 交互、性能优化、类型安全、可访问性……人脑很难同时 hold 住这么多维度。

3. 工具链复杂
TypeScript、ESLint、Prettier、Tailwind、Vite……每个工具都有自己的配置和规则。新手光配环境就要一周。

4. 设计稿到代码的鸿沟
设计师给你一个 Figma,你要把视觉语言翻译成代码。这个过程很耗时,而且容易出错。

AI Agent 可以自动化这些流程。它不是替代你写代码,而是把你从机械劳动中解放出来,让你专注于「这个组件应该怎么设计」,而不是「这个 className 拼错了没有」。

工作原理深入:从 Function Calling 到自主循环

让我们拆解一个完整的 AI Agent 工作流程。以「生成 React 组件」为例:

1. Function Calling 机制

用户请求:「生成一个可排序的表格组件」

AI 收到请求后,会进行思维链推理(Chain of Thought):

我需要:
1. 先看看项目里有没有类似的组件(工具:search_codebase)
2. 检查项目用的是什么 UI 库(工具:read_file package.json)
3. 生成代码
4. 保存文件(工具:write_file)
5. 运行类型检查(工具:run_command "tsc --noEmit")

然后它会返回一个 tool_use 请求:

{
  "type": "tool_use",
  "id": "toolu_01A",
  "name": "read_file",
  "input": {
    "path": "package.json"
  }
}

你的代码执行这个工具,把结果返回给 AI。AI 继续下一步。这个循环会一直持续,直到任务完成。

2. 上下文管理:不让 Token 爆炸

AI 的「记忆」是有限的。Claude 3.5 Sonnet 的上下文窗口是 200K tokens,看起来很大,但一个中型项目的所有代码加起来可能就超过这个数字。

怎么办?选择性加载上下文

聪明的 Agent 不会一次性把整个项目塞给 AI,而是:

举例:生成一个新组件时,Agent 会:

// 高优先级:设计系统相关
- components/design-system/Button.tsx  // 看看按钮怎么写的
- styles/theme.ts  // 主题配置
- tsconfig.json  // TypeScript 规则

// 中优先级:相似组件
- components/Table.tsx  // 如果已经有表格组件

// 低优先级:其他
- package.json  // 只看依赖列表,不看 devDependencies

这就像人类开发时的思维方式:你不会为了写一个按钮组件就把整个项目所有代码重读一遍,你只会参考相关的部分。

3. 工具链设计:CLI 还是 IDE 插件?

当前主流的 AI 编程工具分两派:

CLI 派(如 Cursor、Aider)

IDE 插件派(如 GitHub Copilot、Continue)

我个人倾向于 混合方案

这和你平时开发时同时开着 VSCode 和终端是一个道理。

实战:组件自动生成 Agent

说了这么多理论,来点实战的。我们用 Claude API 实现一个简单但完整的「React 组件生成器」。

场景设定

需求:「生成一个用户评分卡片组件,支持 1-5 星评分,显示用户头像、名字、评论内容和时间」

完整代码实现

// component-generator.ts
import Anthropic from '@anthropic-ai/sdk';
import fs from 'fs/promises';
import path from 'path';

interface GenerateComponentOptions {
  description: string;  // 组件需求描述
  outputDir: string;    // 输出目录
  projectContext?: {    // 项目上下文(可选)
    usesTailwind: boolean;
    usesTypeScript: boolean;
  };
}

class ComponentGenerator {
  private client: Anthropic;
  
  constructor(apiKey: string) {
    this.client = new Anthropic({ apiKey });
  }

  /**
   * 定义 Agent 可用的工具
   */
  private getTools() {
    return [
      {
        name: 'write_component_file',
        description: '创建 React 组件文件',
        input_schema: {
          type: 'object',
          properties: {
            filename: {
              type: 'string',
              description: '文件名(不含路径),如 UserRatingCard.tsx'
            },
            content: {
              type: 'string',
              description: '完整的组件代码'
            }
          },
          required: ['filename', 'content']
        }
      },
      {
        name: 'validate_typescript',
        description: '验证 TypeScript 代码是否有类型错误',
        input_schema: {
          type: 'object',
          properties: {
            code: { type: 'string', description: 'TypeScript 代码' }
          },
          required: ['code']
        }
      }
    ];
  }

  /**
   * 执行工具调用
   */
  private async executeTool(
    toolName: string,
    toolInput: Record<string, unknown>,
    outputDir: string
  ): Promise<string> {
    switch (toolName) {
      case 'write_component_file': {
        const { filename, content } = toolInput as { 
          filename: string; 
          content: string; 
        };
        const filePath = path.join(outputDir, filename);
        
        try {
          await fs.mkdir(outputDir, { recursive: true });
          await fs.writeFile(filePath, content, 'utf-8');
          return `文件已创建: ${filePath}`;
        } catch (error) {
          return `创建文件失败: ${(error as Error).message}`;
        }
      }

      case 'validate_typescript': {
        const { code } = toolInput as { code: string };
        
        // 简单的静态检查(实际项目中应该调用 tsc)
        const hasExport = /export\s+(default\s+)?(function|const|class)/.test(code);
        const hasProps = /interface\s+\w+Props/.test(code) || /type\s+\w+Props/.test(code);
        
        if (!hasExport) {
          return '错误:组件没有 export';
        }
        if (!hasProps && code.includes('props')) {
          return '警告:使用了 props 但未定义类型';
        }
        
        return '类型检查通过';
      }

      default:
        return `未知工具: ${toolName}`;
    }
  }

  /**
   * 生成组件的核心循环
   */
  async generate(options: GenerateComponentOptions): Promise<string> {
    const { description, outputDir, projectContext } = options;
    
    // 构建初始 prompt
    const systemPrompt = `你是一个专业的 React 组件生成助手。
    
项目配置:
- TypeScript: ${projectContext?.usesTypeScript ? '' : ''}
- Tailwind CSS: ${projectContext?.usesTailwind ? '' : ''}

要求:
1. 生成的组件必须是 TypeScript(.tsx)
2. Props 必须有完整的类型定义
3. 使用函数组件 + Hooks
4. ${projectContext?.usesTailwind ? '使用 Tailwind 类名' : '使用 CSS-in-JS'}
5. 包含详细的注释
6. 生成完成后调用 write_component_file 保存文件

注意:
- 组件名用 PascalCase
- 文件名和组件名一致
- 代码必须可直接运行,不要有占位符`;

    const messages: Anthropic.MessageParam[] = [
      {
        role: 'user',
        content: `请生成组件:${description}`
      }
    ];

    let continueLoop = true;
    let iterations = 0;
    const maxIterations = 10;  // 防止无限循环
    let finalOutput = '';

    // Agent 循环
    while (continueLoop && iterations < maxIterations) {
      iterations++;
      
      const response = await this.client.messages.create({
        model: 'claude-3-5-sonnet-20241022',
        max_tokens: 4096,
        system: systemPrompt,
        messages,
        tools: this.getTools(),
      });

      // 检查 stop_reason
      if (response.stop_reason === 'end_turn') {
        continueLoop = false;
      }

      // 处理响应内容
      const assistantMessage: Anthropic.MessageParam = {
        role: 'assistant',
        content: response.content
      };
      messages.push(assistantMessage);

      // 执行工具调用
      const toolResults: Anthropic.ToolResultBlockParam[] = [];
      
      for (const block of response.content) {
        if (block.type === 'text') {
          finalOutput += block.text + '\n';
        }
        
        if (block.type === 'tool_use') {
          console.log(`🔧 执行工具: ${block.name}`);
          console.log(`📥 参数:`, JSON.stringify(block.input, null, 2));
          
          const result = await this.executeTool(
            block.name,
            block.input,
            outputDir
          );
          
          console.log(`📤 结果: ${result}\n`);
          
          toolResults.push({
            type: 'tool_result',
            tool_use_id: block.id,
            content: result
          });
        }
      }

      // 如果有工具调用,继续循环
      if (toolResults.length > 0) {
        messages.push({
          role: 'user',
          content: toolResults
        });
        continueLoop = true;
      }
    }

    return finalOutput;
  }

  /**
   * 估算 token 消耗和成本
   */
  estimateCost(inputTokens: number, outputTokens: number): {
    tokens: { input: number; output: number };
    cost: number;
  } {
    // Claude 3.5 Sonnet 定价 (2026-02-13)
    // Input: $3 / 1M tokens
    // Output: $15 / 1M tokens
    const inputCost = (inputTokens / 1_000_000) * 3;
    const outputCost = (outputTokens / 1_000_000) * 15;
    
    return {
      tokens: { input: inputTokens, output: outputTokens },
      cost: inputCost + outputCost
    };
  }
}

// 使用示例
async function main() {
  const apiKey = process.env.ANTHROPIC_API_KEY;
  if (!apiKey) {
    throw new Error('请设置 ANTHROPIC_API_KEY 环境变量');
  }

  const generator = new ComponentGenerator(apiKey);
  
  const result = await generator.generate({
    description: '用户评分卡片组件,支持 1-5 星评分,显示用户头像、名字、评论内容和时间',
    outputDir: './generated-components',
    projectContext: {
      usesTailwind: true,
      usesTypeScript: true
    }
  });

  console.log('生成完成!');
  console.log(result);
}

// 错误处理
main().catch(error => {
  console.error('生成失败:', error);
  process.exit(1);
});

生成的组件示例

上面的代码运行后,会自动生成类似这样的组件:

// UserRatingCard.tsx
import React from 'react';

/**
 * 用户评分卡片组件的 Props 类型定义
 */
interface UserRatingCardProps {
  /** 用户头像 URL */
  avatarUrl: string;
  /** 用户名 */
  userName: string;
  /** 评分(1-5) */
  rating: 1 | 2 | 3 | 4 | 5;
  /** 评论内容 */
  comment: string;
  /** 评论时间(ISO 8601 格式) */
  timestamp: string;
  /** 自定义类名 */
  className?: string;
}

/**
 * 用户评分卡片组件
 * 
 * @example
 * <UserRatingCard
 *   avatarUrl="https://i.pravatar.cc/150?img=1"
 *   userName="张三"
 *   rating={5}
 *   comment="非常好用!"
 *   timestamp="2026-02-13T10:00:00Z"
 * />
 */
export default function UserRatingCard({
  avatarUrl,
  userName,
  rating,
  comment,
  timestamp,
  className = ''
}: UserRatingCardProps) {
  // 格式化时间显示
  const formatTime = (isoString: string): string => {
    const date = new Date(isoString);
    const now = new Date();
    const diffMs = now.getTime() - date.getTime();
    const diffMins = Math.floor(diffMs / 60000);
    
    if (diffMins < 60) return `${diffMins} 分钟前`;
    if (diffMins < 1440) return `${Math.floor(diffMins / 60)} 小时前`;
    return date.toLocaleDateString('zh-CN');
  };

  // 渲染星星
  const renderStars = () => {
    return Array.from({ length: 5 }, (_, i) => (
      <svg
        key={i}
        className={`w-5 h-5 ${
          i < rating ? 'text-yellow-400 fill-current' : 'text-gray-300'
        }`}
        viewBox="0 0 20 20"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path d="M10 15l-5.878 3.09 1.123-6.545L.489 6.91l6.572-.955L10 0l2.939 5.955 6.572.955-4.756 4.635 1.123 6.545z" />
      </svg>
    ));
  };

  return (
    <div className={`bg-white rounded-lg shadow-md p-6 ${className}`}>
      {/* 用户信息区 */}
      <div className="flex items-center gap-4 mb-4">
        <img
          src={avatarUrl}
          alt={userName}
          className="w-12 h-12 rounded-full object-cover"
          loading="lazy"
        />
        <div className="flex-1">
          <h3 className="font-semibold text-gray-900">{userName}</h3>
          <div className="flex items-center gap-2">
            <div className="flex gap-1">{renderStars()}</div>
            <span className="text-sm text-gray-500">
              {formatTime(timestamp)}
            </span>
          </div>
        </div>
      </div>

      {/* 评论内容 */}
      <p className="text-gray-700 leading-relaxed">{comment}</p>
    </div>
  );
}

实际效果分析

运行上面的代码后,你会得到:

  1. ✅ 完整的 TypeScript 类型定义
  2. ✅ Tailwind CSS 样式(响应式、可访问性)
  3. ✅ 时间格式化逻辑(相对时间显示)
  4. ✅ 详细的注释和使用示例
  5. ✅ 懒加载图片优化

Token 消耗估算

对比手写:如果你从零开始写这个组件,包括类型定义、样式调整、时间格式化逻辑,大概需要 15-20 分钟。AI Agent 只用了 10 秒。

Prompt 设计技巧

上面代码中的 systemPrompt 是关键。好的 Prompt 需要:

1. 明确约束条件
不要说「生成一个组件」,要说「生成一个 TypeScript React 函数组件,使用 Tailwind CSS,Props 必须有类型定义」。

2. 提供上下文
告诉 AI 项目的技术栈、代码风格、命名规范。你给的信息越详细,生成的代码越符合预期。

3. 强调关键点
「代码必须可直接运行,不要有占位符」——这句话能避免 AI 生成 // TODO: implement this 这种废话。

4. 分步引导
如果任务复杂,可以在 Prompt 里写「步骤 1: xxx,步骤 2: xxx」,帮助 AI 规划任务。

边界情况处理

实际使用中,你还需要考虑:

// 处理 API 错误
try {
  const response = await this.client.messages.create({...});
} catch (error) {
  if (error instanceof Anthropic.APIError) {
    if (error.status === 429) {
      // 速率限制,等待后重试
      await sleep(1000);
      return this.generate(options);
    }
    throw new Error(`API 错误: ${error.message}`);
  }
  throw error;
}

// 防止无限循环
if (iterations >= maxIterations) {
  throw new Error('Agent 执行超时,可能陷入循环');
}

// Token 超限检查
if (response.usage && response.usage.input_tokens > 180000) {
  console.warn('上下文即将超限,考虑清理历史消息');
}

和现有工具对比

你可能会问:这和 v0.dev、Cursor 有什么区别?

v0.dev

Cursor

自建 Agent

我的建议:日常用 Cursor,复杂定制用自建 Agent。两者不冲突。

下一篇预告:代码审查与测试生成

生成组件只是第一步。代码生成后:

下一篇我们会深入这些主题,并实现一个完整的「生成 → 审查 → 测试」流水线。


关键要点总结

  1. AI Agent ≠ 聊天机器人,核心是工具调用能力
  2. Function Calling 让 AI 能「做事」,不只是「说话」
  3. 上下文管理很关键,不要无脑塞所有代码
  4. 组件生成的成本很低(~$0.02),但价值很高(节省 20 分钟)
  5. Prompt 设计决定了生成质量,要明确、详细、可执行

代码已开源在 GitHub,你可以直接拿去用。


Share this post on:

Previous Post
本地 AI 助手如何安全暴露到公网?SSH 反向隧道实战
Next Post
OpenClaw 多 Agent 配置实战:踩坑指南与最佳实践