归途
avatar

如果不存在过

看板娘回句话要等17.9秒?顺手给她换上了真流式

右下角的看板娘 33 前两天罢工了,修好以后又发现她回一句话要憋 17.9 秒。这篇记录一下把她改成真流式(边生成边显示)的过程,以及中间踩到的几个坑:说漏嘴、工具调用、上游抽风,还有一个差点把访客 IP 串给别人的缓存问题。

33 正在一边想一边说

写在前面

前两天闲着没事,点开右下角的 33 想跟她聊两句,结果人家理都不理我。一查才知道,原来用的那个免费模型被下架了,上游还顺手把 Cloudflare 的出口给限流了,换个模型名照样进不去。

没办法,只好给她换了个新模型。新模型确实聪明多了,但是问一句话,要先憋十几秒,然后一口气全倒出来。我掐表测了一下,17.9 秒(对,你没看错,十七点九秒)。这段时间对话框里就一个小圈圈在那转,谁也不知道她是在想,还是已经跑路了。

于是又折腾了大半天,把她改成了真流式。老规矩,发篇文章记录一下,免得以后忘了。

关于原来的“打字效果”

说出来有点丢人,33 以前那种一个字一个字往外蹦的效果,其实是假的。后端先等模型把整段话写完,再切成八个字一块发给浏览器,假装自己在打字。

模型快的时候还能糊弄过去,模型一慢就露馅了,前面那十几秒的空白,怎么演都演不出来。所以这次干脆来真的:模型吐一个字,后端转一个字,浏览器显示一个字。

改成真流式遇到的问题

听起来挺简单,真动手了才发现没那么容易,主要是下面两个问题:

  • 说漏嘴:33 有条规矩,不管底下用的是什么模型,她只承认自己是看板娘。以前整段话拿到手,从头扫一遍,把说漏嘴的句子删掉就行。现在话是一个字一个字往外冒的,等发现说漏嘴,前半句早就发出去了。
  • 工具调用:33 会帮人查文章、翻接口文档、带人去某个板块。流式里“调用工具”这件事也是一片一片拼出来的,名字一片,参数一片,得全拼完才知道她要干嘛。

说漏嘴的问题,最后用了个笨办法:一句话说完,再放行。

// 一句话说完才放行;句子里出现不该说的名字,整句吞掉
const CLAUSE_END = /[。!?!?;;,,\n]/;
let buf = '';

function push(text: string): string {
  buf += text;
  let out = '';
  for (let i = buf.search(CLAUSE_END); i >= 0; i = buf.search(CLAUSE_END)) {
    const clause = buf.slice(0, i + 1);
    buf = buf.slice(i + 1);
    if (!LEAK.test(clause)) out += clause;
  }
  return out;
}

按逗号、句号切开,每一小句攒齐了再检查,没问题的放出去,有问题的整句吞掉。看起来会一顿一顿的,没那么丝滑,但总比憋 17.9 秒强吧。

工具调用就是拼完、执行、把结果塞回去,再开下一轮。中间还发现一个挺好笑的事:她调工具之前,喜欢先来一句“我帮你查一下~”。以前这句是藏起来的,访客看不到,现在直接显示出来了,结果查完回来,她又从头说一遍,跟复读机似的。只好在工具结果后面补一句提醒:开场白已经说过了,接着往下说。

上游的几个小脾气

改成真流式以后,以前被“整段等待”藏起来的毛病,也全都露出来了:

  1. 偶尔很慢:大概每八次就有一次,第一个字要等十几秒,最长的一次等了五十多秒。现在 10 秒还不开口就直接放弃,重新问一遍,重问的那次一般一两秒就出来了(有点玄学,但是管用)。
  2. 把工具调用当成正文说出来:偶尔会把一串奇奇怪怪的标签原样吐给访客。现在看到这种标签就直接截住,一个字都不放出去,后端自己把它解析成正常的工具调用。
  3. 忙的时候只回一个 ok:对,就俩字母,状态码还是 200。所以每一轮开头先攒够几个字再放行,攒下来要是只有一个 ok,就当失败处理,重新来。

另外,她在想、工具在跑的时候,后端每隔 8 秒会给浏览器发一行空注释,意思就是“我还在,别挂”。不然浏览器等太久没收到东西,会以为连接断了。

顺便加了个小功能

以前在文章底下问她“这篇讲了什么”,她得先去翻全文,翻得还很慢。现在构建博客的时候,每篇文章、每个接口文档都会顺手生成一份精简的 JSON,你在哪一页问她,那一页的内容就跟着问题一起递给她。

在接口文档页问她“要传什么参数”,第一个字不到一秒就出来了。

差点出事的一个改动

为了按访客限流,我让博客的代理把访客的真实 IP 一起转给 API。本来以为就是多带一个请求头,能有什么事。

结果上线不到十分钟,复查的时候发现:API 有一层缓存,是按网址把整段结果存下来的,而结果里带着请求者的 IP。也就是说,一个人的 IP,可能会被缓存下来,然后显示给下一个人。

赶紧收回,代理改成只给看板娘转发 IP。然后顺手把缓存这个老毛病也修了:命中缓存的时候,IP 换成当前访问者自己的;查自己 IP 的请求,干脆不进缓存。

加一个看起来没什么影响的请求头,可能会悄悄改变别的地方的行为。改动的影响范围,往往比想象的要大那么一点。

改完前后对比

对比项改之前改之后
第一个字出来17.9 秒(整段一起出来)一般一两秒
打字效果假的,等写完再切块真的,边生成边显示
在文章页问“这篇讲了啥”要先去翻全文当前页的内容直接递给她
上游抽风的时候一直转圈10 秒不开口自动重问
按访客限流所有访客共用一个 IP每个访客各算各的

写在最后

第一个字从 17.9 秒变成一两秒,整段话说完的时间其实没短多少,但是等的感觉完全不一样了。

折腾了大半天,改了两个仓库,修了一堆坑,还差点把访客的 IP 给串了。不过总算是弄好了,有兴趣的可以点开右下角,跟 33 聊两句试试。

就这样吧,特发文章记录一下。

看板娘 流式输出 Cloudflare Workers 踩坑
评论区一直是坏的,干脆在 Cloudflare 上给 Waline 重写了个后端 放下那些该死的坚持

若你也路过这片海,请为我掷下一枚硬币,就当是风过无痕的默契。

支付宝打赏 支付宝 微信打赏 微信