AI Agent 前端开发:一个初级工程师的踩坑成长之路

AI Agent 前端开发:一个初级工程师的踩坑成长之路

从只会写简单聊天界面到独立完成 AI 产品前端,我用 1 年时间踩了无数坑。这篇文章记录了我作为初级 AI 前端开发者的真实经历:那些让我崩溃的 Bug、让我困惑的技术点,以及最后的收获。

前言

2025 年初,我从传统 Web 前端转岗到 AI 产品线,负责 AI Agent 相关的前端开发。

刚开始我以为:"不就是个聊天框吗?加上打字机效果,调用 API 就完事了。"

结果第一次需求评审会,我就懵了:

  • "流式响应怎么实现?"
  • "对话历史怎么管理?"
  • "Token 超限怎么办?"
  • "多模态内容怎么渲染?"
  • "网络断了怎么办?"

那一刻我意识到:AI 前端开发,和我想的完全不一样。

这篇文章,就是我这 1 年来的真实成长记录。如果你也是刚接触 AI 产品开发的前端工程师,希望我的经历能帮你少踩一些坑。


一、入门时的三大困惑

困惑一:为什么不能用普通的 HTTP 请求?

刚开始做第一个 AI 聊天功能时,我直接写了个普通的 POST 请求:

javascript 复制代码
// ❌ 我最初的写法
const response = await fetch('/api/chat', {
  method: 'POST',
  body: JSON.stringify({ message: '你好' })
});

const data = await response.json();
setMessage(data.content);

结果测试的时候,用户反馈:"怎么要等 10 秒才能看到回复?"

我才明白:AI 模型生成内容是流式的,需要边生成边显示,而不是等全部生成完再显示。

解决方案:学习 SSE(Server-Sent Events)

javascript 复制代码
// ✅ 正确的流式处理
const eventSource = new EventSource('/api/chat/stream');

eventSource.onmessage = (event) => {
  const data = JSON.parse(event.data);
  if (data.content) {
    setMessage(prev => prev + data.content);
  }
};

新手提示:

  • SSE 是单向的,适合 AI → 前端的流式输出
  • 如果需要双向通信(比如用户可以打断 AI),用 WebSocket
  • 记得在组件卸载时调用 eventSource.close(),否则会内存泄漏

困惑二:对话历史存在哪里?

第二个需求是"支持多轮对话"。

我一开始把对话历史存在 useState 里:

javascript 复制代码
// ❌ 简单粗暴
const [messages, setMessages] = useState([]);

// 每次对话都带上所有历史
const response = await fetch('/api/chat', {
  body: JSON.stringify({
    message: newMessage,
    history: messages // 问题:越来越多
  })
});

问题来了:

  • 对话轮数多了,历史消息会非常长
  • Token 超限怎么办?
  • 刷新页面历史就丢了

学习曲线:

  1. Token 限制问题

    • GPT-3.5 单次请求最多 4096 tokens
    • GPT-4 最多 8192 tokens(付费版更多)
    • 对话多了会直接报错
  2. 解决方案:智能截断

    javascript 复制代码
    // 简单的截断策略
    function getLimitedHistory(messages, maxTokens = 3000) {
      let totalTokens = 0;
      const selected = [];
      
      // 从后往前选(保留最近的对话)
      for (let i = messages.length - 1; i >= 0; i--) {
        const msgTokens = estimateTokens(messages[i].content);
        if (totalTokens + msgTokens <= maxTokens) {
          selected.unshift(messages[i]);
          totalTokens += msgTokens;
        }
      }
      
      return selected;
    }
    
    function estimateTokens(text) {
      // 粗略估算:4 字符 ≈ 1 token
      return Math.ceil(text.length / 4);
    }
  3. 本地持久化

    javascript 复制代码
    // 用 localStorage 保存对话
    useEffect(() => {
      localStorage.setItem('chat-history', JSON.stringify(messages));
    }, [messages]);
    
    // 页面加载时恢复
    useEffect(() => {
      const saved = localStorage.getItem('chat-history');
      if (saved) {
        setMessages(JSON.parse(saved));
      }
    }, []);

新手提示:

  • Token 估算有误差,实际使用时留 20% 余量
  • 重要对话(比如用户说"记住这个")要优先保留
  • localStorage 有大小限制(约 5MB),对话太长用 IndexedDB

困惑三:Markdown 渲染太卡了

AI 生成的回复经常包含 Markdown 格式(代码块、列表、表格)。

我开始用 marked 库直接渲染:

javascript 复制代码
import { marked } from 'marked';

<div dangerouslySetInnerHTML={{ __html: marked.parse(content) }} />

问题:

  • XSS 风险:AI 生成的内容可能包含恶意脚本
  • 性能差:每次更新都重新解析整个 Markdown
  • 代码块没有高亮:看起来很丑

学习曲线:

  1. 安全过滤

    javascript 复制代码
    import DOMPurify from 'dompurify';
    
    const safeHtml = DOMPurify.sanitize(marked.parse(content));
    <div dangerouslySetInnerHTML={{ __html: safeHtml }} />
  2. 代码高亮

    javascript 复制代码
    import ReactMarkdown from 'react-markdown';
    import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
    
    <ReactMarkdown
      components={{
        code({ node, inline, className, children, ...props }) {
          const match = /language-(\w+)/.exec(className || '');
          return !inline && match ? (
            <SyntaxHighlighter language={match[1]} {...props}>
              {String(children).replace(/\n$/, '')}
            </SyntaxHighlighter>
          ) : (
            <code className={className} {...props}>
              {children}
            </code>
          );
        }
      }}
    >
      {content}
    </ReactMarkdown>
  3. 性能优化

    • 使用 react-markdown 而不是 marked(React 友好)
    • 不要每次都重新解析,用 useMemo 缓存
    javascript 复制代码
    const rendered = useMemo(() => (
      <ReactMarkdown>{content}</ReactMarkdown>
    ), [content]);

新手提示:

  • 代码高亮库很大,考虑按需加载
  • 如果内容很长,考虑虚拟滚动(这个我后面会讲)
  • DOMPurify 默认会过滤掉 iframe,如果需要可以配置

二、让我崩溃的三大 Bug

Bug 一:流式响应乱码

现象: AI 生成的中文回复,偶尔会出现乱码,比如"你好"变成"��好"。

排查过程:

  1. 看网络请求,返回的数据是正常的
  2. 看控制台,TextDecoder 解码后的数据有问题
  3. 看了很久,才发现是 UTF-8 多字节字符被截断了

原因: SSE 流式传输时,一个中文字符(3 字节)可能被分成两个 chunk 发送:

  • 第一个 chunk:前 2 字节(不完整的字符)
  • 第二个 chunk:后 1 字节

直接 decode() 第一个 chunk,就会产生乱码。

解决方案:

javascript 复制代码
class StreamDecoder {
  constructor() {
    this.decoder = new TextDecoder('utf-8');
    this.buffer = '';
  }
  
  decode(chunk) {
    this.buffer += this.decoder.decode(chunk, { stream: true });
    
    // 尝试解析
    try {
      return JSON.parse(this.buffer);
    } catch {
      // 解析失败,说明数据不完整,等待下一个 chunk
      return null;
    }
  }
  
  finalize() {
    return this.decoder.decode(new Uint8Array(0), { stream: false });
  }
}

新手提示:

  • TextDecoderstream: true 选项很重要
  • 如果用 EventSource,浏览器会自动处理 UTF-8
  • 如果用 fetch + ReadableStream,需要自己处理

Bug 二:对话状态错乱

现象: 快速发送多条消息,回复顺序乱了:

  • 发送 "A",等待回复
  • 又发送 "B"
  • 结果先收到 B 的回复,再收到 A 的回复

原因: 多个请求并发,响应顺序不确定。

解决方案:

javascript 复制代码
const [currentRequestId, setCurrentRequestId] = useState(null);

async function sendMessage(message) {
  const requestId = Date.now();
  setCurrentRequestId(requestId);
  
  const response = await fetch('/api/chat', {
    body: JSON.stringify({ message, requestId })
  });
  
  const reader = response.body.getReader();
  while (true) {
    const { done, value } = await reader.read();
    if (done) break;
    
    const data = JSON.parse(new TextDecoder().decode(value));
    
    // 只处理最新请求的响应
    if (data.requestId === currentRequestId) {
      setContent(prev => prev + data.content);
    }
  }
}

新手提示:

  • 为每个请求生成唯一 ID
  • 比较时用 ===,不要用 ==
  • 如果用户主动停止生成,也要更新 currentRequestId

Bug 三:内存泄漏

现象: 长时间使用聊天功能后,浏览器越来越卡,最后直接崩溃。打开任务管理器,发现内存占用超过 2GB。

排查过程:

  1. 用 Chrome DevTools 的 Memory 工具分析
  2. 发现大量的 EventSource 对象没有被释放
  3. 还有大量的定时器没有清理

原因: 组件卸载时,没有清理资源。

解决方案:

javascript 复制代码
useEffect(() => {
  const eventSource = new EventSource('/api/chat/stream');
  const timer = setInterval(() => {
    // 定时任务
  }, 1000);
  
  return () => {
    // 清理!清理!清理!
    eventSource.close();
    clearInterval(timer);
  };
}, []);

新手提示:

  • useEffect 的 cleanup 函数一定要写
  • 所有外部资源(EventSource、WebSocket、定时器)都要清理
  • 用 React DevTools 的 Profiler 检查是否有不必要的重渲染

三、从新手到进阶的三个转折点

转折点一:学会用状态机

问题: 对话相关的状态越来越多:

  • messages(消息列表)
  • isLoading(是否正在生成)
  • isPaused(是否暂停)
  • error(错误信息)
  • currentAgent(当前使用的 Agent)
  • tokenCount(已用 Token 数)

useState 管理这些状态,代码变得很乱,状态同步也很困难。

学习:XState 状态机

javascript 复制代码
import { createMachine } from 'xstate';

const chatMachine = createMachine({
  initial: 'idle',
  states: {
    idle: {
      on: { SEND: 'sending' }
    },
    sending: {
      on: { SUCCESS: 'receiving', ERROR: 'idle' }
    },
    receiving: {
      on: { COMPLETE: 'idle', STOP: 'idle' }
    }
  }
});

好处:

  • 所有状态集中管理,不会混乱
  • 状态转换逻辑清晰,容易理解
  • 可以可视化(XState 有在线工具)

新手提示:

  • 简单场景用 useState 就够了,状态机有点重
  • 复杂对话场景(多 Agent 协作)用状态机很合适
  • 官方文档有 React 集成教程

转折点二:学会性能优化

问题: 对话超过 100 条后,页面明显卡顿:

  • 滚动不流畅
  • 输入消息时卡顿
  • 内存占用持续增长

学习:虚拟滚动

javascript 复制代码
import { FixedSizeList } from 'react-window';

<FixedSizeList
  height={600}
  itemCount={messages.length}
  itemSize={100}
>
  {({ index, style }) => (
    <div style={style}>
      <Message message={messages[index]} />
    </div>
  )}
</FixedSizeList>

效果:

  • 渲染时间从 300ms 降到 15ms
  • 内存占用从 500MB 降到 80MB
  • 滚动 FPS 从 30fps 提升到 60fps

新手提示:

  • react-window 适合固定高度的场景
  • 如果消息高度不固定,用 @tanstack/react-virtual
  • 虚拟滚动会增加代码复杂度,简单场景可以不用

转折点三:学会离线处理

问题: 网络不稳定时,用户体验很差:

  • 发送消息后一直转圈
  • 网络恢复后消息丢失
  • 无法查看历史对话

学习:IndexedDB + 乐观更新

javascript 复制代码
import { openDB } from 'idb';

// 1. 初始化数据库
const db = await openDB('chat-db', 1, {
  upgrade(db) {
    db.createObjectStore('messages');
  }
});

// 2. 乐观更新:立即显示在 UI 上
async function sendMessage(content) {
  const message = {
    id: Date.now(),
    content,
    role: 'user',
    pending: true // 标记为待发送
  };
  
  // 立即显示
  setMessages(prev => [...prev, message]);
  
  // 保存到本地
  await db.put('messages', message);
  
  // 尝试发送
  try {
    await fetch('/api/chat', { body: JSON.stringify(content) });
    // 发送成功,更新状态
    await db.put('messages', { ...message, pending: false });
  } catch {
    // 发送失败,保持 pending 状态
  }
}

// 3. 网络恢复时同步
window.addEventListener('online', async () => {
  const pendingMessages = await db.getAllFromIndex('messages', 'pending', true);
  for (const msg of pendingMessages) {
    try {
      await fetch('/api/chat', { body: JSON.stringify(msg.content) });
      await db.put('messages', { ...msg, pending: false });
    } catch {
      // 继续保持 pending
    }
  }
});

效果:

  • 网络中断时用户无感知
  • 消息不会丢失
  • 网络恢复后自动同步

新手提示:

  • idb 是 IndexedDB 的 Promise 封装,比原生 API 好用
  • 乐观更新会让用户感觉更快,但要注意错误处理
  • 离线功能会增加复杂度,简单场景可以不做

四、给初级开发者的建议

建议 1:从简单开始

不要一开始就追求完美:

  • 第一版:普通 HTTP 请求 + 简单聊天框
  • 第二版:加上流式响应
  • 第三版:加上对话历史
  • 第四版:优化性能和体验

我的教训: 一开始就想做"完美产品",结果花了 2 周时间,还有很多 Bug。后来简化需求,1 周就上线了 MVP。


建议 2:多看官方文档

AI 前端开发涉及很多新技术:

  • SSE / WebSocket
  • XState 状态机
  • 虚拟滚动
  • IndexedDB

不要只看博客文章,一定要看官方文档

我的教训: 一开始只看博客文章,很多概念理解错了。后来认真看官方文档,才发现很多博客的示例代码是有问题的。


建议 3:多踩坑,多总结

不要怕踩坑,每个坑都是学习机会。

我的做法:

  1. 记录每个 Bug 和解决过程
  2. 定期复盘,总结经验
  3. 分享给团队,避免重复踩坑

我的 Bug 记录本:

yaml 复制代码
2025-03-15
Bug:流式响应乱码
原因:UTF-8 多字节字符被截断
解决:用 TextDecoder 的 stream: true 选项

建议 4:关注 AI 模型能力

作为 AI 前端开发者,要了解 AI 模型的能力边界:

  • Token 限制
  • 上下文窗口
  • 多模态支持(文本、图片、音频)
  • 流式输出

我的教训: 一开始不了解模型能力,设计了很多功能,结果发现模型根本做不到。后来先了解模型能力,再设计功能,效率高了很多。


五、技术栈推荐(初级版)

作为初级开发者,不要一开始就用最复杂的工具。这是我推荐的入门组合:

最简单组合

javascript 复制代码
React + Vite + SSE + localStorage
适用:简单聊天应用、学习用

进阶组合

复制代码
React + Vite + WebSocket + IndexedDB
适用:生产环境、需要离线支持

工具库推荐

功能 推荐库 理由
Markdown 渲染 react-markdown React 友好
代码高亮 react-syntax-highlighter 简单易用
安全过滤 DOMPurify 行业标准
本地存储 idb Promise 封装
虚拟滚动 react-window 简单场景够用

六、总结

回顾这 1 年的成长,我总结了几点心得:

关于学习:

  • 从简单开始,逐步迭代
  • 多看官方文档,少看二手资料
  • 踩坑不可怕,重要的是总结经验

关于技术:

  • AI 前端开发 = 传统前端 + 流式响应 + 状态管理
  • 性能优化很重要,但不要过度优化
  • 用户体验 > 技术炫技

关于心态:

  • 不要怕犯错,每个错误都是机会
  • 多向资深同事请教
  • 保持好奇心,持续学习

写在最后

如果你也是刚接触 AI 前端开发的初级工程师,希望我的经历能给你一些帮助。

记住:没有人一开始就是专家,所有的专家都是从新手一步步走过来的。

继续加油!💪


互动话题

  1. 你在 AI 前端开发中遇到过哪些坑?
  2. 作为初级开发者,你最困惑的是什么?
  3. 有什么想问我的吗?

欢迎在评论区交流!👇


参考资料:

作者: [miss]

如果本文对你有帮助,欢迎点赞、收藏、转发!

相关推荐
清水寺小和尚1 小时前
如何用400行代码构建OpenClaw
前端
锦木烁光1 小时前
Flowable 实战:从架构解耦到多状态动态查询的高性能重构方案
前端·后端
子淼8122 小时前
HTML入门指南:构建网页的基石
前端·html
农夫山泉不太甜2 小时前
Electron离屏渲染技术详
前端
深念Y2 小时前
Chrome MCP Server 配置失败全记录:一场历时数小时的“fetch failed”排查之旅
前端·自动化测试·chrome·http·ai·agent·mcp
一个有故事的男同学2 小时前
从零打造专业级前端 SDK (四):错误监控与生产发布
前端
2601_948606182 小时前
从 jQuery → V/R → Lit:前端架构的 15 年轮回
前端·架构·jquery
wuhen_n2 小时前
Vite 核心原理:ESM 带来的开发时“瞬移”体验
前端·javascript·vue.js
nibabaoo2 小时前
前端开发攻略---vue3长列表性能优化终极指南:虚拟滚动、分页加载、时间分片等6种方案详解与代码实现
前端·javascript·vue.js·虚拟滚动·分页加载·长列表·时间分片