基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
AIHT
内容
精选全部 AI 动态热点榜AI 日报收藏
分类
模型行业编程产品技术观点开源
更多
Agent 接入
返回首页开发者导航
AIHT
精选 / 资讯详情
8.0
热点
AI SCORE
技术实践2026-10-29 16:00

用.NET搭建带权限过滤的本地RAG

dev.to · AI#RAG#向量检索#权限控制
Editor brief · 编辑速览

教程使用ASP.NET Core、Qdrant和Ollama构建C# RAG API,覆盖按页与标题切块、向量检索、权限过滤及带引用的流式回答。内容还包含效果测量,涉及从文档处理到回答评估的完整链路。

文章思维导图
Knowledge map
拖拽缩放
AI deep read

AI 深度解读

用自己的文档做问答,难点往往不在接上模型,而在回答能否找到出处、不同客户的数据会不会串、资料不足时能否停下来。这篇教程用 C#、ASP.NET Core、Qdrant 和 Ollama,把这些问题串成了一条 RAG 流水线。

作者的核心主张是:可靠性要落实到文档处理、检索权限和评测里。它值得程序员读,但不能当效果保证:评测只有 8 份虚构文档、59 道题,而且输入原文没有展示具体成绩。

引用准不准,从文档入库时就决定了

RAG(检索增强生成)先从文档中找相关片段,再让模型依据片段回答。作者将它拆成两条流程:后台负责提取、切块、生成向量和入库;每次提问负责检索、组织上下文和生成答案。

实现来自 SupportDesk AI,面向客服知识库。项目要求 .NET 10 SDK,流式接口使用 ASP.NET Core 10 新增的 TypedResults.ServerSentEvents。默认模型通过 ollama pull qwen2.5:7b && ollama pull nomic-embed-text 拉取,前者回答,后者生成 768 维向量。

作者先保留文档结构:PDF 用 PdfPig 按页提取,Word 用 Open XML SDK 按标题拆分,表格转成 cell | cell 行;Markdown 按标题拆分,但忽略代码围栏里的 #;纯文本作为一个 section。

PDF 还有两个实际问题。ContentOrderTextExtractor 尽量按阅读顺序处理双栏内容;页面非空白字符少于 ScannedPageMaxChars = 20 时尝试 OCR。安装 Tesseract 后,系统读取扫描图像,并跳过小于 200 × 200 像素的图片。这里的尺寸判断是项目启发式规则。

切块采用最多 1,200 个字符、重叠 150 个字符,优先保留完整段落,超长段落再按空白切开。硬约束是不跨 section,否则引用里的页码或标题就可能失真。

生成向量前,再拼上文档标题和章节标题,帮助模型理解「30 天内退货」属于哪份政策。正文仍单独存入 payload,方便展示和引用。Ollama 实现调用 /api/embed,每批处理 32 段文本;文档和问题必须使用相同模型及设置。

RAG 教程原文配图

权限必须进入检索,不能等回答生成后再检查

Qdrant 的启动命令是 docker run -d --name qdrant -p 127.0.0.1:6333:6333 -p 127.0.0.1:6334:6334 -v qdrant-data:/qdrant/storage qdrant/qdrant:v1.18.3。6333 提供 REST 和控制台,6334 供 Qdrant.Client 使用 gRPC;仓库 compose 的宿主机端口则是 6335、6336。绑定本机地址,是因为未配置 API key 时 Qdrant 没有认证。

更关键的是索引设计。collection 名包含 provider、model 和 dimension,例如 supportdesk__ollama__nomic-embed-text__768,向量距离使用 Distance.Cosine。更换模型就进入新集合,避免把不同向量空间混在一起。

payload 同时保存引用信息与权限信息,包括文档、组织、可见性、正文、页码和章节;document_id、organization_id、visibility 建立 keyword 索引。重新处理文档时先删除旧片段,再写入新片段,避免旧政策继续被引用。SQL Server 保存 Pending、Processing、Completed、Failed 状态,后台逐份处理,重启后重新排队未完成任务。

权限应当决定哪些片段有资格参与检索。

下面这段代码将组织和可见性直接放进向量查询:

private static Filter AccessFilter(AccessScope access)
{
    var filter = new Filter();
    filter.Must.Add(Conditions.MatchKeyword("organization_id", access.OrganizationId.ToString()));
    filter.Must.Add(Conditions.Match("visibility", access.Visibilities.Select(VisibilityValue).ToList()));
    return filter;
}

// 权限属于每次查询的一部分,Qdrant 只考虑用户可见的片段。
var dense = await client.QueryAsync(
    collection,
    query: vector.ToArray(),
    filter: AccessFilter(access),
    scoreThreshold: (float)minScore,
    limit: (ulong)limit,
    payloadSelector: true,
    cancellationToken: cancellationToken);

要看的是 filter: AccessFilter(access)。如果先取 top 5 再过滤,前五条可能全是无权访问的内容,既漏掉合法答案,也增加泄漏机会。项目要求检索必须传入来自登录用户的 AccessScope,没有无权限参数的重载。

作者还做过反向验证:故意移除过滤条件,组织 B 的片段进入组织 A 的结果,集成测试随即失败。新文档默认 Internal;改为 Public 等可见性变化只更新 payload,不必重新生成向量。底层使用较新的 QueryAsync,应与业务封装层的 vectors.SearchAsync 区分。

相似度够高,也不代表资料能回答问题

默认检索取 top 5,余弦相似度门槛是 MinScore = 0.3。没有结果时,系统直接返回固定文本,不调用聊天模型:

var matches = await RetrieveAsync(question, access, request, cancellationToken);
if (matches.Count == 0)
{
    return new RagAnswer(
        RagPrompt.NotFoundAnswer, Grounded: false, [], Metrics: Finish(request, Outcomes.NoContext), SearchQuery: searchQuery);
}

var response = await chat.CompleteAsync(
    RagPrompt.Build(question, matches, options.Value.MaxContextChars), cancellationToken);

关键是 matches.Count == 0 分支提前返回。固定回答为 I couldn't find information about that in the knowledge base.,此时只花费问题向量化的调用成本。

但门槛只能过滤明显无关内容。作者评测中,不可回答问题的最高匹配分数达到 0.44 至 0.61,最弱的可回答问题只有 0.54。提高门槛无法干净地分开两者。项目笔记还给出相关文本的大致区间:nomic-embed-text 为 0.6 至 0.8,OpenAI 的 text-embedding-3-small 为 0.4 至 0.6,换模型后需要重测。

第二道拒答交给提示词:片段编号为 [1] 至 [5],要求引用、禁止外部知识,并声明文档里的指令属于不可信数据。

作者也发现了过度拒答。两道题的正确片段都排在第一,但政策写「30 天」,用户问「六周」或「两个月」,模型仍不敢回答。增加「将规则应用到客户具体情况」后,该模式得到修正;作者称没有增加幻觉,但证据仍限于这套小评测。

上下文以 MaxContextChars = 12,000 限制字符数,首个片段始终保留。字符预算不等于 token 预算,还要检查 Ollama 的上下文窗口,避免提示词被静默截断。

前端拿到的是 SSE(服务器发送事件):依次发送 conversation、可选的改写后 query、sources、多次 delta,最后 done 或 error。来源包含分数和 300 字符摘录,先发来源便于边生成边显示引用。接口 /chat/stream 使用带 bearer token 的 POST,问题最长 2,000 字符;浏览器原生 EventSource 不支持这种调用方式。输入原文在介绍 React 通过 fetch 读取流时截断,没有提供后续完整实现。

我的判断

这套方案最有价值的是把可靠性变成可检查的工程约束:保留出处、隔离向量空间、查询内鉴权、空结果直接拒答。相比只接一个通用 RAG SDK,这些边界更值得理解。作者用 typed HttpClient 和 IEmbeddingService、IChatService 解耦,也允许底层替换成 Microsoft.Extensions.AI 或 Semantic Kernel。

但它没有证明这套配置优于其他方案。评测使用单张 8 GB GPU、qwen2.5:7b 和 nomic-embed-text,运行日期为 2026-10-05;语料小而干净,拒答与权限题更少,输入里也没有具体指标表或完整评测步骤。1,200/150 的切块、top 5、0.3 门槛应视为起点。

还有两处边界:提示词声明「不要执行文档指令」不能单独证明防住了提示注入;先删旧片段再写入新片段会如何处理失败和检索空窗,原文没有说清。权限测试能证明特定过滤条件被覆盖,不能据此推断整个系统安全。

能拿来做什么

  • 做知识库前先检查提取结果,确认扫描页有正文、页码和标题能随片段保留,再讨论更换回答模型。
  • 做多租户检索接口时,将服务端用户身份生成的权限范围设为必填参数,并测试跨组织内容是否会进入结果。
  • 做 React 问答界面时,按 sources、delta、done、error 分别处理来源、增量文本和结束状态;后端将取消信号传到模型调用,用户关闭页面后停止生成。
  • 用自己的问题集调整参数,分别统计 answered、no_context、declined、failed、cancelled,同时检查检索正确却拒答的情况,不只盯着相似度分数。
Original source

本文由 AI 通读原文后重新编写,观点部分为站内判断,细节以原文为准,来源 dev.to · AI,原文版权归原作者所有。

阅读原文
上一篇
一句问候消耗17.7万Token,如何排查
下一篇
Expo官方技能包补齐AI开发上下文

资讯速览

dev.to · AI · 2026-10-29 16:00

AI 评分 8.0 / 10

相关标签

#RAG#向量检索#权限控制

继续阅读

  • 美团分布式存储团队的AI编程验证实践
  • 一句问候消耗17.7万Token,如何排查
  • Expo官方技能包补齐AI开发上下文
  • 英伟达开放多语种转录与说话人归属模型
  • 用四个检查点定位代理记忆丢失