登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

向量检索结果如何做可解释验收:相似度阈值、引用片段与空召回处理

来源:17golang原创

时间:2026-08-28 11:55:27 297浏览 收藏

向量检索最容易制造一种错觉:结果带着相似度分数,似乎就已经足够可信。实际接入 RAG 后,真正需要验收的是一条可追溯链——查询拿到了哪些片段、每个片段来自哪个来源、分数是否跨过业务阈值,以及低分或空结果时系统是否会停下来说明“没有足够证据”。

实践要点:
  • 把相似度分数当作筛选信号,不把它直接当成答案正确率。
  • 检索结果至少保留 source_id、片段文本和分数,回答才能回链。
  • 阈值要在标注样本上验收,0.78 只能作为本地实验起点。
  • 低分与空召回进入明确的降级分支,不要硬拼上下文。

先把“命中了什么”记录下来

Qdrant 的搜索接口会返回点的标识、payload 和 score,过滤条件也可以和向量查询一起使用。OpenAI 的知识检索方案同样把引用和评估放在可靠回答的工程链路里。对应用来说,最小可验收结果不是一段字符串,而是一个包含来源、片段和分数的记录。

下面用 Python 做一个与具体 SDK 解耦的收口函数。真实项目可以把 `query_points` 替换成向量数据库客户端调用,但不要丢掉这三个字段:`source_id` 用于回链,`score` 用于阈值判断,`引用片段` 用于人工核对。

向量检索从 query_points 返回 score 和 source_id,再形成带引用片段的证据链
检索验收的重点是把 query_points 的返回值整理成可以回链的证据记录。

初始化一组能复查的检索结果

先准备两条模拟结果:它们来自不同的 `source_id`,并且都保留原始片段。这里不调用模型,目的是先把检索层的验收逻辑固定下来。

from dataclasses import dataclass

@dataclass
class Hit:
    source_id: str
    score: float
    引用片段: str

def query_points() -> list[Hit]:
    return [
        Hit("manual-17", 0.86, "重建索引前先确认 collection 的向量维度"),
        Hit("runbook-03", 0.74, "空召回时返回澄清问题,不拼接无关片段"),
    ]

示例里的 `query_points` 是本地实验函数,不是某个数据库的默认实现;它只模拟“查询点并得到命中列表”。`source_id` 和 `引用片段` 必须来自真实入库记录,不能由生成模型补写。

用 score_threshold 筛掉不够近的片段

阈值不是越高越好。阈值过低会把主题相邻但不能回答问题的文本塞进上下文,过高则容易得到空结果。先把阈值写成显式参数,任何一次验收都能复现。

def retrieve(query: str, score_threshold: float = 0.78) -> list[Hit]:
    hits = query_points()
    accepted = [hit for hit in hits if hit.score >= score_threshold]
    return accepted

hits = retrieve("如何确认向量维度")
for hit in hits:
    print(hit.source_id, hit.score, hit.引用片段)

运行后应只保留 `manual-17` 的 0.86 结果,`runbook-03` 的 0.74 被阈值挡住。这个判断只能说明“分数达到当前筛选线”,不能推出“回答必然正确”;还要检查片段是否真的覆盖问题中的实体和条件。

把引用片段绑定到回答输入

检索通过后,再把证据格式化成模型能看懂、用户也能回查的上下文。格式化函数只接收已验收的 `Hit`,避免把原始未过滤结果混进提示词。

def build_context(hits: list[Hit]) -> str:
    return "\n".join(
        f"[{hit.source_id}] {hit.引用片段}(score={hit.score:.2f})"
        for hit in hits
    )

context = build_context(hits)
print(context)

回答生成后仍应把 `source_id` 传到展示层,至少允许用户点开或检索原文。不要只显示“参考资料 1、2”,那样人工复查时还要重新猜它对应哪条文档。

低分和空召回走独立分支

真正容易出错的是没有可靠片段时仍然让模型自由作答。把空结果处理成一种业务状态,既可以返回澄清问题,也可以转人工或请求用户补充关键词。

def answer_route(query: str) -> dict:
    hits = retrieve(query, score_threshold=0.78)
    if not hits:
        return {
            "status": "needs_clarification",
            "message": "没有找到达到阈值的引用片段",
            "citations": [],
        }
    return {
        "status": "grounded",
        "message": build_context(hits),
        "citations": [hit.source_id for hit in hits],
    }

这里的 `needs_clarification` 不是失败吞掉,而是可观察的结果状态。日志中应同时记录查询文本的脱敏版本、阈值、命中数和返回的 `source_id`。如果调用的是 Qdrant,还应把实际过滤条件与 collection 名称纳入链路日志,方便区分“没有相似内容”和“过滤条件把结果排空”。

向量检索低于 score_threshold 后进入 needs_clarification,不把空结果硬拼给模型
低分命中与真正空召回都应进入可观察的 needs_clarification 状态。

用小样本校准阈值,不要照抄示例数字

准备一组真实问题和人工标注的相关片段,分别记录不同阈值下的命中、误召回和空结果数量。先看业务更怕哪一种错误:客服知识库可能更怕把无关条款带进回答,内部搜索则可能更愿意接受稍低分的候选。

  • 把查询、文档 ID 和相关性标注固定下来,避免每次换样本。
  • 分别测试 0.70、0.78、0.85 等候选线,记录命中率与空召回率。
  • 观察不同主题、不同文档长度的分数分布,不要把单一问题的最高分当默认线。
  • 上线后持续抽查引用片段,发现分数高但答非所问时,回到切片和嵌入模型排查。

阈值只负责第一道筛选。对高风险回答,还可以增加关键词覆盖、文档时效、权限过滤和二次重排,但每增加一个条件,都要能在日志里解释为什么这条片段被接受或排除。

常见问题与边界

score 越高就代表答案越正确吗?

不代表。score 是向量空间中的相似度信号,仍需核对片段是否覆盖问题、来源是否有权限、内容是否过期。

空召回时可以把阈值调低再问一次吗?

可以作为明确的降级策略,但要记录第二次阈值并限制次数。不能静默降低标准后把无关片段当证据。

为什么一定要保存 source_id?

它把生成回答和原始资料连起来,便于用户复查、审计和定位错误。没有来源标识,分数再高也难以解释。

清理实验并保留验收结论

完成实验后删除临时样本,保留阈值、标注集版本、命中日志和几条人工复查记录。一个合格的向量检索链不是“总能返回结果”,而是能明确区分 grounded、低分和 needs_clarification,并让每个 grounded 片段回到真实来源。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>