WRITING / 2026.09.23

检索到了,为什么还是答错?

沿着召回、过滤、重排与上下文装配定位记忆使用失败,用 Java 词项检索演示有效性约束,并说明混合检索和 Token 预算的扩展方式。

Alice 问“继续讲 Python 生成器”。记忆库里确实有“学到 Python 生成器”,但助手仍从变量定义讲起。另一次它找到了“主要写 Java”,却忽略背景已改成 Kotlin。两次失败都不能只用“召回率低”解释:前者可能在排序或生成,后者可能在版本过滤。

本篇把记忆使用拆成候选召回、硬约束过滤、重排、上下文装配和回答使用五个环节。每一环都应留下能够定位问题的证据。本专题的Java 示例只实现其中可确定性验证的部分,真实语义理解与生成需要另行评测。

先确定这次问题需要哪类记忆

“继续讲 Python 生成器”主要需要学习进度;“按我熟悉的语言解释”需要技术背景;“这个月安排怎样”需要阶段性目标。如果所有问题都检索所有记忆,背景、目标和过期计划可能一起挤占上下文。任务意图可以帮助限定类型,但路由错误也会造成漏召回。

可以先以简单规则或调用方明确参数指定目标类型,再考虑模型路由。对跨类型问题允许组合多个检索请求,并记录每一路候选来源。这样的分解能解释漏召回发生在哪个路由,而不是把一个低分统一归因于向量模型。

离线示例没有实现意图路由,只检索当前用户的所有有效槽位。它用很少的数据展示过滤与排序顺序,避免让读者误以为一个简单检索方法就覆盖了完整 Agent 规划。生产扩展可以添加类型参数,但必须继续保留用户和有效性约束。

权限和有效性应当先于相关度

示例的 search 先调用 current(user),排除其他用户和失效版本,然后计算相关度。这样 Bob 的 Python 背景即使十分相关,也不会进入 Alice 的候选;旧 Java 版本即使词项完全匹配,也不进入当前查询。

实际使用外部索引时,通常应把用户或租户过滤尽量下推到检索阶段。因为索引更新可能滞后,候选返回后还要在可信状态源检查删除、权限和版本。仅在最终结果展示前过滤,可能导致错误内容进入重排模型或日志,也可能耗尽候选额度。

硬约束不能轻易退化为低分项。“过期内容减几分”仍允许它在候选不足时排到前面。如果需求是当前事实检索,过期就是排除条件;如果用户主动询问历史,应该进入明确的历史查询模式,并标出对应时点。

离线词项检索到底做了什么

示例把拉丁字母转成小写,按词项匹配;连续中文文本拆成相邻双字词项。例如“生成器”产生“生成”和“成器”,查询与记忆值的词项交集大小就是相关分。它没有计算 IDF,没有文档长度归一化,因此不是 BM25;也没有向量,所以不具备同义词语义匹配能力。

查询 Python 时,“Python 生成器”和“Python”都获得一个交集词项。随后按有效开始时间从近到远排序,因此较新的进度排在较早的目标前面。分数与时间都相同,最后按记忆 ID 的字典序排序,保证多次调用结果稳定,不把集合遍历顺序当成业务规则。

var store = MemoryDemo.learningAssistant();
var hits = store.search("alice", "Python", 10);
var context = store.context("alice", "Python", 200);

固定案例的顺序是学习进度在前、学习目标在后。查询 Rust 或空字符串返回空列表。无候选本身就是一种有效结果,调用方应该允许助手说明缺少信息,而不是一定要选出分数最高的一条无关记忆。

混合召回如何扩展

词项检索适合精确名称、技术符号和某些实体;向量检索有机会覆盖不同表达之间的语义联系;结构化查询擅长时间、类型与主体约束;图遍历适合已经建模的实体关系。是否组合这些方法,应由真实失败案例决定,而不是先把所有后端接进来。

组合时不要直接相加量纲不同的分数。一个词项分数、一个向量相似度和一个图距离通常不可直接比较。可以先在各路得到排名,再采用排名融合形成候选池,最后按业务条件重排。方法选定后仍要验证相关候选是否增加,以及新增噪声是否伤害最终回答。

还要处理重复来源。同一事实可能既被关键词检索命中,也被向量检索命中;同一原始事件也可能形成多份摘要。应按稳定记忆 ID 或明确的事实归并规则去重,并保留它由哪些路径被召回的信息。把重复命中当成多份独立证据,会夸大确定性。

重排需要服务当前任务

时间新鲜度有帮助,但不是越新越正确。长期稳定的主要语言可能比几分钟前的一次临时实验更有解释价值。重排可以考虑任务类型、来源可靠性、有效范围和互补性,但每项信号都应对应具体失败案例,并通过消融实验确认价值。

例如用户问“上个月主要用什么语言”,最新背景不一定回答了问题;用户问“现在继续哪一章”,过旧进度即使语义相似也可能误导。时间条件首先应改变候选范围,再决定候选内部如何排序。仅给新内容加权,无法代替历史查询语义。

模型重排也会增加费用与延迟,并可能受输入中的恶意指令影响。应把候选视为数据,限制输入长度,避免附带不必要的私人原文;模型输出只影响排序,不应改变访问权限、恢复已删除记录或创造不存在的证据。

上下文预算要计算完整开销

示例的每条上下文包含记忆 ID、槽位、来源事件 ID 和值。预算按 Unicode 码点计数,包含这些标签和换行;放不下一整条就跳过,再尝试后续较短候选。不会把“主要语言不是 Java”截成“主要语言是 Java”这类改变语义的碎片。

[m-3 progress.current source=e3] Python 生成器
[m-2 goal.current source=e2] Python

这段输出是固定样例的检索结果,不是模型回答。在真实模型中,预算还应计入系统约束、当前问题、工具定义、检索文档和预留输出。不同 Tokenizer 对中文、标点与代码的计数不同,不能用 Java 字符长度冒充精确 Token 费用。

如果一条重要事实太长,可以在更早的阶段形成可追溯摘要,而不是在最终装配时无边界截断。摘要也可能丢失否定、主体和条件,应该保留原始来源,必要时二次读取。装配器要知道摘要与原文的关系,不能把两者误当成相互印证的独立材料。

找到信息之后,仍需验证使用方式

回答可能没有采用已经传入的进度,也可能把示例中的 Bob 当成当前用户。评测应同时记录检索候选、最终上下文和回答,才能区分“没找到”“没放进去”“放进去没用对”。只记录最终回答,会让优化方向依赖猜测。

可以给上下文附上明确的数据角色和来源,要求回答围绕证据作出有限判断;但 Prompt 要求不是保证。遇到证据缺失或互相冲突,应允许询问、澄清或拒绝猜测。最终判断是否准确,需要独立标注或人工审核,不能由系统自我声明完成验证。

对于工具执行,要求更严格。记忆中保存“用户以前偏好自动提交”,并不等于本次操作获得授权。个人偏好可以影响表达方式,不能自动扩大工具权限。生成内容和执行决策应分别检查,尤其是写入外部系统的动作。

调试时按链路保留最小证据

建议为一次请求记录查询意图、检索范围、候选 ID、过滤原因、排序结果和最终使用 ID。默认避免把全部私人正文写进日志。先从标识和原因定位,再在受控环境查看原文,更容易兼顾诊断能力与数据边界。

一个实用顺序是:先验证事实是否正确写入,再验证版本与权限过滤,再检查候选是否命中和是否被预算挤出,最后检查模型是否使用正确。不要在事实根本没写入时调向量参数,也不要在错误来自旧版本时盲目增加 Top K。

测试程序覆盖无匹配、稳定排序、中英文词项和整条预算,属于检索工程契约。混合检索的实际收益需要第六篇的对照实验验证。进一步的基础设施实践可阅读ES 混合检索文章,框架如何组织这些能力则见下一篇

AI 记忆工程:从概念到可验证的系统

  1. AI 记忆工程指南:从概念到可验证的系统
  2. AI 记忆、RAG 与上下文管理是什么关系?
  3. 从一段对话到一条可靠记忆
  4. 用户改变主意后,AI 如何更新记忆?
  5. 检索到了,为什么还是答错? · 当前文章
  6. 用同一个案例拆解 Mem0、Graphiti 与 LangGraph 记忆机制
  7. 加了记忆,Agent 到底变好了吗?
  • AI 长期记忆系统:从提取、召回到生命周期治理

    把长期记忆视为一条数据与决策链路,拆解记忆提取、检索召回、冲突处理、遗忘与评估。

  • 从知识库问答到 Agentic RAG:让 Agent 找到可靠答案

    以结算服务超时排查为例,拆解文档切分、元数据过滤、关键词与向量检索、重排和引用,比较固定 RAG 与动态补充检索。通过离线可复现的证据链定位未召回、召回错误和生成错误,并明确教学向量与真实语义检索的区别。

  • Agent 的上下文与记忆:如何记住有用信息

    围绕研发排障助手,区分系统指令、任务状态、知识库、会话历史与长期记忆,展示每轮调用如何选择和组装上下文。用离线 Python 示例验证跨会话读取、过期过滤、用户隔离及长度预算,并讨论摘要丢证据和不可信内容混入的风险。

按时间浏览