首页 / 现场

现场

企业知识库接入大模型后的一次权限事故

服务器指示灯

检索把不该看见的纪要召回了。问题不在模型会说话,而在索引没有继承文档权限。

一家公司把内部知识库接上大模型问答后,普通员工在对话里读到了高管会议纪要。事故复盘写得很短:索引没有继承文档权限。模型只是把检索到的片段说出来。真正的漏洞在切片和向量库。

原来的网盘权限模型,没有跟着进 RAG 管道。这不是一次“提示词注入”的奇闻,而是一次更常见的工程遗漏:把文档当成了纯文本,把人当成了同一权限等级。

召回先于生成

大多数问答系统先检索,再让模型组织语言。如果检索阶段已经越过 ACL,后面的安全提示词拦不住。日志显示,敏感片段在事故被报告前,已经被召回过多次,只是没人注意到对话记录。

因此治理必须前移。能进索引的文档,必须带着主体、角色和过期时间。切片时要把权限键一并写入元数据,查询时先过滤再计算相似度。顺序反了,后面全是补丁。

现场怎么停

安全团队先关掉问答入口,再按人员组重建索引。他们发现测试环境用的是全量文档、生产环境复用了同一套向量库。这是第二处错误:演示方便,等于把生产权限打穿。

恢复上线的条件被写成清单:权限是否随切片走、提示词能否套出系统提示、对话日志谁可以搜、测试索引与生产索引是否隔离。模型能力越强,这一张清单越不能省。

权限必须进检索,而不是进客套话

下面这段示例演示最小可用的过滤:每个切片带上 acl,查询时用当前用户的角色做交集。没有交集的片段,根本不应出现在提示词里。

from dataclasses import dataclass

@dataclass
class Chunk:
    text: str
    source: str
    acl: set  # 可读该切片的角色,如 {"finance", "exec"}

def visible_chunks(chunks: list[Chunk], roles: set[str]) -> list[Chunk]:
    return [c for c in chunks if c.acl & roles]

def build_prompt(question: str, chunks: list[Chunk], roles: set[str]) -> str:
    allowed = visible_chunks(chunks, roles)
    if not allowed:
        return "没有你有权限查看的资料。请改走工单,不要追问原文。"
    context = "\n\n".join(f"[{c.source}] {c.text}" for c in allowed)
    return (
        "只根据下列资料回答。资料未覆盖的内容必须说不知道。\n\n"
        f"{context}\n\n用户问题:{question}"
    )

chunks = [
    Chunk("Q3 利润草案……", "exec-memo.pdf", {"exec"}),
    Chunk("报销标准见第 3 页。", "hr-policy.pdf", {"staff", "finance", "exec"}),
]
print(build_prompt("利润率是多少?", chunks, roles={"staff"}))

这段代码解决不了所有问题。它不管文档更新后的缓存、不管管理员角色被错误授予、不管模型把两个低敏感片段拼出高敏感结论。它只强调一件事:看不见的东西,不要送进上下文。事后再让模型“不要泄露”,是靠不住的。

之后的清单

上线检查还应包括:对话日志是否按同样的 ACL 检索、运营人员是否能导出别人的问答、供应商的评测集是否混入了真实纪要。把知识库交给模型,等于把档案室的钥匙复制了一把给软件。钥匙的管理,必须按档案室的标准来,而不是按聊天机器人的标准来。