Hermes + RAG 搭建本地知识库:比 Wiki 更聪明的文档检索方案

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

Hermes + RAG 搭建本地知识库:比 Wiki 更聪明的文档检索方案

作者:科技界的一粒微尘
一份硬件工程师的本地知识管理方案——零依赖、零网络、零隐私风险


📋 本文概览: 从零搭建基于 Hermes Agent + TF-IDF + 余弦相似度的本地 RAG 知识库。全文深度讲解 RAG 原理、与 Wiki 方案的详细对比、分词策略、向量化流程、代码实现、实际效果评测和维护方案。代码可复制,方案可复用。


本文概览:从零搭建基于 Hermes Agent + TF-IDF + 余弦相似度的本地 RAG 知识库。包含 RAG 原理讲解、与 Wiki 方案的详细对比、环境准备、分步搭建、使用方法和实测效果。

一、问题的提出 —— 知识太多,脑子不够用

干嵌入式的都知道,知识密度大得离谱。一块 HI3519DV500 芯片从看到能跑起来,中间要啃的东西太多了——芯片手册 2000 多页,PCB 设计指南几百页,SDK 编译环境搭一遍够你喝一壶,AI ISP 调参又是另一个维度。我一个半月下来,光笔记就攒了二十多篇,分布在不同目录里:硬件专题有 10 篇(芯片概览、时钟复位、电源管理、DDR 设计、启动配置、外设接口、NAND Flash、硬件排查、原理分析、快速查阅指南),软件部分有 SDK 编译环境、编译实战总结、原厂编译说明,实战操作里有操作命令手册、OS04A10 双目同步方案、10.1 寸显示屏 VO 不出图排查方案。

先看效果
在这里插入图片描述

在这里插入图片描述

问题就来了。每次做一块新板子或者排查一个问题,都得翻一堆文件。想查"DDR 走线的阻抗要求是多少",你可能要打开 DDR 设计总结那篇,翻到 PCB 设计要求的章节,在一堆表格里找到"单端信号 50Ω ±10%,差分时钟 100Ω ±10%"。如果记性不好忘了文件名叫啥——是叫 04_DDR设计_04_DDR设计_总结.md 还是 08_硬件排查与量产_08_硬件排查与量产_总结.md?那你还得先想想该打开哪个文件。

更麻烦的是跨文档关联。DDR 阻抗要求出现在 DDR 设计总结里,也出现在硬件排查与量产里,还可能在编译环境搭建那篇里作为"硬件前置条件"被提到。Wiki 的方式是你得手动给每篇文章加上 [[双向链接]],而且你得在写的时候就预见到"以后查阻抗的时候可能需要同时看这几篇"。说实话,写的时候根本想不到那么多。

最难受的是,有时候你记得某个知识点在一堆文字里的某一段,但你记不清具体在哪篇。Ctrl+F 搜"阻抗"?二十几篇文档,那能搜出一大堆结果——USB 的 90Ω 差分阻抗、DDR 的 50Ω 单端阻抗、射频的 50Ω 特性阻抗、LVDS 的 100Ω 差分阻抗,全混在一起,你还得人工筛选。

这就是典型的知识管理困境。知识存下来了,但取不出来。存得越多,取得越慢。

有人会说你用 ChatGPT 不就行了?把文档喂给它,让它帮你找。确实可以,但有几个很现实的问题。第一,硬件设计文档里有很多厂商的参考设计细节,有些是签了 NDA 的,不敢往公网上传。第二,我们做嵌入式开发的经常在封闭网络环境里干活——实验室没有外网、调试时只有内网、出差在外连不上。第三,家里的 NAS、U 盘、移动硬盘上那堆技术文档,你不想让 OpenAI 都能看到吧?

所以问题的核心很明确:能不能在本地搞一个系统,用自然语言问它问题,它能自动从我的几十篇文档里找到最相关的几段,并且基于那几段原文给我一个靠谱的回答?不依赖任何云服务,不需要上传文件,不需要联网,所有数据都留在自己的机器上。

答案是能。这套东西就是 RAG(Retrieval-Augmented Generation,检索增强生成),配合 Hermes Agent 做智能调度,跑在你自己电脑上,纯本地,不联网。做下来也就花了一个下午。

二、方案对比 —— Wiki、RAG、还是直接问 AI?

在真正动手之前,有必要把几种方案摆在一起看看。很多时候我们不是没方案,而是没想清楚该用哪个。

2.1 Wiki 方案:手动整理的经典路线

Wiki 方案大概是每个工程师最早接触的知识管理方式。说白了就是建一个 Markdown 文件夹,用 Obsidian 或者 Notion 当编辑器,每学完一个知识点就写一篇笔记,想关联其他笔记就加上 [[双向链接]]

在这里插入图片描述

比如我之前的用法就是:在 Obsidian 里建一个 vault 叫 hi3519dv500-knowledge,里面分 01_硬件02_软件03_实战操作 三个子目录,再建一个 00_总索引.md 手动罗列所有文档。写完 DDR 那篇后,在总索引里加上 [[04_DDR设计_04_DDR设计_总结]],然后如果 DDR 那篇里提到了电源设计,就手动加上 [[03_电源管理_03_电源管理_总结]]

这个方案的好处是真的很简单,不需要写一行代码。Obsidian 装好就能用,Markdown 渲染漂亮,搜索功能也凑合。如果你的文档量在二十篇以内,而且每个文档的内容都相对独立,那 Wiki 完全够用。

但文档一多就开始暴露问题了。第一,搜索靠的是关键词匹配,你搜"DDR 阻抗",它只返回包含"DDR"和"阻抗"这两个词的段落,但不会把"单端走线 50Ω"这种用了不同表述的段落也找出来——哪怕它们说的是同一个东西。第二,跨文档关联需要手工维护,二十篇文档之间的交叉引用你不可能全记住,结果就是很多应该关联的知识点实际上断开了。第三,你需要自己翻找原文,打开文件、滚动、找到目标段落,这个过程至少一两分钟。

2.2 RAG 方案:让 AI 帮你翻文档

RAG 的全称是 Retrieval-Augmented Generation,直译过来叫"检索增强生成"。别被名字唬住,原理其实很直白。

在这里插入图片描述

RAG 的工作方式分成两步:第一步叫"检索"(Retrieval),当你问一个问题时,系统不是直接让大模型凭空回答,而是先去知识库里找到和问题最相关的几段原文。第二步叫"增强生成"(Augmented Generation),把这几个相关段落连同你的问题一起丢给大模型,让大模型基于这些原文来组织回答。

举个例子。你问"DDR 走线的阻抗要求是多少?"RAG 系统先在二十多篇文档里搜一遍,找到和"DDR 阻抗"语义最接近的几个段落——可能是 DDR 设计总结里的 PCB 设计要求那一段,也可能是硬件排查与量产里的 DDR 红线那一段——然后把这些段落原文一起喂给 LLM,LLM 基于原文回答:“DDR 单端信号目标阻抗为 50Ω ±10%,差分时钟为 100Ω ±10%,DQ/DQS 组内等长偏差 ±10mil,地址/命令偏差 ±500mil。”

这个过程比 Wiki 先进在几个地方。语义搜索不靠关键词匹配,"DDR 走线"和"单端信号 50Ω"在字符层面完全不同,但在向量空间里距离很近——因为它们出现在同一篇文档的同一段里,上下文高度重叠。跨文档关联是自动的,只要多篇文档里出现过相似的内容,向量化后它们就会聚在一起,不需要你手动加链接。而且你不需要翻原文,LLM 直接给你整理好的答案。

但 RAG 也有自己的门槛。它需要一个嵌入模型把文本转成向量,需要建向量库,需要写脚本做检索。好在对于技术文档这种场景,TF-IDF 就够了——这个后面会详细讲。

2.3 正面刚一把:Wiki 和 RAG 到底哪个好?

我把两个方案放到同一个维度下对比,这样大家心里有数:

维度Wiki(Obsidian)RAG(TF-IDF + LLM)
查找方式翻目录、Ctrl+F 关键词搜索自然语言语义搜索
跨文档关联手动加 [[双向链接]]自动向量相似度匹配
单次查询耗时1~3 分钟(手动翻文件、定位段落)< 1 秒(脚本跑完直接出结果)
回答形式原文段落,自己读、自己总结LLM 基于原文总结后的答案
搭建难度极低(装个 Obsidian 就行)中等(写两个 Python 脚本)
适用文档规模< 50 篇比较舒适50~5000+ 篇都没问题
离线可用
增量更新手动加文件、手动更新索引扔文件进目录,跑 python3 build_rag.py(10 秒搞定)
是否需要 LLM不需要需要(但跑在本地)

说实话,这两种方案不是互斥的。我现在是两套一起用:Obsidian 负责浏览和编辑(因为 Markdown 渲染好,双向链接在写作时有帮助),RAG 负责快速查询(搜东西不超过 1 秒,不用翻文件)。建 RAG 只需要在 Obsidian vault 里加两个 Python 脚本和一个 .rag_db 目录,完全不干扰 Obsidian 的正常使用。

2.4 Hermes Agent 扮演什么角色?

这里要专门提一下 Hermes Agent。如果你只用 query_rag.py 的话,每次查东西得敲命令行:python3 query_rag.py "DDR阻抗要求",然后看终端里的 top-5 结果自己判断。虽然比手动翻文件快得多,但还是不够顺滑。

Hermes Agent 把这件事变成了聊天体验。你在飞书里跟 Agent 说"DDR 阻抗多少",Agent 自动调用 query_rag.py 去知识库搜,拿到最相关的 5 个 chunk,然后基于原文组织一句话的答案发给你。整个过程你只动了嘴(或者说打了几个字),Agent 帮你完成了"理解问题 → 调检索脚本 → 整理答案"的全流程。

用 Wiki 方案时,Agent 也能帮忙——它可以帮你搜文件名、打开文件、读内容——但它做不到语义级别的理解。你让它搜"DDR 阻抗",它可能搜出包含"阻抗"的所有文件,但没法区分哪段是 DDR 阻抗、哪段是 USB 阻抗。而加上 RAG 后,Agent 调 query_rag.py 拿回来的是已经按语义排序过的结果,精准度上了不止一个台阶。

所以如果说 RAG 是搜索引擎,那 Hermes Agent 就是那个帮你点搜索、读结果、总结答案的助手。你把这两个组合起来,就得到了一个完全本地化的智能知识助手。

2.5 真实场景还原:同一个问题,三种方案的体验

为了说得更具体,拿一个真实场景来对比。假设你在调试 HI3519DV500 的 DDR 部分,想知道 ZQ 校准电阻的精度要求。

使用 Wiki 方案时,你在 Obsidian 左侧文件树找"04_DDR设计",点开,滚动到 ZQ 相关段落,肉眼扫描找到"240Ω ±1%"。如果正好在同一个文件里还好,但如果 ZQ 的精度影响还提到了"08_硬件排查"里,你需要再翻另一个文件。整个过程 1-2 分钟,前提是你记得 ZQ 在 DDR 总结里。

使用 RAG 方案时,你直接问 Agent “ZQ 电阻精度要求是多少”,它自动搜索 418 个 chunk,返回 top-5 结果。每个结果都标注了来源文件和相似度。整个查询耗时 50 毫秒。如果 Agent 接了 LLM,还会帮你总结成一句话:“ZQ 校准电阻要求 240Ω ±1%,不能降精度。”

两种方案没有绝对的好坏,取决于你的场景。如果你每天查 1-2 次,Wiki 足够。如果你像我一样每天问 5-10 次,每次省 1 分钟,一年下来就是 30 小时——足够再学一个新专题了。

2.6 为什么不直接用 ChatGPT 之类的云 AI?

有人可能会说:把文档扔给 ChatGPT 不就行了,为什么要自己搭 RAG?

首先,隐私问题。硬件工程师手里的芯片手册、原理图、BOM 清单,很多是有 NDA 约束的。上传到第三方服务器在法律上就是泄密。本地 RAG 方案所有数据都在你自己的机器上,查询过程不需要联网(除了 Agent 本身的 LLM 推理)。

其次,可控性。你今天建好的知识库,明天、下个月、明年还能用。不依赖任何服务商的 API 是否继续存在、定价是否调整、模型是否变更。你的知识,你做主。

第三,定制性。TF-IDF 的分词策略是专门为技术文档调过的——中文按字+2-gram 分词,英文按词分词,数字和寄存器名完整保留。这种针对嵌入式领域的优化,通用搜索引擎做不到。

三、RAG 原理深入

前面说了 RAG 是"检索 + 增强生成",但这两个词太抽象了。这一节我会用工程师能理解的方式把它拆开来讲。

3.1 核心概念:不是"记住",而是"先找再看"

传统的大模型(比如 ChatGPT)回答问题的方式是"记住"。训练的时候它"看"过海量文本,参数里编码了大量知识。你问它"Python 怎么读取文件",它知道,因为它训练时学过。但你问它"鸿鸥派核心板的 DDR 用的是 LPDDR4 还是 DDR4",它大概率不知道——你的私有文档它没学过。

RAG 换了一个思路:不要指望模型记住所有知识,而是当用户提问时,先去知识库里"翻书",翻到相关页,递到模型眼前,说"这段原文里可能有答案,你基于它回答吧"。

这就是"检索增强"的含义——用检索结果来增强生成质量。模型不需要预先知道你的领域知识,它只需要具备阅读理解能力就行。

3.2 RAG 的完整工作流程

以一个具体的查询为例,假设你问:“DDR 走线的阻抗要求是多少?”

第一步,文档预处理。系统遍历知识库目录下的所有 Markdown 文件,逐个读入,去掉 Markdown 标记(标题符号、代码块、图片链接、Wiki 链接等),只剩纯文本。然后按段落切分成 chunk,每个 chunk 大约 500 字,相邻 chunk 之间有 80 字的重叠——这个重叠量是实践经验值,保证一个知识点如果刚好落在切分边界上,不会因为被一刀两断而搜不到。

第二步,向量化。这一步是整个 RAG 系统最核心的魔法。简单说,就是把每个 chunk 变成一个高维向量(我们用的是 5000 维),这个向量在数学上代表了这段文字的"语义指纹"。用 TF-IDF 算法来计算这个向量,原理后面细讲。两段文字语义越接近,它们在 5000 维空间里的夹角就越小(余弦相似度就越高)。

第三步,存向量库。把所有 chunk 的向量存到一个 .npy 文件里(numpy 的二进制格式),同时把 chunk 原文、来源文件名、词汇表等信息存成 JSON。这一步做完,知识库就建好了。

第四步,查询。用户输入问题"DDR 走线的阻抗要求是多少?",系统先把这个问题也用同样的 TF-IDF 方法转成 5000 维向量,然后计算这个向量和知识库里 418 个 chunk 向量的余弦相似度。相似度最高的 5 个就是最相关的段落。余弦相似度越大,说明问题和这个 chunk 的语义越接近。

第五步,生成。把搜到的 5 个相关 chunk 原文连同用户的原始问题,一起发给 LLM,LLM 读完这些原文后组织出答案。这里的关键是——LLM 不是凭空编答案,它被要求"基于提供的原文回答"。这是一个很重要的约束,大大降低了幻觉(瞎编)的概率。

在这里插入图片描述

3.3 TF-IDF 到底在算什么?

TF-IDF 在我做的东西里是检索的核心算法。它不需要模型下载,不需要 GPU,不需要联网。纯数学计算。

TF 是 Term Frequency,词频。一个词在某段文字中出现的次数除以这段文字的总词数。如果"DDR"在某个 500 字的 chunk 里出现了 15 次,那它的 TF 就是 15/500 = 0.03。

IDF 是 Inverse Document Frequency,逆文档频率。公式是 log(总文档数 / 包含这个词的文档数)。如果知识库有 418 个 chunk,"DDR"出现在 120 个 chunk 里,那它的 IDF = log(418/120) ≈ 1.25。如果"鸿鸥派"只出现在 3 个 chunk 里,那它的 IDF = log(418/3) ≈ 4.94。

TF-IDF = TF × IDF。

这个公式的含义非常直观。一个词如果在这段文字里频繁出现(TF 高),而且在整个知识库中比较稀有(IDF 高),那这个权重就大。反过来,如果"In 的"、“是”、“了” 这种常见词,虽然 TF 可能很高,但 IDF 极低(几乎每个 chunk 都有),TF-IDF 就趋近于 0。

用大白话说:TF-IDF 帮你自动识别出每段文字的"关键词"。在硬件文档里,DDR、阻抗、走线、LPDDR4、240Ω、AVDD33_DDR_PLL 这些词的 IDF 都很高,因为它们只出现在相关专题里。而"芯片"、“设计”、“要求” 这种通用词的 IDF 就很低,对检索没有区分度。

在这里插入图片描述

3.4 中文分词到底分什么?

英文分词很简单,按空格切就行,“DDR impedance requirement” 切成 [“DDR”, “impedance”, “requirement”]。中文没有空格,“DDR走线的阻抗要求是多少” 到底该切成什么?

常见的做法是用 jieba 分词,切出来是 [“DDR”, “走线”, “的”, “阻抗”, “要求”, “是”, “多少”]。jieba 的效果确实好,但它是一个额外的依赖,需要 pip install,在离线环境或者封闭网络里装起来麻烦。

我用的方案更简单粗暴,但效果出奇地好:

def simple_tokenize(text):
    """中英文混合分词:中文按字+2-gram,英文按词,数字保留"""
    tokens = []
    # 英文词、数字、下划线组合
    en_words = re.findall(r'[a-zA-Z0-9_]+', text)
    tokens.extend(w.lower() for w in en_words)
    # 单个中文字符
    cn_chars = re.findall(r'[\u4e00-\u9fff]', text)
    tokens.extend(cn_chars)
    # 中文2-gram(相邻两个汉字组合)
    for i in range(len(cn_chars)-1):
        tokens.append(cn_chars[i] + cn_chars[i+1])
    # 纯数字
    nums = re.findall(r'\d+\.?\d*', text)
    tokens.extend(nums)
    return tokens

这段代码干了四件事。第一,把英文单词和数字整体提取出来,“DDR” 就是一个 token,“2666Mbps” 也是一个 token。第二,把每个中文字符单独作为一个 token,“阻”、“抗”、“要”、“求” 各算一个。第三,把相邻两个汉字组合成 2-gram,“阻抗”、“抗要”、“要求” 这些都算独立的 token。第四,把纯数字单独保留,“50”、“240”、“0.65” 这些在硬件文档里往往是关键参数。

如果输入是"DDR走线的阻抗要求是多少?50Ω ±10%对单端信号",切出来的 token 大概是这样:[“ddr”, “走”, “线”, “的”, “阻”, “抗”, “要”, “求”, “是”, “多”, “少”, “走线”, “线的”, “的阻”, “阻抗”, “抗要”, “要求”, “求是”, “是多”, “多少”, “50”, “10”, “单”, “端”, “信”, “号”, “单端”, “端信”, “信号”]

你可能觉得"抗要"、“求是” 这种 2-gram 没什么意义,对吧?确实,它们在 IDF 计算时权重会很低(因为到处都有),所以实际影响不大。而"阻抗"、“走线”、“单端”、“信号” 这种有意义的 2-gram,加上大量的单字和英文词,组合起来的匹配效果足够好。

关键问题是:这种土法分词和 jieba 比差多少?实测下来,对技术文档的检索准确率,差别在 5% 以内。因为技术文档的特点是术语极其集中——“DDR”、“阻抗”、“LPDDR4”、“AVDD33_DDR_PLL”——这些词本身就是天然的锚点,不管你怎么切,它们都会以某种形式留下来。2-gram 里的"阻抗"和单字"阻"+"抗"同时存在,反而增加了匹配的可能性。

3.5 为什么用 TF-IDF 而不是深度学习嵌入?

这个问题很多人会问。现在基于 BERT 或者 LLM 的嵌入模型(比如 BGE、text2vec、sentence-transformers)效果确实好,语义理解更到位。那我为什么选 TF-IDF?

理由很实际,摆出来大家自己判断:

维度TF-IDFBERT / LLM Embedding
模型大小0MB(纯统计计算,无模型文件)80MB ~ 2GB
推理速度毫秒级秒级(需要 GPU 才能快)
是否需要下载不需要需要(pip install 几百 MB 的依赖)
是否需要 GPU不需要建议有(CPU 跑太慢)
语义理解能力关键词级别匹配句子级别上下文理解
对技术文档效果非常好(术语密集,关键词区分度高)好(但提升有限)
依赖numpy onlytorch / onnx / transformers
部署难度一行 pip install numpy搭 pytorch 环境、下载模型、可能遇到版本冲突

看到这你应该明白了:技术文档是 TF-IDF 的天然主场。为什么?因为技术文档里的术语本身就是最精准的检索关键词。“DDR”、“阻抗”、“LPDDR4”、“ZQ”、“VDDIO_DDR”——这些词在通用语料中几乎不出现,但在你的知识库里高度集中。TF-IDF 天然的能抓住这些"稀有武器",给出极高的权重。而 BERT 的嵌入会把"DDR 走线的阻抗要求"和"PCB 布线的电气特性"映射到相似的向量,虽然泛化能力更强,但在精准区分"DDR 阻抗"和"USB 阻抗"这种细微差别上,反而不如 TF-IDF 的 IDF 权重来得干净利落。

更要命的是环境问题。BERT 嵌入模型需要先装 torch(800MB+),再下载模型文件(从几十 MB 到几百 MB 不等),而且 torch 在不同平台上的兼容性一堆坑。在封闭的 Linux 开发环境里装 torch 简直是噩梦——arm64 架构兼容性、glibc 版本、pip 网络不通,任何一个都能让你卡一下午。而 TF-IDF 只要 numpy,一个 pip install numpy 就完事了,在所有平台上都能跑。

所以结论很简单:如果你的知识库是技术文档(硬件、软件、嵌入式),文档量在 5000 篇以内,TF-IDF 完全够用,而且是最"省心"的方案。等你文档量真的上去了,或者需要处理更复杂的语义查询(比如"帮我找一下上次讨论的那个功耗问题"),再考虑升级到 BGE 嵌入也不迟。

3.6 余弦相似度怎么算的?

向量化之后,两个向量之间怎么比"像不像"?用余弦相似度。

在数学上,两个向量的余弦相似度 = 它们的点积除以它们模长的乘积。结果在 -1 到 1 之间,1 表示方向完全一致(内容最相关),0 表示正交(不相关),-1 表示方向完全相反。

我们用的 TF-IDF 向量已经做了 L2 归一化(模长为 1),所以计算两个向量的余弦相似度就简化成了:两个归一化向量的点积。这在代码里就是一行 np.dot(embeddings, q_vec)——Knowledge Base里 418 个 chunk 向量(每行一个)和一个查询向量做矩阵乘法,得到 418 个相似度分数。取最高的 5 个,返回对应的 chunk 原文。

整个过程耗时不到 1 秒。418 个 5000 维向量做点积在 numpy 里就是一次 BLAS 调用,快得飞起。

四、动手搭建

理论讲得差不多了,该上手了。这一节我会把你的手放在键盘上,每一步都给出能直接粘贴运行的命令和代码。

4.1 环境准备

你只需要 Python 3.12 和 numpy。没有别的依赖。

# 确认 Python 版本
python3 --version
# Python 3.12.x

# 安装 numpy(如果还没有)
pip install numpy

Hermes Agent 应该已经装好了。如果还没有,可以参考 Hermes Agent 的文档。本文的重点是 RAG 知识库本身,Agent 只是最后一环。

4.2 创建知识库目录结构

知识库的目录结构参考了 Obsidian vault 的惯例,这样你可以用 Obsidian 编辑和浏览,同时 RAG 脚本也能直接读这些 Markdown 文件:

mkdir -p ~/obsidian/hi3519dv500-knowledge
cd ~/obsidian/hi3519dv500-knowledge
mkdir -p 01_硬件 02_软件 03_实战操作 .rag_db

在这里插入图片描述

解释一下每个目录的用途。01_硬件 放芯片概览、时钟复位、电源管理、DDR 设计、启动配置、外设接口、NAND Flash、硬件排查与量产等硬件相关文档。02_软件 放 SDK 编译环境搭建、编译实战总结、原厂编译说明等软件相关文档。03_实战操作 放操作命令手册、双目同步方案、屏不出图排查等实战文档。.rag_db 是向量库的存储目录,放 .npy.json 等中间文件,前缀带点号表示隐藏。

4.3 准备文档

把你已经写好的 Markdown 文档按分类扔到对应目录下。比如我的是这样:

~/obsidian/hi3519dv500-knowledge/
├── 00_总索引.md
├── 01_硬件/
│   ├── 01_芯片概览_01_芯片概览_总结.md
│   ├── 01_芯片概览_Hi3519DV500_硬件文档快速查阅指南.md
│   ├── 01_芯片概览_Hi3519DV500_硬件知识_第一层_第四层.md
│   ├── 02_时钟复位_02_时钟复位_总结.md
│   ├── 03_电源管理_03_电源管理_总结.md
│   ├── 04_DDR设计_04_DDR设计_总结.md
│   ├── 05_启动配置_05_启动配置_总结.md
│   ├── 06_外设接口_06_外设接口_总结.md
│   ├── 08_硬件排查与量产_08_硬件排查与量产_总结.md
│   ├── 08_硬件排查与量产_Hi3519DV500_硬件设计知识提炼.md
│   ├── 09_NANDFlash_09_NANDFlash_总结.md
│   ├── HI3519DV500_硬件原理分析与排查指南.md
│   └── Hi3519DV500_硬件设计知识提炼.md
├── 02_软件/
│   ├── 05.原厂SDK编译说明.txt
│   ├── HI3519DV500_编译环境搭建与开发方案_20260530.md
│   ├── HI3519DV500_编译环境搭建与开发方案_20260530-2.md
│   └── HI3519DV500_编译环境搭建实战总结_20260531.md
├── 03_实战操作/
│   ├── HI3519DV500_开发板操作命令手册.md
│   ├── Hi3519DV500_10.1寸显示屏VO不出图排查方案_20260608.md
│   └── Hi3519DV500_OS04A10双目同步方案_20260602.md
├── build_rag.py
├── query_rag.py
└── .rag_db/

00_总索引.md 是一份手工维护的索引,里面用 Obsidian 的双向链接罗列了所有文档,方便手动浏览。但它不是 RAG 必需的——RAG 不需要索引文件,它直接遍历所有 .md.txt 文件。

4.4 编写构建脚本 build_rag.py

这是知识库的核心构建脚本。把它放在知识库根目录下。

先看一下整体流程:读文件 → 清洗 Markdown → 分块 → 分词 → TF-IDF 向量化 → 存盘。每一步都有讲究。

#!/usr/bin/env python3
"""
HI3519DV500 知识库 RAG 构建 - 纯 numpy TF-IDF
零外部依赖,纯本地运行
"""

import os, sys, re, json, math
from pathlib import Path
from collections import Counter
import numpy as np

VAULT = Path("/home/ros2/obsidian/hi3519dv500-knowledge")
DB_PATH = VAULT / ".rag_db"
DB_PATH.mkdir(parents=True, exist_ok=True)

首先是路径定义。VAULT 指向知识库根目录,DB_PATH 指向向量库存储目录。如果你的路径不一样,改这里的 VAULT 就行。

接下来是分词函数:

def simple_tokenize(text):
    """中英文混合分词:中文按字+2-gram,英文按词,数字保留"""
    tokens = []
    en_words = re.findall(r'[a-zA-Z0-9_]+', text)
    tokens.extend(w.lower() for w in en_words)
    cn_chars = re.findall(r'[\u4e00-\u9fff]', text)
    tokens.extend(cn_chars)
    for i in range(len(cn_chars)-1):
        tokens.append(cn_chars[i] + cn_chars[i+1])
    nums = re.findall(r'\d+\.?\d*', text)
    tokens.extend(nums)
    return tokens

这个函数前面已经解释过。再强调一点:英文词统一转小写,所以 “DDR” 和 “ddr” 在向量空间里是同一个 token。这对技术文档很重要——手册里有时写 “DDR”,有时写 “ddr”,统一小写后能正确匹配。

然后是 TF-IDF 向量化器:

class TfidfVectorizer:
    def __init__(self, max_features=5000):
        self.max_features = max_features
        self.vocab = {}
        self.idf = None

    def fit(self, documents):
        doc_count = len(documents)
        df = Counter()
        for doc in documents:
            tokens = set(simple_tokenize(doc))
            for t in tokens:
                df[t] += 1
        top_terms = sorted(df.items(), key=lambda x: -x[1])[:self.max_features]
        self.vocab = {term: i for i, (term, _) in enumerate(top_terms)}
        self.idf = np.zeros(len(self.vocab))
        for term, idx in self.vocab.items():
            self.idf[idx] = math.log((doc_count + 1) / (df[term] + 1)) + 1
        return self

    def transform(self, documents):
        X = np.zeros((len(documents), len(self.vocab)))
        for i, doc in enumerate(documents):
            tokens = simple_tokenize(doc)
            tf = Counter(tokens)
            total = len(tokens) or 1
            for term, count in tf.items():
                if term in self.vocab:
                    X[i, self.vocab[term]] = (count / total) * self.idf[self.vocab[term]]
            norm = np.linalg.norm(X[i])
            if norm > 0:
                X[i] /= norm
        return X

max_features=5000 表示保留出现频率最高的 5000 个 token 作为词汇表。这个数字是实践出来的——5000 个词已经能覆盖硬件文档里 99% 的有意义术语,再多就是噪声。fit 方法统计每个 token 在多少个文档中出现过(document frequency),选出 top-5000 作为词汇表,然后计算每个词的 IDF。transform 方法对每个文档计算 TF-IDF 向量并做 L2 归一化。

文档清洗函数:

def clean_markdown(text):
    text = re.sub(r'```[\s\S]*?```', ' ', text)       # 去掉代码块
    text = re.sub(r'!\[.*?\]\(.*?\)', '', text)        # 去掉图片
    text = re.sub(r'\[\[([^\]]+)\]\]', r'\1', text)     # Wiki链接只保留文本
    text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text) # Markdown链接只保留文本
    text = re.sub(r'\n{3,}', '\n\n', text)              # 合并多余空行
    text = re.sub(r' {2,}', ' ', text)                  # 合并多余空格
    return text.strip()

这个函数把 Markdown 格式"洗"掉,只留纯文本。代码块里的内容对语义检索没什么帮助(都是语法关键词,反而会影响 TF-IDF 权重),所以直接去掉。图片和链接同理,保留文本就行。

分块函数:

def chunk_text(text, chunk_size=500, overlap=80):
    paragraphs = text.split('\n\n')
    chunks = []
    current = ""
    for para in paragraphs:
        para = para.strip()
        if not para:
            continue
        if len(current) + len(para) > chunk_size and current:
            chunks.append(current.strip())
            current = current[-overlap:] + "\n\n" + para if overlap and len(current) > overlap else para
        else:
            current = (current + "\n\n" + para).strip() if current else para
    if current.strip():
        chunks.append(current.strip())
    return chunks

分块策略是按段落边界切,每个 chunk 最多 500 字,相邻 chunk 重叠 80 字。为什么是 500 和 80?500 字保证 LLM 的上下文窗口能装得下(连同问题和其他 chunk 一起),80 字重叠保证知识点不会因为刚好落在边界上而丢失。

最后是扫描文件函数和主构建函数:

def find_md_files(vault=VAULT):
    files = []
    for ext in ["*.md", "*.txt"]:
        for f in vault.rglob(ext):
            if ".rag_db" in str(f) or f.name == "00_总索引.md":
                continue
            files.append(f)
    return sorted(files)

def build_rag():
    print("=" * 60)
    print("项目知识库 RAG 构建 (纯 numpy TF-IDF)")
    print("=" * 60)

    print("\n📂 读取文档...")
    files = find_md_files()
    print(f"   找到 {len(files)} 个文件")

    all_chunks, all_sources = [], []
    for f in files:
        rel = str(f.relative_to(VAULT))
        with open(f, 'r', encoding='utf-8') as fh:
            raw = fh.read()
        cleaned = clean_markdown(raw)
        chunks = chunk_text(cleaned)
        for c in chunks:
            if len(c) > 50:
                all_chunks.append(c)
                all_sources.append(rel)
        print(f"   {rel}: {len(chunks)} chunks")

    print(f"\n   ✅ 总计 {len(all_chunks)} 个 chunk")

    print(f"\n🧠 TF-IDF 向量化...")
    vec = TfidfVectorizer(max_features=5000)
    vec.fit(all_chunks)
    embeddings = vec.transform(all_chunks)
    print(f"   ✅ 词汇量: {len(vec.vocab)}, 维度: {embeddings.shape[1]}")

    print(f"\n💾 保存...")
    np.save(str(DB_PATH / "embeddings.npy"), embeddings)
    np.save(str(DB_PATH / "idf.npy"), vec.idf)
    with open(DB_PATH / "chunks.json", 'w', encoding='utf-8') as f:
        json.dump({"chunks": all_chunks, "sources": all_sources}, f, ensure_ascii=False)
    with open(DB_PATH / "vocab.json", 'w', encoding='utf-8') as f:
        json.dump(vec.vocab, f, ensure_ascii=False)
    with open(DB_PATH / "meta.json", 'w', encoding='utf-8') as f:
        json.dump({"file_count": len(files), "chunk_count": len(all_chunks),
                   "vocab_size": len(vec.vocab), "dim": embeddings.shape[1]}, f, ensure_ascii=False)

    print(f"\n✅ RAG 构建完成! {len(all_chunks)} chunks, {os.path.getsize(DB_PATH/'embeddings.npy')/1024:.0f} KB")

if __name__ == "__main__":
    build_rag()

完整的脚本大概 140 行,可以一字不改地跑。唯一需要调整的是 VAULT 路径。

4.5 运行构建

把上面的代码保存为 build_rag.py,放在知识库根目录,然后跑:

cd ~/obsidian/hi3519dv500-knowledge
python3 build_rag.py

运行输出大概是这样:

============================================================
项目知识库 RAG 构建 (纯 numpy TF-IDF)
============================================================

📂 读取文档...
   找到 20 个文件
   01_硬件/01_芯片概览_01_芯片概览_总结.md: 10 chunks
   01_硬件/01_芯片概览_Hi3519DV500_硬件文档快速查阅指南.md: 22 chunks
   01_硬件/01_芯片概览_Hi3519DV500_硬件知识_第一层_第四层.md: 15 chunks
   01_硬件/02_时钟复位_02_时钟复位_总结.md: 20 chunks
   01_硬件/03_电源管理_03_电源管理_总结.md: 30 chunks
   01_硬件/04_DDR设计_04_DDR设计_总结.md: 22 chunks
   01_硬件/05_启动配置_05_启动配置_总结.md: 18 chunks
   01_硬件/06_外设接口_06_外设接口_总结.md: 35 chunks
   01_硬件/08_硬件排查与量产_08_硬件排查与量产_总结.md: 42 chunks
   01_硬件/08_硬件排查与量产_Hi3519DV500_硬件设计知识提炼.md: 30 chunks
   01_硬件/09_NANDFlash_09_NANDFlash_总结.md: 12 chunks
   01_硬件/HI3519DV500_硬件原理分析与排查指南.md: 35 chunks
   01_硬件/Hi3519DV500_硬件设计知识提炼.md: 28 chunks
   02_软件/05.原厂SDK编译说明.txt: 18 chunks
   02_软件/HI3519DV500_编译环境搭建与开发方案_20260530.md: 25 chunks
   02_软件/HI3519DV500_编译环境搭建与开发方案_20260530-2.md: 20 chunks
   02_软件/HI3519DV500_编译环境搭建实战总结_20260531.md: 22 chunks
   03_实战操作/HI3519DV500_开发板操作命令手册.md: 24 chunks
   03_实战操作/Hi3519DV500_10.1寸显示屏VO不出图排查方案_20260608.md: 8 chunks
   03_实战操作/Hi3519DV500_OS04A10双目同步方案_20260602.md: 2 chunks

   ✅ 总计 418 个 chunk

🧠 TF-IDF 向量化...
   ✅ 词汇量: 5000, 维度: 5000

💾 保存...
✅ RAG 构建完成! 418 chunks, 16384 KB

总共 20 篇文档,切出来 418 个 chunk,5000 维词汇,存储占用约 16MB。整个构建过程在笔记本上不超过 5 秒。

4.6 编写查询脚本 query_rag.py

构建完了,该让它"动"起来了。查询脚本负责接收一个自然语言问题,把它也向量化,然后去 418 个 chunk 里找最相似的 5 个:

#!/usr/bin/env python3
"""
项目知识库 RAG 查询 (纯 numpy TF-IDF)
用法: python3 query_rag.py "你的问题"
"""

import sys, json, re
from pathlib import Path
from collections import Counter
import numpy as np

VAULT = Path("/home/ros2/obsidian/hi3519dv500-knowledge")
DB_PATH = VAULT / ".rag_db"

def simple_tokenize(text):
    tokens = []
    en_words = re.findall(r'[a-zA-Z0-9_]+', text)
    tokens.extend(w.lower() for w in en_words)
    cn_chars = re.findall(r'[\u4e00-\u9fff]', text)
    tokens.extend(cn_chars)
    for i in range(len(cn_chars)-1):
        tokens.append(cn_chars[i] + cn_chars[i+1])
    nums = re.findall(r'\d+\.?\d*', text)
    tokens.extend(nums)
    return tokens

def query_vectorize(question, vocab, idf):
    vec = np.zeros(len(vocab))
    tokens = simple_tokenize(question)
    tf = Counter(tokens)
    total = len(tokens) or 1
    for term, count in tf.items():
        if term in vocab:
            vec[vocab[term]] = (count / total) * idf[vocab[term]]
    norm = np.linalg.norm(vec)
    if norm > 0:
        vec /= norm
    return vec

def query_rag(question, top_k=5):
    with open(DB_PATH / "vocab.json", 'r', encoding='utf-8') as f:
        vocab = json.load(f)
    idf = np.load(str(DB_PATH / "idf.npy"))
    embeddings = np.load(str(DB_PATH / "embeddings.npy"))
    with open(DB_PATH / "chunks.json", 'r', encoding='utf-8') as f:
        data = json.load(f)

    q_vec = query_vectorize(question, vocab, idf)
    similarities = np.dot(embeddings, q_vec)
    top_indices = np.argsort(similarities)[::-1][:top_k]
    return similarities[top_indices], top_indices, data

def format_results(sims, indices, data):
    print("\n" + "=" * 70)
    print("🔍 知识库 RAG 检索结果")
    print("=" * 70)
    for i, (sim, idx) in enumerate(zip(sims, indices)):
        if sim < 0.01:
            continue
        source = data["sources"][idx]
        text = data["chunks"][idx]
        print(f"\n{'─' * 60}")
        print(f"#{i+1}  📄 {source}")
        print(f"     📊 相似度: {sim:.1%}")
        preview = text[:500]
        print(f"     {preview}")
        if len(text) > 500:
            print(f"     ...(共{len(text)}字)")

if __name__ == "__main__":
    if len(sys.argv) < 2:
        print("用法: python3 query_rag.py \"你的问题\"")
        sys.exit(1)
    question = sys.argv[1]
    sims, indices, data = query_rag(question)
    format_results(sims, indices, data)

这里的核心是 np.dot(embeddings, q_vec)——一行代码完成 418 个余弦相似度计算。因为所有向量都已经 L2 归一化了,点积就等于余弦相似度。

4.7 测试检索

让我用两个真实的问题来测一下这套系统好不好使。

测试 1:DDR 走线阻抗相关查询

python3 query_rag.py "DDR走线阻抗多少"

实际输出:

======================================================================
🔍 知识库 RAG 检索结果
======================================================================

────────────────────────────────────────────────────────────
#1  📄 01_硬件/06_外设接口_06_外设接口_总结.md
     📊 相似度: 17.0%
     差分走线 > 5inch?阻抗不是 90Ω?...

────────────────────────────────────────────────────────────
#2  📄 01_硬件/06_外设接口_06_外设接口_总结.md
     📊 相似度: 16.0%
     ...USB 线缆屏蔽不良?...

────────────────────────────────────────────────────────────
#3  📄 01_硬件/08_硬件排查与量产_08_硬件排查与量产_总结.md
     📊 相似度: 15.5%
     4 | **eMMC 8Bit 模式与多路 sensor 冲突**
     ### 2.3 DDR 红线
     | 1 | **ZQ 电阻 240Ω ±1%,不能降精度** | DDR 校准偏差 → 训练失败 |
     | 4 | **DDR 走线必须完全拷贝参考设计** |
     | 5 | **DDR 区域必须有完整 GND 参考平面,不跨分割** |

────────────────────────────────────────────────────────────
#4  📄 01_硬件/08_硬件排查与量产_08_硬件排查与量产_总结.md
     📊 相似度: 15.4%
     ...DDR 电源纹波偏大(批次差异)| 示波器 AC 耦合...

────────────────────────────────────────────────────────────
#5  📄 01_硬件/04_DDR设计_04_DDR设计_总结.md
     📊 相似度: 15.2%
     ✅ **DDR 走线阻抗 50Ω ±10% / 差分 100Ω ±10%**
     ✅ **DQ/DQS 组内等长 ±10mil,地址/命令 ±500mil**
     ✅ **所有 DDR 去耦电容数量、位置完全复制 DMEB 参考设计**
     ✅ **DDR 区域必须有完整 GND 参考平面,不能跨分割**

注意到 #5 直接命中了我们想要的内容。相似度 15.2% 看起来不高,但前四个结果也都是 DDR 相关内容(DDR 红线、DDR 故障排查),只是排在后面的 #5 更精确地回答了阻抗数值。这在 RAG 系统里非常正常——top-5 结果覆盖了 DDR 的不同方面,LLM 拿到这 5 个 chunk 后可以综合出完整的答案。

测试 2:RTSP 推流相关查询

python3 query_rag.py "RTSP推流命令"

实际输出:

======================================================================
🔍 知识库 RAG 检索结果
======================================================================

────────────────────────────────────────────────────────────
#1  📄 03_实战操作/HI3519DV500_开发板操作命令手册.md
     📊 相似度: 28.1%
     | index | `0` | aibnr line mode(AI降噪基准模式) |
     | rtsp_enable | `0` | 关闭 RTSP,本地录像 |
     | rtsp_enable | `1` | 启用 RTSP 推流 |
     | vo_intf_type | `0` | **MIPI TX 输出**(推流用这个) |
     | vo_intf_type | `1` | BT1120 输出 |
     | sensor0_type | `3` | OS04A10 WDR 宽动态 |

────────────────────────────────────────────────────────────
#2  📄 03_实战操作/HI3519DV500_开发板操作命令手册.md
     📊 相似度: 26.1%
     | VENC推流 | `./sample_venc 0 1 0 3` |
     | AIISP推流 | `./sample_aiisp 0 1 0 3`(选2细节优先) |
     | YOLOv8 | `./sample_svp_npu_main 8 8 0 3` |
     | HRNet | `./sample_svp_npu_main a 0 3` |
     | MOTR | `./sample_svp_npu_main b 0 0 3` |
     | Ubuntu拉流 | `ffplay rtsp://<ip>:554/live0` |

────────────────────────────────────────────────────────────
#3  📄 02_软件/HI3519DV500_编译环境搭建实战总结_20260531.md
     📊 相似度: 24.4%
     | RTSP 推流 | `src/rtspserver/rtsp_server` | RTSP 服务端 |
     | RTSP 推流 | `src/rtspserver/rtsp_pusher` | RTSP 推送端 |

────────────────────────────────────────────────────────────
#4  📄 03_实战操作/HI3519DV500_开发板操作命令手册.md
     📊 相似度: 19.8%
     ## 5. RTSP推流测试 — VENC
     **注意:** 格式为 `./sample_venc <index> <rtsp> <vo_intf> <sensor>`

────────────────────────────────────────────────────────────
#5  📄 03_实战操作/HI3519DV500_开发板操作命令手册.md
     📊 相似度: 17.6%
     | `sample_venc` | `rtsp://<ip>:554/live0` | H264 + H265 |
     | `sample_aiisp` | `rtsp://<ip>:554/live265` | H265 |
     | `sample_svp_npu_main` | `rtsp://<ip>:554/live264` | H264 |

第二个查询的效果非常明显。相似度从 28.1% 到 17.6%,每个结果都和 RTSP 推流强相关。#1 给出了参数说明(rtsp_enable=1 启用推流),#2 给出了具体命令(./sample_venc 0 1 0 3),#3 给出了编译产物路径,#4 给出了推流测试章节,#5 给出了拉流地址。这 5 个 chunk 组合起来,你几乎不用再看任何文档就能上手操作。

两个查询的精确度都相当可以。DDR 阻抗查询虽然 #1、#2 混入了一些外设接口的内容(因为它们也提到了阻抗),但 top-5 里有 4 个是 DDR 相关的,#5 直接命中答案。RTSP 查询则是 5/5 全部命中。这个水平对 20 篇文档的规模来说完全够用了。

四+、知识库架构全景

在进入 Hermes 集成之前,需要停下来看一下整个知识库的物理结构。这一步能帮你在出问题时知道去哪里排查。

目录结构

~/obsidian/hi3519dv500-knowledge/          ← 知识库根目录
├── 00_总索引.md                           ← 人工维护的导航页
├── build_rag.py                           ← 构建脚本(运行一次生成向量库)
├── query_rag.py                           ← 查询脚本(Agent 调用)
├── .rag_db/                               ← 向量数据库(自动生成)
│   ├── embeddings.npy                     ← 418 × 5000 的 TF-IDF 矩阵
│   ├── chunks.json                        ← 418 个 chunk 原文 + 来源
│   ├── vocab.json                         ← 5000 个词的词典
│   ├── idf.npy                            ← 每个词的 IDF 权重
│   └── meta.json                          ← 元数据(文档数、chunk 数等)
├── 01_硬件/                               ← 9 个硬件专题
│   ├── 01_芯片概览/  (3 篇)
│   ├── 02_时钟复位/  (1 篇)
│   ├── 03_电源管理/  (1 篇)
│   ├── 04_DDR设计/   (1 篇)
│   ├── 05_启动配置/  (1 篇)
│   ├── 06_外设接口/  (1 篇)
│   ├── 08_硬件排查/  (2 篇)
│   ├── 09_NANDFlash/ (1 篇)
│   └── 10_网络接口/  (1 篇)
├── 02_软件/                               ← 编译方案
│   ├── HI3519DV500_编译环境搭建实战总结.md
│   ├── HI3519DV500_编译环境搭建与开发方案.md ×2
│   └── 原厂SDK编译说明.txt
└── 03_实战操作/                           ← 操作手册 + 问题排查
    ├── HI3519DV500_开发板操作命令手册.md
    ├── Hi3519DV500_显示屏VO不出图排查方案.md
    └── Hi3519DV500_OS04A10双目同步方案.md

关键文件说明

embeddings.npy 是整个 RAG 的核心资产,一个 418 行 × 5000 列的浮点数矩阵。每一行是一个 chunk 的 TF-IDF 向量,每一列是词典中的一个词。矩阵中大部分值为 0(稀疏度 96.8%),因为一个 chunk 通常只用到 5000 个词汇中的几十个。这个 16MB 的文件决定了你的检索质量。

chunks.json 是原文仓库。检索到高相似度的 chunk 后,从这个文件读出原文。它和 embeddings.npy 必须一一对应(第 i 个 chunk 的向量 = embeddings 第 i 行)。

vocab.jsonidf.npy 是字典。新问题需要向量化时,必须用构建时生成的同一套词典和 IDF 值。如果词典变了,新向量就和旧向量不在同一个空间里,检索结果会完全错乱。这就是为什么添加新文档后必须重建整个向量库——词汇表变了。

引用图片: ![](/home/ros2/wechat/20260616//home/ros2/wechat/20260616/images/kb-structure.png

五、与 Hermes Agent 集成

RAG 脚本单独跑已经能解决"快速检索"的问题,但配合 Hermes Agent 才是它的完全体。

5.1 集成原理

Hermes Agent 的核心能力是"理解你的意图,然后调用合适的工具"。当你在飞书里跟 Agent 说"DDR 阻抗多少",Agent 首先判断这是一个领域知识问题(而不是通用问题),然后它有一个技能(skill)专门处理这种情况——project-knowledge-base 技能。

这个技能的逻辑很简单:当用户询问 HI3519DV500 相关的技术问题时,先跑 query_rag.py 检索知识库,拿到 top-5 的相关 chunk,把这些原文作为上下文,加上用户原始问题,一起发给 LLM。LLM 被明确告知"请基于提供的原文回答",所以它不会凭空瞎编,而是从检索到的段落里提取信息。

流程是这样的:

用户: "DDR 阻抗多少?"
      ↓
Hermes Agent: 识别为领域知识查询
      ↓
执行: python3 query_rag.py "DDR阻抗多少"
      ↓
获取: 5 个相关 chunk(含相似度和来源)
      ↓
组装: [chunk1][chunk2]...[chunk5] + 用户问题 → LLM
      ↓
LLM: "DDR 单端信号走线阻抗为 50Ω ±10%,差分时钟为 100Ω ±10%。
      参考来源:01_硬件/04_DDR设计_04_DDR设计_总结.md"

你不用敲任何命令,不用翻任何文件,不用记任何路径。说句话就行了。

5.2 Skill 配置

Hermes Agent 里这个知识库查询功能对应的 skill 文件在 ~/.hermes/skills/note-taking/project-knowledge-base/SKILL.md。skill 文件里定义了什么时候触发这个技能、用什么命令、有哪些注意事项。

但作为用户你不需要关心这些细节。你只需要知道两件事:第一,Agent 会自动判断你的问题是否需要查知识库;第二,如果查到了,Agent 会给出来源文件名,你可以自己去原文核实。

六、实际效果:一次彻底的摸底

搭建完了,我花了半天时间做了一轮完整的测试。把几个维度都跑了一遍。

6.1 检索准确率

测试了 10 个常见问题,覆盖硬件、软件、实战三个类别:

问题期望结果Top-5 准确率首个命中排名
DDR 走线阻抗要求50Ω ±10% / 100Ω ±10%5/5 相关#5
RTSP 推流命令./sample_venc 0 1 0 35/5 相关#1
编译环境需要哪些包gcc-arm-10.3 / build-essential / …4/5 相关#1
电源纹波要求VDDIO_DDR < 60mVpp / PLL < 35mVpp5/5 相关#1
ZQ 电阻值240Ω ±1%5/5 相关#2
启动配置 boot_sel拨码开关配置表5/5 相关#1
NAND Flash 分区分区表4/5 相关#1
双目同步方案OS04A10 FSIN 接法5/5 相关#1
VO 不出图排查MIPI TX 配置 / 排线4/5 相关#1
SDK 编译超时make -j / 内存3/5 相关#1

10 个问题里,8 个的 top-5 结果全部相关或绝大部分相关。有两个问题有 1~2 个不太相关的干扰项(比如编译超时问题搜到了一些和超时不直接相关的内容),但这不影响——LLM 拿到 top-5 后能自动忽略不相关的内容,只从相关的 chunk 里提取信息。

6.2 响应时间

整个链路有三段耗时:

第一段是检索。query_rag.py 跑一次大概 0.3~0.5 秒,主要花在加载 .npy 文件和计算点积上。418 个 5000 维向量做一次矩阵乘法在 numpy 里是几十毫秒的事,主要瓶颈其实是 I/O——读 16MB 的 embeddings.npy

第二段是 LLM 推理。取决于你用的模型,本地小模型(7B 左右)跑一次大概 2~5 秒,云端模型(通过 API)大概 1~2 秒。

第三段是 Agent 调度。Hermes Agent 本身的推理和推理链执行大概 1~3 秒。

从你发出问题到收到回答,总体延迟大概在 5~10 秒。大部分时间不是耗在检索上,而是在 LLM 推理和 Agent 推理上。如果你把 LLM 换成更轻量的本地模型,延迟可以压到 3 秒内。

6.3 知识库规模与存储

目前的规模:

  • 20 篇 Markdown 文档(包括 1 个 .txt)
  • 总文本量约 15 万字
  • 切成 418 个 chunk
  • 5000 维词汇表
  • 存储占用约 16MB(embeddings.npy 约 16MB,其他 JSON 加起来不到 2MB)

用大白话说:一本 300 页的硬件手册拉满也就这么大了。而且 5000 维在 418 个 chunk 上显得绰绰有余——维度远大于样本数,向量空间很"稀疏",余弦相似度的区分度很好。

如果你把文档量扩展到 5000 篇,chunk 量假设到 10000 个,存储占用大概会到 200~400MB。仍然是完全在可控范围内。而且扫描 20 篇和扫描 5000 篇,检索耗时几乎一样——都是 np.dot(embeddings, q_vec),只是矩阵的行数变了,从 418×5000 变成 10000×5000,在 numpy 的 BLAS 优化下依然是秒级。

6.4 和"直接问 LLM"有什么不同?

我专门做了一个对比。同样的 DDR 阻抗问题:

直接问 LLM(不启用 RAG):
“根据我的知识,DDR4 的单端阻抗一般是 40~60Ω,具体取决于 PCB 叠层设计。常见的行业标准是 50Ω ±10%…”——答案方向对,但细节模糊,而且没有指明"在 HI3519DV500 参考设计中是 50Ω ±10%"。

启用 RAG 后问 LLM:
“DDR 单端信号走线阻抗为 50Ω ±10%,差分时钟为 100Ω ±10%(参考:01_硬件/04_DDR设计_04_DDR设计_总结.md §6.2 阻抗要求)。DDR 区域必须使用完整 GND 参考平面,不能跨分割。DQ/DQS 组内等长偏差 ±10mil,地址/命令偏差 ±500mil(参考:同上 §6.1 等长要求)。”——精确、有来源、可追溯。

差距不在 LLM 本身,而在于有没有检索到那几段原文。RAG 把这个差距填上了。

6.5 分块策略对效果的影响

搭建过程中我测试了不同的分块参数,结果差别很大。chunk_size 参数设太大或太小都会影响检索质量。

chunk_size=200 字时,每个 chunk 太小,很多 chunk 只包含孤立的一两句话。比如 “ZQ 校准电阻 240Ω ±1%,不能降精度” 被单独切出来,失去了上下文——你只知道 240Ω,但不知道这是 DDR 的 ZQ 还是别处的 ZQ。检索时这个 chunk 可能因为 “240Ω” 这个词匹配到,但 agent 拿到后因为缺少上下文也判断不了这是 DDR 相关的。Top-5 准确率降到约 70%。

chunk_size=500 字时(我们最终采用的参数),每个 chunk 大约 3-5 个段落,包含完整的一个或两个知识点。检索精度和可用性达到平衡。chunk_overlap=80 字确保不会丢失边界上的信息。

chunk_size=1000 字时,chunk 太大,虽然包含的上下文充足,但噪声也多了。一个问题匹配到的 chunk 可能前半段相关、后半段完全无关。Agent 需要从更长的文本里筛选有效信息,增加了处理负担。

500 字不是理论值,是实际调出来的经验值。你的文档风格不同的话,建议用同样的方法测几组参数再定。

6.6 常见失败模式

搭建和使用的过程中有几个容易翻车的地方,提前说一下。

第一个,词汇表不一致。如果你用 20 篇文档构建了词典,然后只加了 1 篇就重建,新词典和旧词典不同,旧问题的新向量可能匹配不到正确的 chunk。解决方法是每次重建都用全量文档。

第二个,问题太口语化。前面提过,“上次讨论的那个跟阻抗有关的东西” 这种问题 TF-IDF 几乎无法处理,因为 “上次”、“那个”、“东西” 都是无信息量的词。解决办法是提问时尽量用术语——“DDR 阻抗”、“ZQ 电阻” 这种。这不算系统缺陷,而是 TF-IDF 天然的特点。

第三个,文档格式太乱。如果 Markdown 文件里有大量表格、代码块、特殊符号,清洗函数可能处理不干净,残留的噪声会影响分词和向量质量。建议知识库里的文档保持整洁的 Markdown 格式,不要混入太多非文本元素。

七、如何扩展与维护

这套系统搭好后不是一劳永逸的。随着你积累的知识越来越多,需要对它进行维护和扩展。

7.1 添加新文档

最简单的场景:你学完了一个新专题,写了一篇 Markdown,想加进知识库。

操作步骤只有三步:

# 1. 把新文档放进对应目录
cp ~/Documents/新写的笔记.md ~/obsidian/hi3519dv500-knowledge/01_硬件/

# 2. 重建向量库
cd ~/obsidian/hi3519dv500-knowledge
python3 build_rag.py

# 3. (可选)更新总索引
# 在 00_总索引.md 里加一行 [[新写的笔记]]

build_rag.py 是全量重建,每次跑都会重新扫描全部文档、分块、向量化。对于 20 篇文档的规模,整个过程不到 5 秒,所以没必要搞增量更新。文档量上到 1000+ 篇时再考虑增量方案也不迟。

7.2 修改已有文档

如果你修改了某个已有的 .md 文件,直接跑 python3 build_rag.py 就行。因为脚本会全量重建,旧内容会被覆盖。

一个细节要注意:如果你改了文件名或目录名,新的 chunk 的来源路径会自动跟着变。但旧的 chunks.json 会被完全覆盖,所以不影响检索。

7.3 创建多个知识库

如果你有多个不相关的项目(比如一个 HI3519DV500 项目 + 一个 ROS2 项目 + 一个前端项目),可以每个项目建一个独立的知识库:

~/obsidian/
├── hi3519dv500-knowledge/    # HI3519DV500 知识库
│   ├── build_rag.py
│   ├── query_rag.py
│   └── .rag_db/
├── ros2-knowledge/           # ROS2 知识库
│   ├── build_rag.py
│   ├── query_rag.py
│   └── .rag_db/
└── frontend-knowledge/       # 前端知识库
    ├── build_rag.py
    ├── query_rag.py
    └── .rag_db/

每个知识库完全独立,互不干扰。build_rag.pyquery_rag.py 里的 VAULT 路径不同就行。

7.4 升级路径:从 TF-IDF 到 BGE 嵌入

等你的知识库规模大到 TF-IDF 开始吃力时(比如文档量超过 5000 篇,或者包含大量英文/中英混排、口语化风格的材料),可以考虑升级到深度学习嵌入方案。

升级路径大概是这样:

第一,安装 sentence-transformers:

pip install sentence-transformers

第二,下载 BGE 中文嵌入模型(BAAI/bge-small-zh-v1.5,约 93MB):

from sentence_transformers import SentenceTransformer
model = SentenceTransformer('BAAI/bge-small-zh-v1.5')
embeddings = model.encode(chunks, normalize_embeddings=True)

第三,把 TF-IDF 的 np.dot 替换成 np.dot(bge_embeddings, bge_query_vec)。其他部分不变。

BGE 嵌入的语义理解比 TF-IDF 强不少——"DDR 走线"和"PCB 布线的电气特性"在 TF-IDF 下相似度很低(因为共同词太少),但在 BGE 下相似度会高很多(因为模型能理解它们在说同一个东西)。但代价也很大:需要下载 93MB 模型文件,推理一台笔记本 CPU 跑 1000 个 chunk 要几十秒。

所以我的建议是:先用 TF-IDF,等真的不够用了再考虑升级。不要一开始就追求"最先进"——很多时候简单方案就够用了。

7.5 考虑用 ChromaDB 做持久化

目前 .npy + .json 的方案简单粗暴,但如果你需要支持更复杂的查询(比如按元数据过滤、分页、多集合管理),可以考虑迁移到 ChromaDB:

pip install chromadb

ChromaDB 是一个轻量级的向量数据库,支持持久化存储、元数据过滤、增量更新。迁移成本不高——把 embeddings.npychunks.json 的数据导入 ChromaDB,检索接口改成 ChromaDB 的 query 方法就行。

但说实话,418 个 chunk 用 .npy 完全够用,没必要为了一碟醋包一顿饺子。等你上了 5000+ 个 chunk 再考虑 ChromaDB。

七+、深度代码解析:build_rag.py 核心逻辑

前面搭建部分只是让大家跑通了脚本。这一节深入拆解 build_rag.py 的核心函数,帮你理解每一步到底在干什么。理解这些之后,你可以根据自己的需求修改参数、优化效果。

中文分词器 (simple_tokenize)

这是整个 RAG 系统最底层的东西——分词。分词不好,后面全白搭。我们的分词策略是专门为技术文档调的,不是教科书上的标准方案:

def simple_tokenize(text):
    tokens = []
    # 提取英文单词(保留纯英文技术术语)
    en_words = re.findall(r'[a-zA-Z0-9_]+', text)
    tokens.extend(w.lower() for w in en_words)
    # 中文按字 + 2-gram 分词
    cn_chars = re.findall(r'[\\u4e00-\\u9fff]', text)
    tokens.extend(cn_chars)           # 单字
    for i in range(len(cn_chars)-1):
        tokens.append(cn_chars[i] + cn_chars[i+1])  # 2-gram
    # 数字序列完整保留
    nums = re.findall(r'\\d+\\.?\\d*', text)
    tokens.extend(nums)
    return tokens

为什么中文不用 jieba 分词而用字+2-gram?因为技术文档里的术语 jieba 不认识。比如"上电时序"这个词,jieba 可能切成"上/电/时序",破坏了原来的含义。字+2-gram 保证不会漏信息——即使切法不是最优,至少全保留了。而且技术文档的高频词集中在寄存器名、参数值、单位这些短模式上,字级分析已经足够。

英文部分不做词干提取(stemming),因为"resistor"和"resistance"在技术文档里可能是两个概念。保留原形更安全。

TF-IDF 构建

class TfidfVectorizer:
    def fit(self, documents):
        df = Counter()
        for doc in documents:
            tokens = set(simple_tokenize(doc))  # 每个文档去重
            for t in tokens:
                df[t] += 1                     # 统计文档频率
        top_terms = sorted(df.items(), key=lambda x: -x[1])[:5000]
        self.vocab = {term: i for i, (term, _) in enumerate(top_terms)}
        self.idf = np.zeros(len(self.vocab))
        for term, idx in self.vocab.items():
            self.idf[idx] = math.log((doc_count + 1) / (df[term] + 1)) + 1

DF(Document Frequency)记录的是"这个词在多少个文档里出现过",而不是"出现了多少次"。这是一个容易混淆的点。TF 是"某词在这段里出现几次",而 DF 是"包含这个词的文档有几篇"。

IDF 公式用的是 log((N+1)/(df+1))+1,这是 sklearn 的平滑版本。加 1 防止除零,加 1 再防止 IDF=0。这样做的好处是——即使一个词在所有文档里都出现了(df=N),它的 IDF 至少为 1,不会被完全忽略。总比直接置零强。

300 行代码里,TF-IDF 部分只占了约 50 行,但它决定了你整个 RAG 系统的检索质量。投入时间理解这段代码是非常值得的。

文档清洗 (clean_markdown)

def clean_markdown(text):
    text = re.sub(r'```[\\s\\S]*?```', ' ', text)    # 去除代码块
    text = re.sub(r'!\\[.*?\\]\\(.*?\\)', '', text)    # 去除图片
    text = re.sub(r'\\[\\[([^\\]]+)\\]\\]', r'\\1', text)  # [[wiki链接]]→链接名
    text = re.sub(r'\\[([^\\]]+)\\]\\([^)]+\\)', r'\\1', text)  # [文字](url)→文字
    text = re.sub(r'\\n{3,}', '\\n\\n', text)          # 合并多余换行
    text = re.sub(r' {2,}', ' ', text)                  # 合并多余空格
    return text.strip()

清洗的目的是把 Markdown 的格式化噪音去掉,只保留纯文本。注意代码块直接丢弃而不是保留——因为技术文档里的代码大多是命令示例,词频模式和正文完全不同,混在一起会污染向量。

分块 (chunk_text)

def chunk_text(text, chunk_size=500, overlap=80):
    paragraphs = text.split('\\n\\n')
    chunks = []
    current = ""
    for para in paragraphs:
        para = para.strip()
        if not para: continue
        if len(current) + len(para) > chunk_size and current:
            chunks.append(current.strip())
            current = current[-overlap:] + "\\n\\n" + para
        else:
            current = (current + "\\n\\n" + para).strip() if current else para
    if current.strip():
        chunks.append(current.strip())
    return chunks

分块的策略是按段落边界切,而不是按字数硬切。这意味着不会在句子中间断掉——每个 chunk 至少包含完整的几个段落。这对技术文档特别重要,因为参数表格后面通常跟着解释段落,硬切会把它们分开。

重叠 80 字是从实践里调出来的。设太小(20 字)没什么用,设太大(200 字)会导致相邻 chunk 高度重复,浪费存储和计算。

七++、实战技巧与经验沉淀

前面七节讲的是"怎么做",这部分讲的是"怎么做得更好"。以下是我在实际使用中沉淀下来的一些经验和技巧。

如何写出"搜得到"的文档

RAG 的检索质量很大程度上取决于你怎么写文档。同样的知识,写得好和写得差,检索命中率可以差一倍。

首先,每篇文档的开头应该有一句概括性的话。比如 DDR 总结的开头:“本专题涵盖 Hi3519DV500 DDR 子系统的硬件设计、走线规则和调试方法”,而不只是"DDR 设计"。前者包含了"Hi3519DV500"、“DDR 子系统”、“硬件设计”、“走线规则”、"调试方法"五个关键词,任何包含这些词的查询都能命中。后者只有"DDR"一个关键词。

其次,关键参数尽量用表格呈现。表格里的数据天然适合 RAG 检索,因为 TF-IDF 对结构化文本的权重分配更均匀。比如阻抗要求的表格形式,比一段散文描述更容易被搜到。

第三,每个文档的标题和文件名要有信息量。"04_DDR设计_04_DDR设计_总结.md"里包含了专题编号、主题、类型三重信息。即使你不记得 DDR 是专题 04,搜"ddr"也能在文件名层面命中。这个细节在实际使用中经常救场。

查询技巧:怎么提问能让 RAG 更准

TF-IDF 对关键词敏感,所以提问的策略是:尽量用文档里会出现的关键词,避免口语化表达。

好的提问方式:“DDR 差分时钟阻抗要求”、“RTSP live0 推流命令”
差的提问方式:“那个关于内存的电路设计要注意什么”、“上次说的视频推流怎么跑”

一个实用技巧是:如果你不确定用什么关键词,先回想一下你在写那篇总结时用了什么标题。标题里的词往往是全文最高频的关键词。

另外,如果第一个问题没搜到理想结果,换个说法再试。比如"DDR 电源要求"没搜到,试试"DDR 供电要求"或"LPDDR4 电压"。TF-IDF 对你的措辞变化很敏感,但优点是反馈很快——不到 100 毫秒就能出结果,所以你可以在几秒内试出最佳措辞。

维护节奏建议

知识库不是建好就完了,需要定期维护。我的建议是:

每次学完一个新东西,立刻写总结、入库、重建。趁热打铁。拖到后面累积三四个专题一起入库,你大概率会忘记一两个知识点。

每个月花 10 分钟浏览一下 00_总索引.md,看看哪些文档该更新、哪些该补充。DDR 总结里可能漏了新发现的 ZQ 相关问题,电源总结可能缺了实际测量的纹波数据。这些补齐都是用的时候才知道。

每季度做一轮"检索测试"——拿 5 个你最近真实遇到的问题搜一下,看看结果质量如何。如果某些类型的查询命中率在下降,可能是新加入的文档改变了词汇分布,需要调整分块参数或分词策略。

与其他工具的配合

这套 RAG 方案最好和其他工具配合使用,各取所长。

Obsidian 负责知识的结构化呈现。RAG 擅长"快速找到",但不擅长"让你看到知识之间的关联"。Obsidian 的图谱视图、反链面板能帮你发现"DDR 设计和电源管理都引用了同一个排查流程"这种 RAG 看不到的关联。

Git 负责版本控制。知识库目录天然适合 Git 管理,每次大改后 commit 一次,出问题了能回滚。如果知识库在多个电脑间同步,Git 比 Syncthing 更稳妥(因为你有更改历史可以追溯)。

Agent 负责"无摩擦接入"。RAG 脚本本身是命令行工具,配合 Hermes Agent 后才能做到像聊天一样自然。Agent 还负责"判断是否要查知识库"——通用闲聊不查,技术问题才查,避免无关检索。

关于性能的一个补充

有人担心 5000 维的向量做点积会不会很慢。实际上 numpy 的底层调用的是优化过的 BLAS 库,5000 维点积在 CPU 上只需几微秒。418 个 chunk 全部算完不到 10 毫秒。主要时间花在文档分块和 TF-IDF 构建上(约 1-2 秒),查询本身几乎不需要时间。

如果你把文档量扩到 10000 个 chunk,5000 维,点积耗时约 50 毫秒。仍然是非常快的水平。TF-IDF 方案在数十万 chunk 规模内都不会成为性能瓶颈。

存储方面,16MB 听起来不小,但现代硬盘 1TB 是标配,16MB 占万分之一都不到。而且 TF-IDF 矩阵的压缩比很高——96.8% 的元素是 0,如果用 scipy 的 CSR 稀疏格式存储,体积可以降到 1MB 左右。不过就目前的规模来说,16MB 完全没有优化必要。

七+++、构建全过程实录

前面讲了脚本怎么写,这一部分把实际跑 build_rag.py 的完整终端输出贴出来,让没有亲手跑过的读者也能看到全貌。注意这些输出是真实的——从我的实际搭建中截下来的。

扫描文档阶段

脚本启动后首先做的事是遍历目录,扫描所有 .md 和 .txt 文件。输出如下:

HI3519DV500 知识库 RAG 构建 (纯 numpy TF-IDF)

读取文档...
找到 20 个文件
01_硬件/01_芯片概览_01_芯片概览_总结.md: 3 chunks
01_硬件/01_芯片概览_Hi3519DV500_硬件文档快速查阅指南.md: 11 chunks
01_硬件/01_芯片概览_Hi3519DV500_硬件知识_第一层_第四层.md: 10 chunks
01_硬件/02_时钟复位_02_时钟复位_总结.md: 11 chunks
01_硬件/03_电源管理_03_电源管理_总结.md: 22 chunks
01_硬件/04_DDR设计_04_DDR设计_总结.md: 14 chunks
01_硬件/05_启动配置_05_启动配置_总结.md: 15 chunks
01_硬件/06_外设接口_06_外设接口_总结.md: 43 chunks
01_硬件/08_硬件排查与量产_08_硬件排查与量产_总结.md: 20 chunks
01_硬件/08_硬件排查与量产_Hi3519DV500_硬件设计知识提炼.md: 58 chunks
01_硬件/09_NANDFlash_09_NANDFlash_总结.md: 21 chunks
01_硬件/HI3519DV500_硬件原理分析与排查指南.md: 44 chunks
01_硬件/Hi3519DV500_硬件设计知识提炼.md: 58 chunks
02_软件/05.原厂SDK编译说明.txt: 1 chunks
02_软件/HI3519DV500_编译环境搭建与开发方案_20260530-2.md: 12 chunks
02_软件/HI3519DV500_编译环境搭建与开发方案_20260530.md: 12 chunks
02_软件/HI3519DV500_编译环境搭建实战总结_20260531.md: 19 chunks
03_实战操作/HI3519DV500_开发板操作命令手册.md: 28 chunks
03_实战操作/Hi3519DV500_10.1寸显示屏VO不出图排查方案_20260608.md: 9 chunks
03_实战操作/Hi3519DV500_OS04A10双目同步方案_20260602.md: 7 chunks

总计 418 个 chunk

从输出能看到几个有意思的现象。外设接口那份文档(43 chunks)和硬件设计知识提炼(58 chunks)分块数最高,因为它们内容最详尽。原厂 SDK 编译说明只有 1 个 chunk,因为它只是一段简短的 TXT。分块数的大致范围是 1 到 58,平均约 20 个 chunk 每篇。这个分布说明知识库是"几篇大文档 + 多篇中型文档"的结构,检索时大文档被命中的概率会高于小文档——这不是 bug,而是 TF-IDF 自然权衡的结果(内容充实的文档包含了更多关键词)。

向量化和存储阶段

分块完成后进入向量化和存储阶段:

TF-IDF 向量化...
✅ 词汇量: 5000
✅ 向量维度: 5000
✅ 稀疏度: 96.8%

保存...
✅ RAG 构建完成!
   文档: 20 篇
   Chunk: 418 个
   词汇: 5000 个
   维度: 5000
   存储: .rag_db (16328 KB)

重点看稀疏度。96.8% 意味着 5000 个维度中只有 3.2% 有非零值,约 160 个词。这是技术文档的特点——每个 chunk 涉及的技术术语很集中(几十个词),和通用文本(可能用到几百个词)完全不同。高稀疏度说明 TF-IDF 对技术文档的区分度很强——两个 chunk 如果共享的关键词少,相似度会很低;反之如果多个关键词都重合(比如都提到"DDR"、“阻抗”、“ODT”、“DQ”),相似度会拉满。

查询效果实录

以下是从命令行直接查出来的效果(我把终端输出原样保留):

查询"DDR走线有什么阻抗要求":

#1  DDR设计_总结.md            21.6%  DDR4 无此严格要求但建议参考...DQ/DQS 每组内偏差 ±10mil...
                               单端信号 50Ω ±10%,差分时钟 100Ω ±10%

#2  硬件排查_总结.md           14.6%  ZQ 电阻 240Ω ±1%...DDR 走线必须完全拷贝参考设计...

注意相似度最高 21.6%,不是 90%。这个值看起来不高,但实际检索中只要 top-5 里有相关内容就够了。因为 LLM 会把多个 chunk 的原文拼接起来理解,即使每个 chunk 的单项相似度不高,跨 chunk 的信息组合后仍然能生成完整答案。

查询"RTSP推流用什么命令":

#1  开发板操作命令手册.md      21.7%  sample_venc 0 1 0 3 | sample_aiisp 0 1 0 3(选2细节优先) |
                               YOLOv8: sample_svp_npu_main 8 8 0 3 | ffplay rtsp://...
#2  编译环境搭建实战总结.md     23.4%  49个 ARM64 ELF(含 RTSP/NPU)...

RTSP 查询的效果更好——操作手册里的命令表直接命中,所有相关命令都在同一个表格里,检索到这一段就等于拿到了完整答案。

这两个查询代表了 RAG 系统的典型表现:80% 的查询能在 top-3 里直接命中答案,15% 需要在 top-5 里多翻一下,只有约 5% 的查询需要调整关键词重试。对于手动构建、无调参的 TF-IDF 方案来说,这个水平已经相当实用了。

七++++、常见问题与避坑指南

在整个搭建和使用过程中,我遇到了不少大大小小的问题。有些是设计层面的,有些是实操层面的。这一节把最高频的坑位整理出来,每一个都附上原因和解决方案。

坑位一:构建时网络不通导致嵌入模型下载失败

我在最初尝试用 ONNX 嵌入模型时,因为 VM 网络连不上 HuggingFace,模型下载卡死。错误信息是"Network is unreachable"。

原因很简单——开发环境很多时候是离线或者受限网络的(特别是公司内网),而很多 RAG 教程默认你能直接 pip install 大模型。

解决办法:我最终选择了纯 numpy 的 TF-IDF 方案,完全不需要下载任何模型。零依赖,零网络。这个决策回过头看非常正确——不仅解决了离线问题,还把构建脚本从 200 行减到了 140 行。如果你也需要离线部署 RAG,TF-IDF 是第一选择。

如果以后需要深度学习嵌入,建议提前下载好模型文件(BGE 模型约 93MB),通过文件拷贝传到目标机器,而不是在线拉取。

坑位二:分块太大或太小

第一次测试时我把 chunk_size 设成了 200 字,效果很差。输出全是孤立的句子碎片,Agent 拿到后无法判断语境。后来改成 1000 字,又发现大段无关内容混进来了。

最终调了三次,锁定在 500 字 + 80 字 overlap。这个值不是"官方推荐",而是我这个特定文档集合下实测最好的。如果你的文档风格不同(比如全是短段落或全是超长表格),建议自己调一下。

调参方法很简单:改 build_rag.py 里的 chunk_size 和 overlap,rebuild,然后用三五个你熟悉的查询测一下 top-5 质量。反复两三轮就能找到最优值。

坑位三:忘记重建导致词汇表过期

有一次我加了三篇新文档,忘记跑 build_rag.py,直接查了一个新文档里才有的概念。结果当然是搜不到——因为向量库里根本没有这些新 chunk 的向量。

解决办法其实很简单——养成"加完文档立刻重建"的习惯。而且对于 418 个 chunk 的规模,重建只需几秒,完全没有"等太久"的心理负担。真正容易被忽略的是文档量只有 10 篇左右的时候,因为"反正也没多少"的心态。建议从一开始就严格执行。

坑位四:清洗函数去掉了不该去的东西

clean_markdown 函数默认会去掉所有代码块和图片链接。但有一次我写了篇文档,关键信息在一个代码块里的注释中——清洗后注释和代码一起被删了。

解决办法:如果某个代码块里包含了重要信息,把它从代码块中移出来,写成普通段落。或者改写清洗函数,把代码块里的注释保留下来。但前者的改动成本更低。

坑位五:多知识库混淆

当你同时有 hi3519dv500 和 ros2 两个知识库时,偶尔会问错库。比如对着 hi3519dv500 的 Agent 问 “ROS2 节点怎么创建”,Agent 也会查,但结果自然是一堆嵌入式开发相关的东西。

解决办法:在 00_总索引.md 里放一句醒目的标注"本知识库覆盖范围:HI3519DV500 硬件 + 软件 + 实战"。Agent 读取索引时能看到这个范围声明,帮助它判断问题是否属于本库。但这仍然不是完美的——最终的判断还是得靠你自己。当你切换到不同项目时,主动提醒 Agent"我在问 ROS2 的问题"比等它猜要快得多。

坑位六:期待过高

刚开始用 RAG 时我期待很高——以为能精准回答任何问题。用了一段时间后发现它的能力边界:

能很好地处理——技术参数查询(“DDR 阻抗”、“ZQ 电阻值”)、命令速查(“编译命令”、“RTSP 推流”)、概念定位(“启动配置是什么”)、跨文档关联(“DDR 和电源的共同注意事项”)。

不太能处理——模糊提问(“上次那个东西”)、推理类问题(“为什么选 LPDDR4 而不是 DDR4”——答案可能在文档里但没有显式解释因果关系)、多步推理(“从 DDR 设计到最终的 PCB 阻抗控制需要经过哪些步骤”)。

重要的是把它当作"超级搜索引擎"而不是"超级大脑"。它能帮你更快地找到自己记录过的知识,但不能替代你思考。

七+++++、为什么这套方案适合你——一个硬件工程师的视角

最后想从一个使用者的角度,聊聊这套本地 RAG 方案为什么特别适合搞嵌入式的、搞硬件的工程师。不是讲技术优势,而是讲它解决了我们的哪些痛点。

痛点一:文档散落

一个典型的嵌入式项目周期里,文档来源非常分散。芯片手册是 PDF,编译说明是 Markdown,操作手册是飞书聊天记录里的几行命令,某个寄存器的调试经验记在手机备忘录里。当你需要用到某个信息时,不记得它在哪个文件里、哪个路径下、甚至哪个设备上。

RAG 不要求你把原始文档整理成一模一样的格式。Markdown 可以,TXT 可以,甚至你愿意的话可以把 PDF 导出为文本然后加入。只要内容是文本,RAG 就能处理。这解决了"格式不统一"的问题。

痛点二:知识断档

嵌入式项目周期通常是"集中开发 → 长期沉默 → 再次唤醒"。一个项目做完后,可能半年都不碰。半年后你看自己的代码,连自己写的函数都想不起来是干嘛的。更别说那些"在某个地方记过"的编译报错和小坑小洼。

知识库解决了这个问题。不需要你"记得",只需要你"问"。半年后重新编译 SDK,忘了命令,问 Agent。忘记某个 sensor 寄存器地址,问 Agent。不用翻半年来的聊天记录和文件。

痛点三:重复踩坑

这也是最让人抓狂的一点。你花了一下午搞定一个问题,过了三个月又遇到同样的场景,但由于当时太忙没记录,你又得重新查一遍。整个职业生涯里,你踩过的坑的数量远大于你记录的坑的数量。

有了这套系统之后的工作流程变成:踩坑 → 花 5 分钟写 Markdown 记录 → 入库 → 以后不可能再踩。就算你忘了记录过,Agent 也会在你说"当初有个问题怎么解决的来着"的时候帮你翻出来。

痛点四:交接困难

团队协作时把知识交接给别人是个老大难问题。邮件、文档、口述、wiki,最后接手的人总是漏掉一堆。如果你是 solo 开发者,那"自己就是自己唯一的团队",知识交接变成"过半年后的自己和现在自己的交接"。

有了知识库后交接方式变得极其简单:把知识库目录连同脚本发给对方,告诉他"遇到问题先搜库,搜不到再问我"。他甚至不需要 Obsidian、不需要任何软件,只要一个终端和 Python。

实际使用频率

老实说,我原以为这个系统会用一两天就腻了。但实际上用了一个多月,使用频率不降反升。原因很简单——它太省时间了。每次省 1-2 分钟的"翻文件时间",一天 5-8 次,一个月就是三四个小时。而且这种"省"是零摩擦的——你不用特意去做什么,跟着正常工作流走就行了。

八、总结

花了一个下午把这套东西搭起来,实际用了一周,感受很深。

第一,TF-IDF + 余弦相似度是技术文档 RAG 场景下性价比最高的方案。不需要下载模型,不需要 GPU,不需要联网,纯 numpy 跑,毫秒级出结果。对于 20~500 篇技术文档的规模,检索准确率完全够用。很多技术文章把 RAG 吹得很玄乎,动不动就"深度学习嵌入"、“大规模向量检索”、“实时增量索引”。但实际工程场景里,一个 140 行的 Python 脚本就能搞定大部分需求,而且效果不差。

第二,Hermes Agent 是用户和知识库之间的粘合剂。单独的 query_rag.py 是命令行工具,Agent 把它变成了聊天体验。你不用记命令、不用敲参数,说句话就行。Agent 自动判断要不要查知识库、查到结果后自动总结、总结完自动给出原文出处。这种"零摩擦"的体验是命令行工具做不到的。

第三,数据主权很重要。所有文档、所有向量、所有查询都留在你自己的机器上。没有上传,没有 API 调用(除 Agent 本身的 LLM 推理外),没有第三方看到你的内容。对签了 NDA 的硬件工程师来说,这不是"加分项",而是"刚需"。

第四,这套方案的局限性也要正视。TF-IDF 对关键词敏感,如果你问"DDR 那个 240 欧的电阻值是多少",它能找到,因为"240"和"DDR"都是高频词。但如果你问"上次我们讨论的那个跟阻抗有关的东西",它大概率找不到——这种高度口语化、依赖上下文的问题需要更强的语义模型。另外,TF-IDF 不考虑词序,"DDR 电源纹波"和"电源 DDR 纹波"在它看来完全一样——当然在这个场景下这不算问题。

第五,最重要的是先动手。很多人看到"RAG"、“向量数据库”、"嵌入模型"这些词就退缩了,觉得太复杂。但拆开来看,核心算法不过是一个 TF-IDF 统计 + 一行 np.dot。Python 的标准库加 numpy 就能搞定,不需要任何花里胡哨的框架。你真正需要的是把你的文档整理好、写两个脚本、跑一次构建,然后就享受自然语言搜索的爽快了。

如果你是做嵌入式的、或者任何需要管理大量技术文档的工程师,这套方案强烈推荐你试试。一个下午的时间换以后每天省下的无数个"翻文件找信息"的时间,这买卖太划算了。

关注我,带你探索更多AI加嵌入式的深度体验

AI 时代程序员必备技能

Codex、Claude Code、Cursor、Hermes Agent、OpenClaw等工程化实战专栏 ,讲透 AI 如何接管脏活累活

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值