AI Agent 前端开发:一个初级工程师的踩坑成长之路
从只会写简单聊天界面到独立完成 AI 产品前端,我用 1 年时间踩了无数坑。这篇文章记录了我作为初级 AI 前端开发者的真实经历:那些让我崩溃的 Bug、让我困惑的技术点,以及最后的收获。
前言
2025 年初,我从传统 Web 前端转岗到 AI 产品线,负责 AI Agent 相关的前端开发。
刚开始我以为:“不就是个聊天框吗?加上打字机效果,调用 API 就完事了。”
结果第一次需求评审会,我就懵了:
-
“流式响应怎么实现?”
-
“对话历史怎么管理?”
-
“Token 超限怎么办?”
-
“多模态内容怎么渲染?”
-
“网络断了怎么办?”
那一刻我意识到:AI 前端开发,和我想的完全不一样。
这篇文章,就是我这 1 年来的真实成长记录。如果你也是刚接触 AI 产品开发的前端工程师,希望我的经历能帮你少踩一些坑。
一、入门时的三大困惑
困惑一:为什么不能用普通的 HTTP 请求?
刚开始做第一个 AI 聊天功能时,我直接写了个普通的 POST 请求:
// ❌ 我最初的写法
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)
// ✅ 正确的流式处理
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 里:
// ❌ 简单粗暴
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(付费版更多)
-
对话多了会直接报错
解决方案:智能截断
// 简单的截断策略
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);
}
本地持久化
// 用 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 库直接渲染:
import { marked } from 'marked';
<div dangerouslySetInnerHTML={{ __html: marked.parse(content) }} />
问题:
-
XSS 风险:AI 生成的内容可能包含恶意脚本
-
性能差:每次更新都重新解析整个 Markdown
-
代码块没有高亮:看起来很丑
学习曲线:
安全过滤
import DOMPurify from 'dompurify';
const safeHtml = DOMPurify.sanitize(marked.parse(content));
<div dangerouslySetInnerHTML={{ __html: safeHtml }} />
代码高亮
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>
性能优化
-
使用
react-markdown而不是marked(React 友好) -
不要每次都重新解析,用
useMemo缓存
const rendered = useMemo(() => (
<ReactMarkdown>{content}</ReactMarkdown>
), [content]);
新手提示:
-
代码高亮库很大,考虑按需加载
-
如果内容很长,考虑虚拟滚动(这个我后面会讲)
-
DOMPurify 默认会过滤掉
iframe,如果需要可以配置
二、让我崩溃的三大 Bug
Bug 一:流式响应乱码
现象: AI 生成的中文回复,偶尔会出现乱码,比如”你好”变成”��好”。
排查过程:
-
看网络请求,返回的数据是正常的
-
看控制台,
TextDecoder解码后的数据有问题 -
看了很久,才发现是 UTF-8 多字节字符被截断了
原因: SSE 流式传输时,一个中文字符(3 字节)可能被分成两个 chunk 发送:
-
第一个 chunk:前 2 字节(不完整的字符)
-
第二个 chunk:后 1 字节
直接 decode() 第一个 chunk,就会产生乱码。
解决方案:
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 的回复
原因: 多个请求并发,响应顺序不确定。
解决方案:
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对象没有被释放 -
还有大量的定时器没有清理
原因: 组件卸载时,没有清理资源。
解决方案:
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 状态机
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 条后,页面明显卡顿:
-
滚动不流畅
-
输入消息时卡顿
-
内存占用持续增长
学习:虚拟滚动
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 + 乐观更新
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 记录本:
2025-03-15
Bug:流式响应乱码
原因:UTF-8 多字节字符被截断
解决:用 TextDecoder 的 stream: true 选项
建议 4:关注 AI 模型能力
作为 AI 前端开发者,要了解 AI 模型的能力边界:
-
Token 限制
-
上下文窗口
-
多模态支持(文本、图片、音频)
-
流式输出
我的教训: 一开始不了解模型能力,设计了很多功能,结果发现模型根本做不到。后来先了解模型能力,再设计功能,效率高了很多。
五、技术栈推荐(初级版)
作为初级开发者,不要一开始就用最复杂的工具。这是我推荐的入门组合:
最简单组合
React + Vite + SSE + localStorage
适用:简单聊天应用、学习用
进阶组合
React + Vite + WebSocket + IndexedDB
适用:生产环境、需要离线支持
工具库推荐
| 功能 | 推荐库 | 理由 | | Markdown 渲染 | react-markdown | React 友好 | | 代码高亮 | react-syntax-highlighter | 简单易用 | | 安全过滤 | DOMPurify | 行业标准 | | 本地存储 | idb | Promise 封装 | | 虚拟滚动 | react-window | 简单场景够用 |
六、总结
回顾这 1 年的成长,我总结了几点心得:
关于学习:
-
从简单开始,逐步迭代
-
多看官方文档,少看二手资料
-
踩坑不可怕,重要的是总结经验
关于技术:
-
AI 前端开发 = 传统前端 + 流式响应 + 状态管理
-
性能优化很重要,但不要过度优化
-
用户体验 > 技术炫技
关于心态:
-
不要怕犯错,每个错误都是机会
-
多向资深同事请教
-
保持好奇心,持续学习
写在最后
如果你也是刚接触 AI 前端开发的初级工程师,希望我的经历能给你一些帮助。
记住:没有人一开始就是专家,所有的专家都是从新手一步步走过来的。
继续加油!💪
互动话题
-
你在 AI 前端开发中遇到过哪些坑?
-
作为初级开发者,你最困惑的是什么?
-
有什么想问我的吗?
欢迎在评论区交流!👇
参考资料:
-
MDN - Server-Sent Events
-
React 官方文档
-
XState 入门教程
如果这篇文章对你有帮助,欢迎点赞、收藏、转发!