回答之前:可回答性与证据链
“今年允许远程办公几天?”不是一个纯语言问题。答案随政策变动,且不同团队权限不同。把政策放入提示可以更新事实;让模型背政策则需要重新训练,也不能提供逐条访问控制。RAG 把外部证据选入上下文,生成器再据此回答。它适合有可靠资料的问题;计算实时余额应调用业务 API,资料完全不存在时应请求补充或拒答。
先看一个不依赖模型的闭环
下面的集合表示已经分好的查询词,不是中文分词算法。retrieve 先按用户所属团队筛选,再对候选算词交集;answer 只引用确实进入候选的内容。这是可重复的抽取式基线,不冒充语义检索或 LLM。
docs = [
{'id': 'p1', 'team': 'A', 'terms': {'远程', '天数'}, 'text': 'A组每周远程2天。'},
{'id': 'p2', 'team': 'B', 'terms': {'远程', '天数'}, 'text': 'B组每周远程3天。'},
]
def retrieve(query, team):
scored = [(len(query & d['terms']), d) for d in docs if d['team'] == team]
return [d for score, d in sorted(scored, key=lambda x: -x[0]) if score > 0][:2]
def answer(query, team):
evidence = retrieve(query, team)
return '\n'.join(f"{d['text']} [{d['id']}]" for d in evidence) or '资料不足'
assert answer({'远程'}, 'A') == 'A组每周远程2天。 [p1]'
assert answer({'工资'}, 'A') == '资料不足'
assert answer({'远程'}, 'C') == '资料不足'
print('evidence chain passed')三个断言检查的是正确租户、不可回答和无权限,缺一项都可能把“有输出”误认为成功。先检索后过滤虽能阻止最后输出未授权片段,但 top-k 候选可能全部被别的租户占满,导致自己的相关资料丢失。权限应在每个召回通道中生效,缓存身份也要包含权限版本。
生成器进入管道后增加了什么责任
在线流程是问题→授权召回→重排→上下文→模型→引用校验。模型可把多段证据压成自然语言,却可能遗漏限制、混合年份或补出不存在的结论。输出引用 [p1] 只证明它写了一个编号,程序还要确认编号确实来自这次检索,并检查相应事实是否被证据支持。事实支持需要语义判断或人工复核,不能靠正则完成。
保留 query_id、document_version、chunk_id、retrieval_score、prompt_version、model_id,便能重建模型当时看见了什么。记录脱敏后的证据摘要或访问受控的文档引用,避免日志成为另一个泄露渠道。对需要严格计算的问题,先用确定性代码求值,再让生成器解释计算结果。
RAG 减少无依据回答的机会,但资料错、召回错、读错都仍会导致错答。“回答必须基于资料”是行为要求,不是安全机制。外部资料中的“忽略规则”仍是数据,不能取得调用工具和读取私密文件的权限。
练习
给上述语料增加一条仅 A 组可见、词集合为 {"休假"} 的政策。检查 A 能查到、B 查不到,以及原远程查询结果不变。
完成后查看本章答案。
下一章:从原文到可替换的索引。