把 LLM 塞进视图层:一次「每点一次都重新生成」的 Web 实验
开发者用 LLM 直接充当 Web 应用的视图层,每次点击都触发模型生成新 HTML,并实测了多款模型的生成耗时与单页成…
JD Trask 在「Prompt Poets Society」六月聚会上抛出一个想法:Web 服务器本质上是「文本进、文本出」,而 LLM 也是「文本进、文本出」。顺着这条思路,他做了一个名为 🔥 token burner 🔥 的实验性 Web 应用——LLM 直接充当整个应用的视图层,每一次点击都不是在调用已经写好的前端组件,而是触发模型重新生成一整张页面。
作者坦言这是一个「显而易见很糟糕的主意」:每次点击都要花钱、要等几秒钟、且返回结果每次都略有不同。但他依然把它实现了出来,因为在交互范式上,它和传统的 vibe coding 工具有本质区别——生成的 HTML 不是最终产物,而是对话本身的一个回合。
一次点击如何变成一张新页面
应用的机制并不复杂。不是所有交互都会回到模型;很多页面会保留普通的客户端 JS 来处理动画或本地 UI 状态。但只要模型想让某个元素触发新一轮对话,就会给该元素挂上一个 data-prompt 属性(表单则用 data-prompt-template),由一段注入的脚本把点击和提交转换成对 /c/:convoId/:turnId/act 的 POST 请求,body 就是下一条消息。生成的按钮本质上就是「待发送的下一条对话」。
服务端收到请求后做三件事:
- 加载历史:通过一条递归 SQL 查询沿着
parent_turn_id向上回溯对话树,避免兄弟分支的回合混入上下文。 - 强制调用工具:把新提示追加进历史后调用模型,
toolChoice锁定到唯一的render_page({ body_html, summary, style_hints })工具,模型只能返回结构化 HTML。 - 渲染并返回:把
body_html填入共享模板(预加载 Tailwind、D3、Chart.js、Three.js、Tone.js),再作为GET /c/:convoId/:turnId的真实响应返回。
一个关键设计是:模型永远看不到自己上一回合生成过的原始 HTML,每次回放给它的只有它自己写的 summary 字符串。这让输入上下文保持精简,模型在输出 token 上「挥霍」,在输入侧却相当克制。
工程上必须解决的细节
要把单次好玩变成可用的原型,还需要补上几块工程拼图:
- 生成是异步的:回合先以
status: "pending"写入数据库,LLM 调用后台执行,前端每秒轮询一次/status,完成前显示 spinner。 - 分支与去重:回合是一棵以
parent_turn_id为键的树,从同一个父节点触发相同提示时,会复用已有页面而不是重新生成。 - 系统提示要划清边界:作者显式禁止模型在前端伪造「后端功能」,任何需要真实逻辑的交互都必须再走一轮对话。
- 浏览器负责导航:模型被禁止渲染返回按钮,因为生成的「返回」无法真正回到任何地方,只会再烧一轮去模仿之前的页面。真正的浏览器后退键已经够用,沿树走回去再开新分支才是这套交互的意义所在。
不同模型的生成耗时与成本
作者提供了一个模型选择器,让用户在「更聪明」和「更快更便宜」之间自行权衡。下面的数据来自他在不同模型下生成页面的实测(默认模型 claude-sonnet-4.6 的样本量远大于其他模型,其余为更小、噪声更大的样本):
平均页面生成耗时:
- claude-sonnet-4.6:76.1 秒
- claude-opus-4.7:64.7 秒
- gemini-3.5-flash:57.0 秒
- gpt-5.4-nano:47.7 秒
- claude-haiku-4.5:11.6 秒
- gemini-3.1-flash-lite:4.0 秒
平均单页成本:
- claude-opus-4.7:$0.1493
- gemini-3.5-flash:$0.1063
- claude-sonnet-4.6:$0.0904
- claude-haiku-4.5:$0.0098
- gpt-5.4-nano:$0.0079
- gemini-3.1-flash-lite:$0.0016
作者的评价是「比它应得的可用得多」。前沿模型下每一页都能感受到生成延迟,spinner 久到让人烦躁;用 gemini-3.1-flash-lite 这种小而快的模型时,体感已经接近普通页面加载的水平。他同时提醒,因为对话链接是可分享的,打开别人分享的分支相当于执行对方提示词诱导模型写出的 JS——这是一个玩具,请按玩具对待。
