origin/main(commit e601b72)做出来了,而且没有一步用到 AI。四步流水线:
`
随机抽一座城 → TF-IDF 算特色关键词 → 意象概念桥翻译 → 意象向量点积检索 → 8 首诗 + 逐首匹配理由
1335 城/172 国 每城 14 个词 38 桶/520 触发词 9378 中 + 1696 英 浏览器本地 <10ms
`
实测效果(真实截图可验):
| 城市 | 算出的关键词 | 点亮的意象 | 匹配到的诗 |
|---|---|---|---|
| 敦煌市 | 敦煌石窟·敦煌壁画·莫高窟·玉门关·阳关·长城 | 大漠戈壁·风沙塞外·边塞关防·梵刹禅林·商旅市井 | 《凉州词》《从军行》《出塞》《使至塞上》 |
| 撒马尔罕 | 帖木儿·波斯·中亚·史迹·古城·突厥·粟特·丝绸 | 遗址怀古·古都王城·清真穹顶·楼台亭阁·商旅市井 | 《苏台览古》《金陵凤凰台置酒》《铜雀台》 |
| 威尼斯 | 潟湖·文艺复兴时期·威尼斯共和国·桥梁·勒班陀战役 | 江河水泽·兵燹战乱·海洋港湾·古都王城·商旅市井 | 江河/舟楫/兴亡类 |
| 拉萨 | 雅鲁藏布江·拉萨河·喜马拉雅·藏语 | 山岳丘陵·梵刹禅林·江河水泽·农桑田园 | 山寺钟磬类 |
敦煌 → 边塞诗、撒马尔罕 → 怀古诗,这不是碰巧——是概念桥把「石窟/玉门关/丝路」翻译成「沙·漠·塞·关·戍·驼」之后必然的结果。
真正的坎不是"提关键词",是"关键词和古诗对不上字"。
城市关键词是现代地理名词——"雾都""潟湖""火山""三角洲"——古诗里一个都不出现。直接拿关键词去诗库做字面检索,结果必然是零。
三个设计把这道坎绕过去,每一个都是「用人类已经做完的标注」替代「模型的理解」:
维基条目正文里的蓝色内链(重庆条目里的"长江""嘉陵江""吊脚楼")本身就是人工已经标注好的实体。
拿它们当候选词 = 白嫖了全世界维基编者做的分词、实体识别、消歧三件事。人工分类("世界遗产""港口城市")同理。
对比:如果去切正文全文,会切出一堆"行政区划/人口普查/常住人口"的模板噪声。
score = TF × log(N / DF),N = 1335 座城。
"人口""面积""气候"几乎每座城都有 → DF 极大 → IDF 趋近 0 → 自动消失;
"莫高窟""帖木儿""潟湖"只有一两座城有 → IDF 极高 → 自动冒头。
不需要人肉维护停用词表,这是 IDF 的天然性质。
一张 38 个概念桶、520 条触发词、379 个中文意象字的人工对照表:
`
城市关键词 → 概念桶 → 诗歌意象字
长江/运河/河畔 → 🌊江河水泽 → 江 河 川 水 流 波 浪 渡 舟 帆 津 潮 湖 溪
石窟/佛寺/禅宗 → 🛕梵刹禅林 → 寺 僧 禅 钟 塔 梵 经 香 空 净 磬
沙漠/戈壁/绿洲 → 🏜️大漠戈壁 → 沙 漠 塞 荒 孤烟 驼 胡 瀚海
`
再加一层稀有度加权 log(总城数 / 点亮该概念的城数):
"冰雪极寒"只有 9 座城点亮,一旦命中信息量远大于人人都有的"江河水泽"——这是 IDF 思想在概念层的复用。
城市和诗歌各自预计算一个 38 维意象向量,检索就是点积排序:
`
得分 = Σ(城市概念权重 × 诗歌意象强度) × (1 + 0.35 × (共享概念数 − 1))
`
那个共享概念加成是关键:共享 3 个概念的诗,说明整首诗的气质贴合,而不是单个字碰巧撞上。再加同一作者最多取 2 首,避免整页都是杜甫。
| 得 | 失 | |
|---|---|---|
| 可解释性 | ✅ 每一条匹配都能说清"哪个关键词 → 点亮哪个概念 → 命中哪几个字",页面上直接标出来 | — |
| 成本 | ✅ 零推理成本,一次构建永久使用 | — |
| 稳定性 | ✅ 同一座城永远给同一批结果,不会今天一个样明天一个样 | — |
| 离线 | ✅ 运行时零第三方接口、零 API key、零大模型调用 | — |
| 覆盖面 | — | ❌ 概念桥没覆盖的题材会漏(比如"极光""咖啡文化""爵士乐"目前没有桶) |
| 细腻度 | — | ❌ 只能到"意象类型"这一层,做不到"这首诗的语气和这座城的气质像"这种细腻判断 |
这张概念表就是要长期加厚的地方——它是这个引擎的资产,也是它的天花板。
写了一个专门的验收器 build/verify.mjs,判据任一不过 → 退出码 2 禁止上线:
| # | 判据 | 结果 |
|---|---|---|
| 1 | 城市库 / 诗库装载成功,覆盖 >80 国 | ✅ 1335 城 / 172 国 / 9378 首中文诗 |
| 2 | 随机 12 座城,每座必须真匹配出 ≥5 首诗 | ✅ 12/12 全部出满 8 首 |
| 3 | 每首诗必须带"为什么是这首"的理由 | ✅ 逐首都有 |
| 4 | 10 座地貌鲜明城市的人工常识断言必须过 | ✅ 拉萨→山岳/梵刹、敦煌→大漠/梵刹、撒马尔罕→遗址/商旅/清真、库斯科→山岳/遗址、瓦拉纳西→江河/梵刹、威尼斯→海洋/江河、爱丁堡→古都/教堂/边塞、开罗→清真/遗址、京都→古都、雷克雅未克→海洋,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.5MB | 1335 城 / 172 国,每座 14 个 TF-IDF 关键词 + 意象概念权重 | 中文维基百科(CC BY-SA) |
concepts.json 20KB | 38 概念桶 / 520 触发词 / 379 中文意象字 + 286 英文意象词 | 人工编写 |
poems_zh.min.json 2.4MB | 9378 首(唐诗 / 宋诗 / 宋词 / 诗经 / 楚辞 / 花间集 / 纳兰词 / 曹操诗集) | chinese-poetry(MIT,公有领域作品) |
poems_en.min.json 1.3MB | 1696 首英文诗(懒加载,切到英文档才下载) | 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。