☰
镜子AI工程实践:用 Python 自动生成 GEO 问答语料与意图覆盖矩阵(零依赖)
2026/10/29 2:15:53 网站建设 项目流程

本文由镜子AI技术团队整理。内容为通用的"问答语料生成 + 覆盖度分析"工程方法,与具体业务无关,代码可直接复用。

一、问题背景

做生成式引擎优化(GEO)时,一个基础但繁琐的工作是:为一个实体(门店/机构/产品)批量准备"用户可能会问 AI 的问题",并检查这些问题是否覆盖了真实的提问意图。

手工列问题有三个痛点:

  1. 慢:一个实体动辄要备几十条,靠人脑想容易漏;
  2. 偏书面:运营写出来的多是"XX费用多少",但真人问的是"XX贵不贵啊";
  3. 没法验收:哪些意图没覆盖、哪些问题不像真人提问,全凭感觉。

本文给一个零依赖 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}但实体字典没传name,str.format直接报错。生产环境建议用string.Template.safe_substitute或先做槽位校验,缺失时降级跳过该模板并记日志,而不是整批失败。

坑二:去重归一化不能过度。第一版我把所有常用字都当噪声删掉,结果"学口语贵不贵"和"学口语难不难"被误判成同一条。去重只应去掉标点和纯语气词,不能删承载意图的实义词。

坑三:长度阈值是经验值,不是真理。6-24 字是观察本地问答语料的经验区间,换语种、换品类(比如 B2B 长问句)要重新标定,别硬编码到所有场景。

坑四:模板法的天花板是"同义改写"。它能保证覆盖和效率,但生成的问题句式固定、缺乏长尾多样性。要进一步拟真,可以接 LLM 对每个意图做 few-shot 扩写,再用本文的打分器和覆盖矩阵做验收——模板保底覆盖、LLM 扩充多样性、规则器把关质量,三者配合比单押任何一个都稳。

七、方法边界

  • 只解决"问题侧"语料:生成的是"用户可能怎么问",不负责生成对应答案,答案质量仍需领域知识;
  • 模板依赖人工设计:8 类意图和模板需按行业维护,跨行业迁移要重写模板;
  • 打分是启发式的:意图词命中 + 长度,只是对"像不像真实检索"的粗近似,不等于真实搜索量;
  • 覆盖矩阵是必要不充分条件:覆盖全不代表内容会被 AI 引用,信源权威性、事实可溯源等同样关键。

八、小结

这套"实体槽位 × 意图模板 + 归一化去重 + 可检索性打分 + 覆盖矩阵"的组合,用一百行左右零依赖代码,把原本靠拍脑袋的问答语料准备,变成了可批量生成、可量化验收、可持续迭代的工程流程。最有价值的不是生成了多少条问题,而是那张矩阵诚实地告诉你:**哪类客户的问题,你其实一条都没接住。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询