前两篇我们已经做了两件事:
- 第 1 篇:盘点知识源,设计分类体系。
- 第 2 篇:把原始文档清洗成
processed/chunks.jsonl。
现在的问题是:这些知识块能不能直接丢给 RAG 用?
不能。
企业知识库至少还差三道闸:
- 权限:销售不能看到财务敏感制度,普通客服不能看到 VIP 客户补偿策略。
- 版本:用户问旧产品时,不能拿新产品手册回答;问当前政策时,不能引用旧政策。
- 同步:源文档更新后,知识库要知道哪些文件变了,哪些索引需要重建。
本篇会做一个可运行的小实验。你会得到:
kb-migration/
processed/chunks.jsonl
tools/access_control.py
tools/sync_state.json
tools/sync_check.py先准备一份更真实的 chunks.jsonl
假设第二篇已经生成了 processed/chunks.jsonl。为了讲清楚权限和版本,这里先手动准备一份带差异的样例。
processed/chunks.jsonl:
{"id":"refund-2025","text":"2025 版售后政策:签收后 7 天内可申请无理由退货。","source":"docs/refund_policy.md","owner":"客服运营","domain":"business","topic":"after_sales","permission":"internal","department":"customer_service","version":"2025.12","effective_from":"2025-12-01","effective_to":"2026-03-31","lifecycle":"deprecated"}
{"id":"refund-2026","text":"2026 版售后政策:签收后 7 天内,商品未拆封且配件齐全,可申请无理由退货。","source":"docs/refund_policy.md","owner":"客服运营","domain":"business","topic":"after_sales","permission":"internal","department":"customer_service","version":"2026.04","effective_from":"2026-04-01","effective_to":"","lifecycle":"current"}
{"id":"vip-compensation","text":"VIP 客户补偿策略:补偿金额超过 100 元必须由客服主管审批。","source":"docs/vip_policy.md","owner":"客服主管","domain":"business","topic":"after_sales","permission":"department","department":"customer_service","version":"2026.04","effective_from":"2026-04-01","effective_to":"","lifecycle":"current"}
{"id":"finance-rule","text":"财务制度:月度成本报表仅财务部和管理层可见。","source":"docs/finance_policy.md","owner":"财务部","domain":"policy","topic":"finance","permission":"department","department":"finance","version":"2026.01","effective_from":"2026-01-01","effective_to":"","lifecycle":"current"}
{"id":"secret-discount","text":"特殊折扣策略:大客户续约可申请隐藏折扣。","source":"docs/discount_secret.md","owner":"销售总监","domain":"business","topic":"sales","permission":"restricted","allowed_users":["u_manager"],"version":"2026.02","effective_from":"2026-02-01","effective_to":"","lifecycle":"current"}这 5 条知识覆盖了三类权限和两个版本状态:
| id | 权限 | 版本状态 | 说明 |
|---|---|---|---|
refund-2025 | internal | deprecated | 旧版政策,默认不应该被检索 |
refund-2026 | internal | current | 当前售后政策 |
vip-compensation | department | current | 只有客服部门可见 |
finance-rule | department | current | 只有财务部门可见 |
secret-discount | restricted | current | 只有指定用户可见 |
权限不是 Prompt,权限是检索前过滤
很多人会在 Prompt 里写:
不要泄露用户无权访问的知识。这句话有用,但不够。
正确做法是:无权限知识不能进入模型上下文。模型看不到,才不会说出来。
也就是说,权限过滤发生在这里:
用户问题 -> 读取候选知识 -> 权限过滤 -> 版本过滤 -> 检索/生成而不是:
用户问题 -> 检索所有知识 -> 交给模型 -> 希望模型别泄露第二种方式风险很高。只要 Prompt 被绕过,或者模型理解错了权限说明,就可能泄露。
定义用户上下文
权限过滤需要知道“谁在问”。
我们用一个简单用户对象表示:
{
"user_id": "u_cs_001",
"departments": ["customer_service"],
"scopes": ["public", "internal"]
}字段含义:
user_id:用户 ID,用于 restricted 精确授权。departments:用户所属部门,用于 department 权限。scopes:用户拥有的通用权限,比如 public、internal。
真实系统里,这些信息通常来自登录态、JWT、IAM、企业通讯录或后台权限系统。
编写权限和版本过滤脚本
tools/access_control.py:
import json
from pathlib import Path
CHUNKS_FILE = Path("processed/chunks.jsonl")
def load_chunks() -> list[dict]:
return [
json.loads(line)
for line in CHUNKS_FILE.read_text(encoding="utf-8").splitlines()
if line.strip()
]
def can_access(user: dict, chunk: dict) -> bool:
permission = chunk.get("permission")
if permission == "public":
return True
if permission == "internal":
return "internal" in user.get("scopes", [])
if permission == "department":
return chunk.get("department") in user.get("departments", [])
if permission == "restricted":
return user.get("user_id") in chunk.get("allowed_users", [])
return False
def is_current(chunk: dict) -> bool:
is_not_expired = chunk.get("effective_to") in (None, "")
is_not_deprecated = chunk.get("lifecycle") != "deprecated"
return is_not_expired and is_not_deprecated
def version_match(chunk: dict, requested_version: str | None = None) -> bool:
if requested_version:
return chunk.get("version") == requested_version
return is_current(chunk)
def filter_chunks(user: dict, chunks: list[dict], requested_version: str | None = None):
return [
chunk for chunk in chunks
if can_access(user, chunk) and version_match(chunk, requested_version)
]这个脚本只做两件事:
can_access:判断这个用户能不能看这条知识。version_match:判断这条知识是不是当前应该使用的版本。
注意,权限和版本是两个不同维度。一个用户有权限看某条知识,不代表这条知识就是当前有效版本。
写一个测试入口
继续在 tools/access_control.py 末尾加:
def print_visible(user: dict, requested_version: str | None = None):
chunks = load_chunks()
visible = filter_chunks(user, chunks, requested_version)
print(f"user={user['user_id']}, version={requested_version or 'current'}")
for chunk in visible:
print("-", chunk["id"], chunk["version"], chunk["permission"], chunk["text"])
if __name__ == "__main__":
users = [
{
"user_id": "u_cs_001",
"departments": ["customer_service"],
"scopes": ["public", "internal"],
},
{
"user_id": "u_fin_001",
"departments": ["finance"],
"scopes": ["public", "internal"],
},
{
"user_id": "u_manager",
"departments": ["sales"],
"scopes": ["public", "internal"],
},
]
for user in users:
print_visible(user)
print()
print("指定查看旧版本:")
print_visible(users[0], requested_version="2025.12")运行:
python3 tools/access_control.py你应该看到类似结果:
user=u_cs_001, version=current
- refund-2026 2026.04 internal 2026 版售后政策...
- vip-compensation 2026.04 department VIP 客户补偿策略...
user=u_fin_001, version=current
- refund-2026 2026.04 internal 2026 版售后政策...
- finance-rule 2026.01 department 财务制度...
user=u_manager, version=current
- refund-2026 2026.04 internal 2026 版售后政策...
- secret-discount 2026.02 restricted 特殊折扣策略...
指定查看旧版本:
user=u_cs_001, version=2025.12
- refund-2025 2025.12 internal 2025 版售后政策...这个结果说明:
- 客服能看到当前售后政策和客服部门知识。
- 财务能看到当前售后政策和财务制度,但看不到客服补偿策略。
- 指定用户
u_manager能看到 restricted 折扣策略。 - 默认不返回
refund-2025,除非明确请求旧版本。
版本选择为什么要这么做
版本逻辑最容易被低估。
假设用户问:
2025 年买的产品还能按旧政策退货吗?这时候你可能需要查旧版本。
但如果用户问:
现在七天无理由退货条件是什么?就应该只查当前版本。
所以默认策略应该是:
- 不传版本:只返回当前有效知识。
- 明确传版本:返回指定版本。
- 旧版本可以查,但不能默认参与当前问答。
这能避免 AI 引用过期知识。
把过滤接到 RAG 检索前
真实 RAG 里通常有两种做法。
第一种:先过滤再做向量检索。
visible_chunks = filter_chunks(user, all_chunks)
results = vector_search(query, visible_chunks)适合小规模知识库或内存检索。
第二种:把权限和版本字段写进向量库 metadata,在查询时加 filter。
伪代码:
vector_store.search(
query=query,
filters={
"permission_scope": {"$in": user["scopes"]},
"lifecycle": "current",
}
)适合生产系统。因为数据量大时,先从所有 chunk 里加载再过滤会很慢。
本篇先用 Python 列表演示,是为了让你看懂权限和版本的逻辑。等逻辑确认后,再把它搬到 Chroma、Qdrant、Milvus、Elasticsearch 或 pgvector 的 metadata filter 里。
同步:怎么知道源文档变了
知识库不是一次性导入。源文档会更新,所以需要同步状态。
我们先做最简单的同步状态文件:
tools/sync_state.json:
{
"docs/refund_policy.md": {
"last_hash": "",
"last_synced_at": ""
},
"docs/vip_policy.md": {
"last_hash": "",
"last_synced_at": ""
}
}含义:
last_hash:上次同步时文件内容的 hash。last_synced_at:上次同步时间。
只要当前文件 hash 和 last_hash 不一致,就说明文件变了,需要重新清洗、分块、入库。
编写同步检查脚本
tools/sync_check.py:
import hashlib
import json
from datetime import datetime, timezone
from pathlib import Path
STATE_FILE = Path("tools/sync_state.json")
def file_hash(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def load_state() -> dict:
if not STATE_FILE.exists():
return {}
return json.loads(STATE_FILE.read_text(encoding="utf-8"))
def save_state(state: dict):
STATE_FILE.write_text(json.dumps(state, ensure_ascii=False, indent=2), encoding="utf-8")
def check_file(path: Path, state: dict) -> bool:
current_hash = file_hash(path)
old_hash = state.get(str(path), {}).get("last_hash")
return current_hash != old_hash
def mark_synced(path: Path, state: dict):
state[str(path)] = {
"last_hash": file_hash(path),
"last_synced_at": datetime.now(timezone.utc).isoformat(),
}
def main():
state = load_state()
files = [Path("docs/refund_policy.md"), Path("docs/vip_policy.md")]
changed_files = []
for path in files:
if not path.exists():
print(f"skip missing: {path}")
continue
if check_file(path, state):
changed_files.append(path)
if not changed_files:
print("no changes")
return
print("changed files:")
for path in changed_files:
print("-", path)
mark_synced(path, state)
save_state(state)
if __name__ == "__main__":
main()准备两个源文档:
mkdir -p docs
echo 'refund policy v1' > docs/refund_policy.md
echo 'vip policy v1' > docs/vip_policy.md
python3 tools/sync_check.py第一次运行会输出:
changed files:
- docs/refund_policy.md
- docs/vip_policy.md再运行一次:
python3 tools/sync_check.py应该输出:
no changes修改一个文件:
echo 'refund policy v2' > docs/refund_policy.md
python3 tools/sync_check.py应该只看到:
changed files:
- docs/refund_policy.md这就完成了最小同步检测。
同步检测之后要做什么
检测到文件变化后,不是只更新状态就完了。真实流程应该是:
发现源文档变化
-> 重新解析文档
-> 重新清洗和分块
-> 生成新的 chunk id 或覆盖旧 chunk
-> 更新向量索引
-> 跑验收问题
-> 记录同步日志如果是高风险知识,例如财务制度、法务条款、补偿规则,还应该进入人工审核:
发现变化 -> 清洗分块 -> 标记 draft -> 人工审核 -> approved 后入正式索引不要让所有文档一更新就直接进入生产 RAG。不同知识的风险不同,同步策略也应该不同。
常见问题
为什么客服能看到 internal 的售后政策?
因为用户 scopes 里有 internal。这表示公司内部用户都能访问这条知识。
为什么客服看不到 finance-rule?
因为 finance-rule 是 department 权限,且它的 department 是 finance。客服用户的 departments 是 customer_service,所以被过滤掉。
为什么默认看不到 refund-2025?
因为它的 lifecycle 是 deprecated,并且 effective_to 有值。默认查询只返回当前有效版本。
restricted 和 department 有什么区别?
department 是部门级授权,适合“财务部可见”“客服部可见”。restricted 是用户级授权,适合少数人可见的敏感策略。
能不能只用 permission,不用 department?
不建议。permission=department 只是说明它按部门授权,具体哪个部门能看还需要 department 字段。
验收清单
完成本篇后,你应该能验证:
- 普通内部用户看不到 restricted 知识。
- 客服部门能看到客服知识,看不到财务部门知识。
- 财务部门能看到财务知识,看不到客服部门的补偿策略。
- 默认只返回当前有效版本。
- 指定
requested_version时可以查询旧版本。 - 文件内容没变时不会重复同步。
- 文件内容变化时能被识别出来。
如果这些都通过,说明你的知识库已经具备进入 RAG 检索前最关键的三道控制:权限、版本、同步。

