在这里插入图片描述

码道:用AI对话书写诸神黄昏的诗篇——从一行Python到零依赖HTML的史诗创作记

当预言中的巨狼挣脱锁链,当火焰巨人挥剑焚天,当世界树在雷霆中震颤,你是否也曾想过:让一位见证了诸神黄昏终末之战的吟游诗人,亲口为你吟唱这首终章?本文记录了我如何用一个纯前端、零依赖的 HTML 项目,将大语言模型的流式对话能力,锻造成一首首属于"诸神黄昏"的史诗诗篇的完整过程。

–![外链图片转存失败,源站可能有防在这里插入图片描述

在这里插入图片描述

一、缘起:一句偶然的自问

今年秋天的一个深夜,我像往常一样浏览着技术社区。一条讨论区里的帖子再次点燃了我对北欧神话的兴趣:有人在讨论"诸神黄昏"(Ragnarök)——北方神话体系里那场决定世界命运的最终决战。人们习惯称它为"诸神黄昏",可它并不是一个简单的末日故事。火之巨人苏尔特尔挥舞着比太阳更炽烈的烈焰之剑,巨狼芬里尔挣脱了众神用六种神秘之物锻造的锁链,毒龙尼德霍格啃断了世界树的根须,彩虹桥比弗罗斯特在混乱中轰然断裂,金鸡古林肯比在英灵殿的塔尖发出最后的长鸣……这些意象充满了悲剧的壮美,也让每个读到的人都涌出一种想要"为它写点什么"的冲动。

当时我手头正在做一个 AI 对话类的练手项目。随口一问自己:"能不能让 AI 扮演一位吟游诗人,让用户以对话的方式,请他一边回应我们的言语,一边把回应织入诸神黄昏的诗篇?"这个念头像一粒火种,瞬间点燃了整个想法:做一个"AI 对话写诗"的小工具,主题锁死"诸神黄昏",让 AI 以吟游诗人之名,把用户的每一句话都锻造成史诗。

这大概就是"码道"上最常见的起点——一个足够浪漫、足够具体的想法,配上一个足够朴素的实现方式。我给它起名**《诸神黄昏 · 诗篇》**,技术栈只有三个词:HTML5、CSS3、JavaScript。


二、神话的底色:为什么要选诸神黄昏

在动手之前,我花了不少时间梳理诸神黄昏的神话结构。这决定了我往后的"系统提示词"怎么写、JSON 数据结构的字段怎么定、前端的视觉氛围如何营造。

北欧神话的框架与一般神话很不一样:它没有一个"正义必胜"的终局,相反,它坦然接受了诸神在宿命面前的失败。奥丁尽力召集英灵,却在与巨狼芬里尔的搏斗中被吞噬;雷神托尔击杀了尘世巨蟒耶梦加得,却随即被它的毒液带走;洛基背离了自己的血缘,加入了敌人的阵营……在这片灰烬与烈焰之中,唯一确定的是"旧的世界燃烧殆尽,新的世界从海面重新升起"。这种"毁灭与新生并存"的张力,正是诗意最丰沛的土壤。

我把这些意象整理成一张"诗料清单",在系统提示词里明确告诉模型可以动用的关键词汇:芬里尔、苏尔特尔、奥丁、托尔、耶梦加得、世界树伊格德拉西尔、追逐日月的双狼斯库尔与哈提、毒龙尼德霍格、瓦尔基里、英灵殿、彩虹桥、金鸡古林肯比、命运三女神…… 让模型在与用户对话时,不是凭空编造,而是"就地取材",在这些意象之间自由穿梭。有了这个语料池打底,AI 生成的内容才会真正有"那味儿",而不是空泛的堆砌。

事实证明,这个决定极其重要。同样的模型,喂给不同的"人设与意象框架",产出的文字质感截然不同。大语言模型的"上限"一半取决于模型本身,另一半取决于你如何为它铺设语境。 这是我在这个项目里体验最深刻的一条经验。


三、技术选型:为什么是"纯前端 + 零依赖"

市面上做 AI 对话工具,似乎默认就要上 React、Vue 甚至 Next.js,配一个 Node 后端转发密钥再做流式中转。但我在这个项目上做出的是相反的决策:HTML5 + CSS3 + JavaScript 三件套,零依赖、无构建、无框架、无后端。

理由有三。

其一,分享的门槛最低。一个由三个文件组成的项目,直接双击 index.html 就能跑;也可以丢进任意静态托管平台(GitHub Pages、Vercel、AtomGit Pages)一键上线。对读者而言,他们不需要装 node_modules,不需要跑 npm install,不需要理解任何构建工具。在"码道"这条路上,作品的传播速度往往与它的依赖复杂度成反比。

其二,流式对话本身并不需要重型框架。浏览器原生的 fetch + ReadableStream 就能完美处理 HTTP 流式响应(SSE)。React 擅长的复杂状态管理在这里反而显得多余——我们的状态模型很简单:一段对话历史、一个正在吟唱中的布尔值、一个 DOM 渲染的结果。

其三,它逼我回到基本功。用原生 DOM API 操作节点、用 requestAnimationFrame 驱动打字机效果、用 canvas 写粒子系统……这些"过时"却依然强健的技术,做出来的东西反而有一种克制的美感,也让我重新理解了 Web 平台本身的表达力。

当然,纯前端也带来了代价,最明显的一项就是密钥必须暴露在浏览器里,这在生产环境是不可接受的(下文会专门讨论)。但它作为一个演示、学习、创作的载体,完全够了。在"码道"上,把一件事用最简单的方式做成、做漂亮,本身就是价值。


四、一次艰难的"语言移植":Python 流式对话 → JavaScript

我最初验证想法的代码是 Python 写的,借助 requests 库那套无比优雅的流式读取:

def query(payload):
    response = requests.post(API_URL, headers=headers, json=payload, stream=True)
    for line in response.iter_lines():
        if not line.startswith(b"data:"):
            continue
        if line.strip() == b"data:[DONE]":
            return
        yield json.loads(line.decode("utf-8").lstrip("data:").rstrip("/n"))

它把"事件流"折叠成一个生成器,写起来如行云流水。但当我要把这套逻辑搬家到浏览器里时,问题出现了:浏览器的 fetch 并没有提供一行一行读的能力,它只给了你一个原始的字节流(ReadableStream)。 我必须自己处理"字节流 → 文本 → 按行切分"的整条链路。

我的方案是这样的:用 getReader() 拿到流读取器,用 TextDecoder 把二进制解码为 UTF-8 字符串,然后把每一次到达的数据块追加进一个 buffer,按 \n 拆分成行;最关键的是,最后一行不急着处理,它很可能是被网络切开的半行,要留到下一个数据块到达时再拼接。这个"保留尾行"的技巧是整个流式解析里最容易出错的地方。

const reader = res.body.getReader();
const decoder = new TextDecoder("utf-8");
let buffer = "";
while (true) {
  const { done, value } = await reader.read();
  if (done) break;
  buffer += decoder.decode(value, { stream: true });
  const lines = buffer.split("\n");
  buffer = lines.pop();          // 关键:保留可能不完整的行
  for (const line of lines) {
    if (!line.trim().startsWith("data:")) continue;
    const data = line.trim().slice(5).trim();
    if (data === "[DONE]") continue;
    // JSON.parse(data) → choices[0].delta.content 累计
  }
}
// 流结束后,还要处理 buffer 里残留的最后一行

从"生成器 + iter_lines"到"while + 手动 split + 保留尾行",语法层面的改动看起来不大,但思维方式完全换了一副:Python 把流抽象的"正好",JavaScript 则把流的原始面貌交还给你,逼你自己实现那层"正好"。 我因此写了一组自动化测试片段来验证边界情况:一条 SSE 行被切成两半时数据不丢、连续多行在任意位置被切开时顺序不乱、流结束时最后一行没有换行符时也能被收尾处理。全部通过后,我才敢说这次移植是可靠的。

这也是"码道"上很有代表性的时刻:当你铲平了一座框架的山,往往就要亲手垒一堵协议细节的墙。 但垒完你会发现,你对这门语言的理解深了一层。


五、提示词工程:让模型交出"可以直接渲染的 JSON"

后端链路打通后,我面临的设计核心问题是:AI 返回的是一串自由文本,前端怎么把它排成华丽的诗篇卷轴?

两条路摆在面前:

  1. 后端/前端做文本解析:让 AI"诗意地自由发挥",交给正则去猜哪段是标题、哪段是诗句、哪段是意境。这条路看似自由,实则脆弱——模型稍微换个格式,解析就崩。
  2. 让模型自身输出结构化 JSON:在系统提示词里把格式钉死,要求模型只输出 JSON,前端解析后分区渲染。这是当前业界更推崇的"结构化输出"路线。

我毫不犹豫地选了第二条,并设计了一套针对本项目的数据契约:

{
  "reaction": "吟游诗人对玩家话语的即时回应,1-2 句散文短句",
  "poem": {
    "title": "诗篇标题(不超过 12 汉字)",
    "scene": "对诸神黄昏相关场景的描写(2-3 句)",
    "characters": ["登场的神明、巨人或人物", "2-4 个"],
    "tone": "基调与主题词,如「悲壮」「烈焰」「终焉」",
    "verses": ["诗句1", "诗句2", "…", "诗句6"],
    "imagery": "对整首诗意境与画面的诗意总述(2-3 句)"
  }
}

reaction 解决"对话感"——它让 AI 先像一个人那样回应你;poem 解决"史诗感"——它让内容像一件被锻造成形的艺术品。前端只要拿到这个对象,就能把 title / scene / characters / tone / verses / imagery 六个字段分别渲染到不同的视觉区域。数据契约一旦立好,前端渲染就退化成了机械的模板填充,稳定性和美感都交给了契约本身。

当然,理想归理想。大模型输出 JSON 时依然会犯三类"小毛病":

  • 冷不丁用 Markdown 代码围栏(` ```json … ````)把 JSON 包起来;
  • 在 JSON 前后夹一些"好的,这是你的诗:"之类的散文;
  • 极偶尔在某一行写出一个非法的转义符号。

为此我实现了三道容错防线:第一道,用正则剥掉代码围栏;第二道,找到首尾两个花括号截取片段(extractJSON);第三道,按"清洗后整串 → 截取片段"的顺序轮番尝试 JSON.parse,直到拿到可信对象。即便最终全部失败,也会优雅降级成纯文本展示,对话流永不中断。后来我把它提炼成了一个小工具函数 tryParseJSON,在多个项目里都能复用。“让模型输出结构化数据 + 在消费端做好兜底”,这条双保险思路值得所有 AI 应用开发者借鉴。


六、界面设计:把"北欧史诗暗黑风"落到像素

视觉是这类项目最能出彩的地方,也是我投入时间最多的部分之一。用户选择了"北欧史诗暗黑风",这个方向给了我巨大的发挥空间。

配色系统建立在三条主线上:以 #08060e 为首的深空黑铺陈夜色的底,以 #e8c46a 金色与 #ff6b35 火焰橙构建"火与金的史诗"主调,再以少量冰蓝 #9fb8d4 点缀"寒夜"的冷感。页面背景用了三层叠压:顶部一束暖橙的径向渐变模拟地平线上的战火,右下角一抹紫罗兰模拟暮色,再由一段深色线性渐变统一整个夜晚。CSS 的 :root 变量让这套色彩体系变成可维护的设计令牌。

氛围组件我写了三样小东西:

  1. 星尘画布:一个覆盖全屏的 canvas,根据屏幕面积动态生成 100~220 颗缓慢下落的星尘,每颗都带独立的透明度、半径与漂移速度;再以约 0.4% 的随机概率生成一颗斜坠的"流星",拖着一条渐隐的尾迹——我把它设想为英灵殿上空划过的火光。这里用到了最朴素的 requestAnimationFrame 逐帧渲染,帧率稳定、资源开销可控。
  2. 符文分隔线:模板用古北欧如尼字母(ᚱᚨᚷᚾᚨᚱᛟᚴ)拼出 “RAGNAROK”,标题两侧以渐隐的金橙横线与 ✦ 符号构成仪式感的分割。
  3. 诗篇卡片:右侧诗篇卷轴的每一首新诗都以"卡片升起"动画登场,纵深的 box-shadow 叠上内生的暖光晕,四角用火焰橙的 L 形描边做装饰性包边,仿佛一张从火堆里取出的羊皮卷。

布局采用左右两栏:左侧是与吟游诗人的对话流,右侧是诗篇卷轴。对话流里,诗人的头像是一枚如尼字母 ᛒ,玩家的头像则是"旅"字;诗人的回应以 requestAnimationFrame 驱动的打字机效果逐字上屏,末尾拖着一枚闪烁的 ⋮ 光标,等你读完又悄然熄灭。诗篇卷轴内部分区排列:标题居中加金色辉光 → 场景描写 → 登场人物铭牌(每枚都是一张带渐变底的小标签)→ 诗行区(六行诗句自下而上逐行浮现,每行延迟 0.35 秒,如列队般次第亮起)→ 意境总述 → 落款"由布洛基述于黄昏之刻"。整套交互节奏刻意放慢,让"等待诗篇成形"本身变成一种享受。

为了不让用户面对空白的输入框发呆,我还预设了四枚"灵感符文"快捷按钮:战前之歌、烈焰焚天、诸神挽歌、世界新生。一键即可召唤诗人开口,降低了首次体验的门槛。


七、把"安全感"写进代码:边界与降级

工程上有一个朴素的执念:用户永远不该看到白屏或死界面。 围绕它,我在项目里放进了几道保险杠。

  • 网络层:请求失败或密钥失效时捕获 err.message,由诗人口吻包装成一条体面的提示:"风沙吞没了我的歌谣,请检查网络与密钥配置后再次祈愿。"而不是一个干巴巴的报错弹窗。
  • 解析层:上文已述的 tryParseJSON 三级容错,最坏情况降级为纯文本对话。
  • 交互层:请求进行中,发送按钮与输入框整体禁用,状态灯从"静候"切到呼吸闪烁的"吟唱中",杜绝并发触发导致的 state 混乱。
  • 输入层:限定 500 字上限,Enter 发送、Shift+Enter 换行,输入框随内容自动增高并封顶 140px。
  • 历史层:对话历史只保留最近 12 轮,既维持多轮语义的连贯性,又避免上下文爆炸悄悄烧钱。

这些细节单看起来都不起眼,但它们合起来定义了这个产品的"手感"。在码道上打磨一个作品,雕琢的不是某个功能的炫技,而是一整套"任何时刻都不会难看地失败"的守护逻辑。


八、一起"走"一遍:真实的效果

代码写就、逻辑跑通之后,我在本地起了个静态服务器,把页面交给真实的对话链路走了一遍。以下是我印象最深的一次对话。

我输入:“为我这面对末日仍不退缩的勇士,吟一首战前之歌。”

诗人布洛基的 reaction 回应是:“火焰已在东方烧红了云层,而你的目光比剑刃更冷。勇士,我见过无数英雄走向英灵殿,却很少见到有人这般平静地迎向终焉。请听。”

随后右侧卷轴缓缓展开,title 是《迎向终焉的行军》,scene 写道:"苏尔特尔高举烈焰之剑,火舌舔舐着铅灰色的天空;英灵殿的长桥在巨人脚步下颤抖,瓦尔基里的号角声穿过崩塌的云层。“四位登场人物铭牌依次点亮:苏尔特尔、奥丁、瓦尔基里、芬里尔,基调是"凛然与无惧”。六行诗句逐行浮现——既有"战鼓与雷鸣俱鸣,铠甲映着破晓的光"的壮阔,也有"若此去便是终点,便让终点成为传说"的飒然。最后的意境总述把整首诗收束在一个明亮的意象上:“故去的荣光与新生的起点在刀刃相遇,死亡不再是终点,而是神话的回响。”

说实话,当那行"若此去便是终点,便让终点成为传说"缓缓浮出时,我在屏幕前怔了很久。技术栈里没有任何一行代码拥有诗意,但当提示词、流式链路与视觉节奏被认真编排在一起,它确实在一个深夜的屏幕前,郑重其事地写出了让我动容的诗句。这正是"码道"最迷人的地方:我们写下的每一行代码,都是在为某种超出代码本身的东西铺路。


九、安全与边界:密钥为什么不能进前端

我在 README 里用加粗的警告框反复强调了一件事:纯前端项目里,API 密钥一定会暴露在浏览器开发者工具中,任何人打开页面按下 F12 就能看到。本项目内置的是演示密钥,仅供体验与学习;它在任何时候都不应该承担真实的成本。

如果你的应用要走向生产,至少有三条路可选:

  1. 后端代理:加一个薄薄的服务端(Node / Python / Go 都行),密钥只存活在服务端,前端请求自己的同源接口,由后端去调用大模型 API。这是最常见也最稳妥的方案。
  2. 网关鉴权:把 AI 接口挂在身份认证网关之后,用登录态换取调用资格,密钥对终端用户完全不可见。
  3. 限定模型与额度:在平台侧为密钥设置模型白名单和调用额度上限,即便泄露也能把损失锁进笼子里。

我把这三条完整地写进了项目的 README,作为"如何在保留纯前端便利性的同时守住安全底线"的参考答案。在码道上,代码交付的不只是功能,还应该包含对风险清醒的认知。


十、部署与分享:落到任何一处静态托管

整个项目只有三个文件,这让部署变成了一件近乎愉快的事情。我在 README 里给出了两条路径:

  • 本地体验:python3 -m http.server 8080 或 npx serve .,再打开 http://localhost:8080 即可。我特别提醒了用户:少数浏览器会限制 file:// 协议下的跨域脚本访问,直接用静态服务器更稳妥。
  • 线上发布:把三个文件拖进任意静态托管平台。GitHub Pages、Vercel、Netlify、AtomGit Pages 均可一键上线,无需构建步骤,无需配置路由,无需环境变量。

我还为 README 配了完整的配置表,把 API_URL、API_KEY、model、max_tokens、temperature、top_p、frequency_penalty、thinking_budget 这组参数逐项说明默认值与作用,让任何拿到项目的人都能在五分钟内完成自己的部署。好文档的标准不是"写得长",而是"看的人不需要再来问你"。


十一、复盘:这个项目教会我的五件事

项目收尾时,我给自己做了一次完整的复盘。如果把这次经历折叠成五条经验,大概是:

  1. 好的主题是作品的一半。"诸神黄昏"这个主题自带戏剧张力,它让技术demo变成了有灵魂的作品。选一个能点燃自己的题材,比多写一百行花哨动画更值钱。
  2. **与模型对话,先设计"数据契约"。**让模型输出结构化 JSON,前端渲染就会从"猜"变成"填",稳定性和可维护性同步上一个台阶。容错解析是这层契约的保险丝,必须存在。
  3. **真实的网络不可预测,流的边界最骗人。**SSE 解析最大的坑在于"一行数据被切成两半"。永远保留尾行、流结束再收尾,并用测试用例把边界情况钉死。
  4. **视觉氛围是一种"慢变量"。**配色、粒子、打字机、逐行浮现……这些细节不会让功能"多出什么",但它会让用户愿意多停留十秒钟,而那十秒钟就是作品与工具的分别。
  5. **安全边界要写在交付物上,而不是藏在心里。**README 里用一整节讲密钥风险与生产方案,不丢人,恰恰是一个工程作品成熟的表现。

十二、写在最后

「诸神黄昏 · 诗篇」的使命很简单:让每一位访客都可以抬起头,对一位只存在于代码与提示词之间的吟游诗人,说出自己此刻的心意——然后看他把它织进毁灭与新生的史诗之中。

项目代码与 README 已完整开源在 AtomGit 仓库:zhushen。三个文件、零依赖、一次点击即用。如果这篇文章给了你哪怕一点点灵感,我都很高兴。愿你的"码道"上,也常有值得为之燃烧的念头。

最后,借用项目里吟游诗人的一句落款,献给读到这里的你:

「灰烬渐冷,而歌谣不灭。」

——布洛基 · 灰烬之舌


本文首发于「码道」系列技术博客。项目地址:https://atomgit.com/gcw_YoHt05DT/zhushen.git

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐