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 超限怎么办?
- 刷新页面历史就丢了
学习曲线:
-
Token 限制问题
- GPT-3.5 单次请求最多 4096 tokens
- GPT-4 最多 8192 tokens(付费版更多)
- 对话多了会直接报错
-
解决方案:智能截断
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); } -
本地持久化
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
- 代码块没有高亮:看起来很丑
学习曲线:
-
安全过滤
javascriptimport DOMPurify from 'dompurify'; const safeHtml = DOMPurify.sanitize(marked.parse(content)); <div dangerouslySetInnerHTML={{ __html: safeHtml }} /> -
代码高亮
javascriptimport 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> -
性能优化
- 使用
react-markdown而不是marked(React 友好) - 不要每次都重新解析,用
useMemo缓存
javascriptconst rendered = useMemo(() => ( <ReactMarkdown>{content}</ReactMarkdown> ), [content]); - 使用
新手提示:
- 代码高亮库很大,考虑按需加载
- 如果内容很长,考虑虚拟滚动(这个我后面会讲)
- DOMPurify 默认会过滤掉
iframe,如果需要可以配置
二、让我崩溃的三大 Bug
Bug 一:流式响应乱码
现象: AI 生成的中文回复,偶尔会出现乱码,比如"你好"变成"��好"。
排查过程:
- 看网络请求,返回的数据是正常的
- 看控制台,
TextDecoder解码后的数据有问题 - 看了很久,才发现是 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 });
}
}
新手提示:
TextDecoder的stream: 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。
排查过程:
- 用 Chrome DevTools 的 Memory 工具分析
- 发现大量的
EventSource对象没有被释放 - 还有大量的定时器没有清理
原因: 组件卸载时,没有清理资源。
解决方案:
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
不要只看博客文章,一定要看官方文档:
- MDN Web Docs - Web API
- React 官方文档 - React 最佳实践
- XState 官方文档 - 状态机
- OpenAI API 文档 - AI 模型能力
我的教训: 一开始只看博客文章,很多概念理解错了。后来认真看官方文档,才发现很多博客的示例代码是有问题的。
建议 3:多踩坑,多总结
不要怕踩坑,每个坑都是学习机会。
我的做法:
- 记录每个 Bug 和解决过程
- 定期复盘,总结经验
- 分享给团队,避免重复踩坑
我的 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 前端开发的初级工程师,希望我的经历能给你一些帮助。
记住:没有人一开始就是专家,所有的专家都是从新手一步步走过来的。
继续加油!💪
互动话题
- 你在 AI 前端开发中遇到过哪些坑?
- 作为初级开发者,你最困惑的是什么?
- 有什么想问我的吗?
欢迎在评论区交流!👇
参考资料:
作者: [miss]
如果本文对你有帮助,欢迎点赞、收藏、转发!