W3D3 学习手册 1.全景2.ACL模型3.检索过滤4.失效删除5.增量 6.死信7.一致性8.ES过滤9.自查询10.运维 11.评测集达标①达标②自测速记

FDE W3D3 学习手册 · 权限和文档治理(Chunk 级 ACL / 失效删除 / 死信重试)

W3 Day3 · A 级(必须掌握,面试核心)· 5h · 学完能讲透"企业 RAG 的权限闸与文档生命周期:谁能看、旧了怎么退场、错了怎么兜底"

本日定位:W3D1 讲了数据底座(元数据/血缘),W3D2 讲了检索执行(混合+Rerank),W3D3 落到"治理与安全"——企业 RAG 不只是"能搜到",更要"搜得对的人、旧的下得去、错的兜得住"。这呼应你做医疗数据治理时最熟悉的"权限 + 一致性 + 重试"三件套。
学完能回答:① Chunk 级 ACL 怎么实现、检索时怎么过滤;② 文档更新/删除时索引一致性怎么保证、死信和重试怎么设计;③ 评测集怎么扩到 30 个 Cases。
使用方法:通读原理 → 重点看「🔧工程含义」「🎤面试话术」「⚠易错点」→ 做自测清单 → 配合《FDE-W3D3-评测题.md》。选中不熟的词可标注(左下★重要 / 右下📌待查)。

一、权限与文档治理全景:企业 RAG 的"安全闸"

消费级 RAG(如公开文档问答)不用管权限,但企业 RAG 直接关系商业机密与个人隐私:一份《高净值客户特药福利》手册,普通客服不该看到;一个客户的理赔记录,其他客户绝不可检索。文档治理则保证"知识库不是一潭死水"——条款修订、手册下线、解析失败都要有严谨处理,否则检索结果会混入过期/越权内容。

治理三大命题:① 权限(谁该看)→ Chunk 级 ACL;② 生命周期(旧的怎么退场)→ 失效/删除/增量更新;③ 可靠性(错的怎么兜底)→ 重试/死信/一致性校验。三者共同决定企业 RAG 的"可信边界"。
工程含义:权限与治理是"合规红线"。医疗行业受《个人信息保护法》、保险监管约束,越权泄露客户健康信息后果严重。这不是"锦上添花",是上线前置条件。你的数据治理经验在这里直接变现。
企业 RAG 和消费级最大的区别就是"治理"——谁能看、旧的怎么下、错的怎么兜。具体三件事:Chunk 级 ACL 控制权限,文档失效/删除/增量更新管生命周期,重试+死信+一致性校验保可靠性。医疗场景里越权泄露客户健康信息是直接违规,所以这是上线前置条件,不是可选项。
① "先上线后补权限"是高危做法——一旦越权检索发生就是事故。② 权限不能只做在应用层"展示时遮罩",必须在检索层就过滤,否则被绕过/被向量库直查就泄露。③ 治理和 W3D1 的元数据强绑定:ACL、状态、版本都依赖 chunk 上挂的元数据字段。

二、Chunk 级 ACL 数据模型设计

权限要到 Chunk 级而非 Document 级——一份 PDF 里可能前 10 页公开、后 5 页仅限理赔审核岗。所以 ACL 要挂在每个 chunk 上。

2.1 三种常见授权维度

维度示例chunk 上字段
用户(user)仅保单持有人本人allowed_users: ["u_1001"]
角色(role)理赔审核岗、客服岗allowed_roles: ["claims_auditor"]
部门(dept)华东分公司、特药事业部allowed_depts: ["special_drug"]

2.2 chunk 上的 ACL 字段设计

{
  "chunk_id": "c_8f3a...",
  "content": "高净值客户特药年度额度 50 万...",
  "acl": {
    "allowed_roles": ["claims_auditor", "vip_advisor"],
    "allowed_depts": ["special_drug"],
    "allowed_users": [],
    "visibility": "internal"        // public / internal / restricted
  }
}

2.3 设计要点

工程含义:ACL 到 chunk 级是"最小授权原则"的落地。Document 级太粗(一份手册里混着公开与机密),用户级太细(每条都配不现实)。角色+部门二维组合 + 默认最小权限,既安全又可控。
权限要做到 Chunk 级,因为一份 PDF 里可能前几页公开、后面几页只有理赔审核岗能看。ACL 挂在每个 chunk 上,用"角色+部门+显式用户"三个维度,检索时命中任一即放行(OR 组合)。默认最小权限——没标的 chunk 当内部处理,防止漏配泄露。chunk 默认继承文档级 ACL,敏感段落再单独收紧。
① ACL 只做用户级会维度爆炸(每 chunk 列全用户),必须上"角色/部门"抽象。② 默认权限设成"公开"是灾难——漏配就泄露,必须默认最小(internal/restricted)。③ 继承关系要清晰:chunk 覆盖 document 时,更新 document ACL 不会自动同步到已覆盖的 chunk,要约定清楚。④ ACL 字段也要版本化,权限变更要可追溯。

三、检索时的权限过滤(用户 / 角色 / 部门)

权限过滤必须发生在检索阶段(预过滤),确保越权 chunk 根本不进候选集。以 ES 为例:

def build_acl_filter(user):
    return {
      "bool": {
        "should": [
          {"terms": {"acl.allowed_roles": user.roles}},
          {"terms": {"acl.allowed_depts": user.depts}},
          {"terms": {"acl.allowed_users": [user.id]}}
        ],
        "must_not": [ {"term": {"acl.visibility": "restricted_external"}} ],
        "minimum_should_match": 1
      }
    }

# 检索:ACL 过滤 与 BM25/向量 召回 在 bool 内 AND
query = {
  "bool": {
    "filter": [ build_acl_filter(current_user) ],     # 权限硬约束
    "should": [ {"match": {"content": q}},
                {"knn": {"field":"embedding","query_vector":qv,"k":50}} ]
  }
}

3.1 关键原则

工程含义:权限过滤是" WHERE 条件 + 安全闸"。用 filter 上下文既能保证不评分不影响召回质量,又能被 ES 缓存提速。服务端取身份是关键——任何"客户端声明角色"的方案都能被绕过。
权限过滤必须放检索阶段用预过滤,越权 chunk 根本不进候选。ES 里用 bool.filter 挂 ACL 条件(角色/部门/用户命中其一即放行,minimum_should_match=1),filter 不评分还能被缓存提速。身份必须从服务端会话取,不能信前端传的角色——否则能被伪造绕过。传给 LLM 的也必须是已过滤的 chunks。
① 把权限当 must 会影响评分且不被缓存,应用 filter 上下文。② "前端传 role"会被篡改绕过,必须从服务端会话/网关取身份。③ 只过滤检索不够——LLM 拿到的上下文也要是过滤后的,且最终回答不能"顺带引用"越权 chunk。④ 测试要覆盖"越权用户应召回 0 条"的负向 case。

四、文档失效、删除与索引失效处理

4.1 三种"下线"语义

操作含义索引处理
失效(deprecate)条款过期但保留审计chunk.status=deprecated,检索过滤排除
删除(delete)彻底移除物理删除向量 + 元数据,记删除日志
替换(replace)新版上线旧版退场新 chunk active + 旧 chunk deprecated(原子切换)

4.2 软失效优于硬删除

医疗合规要求"可追溯",所以优先 软失效(status 标记)而非物理删。只有确认无审计价值且合规允许才硬删。

下线请求 → 校验权限/合规 → 标记 status=deprecated(软)或 物理删除(硬) → 同步更新"文档→chunk"索引映射 → 触发增量重算(如影响召回统计)
工程含义:失效/删除不是"删库跑路",是受控的状态迁移。软失效保留审计链、支持回滚;物理删除要留删除日志与备份点。替换要"原子"——新旧切换瞬间完成,避免用户一会儿看到旧版一会儿新版。
文档下线分三种:失效(保留审计,标 deprecated)、删除(物理移除+记日志)、替换(新版 active、旧版 deprecated 原子切换)。医疗合规优先软失效,因为要可追溯、可回滚。关键是所有下线都是"受控状态迁移",不是直接删,而且要同步更新文档到 chunk 的索引映射,避免脏数据。
① 直接物理删而不留日志,审计追溯断链,监管质疑时无法自证。② 替换非原子会导致用户短暂看到混合版本,要用"先标新 active 再标旧 deprecated"或版本指针切换。③ 失效后忘了同步更新"文档→chunk"映射,会有孤儿 chunk 仍被索引。④ 删除操作要有二次确认与权限校验,防误删。

五、增量更新与一致性保证

5.1 增量更新流程

文档小改(如某页条款数字调整)不必全量重摄取,只更新受影响 chunk:定位变更 chunk → 重新解析该块 → 重算 embedding → 原子替换旧向量。

5.2 一致性三要素

原文件 ⇄ 解析结果 ⇄ 向量索引 三者必须一致 改其一 → 要么级联更新另两者,要么标记不一致待修复
工程含义:增量更新省成本(不必全量重算),但引入"部分更新导致不一致"的风险。必须有"变更→级联更新→一致性校验"的闭环,否则索引慢慢腐化(W3D1 说的"数据腐烂")。
小改不必全量重摄取,只更新受影响的 chunk:定位变更块→重解析→重算向量→原子替换。但增量更新要保证"原文件、解析结果、向量索引"三者一致——改一个要级联改另外两个,否则索引会慢慢腐烂。还要校验版本一致、计数一致(文档 chunk 数 = 索引里 active 数)、引用一致。
① 增量只更新了向量却忘了更新 chunk 的 doc_version,导致版本漂移(呼应 W3D1)。② 部分更新中途失败会留"半成品"chunk,要有事务/补偿。③ 计数不一致若不校验,检索可能命中已失效 chunk。④ 增量与全量并存时要防冲突(同一 doc 同时跑两种任务)。

六、失败重试与死信队列(DLQ)

6.1 重试策略

策略做法适用
立即重试失败马上重试 1~2 次(短退避)瞬时故障(网络抖动、限流)
指数退避等待 2^n 秒后重试依赖服务繁忙
死信队列超过最大重试进 DLQ 待人工/定时处理持久失败(格式错、权限错)

6.2 死信队列设计

def handle_ingest(task):
    for attempt in range(MAX_RETRY := 5):
        try:
            run_pipeline(task); return mark_success(task)
        except TransientError:
            sleep(backoff(attempt)); continue
        except PermanentError as e:
            send_to_dlq(task, reason=str(e)); return   # 不无限重试
    send_to_dlq(task, reason="exceeded retries")
死信记录至少含:task_id、失败阶段、错误类型、原始输入引用、重试次数、首次/最后失败时间、payload 快照。便于排查与重放。
工程含义:重试不能无限——持久错误(如解析器崩、格式不支持)重试 N 次也失败,占着 worker 还产生错误结果。死信队列把"处理不了的"隔离出来,告警+人工介入,保证主流程不被脏任务拖死。这正是你 ETL 里"失败任务进异常表"的翻版。
摄取失败要分瞬时和持久:瞬时故障(网络抖、限流)用指数退避立即重试;持久错误(格式不支持、权限错)重试也没用,超过最大次数就进死信队列(DLQ)隔离,告警+人工处理。死信记录要带 task_id、失败阶段、错误类型、重试次数、payload 快照,方便重放。这和你 ETL 里"失败任务进异常表"一模一样。
① 无限重试持久错误会拖垮整个摄取管道(worker 被占满)。② 死信不能"进了就忘"——要有监控告警和定期重放机制,否则失败任务堆积成知识库缺口。③ 退避要加 jitter,避免大量任务同时重试造成"重试风暴"。④ DLQ 也要有容量上限与老化策略。

七、原文件 / 解析结果 / 索引一致性校验

三份数据(原始文件、解析后的结构化文本、向量索引)可能因中途失败而漂移,需要定期/事件触发校验。

校验项方法不一致时
文件↔解析重算 file_hash 比对,抽样全文比对重新解析该 doc
解析↔索引每个 chunk 应有对应向量;向量数=active chunk 数补算缺失向量
索引↔元数据chunk 的 status/version 与实际一致修正状态/版本
文档↔chunk 映射doc.chunk_count = 索引中该 doc 的 chunk 数清理孤儿/补建
def consistency_check(doc_id):
    src_hash = sha256(load_raw(doc_id))
    parsed  = load_parsed(doc_id)
    chunks  = es.count(filter={"document_id": doc_id, "status":"active"})
    vectors = vector_store.count(document_id=doc_id)
    assert parsed.file_hash == src_hash, "解析与源不一致"
    assert chunks == vectors, "chunk 数与向量数不一致"
    # 不一致 → 入修复队列
工程含义:一致性校验是"数据可信的守门员"。它把 W3D1 的元数据(hash/version/status)真正用起来——定期扫一遍,发现"有 chunk 没向量""有向量版本旧了"就自动进修复队列。没有它,索引腐化是静默的。
原文件、解析结果、向量索引三份数据会因中途失败漂移,要定期校验:文件↔解析用 hash 比对,解析↔索引看 chunk 数和向量数是否相等,索引↔元数据看 status/version 是否一致,文档↔chunk 映射看计数对不对。不一致就自动进修复队列重算。这把 W3D1 设计的 hash/version/status 真正用起来了,防止索引静默腐化。
① 一致性校验要"可修复"而不只是"报警"——发现缺失向量要能自动补算,否则只是知道坏了修不了。② 全量校验成本高,要增量(只查变更 doc)+ 定时全量兜底。③ 校验本身也可能失败/超时,要设超时与重试,别让校验任务卡死。④ 修复过程要保证幂等,重复跑不产生脏数据。

八、ES Metadata Filter 实现 ACL 过滤

Elasticsearch 的 terms 查询是 ACL 过滤的利器——用服务端用户身份构造过滤条件。

# 服务端取身份(不从前端信任)
user_roles = get_roles_from_session(request)     # ["claims_auditor"]
user_depts = get_depts_from_session(request)     # ["special_drug"]
user_id    = get_user_id(request)                # "u_1001"

acl_filter = {
  "bool": {
    "should": [
      {"terms": {"acl.allowed_roles": user_roles}},
      {"terms": {"acl.allowed_depts": user_depts}},
      {"terms": {"acl.allowed_users": [user_id]}}
    ],
    "minimum_should_match": 1
  }
}
# 与召回 AND:{ "bool": { "filter": [acl_filter], "should": [bm25, knn] } }
官方文档:Elasticsearch Terms Query(ACL 过滤核心)。多值字段用 terms 做"命中集合任一即匹配",正是角色/部门授权的语义。
工程含义:ES terms 查询天生匹配"集合相交",角色/部门授权直接用。配合 W3D2 的 bool+knn 混合检索,ACL 作为 filter 上下文零评分、可缓存。关键是身份从会话取,条件在服务端构造——这是权限不外泄的底线。
ES 的 terms 查询天生做"集合相交"——用户角色集合和 chunk 的 allowed_roles 集合任一相交即放行,正好是角色/部门授权的语义。ACL 作为 bool.filter 挂上,和 BM25+向量召回 AND,filter 不评分还能缓存。身份必须从服务端会话取,过滤条件在服务端构造,绝不能信前端。
① terms 查询要求字段是 keyword 类型,ACL 字段若设为 text 会被分词失效。② 空用户集合(如 roles=[])配合 minimum_should_match=1 会"全不匹配"——要处理"公开 chunk"用 visibility=public 单独放行。③ 过滤条件拼接要用参数化,防注入(尤其用户 id 进查询)。④ 别把 ACL 逻辑只写在应用层 if 判断,检索层必须过滤。

九、LangChain 自查询检索器(Self-Query Retriever)

自查询检索器让 LLM 从自然语言问题里自动抽取结构化过滤条件(metadata filter),把"用户口语"转成"ES 过滤+语义查询"。

from langchain.retrievers import SelfQueryRetriever
from langchain.retrievers.self_query.base import AttributeInfo

metadata_field_info = [
    AttributeInfo(name="acl.allowed_roles", description="可访问角色", type="list[string]"),
    AttributeInfo(name="doc_version", description="文档版本", type="string"),
    AttributeInfo(name="section", description="章节", type="string"),
]
retriever = SelfQueryRetriever.from_llm(
    llm, vectorstore, "保险知识库", metadata_field_info,
    search_kwargs={"k": 8}
)
# 用户问"理赔审核岗能看的靶向药报销范围" → LLM 生成 filter: allowed_roles contains claims_auditor
docs = retriever.get_relevant_documents("理赔审核岗能看的靶向药报销范围")
官方文档:LangChain Self-Query Retriever —— 用 LLM 把自然语言转成结构化 filter + 检索。
工程含义:自查询把"用户描述的权限/范围"自动转成 metadata 过滤,减少手写规则。但它生成的 filter 必须再和服务端真实 ACL 做交集——不能让 LLM 决定"用户能看到什么"(否则被 prompt 注入绕过权限)。自查询是"用户意图"的便利层,最终权限仍以服务端 ACL 为准。
LangChain 自查询检索器用 LLM 从自然语言里抽结构化过滤条件,比如用户问"理赔审核岗能看的靶向药范围",它自动生成 allowed_roles contains claims_auditor 的 filter。很方便,但生成的 filter 必须再和服务端真实 ACL 取交集——绝不能让 LLM 决定用户能看到什么,否则被 prompt 注入就能越权。自查询只是"意图便利层",权限底线还是服务端 ACL。
① 自查询生成的 filter 不能代替权限校验,必须和服务端 ACL 取交集,防 prompt 注入越权。② LLM 抽 filter 可能抽错字段/值,要有字段白名单校验,非法字段直接拒绝。③ 自查询增加一次 LLM 调用延迟,高频场景要缓存或降级为规则。④ metadata_field_info 描述要准,否则 LLM 抽错。

十、文档治理的运维与监控

监控项含义告警
ACL 命中率检索中被权限过滤掉的比例异常高/低都查
越权访问尝试用户请求了无权限 doc立即告警(安全)
孤儿 chunk索引里有但文档已删/失效>0 即修复
死信堆积DLQ 任务数>0 即处理
软失效占比deprecated chunk 比例过高说明清理滞后
工程含义:治理不是"配一次就完",要持续可观测。越权访问尝试是安全事件级告警;孤儿 chunk 和死信堆积是数据腐烂信号。医疗场景建议定期出"权限审计报告"给合规团队。
治理要持续可观测:监控 ACL 命中率、越权访问尝试(安全事件级告警)、孤儿 chunk(索引里有但文档已删)、死信堆积、软失效占比。越权访问要立即告警,孤儿 chunk 和死信大于 0 就修。医疗建议定期出权限审计报告给合规团队——这就是数据治理的常态动作。
① 越权访问只记日志不告警,等于没防。② 孤儿 chunk 不清理会持续污染检索,要自动修复任务。③ 软失效 chunk 长期不物理清理会拖慢索引、占存储。④ 审计报告要能回答"某用户此刻能看到哪些文档",这是合规的硬要求。

十一、评测集扩展到 30 Cases 的策略

W3D1 你有 10 个 Golden Case,W3D3 治理主题要把评测集扩到 30,重点是补权限与治理类 case

10 个(基础问答) → +10 权限/ACL case → +10 失效/删除/一致性/重试 case → 共 30
新增类别示例测什么
越权负向普通客服检索"高净值特药额度"应召回 0ACL 过滤生效
权限正向理赔审核岗检索同内容应命中角色授权正确
失效一致性手册失效后旧条款不应再被召回status 过滤
死信降级解析失败的手册不应污染检索失败隔离
工程含义:评测集要随功能长。治理功能(权限/失效/重试)不上评测,就等于"没人验证的安全闸"。每加一个治理能力就补对应 case,且权限类必须以"负向 case(应召回 0)"验证——只测正向会漏掉越权漏洞。
W3D1 的 10 个基础 case,到 W3D3 扩到 30:加 10 个权限/ACL case(越权应召回 0、授权应命中),加 10 个失效/删除/一致性/重试 case(手册失效后旧条款不应召回、解析失败不污染检索)。关键是权限类必须包含"负向 case"——只测正向会漏掉越权漏洞,那才是最危险的。
① 评测集只加正向 case 不设越权负向 case,等于没测权限。② 失效/删除 case 要验证"旧版本确实不再被召回",否则软失效没生效也发现不了。③ 死信 case 要确认失败任务不污染检索结果。④ 评测集版本要同步更新(eval_v2),前后对比才有意义(呼应 W3D1)。

十二、面试达标线①:Chunk 级 ACL 实现与检索过滤

问题答案要点
ACL 挂哪挂每个 chunk(非 document 级),角色+部门+用户三维,默认最小权限
检索时怎么过滤bool.filter 上下文 + terms 查询,命中任一即放行;身份从服务端会话取
为什么检索层过滤应用层遮罩可被绕过/直查向量库泄露;过滤在检索阶段彻底隔离
自查询注意LLM 生成的 filter 须与服务端 ACL 取交集,防注入越权
核心一句话:ACL 挂到 chunk 级(角色+部门+用户三维、默认最小权限),检索时用 ES bool.filter + terms 查询在服务端做预过滤,身份从会话取不信任前端;自查询检索器生成的 filter 必须再和真实 ACL 取交集。权限过滤必须在检索层,不能只做展示遮罩——否则被直查向量库就泄露。

十三、面试达标线②:更新/删除一致性与死信重试设计

更新/删除:软失效(status=deprecated)优先 → 同步文档→chunk 映射 → 替换要原子 一致性:原文件⇄解析⇄向量 三份一致,定期校验+自动修复队列 重试:瞬时故障指数退避,持久错误进 DLQ(带 task_id/阶段/错误/重试次数),告警+重放
  1. 文档下线三语义:失效(保留审计)/删除(物理+日志)/替换(原子切换新旧)。
  2. 一致性三要素:版本一致、计数一致(doc chunk 数=索引 active 数)、引用一致。
  3. 增量更新:只更新受影响 chunk,级联更新另两份数据,防索引腐化。
  4. 死信重试:区分瞬时/持久错误,持久错误超重试进 DLQ 并告警重放,主流程不被拖死。

十四、W3D3 自测清单

十五、高频面试题速记卡

Q:权限为什么要做到 Chunk 级而不是 Document 级?
一份 PDF 里常混着公开与机密段落,Document 级太粗会过度暴露或过度限制。Chunk 级遵循最小授权,角色+部门+用户三维组合。
Q:权限过滤放检索层还是应用层?
必须检索层(预过滤),越权 chunk 根本不进候选。应用层遮罩可被直查向量库绕过,是泄露隐患。
Q:用户身份能不能从前端传?
不能。必须从服务端会话/网关取,否则角色可被伪造,ACL 形同虚设。
Q:文档下线用软失效还是硬删除?
医疗合规优先软失效(status=deprecated,保留审计、可回滚);仅确认无审计价值才硬删并留日志。
Q:为什么检索结果会混入旧版本条款?
替换非原子或失效后没同步文档→chunk 映射,导致 deprecated chunk 仍被召回。要原子切换+一致性校验。
Q:死信队列解决什么问题?
持久错误重试也没用,无限重试会拖垮管道。DLQ 隔离失败任务+告警+重放,主流程不被脏任务卡死。
Q:原文件、解析、向量三者怎么保证一致?
定期/事件校验:hash 比文件↔解析、chunk 数=向量数、status/version 一致、文档↔chunk 计数一致,不一致进修复队列。
Q:LangChain 自查询检索器能决定权限吗?
不能。它只把自然语言转成 filter 意图,生成的 filter 必须和服务端真实 ACL 取交集,防 prompt 注入越权。
Q:ES terms 查询在 ACL 里干嘛用?
做"集合相交"——用户角色/部门集合与 chunk 的 allowed 集合任一相交即放行,正是授权语义;字段须 keyword 类型。
Q:评测集扩到 30 为什么要补负向 case?
权限类必须含"越权应召回 0"的负向 case,只测正向会漏掉越权漏洞——那是最危险的。
FDE W3D3 学习手册 · 权限和文档治理(面试级)· 配合《FDE-W3D3-评测题.md》自测
📌 待查★ 重要