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

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

相关推荐
Ricon组态薄荷糖5 小时前
Ricon组态系统:工业组件开发指南与实践
前端·后端·物联网
flash俊杰5 小时前
Electron 桌面应用的进程模型:为什么 fork Next.js standalone
前端
沙蒿同学5 小时前
我把架构约定编译成了会变红的测试:Wails v2 + Go + Vue3 桌面脚手架实战
前端·后端·github
cpolar技术支持5 小时前
本地 Playwright 测试报告怎么远程复盘?Trace Viewer 跑起来后,用 cpolar 分享失败现场
前端·自动化测试·测试工具·cpolar·playwright
wordbaby5 小时前
企业级后台管理系统路由设计与最佳实践指南
前端
胡志辉的博客5 小时前
【完全开源】IP 纯净度检测 可一键部署到自己的CF
前端·javascript·chrome·ip·chromium
Hilaku5 小时前
作为面试官,我最怕遇到什么样的候选人?
前端·javascript·程序员
TiDi6 小时前
Pinia优化重复请求
前端
web3d5206 小时前
01-用 Leafletjs 10 分钟搭一张水利一张图(Vue3 + Vite 实战)
前端·javascript
晚安日记wanna6 小时前
Vue2 的 defineProperty 差在哪四层追问筛掉九成候选人
前端·vue.js·面试