前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片NEW
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 业务场景题真实业务问题与追问
  • 查漏补缺常见问题解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
    • AI 定制路线NEW按你的简历现排
    • AI 知识地图NEW串起全站知识点
  • 动态
    • AI 热点NEWAI 每日动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
AI 助手NEW
旧版
返回 AI 情报前线
All News · 全部资讯8836
  • Bedrock 上用开源权重模型做 AI 编程助手
  • Anthropic 实验室:Claude 自主发现类 CRISPR 新型酶系统
  • ChatGPT Voice 集成邮件、日历和 Slack:Altman 心中的"Her"更近一步
  • OpenAI GPT-6 Sol/Luna 和 Claude Opus 5.5 同步降价 50%
  • AI 工具循环必须显式传递 Retry-After 头否则必死循环
  • AI 代码补丁静默引入新工具调用:merge 前必须强制契约检查
  • AI 写 API 文档无法区分 null/0/缺省三态:OpenAPI 契约必须显式约束
  • AI 编程 Agent 工具输出遭截断:应记录 stdout_bytes 和截断标志
  • Anthropic工程师揭秘:Claude为何越进化写作越差
  • Gemini 3.8 Flash / Flash-Lite TTS 发布:千款语音、30秒克隆、逐行台词控制
  • 工程师详解:新版Claude为何写作风格变得怪异
  • 小米MiMo-V3将搭载HySparse 2:100万Token下KV缓存缩小4.5倍
  • GitHub Copilot 应用新增本地沙箱隔离功能
  • AI Agent 调试指南:重启不是调试,七层架构定位根因
  • AI 加剧软件供应链攻击威胁,行业如何应对
  • 阿里 Qwen Audio 3.1 发布:语音识别/TTS 多模型,API 价格最高降 95%
  • Claude Code部署到Lizard平台实战指南
  • Claude Opus 5.5降价却破坏四个Agent依赖项
  • OpenAI GPT-6 Sol/Luna 半价发布,缓存机制或为更大降本杠杆
  • treg:聚合 3000+ Agent 工具的统一网关
  • 向量检索权限校验应内嵌到 pgvector 查询中
  • Univer:面向 AI Agent 的开源办公套件 SDK
  • DeepSeek公开Agent训练新论文,梁文锋署名
  • 为 AI 编程 Agent 构建可复用技能系统的实践
  • DeepMind研究:百个AI智能体协作求解时出现作弊与告密现象
  • Google AX:开源Agent编排运行时
  • 阿里千问发布Qwen-Audio-3.1:TTS降价70%、ASR降价95%
  • 诺基亚开源AnyJev:无训练即可将任意开源LLM转为校准决策模型
  • 2026年AI网关横评:Bifrost领跑,多路 failover 哪家强
  • GPT-6 Sol/Luna 发布:准确率翻倍、成本减半,价格战开启
  • Claude Opus 5.5 登场,AI 模型价格普降 40-50%
  • Kyutai开源语音模型Voice of Reason,口算GSM8K准确率从27%升至77%
  • 微软Copilot大促:10万席最高半价,向超级AI应用转型
  • OpenAI GPT-6 Sol/Luna:API价格腰斩,长任务Prompt缓存优化
  • AI 编码 Agent 在移动端多久“看”一次才够用
  • GPT-6 Astra 引领 3D 生成技术,竞争格局生变
  • Claude Opus 5.5 缓存读取降价 60%,68 万行代码迁移攻略
  • 用 AI 辅助求职的工程实践:诚实原则与确定性设计
  • Agent 补丁评分卡:防止测试被悄悄弱化的 CI 策略
  • Opus 5.5 缓存降价后 text-to-SQL 成本重算分析
  • Slack推出Code Channels:团队对话内集成AI编程助手
  • GPT-6 Sol/Luna 登场:价格腰斩,定位细分
  • TypeSafe Jev:让AI返回结构化决策而非段落文本
  • Text-to-SQL访问控制:身份决定Schema可见性
  • Jev 1.13实测:工单分类精度达100%,对比规则引擎和本地LLM
  • GitHub Copilot应用支持OpenTelemetry可观测性配置
  • Copilot for JetBrains新增工具审批和Agent控制功能
  • 两款 AI 代码审查基准测评结论相反
  • AI 编码提速后:验证环节成为新瓶颈
  • AI 审查同一 PR 给出不同结论:确定性判定器来解决
  • AI计量工具审计启示:独立交易ref是防重复计费的关键
  • 已加载 51 / 8836
8.0
热点
AI SCORE
技术实践2026-09-23 17:30

向量检索权限校验应内嵌到 pgvector 查询中

dev.to · AI#pgvector#RAG#安全
Editor brief · 编辑速览

Post-filter 方式存在安全漏洞和排序正确性缺陷,应将访问控制推入 SQL 查询本身,防止越权结果进入候选集。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

在 RAG 系统中,大多数权限 bug 在代码审查时看起来人畜无害。你取出 top-k 个最近邻文本块,然后把用户无权查看的丢掉。读起来完全正确。其实不然,而且这种失败是静默的。

我想告诉你为什么后置过滤是错误的位置,以及如何将访问控制决策推送到检索查询内部,使得未授权的文本块根本不会被选中、不会被排名、不会被计数。我会基于我的一个小项目 vaultrag 来具体说明——这是一个支持权限感知的 RAG 项目,但这个技术可以迁移到任何 Postgres + pgvector 技术栈。

为什么后置过滤会泄露且不准确

这里有一个我希望你停止编写的模式。

# fetch top 10 by vector distance
rows = fetch_nearest(query_embedding, limit=10)
# then remove what the user cannot see
visible = [r for r in rows if user_can_see(r)]

这里有两个问题。

第一个是正确性 bug。你的 LIMIT 10 是在表中每个文本块上运行的。如果十个最近邻中有八个属于该用户无权阅读的文档,你会把它们过滤掉,只返回两个结果。用户感受到的是搜索坏了,而不是安全边界被触碰到了。他们有权看到的相关材料排在第 11 位,根本没进入候选集。

第二个更糟糕,这也是值得关注的理由。每一条触碰原始结果集的代码路径,现在都可能成为泄露的入口。记录过滤前的行、指标计数器、调试端点、按查询文本做 memoize 的缓存层——每一个都能看到用户无权看到的文本块。过滤器只保护了一个出口。它上游的一切都在拿着禁止访问的数据。

修复方案是让持有这些数据在一开始就结构上不可能。

将访问控制建模为数据

从每个文档的显式访问控制列表开始。在 vaultrag 中,一个文档有一个由 principal 组成的 ACL,其中 principal 可以是用户 ID 或组名。

CREATE TABLE documents (
    id          TEXT PRIMARY KEY,
    title       TEXT NOT NULL,
    deleted_at  TIMESTAMPTZ
);

CREATE TABLE doc_acl (
    doc_id      TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
    principal   TEXT NOT NULL
);

CREATE TABLE chunks (
    id          BIGSERIAL PRIMARY KEY,
    doc_id      TEXT NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
    text        TEXT NOT NULL,
    embedding   VECTOR(1536)
);

关键的设计选择是在服务器端解析调用者的 principal,而不是从请求中获取。如果客户端能自行声明自己的组成员身份,那 ACL 就是装饰品。

async def resolve_principal(conn, user_id: str):
    async with conn.cursor(row_factory=dict_row) as cur:
        await cur.execute("SELECT id, groups FROM users WHERE id = %s", (user_id,))
        row = await cur.fetchone()
    if row is None:
        return None
    # principals we match against: the user id plus every group they belong to
    return [row["id"], *(row["groups"] or ())]

把边界放在一个所有查询都读取的 CTE 中

这个技术的核心是一个公共表表达式 visible,它定义了当前调用者可以看到的文本块的集合。查询中所有其他部分都从 visible 读取,而不是从 chunks。没有任何路径可以从别处开始。

WITH visible AS (
    SELECT c.id, c.doc_id, c.text, c.embedding
    FROM chunks c
    JOIN documents d ON d.id = c.doc_id
    WHERE d.deleted_at IS NULL
      AND EXISTS (
          SELECT 1 FROM doc_acl a
          WHERE a.doc_id = d.id
            AND a.principal = ANY(%(principals)s)
      )
),
ranked AS (
    SELECT id, doc_id, text
    FROM visible
    WHERE embedding IS NOT NULL
    ORDER BY embedding <=> %(embedding)s::vector
    LIMIT %(limit)s
)
SELECT * FROM ranked;

读一下操作顺序,因为这就是全部要点。EXISTS 对 doc_acl 的检查在 ORDER BY ... <=> 之前运行。近邻排序和 LIMIT 操作在 visible 上,而 visible 已经排除了禁止访问的文本块。你的 top-k 现在是用户有权查看的 top-k。前述的正确性 bug 已经消失,泄露面也随之消失,因为查询规划器永远不会将禁止访问的行物化到候选集中。

有一个值得借鉴的细节:使用 EXISTS 而不是普通的 JOIN doc_acl。如果一个文档有三条匹配的 ACL 记录,用 JOIN 会让每个文本块乘以三份复制。EXISTS 在第一次匹配时就会短路,因此每个可见文本块只出现一次。

同样的 visible CTE 与混合搜索也能干净地组合。在 vaultrag 中,向量检索分支和全文检索分支都在融合之前从 visible 中选择,然后通过 reciprocal rank fusion 合并,因此两个分支都不会暴露另一个分支无权访问的文本块。

WITH visible AS ( ... ),                 -- the boundary, defined once
vec AS (
    SELECT id, ROW_NUMBER() OVER (ORDER BY embedding <=> %(embedding)s::vector) AS rank
    FROM visible WHERE embedding IS NOT NULL
    ORDER BY embedding <=> %(embedding)s::vector LIMIT %(candidates)s
),
kw AS (
    SELECT id, ROW_NUMBER() OVER (
        ORDER BY ts_rank_cd(tsv, websearch_to_tsquery('english', %(q)s)) DESC) AS rank
    FROM visible WHERE tsv @@ websearch_to_tsquery('english', %(q)s)
    LIMIT %(candidates)s
)
-- fuse vec and kw, both already scoped to visible

因为两个分支都从 visible 读取,函数中没有任何代码路径可以对未授权文本块进行排名。这是一个你可以对查询陈述的属性,而不是你寄希望于过滤器来强制执行的行为。

将 ACL 推入查询只是移动了边界,并没有让边界消失。EXISTS 子查询对每个候选文档运行,在大表上规划器的选择很关键。你需要一个让 ACL 检查足够廉价的索引:

CREATE INDEX ON doc_acl (doc_id, principal);

这里与 pgvector 索引存在真实的张力。HNSW 或 IVFFlat 索引能让你快速得到近似近邻,但近似发生在扫描内部你的 WHERE 过滤器应用之前。当过滤器具有高度选择性——意味着用户只能看到极小一部分文档时——向量索引可能返回一个页面全是候选、然后几乎全部被过滤掉,你就会得到比 LIMIT 要求少得多的结果。这就是知名的 filtered-search 问题。存在缓解手段:调高 hnsw.ef_search、使用分区,或者在较新版本的 pgvector 上使用迭代索引扫描。在你的数据上测量它。不要因为 CTE 是正确的就假设它是免费的。

当权限检查是排名的前置条件而不是事后的清理步骤时,正确性和安全性都会提升。在一个 CTE 中一次性定义可见集合,让搜索的每个分支都从它读取。你获得的保证是结构性的:未授权的文本块不是被后期过滤掉的,它根本不会被选中。

如果你想要一个完整可用的参考实现,检索查询、ACL schema 和服务器端 principal 解析都在我的项目中:github.com/AgentPostmortem/vaultrag。借鉴这个 CTE 并将 ACL 模型适配到你自己的租户规则中。

Original source

本文由 AI 翻译整理自 dev.to · AI,原文版权归原作者所有。

阅读英文原文
上一篇
treg:聚合 3000+ Agent 工具的统一网关
下一篇
Univer:面向 AI Agent 的开源办公套件 SDK