8 月 29 号,等一次构建跑完的空当,我刷到了那条。标题里有个 Lake America,看着别扭,手指就顿了一下——第一反应是哪家标题党又整活。点进去是真的:GNIS 已经把美国境内 Lake Ontario 的官方名称正式改成 Lake America,Google 地图跟着官方源更新了,美区用户看到的就是新名字。
看完我第一个念头:这有什么好吵的。骂 Google 属于骂错人。
地图不生产地名。地图是层渲染,底下接各国自己那套官方地名库,美国这边就是 GNIS。GNIS 改了而 Google 不改,那才叫 bug。这个链路做过多地区产品的人一眼就懂,不展开。
但往下想了两天,我改口了一半。值得琢磨的根本不是"美区为什么显示 Lake America"——那部分毫无悬念。有意思的是它怎么处理分歧。
两个名字一起摆出来,不是偷懒
机制先摆出来。这类东西落到代码里,形状大概跑不出这样:
def resolve_label(feature_id, viewer_region):
sources = authoritative_sources_for(viewer_region)
labels = {s.display_name(feature_id) for s in sources if s.has(feature_id)}
if len(labels) == 1:
return labels.pop()
return " / ".join(sorted(labels)) # 有分歧就摊开,不替用户选
画外音:这段是我按它的行为反推的形状,不代表 Google 内部真长这样,我手上没有它的代码。
关键在最后那行 return。分歧出现,只有两条路——替用户裁一个,或者把两个都摆出来。美加以外的用户看到的正是后者。我一开始觉得这选择很丑,两个名字并排挂同一片水域上,像极了某次需求没对齐、最后硬合并的产物。后来我改主意了。
你以为"两个都显示"是没想清楚,其实它是唯一能站得住的默认值。另一种做法的代价是隐性的:你替用户选了一个名字,就等于替他对这片湖的归属表了态。地图这种产品最不该碰的就是这个。把分歧显式暴露出来让人自己判断——这不叫偷懒,这是这类产品唯一不用事后道歉的默认值。
再往实现层想一层,这策略是有成本的。你不能拿 feature_id 当全局缓存 key,同一个 id 在不同地区要产出不同内容,缓存维度必须带上地区;不然美区用户和加区用户会互相把对方的渲染结果打穿。这类 bug 上线时特别隐蔽,只有跨区用户偶尔撞上,报都报不清楚。他们缓存怎么设计的我不知道,但我知道这东西不便宜。
还有个点我到现在也没想明白:这个"地区"按账号算还是按 IP 算?按 IP 的话,有人从多伦多开车过境到水牛城,同一片湖在他手机上的名字,是在过桥的哪一秒变的?我猜是 IP,但不打算把这个当结论写下来,因为我不知道。
真正该抄的是政策先于事件
那篇里最该被划线的是另一句:Google 说这类处理遵循它针对跨国名称不一致水域的长期政策。
这句比改名本身重要得多。说明这不是临时写的一段特例代码,是规则在争议发生之前就已经在了。事件来了,它只是一次正常的数据更新。
反过来做的我见太多了——平平安安的时候没人愿意花时间定义规则,因为"现在又不会出问题";真出问题了,一群人半夜在群里吵这算谁的,然后热修一版,代码里加个 if,注释写"临时方案,后续优化"。三年前的临时方案现在还在跑。
争议性数据的处理规则,本来就该在没有争议的时候定下来。这话说出来像常识,真做的人不多,因为定规则的那个当下你看不到任何收益,只看得到成本。
名字是 label,不是 ID
扯远了,说回我自己。
这事让我停这么久,是因为我上个月刚踩了个同款坑。我自己捣鼓的一个小工具会从某个公开接口拉一批地理要素,图省事,我把上游返回的字段名直接写进了业务逻辑:
// 当时的写法
const label = item.name_en;
然后上游某天把 name_en 换成了 name_intl。全站地名那一栏变成 undefined。
最恶心的不是它坏,是它坏得悄无声息——不抛异常,不打日志,页面上就是一片空白。我隔了两天才自己发现的。后来改成两层,主键用上游给的 feature id,名字单独存成一个 map,而且允许它是空的:
const place = {
id: item.feature_id, // 这个才是主键
labels: { en: item.name_en }, // 这个随时会改名,别信它
};
名字是 label,不是 ID。这话听着像废话,真动手写的时候,直接拿名字当 key 存进库的人多得是,包括曾经的我。
GNIS 这次改名,谁的系统里拿 "Lake Ontario" 这个字符串当过主键——某个报表的枚举、某个配置文件的 key——现在就得做一次全表迁移。不是数据量大,是得把所有引用点翻出来,还得担心有没有哪个角落硬编码过。特别烦,而且一点都不光彩。
所以碰到外部数据源,我现在固定问三句:这份数据谁在维护、他什么时候会改、他改了我这边炸多大。第三句最有价值,因为答案往往不是"报错",是"静默"。
绕回 AI 那层
前面说的都是数据源的事,真正让我想写这篇的是 AI 那层。
我现在让 agent 帮我查东西、拉数据,最怕的不是它答错。是它答得完全正确,但没告诉你这个答案有适用范围。
你问它一个地名,它给你一个名字。它不会附一行"这个名字只在美国的官方源里成立"。它不是不知道,是它的输出格式里根本没有"管辖范围"这个字段——除非你主动问。而绝大多数时候人不会主动问,因为输出的样子太像标准答案了。
这跟我们平时说的"AI 写的代码要审得更严"是同一件事的两面。AI 写错代码,你靠测试还能拦住;AI 给你一个语法上完全正确、语义上只在某个你没问过的前提里成立的答案,你连拦它的理由都找不到,因为它看起来没有任何问题。
我现在的做法很土,也不性感 😅:凡是涉及外部数据的活,不让 agent 直接给结论,让它在结论后面强行跟三行——这份数据从哪来、来源是谁维护的、来源变了我这边会怎样。它写得对不对另说,至少逼它把前提摊出来。我自己扫一眼,就能看出哪句是它替我拍的板。
本期不用缴智商税,这次的税是别人缴的,我白嫖一次。
留一条规则,一行:任何从外部源读进来的东西,主键和显示名分开存,改名不该构成一次迁移。
