站内搜索框被 AI 搜索引擎当成摆设之后:SearchAction 与 potentialAction 的规范落地
适用读者:电商与零售站的前端 / SEO 工程师、负责结构化数据的后端同学、正在把站点接进 AI 搜索与生成式引擎优化(Generative Engine Optimization, GEO)链路的技术负责人。你要能改模板里的 JSON-LD,看得懂 URL 模板占位符,对浏览器地址栏的直达搜索有印象。
搜索框在 AI 眼里就是块死掉的 div
我们维护的一个电商站,站内搜索每天扛着几十万次请求,搜索结果页的转化一直不差。怪的是,用户在 AI 搜索里问「这个站有没有 XX 型号」,回答里永远只有一条首页链接,后面配一句「可在官网查询」。用户点进去还得自己找输入框,把关键词再敲一遍。

反差就摆在眼前:站内明明有检索能力,AI 却当它不存在。它看到的是一个 <input> 标签,不是一份「我支持搜索、参数这么传」的机器可读声明。
补了不到 20 行 JSON-LD 之后,两周左右,AI 搜索结果里开始出现「在某商城搜索 XX」的直达链接。这篇记的就是那次改造,包括校验器甩给我们的三类报错。
先搞清楚 potentialAction 该挂在哪一层
潜在动作(potentialAction)不是某个页面专属字段,它是 schema.org 动作模型(Actions)里的挂点,用来声明「这个实体能接受什么动作」。层级挂错,标注写了也白写。
我们第一版把它塞进 WebPage 节点,校验器一声不吭,AI 也不认——因为 WebPage 说的是「这一页」,而站内搜索是整站能力,规范要求的挂点是 WebSite(WebSite)。
| 挂点类型 | 适合声明什么动作 | 站内搜索该挂吗 |
|---|---|---|
| WebSite | 整站级能力:站内搜索、站内导航 | 该挂,这是规范指定的层级 |
| WebPage | 单页动作:阅读、评论、分享 | 不该,AI 不会当整站能力读 |
| Organization | 联系方式、logo、社交账号 | 不该,与检索意图无关 |
判断标准就一条:动作的作用域是「整个站点」还是「这一个页面」。 站内搜索属于前者,所以 WebSite + SearchAction 是规范指定的组合方式。
原理剖析:query-input 占位符是怎么被解析的
potentialAction 里真正被消费的有两个字段,缺一个都会掉链子。
target(或 EntryPoint.urlTemplate)给的是 URL 模板,里面的 {search_term_string} 是占位符,不是查询参数名。解析方把它整段替换成用户的查询词,得到一条可直接访问的结果页地址。query-input 则声明「前端输入框里填的东西叫什么」,写成 required name=search_term_string,等号右边的名字必须与花括号里的占位符逐字符相同。
flowchart LR
A[抓取页面并抽取 JSON-LD] --> B[@type 是否为 WebSite]
B -- 否 --> C[整段 potentialAction 被丢弃]
B -- 是 --> D[取出 SearchAction]
D --> E[读 urlTemplate 中的占位符]
E --> F{query-input 名字是否匹配}
F -- 不匹配 --> G[判定为不可执行动作]
F -- 匹配 --> H[用用户查询替换占位符]
H --> I[产出直达搜索链接]
关键点在于「匹配」这一步。校验器的逻辑是先扫模板里的花括号,再拿 query-input 声明的名字去对,对不上就报 Missing 'query-input',哪怕你其实写了这个字段。我们踩的第一坑就是模板里写 {q}、声明里写 search_term_string,肉眼看着都有,机器认为没有。
AI 引擎到底什么时候才把搜索动作接过去
不是标了就一定触发。从我们观察到的行为看,它更像一个工具调用的选择过程:引擎先判断用户的查询是不是「某站点内的精确查找」,是的话才去翻这个站有没有声明 SearchAction。
sequenceDiagram
participant U as 用户
participant AI as AI 搜索引擎
participant S as 站内搜索结果页
U->>AI: 问某站有没有某个型号
AI->>AI: 判断是否为站内检索意图
AI->>AI: 查该站 WebSite 的 potentialAction
AI->>S: 按 urlTemplate 拼接查询 URL
S-->>AI: 返回结果页
AI-->>U: 回答中附直达搜索链接
所以 GEO 语境下的收获不是排名,是被当成工具调用的一个候选。 模型手里有一堆可选动作,你的站点声明得越规范、URL 模板越稳定,它被选中的概率才谈得上提升。
落地:那份 JSON-LD 我们改了哪些地方
先说依赖与环境:站点是 SSR 渲染的电商站,模板层是 Node 18 + EJS,JSON-LD 在 layout.ejs 里输出,只放在首页(规范建议放首页,全站每页都塞没意义)。下面是改造前的版本。
{
// 上下文固定写 https://schema.org,不要带 https:// 以外的变体
"@context": "https://schema.org",
// 问题一:挂点写成了 WebPage,站内搜索是整站能力,应为 WebSite
"@type": "WebPage",
// 站点名,这里没问题
"name": "某商城",
// 问题二:url 缺结尾斜杠,与 canonical 不一致,校验会提示不匹配
"url": "https://shop.example.com",
// 问题三:动作挂在错误层级上,AI 引擎直接跳过不读
"potentialAction": {
// 动作类型本身写对了
"@type": "SearchAction",
// 模板里的占位符写成了 q
"target": "https://shop.example.com/search?q={q}",
// 声明的名字是 search_term_string,与占位符对不上
"query-input": "required name=search_term_string"
}
}
改完之后的版本长这样,EntryPoint 这种写法是 Google 文档现在推荐的,target 直接给字符串的简写形式也合法,但语义上少一层,我们选了前者。
<script type="application/ld+json">
{
// 上下文
"@context": "https://schema.org",
// 挂点修正为 WebSite,代表整站
"@type": "WebSite",
// 站点根地址,与首页 canonical 保持逐字符一致,带结尾斜杠
"url": "https://shop.example.com/",
// 站点名,用于 AI 结果里的来源展示
"name": "某商城",
// 潜在动作:声明整站支持的搜索动作
"potentialAction": {
// 动作类型大小写敏感,必须写 SearchAction
"@type": "SearchAction",
// EntryPoint 形式声明入口点,语义比裸字符串更完整
"target": {
// 入口点类型
"@type": "EntryPoint",
// URL 模板:占位符统一用 search_term_string,与后端 keyword 参数对应
"urlTemplate": "https://shop.example.com/search?keyword={search_term_string}"
},
// 声明输入框必填项,名字必须与上面的占位符完全一致
"query-input": "required name=search_term_string"
}
}
</script>
这里有个容易翻车的细节:urlTemplate 里的查询参数名(keyword)是给我们自己后端看的,占位符名字(search_term_string)是给解析方看的,两者不是一回事,别混着改。我们站后端只认 keyword,所以模板写成 ?keyword={search_term_string},完全合法。
为了防止后面有人手抖改坏,构建期加了一段自检,跑在 CI 的 lint 阶段。
// 从模板里抽出所有花括号占位符
const template = 'https://shop.example.com/search?keyword={search_term_string}';
// matchAll 返回迭代器,展开后取捕获组
const holders = [...template.matchAll(/\{(\w+)\}/g)].map((m) => m[1]);
// 与 query-input 声明的名字做比对
const declared = 'search_term_string';
// 占位符必须恰好一个,且名字完全相等
const matched = holders.length === 1 && holders[0] === declared;
// 不合法就打断构建,别让脏数据上线再靠人眼发现
if (!matched) {
// 抛出错误时把两边的值都打出来,排查时省一轮
throw new Error(`SearchAction 占位符不一致: ${holders.join()} vs ${declared}`);
}
// 通过也留一条日志,方便回溯哪次构建检查过
console.log('SearchAction 自检通过');
三类校验报错,一个都没躲过
改造当天跑结构化数据校验器,收了三类错。原文我摘在下面,域名替换成了 example.com。
// 报错一:占位符与 query-input 声明名字不一致
// 原文:Missing 'query-input' for SearchAction (target uses {q})
// 原因:模板用 {q},声明写 required name=search_term_string
// 修法:统一成 search_term_string,全站模板一起改
// 报错二:url 与页面 canonical 不匹配
// 原文:The value provided for 'url' does not match the canonical URL
// 原因:JSON-LD 里是 https://shop.example.com,canonical 带结尾斜杠
// 修法:直接读模板里的 canonical 变量,别手写字面量
// 报错三:动作挂在不支持的类型上
// 原文:potentialAction is not a valid property of WebPage in this context
// 原因:@type 写成 WebPage,应为 WebSite
// 修法:新建独立的 WebSite 节点承载 potentialAction
| 报错 | 触发条件 | 修法 | 排查耗时 |
|---|---|---|---|
| Missing 'query-input' | 占位符名与声明名不一致 | 全局统一为 search_term_string | 约 10 分钟 |
| url 与 canonical 不匹配 | 字面量拼错、缺结尾斜杠 | 复用 canonical 变量 | 约 20 分钟 |
| 类型不支持该属性 | 挂点写 WebPage / Organization | 独立 WebSite 节点 | 约 5 分钟 |
第二类最折腾人,因为校验器只告诉你不匹配,不告诉差在哪一个字符。我们的办法是把 canonical 变量直接注入 JSON-LD,从根上杜绝手写。
URL 参数的几条土规矩
| 约定 | 说明 |
|---|---|
占位符统一 search_term_string |
生态通用名,{query} 语义上更泛但兼容性差 |
| 模板要写完整 URL | 相对路径 /search?... 会被判定为不可执行 |
| 结果页服务端可直出 | AI 抓的是 URL,前端渲染的空壳页等于没结果 |
| 参数名别轻易改 | 改了等于换工具签名,已收录的模板会失效 |
第四条是我们事后补的。上线后第三天,后端同学顺手把 keyword 改成了 kw,那批刚被 AI 收录的直达链接当天就开始返回空结果页,回滚才恢复。
上线两周后观察到的几件事
不编数字,只说我们实际看到的行为变化。
第一件:AI 搜索回答里出现了「在某商城搜索 XX」这类直达链接,位置在正文引用之后,点击后落到站内搜索结果页。改造前同样的提问方式只给首页链接。
第二件:浏览器地址栏敲域名后,部分浏览器开始提示「按 Tab 键在此站内搜索」,这个能力依赖的正是同一份标注。前端同事原话是「原来这玩意儿是读 JSON-LD 的,我还以为是浏览器自己猜的」。
第三件:有个非预期的连带效果。站内搜索结果页被 AI 引用时,引用块里带的词更接近用户原始查询词,而不是我们 SEO 同学预设的类目词。我们的解释是:直达链接的 URL 里带着原始查询词,模型更容易对齐表述。这只是观察,没有做对照实验,别当成结论用。
误区澄清:它不是排名开关
最常见的误会是「加了 SearchAction 排名会涨」。不会。它做的是让 AI 知道你有个可用的检索入口,属于 GEO 里「让机器能调用你」这一层,和相关性排序是两件事。
另一个误会来自 Google 那条历史公告:Google 在 2022 年底下线了 Sitelinks Searchbox 的搜索结果展示,于是有人推断这套标注废了。展示形态确实没了,但标注本身还在被 AI 引擎、浏览器和第三方解析器消费,官方文档至今保留。我们这次观察到的直达链接也不是来自 Sitelinks Searchbox,是 AI 回答里的引用链接,两码事。
趋势:动作声明正在变成工具签名
从 AI 搜索这一两年的走向看,我倾向于认为 potentialAction 这类声明的价值会从「给爬虫看的结构化数据」往「给模型看的工具签名」迁移。模型侧越来越像在做 function calling:它需要知道工具叫什么、参数怎么传、返回什么形态,SearchAction 恰好就是一个刚好够用的最小签名。
这么推的话,后面值得盯的是动作类型会不会往外扩——比如 OrderAction、ReserveAction,让 AI 不只是「跳到你的搜索页」,而是能直接把动作串进回答链路。眼下能做的准备很朴素:把 URL 模板写稳、参数名别乱动、结果页服务端直出,这三件事现在做和两年后做,成本差很多。
收尾
这次改造真正花时间的不是写 JSON-LD,是统一占位符命名和让 canonical 与标注同源。校验器那三类报错看着唬人,实际都是命名不一致引发的连锁反应。
如果你也在做电商站的 AI 搜索适配,建议先把首页的 JSON-LD 抓下来过一遍校验器,看看 potentialAction 挂的到底是 WebSite 还是别的。你们那边有没有遇到别家的报错类型、或者观察到不一样的行为变化,评论区聊一下,我对「AI 到底在什么条件下才选这个动作」这一点还没摸透。
参考与延伸
- schema.org SearchAction 类型定义:https://schema.org/SearchAction
- schema.org 动作模型总览(Actions Overview):https://schema.org/docs/actions.html
- Google 站内搜索框结构化数据文档:https://developers.google.com/search/docs/appearance/structured-data/sitelinks-searchbox
- schema.org WebSite 类型定义:https://schema.org/WebSite
GEO、AI搜索、SearchAction、potentialAction、JSON-LD、Schema.org、站内搜索直达、AI优化AIO