Skip to content

2.4.1 为什么知识库不只是“存内容”,还要管理权限和时效性? ​

因为在真实系统里,知识从来不是脱离场景存在的。

一份内容是否应该被看到、何时应该生效、何时应该失效,往往和内容本身同样重要。

只“存内容”会带来什么问题 ​

如果知识库只是把文本存下来,而不管理权限和时间,很快就会出现三类风险:

1. 看到不该看的内容 ​

不同用户、角色、部门、租户,往往能访问的内容并不一样。

如果系统只按“相关性”返回,而不按权限过滤,就可能把本不该展示的资料直接送进答案。

2. 用到过期内容 ​

很多业务知识不是永久有效的,比如:

  • 产品规则
  • 活动政策
  • 操作流程
  • 内部制度

如果旧内容没有被标记失效,系统就可能拿历史资料回答当前问题。

3. 新旧版本互相冲突 ​

如果知识库里同时存在多个版本,但系统没有明确版本状态,就容易:

  • 把旧版本检索出来
  • 把多个版本混着给模型
  • 让答案出现自相矛盾

为什么 RAG 特别需要这层治理 ​

因为 RAG 的回答不是直接从数据库里原样返回,而是经过检索、拼接和生成。

一旦错误内容先进入上下文,后面模型再自然地组织语言,就会让问题显得更“像正确答案”。这比单纯搜索返回一条错误结果更隐蔽。

权限和时效性其实都是“知识边界” ​

从系统视角看,权限和时效性本质上都在回答一个问题:

这条知识,是否应该在当前用户、当前时间、当前上下文里被使用?

所以知识库真正管理的,不只是文本,而是文本的使用边界。

一个更准确的理解方式 ​

知识库至少要同时管理三层内容:

  • 知识正文:具体说了什么
  • 访问边界:谁可以看
  • 时间边界:什么时候有效

少了后两层,前面的内容再丰富,也很难支撑生产级 RAG。

真正落地时,通常要管理哪些字段 ​

如果要把“权限”和“时效性”真正做进知识库,通常至少要给文档或 chunk 补上下面几类字段:

  • doc_id:稳定文档 ID
  • chunk_id:稳定片段 ID
  • source:原始来源
  • version:版本号
  • status:draft、active、deprecated、expired
  • effective_from:生效时间
  • effective_to:失效时间
  • tenant_id:租户边界
  • owner_org:所属组织或部门
  • visibility:可见角色、组或 ACL

这些字段的作用,不只是“记录信息”,而是为了支撑后面的关键动作:

  • 检索时先做边界过滤
  • 排序时优先当前有效版本
  • 生成时展示正确来源
  • 更新时替换旧片段
  • 审计时追踪使用记录

如果你觉得字段太多,可以先抓住一个原则:字段不一定一步到位,但必须能回答三个问题:

  1. 这是谁的内容?
  2. 现在还能不能用?
  3. 谁可以用?

只要这三个问题回答不出来,知识库就还没有真正进入“可治理”状态。

权限和时效性在系统里到底怎么生效 ​

很多团队会把这些字段存进数据库,但没有真正用起来。更常见、也更实用的落地方式通常是下面这条链路:

  1. 接入阶段写入边界字段。 也就是文档刚进入系统时,就把 tenant_id、status、effective_from、effective_to、visibility 这类字段补齐。

  2. 切块阶段把边界继承到 chunk。 如果文档级元数据没有跟着 chunk 一起下沉,到了检索层就无法真正过滤。

  3. 检索阶段按用户上下文和当前时间过滤。 比如只召回 status == active、当前时间处于有效期内、且当前用户角色允许访问的 chunk。

  4. 重排和上下文构造阶段继续保留边界。 不是说召回时过滤过一次就结束了。如果后面还有重排、合并、引用抽取,也要保留这些字段,避免中间层把边界丢掉。

  5. 答案返回后保留引用和命中原因。 这样你才能知道这次回答为什么用了某条知识,也能在用户投诉“这不是当前规则”时追溯原因。

这条链路里最容易出问题的点,不是“不会写过滤条件”,而是元数据在某一层掉了。比如原始库里有 effective_to,到了向量库里没写进去;或者召回结果里带了 visibility,到 prompt 组装时又被清掉了。只要丢一次,边界就失效了。

一套最小可用的治理思路 ​

如果先不追求复杂系统,最小也应该把治理拆成四步:

1. 接入时补边界 ​

内容进入知识库时,不只抽正文,还要补上:

  • 来源
  • 版本
  • 时间
  • 权限
  • 租户

这一步漏掉了,后面往往很难补救。

2. 索引时保边界 ​

这些字段不能只存在原始数据库里,还要跟着 chunk 一起进入检索层。否则到了真正召回时,系统仍然不知道该怎么过滤。

3. 查询时用边界 ​

用户发起查询时,系统要把下面这些上下文一起带进去:

  • 当前用户身份
  • 当前租户
  • 当前时间
  • 当前会话环境

然后在召回前或召回时就做过滤,而不是最后展示时再补。

4. 回答后可追踪 ​

最终返回的答案最好能知道:

  • 用了哪些 chunk
  • 这些 chunk 属于哪个版本
  • 为什么当前用户能看到
  • 当时命中的权限规则是什么

这会直接决定系统后面是否可排查、可审计、可回滚。

状态和时间边界最好不要只靠自然语言理解 ​

很多制度文档会在正文里写“本规则自 2026 年 4 月 1 日起生效”或“旧版同时废止”。如果你只把这句话存成正文,不再额外结构化,系统就很难稳定处理时效性。

更稳的做法通常是:

  • 把生效时间和失效时间提取成明确字段
  • 把版本状态显式写成 draft、active、deprecated、expired
  • 同一份规则切换版本时,只允许一个主版本处于 active
  • 如果旧版本需要保留审计记录,也应当退出默认检索集合

这样做的意义不是“字段更漂亮”,而是避免系统把“看得懂正文”误当成“理解了版本规则”。模型可能能读懂一句话,但系统治理不能依赖模型每次都读对。

一个更贴近实际的查询过滤示意 ​

python
from datetime import datetime, timezone


def is_active_now(metadata: dict, now: datetime) -> bool:
    if metadata["status"] != "active":
        return False

    start = datetime.fromisoformat(metadata["effective_from"].replace("Z", "+00:00"))
    end = metadata["effective_to"]
    if now < start:
        return False
    if end is not None and now > datetime.fromisoformat(end.replace("Z", "+00:00")):
        return False
    return True


def can_user_access(metadata: dict, tenant_id: str, role_ids: list[str]) -> bool:
    if metadata["tenant_id"] != tenant_id:
        return False
    return any(role in metadata["visibility"] for role in role_ids)


def filter_chunks(chunks: list[dict], tenant_id: str, role_ids: list[str]) -> list[dict]:
    now = datetime.now(timezone.utc)
    return [
        chunk
        for chunk in chunks
        if is_active_now(chunk["metadata"], now)
        and can_user_access(chunk["metadata"], tenant_id, role_ids)
    ]

这里最值得注意的不是代码细节,而是两个判断必须同时成立:

  • 这条知识现在有效
  • 这个用户现在有权使用

少了任何一个,回答都可能错。

一个最小元数据示意 ​

python
chunk = {
    "chunk_id": "refund-policy-v3#chunk-08",
    "text": "高风险订单退款需要人工复核。",
    "metadata": {
        "doc_id": "refund-policy-v3",
        "source": "/policy/refund",
        "version": "v3",
        "status": "active",
        "effective_from": "2026-04-01T00:00:00Z",
        "effective_to": None,
        "tenant_id": "tenant-a",
        "owner_org": "risk-control",
        "visibility": ["risk-team", "support-manager"]
    }
}

这里真正重要的不是字段名字本身,而是这些边界已经被显式表达出来。只有这样,后面系统才能“按边界使用知识”,而不是“按相关性盲目用知识”。

为什么很多团队会在这里吃亏 ​

因为最开始做 Demo 时,大家通常只有一个默认用户、一个默认租户、一套默认资料,所以权限和时效性看起来像“以后再说”。

但一旦进入真实环境,问题会迅速暴露:

  • 不同部门可见内容不同
  • 同一制度有多个版本
  • 同一知识对不同租户边界不同
  • 同一问题在不同时间应该引用不同规则

这时如果知识库里没有这些边界字段,后面就只能用外层逻辑勉强兜底,通常既不稳也不可审计。

从 0 开始时,一个更实用的实施顺序 ​

如果你现在的知识库还很早期,比较现实的推进方式通常是:

  1. 先统一元数据模型。 至少把 doc_id、version、status、tenant_id、visibility、effective_from、effective_to 统一下来。

  2. 再改接入流程。 任何文档如果缺少这些关键字段,就不要直接入库。宁可先进入待处理队列,也不要带着不完整边界进入线上检索。

  3. 再改检索过滤。 先让检索默认只返回当前有效、当前租户、当前角色可见的内容。

  4. 再补追踪和审计。 至少能看到这次回答用了哪些 chunk、它们是什么版本、为什么会被命中。

  5. 最后再去做更复杂的策略。 比如按组织继承权限、按政策版本做灰度、按场景切不同知识范围。这些都应该建立在前面的基础边界已经稳定之上。

怎么判断你的知识库治理还没到位 ​

如果你遇到下面这些现象,通常说明权限和时效治理还没有真正做进去:

  • 只能靠文档标题或正文关键字判断哪个版本是新的
  • 同一问题上午和下午答得不一样,但查不出用了哪版内容
  • 用户反馈“这条规则我没权限看”,系统却无法解释为什么会命中
  • 文档更新以后,旧内容还会继续被召回很久
  • 排查问题时只能看到最终答案,看不到底层 chunk 和元数据

这些现象看起来分散,本质上都说明系统没有把“知识边界”做成明确可执行的规则。

一句话总结 ​

知识库不只是“存内容”,还要管理权限和时效性,因为 RAG 真正使用的不是抽象知识,而是“在当前用户和当前时间下,允许被使用的知识”。