普通视图
沪深两市成交额超2万亿元
小鹏与福耀联合发布AI调光隐私玻璃,首块量产交付
汉王科技发布Clear7锦鲤电纸书
独家|阿里认领屠榜神秘模型「欢乐马」,ATH 郑波团队打造
刚刚,阿里巴巴 ATH 确认 HappyHorse 为阿里 ATH 旗下创新事业部研发。
前段时间网友们一直在猜测的神秘视频生成模型 HappyHorse,正式在微博介绍自己,来自阿里 ATH 创新事业部的内测产品,目前尚未上线,网上流传的「官网」都不是真的。
阿里巴巴 ATH 方面表示:HappyHorse 是阿里 ATH 旗下创新事业部研发的模型,目前正处于内测中,也会于近期开放 API。
ATH 创新事业部已启动一个 AI 时代的全新交互方式探索计划,HappyHorse 是这个探索方向的一部分,更多的产品我们会陆续推出。
APPSO 独家获悉,负责此次 HappyHorse 视频生成模型的是来自阿里 ATH 的郑波团队。
郑波是阿里巴巴副总裁,清华大学计算机系博士,2006 年到 2017 年,领导谷歌的展示广告算法团队以及中国地图团队。
![]()
他在 2017 年 9 月加入阿里巴巴,曾担任淘宝搜推算法负责人、阿里妈妈 CTO、淘天集团算法技术负责人,主要研究方向为大模型,多模态,决策智能,深度学习,搜索、推荐和广告算法以及引擎优化等领域。
![]()
本周三,HappyHorse-1.0 视频生成模型突然出现在 AI 评测平台 Artificial Analysis 的视频竞技场榜单上,以压倒性的表现登顶文生视频、图生视频等多个赛道,直接超越前段时间大火的 Seedance 2.0。
网友们都在猜测 HappyHorse 究竟是哪一家模型厂商的作品,一些山寨的「快乐马」网站也开始在社交媒体上传播,声称可以提供模型访问权限。
直到今天,HappyHorse 通过官方微博账号 HappyHorse_AI 正式发文,确认是由阿里 ATH 创新事业部研发。
AI 评测平台 Artificial Analysis 也在阿里确认这一消息后,在 X 平台迅速发文,表示 HappyHorse-1.0 目前已经在视频竞技场的所有排行榜上都取得了第 1 或第 2 名的好成绩。
在平台「无音频」排行榜上,HappyHorse-1.0 稳居第一;「有音频」排行榜中,它的 Elo 分数几乎与字节的 Seedance 2.0 完全相同。
Artificial Analysis 还提到 HappyHorse-1.0 支持四种视频生成模式:文本转视频、图像转视频,每种模式均可选择是否添加原生音频,而 API 接口计划于 4 月 30 日开放。
在这则推文下,Artificial Analysis 给出了多个 HappyHorse 视频生成的实例。
通过与Seedance 2.0、Kling 3.0 Pro、grok-video-imagine 和 PixVerse V6 的对比,我们能看到 HappyHorse 这匹突然杀出来的黑马,潜力确实不小。
▲提示词:一部皮克斯风格的短片,讲述一个紧张兮兮的小交通锥梦想成为大型比赛的终点线标志杆的故事。
▲提示词:一个篮球在空荡荡的室内球场上弹跳,每一次拍打在光滑的硬木地板上都会发出响亮而有节奏的回声,并伴有橡胶运动鞋的尖锐吱嘎声。
▲提示词:一束手电筒光束探索着一个洞穴系统,照亮了潮湿的石灰岩地层。光线捕捉到闪闪发光的结晶方解石沉积物。当光束穿过浅浅的积水时,在水下地面上投射出明亮的光影图案。节奏的回声,并伴有橡胶运动鞋的尖锐吱嘎声。
▲提示词:一个可爱的小蝙蝠侠,有着巨大的头、小小的身体和大大的眼睛,看起来很可爱而不是可怕。
在阿里今天正式宣布之前,前阿里千问大模型负责人林俊旸昨天就在 X 转发了关于 HappyHorse 的消息,附文「happy horse is insanely happy」。评论区当时就有人在猜测,看来 HappyHorse 可能是千问的视频模型。
阿里这段时间以来,关于 AI 的调整相当频繁。
3 月 16 日,阿里巴巴正式成立 Alibaba Token Hub「ATH」事业群,由 CEO 吴泳铭直接负责,ATH 覆盖了通义实验室、MaaS 业务线、千问事业部、悟空事业部、AI 创新事业部,几乎把阿里现有 AI 关键拼图全部装进了一个框架里。
4 月 8 日,CEO 吴泳铭发布全员信,再宣布 AI 相关组织的重大调整,成立集团技术委员会,原通义实验室升级为通义大模型事业部。
短短 23 天,完成了两次 AI 组织架构的调整。
HappyHorse 模型的推出,大概能看到阿里 AI 战略的主线,会不断地从模型能力,到平台分发,再到具体应用,都要争做第一,实现完整的闭环。
#欢迎关注爱范儿官方微信公众号:爱范儿(微信号:ifanr),更多精彩内容第一时间为您奉上。
生态环境部:拟自2027年起,禁止生产以HCFC-22为制冷剂的多联式空调(热泵)机组产品
中康科技推出医生超级助理MedMate
多重利好叠加,一季度我国汽车出口增长强劲
为什么生产环境很少手写流式响应:AI SDK 三层架构一次讲清
我们在 AI 应用开发 | 手写流式输出:把打字机效果背后的数据流拆开看 里面,已经把流式输出这件事手写跑通了。
但真把这套东西往聊天应用里接的时候,你很快就会感觉到,问题已经不是字怎么出来了,而是后面那一串和业务本身无关、却又必须有人接住的细节:
chunk 边界、半截 JSON、消息历史怎么记、流到一半要不要让用户停下。
今天聊的是 为什么要从手写切到 Vercel AI SDK。
先划重点
手写流式响应跑通之后,往真实聊天应用走,会被两件事情拖住:
一类是看不见的协议翻译,拆 chunk、拼半个字、认结束标记; 另一类是看不见的对话状态,记历史、知道在不在流、能不能重生成。
AI SDK 把这两类工作,分散到三个位置接走:
- 在最上游,把不同模型的 API 翻成一种统一格式
- 在服务端,统一调模型 + 统一回一条结构化事件流
- 在前端,统一维护消息数组和流状态
手写版不是不能跑,是越往前走越像在造基础设施
手写版跑通之后,真接聊天应用你会发现,花时间的地方已经不在打字机效果上了。
一部分时间花在协议边界上。
一个中文字符完全可能被拆到前后两个 chunk 里,一段 JSON 只拿到半截就 JSON.parse 直接炸掉,SSE 事件还得自己按 \n\n 切分、挑出 data 字段、认结束标记。
这些工作原本和业务没一点关系,但不接住一个,整条链就走不通。
chunk 是网络边界,不是业务消息边界。
![]()
另一部分时间花在对话状态上。
手写版的前端 state 一开始通常就长这样:
const [answer, setAnswer] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
answer 这个字符串 state 一开始单轮够用,但一旦需求里出现多轮历史、重生成、从中间分叉,每条消息还得分清是用户还是 AI、是文字还是工具调用、有没有完成,这一串东西塞不进一个字符串里。
一段字符串记得住一句话,记不住一段对话。
![]()
所以手写版的问题不是不能跑,是你一旦从 demo 往真实聊天应用走,就得在协议和状态这两层各造一套自己的基础设施。
花时间的地方不再是业务,而是这些重复又容易出错的细节。
AI SDK 覆盖的是整条链,不是某一个点
聊到 Vercel AI SDK 之前,我先说一下我自己对它的认知是怎么变的。
我一开始也以为它就是个 React hook,useChat 一调就完事了。后来多看几次才发现,它其实是一整套工具。
从模型怎么接进来、服务端怎么调模型、到前端怎么维护对话状态,整条链都覆盖了。
前面那两层工作,协议翻译和状态管理,不是某一个文件的问题,是从模型到前端整条链上散落的细节。
能把这两层一起接住的,也只能是一套覆盖整条链的东西,而不是一个孤立的 hook 或者一个孤立的服务端函数。
我自己是用一家餐厅在脑子里记这条链的
一家餐厅要上菜,它先得有供应商。
不同供应商送来的东西规格千差万别,有的按斤、有的按箱,包装单据也都不一样。
如果后厨每接一家就要重学一遍人家的规矩,这家餐厅根本开不起来。
所以稍微大一点的餐厅都会有一个收货环节,不管哪家供应商送来的,都按统一的规格入库,后厨拿到的永远是同一种格式。
收货之后,后厨才开始干自己的活,按统一的菜单做菜,按统一的餐具盛出来,再交给前台。后厨不关心这块牛肉是哪家送的,它只认入库后的规格。
前台的工作又是另一回事。它不做菜,但它要记住这一桌点了什么、上到第几道、客人有没有催菜、能不能换菜。
AI SDK 这套架构,几乎就是把这家餐厅的分工原封不动搬过来了。
- 不同模型厂商就是不同的供应商
-
provider adapter(
@ai-sdk/xxx) 就是收货环节,把不同厂商 API 的差异在这里一次性抹平 -
streamText+toUIMessageStreamResponse()就是后厨,统一调模型 + 统一往外回一条结构化事件流 -
useChat就是前台,记住这桌的整段对话现在是什么状态
![]()
带着这张图往下读,后面三个节点就是这条链上的三个停靠点。
第一个节点:先把模型接进来
不同模型厂商的 API 本来就未必长一样,路径、鉴权 header、流式事件格式、文本字段路径都可能不一样。
你如果每次都直接对着厂商 API 写代码,很快就会反复写这一家怎么接、那一家怎么接。
这时候 AI SDK 的模型接入层就开始把这事接过去了。
provider 和 adapter 这两个词后面会反复出现,我们先来认识一下。
provider 就是模型的供货方,DeepSeek 是一个 provider,OpenAI 是一个 provider,Anthropic 也是一个 provider。
adapter 就是替你跟这家 provider 对话的那段代码。AI SDK 的做法是,每家 provider 都对应一个 adapter 包。
一个 adapter 收一家 provider
import { createOpenAICompatible } from '@ai-sdk/openai-compatible';
export function createDeepSeekAiSdkProvider() {
return createOpenAICompatible({
name: 'deepseek',
apiKey: process.env.DEEPSEEK_API_KEY!,
baseURL: process.env.DEEPSEEK_BASE_URL ?? 'https://api.deepseek.com'
});
}
@ai-sdk/openai-compatible、@ai-sdk/anthropic 这一类包,做的事不是生成文本。
它先替你把不同模型厂商接成 AI SDK 能认的一种统一模型接口。
adapter 的粒度,不是公司品牌,而是协议形状
这里我一开始也有点疑惑,DeepSeek 是一家独立的大模型公司,为什么要用一个叫 openai-compatible 的包来接?
DeepSeek 的 API 形状和 OpenAI 高度兼容,请求路径、请求体字段、流式事件格式基本一致,所有为 OpenAI 写的工具链几乎零代码就能接入。
这不是 DeepSeek 一家的选择,Moonshot、Groq、OpenRouter、Ollama 走的都是同一条路。
所以 @ai-sdk/openai-compatible 根本不在乎对面是 OpenAI 还是 DeepSeek,它只看协议形状对不对,给它配上 baseURL 和 apiKey 就行。
Claude 是走另一条路的典型。
它有自己一套独立的 API 形状,路径、鉴权 header、事件类型、字段结构都和 OpenAI 不一样,所以要走 @ai-sdk/anthropic 这种专门适配。
这一层先替你接住的,不是页面,而是上游模型 API 的差异。
第二个节点:服务端怎么统一调模型和回流
模型接上之后,前端发一条消息过来,服务端总得先有一层把这次请求接住,拿到消息、调模型、再把结果回给前端。
如果你用的是 Next.js,这层通常就在 app/api/.../route.ts 里,最常见就是那个 POST 函数。
后面如果我提到 route handler,你先把它理解成这层服务端入口就行。
原来这层里那一坨读流、缓冲、拼字符串的代码,现在基本就剩调一个函数加返回一个函数。
SDK 版服务端入口还剩什么
import { convertToModelMessages, streamText, type UIMessage } from 'ai';
export async function POST(req: Request) {
const { messages }: { messages: UIMessage[] } = await req.json();
const result = streamText({
model: deepseek.chatModel('deepseek-chat'),
messages: await convertToModelMessages(messages)
});
return result.toUIMessageStreamResponse();
}
这几行做了三件事:
convertToModelMessages 把前端消息格式翻成模型能认的格式;
streamText 调模型拿流式结果;
toUIMessageStreamResponse() 再把结果翻回前端能消费的事件流。
原来手写版那一大坨读流、缓冲、拼字符串的代码,基本都被这三个函数接走了。
手写版那层细节,SDK 是怎么处理的
![]()
请求上游模型、读流续字、处理事件边界和结束标记,这些原来散落在服务端入口这层的工作,
全部被 provider adapter 加 streamText 在内部接走。
最后那条「怎么把结果回给前端」的统一响应,则由 toUIMessageStreamResponse() 接走。
服务端轻下来的不是业务逻辑,是那些重复的流式协议处理。
前后端之间,其实还隔着一道翻译
convertToModelMessages 和 toUIMessageStreamResponse() 这两个函数,是一对翻译器。
前端要渲染工具调用卡片、错误块、思考过程,所以 UIMessage 里带了一堆 parts;
但模型只认扁平的 role + content。
一端多一端少,中间就得有人翻。
convertToModelMessages 是把前端消息翻给模型,toUIMessageStreamResponse() 是把模型流再翻回前端消息流。
进去翻一次、出来也翻一次。
为什么回流不能只是纯文本
这里我一开始也没想清楚,手写版那套纯文本流明明能 work,toUIMessageStreamResponse() 为什么要做成一串带 type 字段的 JSON 事件?
因为前端要消费的不止是字。
AI 中途要显示「正在调用 search 工具」和工具结果卡片,要把灰色的「思考过程」和最终回答分开。
中途抛错要让 UI 识别这不是回答内容,结束时还要有一个明确信号解锁输入框。
如果流里只有字,浏览器根本没法区分哪段是思考、哪段是工具调用、哪段是错误、哪段是最终回答。
所以 SDK 把回流做成了结构化事件流,每个事件都带 type: "text-delta" 或 type: "tool-call" 这种标签,前端看到什么 type 就走什么分支。
![]()
打开浏览器 DevTools 看一眼真实返回流会更直观:
{"type":"text-delta","id":"txt-0","delta":"Vercel"}
{"type":"text-delta","id":"txt-0","delta":" AI"}
{"type":"text-end","id":"txt-0"}
{"type":"finish-step"}
{"type":"finish","finishReason":"stop"}
[DONE]
模型 原始 SSE 里,文本增量走的是厂商自己的字段路径,结束信号也走的是厂商自己的结束格式。
AI SDK 回给前端的事件流里,文本增量统一变成 text-delta,结束信号统一变成 text-end、finish-step、finish 这一类标签。
浏览器看到的已经是统一后的结果,不是模型厂商自己的原始协议。
前端处理渲染只要按 type 分发就行,不需要再认任何厂商的方言。
顺带留个钩子:messages 为什么是整段传上来的
你看这层服务端入口里 const { messages } = await req.json(),可能会以为前端传一句话、后端维护历史就行。
但大模型 API 本身是无状态的。
OpenAI、Claude 等大模型的 HTTP 接口其实都没有会话这个概念,每次请求都得把整段对话历史重新喂一次,模型拿到数组那一刻才「恢复」出上下文,生成完立刻丢掉。
所以这条链上的服务端入口每次都是全新的、无记忆的,它只是个转发层,状态落在前端的 messages 数组里。
对话越来越长之后,这个数组也会越来越大,token 吃不消了怎么办、记忆怎么维护,这是下一篇要讲的事。
第三个节点:前端怎么接住消息状态
服务端这层轻下来以后,前端这层也就没那么复杂了。
手写版前端状态,一开始通常就长这样
const [input, setInput] = useState('');
const [answer, setAnswer] = useState('');
const [isStreaming, setIsStreaming] = useState(false);
SDK 版就会变成:
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({ api: '/api/ai-sdk-chat' })
});
useChat 暴露的不是 answer,是整个 messages 数组
这两段代码的差别,真不是少写几个 state 这么简单。
useChat 接住的是请求发送、流读取、消息追加、状态流转。
你不用再自己读 response.body,也不用自己一边拼字符串,一边维护 isStreaming 什么时候开、什么时候关。
更关键的是它暴露给你的不是一个 answer,是一整个 messages 数组,每条消息长这样:
type UIMessage = {
id: string;
role: 'user' | 'assistant';
parts: Array<{ type: string; text?: string }>;
};
这套结构本来就是给整段对话准备的。
多轮、重生成、从中间分叉这些功能往上一加,全都顺势就成立。
举个例子,regenerate() 就是对 AI 最后一条回答不满意,重新生成一次。
手写版里这件事通常不是再发一次请求这么简单,你得先砍掉最后一条 answer、把字符串状态倒推回消息数组、再补重发逻辑。
而 AI SDK 这边,messages 数组本来就记着每条消息,调一个方法的事。
所以真正少的不是代码行数,是你不用自己从零搭一套消息结构。
但页面最后怎么长,还是业务自己决定
它也没有替你把前端全部做完。
输入框 state、消息怎么渲染、错误 UI 怎么展示、按钮什么时候禁用、滚动什么时候到底,这些还是业务自己决定。
比如输入框的 input / setInput 就是业务自己维护的,useChat 不管:
<textarea value={input} onChange={(event) => setInput(event.target.value)} />
<button type="submit" disabled={status !== 'ready'}>
{status === 'streaming' ? 'AI SDK 流式输出中...' : '调用 AI SDK 流式输出'}
</button>
useChat 接的是聊天状态,不是聊天页面长相。
如果你正从手写版迁过来,先改这三处
前面那条三层链路看懂之后,回到代码里第一刀该下在哪,其实顺序是固定的。
第一刀,先改 provider。
先别动前端,用 @ai-sdk/openai-compatible 或对应的 adapter 包把厂商 API 包一层,让后面 streamText 能直接调,上游差异先在这里隔离掉。
第二刀,再改服务端入口。
把手写读流、缓冲、组装响应那层换成 streamText 加 toUIMessageStreamResponse(),服务端重复的协议细节先拿掉。
第三刀,最后改前端状态。
把 answer 这种单字符串状态换成 useChat 和 messages,后面多轮、重生成、分叉、工具调用才有稳定地基。
从手写版切到 AI SDK,第一刀先别改页面,先改协议层和状态层。
![]()
改完这三刀,你会在两件事上感觉到变化。
一是原来服务端那坨 chunk 拼接、JSON 兜底、字段路径的代码,不用再看第二眼,协议那层不用碰了。
二是后面再冒出「重生成」「从某条消息重开」这类需求的时候,你不用先回头重构 state,useChat 暴露的 messages 数组已经为这些需求打好地基了。
省下来的不只是代码行数,更像是脑子不用再同时装怎么读流和怎么记对话这两件事。
现在来想想以下问题
Q1:服务端返回了一个 type: "tool-call" 的事件,但前端不知道怎么渲染。你觉得问题出在三层里的哪一层?
💡 provider adapter 负责接模型,streamText 负责调模型和回流,useChat 负责前端状态。tool-call 事件已经到了前端,说明前两层没问题。
Q2:如果你要给聊天页面加一个「从这条消息重新开始」的功能,手写版和 SDK 版各需要改什么?
💡 手写版的 answer 是一段字符串,你得先重构出消息数组才能定位到"这条消息"。SDK 版的 messages 数组里每条消息都有 id,天然支持这个操作。
Q3:现在 useChat 每次请求都把整个 messages 数组传给服务端,对话聊了 50 轮之后,你觉得会先撞到什么问题?
💡 这是下一篇的主题:对话越来越长,token 吃不消了,记忆怎么维护。
感谢您的阅读~🌹
我在微信公众号 前端Fusion 中也会持续同步更新关于 AI 与前端开发的相关文章,欢迎大家关注,一起交流学习。
![]()
SBTI 测试挤崩服务器:一个程序员视角的技术复盘
昨晚你的朋友圈,是不是也被"尤物""吗喽""愤世者"刷屏了?
4月9日晚,一个叫 SBTI 的人格测试突然引爆社交网络。用户蜂拥而至,网站直接崩了——页面打不开、链接失效,大家只能靠截图"云测试"。作者深夜紧急发布新链接,称"做了略微修改,应该不会再崩了"。
作为一个技术人,看到"网站崩了"和"略微修改就不崩了"这两句话,我的职业病就犯了。今天我们不聊测试准不准,只聊:这背后到底发生了什么?如果是你来做,怎么才能扛住这波流量?
一、崩溃现场还原:一个经典的"雷群效应"
SBTI 测试的崩溃,是教科书级别的 Thundering Herd(雷群效应)案例。
简单说就是:一个原本只为"劝朋友戒酒"而做的小项目,突然被几百万人同时访问。这就好比你开了一家只有两张桌子的面馆,突然被美食博主推荐,门口排了两公里的队。
根据公开信息推测,SBTI 最初大概率是这样的架构:
用户浏览器 → 单台服务器(前端 + 后端 + 数据库 all-in-one)
这种架构在日常几百、几千 PV 的场景下完全够用。但当朋友圈裂变式传播启动后,并发量可能在几分钟内从个位数飙升到数万甚至数十万级别。单机扛不住,结果就是:
- 连接池耗尽:服务器能同时处理的请求数是有限的,超出后新请求直接被拒绝
- 带宽打满:测试页面包含图片、样式、脚本,每个用户加载一次就消耗几百 KB 到几 MB 的带宽
- 如果有后端逻辑:数据库连接数爆满,CPU 被打满,响应时间从毫秒级飙升到超时
二、"略微修改"背后的技术真相
作者说"做了略微修改,应该不会再崩了"。这句话信息量很大。
对于一个测试类 H5 应用,最高效的"略微修改"大概率是以下几种操作之一(或组合):
方案 A:纯静态化 + CDN 分发
测试类应用的核心逻辑其实很简单:展示题目 → 用户选择 → 前端计算结果 → 展示结果页。整个过程完全可以在浏览器端完成,不需要后端服务器参与。
之前:用户 → 源站服务器(动态渲染)
之后:用户 → CDN 边缘节点(静态资源)→ 前端 JS 本地计算结果
把所有页面打包成纯静态文件(HTML + CSS + JS + 图片),扔到 CDN 上。CDN 在全国有几百个边缘节点,用户访问时会自动路由到最近的节点。这样源站压力几乎为零,理论上可以承载千万级并发。
方案 B:更换托管平台
从自建服务器迁移到 Vercel、Cloudflare Pages、Netlify 等现代静态托管平台。这些平台天然具备全球 CDN 分发能力,部署一个静态站点只需要几分钟。
方案 C:Serverless 化
如果测试逻辑中确实有需要后端参与的部分(比如 AI 生成结果文案),可以将后端逻辑迁移到 Serverless 函数(如 AWS Lambda、阿里云函数计算)。Serverless 的核心优势是自动弹性伸缩——流量来了自动扩容,流量走了自动缩容,按实际调用次数计费。
三、如果让你从零设计,架构应该长什么样?
假设你现在要做一个类似 SBTI 的病毒式传播测试应用,并且预期它可能会爆火,推荐的架构如下:
┌─────────────────────────────────────────────────┐
│ 用户浏览器 │
│ ┌───────────┐ ┌──────────┐ ┌───────────────┐ │
│ │ 答题引擎 │ │ 计分逻辑 │ │ 结果图片生成 │ │
│ │ (纯前端) │ │ (纯前端) │ │ (Canvas/SVG) │ │
│ └───────────┘ └──────────┘ └───────────────┘ │
└──────────────────────┬──────────────────────────┘
│ 静态资源请求
▼
┌─────────────────┐
│ CDN 边缘节点 │
│ (全国 300+ 节点) │
└────────┬────────┘
│ 回源(极少触发)
▼
┌─────────────────┐
│ 对象存储 (OSS) │
│ HTML/CSS/JS/图片 │
└─────────────────┘
核心设计原则:
- 计算下沉到客户端:题目数据、计分逻辑、结果映射全部内嵌在前端代码中,浏览器本地完成所有计算,服务端零压力
- 资源全量 CDN 化:所有静态资源通过 CDN 分发,用户就近访问,首屏加载时间控制在 1-2 秒内
- 结果图片客户端生成:使用 Canvas API 或 html2canvas 在用户浏览器中直接生成分享图片,避免服务端图片渲染的性能瓶颈
- 零后端依赖:整个应用不需要数据库、不需要后端 API,运维成本趋近于零
四、病毒传播的技术引擎:分享链路优化
SBTI 能刷屏朋友圈,除了内容本身的娱乐性,分享链路的技术设计也至关重要。
微信分享卡片优化
// 微信 JS-SDK 分享配置
wx.updateAppMessageShareData({
title: '我的SBTI人格是【尤物】,你是什么?', // 动态标题,包含测试结果
desc: 'MBTI已经过时了,来测测你的SBTI人格吧',
link: 'https://example.com/sbti?from=share', // 带来源追踪参数
imgUrl: 'https://cdn.example.com/sbti-share.jpg' // 高辨识度的分享缩略图
})
关键技术点:
- 动态分享标题:将用户的测试结果嵌入分享标题,制造好奇心驱动的点击欲望
- 结果图片生成:用 Canvas 将测试结果渲染为一张精美的图片,方便用户保存并发到朋友圈
- 短链接 + UTM 追踪:通过 URL 参数追踪传播路径,了解哪个渠道带来的流量最大
分享图片的客户端生成方案
import html2canvas from 'html2canvas';
async function generateShareImage(resultElement) {
const canvas = await html2canvas(resultElement, {
scale: 2, // 2倍分辨率,保证清晰度
useCORS: true, // 允许跨域图片
backgroundColor: null // 透明背景
});
// 转为图片供用户长按保存
const imgUrl = canvas.toDataURL('image/png');
return imgUrl;
}
这个方案的好处是:图片在用户手机上生成,不需要服务端渲染,即使同时有 100 万人生成分享图,服务器也毫无压力。
五、AI 在其中扮演的角色
根据公开信息,SBTI 的人格描述内容使用了 AI 生成技术。这带来了一个有趣的架构选择:
方案一:预生成(推荐)
在开发阶段就用 AI 生成好所有人格类型的描述文案,作为静态数据打包到前端代码中。运行时不需要调用 AI 接口,零延迟、零成本。
// 预生成的结果数据,直接内嵌在前端代码中
const SBTI_RESULTS = {
'ABCD': {
title: '尤物',
description: 'AI生成的人格描述文案...',
image: '/assets/results/abcd.png'
},
'EFGH': {
title: '吗喽',
description: 'AI生成的人格描述文案...',
image: '/assets/results/efgh.png'
}
// ... 其他类型
}
方案二:实时生成(不推荐用于病毒传播场景)
每次用户完成测试后实时调用 AI API 生成个性化描述。这种方案在流量暴增时会面临:API 调用成本飙升、响应延迟增大、API 限流等问题。
从 SBTI 的实际表现来看(崩溃后"略微修改"就恢复了),大概率采用的是方案一——AI 只在开发阶段参与内容生产,运行时是纯静态应用。
六、成本算一笔账
假设 SBTI 在爆火期间有 500 万次访问,每次访问加载约 2MB 资源:
| 方案 | 预估成本 | 能否扛住 |
|---|---|---|
| 单台云服务器(2核4G) | ¥100/月,但会崩 | ❌ |
| CDN + 对象存储 | 流量费约 ¥500-1000 | ✅ |
| Vercel/Cloudflare Pages 免费版 | ¥0(有带宽限制) | ⚠️ |
| Vercel Pro + CDN | ¥150/月 | ✅ |
一个纯静态的测试应用,即使面对百万级流量,CDN 方案的成本也就是一顿火锅钱。而如果用单机硬扛,服务器费用可能不高,但用户体验的损失是无法估量的——多少潜在的传播链路因为"页面打不开"而断裂了。
七、给开发者的 Takeaway
SBTI 事件给我们的启示:
-
永远为最好的情况做准备:如果你的产品有社交传播属性,请在架构设计时就考虑流量暴增的场景。CDN + 静态化的成本几乎为零,但收益是巨大的。
-
能在前端做的事,别放到后端:测试类应用的计算逻辑完全可以在浏览器端完成。每减少一次服务端请求,就多了一份稳定性。
-
分享体验就是增长引擎:结果图片的生成质量、分享卡片的文案设计,直接决定了传播系数。技术上要保证分享链路的流畅性。
-
AI 是内容生产工具,不是运行时依赖:对于这类应用,AI 最适合在开发阶段批量生成内容,而不是在运行时实时调用。
-
小项目也值得好架构:SBTI 的作者最初只是想劝朋友戒酒,没想到会火。但如果一开始就用静态托管方案,根本不会有崩溃这回事。好的架构不一定复杂,但一定要匹配场景。
一个为劝朋友戒酒而生的测试,意外成为了一堂生动的高并发架构课。技术世界的浪漫,大概就是这样吧。
八、最后
文中技术分析基于公开信息推测,不代表 SBTI 实际技术实现。
如果你也在职场摸索成长路线,想了解更多内部跳槽、团队优化、技术实践和职场认知升级的经验,可以关注我的公众号: [码农职场]
后续我会分享更多干货,帮助你在职场和技术上持续突破。