从裸辞到重新上手:我在前端动画里折腾 Fine-tuning 的血泪史

凉薄少年
2026-04-22 06:36
阅读 1002

去年夏天,我一冲动裸辞了。Gap半年,刷了不少开源项目,也顺手把 Vim 配置翻新了好几轮(没错,我还是那个打死不用 VS Code 的倔强 Vim 党)。结果今年初重新杀回职场,进了一家做互动营销平台的中厂,一干就是快两年。团队氛围还不错,虽然产品经理时不时提些“能不能加个粒子动效”的需求,但好在不逼我们用低代码拖拽——至少还能写点真代码。

最近有个需求让我差点又想裸辞:我们要给一个用户行为分析面板加上智能提示动画,根据用户操作习惯动态调整提示内容的出现时机和方式。听起来很酷?实际上就是要把大模型的能力塞进前端交互里。领导说:“你看 Kimi 那种效果就行。”我心想:Kimi 是好,但咱这破浏览器环境能跑吗?

于是,Fine-tuning 和前端动画的奇妙(或者说惨烈)结合,就这么开始了。


为什么非得搞 Fine-tuning?

一开始我想偷懒,直接调 Kimi 的 API。但测试发现延迟太高——平均 800ms+,用户鼠标悬停都移走了,提示才慢悠悠冒出来,体验直接负分。而且公司对数据隐私卡得严,用户行为日志不能外传,Kimi 的云端推理直接出局。

那能不能本地跑个小模型?行,但通用模型太“傻”:它不知道我们业务里“点击热力图”和“漏斗转化”是高频词,也不知道“导出失败”后面通常要跟“重试”而不是“联系客服”。这时候 Fine-tuning 就成了必选项:用我们自己的日志微调一个轻量级语言模型,让它更懂业务上下文。

选型上,我们最终用了 Hugging Face 上的 TinyLlama-1.1B,再配合 ONNX Runtime Web 在浏览器里跑推理。别笑,1.1B 参数在现在算“Tiny”,但放到前端还是个巨无霸。好在我们只需要做 next-token prediction,不需要生成整段文本,勉强能压到 300ms 内响应。


踩坑实录:Fine-tuning 不是点点鼠标就完事

坑一:数据格式你以为很简单?

Fine-tuning 需要训练数据。我从埋点日志里捞了三个月的用户操作序列,格式大概是这样:

{
  "session_id": "abc123",
  "events": [
    {"type": "click", "target": "export-btn"},
    {"type": "error", "msg": "Network timeout"},
    {"type": "click", "target": "retry-btn"}
  ]
}

理想情况下,模型应该学会:当看到 error + Network timeout,下一个动作很可能是 retry-btn。但原始日志里有大量噪声:测试账号、爬虫、误触……直接喂给模型,结果它学会了推荐“刷新页面”——因为测试同学总这么干。

解决方案:加一层清洗规则,过滤掉非真实用户行为,并把事件序列转成自然语言 prompt:

User encountered 'Network timeout' after clicking export. Next likely action: retry.

这一步花了我整整三天,期间还被测试同学追着问:“你删我测试数据干嘛?”我说:“为了不让 AI 学你乱点。”


坑二:模型太大,浏览器扛不住

Fine-tuned 模型导出后 1.2GB,直接加载?Chrome 直接弹“页面无响应”。我一度想放弃,但想起之前研究过的 量化(Quantization) 技术。

onnxruntime-webquantize 工具把 FP32 模型转成 INT8,体积瞬间降到 300MB。虽然精度损失了约 5%,但在我们的场景下几乎无感——毕竟不是做医疗诊断,只是猜用户下一步点哪。

关键代码如下:

// 使用 ONNX Runtime Web 加载量化模型
import { InferenceSession } from 'onnxruntime-web';

const session = await InferenceSession.create('./model_quantized.onnx');

// 输入处理:把当前上下文转成 token IDs
const inputIds = tokenizer.encode(currentContext).ids;

const feeds = {
  input_ids: new Tensor('int32', inputIds, [1, inputIds.length])
};

const output = await session.run(feeds);
const logits = output.logits.data;
const predictedTokenId = argmax(logits); // 取概率最高的 token
const suggestion = tokenizer.decode([predictedTokenId]);

注意:这里必须用 Tensor 构造输入,否则会报 TypeError: Cannot read property 'data' of undefined——这个错我 debug 了两小时,最后发现是维度没对齐。


坑三:动画和模型预测怎么联动?

这才是最骚的操作。我们不能等模型返回再启动动画,那样还是卡。于是搞了个“预加载 + 动画插值”策略:

  1. 用户 hover 到某个区域时,立刻启动一个 200ms 的淡入动画
  2. 同时异步请求模型预测
  3. 如果预测结果和默认提示不同,在动画中途切换内容(用 GSAP 做平滑过渡)
let defaultTip = "Click to export data";
let animatedTip = createAnimatedElement(defaultTip); // 用 GSAP 初始化

hoverTarget.addEventListener('mouseenter', async () => {
  animatedTip.startFadeIn(); // 立刻开始动画
  
  const prediction = await predictNextAction(currentUserContext);
  if (prediction !== defaultTip) {
    // 在动画进行到 50% 时切换文本,避免突兀
    gsap.to(animatedTip.textNode, {
      duration: 0.1,
      innerText: prediction,
      ease: "power2.inOut"
    });
  }
});

说实话,第一次看到提示语在淡入过程中“变脸”,我还挺得意。结果产品经理说:“能不能让它像呼吸一样自然?”……我默默关掉了 Slack。


效果对比:值不值得折腾?

上线两周后,我们拉了 A/B 测试数据:

指标 默认提示(对照组) Fine-tuned + 动画(实验组)
提示点击率 12.3% 19.7%
任务完成时长 42s 36s
用户满意度(NPS) +28 +41

虽然提升不算爆炸,但考虑到这是个辅助功能,已经算超预期了。更重要的是,运维没来骂我——因为模型跑在客户端,服务器零新增负载。


几点血泪心得

  1. Fine-tuning 不是银弹:如果你的业务数据杂乱无章,先花时间清洗,别指望模型自动过滤噪音。
  2. 前端也能玩大模型:只要做好量化+懒加载+缓存,1B 以下的模型完全可以在现代浏览器跑起来。
  3. 动画是掩盖延迟的神器:用户对“等待”的容忍度极低,但对“流畅过渡中的变化”接受度很高。
  4. 别信产品经理的“Kimi 那种效果”:他们根本不知道 Kimi 背后是几百块 GPU 在跑。你要做的是“看起来像”,不是“真的是”。

现在回头看,这段折腾其实挺值得。裸辞那半年让我意识到:技术探索不能只停留在“看文档”,必须落到具体场景里去摔打。Fine-tuning 听起来高大上,但拆解下来,无非是数据、模型、部署、交互四个环节的缝合。而前端工程师的优势在于——我们离用户最近,知道什么样的“智能”才是真正有用的。

对了,我的 Vim 配置里现在多了个快捷键:<leader>ft,一键打开 Fine-tuning 日志目录。毕竟,下次产品经理再说“加个 AI 动效”,我至少知道该从哪开始砸键盘了。

(完)

评论 0

最热最新
暂无评论
凉薄少年Lv.1
0
影响力
0
文章
0
粉丝