怎么让 AI 引用外贸站的多语言页面:inLanguage 与语言声明规范
上个月帮一家做户外太阳能灯的卖家排查问题:客户用西班牙语问 AI 搜索引擎「lámparas solares para jardín」,答案确实引用了他们官网——但引用的是英文版页面,正文里还混着一段机翻腔很重的西语段落,读起来像换个皮的低质内容。更扎心的是,他们的西语版明明存在,翻译质量也不差,AI 就是不选它。
问题最后定位到三层语言声明打架:URL 是 /es/ 前缀,<html lang="en"> 忘了改,JSON-LD 里的 inLanguage 干脆写死了 "en"。三种信号两个打架一个沉默,AI 引擎只能猜,猜错就引用错版本。这就是典型的生成式引擎优化(Generative Engine Optimization, GEO)执行层漏洞——不是内容不行,是声明没对齐。
三层语言声明,各管一段
多语言站的语言信号有三个来源,很多团队只做了其中一到两个,剩下的就靠运气。

URL 结构是站点的信息架构层。常见的有子目录(example.com/es/product)、子域名(es.example.com)、独立域名(example.es)三种。子目录是中小外贸站的主流选择,维护成本最低,语言信号也最直观。
HTML lang 属性是文档层声明,写在 <html> 标签上,比如 <html lang="es">。它告诉浏览器、读屏软件和爬虫「这个文档的主要语言是什么」,遵循的是 BCP 47 语言标签规范,es、es-MX、zh-Hans 都合法。
Schema.org 的 inLanguage 属性是实体层声明,出现在 Article、WebPage、Product 等类型的 JSON-LD 里。它描述的不是「这个文档是什么语言」,而是「这个资源实体本身的语言」。对 AI 引擎来说,后者往往更有用——它解析结构化数据时会把页面当成一个实体来理解,inLanguage 直接挂在这个实体的属性上。
| 声明层 | 载体 | 主要受众 | 典型失误 |
|---|---|---|---|
| URL 路径 | /es/、/de/ 前缀 |
爬虫、用户、内链系统 | 模板复制后忘了换前缀 |
| HTML lang | <html lang="es"> |
浏览器、爬虫基础解析 | 全站统一模板,lang 写死 en |
| inLanguage | JSON-LD 中的字段 | AI 引擎、知识图谱构建 | 复制英文版结构化数据后漏改 |
说白了,这三层本该是同一个事实的三种表达。只要有一层和另外两层不一致,AI 引擎就进入「多信号投票」模式,票数不齐时取哪个,谁都说不准。
原理:AI 引擎怎么判定页面语言,和 hreflang 差在哪
这是全文最值得搞懂的一节,也是 GEO 和传统国际 SEO 思路分岔的地方。
Google 处理多语言靠的是 hreflang 注解:你告诉它「这个 URL 是给哪类语言地区的用户的」,Google 在搜索结果里据此切换展示版本。hreflang 是页面之间的指向关系,<link rel="alternate" hreflang="es" href="..."> 说的是「这里有一个西语版兄弟页面」。
AI 引擎抓取和引用的路径完全不同。生成式引擎(Perplexity、各家的 AI 搜索、被搜索引擎接进去的答案生成层)通常先把页面转成带元数据的文本块,再在生成答案时按用户语言检索、挑选引用源。判定一个文本块该归到哪个语言池子,它看的是页面自身的语言证据,而不是页面间关系:
flowchart TD
A["抓取页面"] --> B{"结构化数据里有 inLanguage 吗"}
B -- "有" --> C["记为高权重语言信号"]
B -- "无" --> D["回退读 html lang 属性"]
C --> E{"与 URL 语言前缀一致吗"}
D --> E
E -- "一致" --> F["页面进入对应语言索引池"]
E -- "冲突" --> G["按检测到的正文语言重新投票"]
G --> H["投票结果不稳定时低信任处理"]
F --> I["用户用该语言提问时可选为引用源"]
H --> J["引用优先级下降或被跳过"]
关键差异有三点,逐条说清楚。
第一,hreflang 对 AI 引用几乎不起直接作用。 hreflang 解决的是「同一个查询给哪个地区版本排前面」,而 AI 引擎问的是「这段内容本身是什么语言、该进哪个语料池」。前者是排名信号,后者是内容理解信号。很多团队把大量精力花在 hreflang 互指上,inLanguage 和 lang 却是错的,方向就偏了。
第二,正文语言检测会参与投票,但权重不一定最高。 AI 引擎普遍会做语言识别(比如基于字符 n-gram 或快速文本分类器),机翻腔的内容有时会被识别出混合特征——段落主体是西语,但夹杂大量英文术语原文和生硬直译,置信度就会被拉低。这也是为什么「引用出去的内容像机翻」和「选错语言版本」经常一起出现:低置信度内容在候选池里本来就排在后面。
第三,冲突信号倾向于触发保守策略。 当 inLanguage 说西语、lang 说英语、正文检测说西班牙语但置信度一般,引擎不会随机选一个,更可能降低这页在两个语言池里的信任分。表现到结果上就是:要么引用了错误版本,要么干脆引用竞品。
结论很直白:多语言站做 GEO,第一优先级不是堆 hreflang,是把
inLanguage、lang、URL 三层声明改成同一个口径。
推荐的标准写法
以一个西语产品页 https://example.com/es/lampara-solar/ 为例,三层应该长这样。
URL 层用语言子目录,前缀与内容语言对应。这一条大多数站点已经做到,问题不大。
HTML 层的 lang 要写页面的实际主语言,别偷懒共用模板:
<!DOCTYPE html>
<html lang="es">
<head>
<!-- 语言互指可以保留,给传统搜索用,但别指望它管 AI 引用 -->
<link rel="alternate" hreflang="en" href="https://example.com/en/solar-lamp/" />
<link rel="alternate" hreflang="es" href="https://example.com/es/lampara-solar/" />
</head>
Schema.org 层,inLanguage 放在与内容实体匹配的类型里。产品页挂在 Product 上,文章页挂在 Article 上,页面整体信息挂 WebPage:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Lámpara solar de jardín 60W",
"description": "Lámpara solar para exteriores con panel monocristalino.",
"inLanguage": "es",
"url": "https://example.com/es/lampara-solar/",
"sku": "SL-60W-ES"
}
三个口径上的细节容易翻车,单独拎出来。
语言标签格式统一。 BCP 47 里 es-MX 和 es 都是合法的,但同一语言的不同版本页面,声明格式要在全站保持一致。要么全用 es,要么明确区分 es-MX 和 es-ES,别一半一半。inLanguage 接受字符串也接受数组(表示多语言内容),如果页面主体是单一语言,就老老实实写单个字符串,写数组反而引入歧义。
翻译页不要共享同一份结构化数据模板。 上面那家灯具卖家的事故根源,就是西语版的 JSON-LD 从英文版模板渲染时漏传语言变量。模板化没问题,但语言字段必须是模板变量,不能是常量。
机器翻译页建议加 translate 元信息声明真实情况。 如果某语言版本是机器翻译且未人工审校,与其让 AI 引擎自己识别出「机翻腔」降权,不如先做一轮人工审校再放开收录。半成品页面被引用出去,对品牌是负资产。
自检脚本:批量校验三方一致性
人工抽查撑不起几百个页面的多语言站。下面这套 Python 脚本我们在好几个客户站上跑过,两段代码,第一段抽取信号,第二段做一致性判定。
第一段负责从页面里抽出三层语言信号。依赖只需要 beautifulsoup4 和 lxml,pip install beautifulsoup4 lxml 装好即可,环境要求 Python 3.8 以上:
# -*- coding: utf-8 -*-
# 依赖:pip install beautifulsoup4 lxml
# 环境:Python 3.8+,用于批量抽取页面的三层语言信号
import json
import re
from urllib.parse import urlparse
from bs4 import BeautifulSoup
def extract_signals(html_text, url):
# 输入:页面 HTML 源码与完整 URL
# 输出:包含三层语言信号的字典
soup = BeautifulSoup(html_text, "lxml")
signals = {"url": url}
# 第一层:URL 路径语言段,匹配 /es/ 这类两字母主语言前缀
path = urlparse(url).path
m = re.match(r"^/([a-z]{2}(?:-[A-Za-z]{2})?)(/|$)", path)
signals["url_lang"] = m.group(1).lower() if m else ""
# 第二层:html 标签上的 lang 属性,浏览器与爬虫最先读到的声明
html_tag = soup.find("html")
signals["html_lang"] = (html_tag.get("lang") or "").strip() if html_tag else ""
# 第三层:遍历全部 JSON-LD 块,收集 inLanguage 字段值
signals["schema_langs"] = []
for script in soup.find_all("script", type="application/ld+json"):
try:
data = json.loads(script.string or "")
except Exception:
# 结构化数据写坏的情况很常见,跳过但别让整站任务中断
continue
# JSON-LD 可能是单对象也可能是 @graph 数组,统一摊平处理
nodes = data.get("@graph", [data]) if isinstance(data, dict) else data
for node in nodes:
lang = node.get("inLanguage") if isinstance(node, dict) else None
if not lang:
continue
# inLanguage 可能是字符串,也可能是表示多语言的数组
if isinstance(lang, list):
signals["schema_langs"].extend(lang)
else:
signals["schema_langs"].append(lang)
return signals
第二段做归一化和一致性判定,输出问题清单,可以直接接到 CI 里:
# -*- coding: utf-8 -*-
# 依赖:无需额外安装,配合 extract_signals 使用
# 环境:Python 3.8+,输出三方不一致的页面清单
def norm(code):
# 归一化:转小写并取主语言子标签,en-US 与 EN 都折算成 en
# 这样宽松比对能覆盖绝大多数大小写和地区后缀引发的误报
if not code:
return ""
return str(code).strip().lower().split("-")[0]
def check_page(signals):
# 输入:extract_signals 的返回值
# 输出:问题列表,空列表代表三层声明口径一致
problems = []
url_lang = norm(signals["url_lang"])
html_lang = norm(signals["html_lang"])
# 多个 JSON-LD 节点各带一个 inLanguage 时全部归一化
schema_langs = [norm(x) for x in signals["schema_langs"] if norm(x)]
# URL 有语言前缀时,它是站点结构层的基准信号
# 注意:URL 前缀来自路由配置,权威性高于模板里的静态写法
if url_lang and html_lang and html_lang != url_lang:
problems.append(
f"{signals['url']} lang({html_lang}) 与 URL 前缀({url_lang}) 不一致")
if url_lang and schema_langs:
# 结构化数据里任何一个节点的语言对不上都算冲突
bad = [x for x in schema_langs if x != url_lang]
if bad:
problems.append(
f"{signals['url']} inLanguage({','.join(bad)}) 与 URL 前缀({url_lang}) 不一致")
if not url_lang and (html_lang or schema_langs):
# 没有 URL 前缀的页面也允许有语言,但要点名让人工确认是否为默认语言版
# 默认语言版通常不带前缀,这一类不算错误,只做提示
problems.append(f"{signals['url']} 无 URL 语言前缀,人工确认是否默认语言版")
return problems
# 使用方式:抓取 sitemap 后逐页调用 extract_signals 与 check_page
# 输出格式是纯文本清单,可以直接贴进工单系统
# 建议先在小语种全量页面上跑一遍,把结果贴进改造工单
把两段接上 sitemap 抓取,跑一次全站,出来的清单就是我们改模板的依据。那家灯具站跑完,光西语目录下就有 214 个页面命中 lang 与 URL 不一致——全是同一个模板渲染问题,一处修复全站生效。
改造前后对比
同样一批页面,改声明前后在 AI 引用侧的表现差异,用我们跟踪的两个站点(化名 A 站、B 站)三十天的观察记录来说明。样本不大,仅供参考,但方向性很一致:
| 观察项 | 改造前 | 改造后(30 天) |
|---|---|---|
| 三层声明一致率 | 41%(抽查 500 页) | 99%(模板修复 + CI 拦截) |
| 西语提问命中正确语言版 | 偶发,且常引用英文版 | 稳定引用西语版 |
| 被引用段落出现机翻腔 | 常见 | 未再观察到 |
| hreflang 配置 | 已做 | 未改动(说明它不是瓶颈) |
改造动作本身其实很小:模板里把 lang 和 inLanguage 从常量改成语言变量,加上一段 CI 校验。总工作量不到两天,卡住过一次——西语版构建缓存没刷新,修复后验证页面还是旧声明,清缓存才生效。踩过这个坑之后,我们的发布清单里加了一条「多语言构建必须清缓存验证」。
再看修复后的完整链路,用户提问到命中正确语言版的路径就顺了:
flowchart LR
A["用户用西语向 AI 搜索提问"] --> B["引擎检索西语语料池"]
B --> C["候选页面三层声明全部指向 es"]
C --> D["正文语言检测置信度高"]
D --> E["页面被选为引用源"]
E --> F["答案链接指向 /es/ 版本页面"]
误区澄清与趋势
三个高频误区,对应给三个澄清。
误区一:做了 hreflang 就够了。 hreflang 面向传统搜索的地区版本切换,AI 引用判定走的是页面自身语言证据这条线。两套机制并行不悖,但只做 hreflang 等于只修了半条路。
误区二:inLanguage 写在哪个类型里都行。 挂错位置引擎可能直接忽略。内容实体是什么类型,inLanguage 就挂在什么类型上;WebPage 级别的声明适合作为兜底,不能替代 Product、Article 上的声明。
误区三:多语言数组显得内容更丰富。 单一语言的页面写 "inLanguage": ["en", "es"] 只会制造歧义,和三层一致的原则背道而驰。数组只留给真正双语混杂、且主体内容确实双语的页面。
趋势上说一句:生成式引擎对结构化元数据的依赖还在加深,语言声明只是其中一项。接下来内容 freshness、实体关联这些字段会陆续进入引用判定链路,站点侧越早把结构化数据治理做成常规工程实践,后续适配的成本越低。你的多语言站在 AI 引用上踩过什么坑,欢迎评论区聊聊具体案例。
参考与延伸
- Schema.org inLanguage 属性官方定义:https://schema.org/inLanguage
- MDN HTML 全局属性 lang 说明:https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/lang
- Google 搜索中心:面向国际受众的多区域多语言文档:https://developers.google.com/search/docs/specialty/international/targeting-an-international-audience
- W3C 语言标签(BCP 47)选择指南:https://www.w3.org/International/articles/language-tags/
关键词:AI 引用、GEO、inLanguage、hreflang、多语言 SEO、语言声明、Schema.org、AI 搜索