POSTS
记一次被AI坑的有趣经历
最近在 Cursor 的帮助下,我顺利完成了一个纯前端的小工具。 从开始到 demo,全程和 AI 对话,一路点击 “Accept” 即可,非常顺利。 但在最后优化核心数据处理时,踩了个小坑。
这个坑开始于如下需求:
将前端渲染的数据保存到 url 内,以便用户可以通过分享链接直接访问到这个页面。为了节省 url 长度,我们需要将数据进行压缩, 并对压缩后的数据进行某种 url safe 的编码。
对于 url safe 的编码,我了解的很少,只是大概知道 Bas64 或者 urlEncode 大概适合这种场景的,也知道经过编码后,数据会产生一定程度的膨胀。
于是我和 AI 之间有了如下对话:

AI 的回复条理通顺,数据齐全,我没多想,直接采用了 AI 生成的如下编解码函数:
// 添加编码/解码工具函数
const urlSafeChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
function bytesToUrlSafe(bytes) {
let result = '';
for (let i = 0; i < bytes.length; i++) {
result += urlSafeChars[bytes[i] & 0x3F]; // 取低 6 位
if (i < bytes.length - 1) {
result += urlSafeChars[(bytes[i] >> 6) | ((bytes[i + 1] & 0x0F) << 2)];
}
}
return result;
}
function urlSafeToBytes(str) {
const bytes = new Uint8Array(Math.ceil(str.length / 2));
for (let i = 0, j = 0; i < str.length; i += 2, j++) {
const char1 = urlSafeChars.indexOf(str[i]);
const char2 = i + 1 < str.length ? urlSafeChars.indexOf(str[i + 1]) : 0;
bytes[j] = (char1 & 0x3F) | ((char2 & 0x3F) << 6);
}
return bytes;
}
简单测试之后,我确认页面能正常打开,肉眼比较了下,生成的链接似乎也”更短”了。能在不熟悉的领域这么快一次性实现一个功能,我感到很开心,并禁不住向 AI 请教关于这种编码的更多细节:

AI 的措辞真是严谨又谦虚。
但当我把这个项目集成到一个线上服务,用来展示更多真实数据时,却发现了问题:大部分数据无法正常解码。
我一度怀疑是后端服务处理数据时使用的压缩算法,编码算法和前端不一致导致的。反复对比,调试后,我才将焦点转移到了 bytesToUrlSafe 这个自定义编码上,毕竟压缩,序列化都是通过成熟的库完成的,
出错的可能性微乎其微。
而重新审核 bytesToUrlSafe 和 urlSafeToBytes 的代码,我也很快发现了两个疑点:
bytesToUrlSafe函数中,如果i < bytes.length - 1不成立,那么bytes[i]也就是最后一个字节的高两位就没有做任何处理了。urlSafeToBytes函数中,如果编码后的str长度为奇数,其最后一个字符就被直接无视了。
带着疑问,我开始”拷问” AI:

AI 很聪明,一下就理解了我的问题,并给出了修正后的版本:
function bytesToUrlSafe(bytes) {
let result = '';
let bitBuffer = 0;
let bitCount = 0;
for (let i = 0; i < bytes.length; i++) {
bitBuffer = (bitBuffer << 8) | bytes[i];
bitCount += 8;
while (bitCount >= 6) {
const index = (bitBuffer >> (bitCount - 6)) & 0x3F;
result += urlSafeChars[index];
bitCount -= 6;
}
}
// 修改处理剩余位的逻辑
if (bitCount > 0) {
// 左移使剩余位对齐到高位
const index = ((bitBuffer << (6 - bitCount)) & 0x3F);
result += urlSafeChars[index];
}
return result;
}
这个版本虽然更长,但相比第一个版本中前后两个字节错位拼接,绕来绕去的做法,逻辑明显更加清晰直接,很快我就理解了其中的原理,也基本确认了这个版本的正确性。 不过,我第一反应不是立即 accept 这份代码,而是忍不住有点“气愤”地质疑 AI 为什么不早点把这个明显更直白,更简单的算法提出来。AI 的回答一如既往地保持 了情绪稳定,条理分明的特点。

当然,关于 AI 此前对两种编码的空间使用率分析,我也察觉到不对劲了,毕竟不管是 AI 发明的自定义编码,还是传统的 Base64 编码,
都是用 4 个 ascii 字符编码 3 个字节的数据,空间利用率都是 75%。所以最后我没采用 AI 提供的修复版 bytesToUrlSafe ,而是请求 AI 把编码算法改成了 Base64。直接使用标准库的 API 即可,让人安心多了。
而原来实现错误的自定义编码,其实很容易看出来是在拿两个 ascii 字符编码一个字节,空间利用率只有 50%,不知道当时眼花还是精神恍惚了,竟然错误地判断出了它编码后的长度比 Base64 编码后的更短。
对于那些已经生成的无法解码的数据,我还是心有不甘,不想就这么放弃。实际上,从正确性的角度来看,原 bytesToUrlSafe 的疏漏仅在于对最后一个字节的前两位的处理,本质上相当于直接假设了最后一个字节
的前两位都是 0,如果实际情况是 01 10 11 三种的话,这编码就错了,这也是少数数据能打开,大部分数据打不开的原因。好在错误的情况才三种,我还可以“大”力出奇迹,尝试将数据修复回来,
在编码后的字符串末尾追加 “A” 或 “B” 或 “C” 或 “D” 以对应最后一个字节前两位的四种情况,然后再尝试解码和解压缩, 如果解压成功,就说明数据又“活”过来了,几番测试证明这种修复办法是有效的。
const padding = ['', 'A', 'B', 'C', 'D'];
for (let i = 0; i < 5; i++) {
try {
return doDecompressLegacy(compressed + padding[i]);
} catch (e) {
console.error('Decompression error:', e);
if (i === padding.length - 1) {
throw e;
} else {
continue;
}
}
}