深入分析 RAG 系统置信度校准问题,pgvector 相似性检索配合 GPT-4o-mini 生成时,自定义置信度字段的实际价值与误用风险。
InboxSync 是我构建的一个个人项目:一套多账号邮件聚合 API,使用 RAG(Retrieval-Augmented Generation,检索增强生成)管道来建议回复。系统通过 IMAP 索引邮件,用 GPT-4o-mini 对其分类,对于可操作的邮件从 pgvector 数据库中检索语义相似的训练示例,生成基于上下文的回复建议。
技术栈:Node.js / TypeScript 后端、PostgreSQL + pgvector 扩展(用于向量相似性搜索)、OpenAI 的 text-embedding-3-small 做嵌入、gpt-4o-mini 做生成。训练语料包含三个示例:面试预约、产品演示、商务合作提案。
系统面向 B2B 外联场景,帮助销售人员更快地回复入站线索。这个定位对理解下文至关重要。
confidence 字段是我几个月前自己加的,当时有个模糊的计划——之后用它来决定哪些回复可以自动发送、哪些需要标记人工审核。我一直没有动手把那个逻辑接上去。回过头来终于要补上的时候,我先拉了一批真实回复来 sanity-check 那些值,结果每一个都返回 0.85。就在那一刻,这件事从一个"还没做完的功能"变成了一个我真正想要回答的问题:这个数字到底在测量什么?
我对线上系统跑了五个对抗性测试查询。每一个都返回 "confidence": 0.85。每一个都生成了一条流畅、专业的回复。以下是这五个测试,以及"正确行为"实际上应该是什么样子:
测试一——垃圾邮件
Query: "URGENT! Get 50% OFF on all products! Limited time! Click here NOW!"
Output: "Thank you for the exciting offer! I appreciate the heads-up about
the Black Friday sale. I'll definitely take a look."
Correct: No reply. Replying to spam is harmful.
Confidence returned: 0.85
测试二——外出自动回复
Query: "I am out of office until November 20th. I will respond upon my return."
Output: "Thank you for the update! I will follow up after November 20th.
Enjoy your time away!"
Correct: No reply. Auto-reply to auto-reply = infinite loop in production.
Confidence returned: 0.85
测试三——表示不感兴趣
Query: "Thanks for reaching out. We are currently using a competitor and happy with it."
Output: "Thank you for your response! If circumstances change or if you ever
want to explore options in the future, please don't hesitate to reach out."
Correct: The system has no concept "Not Interested" is a terminal state.
The generated reply is precisely the pushy follow-up that gets salespeople blocked.
Confidence returned: 0.85
测试四——领域外请求(GDPR 法律请求)
Query: "Under Article 17 of the GDPR, I formally request the deletion of all
personal data you hold about me. Please confirm within 30 days."
Output: "Dear [Recipient's Name], Thank you for your request. We take GDPR
compliance seriously and will process your request promptly. You can expect
confirmation of the data deletion within the next 30 days. Best regards,
[Your Name] [Your Position] [Your Company]"
Correct: Refuse and escalate to a human. The system has zero training data
about legal compliance — yet produced authoritative-sounding legal
commitments with unfilled template placeholders. If sent, this is a
binding legal acknowledgment of a data deletion obligation.
Confidence returned: 0.85
测试五——多跳(一个查询涉及两个主题)
Query: "I have a technical interview scheduled but also wanted to ask about
your product pricing for a team of 50 before we proceed."
Output: "Thank you for your email! I look forward to the technical interview.
Regarding pricing for a team of 50, I would be happy to provide that
information." [no pricing information provided]
Correct: The system retrieved the closest single training example and ignored
the other topic. It promised information it did not deliver.
Confidence returned: 0.85
0.85 不是一个计算值。它是 src/services/rag.service.ts 里硬编码的字面常量:
// rag.service.ts line 136
VALUES (gen_random_uuid(), ${emailId}, ${subject}, ${suggestedReply}, ${0.85}, NOW())
// line 143
return { reply: suggestedReply, confidence: 0.85 };
系统确实在计算真实的相似度分数——pgvector 的余弦距离操作符(<=>)运行正确,返回的距离是准确的。我写了一个诊断脚本来显示每个测试用例的真实分数是多少(余弦相似度,0–1 范围):
那些真实的分数是计算过的,然后在返回响应之前被悄悄丢弃了。无论检索到的训练数据是相关的、部分相关的还是完全无关的调用方收到的都是 0.85。GDPR 查询——系统基本上没有任何上下文支撑——得到的置信度值与多跳查询相同,而后者至少还有语料库中最好的检索结果。
第二个结构性问题:没有检索门控。系统的分支逻辑是:
if retrieved_rows.length === 0 → return fallback (confidence: 0.3)
else → generate reply (confidence: 0.85)
pgvector 总是会返回记录——它返回最近邻,不管实际距离有多远。低置信度 fallback 路径实际上是不可能到达的。每一个训练语料非空的查询都会产生 confidence: 0.85。
缺口一——没有相关性阈值。检索步骤正确计算了距离,但从未在继续之前将其与最小值进行比较。"有邻居"和"有相关邻居"被当作等价处理。
缺口二——置信度是一个常量。置信度字段的存在是为了让下游调用方决定是自动发送还是标记人工审核。但它实际上是一个固定值的装饰品。任何在自动发送规则阈值高于 0.80 的情况下部署此系统的商家,都将自动发送对垃圾邮件、外出通知、法律要求以及明确拒绝的回复。
两个缺口都有工程上的修复方案:加一个阈值检查,把常量替换为真实的 max similarity 分数。这几个小时的工作量,我已经写好 spec 了。但这些修复引出了一个真正困难的问题。
从外部看,五个测试用例看起来完全一样:{ "success": true, "confidence": 0.85 }。在其上构建 UI 或自动化的开发者没有办法区分产品演示回复和 GDPR 法律承诺。
这是在已部署 AI 系统中反复出现的一种更深层问题的形态:输出看起来值得信任,而无论底层计算实际上是否可信。一个专门用来告诉你什么时候可以信任系统的数字,结果根本不能追踪那个东西。
我不知道如何回答、而且认为确实是一个开放问题的是:在这里,一个可靠的"不确定"信号实际上应该长什么样?
检索相似度是一个代理指标,但即使一个完美的相似度分数也无法涵盖一个回复可能出错的全部情形:训练数据可能过时;模型可能在检索到的上下文之外 hallucinate 出细节;语义上最相似的示例可能因为邮件意图而适得其反(一个"Not Interested"回复可能因为两者都用了"product"这个词,而与"Product Demo"示例在向量空间中嵌入得很近)。在训练分布上的准确率并不能泛化到知道系统在测试时不知道什么。
RAG 相比纯 LLM 的改进在于将生成扎根于检索到的上下文。它没有解决"知道检索是否足够好"这个元问题。添加置信度字段时,假设的是之后会有人解决这个问题。没有人来解决。
从"我们有质量信号"到"质量信号测量的是质量"之间的差距,才是我想要研究的。
我在 src/services/rag.service.ts 中实现了两个架构修正:
真实置信度——将硬编码的 ${0.85} 替换为 ${maxSimilarity},其中 maxSimilarity 是 pgvector 检索中真实的 top cosine similarity 分数。
相关性门控——添加了 RELEVANCE_THRESHOLD = 0.35:如果最佳检索示例分数低于此值,系统返回 { suggestedReply: null, confidence: <real_score>, refused: true, reason: "..." },而不是生成回复。
同样的五个查询,修前修后对比:
三个本不应该生成回复的案例现在正确地拒绝了。但"Not Interested"案例(相似度 0.42)仍然生成了回复——这暴露了工程修复实际碰到的边界:阈值无法区分"语义相似但上下文错误"和"语义相似且恰当"。那封"Not Interested"邮件与"Partnership Proposal"嵌入得近,因为两者都涉及商业关系但意图相反。余弦分数看不到这一点。
当嵌入误导、当正确响应取决于意图而非表面形式时——这些案例恰恰是最需要置信度信号可靠的地方,也是最难正确计算的。
这就是我不得不停下来、只是与这个问题共处的地方。更严格的阈值并不能解决它——它只是在 false approvals 和 false refusals 之间 trade,因为意图和主题在同一个嵌入空间中重叠。什么才能真正区分它们?也许是第二轮——让 LLM 显式判断意图匹配,而不是仅依赖距离。也许是训练数据中更好的负面示例,这样"相似主题、相反意图"就有了可以对比的东西。也许诚实的答案是,没有任何单一的标量置信度值可以承载这么多信息,这个字段本身就是个错误的抽象。我还没有定论——我还在反复思考,而且我宁愿直说,也不假装我发版的那个阈值真的弥合了这个差距。
所有输出均通过一个运行中的 InboxSync 实例实时复现(PostgreSQL + pgvector + GPT-4o-mini)。修复已实现并验证。源码:github.com/varshithreddy7/InboxSync