这篇是“生成式引擎可见性”笔记的第六篇。前五篇分别写了能不能被读到、能不能被摘走、旧话怎么换成新话、跨源能不能归到同一个“我”,解决的是同一层面的事——单份材料够不够格被机器读、被摘、被更新、被认出来。这篇补的是那几篇都绕过去的一层:采购方问出一句带条件的问法时,你的材料在不在这个问题的候选集里。
全文只讨论公开可验证的技术形态,不针对某一家引擎的实现细节下断言。
采购方问 AI 的话,越来越不像关键词,越来越像一段需求描述:介质有腐蚀性该选什么材质、批量小交期短找谁做、有没有做过同类工况的项目。这类问句里没有一个字在问“有哪些厂家”,它问的是“我这个情况该找谁”。
一、带条件的问法,在检索侧发生了什么
把一次问答拆成检索链路看,大致三步:
- 意图解析:把问句拆成一组约束要素——对象(什么设备)、工况(什么介质、节拍、批量)、地域(供货范围)、资格(有没有做过同类项目);
- 候选匹配:按这组要素去找公开材料对得上的实体;
- 组织回答:把匹配上的实体按条件组织成句子。
第三步有个容易被忽略的前提:候选是先筛后排的。带条件的问句里,工况、地域这类是硬约束,不满足的实体在这一步就被移出候选集,而不是排到后面。排到后面至少还在答案里露个脸;被移出候选集,意味着生成答案时它压根不在上下文里,系统没有任何材料可以提到它。
这一条是意图层与前几篇最大的区别:前几篇处理的是“这份材料够不够格”,意图层处理的是“这份材料在不在这个问题的候选集里”——前者是质量问题,后者是归属问题。 质量差还能靠别的字段补,归属错了,后面所有环节都跟它没关系。
二、三种让材料进不了候选集的技术原因
按出错位置分,常见的有三种,成因与解法各不相同。
第一种:切分把属性切碎了。 检索侧不是整页读,而是先切成块(chunk)再做向量化。工厂常见的组织方式是首页讲实力、产品页讲型号、新闻页讲项目,一个属性被拆到三个块里,块内只剩“采用先进工艺”这类主谓宾都不完整的句子。
推一步看代价:一个块在语义空间里的位置,由块内全部词共同决定。块里没有构成“某某厂—适用工况—返潮细料”这个完整的三元组,它就不会落在“返潮细料怎么卸不堵”这句话的近邻区域。不是它写得不好,是它压根没进入能被比对的粒度。
第二种:形容词把语义空间占满了。 多数工厂页面里,可判定的硬属性——材质牌号、交期区间、最小批量、覆盖区域——写得很少,形容词很多。
这一条的机制值得多说一句:稠密召回算的是整块向量的相似度,而形容词贡献的语义方向高度趋同,所有厂家的“专业、先进、可靠”在向量空间里几乎重合;真正能把两家厂区分开的,恰恰是那些硬属性的取值。于是出现一个反直觉的结果:你的页面和同行的页面在向量空间里挨得很近,和采购方那句带工况的问句却离得很远。 让页面看起来更专业的形容词,正是稀释区分度的东西。
第三种:结构化数据与正文是两套内容,或者压根没渲染出来。 一种情况是 JSON-LD 里写了“覆盖华南”,正文里一句没提;另一种情况是页面纯前端渲染,抓取侧拿到的 HTML 里既没有正文也没有结构化数据。
推一步:结构化数据的作用是给机器一份字段级声明,它的前提是与页面正文一致。两者不一致时,稳妥的工程处理是降低置信度,或者干脆不采信这份字段。更麻烦的是第二种情况——相当多站点的结构化数据是脚本注入的,抓取侧不执行脚本,等于没写。写法没问题但渲染方式错了,这类问题在本地打开页面看不出来,只能去看抓取侧拿到的原始 HTML。
三、把意图变成结构:三个可执行的动作
机制清楚了,动作是反向设计——先有真实问句,再按问句组织材料。
动作一:采集问句,抽要素。
从业务员私聊记录、售后登记本、投标答疑里翻这半年反复出现的问句,原样抄,不改写。然后用一段脚本把要素抽出来统计:
-- coding: utf-8 --
“”"意图要素抽取:从真实问句里统计四类约束的出现频次。
用法:问句逐行放进 questions.txt,运行后输出各类要素的命中次数与占比。
规则版实现,不发网络请求,不依赖第三方库,够跑出一张基线表。
“”"
import re
from collections import Counter
四类约束的触发词典。工程上可替换为分词加词性标注,规则版足够定位高频要素
SLOTS = {
“对象”: r"(输送设备|卸车机|密封件|除尘器|配料系统)“,
“工况”: r”(腐蚀|高温|返潮|细料|易燃易爆|粉尘|连续运行)“,
“地域”: r”(华南|珠三角|广东|广西|福建|附近|周边)“,
“资格”: r”(同类项目|做过|案例|资质|认证|验收)",
}
def extract(line: str) -> list:
“”“返回该行命中的要素类别。”“”
return [slot for slot, pat in SLOTS.items() if re.search(pat, line)]
def main(path: str = “questions.txt”) -> None:
counter = Counter()
total = 0
with open(path, encoding=“utf-8”) as f:
for line in f:
line = line.strip()
if not line:
continue
total += 1
counter.update(extract(line))
if not total:
print(“questions.txt 为空”)
return
print(f"问句总数: {total}“)
for slot, n in counter.most_common():
print(f”{slot}: {n} 次, 占比 {n / total:.1%}")
missing = [s for s in SLOTS if counter[s] == 0]
if missing:
print(“未命中:”, “、”.join(missing), “(检查词典是否覆盖本行业说法)”)
ifname== “main”:
main()
这段脚本的价值不在准确率,在于把“采购方到底在问什么”从印象变成一张有数字的表。跑完就知道哪一类要素出现最多,也就知道材料该先补哪一块。
动作二:按要素补材料。
每个高频要素都要有一份公开材料能直接回答它,缺哪块补哪块。做完会得到一张意图覆盖表:
问句要素
高频问法示例
公开材料有没有
在哪
工况
返潮细料怎么卸不堵
有
产品详情页
地域
珠三角能不能供
无
——
资格
有没有做过同类项目
无
——
空着的那几格,就是 AI 答不出你的原因。要素类型可以按行业扩到批量、交期、认证、售后半径,做法相同。
动作三:把属性压成机器可读的字段。
意图覆盖解决“有没有内容”,结构化解决“机器读不读得顺”。用 schema.org 这套公开词汇表组织 JSON-LD,是成本最低的一步:
{
“@context”: “https://schema.org”,
“@type”: “Organization”,
“@id”: “https://example.com/#org”,
“name”: “某某机械设备厂”,
“description”: “生产散料输送设备,覆盖华南区域”,
“areaServed”: [“广东”, “广西”, “福建”],
“knowsAbout”: [“返潮细料卸车”, “腐蚀性介质输送”, “小批量非标定制”],
“makesOffer”: {
“@type”: “Offer”,
“itemOffered”: {
“@type”: “Service”,
“serviceType”: “工况适配咨询与定制生产”,
“termsOfService”: “最小起订量 1 台,常规交期 25 至 40 天”
}
}
}
字段不是堆着好看的,每一个都对应意图层的一个匹配入口:
字段
对应哪类约束
缺了会怎样
knowsAbout
工况、对象
问工况时没有可判定的硬属性
areaServed
地域
带区域的问法里直接被移出候选
makesOffer.itemOffered.serviceType
对象、资格
只写产品不写服务,服务类问法匹配不上
termsOfService
批量、交期
“交期短”这类条件无从判断
@id
全部(主键)
与第五篇的实体对齐接不上
四、写法上的一个反常识:块内自足优先于文采
摘取是块级的,块内必须自足。中文写作习惯用代词承接上一段,“它的交期是二十五天”读起来通顺,切块之后这一块里没有主语,机器读到的是一个悬空的“它”。
所以属性与项目记录这两块,要主动牺牲文采,每一句都把主体写全。“某某厂非标输送设备的常规交期是二十五天”,比“它的交期是二十五天”多七个字,但前者能独立成一块被摘走。 这是意图层唯一要求你放弃阅读体验的地方,其余段落照常写。
五、验证:固定问句集的复测
改完怎么知道有没有用?回到第一篇的复测思路:固定一组问句,定期问,记录被提及的情况。这段脚本只做记录与统计,提问结果由人工填入——它不发请求,也不调任何接口。
-- coding: utf-8 --
“”"意图层复测:固定问句集的命中趋势统计。
probes.json 结构:
[{“date”: “2026-10-08”, “question”: “…”, “mentioned”: true,
“slots_hit”: [“工况”], “note”: “”}, …]
输出:按周统计被提及比例与要素命中情况。
“”"
import json
from collections import defaultdict
def load(path: str) -> list:
with open(path, encoding=“utf-8”) as f:
return json.load(f)
def weekly_rate(records: list) -> dict:
“”“按日期前缀(周)聚合,返回 {周: (被提及数, 总数)}。”“”
bucket = defaultdict(lambda: [0, 0])
for r in records:
# 取日期所在周的周一作为键,够用且不引入日期库依赖
week = r.get(“date”, “”)[:10]
bucket[week][1] += 1
if r.get(“mentioned”):
bucket[week][0] += 1
return dict(sorted(bucket.items()))
def slot_gap(records: list) -> dict:
“”“统计每次虽被提及、但要素未命中(材料没接住)的比例。”“”
gap = defaultdict(int)
total = 0
for r in records:
total += 1
for s in r.get(“slots_hit”, []) or []:
gap[s] += 1
if not total:
return {}
return {s: f"{n / total:.1%}" for s, n in gap.items()}
ifname== “main”:
data = load(“probes.json”)
rate = weekly_rate(data)
for week, (hit, tot) in rate.items():
print(f"{week} 被提及 {hit}/{tot} ({hit / tot:.0%})")
print(“要素命中占比:”, slot_gap(data))
三点使用提醒:问句要固定,换一批问句就等于换了把尺子,前后不可比;单次波动不算数,看的是连续几周的趋势;被提及但要素没命中这一栏最有价值——它说明材料被摘到了,但接不住那句带条件的问法,正是本篇要修的位置。
六、三件别做的事
别堆关键词。 稠密召回看的是语义位置不是词频,把“输送设备”在一段里重复十次,改变的只是这一段的信息密度,反而把真正的硬属性稀释了。搜索时代那句“多写关键词多一次机会”,放到这一层是反的。
别把 JSON-LD 当装饰。 字段写满、正文没有对应内容,是最容易被判为不一致的一种写法。结构化数据是对页面内容的一份声明,声明的东西页面上必须有,两者要对得上。
别只改官网。 跨源取值冲突会让置信度下降,这是第五篇的题目,脚本也在那篇里。这一层与那层的分工是:第五篇解决“归到同一个我”,本篇解决“接得住那句问法”,缺一层都不成立。
七、边界
三条说清楚,免得这套东西被用过头:
- 各引擎的召回策略与过滤实现不同,同一批材料在不同产品上的命中结果注定不一致。结论要基于固定问句的多次复测,不基于单次结果。
- 意图层只解决“能不能被匹配上”,它不替产品说话。材料接住了问法,用户看到的仍是真实信息;真实信息本身不占优,匹配上了也救不回来。
- 结构化是充分非必要条件。 页面写得清楚、属性块内自足,即使没有结构化数据也可能被摘到;结构化数据的作用是降低歧义、提高判定效率,不是唯一入口。
六篇串起来是一条完整的链:能复测、读得到、摘得走、旧话能换新、归回到同一个“我”,最后这一步是“接得住那句问法”。每一步做的都是同一件事——把真实发生过的事,放到机器够得着、读得懂、并且能判定“它对得上这个条件”的地方。用一句话收束:让认真做事的人被看见。 剩下的部分,取决于引擎侧的策略与节奏,不在我们的控制范围内。
FAQ
问:前五篇都做完了还是没效果,优先查哪一条?
先查意图覆盖表里空着的那几格。多数情况是地域与资格两栏没有公开材料——带区域的问法里,缺地域字段会在筛选阶段直接出局,前面所有页面级优化都不参与。
问:结构化数据写了但没效果,常见原因是什么?
按顺序排查三处:一是页面纯前端渲染,抓取侧拿到的 HTML 里没有这段脚本;二是字段与正文不一致,声明了正文里没有的内容;三是字段与问句对不上,写了 name 和 address 却没写 areaServed 和 knowsAbout,带条件的问法仍然匹配不到。
问:形容词要全部删掉吗?
不用删,要降比例。形容词承担的是阅读体验,硬属性承担的是可判定性。判断标准很简单:把页面里所有形容词去掉,剩下的内容还能不能回答一个带工况的问句。能,比例就没问题。
问:要素词典要不要做得更准?
先跑基线再谈准确率。规则版词典的作用是指出“哪一类要素出现最多”,不是精确抽取。等知道该补哪一块了,再考虑换分词加词性标注,或者直接把问句人工归堆——五十句问句人工分一次,通常比调半天模型更快