在优化层的第三站,我们谈安全。LEANN 的检索系统有个独特风险面:向量本身可能泄露原文语义,越权访问比传统数据库更难察觉。
我们主张「检索即授权」:每次查询都带租户上下文,索引层做隔离。下面给出一段带租户过滤的检索,防止跨租户看到彼此向量。
def secure_search(index, q, tenant_id, k=5): cands = index.search(q, k * 4) # 只返回同租户文档,跨租户向量即使相似也不透出 owned = [c for c in cands if c.get('tenant') == tenant_id] return owned[:k] docs = [ {'text': 'A', 'tenant': 't1'}, {'text': 'B', 'tenant': 't2'}, {'text': 'C', 'tenant': 't1'}, ] class Idx: def search(self, q, k): return docs print('t1 可见:', [d['text'] for d in secure_search(Idx(), None, 't1')])
要点:过滤放在检索层而非展示层,因为展示层过滤前,向量可能已在日志或缓存里泄露。安全是默认 deny,不是默认 allow。
输入也要 sanitize,防止查询里的特殊构造触发异常或注入到下游。下面给出最小清洗。
def sanitize_query(text, max_len=256): cleaned = ''.join(ch for ch in text if ch.isprintable()) return cleaned.strip()[:max_len] print(sanitize_query(' drop � table '))
案例:租户隔离缺失
secure_search,检索层就按 tenant 隔离,跨租户候选直接不返回。安全还要可审计。检索系统被越权访问后,若没有留痕,你连「发生了什么」都说不清。下面给出一段审计日志,记录每次查询的租户、命中数与结果脱敏摘要。
def audit(query, tenant, hits, allow=True): rec = { 'tenant': tenant, 'q_len': len(query), 'hit_count': len(hits), 'allowed': allow, # 结果只记数量与首条脱敏,不落原文 'top_preview': (hits[0]['text'][:4] + '***') if hits else None, } return rec print(audit('退货', 't1', [{'text': '退货政策第七条'}]))
审计日志本身也是敏感数据,要单独控访问权限。我们见过团队把审计和普通日志混在一起,结果运维同学能看全部,反而扩大暴露面。正确做法是审计日志只读、加密、定期归档,且查询需二次审批。
合规的另一面是「数据最小化」:检索返回时只带业务必需的字段,别把整文档塞进响应。结合 4.2 的过滤,可以在检索层就裁剪字段,从源头减量。
安全的另一面是「最小权限」。检索系统常接多个业务方,每个业务方只该看到自己的那部分知识。我们在 4.2 讲过滤,在安全语境下它就是授权边界:不是「查出来再决定给不给看」,而是「根本不从别的租户取」。前者在日志、缓存、调试接口里处处漏,后者从源头封死。
还要警惕「向量投毒」。如果索引允许外部写入且不加校验,攻击者可以塞入精心构造的向量,让特定查询稳定召回恶意内容。防御思路是写入侧鉴权加内容审核,并把索引完整性校验纳入健康检查。安全不是某一层的开关,而是从摄取、存储、检索到返回的全链路契约,哪一环松了,前面都白做。
| 防线 | 位置 | 作用 |
|---|---|---|
| 租户隔离 | 检索层 | 源头不取 |
| 写入鉴权 | 摄取侧 | 防投毒 |
| 完整性校验 | 健康检 | 早发现 |
合规要求常常来自行业而非技术。医疗、金融、政务领域的知识库,对数据存储位置和访问审计有硬性规定,轻量神经架构把索引留在本地,天然契合「数据不出域」的要求,这一点我们在 5.4 的案例里已经看到。但本地化只是前提,真正落地还要把访问控制、审计、留存策略一一补齐,缺一不可。
留存策略是另一块容易忘的。审计日志不能无限增长,也不能说删就删,既要控容量又要满足合规保留期。我们建议对审计日志做分级:近期可查、远期归档冷存,既满足追溯又不过度占资源。这种「该留的留、该冷的冷」的思路,和索引的冷热分层是同一哲理。还有一个现实问题:合规不是一次通过就永远通过,法规会变、业务范围会扩,原先合规的方案在新条件下可能破功。所以我们主张把合规检查也写成可定期跑的脚本,像健康检查一样周期性验证。
最后提醒,安全合规的投入要和风险匹配。一个内部 demo 和一个面向公众的医疗问答,该投入的层级完全不同。我们反对为所有场景套同一套重型合规,主张按数据敏感度和影响面分级,把最严的管控用在最该用的地方。落到日常,安全合规最怕「上线前一阵风」,正确的做法是把检查项拆小融入每次提交门禁,让合规成为持续状态而非项目里程碑,安全才真正长在系统里而不是贴在外头。
安全合规还有一个常被低估的收益:它逼着你把系统边界画清楚。当你要写「谁能看什么」的审计规则时,你不得不先想清知识到底分了几类、谁对哪类负责。这个梳理过程本身,经常能暴露平时被忽略的数据归属混乱。所以合规不是负担,而是一次免费的架构体检,借着写规则的机会把糊涂账理清。
需要强调的是,安全不是上完一次就结束的动作。每次新增数据源、每接一个新业务方,攻击面都会变化。我们建议把安全检查做成准入清单,新接入必须过一遍:数据在哪、谁能看、审计全不全、异常能否发现。把安全从「发布前的关卡」变成「每次接入的关卡」,系统才能在持续演化中守住底线,而不是靠某次审计的结论睡大觉。
本节可考核点:能解释「检索即授权」的含义,并说明为何过滤要放在检索层而非展示层。