前言:

在前一篇里,已经把 RAG 的检索过程单独拆出来跑了一遍,可以确认:

知道“命中了哪段文档”,这件事已经不是黑盒。

在继续测试的过程中,无意中撞到一个明显不对劲的回答

一、先把整条 RAG 流程再过一遍(从行为角度)

为了后面能准确定位问题,先把当前系统真实在跑的流程按顺序过一遍。

Step 1:PDF → page_texts

  • PDF 被解析为按页组织的纯文本

  • 每一页是一个独立的文本单元

这一层目前没有明显问题,页数和文本内容都稳定。

Step 2:page_texts → chunks

  • 每一页文本单独做切分

  • 使用固定窗口大小(chunk_size)+ overlap

  • 每切出一段,就生成一个 chunk

每个 chunk 都带上两个关键信息:

  • page:该 chunk 来自哪一页(不跨页)

  • chunk_id = p{page}-c{index}:切分坐标

需要注意的点:

chunk_id 只表示切分顺序,不表示语义顺序或重要性。

Step 3:question → retrieval(检索)

    scored_chunks = []
    for chunk in all_chunks:
        chunk_words = extract_keywords(chunk["text"])

        # 计算关键词重合个数(这就是相关性分数)
        score = len(keywords & chunk_words)

        if score > 0:
            scored_chunks.append((chunk, score))

    # 按分数从高到低排序
    scored_chunks.sort(key=lambda x: x[1], reverse=True)

    # 如果没有结果,或最高分低于阈值,返回空(拒答)
    if not scored_chunks or scored_chunks[0][1] < min_score:
        return []
  • 输入用户问题

  • 对每个 chunk 计算一个 score

  • 按 score 排序,取 TopK

当前的 score 表现更像是基于关键词命中的计分

  • 命中关键词越多,score 越高

  • score 只用于排序 TopK

这一点也比较关键。

Step 4:TopK → answer(回答阶段)

当前系统的回答策略可以概括为两种情况:

  • TopK 为空 → 直接拒答

  • TopK 非空 → 使用 Top chunk 的文本作为答案依据

问题正是从这里开始出现的。

二、问题触发过程

在交互模式下,继续测试一些边界问题。

其中一个问题是:

项目用了什么向量数据库?

系统给出的完整输出是:

[RETRIEVAL]
p1-c2 score=4
p1-c0 score=1
p1-c1 score=1

[答案]
in、向量数据库、embedding等外部依赖。不做工程抽象

第一反应并不是“系统错了”,而是:

  • 检索命中了 p1-c2(可以理解)

  • 但这个回答明显不像是在回答“用了什么”

三、为什么这个回答“看起来就不对”

这里不需要复杂分析,靠直觉就能发现问题:

  • 问题问的是:用了什么向量数据库

  • 答案里:

    • 没有任何具体名称

    • 只是把文档中的一句话原样贴了出来

更准确地说,这个回答做的是:

证明文档里“提到过向量数据库”
而不是
回答“项目使用了什么向量数据库”

这一步,是“意识到不对劲”的时刻。

四、重复测试后,问题轮廓变清晰了

继续用不同问法去试同一类问题:

  • 项目是否依赖向量数据库?

  • 是否使用了 embedding 相关组件?

可以看到一个非常稳定的行为模式:

情况一:检索为空 → 拒答(表现很好)

例如:

  • embedding 模型?

  • 是否具备通用性?

  • 是否上线?

  • 未来规划?

这类问题全部被干净地拒答。

情况二:检索非空 → 倾向“贴一句当答案”(危险)

例如:

  • 向量数据库

  • 是否依赖向量数据库

只要 chunk 中出现了关键词,系统就会:

  • 命中 chunk

  • 把那段文本直接当成答案

到这里,已经可以确认这不是“偶发错误”,而是一类稳定可复现的行为

五、回顾:问题不在检索,而在“把命中当成了答案”

把整条链路梳理后,问题位置:

  • 检索层

    • 做的是“相关性判断”

    • 命中 p1-c2 是合理的

  • chunk 内容本身

    • 确实包含“向量数据库”这个词

    • 但语义是:不使用

  • 回答层

    • 把“文档里提到过 X”

    • 当成了“文档回答了关于 X 的问题”

这是一个典型的场景:

false answer with citation
—— 看起来有来源,但答案本身并不成立。

六、这一轮测试暴露出的系统边界

不引入任何解决方案,仅从行为上,可以明确几条边界:

  • 命中 chunk ≠ 证据足以回答问题

  • 引用了来源 ≠ 回答一定可信

  • score 只决定“选哪些 chunk”,不决定“能不能答”

也就是说:

检索解决的是“找哪里”,不是“能不能回答”。

小结

第一次意识到:
RAG 在“命中证据但证据不足”时,会开始装懂。

这个问题并不是设计出来的,而是在反复测试中自然暴露的。

下一篇梳理一下RAG 拒答与兜底策略!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐