← 回成果库

诗歌地球 PoemGlobe · 城市 → 特色关键词 → 诗歌匹配引擎(纯算法,零 AI)


一、结论先行

做出来了,而且没有一步用到 AI。四步流水线:

`

随机抽一座城 → TF-IDF 算特色关键词 → 意象概念桥翻译 → 意象向量点积检索 → 8 首诗 + 逐首匹配理由

1335 城/172 国 每城 14 个词 38 桶/520 触发词 9378 中 + 1696 英 浏览器本地 <10ms

`

实测效果(真实截图可验):

城市算出的关键词点亮的意象匹配到的诗
敦煌市敦煌石窟·敦煌壁画·莫高窟·玉门关·阳关·长城大漠戈壁·风沙塞外·边塞关防·梵刹禅林·商旅市井《凉州词》《从军行》《出塞》《使至塞上》
撒马尔罕帖木儿·波斯·中亚·史迹·古城·突厥·粟特·丝绸遗址怀古·古都王城·清真穹顶·楼台亭阁·商旅市井《苏台览古》《金陵凤凰台置酒》《铜雀台》
威尼斯潟湖·文艺复兴时期·威尼斯共和国·桥梁·勒班陀战役江河水泽·兵燹战乱·海洋港湾·古都王城·商旅市井江河/舟楫/兴亡类
拉萨雅鲁藏布江·拉萨河·喜马拉雅·藏语山岳丘陵·梵刹禅林·江河水泽·农桑田园山寺钟磬类

敦煌 → 边塞诗撒马尔罕 → 怀古诗,这不是碰巧——是概念桥把「石窟/玉门关/丝路」翻译成「沙·漠·塞·关·戍·驼」之后必然的结果。


二、命门:不用 AI,难在哪,怎么绕过去

真正的坎不是"提关键词",是"关键词和古诗对不上字"。

城市关键词是现代地理名词——"雾都""潟湖""火山""三角洲"——古诗里一个都不出现。直接拿关键词去诗库做字面检索,结果必然是零。

三个设计把这道坎绕过去,每一个都是「用人类已经做完的标注」替代「模型的理解」:

① 关键词候选取自「维基内链 + 人工分类」,不是靠分词硬切

维基条目正文里的蓝色内链(重庆条目里的"长江""嘉陵江""吊脚楼")本身就是人工已经标注好的实体

拿它们当候选词 = 白嫖了全世界维基编者做的分词、实体识别、消歧三件事。人工分类("世界遗产""港口城市")同理。

对比:如果去切正文全文,会切出一堆"行政区划/人口普查/常住人口"的模板噪声。

② TF-IDF 自动压掉废话,不用维护停用词表

score = TF × log(N / DF),N = 1335 座城。

"人口""面积""气候"几乎每座城都有 → DF 极大 → IDF 趋近 0 → 自动消失

"莫高窟""帖木儿""潟湖"只有一两座城有 → IDF 极高 → 自动冒头

不需要人肉维护停用词表,这是 IDF 的天然性质。

③ 意象概念桥 —— 全套流程里唯一的「人工知识」,也正是它替代了 AI

一张 38 个概念桶、520 条触发词、379 个中文意象字的人工对照表:

`

城市关键词 → 概念桶 → 诗歌意象字

长江/运河/河畔 → 🌊江河水泽 → 江 河 川 水 流 波 浪 渡 舟 帆 津 潮 湖 溪

石窟/佛寺/禅宗 → 🛕梵刹禅林 → 寺 僧 禅 钟 塔 梵 经 香 空 净 磬

沙漠/戈壁/绿洲 → 🏜️大漠戈壁 → 沙 漠 塞 荒 孤烟 驼 胡 瀚海

`

再加一层稀有度加权 log(总城数 / 点亮该概念的城数)

"冰雪极寒"只有 9 座城点亮,一旦命中信息量远大于人人都有的"江河水泽"——这是 IDF 思想在概念层的复用。

④ 检索 = 两个 38 维向量点积

城市和诗歌各自预计算一个 38 维意象向量,检索就是点积排序:

`

得分 = Σ(城市概念权重 × 诗歌意象强度) × (1 + 0.35 × (共享概念数 − 1))

`

那个共享概念加成是关键:共享 3 个概念的诗,说明整首诗的气质贴合,而不是单个字碰巧撞上。再加同一作者最多取 2 首,避免整页都是杜甫。


三、不用 AI 换来了什么、赔上了什么(诚实账)

可解释性✅ 每一条匹配都能说清"哪个关键词 → 点亮哪个概念 → 命中哪几个字",页面上直接标出来
成本✅ 零推理成本,一次构建永久使用
稳定性✅ 同一座城永远给同一批结果,不会今天一个样明天一个样
离线✅ 运行时零第三方接口、零 API key、零大模型调用
覆盖面❌ 概念桥没覆盖的题材会漏(比如"极光""咖啡文化""爵士乐"目前没有桶)
细腻度❌ 只能到"意象类型"这一层,做不到"这首诗的语气和这座城的气质像"这种细腻判断

这张概念表就是要长期加厚的地方——它是这个引擎的资产,也是它的天花板。


四、验收(不是"没崩就算过")

写了一个专门的验收器 build/verify.mjs,判据任一不过 → 退出码 2 禁止上线:

#判据结果
1城市库 / 诗库装载成功,覆盖 >80 国✅ 1335 城 / 172 国 / 9378 首中文诗
2随机 12 座城,每座必须真匹配出 ≥5 首诗✅ 12/12 全部出满 8 首
3每首诗必须带"为什么是这首"的理由✅ 逐首都有
410 座地貌鲜明城市的人工常识断言必须过✅ 拉萨→山岳/梵刹、敦煌→大漠/梵刹、撒马尔罕→遗址/商旅/清真、库斯科→山岳/遗址、瓦拉纳西→江河/梵刹、威尼斯→海洋/江河、爱丁堡→古都/教堂/边塞、开罗→清真/遗址、京都→古都、雷克雅未克→海洋,10/10 全中
5单次检索 < 200ms✅ 最慢 5ms
6控制台零报错
7手机视口零横向溢出✅ 溢出 0px
8线上字节 = 仓库字节(5 个文件逐字节 cmp✅ 全部一致
9线上抓回的字节再跑一遍完整验收✅ 全部通过

诚实边界:容器里的 Playwright/Chromium 无法穿本地出网代理(试了 proxy-only / proxy+bypass / --proxy-server 参数 / 忽略证书 四种配置,全部 ERR_CONNECTION_RESET),所以没能用真浏览器直接访问线上域名。替代做法是:把线上五个文件用 curl 抓回本地,先逐字节 cmp 证明"线上 = 仓库这一版",再用这些线上字节跑完整浏览器验收。这验证的是同一批字节,但不是"从公网真机打开"——差别在这里,不含糊。


五、数据(全部自托管,公版)

文件内容来源
cities.json 1.5MB1335 城 / 172 国,每座 14 个 TF-IDF 关键词 + 意象概念权重中文维基百科(CC BY-SA)
concepts.json 20KB38 概念桶 / 520 触发词 / 379 中文意象字 + 286 英文意象词人工编写
poems_zh.min.json 2.4MB9378 首(唐诗 / 宋诗 / 宋词 / 诗经 / 楚辞 / 花间集 / 纳兰词 / 曹操诗集)chinese-poetry(MIT,公有领域作品)
poems_en.min.json 1.3MB1696 首英文诗(懒加载,切到英文档才下载)PoetryDB(公有领域)

城市库复用了仓库里已有的 travel-2d/data/cities_wiki.json(1155 城),另补了 345 座——因为原库是按人口筛的,威尼斯、敦煌、撒马尔罕、库斯科、廷巴克图、瓦拉纳西这类"人口小、文化重"的城市全被漏掉了,而它们恰恰是这个产品最该出现的城市。人口不是文化权重。

诗库从 48599 首瘦身到 9378 首:按"每个概念保留分数最高的 300 首"取并集 + 按正文前 20 汉字去重(同一首诗常以不同标题/标点/作者重复收录)。这样既控体积,又保证每个概念都有足够好的候选


六、红队自审

最强反对回应
"没有 AI,匹配结果会不会其实很水,只是看着像那么回事?"这是最该问的。所以验收里加了10 条人工常识断言——不是"有结果就算过",而是"拉萨必须点亮山岳/梵刹、敦煌必须点亮大漠/梵刹",人一眼能判对错。10/10 全中。但诚实说:这只证明概念级正确,不证明"这首诗是最佳选择"——后者没有客观标尺。
"概念桥是你手写的,会不会只在你测的那几个城市上好看?"部分成立。所以随机抽的 12 座城是程序随机抽的(跑出来是巴古拜/金沙萨/费拉迪圣安娜这些冷门城),不是我挑的。但覆盖率数据也摆出来了:38 个桶里,"秋""日出晚照"点亮率 <1%——这些桶基本是摆设,是真缺口。
"1335 座城够吗?世界上有几万座城市。"不够,明确承认。且分布偏中/印/巴西(底库按人口筛)。补的 345 座只是把最有名的文化名城捞回来。
"98 座小城继承国家画像,算不算蒙混?"算折中,但页面上明写了"本城维基资料太少,参考「某国」整体画像",没有假装是本城自己的信号。不这么做,这 98 座会全部退化成同样三个母题,显示出来就是"引擎在瞎猜"。

可证伪锚:如果拿 30 座随机城市做人工盲评,「匹配到的诗与城市气质相符」的比例低于 50%,则"概念桥方案可用"这个判断不成立,应退回逐城手工策展。


七、缺口(明确说清没做到什么)

1. 没能从公网真机验证(容器 Chromium 无法出网,已试 4 种配置)—— 验的是线上抓回的同一批字节。

2. 概念桥有 5 个桶点亮率 <2%(秋 / 日出晚照 / 春 / 月夜星辰 / 隐逸山林)——城市条目本来很少写季节,属设计边界。

3. 16 座补充名城没抓到(庞贝古城、拉文纳、白川乡、庆州市等,维基条目名不一致)。

4. 英文诗只有 1696 首,远少于中文,英文档多样性弱。

5. 没做「一座城应该配几首诗才算好」的人工盲评——目前只有机器判据,没有人评分。

6. 首屏要下载 ~1.2MB(gzip 后)中文诗库,移动网络首次打开会有 1-2 秒等待。


八、怎么用

打开 https://metazero-terminal.pages.dev/poemglobe/

重新构建见 web/terminal/poemglobe/README.md