RAG 上下文组装 从权限过滤到预算与引用校验
检索排序正确,不代表可以把结果原样交给模型。一段高分材料可能属于其他租户;同一段落可能重复出现;前几个长段落可能占满预算;回答还可能引用根本没有进入本次上下文的片段。
这篇接在混合检索与重排之后,研究“候选已经找到了,怎样变成生成输入”。用纯 Python 标准库把权限过滤、精确去重、预算装箱和引用回查拆开。实验不调用模型,不能据此宣称回答质量提升,也不把示范过滤器当成生产身份认证系统。
候选与重排
知道排名是相关性的线索,不是权限、内容真实性或完整性的保证。
冻结证据集合
保存片段身份、文档版本与原文,限定本轮能被引用的材料。
有出处不等于有依据
字面引文检查能查出伪引用,却不能证明回答被原文蕴含。
先把四个环节分开
| 环节 | 回答的问题 | 不应替它承担的任务 |
|---|---|---|
| 权限过滤 | 当前用户可以看到哪些材料? | 用相似度猜测是否有权访问 |
| 精确去重 | 同一身份的片段是否重复返回? | 把不同文档的相同文字误合为同一出处 |
| 预算组装 | 哪些完整片段能进入本轮输入? | 把字节数直接当作模型 token 数 |
| 引用回查 | 引用是否来自本轮证据,原文是否包含引文? | 证明整个回答的语义一定正确 |
权限应该在检索服务返回候选之前生效,不能先把未授权文档交给外部重排服务,再在生成阶段删掉。本地示例再次过滤是防御检查,不替代检索端的访问控制。
微软的安全过滤文档明确区分字符串过滤模式与真正的身份认证、授权:身份字段只负责匹配,不能证明调用者是谁。生产中的用户、租户和角色应来自已认证服务端会话,不能相信请求体里自填的角色。
flowchart LR A[服务端确认身份与权限] --> B[带权限约束的检索与重排] B --> C[本地再次过滤与精确去重] C --> D[按预算保留完整片段] D --> E[冻结本轮证据快照] E --> F[生成后回查引用]
片段身份必须连着文档版本
示例片段包含 id、tenant、doc、version、text、score,权限表以 (tenant, doc, version) 为键,值是允许访问的角色集合。表中没有记录、角色不相交、租户不相同,全部拒绝。
id 在同一请求的租户内必须唯一。同一 id 重复返回时,文档、版本或正文有冲突就报错;仅分数不同则视为重复,排序靠前的一份保留。生产可使用全局唯一片段键、版本号与内容摘要,并保留页码或原文偏移,不能依靠一个易重复的自然语言标题。
这里仅做身份去重。不对相似段落做语义合并:相同句子在两份不同文件中仍可能承担不同证据角色。近重复、重叠窗口与同文档配额是后续可比较的策略,不应悄悄混进本次基线。
预算的单位比数字更重要
实际模型输入需要使用目标模型匹配的 tokenizer,并预留系统规则、问题、对话历史、工具定义及输出空间。不同接口的窗口与输出计费规则要按对应官方资料核对,本文不提供通用模型窗口数字。
为了让实验无依赖、可精确复算,这里使用 序列化证据 JSON 的 UTF-8 字节预算,包括字段名、标点、引号、转义与正文;它不是 token 预算,也不覆盖完整模型请求。空列表 [] 本身需要 2 字节。中文字符、表情与引号都会影响真实字节数,不能拿 len(text) 代替。
序列化使用 json.dumps(..., ensure_ascii=False, separators=(",", ":")) 再编码为 UTF-8;相关参数见 Python 官方 JSON 文档。生产替换计量器时,必须重新计量实际发送的完整结构,不能只把字节预算变量改名为 token 预算。
按排名扫描不等于最优装箱
策略是:权限过滤后按分数降序排列;相同分数按文档、版本和片段 id 排列,确保可复现;重复身份只处理一次;每次尝试加入一个完整片段,若超过预算就跳过,继续检查后面的片段。
这比“遇到首个超长片段就停止”更容易保留后面较短的材料,但不是背包最优解,也没有证明它最大化回答相关性。分数不是收益的精确值,多段证据之间还可能存在互补或重复。
| 原创示例中的候选 | 处理 | 理由 |
|---|---|---|
| 其他租户的高分片段 | 丢弃 | 分数再高也不能越过租户边界 |
| c1 排障说明 | 尝试加入 | 有权访问且身份有效 |
| 同一 c1 再次返回 | 跳过 | 同一原文身份,不重复占预算 |
| c2 很长的背景材料 | 可能跳过 | 超过剩余预算,不能截成不完整句子 |
| c3 较短的重试说明 | 继续尝试 | 前面超长不代表后面也超长 |
没有任何片段入选时,应走“证据不足”分支,而不是强迫生成一个有出处的答案。
flowchart TD
A[下一条有权候选] --> B{身份是否已经出现}
B -- 是且内容一致 --> A
B -- 是但内容冲突 --> C[停止并报告数据冲突]
B -- 否 --> D[序列化当前证据加候选]
D --> E{是否在预算内}
E -- 是 --> F[纳入本轮证据]
E -- 否 --> G[跳过而不截断]
F --> A
G --> A
可直接运行的 Python 标准库实验
使用 Python 3.9 或更新版本,保存为 context_demo.py 后运行 python context_demo.py。正文与身份均为合成数据,无网络请求、不读凭据、不调用语言模型。Principal 和权限表在实验中直接构造,生产必须改由可信服务端提供。
1 | from dataclasses import dataclass |
样例会保留 c1 与 c3;c2 没有进入本轮证据,因此引用 c2 仍不合法,即使它确实来自知识库。代码记录错误类别,不把未授权正文或引文写入日志。
| 实际运行项 | 输出 | 解释 |
|---|---|---|
ids |
["c1", "c3"] |
去重并跳过超长材料后仍继续扫描 |
evidence_bytes |
164 | 完整证据 JSON 的 UTF-8 字节数,低于 240 |
good |
ok=true |
原文确实包含本轮引文 |
outside |
not_in_context |
c2 虽在候选里,却不在本轮上下文 |
fake |
quote_not_found |
伪造句子不是 c1 的子串 |
revoked |
permission_revoked |
复核时权限表已不再允许访问 |
引文存在不代表回答被支持
当前校验只检查结构、片段入选、权限仍有效、版本一致,以及引文是否为正文的精确子串。它不处理同义改写、空白规范化或 Unicode 等价形式,也没有把答案正文传入校验器。
例如原文说“缓存有效期为 30 分钟”,回答却说“缓存每 30 分钟必然自动刷新”。即便引用原句,字面检查仍会通过,答案却增加了原文没有说的自动刷新机制。要检查这类错误,需要拆分回答断言、核对适用范围与限定词,并做人工抽检或经过独立评估的蕴含校验。
| 错误 | 这个实验能否发现 | 需要补充的环节 |
|---|---|---|
| 引用未入选的 c2 | 能 | 本轮证据集合回查 |
| 引用旧版本号 | 能 | 版本身份校验 |
| 编造一句不存在的原文 | 能 | 精确子串检查 |
| 正文加上来源没有说的机制 | 不能 | 断言与来源的语义核验 |
| 来源本身已经过时或错误 | 不能 | 数据治理、时效与来源可靠性检查 |
| 缺少引用的拒答 | 不能按引用成功计入 | 拒答应有独立响应与评测分支 |
flowchart LR A[引用有可追踪身份] --> B[引文确实存在] B --> C[回答断言是否被支持] C --> D[来源是否可靠且仍适用] A -. 本例检查 .-> E[结构与本轮成员] B -. 本例检查 .-> F[精确子串] C -. 另需评测 .-> G[语义与限定条件]
权限撤销与缓存不能只看第一次请求
示例在引用检查时重新读取当前权限表。如果权限已撤销,不继续展示依赖该证据的回答。生产还必须把权限版本或身份范围纳入检索、上下文和答案缓存策略,避免不同租户或角色共享敏感结果;对整个响应作权限复核,而不只是对引用标签作复核。
这个 Python 片段只是静态快照实验,没有解决权限变化与响应发送之间的竞态。应在系统中定义复核边界和失效机制,不能从一次函数调用推导出完整的访问控制保证。
检索材料仍然是不可信输入
片段中可能出现“忽略上面的规则”等指令。结构化 JSON、分隔符与引用检查都不是提示注入的完整防护;字面引用检查甚至会允许引用这段指令,只要它真实存在。OWASP 提示注入资料说明外部内容也可能成为注入入口。
工程上应限制工具权限与副作用,将外部文本作为证据而非更高优先级指令,验证结构化输出,建立注入测试集,并隔离高风险操作。本篇只实现输入组装与引用回查,不宣称已经防住提示注入。
实践自测 引用回查通过后还能发生什么错误
把示例答案改成“缓存每 30 分钟必然自动刷新”,保留真实引文。字面检查依然可能通过,这正好说明它没有读取、理解答案断言。
分别为身份伪造、上下文越权、伪造引文、错误推断与过期来源建立测试,不要用一个“引用准确率”合并所有失败。
回归指标应分别报告
| 指标 | 本例或生产检查对象 |
|---|---|
| 未授权入选数 | 必须为 0,覆盖跨租户、缺 ACL、角色不匹配 |
| 预算超限数 | 按实际序列化结果计量,必须为 0 |
| 身份冲突拒绝数 | 同一 id 的不同正文、文档或版本不能静默覆盖 |
| 引用结构通过率 | 不等于语义支持率,也不是模型真实效果 |
| 证据覆盖率 | 需要独立标注相关证据,本例没有估计 |
| 空上下文与拒答表现 | 不能通过随意补一句话假装任务成功 |
预算组装每次重新序列化已选列表,最坏存在二次累计成本;这是可读性优先的小实验,不是高吞吐实现。优化时可以增量计算字节增量,但必须覆盖 JSON 转义、数组逗号与 UTF-8,保持与完整序列化一致。
与已有 AI 工程文章接起来
- embedding 与语义检索解释近邻召回与真实相关性为何不同。
- 混合检索与重排解释多路排名、候选窗口与重排截断。
- RAG 知识库评测把失败拆到召回、排序、生成与拒答。
- AI 工程专题提供完整路线与可筛选文章库。
资料核对日期为 2026-10-05。本文中的缓存、重试、权限与分数都是原创合成示例,不能当作任何真实系统的配置或效果数据。