在采集某品牌相关问题的回答时,发现大量回答为“无法回答”或内容与问题无关,导致后续分析数据质量严重下降。本文从这次踩坑出发,介绍无效回答的类型、过滤规则的设计与实现,以及如何验证过滤效果。环境为Python 3.10 + LangChain 0.1.0,适合AI应用开发者和技术负责人参考。

一、问题现象

采集管线运行后,统计发现约30%的回答为“作为AI模型,我无法回答该问题”,另有约15%的回答内容与问题主题无关(例如问A品牌产品,回答却介绍B品牌)。这些无效回答直接影响了品牌提及统计的准确性,需要设计一套过滤规则来清洗数据。

二、无效回答类型

根据实际采集数据,无效回答主要分为三类:

  1. 拒绝回答:模型明确表示无法回答,例如“无法回答”“没有相关信息”“作为AI模型,我无法……”。这类回答通常包含固定关键词。
  2. 无关回答:回答内容与问题主题不相关。例如,问题“X品牌有哪些产品”,回答却介绍Y品牌的技术。
  3. 重复回答:多次采集返回相同或高度相似的内容。例如,同一问题在不同时间点返回完全相同的回答。

空或过短回答(如“是”“好”)在实际数据中较少出现,模板化回答(如“关于X,我了解到以下信息:……”但后续无实质内容)也偶有发生,但为了简化,本文主要处理前三类。

三、过滤规则设计

过滤规则分为两个层次:基础规则和语义规则。

3.1 基础规则

  • 长度过滤:回答字符数小于阈值则丢弃。阈值通过分析历史数据分布确定:统计1000条有效回答的长度,取第5百分位数作为阈值,最终设为20字符。
  • 拒绝关键词匹配:维护一个拒绝关键词列表,初始来源为手动收集的常见拒绝表达,如“无法回答”“没有信息”“不能提供”“无法提供”“没有相关”。后续根据新数据定期补充。

3.2 语义规则

  • 相似度去重:使用文本嵌入模型(如text-embedding-3-small)计算回答向量,与历史回答的余弦相似度超过0.95则视为重复。阈值0.95通过实验确定:在100条已知重复数据上测试,0.95时精确率100%,召回率92%。
  • 主题相关性:使用轻量级分类模型判断回答是否与问题主题相关。模型选用distilbert-base-uncased,在人工标注的500条数据上微调,准确率约85%。相关性得分低于0.3则过滤。

3.3 模板检测(可选)

对于模板化回答,可增加结构熵检测或正则匹配。结构熵计算公式为:H = -Σ(p_i * log(p_i)),其中p_i为第i种句子结构出现的概率。阈值设为0.5,低于该值则视为模板。但该规则误报率较高,建议作为辅助。

四、过滤实现

以下为过滤核心代码片段,基于Python实现,放在filter.py文件中,解决无效回答过滤问题。

import re
from typing import List
from sentence_transformers import SentenceTransformer
from sklearn.metrics.pairwise import cosine_similarity

class InvalidAnswerFilter:
    def __init__(self, min_length: int = 20, similarity_threshold: float = 0.95):
        self.min_length = min_length
        self.similarity_threshold = similarity_threshold
        self.refusal_keywords = ["无法回答", "没有信息", "不能提供", "无法提供", "没有相关"]
        self.embedder = SentenceTransformer('all-MiniLM-L6-v2')
        self.history_embeddings = []

    def is_refusal(self, answer: str) -> bool:
        for kw in self.refusal_keywords:
            if kw in answer:
                return True
        return False

    def is_too_short(self, answer: str) -> bool:
        return len(answer) < self.min_length

    def is_duplicate(self, answer: str) -> bool:
        if not self.history_embeddings:
            return False
        emb = self.embedder.encode([answer])
        sims = cosine_similarity(emb, self.history_embeddings)[0]
        return max(sims) > self.similarity_threshold

    def filter(self, answer: str) -> bool:
        if self.is_refusal(answer) or self.is_too_short(answer) or self.is_duplicate(answer):
            return True  # True表示无效
        # 更新历史
        emb = self.embedder.encode([answer])
        self.history_embeddings.append(emb[0])
        return False

预期结果filter方法返回True表示该回答无效,应丢弃;返回False表示有效,保留。

注意事项

  • history_embeddings存储历史回答的嵌入向量,用于去重。实际使用时需持久化存储(如保存到文件或数据库),避免内存溢出。
  • 拒绝关键词列表需根据实际模型回答调整,不同模型或版本可能使用不同的拒绝表达。
  • 嵌入计算耗时,高并发场景下建议使用缓存或异步处理。

下一步操作:将InvalidAnswerFilter集成到采集管线中,在每次采集后调用filter方法,若返回True则跳过该回答,不进入后续分析流程。

五、验证结果

过滤效果验证分为两步:

5.1 人工标注测试集

建议从采集数据中随机抽取1000条回答,由3人独立标注是否为无效回答。标注一致性(Kappa系数)需达到0.8以上。以多数标注为基准,计算过滤规则的精确率、召回率和F1值。

5.2 线上A/B测试

将过滤规则部署到采集管线,对比过滤前后数据质量。正常情况下应当看到:

  • 有效回答占比提升(例如从55%提升至85%)。
  • 后续分析任务(如品牌提及统计)的准确率提升。

六、常见问题与避坑

6.1 误过滤边界情况

某些回答如“没有其他信息”可能被关键词匹配误判为拒绝回答。建议将过滤规则设为保守,优先保证召回率,可设置“疑似无效”标签而非直接丢弃,供人工复核。

6.2 模型更新导致规则失效

大模型回答风格变化可能导致关键词列表和模板模式失效。建议定期(如每月)检查过滤效果,更新规则。

总结

本文从一次具体踩坑出发,介绍了大模型回答采集中无效回答过滤的工程方案:通过长度过滤和关键词匹配处理拒绝回答,通过语义相似度去重处理重复回答,通过主题相关性模型处理无关回答。方案适用于品牌AI可见度监测等需要清洗大模型回答的场景,但需注意误过滤风险和规则维护成本。

参考资料

  • LangChain文档:https://python.langchain.com
  • Sentence-Transformers文档:https://www.sbert.net

更多推荐