)
本文由镜子AI技术团队整理。内容为通用的问答语料生成 覆盖度分析工程方法与具体业务无关代码可直接复用。一、问题背景做生成式引擎优化GEO时一个基础但繁琐的工作是为一个实体门店/机构/产品批量准备用户可能会问 AI 的问题并检查这些问题是否覆盖了真实的提问意图。手工列问题有三个痛点慢一个实体动辄要备几十条靠人脑想容易漏偏书面运营写出来的多是XX费用多少但真人问的是XX贵不贵啊没法验收哪些意图没覆盖、哪些问题不像真人提问全凭感觉。本文给一个零依赖 Python 方案用**「实体槽位 × 意图模板」批量生成标准问法 口语问法两类候选问题再用一张实体 × 意图覆盖矩阵**做验收。二、设计思路把问题拆成两部分实体槽位Slots机构名、城市、品类、课程、卖点——回答问的是谁意图模板Intents是什么、哪家好、怎么选、多少钱、在哪、适合谁、靠谱吗、多久见效——回答想知道什么。每个意图再配两种模板标准问法书面、完整适合标题和结构化内容口语问法带语气词、省略、倒装贴近真人对 AI 的提问。两者做笛卡尔组合就能批量生成候选问题再经过归一化去重、可检索性打分、覆盖矩阵统计三道工序。三、完整代码# -*- coding: utf-8 -*- geo_qa_generator.py —— 实体×意图的问答语料生成与覆盖矩阵零依赖 ENTITY { name: 晨光英语, city: 宁波, category: 成人英语口语培训, courses: [外教口语小班, 职场英语, 雅思口语], selling_points: [全外教小班, 可约晚间课, 提供试听], } INTENTS { what: {label: 是什么, std: [{city}{category}是什么, {name}是做什么的], talk: [{city}那个{name}到底是干啥的, {name}主要教什么呀]}, which: {label: 哪家好, std: [{city}{category}哪家好, {city}成人学英语选哪家机构], talk: [在{city}想学口语去哪家比较靠谱啊, {city}有没有好点的英语机构推荐]}, how_choose:{label: 怎么选, std: [怎么选{city}的{category}机构, {category}机构挑选标准], talk: [第一次报英语班该怎么挑啊不踩坑, 选口语机构主要看啥]}, price: {label: 多少钱, std: [{city}{category}费用多少, {name}课程怎么收费], talk: [{name}报个班大概要花多少钱, {city}学口语贵不贵啊]}, where: {label: 在哪, std: [{name}地址在哪, {city}{category}机构位置], talk: [{name}在哪个区啊好找吗, 宁波学英语的地方都在哪一片]}, who: {label: 适合谁, std: [{name}适合什么人群, {category}适合零基础吗], talk: [我三十多了零基础还能学口语吗, 上班没空晚上能学的机构有吗]}, trust: {label: 靠谱吗, std: [{name}靠谱吗, {city}{category}机构怎么判断是否正规], talk: [{name}会不会是那种交了钱就不管的, 怎么知道一家英语机构靠不靠谱]}, effect: {label: 多久见效,std: [{category}一般学多久有效果, {name}课程周期多长], talk: [练口语多久才能开口说啊, 报班学英语多长时间能看到效果]}, } # 初版词典偏书面漏掉口语长尾会误伤正常口语问法 INTENT_WORDS_V1 [哪家, 怎么, 多少, 哪, 什么, 适合, 靠谱, 多久, 费用, 地址, 推荐, 选] # 迭代版补入口语化意图词 INTENT_WORDS INTENT_WORDS_V1 [干啥, 贵不贵, 位置, 能不能, 多长, 不管, 没空, 零基础] TALK_MARKERS [啊, 吗, 呢, 呀, 吧, , 怎么, 啥, 哪, 有没有, 能不能] def fill(tpl, ent): return tpl.format(**ent) def generate(ent, intents): rows [] for key, cfg in intents.items(): for tpl in cfg[std]: rows.append((fill(tpl, ent), key, 标准)) for tpl in cfg[talk]: rows.append((fill(tpl, ent), key, 口语)) return rows def dedup(rows): 归一化去重去标点/语气词后相同视为重复保留首次出现 import re seen, out set(), [] for q, k, typ in rows: norm re.sub(r[。、\s呀啊呢吧的], , q) if norm in seen: continue seen.add(norm); out.append((q, k, typ)) return out def retrievable_score(q, words): 可检索性至少含1个意图词 且 长度在 6-24 字 hit sum(1 for w in words if w in q) return 1 if (hit 1 and 6 len(q) 24) else 0 def coverage_matrix(rows, ent, intents): 实体槽位 × 意图 覆盖矩阵该意图的问题是否提到对应实体 slots {机构名: ent[name], 城市: ent[city], 品类: ent[category]} matrix {} for key in intents: qs [q for q, k, _ in rows if k key] matrix[key] {s: int(any(v in q for q in qs)) for s, v in slots.items()} return slots, matrix if __name__ __main__: raw generate(ENTITY, INTENTS) rows dedup(raw) print(f原始生成 {len(raw)} 条去重后 {len(rows)} 条) std_n sum(1 for *_, t in rows if t 标准) print(f标准问法 {std_n} 条 / 口语问法 {len(rows)-std_n} 条) def rate(words): sc [retrievable_score(q, words) for q, _, _ in rows] return sum(sc) v1, v2 rate(INTENT_WORDS_V1), rate(INTENT_WORDS) print(f可检索性 初版词典 {v1}/{len(rows)} ({v1/len(rows)*100:.1f}%)) print(f可检索性 补口语词后 {v2}/{len(rows)} ({v2/len(rows)*100:.1f}%)) talk_qs [q for q, _, t in rows if t 口语] marked sum(1 for q in talk_qs if any(m in q for m in TALK_MARKERS)) print(f口语问法语气自然度 {marked}/{len(talk_qs)} ({marked/len(talk_qs)*100:.1f}%)) print(\n实体 × 意图 覆盖矩阵✓该意图有问题提到该实体) slots, matrix coverage_matrix(rows, ENTITY, INTENTS) print(意图.ljust(6) .join(s.ljust(8) for s in slots)) for key, cfg in INTENTS.items(): line cfg[label].ljust(6) for s in slots: line (✓.ljust(8) if matrix[key][s] else ·.ljust(8)) print(line)四、实跑输出原始生成 32 条去重后 32 条 标准问法 16 条 / 口语问法 16 条 可检索性 初版词典 24/32 (75.0%) 可检索性 补口语词后 32/32 (100.0%) 口语问法语气自然度 13/16 (81.2%) 实体 × 意图 覆盖矩阵✓该意图有问题提到该实体 意图 机构名 城市 品类 是什么 ✓ ✓ ✓ 哪家好 · ✓ ✓ 怎么选 · ✓ ✓ 多少钱 ✓ ✓ ✓ 在哪 ✓ ✓ ✓ 适合谁 ✓ · ✓ 靠谱吗 ✓ ✓ ✓ 多久见效 ✓ · ✓五、结果怎么读三个真实发现发现一意图词典不能只收书面词。初版词典的可检索性只有75.0%。看被误判的问题就明白了——宁波学口语贵不贵啊我三十多了零基础还能学口语吗上班没空晚上能学的机构有吗这些明明是非常自然的真人提问只因为词典里没有贵不贵、零基础、没空这些口语词就被打了 0 分。补上口语长尾词后识别率到100%。这背后是个工程教训检索/分类词典的召回率往往死在口语分布上。线上真实 query 的长尾、省略、语气词远比写代码时脑补的多。发现二覆盖矩阵直接暴露泛意图盲区。矩阵里哪家好怎么选两行机构名那一列是空的·。这非常符合现实——商家生产的问题大多是晨光英语怎么样带名字接住的是老客而真正带来增量的宁波成人学英语选哪家不带名字新客决策里却没有自己。矩阵把这个盲区量化成了两个空格运营一看就知道该补哪类内容。同理适合谁多久见效两行的城市列为空说明这些问题缺少本地化限定也要补。发现三口语自然度不是 100%反而更可信。口语问法的语气自然度是81.2%13/16。没有刻意凑到 100%——因为模板里保留了几条半书面问法真实语料本就该是书面与口语混合的分布。过度追求每条都带啊呀呢生成的问题反而显得假。六、踩坑记录坑一槽位缺失会抛 KeyError。模板里写了{name}但实体字典没传namestr.format直接报错。生产环境建议用string.Template.safe_substitute或先做槽位校验缺失时降级跳过该模板并记日志而不是整批失败。坑二去重归一化不能过度。第一版我把所有常用字都当噪声删掉结果学口语贵不贵和学口语难不难被误判成同一条。去重只应去掉标点和纯语气词不能删承载意图的实义词。坑三长度阈值是经验值不是真理。6-24 字是观察本地问答语料的经验区间换语种、换品类比如 B2B 长问句要重新标定别硬编码到所有场景。坑四模板法的天花板是同义改写。它能保证覆盖和效率但生成的问题句式固定、缺乏长尾多样性。要进一步拟真可以接 LLM 对每个意图做 few-shot 扩写再用本文的打分器和覆盖矩阵做验收——模板保底覆盖、LLM 扩充多样性、规则器把关质量三者配合比单押任何一个都稳。七、方法边界只解决问题侧语料生成的是用户可能怎么问不负责生成对应答案答案质量仍需领域知识模板依赖人工设计8 类意图和模板需按行业维护跨行业迁移要重写模板打分是启发式的意图词命中 长度只是对像不像真实检索的粗近似不等于真实搜索量覆盖矩阵是必要不充分条件覆盖全不代表内容会被 AI 引用信源权威性、事实可溯源等同样关键。八、小结这套实体槽位 × 意图模板 归一化去重 可检索性打分 覆盖矩阵的组合用一百行左右零依赖代码把原本靠拍脑袋的问答语料准备变成了可批量生成、可量化验收、可持续迭代的工程流程。最有价值的不是生成了多少条问题而是那张矩阵诚实地告诉你**哪类客户的问题你其实一条都没接住。