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 混合检索文章,框架如何组织这些能力则见下一篇。