本系列主要是自己的AI学习笔记,希望对大家有所帮助,这个与机器学习分开,机器学习的见ML.net或机器学习标签。
你有没有遇到过这种情况------公司内部文档堆了几百个文件,每次查东西要翻半天,还不一定找得到?或者想搭个"私人 AI 助手",但又不想把敏感资料喂给外部云服务?
这篇文章就是为这个痛点写的。
我们用纯 C# 实现了一套完整的本地 RAG(检索增强生成)知识库系统------代码真实跑过,架构经过打磨,踩过的坑我也一并告诉你。读完这篇,你能直接照着落地,不是只看个热闹。
这个小工具我后来集成到了文件夹右键中,方便右键对文件夹向量化。
🧠 先搞清楚:RAG 到底在解决什么问题
大模型很聪明,但它有个硬伤------不知道你私有的东西。你的项目文档、内部规范、历史数据,它一概不知。
传统做法是 Fine-tuning(微调),成本高、周期长,更新一次数据要重新训练。RAG 换了个思路:先检索,再生成。
流程大概是这样:
bash
1用户提问
2 ↓
3把问题转成向量
4 ↓
5在知识库里找最相关的文本片段(Chunks)
6 ↓
7把原始问题 + 相关片段一起喂给大模型
8 ↓
9大模型基于真实资料回答
说白了,就是给大模型临时开了个"参考资料包"。简单,但效果扎实。
👨💻 先看样式


右键运行效果

🏗️ 整体架构:五层职责分明
咱们这套系统分了五个核心层,每层职责非常清晰:
数据库里有六张表,关系很直观:
为什么 Embedding 单独建表,而不是直接存在 Chunk 里?
因为向量数据(float\[\] 序列化后的 byte\[\])可能很大,1536 维的向量占 6KB。把它拆出去,查 Chunk 列表的时候不会拖慢查询,只在需要做相似度检索时才 JOIN 进来。这个小细节值得记一下。
📄 文档解析:支持主流格式的统一入口
文档处理的第一步是把各种格式"拍平"成纯文本。我们用了一个策略模式的轻量变体:
PDF 用的是 PdfPig,代码级文件(.cs/.py/.js)直接按 UTF-8 读取,HTML 用 HtmlAgilityPack 做了 DOM 清洗------把 <script>、<nav>、<footer> 这些干扰内容剔掉,只留正文节点。
有个细节容易踩坑:HTML 解析时,如果不做节点过滤,导航栏、页脚这些重复内容会污染向量空间,导致检索结果莫名其妙。别问我怎么知道的。
✂️ 文本分块:Chunk 策略直接影响召回质量
这块是 RAG 效果好不好的核心变量之一,很多人在这里翻车。
我们用的是按句子边界切割 + Token 数量控制的方式:
overlap(重叠区)为什么重要? 想象一下,一个完整的技术解释跨越了两个 Chunk 的边界------如果没有重叠,两段都是残缺的,检索时哪个都匹配不好。加了 overlap,每个 Chunk 的开头都"接住"了上一个 Chunk 的尾巴,语义完整性大幅提升。
默认配置是 ChunkSize=600, Overlap=80,这是我在中文技术文档上测出来效果比较稳的值,英文文档可以调大 ChunkSize 到 800-1000。
🔢 向量化:批量生成 + 二进制存储
向量生成这块我们复用了 OpenAI Embeddings API 的接口格式,兼容 Ollama、本地部署的模型:
向量存储时序列化为 byte[]:
为什么不直接存 JSON 字符串?性能差距大概在 3-5 倍。1536 维向量序列化成 JSON 是 ~12KB,存成 byte\[\] 只有 6KB,读写和反序列化都快一倍不止。数据量一上来,这个差距就很明显了。
🔍 相似度检索:余弦相似度的工程实现
检索逻辑在 RagService 里,核心是余弦相似度:
System.Numerics.Tensors.TensorPrimitives 是 .NET 8 里的,底层走 SIMD 指令加速,比手写循环快 4-8 倍,强烈推荐用这个而不是自己写 for 循环。
检索策略上有个小设计------双阶段兜底:
这个兜底逻辑解决了一个真实的用户体验问题:如果阈值设得稍高,某些问题会直接返回"未找到相关信息",用户体验很差。兜底后至少还能给个"最相关的"结果,用户心里有个底。
💬 流式对话:一边生成,一边显示
RAG 的最后一环是把检索到的内容组装成 Prompt,调用 LLM 流式返回:
UI 层用 await foreach 接收,实时更新消息气泡里的内容。这个体验比"等待 10 秒然后一次性显示"好太多------哪怕网络稍慢,用户也能看到内容在"打字",心理上的等待感降低了至少一半。
🔐 安全存储:API Key 不明文落盘
API Key 存储是经常被忽视的安全细节。我们用了 Windows DPAPI:
DPAPI 的好处是密钥由操作系统托管,绑定当前用户账户,其他用户或者把文件拷走都解密不了。对于桌面应用来说,这是最省力又足够安全的方案。
🔄 增量构建:不重复处理没变化的文件
知识库重建有个痛点------文件多了要等很久。我们加了文件哈希比对的增量机制:
同时还有模型变更检测------如果 Embedding 模型换了,向量维度可能都变了,这时候强制全量重建:
这两个机制加在一起,日常迭代时只处理新增/修改的文件,速度能快 80% 以上。
📊 三点核心收获
① 架构分层要早做。接口层(ILibraryService / IRagService)和实现层分离,后面换向量数据库、换 LLM 提供商,改一处就够了,不用到处动。
② Chunk 策略比模型选择更影响召回效果。很多人换了更贵的模型,效果提升有限,但调整了 ChunkSize 和 Overlap 之后召回率明显上去了。先优化数据,再优化模型。
③ 用户体验的细节决定产品质量。流式输出、进度条、增量构建、API Key 加密------这些都不是"功能",是"体验",也是这类工具能不能真正被人用起来的关键。
🗺️ 进阶方向
如果你想在这套基础上继续深化,可以考虑以下几个方向:
-
• 引入向量数据库(如 Qdrant / Milvus)替换当前的内存全量扫描,支持百万级向量的毫秒检索
-
• 加 Reranker 重排序(如 BGE-Reranker)在 Top-K 检索后做二次精排,精准度再上一台阶
-
• 多路召回:把关键词检索(BM25)和向量检索的结果融合,解决向量检索对精确匹配词不敏感的问题
-
• 对话历史管理:当前实现里 ChatSession 和 ChatMessage 已经建好表了,完全可以扩展成多轮对话记忆
觉得有用的话,欢迎微信打赏鼓励一下,让我有动力继续输出这类实战内容。完整源码结构已在各节逐一呈现,可结合项目实际直接落地;如需完整源码,可在公众号聊天窗口获取。