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

写在前面
前两天闲着没事,点开右下角的 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 秒强吧。
工具调用就是拼完、执行、把结果塞回去,再开下一轮。中间还发现一个挺好笑的事:她调工具之前,喜欢先来一句“我帮你查一下~”。以前这句是藏起来的,访客看不到,现在直接显示出来了,结果查完回来,她又从头说一遍,跟复读机似的。只好在工具结果后面补一句提醒:开场白已经说过了,接着往下说。
上游的几个小脾气
改成真流式以后,以前被“整段等待”藏起来的毛病,也全都露出来了:
- 偶尔很慢:大概每八次就有一次,第一个字要等十几秒,最长的一次等了五十多秒。现在 10 秒还不开口就直接放弃,重新问一遍,重问的那次一般一两秒就出来了(有点玄学,但是管用)。
- 把工具调用当成正文说出来:偶尔会把一串奇奇怪怪的标签原样吐给访客。现在看到这种标签就直接截住,一个字都不放出去,后端自己把它解析成正常的工具调用。
- 忙的时候只回一个 ok:对,就俩字母,状态码还是 200。所以每一轮开头先攒够几个字再放行,攒下来要是只有一个 ok,就当失败处理,重新来。
另外,她在想、工具在跑的时候,后端每隔 8 秒会给浏览器发一行空注释,意思就是“我还在,别挂”。不然浏览器等太久没收到东西,会以为连接断了。
顺便加了个小功能
以前在文章底下问她“这篇讲了什么”,她得先去翻全文,翻得还很慢。现在构建博客的时候,每篇文章、每个接口文档都会顺手生成一份精简的 JSON,你在哪一页问她,那一页的内容就跟着问题一起递给她。
在接口文档页问她“要传什么参数”,第一个字不到一秒就出来了。
差点出事的一个改动
为了按访客限流,我让博客的代理把访客的真实 IP 一起转给 API。本来以为就是多带一个请求头,能有什么事。
结果上线不到十分钟,复查的时候发现:API 有一层缓存,是按网址把整段结果存下来的,而结果里带着请求者的 IP。也就是说,一个人的 IP,可能会被缓存下来,然后显示给下一个人。
赶紧收回,代理改成只给看板娘转发 IP。然后顺手把缓存这个老毛病也修了:命中缓存的时候,IP 换成当前访问者自己的;查自己 IP 的请求,干脆不进缓存。
加一个看起来没什么影响的请求头,可能会悄悄改变别的地方的行为。改动的影响范围,往往比想象的要大那么一点。
改完前后对比
| 对比项 | 改之前 | 改之后 |
|---|---|---|
| 第一个字出来 | 17.9 秒(整段一起出来) | 一般一两秒 |
| 打字效果 | 假的,等写完再切块 | 真的,边生成边显示 |
| 在文章页问“这篇讲了啥” | 要先去翻全文 | 当前页的内容直接递给她 |
| 上游抽风的时候 | 一直转圈 | 10 秒不开口自动重问 |
| 按访客限流 | 所有访客共用一个 IP | 每个访客各算各的 |
写在最后
第一个字从 17.9 秒变成一两秒,整段话说完的时间其实没短多少,但是等的感觉完全不一样了。
折腾了大半天,改了两个仓库,修了一堆坑,还差点把访客的 IP 给串了。不过总算是弄好了,有兴趣的可以点开右下角,跟 33 聊两句试试。
就这样吧,特发文章记录一下。