☰
个人Wiki知识库检索增强实践:切块、混合检索与可溯源问答
2026/10/23 1:07:49 网站建设 项目流程

个人 Wiki 这件事,我断断续续维护了六年多,文件夹里躺着上千篇 Markdown,从环境配置备忘、踩坑记录,到读书摘抄和会议纪要,什么都往里塞。问题也一直是同一个:写的时候很爽,用的时候很痛苦。想找"某次调参最后用了多大批量"这种细节,关键词搜出来几十条,一条条点开看;想复盘"当时为什么放弃某个方案",压根想不起来笔记里写过没有。文件名和全文搜索只能匹配字面,它们不懂我在问什么,也不懂笔记里写了什么。llm_wiki 这个项目就是在这个背景下折腾出来的:把本地个人 Wiki 索引成一套可检索的知识库,让模型基于我自己的笔记回答问题,并且每条结论都能标出出自哪个文件的哪一节。它解决的不是"写笔记"的问题,而是"笔记写完就变成死数据"的问题;不追求做通用聊天机器人,只做一件事——让沉淀下来的内容随时能被问出来。适合两类人看:一类是笔记库里已经有几百篇以上存量、被检索折磨过的;另一类是想动手搭一个自己的检索问答系统、但被各种框架文档绕晕的。下面这些内容都是我实际跑过、改过、推翻重来过的东西,参数和取舍一并写清楚,能照着抄的部分我尽量写成能直接落地的形态。

1. 先想清楚:个人 Wiki 缺的到底是什么

1.1 三个绕不过去的痛点

第一个痛点是语义鸿沟。我记得笔记里写过"连接池耗尽导致请求排队",但我搜索时输入的是"服务卡住不动",字面完全不重合,全文索引一条都命中不了。人回忆内容时用的是概念和场景,不是原文里的词,这是关键词检索天然的短板。

第二个痛点是答案散落在多处。一个结论往往分散在"方案调研""实测记录""最终选择"三篇不同时间的笔记里,靠人肉拼起来成本很高,拼完还容易漏。我经常干的事是开三个编辑器窗口来回对照,最后自己写一段总结——这段总结其实又应该回写进笔记,但没人有这个耐心。

第三个痛点是知识会过期,但过期的那条还留在库里。同一台机器的配置改过三版,三篇笔记都在,搜索时先命中的往往是旧的那篇,因为它的关键词密度更高。没有时间和版本意识,检索结果反而会误导人。llm_wiki 在设计时专门留了"文件修改时间"和"标题路径"这两类元数据,后面会讲怎么用。

1.2 为什么"把整个笔记库塞给模型"不成立

一开始我图省事,直接把最相关的几个大文件整个拼进提示词丢给模型。跑了几次就发现三个问题。

成本不可控。整库几十万字,就算只挑十个文件也有几万字,单次请求的输入开销随随便便翻几十倍,日常高频问问题根本撑不住。有效信息被稀释。上下文里塞了大量和问题无关的段落,模型的注意力被摊薄,回答开始出现"看起来对但细节是编的"的情况。没有定位能力。整篇丢进去,模型说"你的笔记里提到过 X",但它说不清是第几节,我还是要自己翻。

所以结论很明确:进模型的上下文必须是筛选过、带位置信息、长度可控的片段。这就是检索增强这条路的起点,llm_wiki 整个架构都围绕"先精准捞出几块,再让模型在几块里说话"来搭。

1.3 三条技术路线的取舍

动手前我认真比较过三种做法,最后选了检索增强,理由写在下面这张表里。

路线做法优势实际撞上的问题我的判断
长上下文硬塞每次把相关文件整篇拼进去实现最快,一天能跑通成本高、定位弱、笔记涨到一定规模后彻底不可用只适合做临时验证
微调用笔记语料训练模型记住内容回答风格贴合个人习惯笔记天天改,改一次要重训;编造内容的风险不降反升维护成本远大于收益
检索增强切块、向量化、检索、拼上下文内容更新即时生效、可溯源、成本线性可控切块和检索要调,工程量在前端个人知识库场景最合适

微调这条路我试过一次就放弃了。核心原因是笔记的本质是持续变化的数据,不是固定知识。今天写的配置明天就可能改,而微调把知识固化进了权重,更新一次的成本和延迟都不可接受。检索增强的好处是索引和内容解耦,改一个文件只需要重建那一个文件对应的块,几分钟的事。

还有一条隐性理由:可溯源。个人知识库最怕的不是答不出来,是答错了还理直气壮。检索增强天然能带上出处,模型说的每句话背后都有具体的笔记片段,我能一眼验证真假。这一点比回答质量本身更重要,因为它决定了我敢不敢用。

2. llm_wiki 的整体架构

2.1 五个阶段与各自的边界

整套系统拆成五段,每段只干一件事,出问题能快速定位到是哪一段的锅,这个拆分方式是被调试逼出来的。

扫描:遍历 Wiki 目录,收集所有 Markdown 文件,计算内容指纹和修改时间。切块:把每篇笔记按结构切成带元数据的小片。向量化:给每个块生成语义向量,写进本地索引。检索:收到问题后先粗排再精排,筛出最相关的几块。生成:把块拼成上下文,让模型基于它回答,并输出引用编号。

为什么不干脆用一个框架一把梭?因为我踩过一次很典型的坑:答案质量下降,我怀疑是提示词问题,改了两天提示词;又怀疑是模型问题,换了两个模型;最后发现是切块把一个配置表格从中间劈开了,模型拿到的是一堆断头的参数行。没有分段隔离,你就没有观测点。分段之后,我可以单独跑检索、把捞出来的块打印出来看,一眼就知道问题出在哪一层。

2.2 目录约定与数据模型

llm_wiki 对 Wiki 目录没有强要求,但有两个约定能显著提升效果。第一,每篇笔记必须有且只有一个一级标题,作为这篇笔记的身份标识;第二,层级不要超过三层,标题路径太长会让切块粒度变碎。我的目录大概是这样:

wiki/ ├── 01-infra/ # 环境与运维 ├── 02-dev/ # 开发与调试 │ ├── 调试记录/ │ └── 性能优化/ ├── 03-reading/ # 读书与摘抄 └── 99-inbox/ # 未整理草稿,默认不索引

数据模型只有两张表,一张是块表,记录块 ID、所属文件、标题路径、正文、内容指纹;另一张是索引映射表,记录块 ID 与向量在索引文件里的位置。两张表用块 ID 关联,删除和更新都靠指纹比对。

注意:99-inbox这类草稿目录一定要在扫描阶段排除掉。我最早的版本没排除,结果草稿里那些"待验证""可能是这样"的半成品句子被检索出来,模型照着它回答,直接给我输出了一段错误结论,排查了半天才发现源头是草稿。

2.3 为什么索引与问答要彻底分离

建库和提问必须是两个独立进程,不能做成"问一次顺手扫一遍目录"。原因有三个。

第一,延迟。全量扫描加向量化在千篇规模下要跑十几分钟,混在查询链路里完全不可用。第二,稳定性。索引构建是最容易出错的环节,网络抖动、单条文本超长、编码异常都可能中断,混在一起会让整个问答过程变得不可靠。第三,成本。嵌入调用是按量计费的,每次提问重新嵌入一遍全库,纯属烧钱。

分离之后的好处是:索引可以离线跑、可以断点续跑、可以只更新变化的文件;查询链路只读索引,毫秒级响应。我现在的做法是每天定时跑一次增量更新,手动改完大批笔记后再补跑一次,查询随时可用。

3. 切块策略:这一步决定天花板

3.1 按标题切,而不是按字数切

我最初用的是最朴素的定长切块,每 500 字一刀,重叠 50 字。效果很差,问题出在语义边界和字符边界完全不对齐。一个"参数说明"小节被从中间切开,前半段有参数名没有取值,后半段有取值不知道对应谁,两块的语义都是残缺的,向量自然也是歪的。

改成按标题层级切之后效果立刻不一样。具体规则:以二级和三级标题为切点,每个块保留自己的标题路径作为前缀。比如一个问题命中"性能优化 / 连接池 / 参数配置"这一节,块的内容就是标题路径加正文,模型看到路径就知道这段在讲什么上下文,不需要额外解释。

超长小节还要做二次切分,这时候才用定长策略,但必须遵守两条:优先在空行处断开,避免切断句子;重叠率控制在 10% 到 15%,够让上下文接得上,又不至于让索引膨胀。表格和代码块一律不切——这是硬规则。

提示:代码块和表格我做过一个保护性处理,切分时把它们当作不可分割的原子单元。如果某个表格比目标块长度还大,宁可让它单独成块、超出长度,也不切开。实测中一个被劈开的参数表造成的错误答案,比多花几百 token 严重得多。

3.2 参数取值与实测对照

下面这组参数是我在自己几千篇笔记上反复试出来的,可以直接当起点,但一定要在自己的库上重新标定。

参数我的取值取值范围调整依据
目标块长度400 中文字符300 到 600太短丢上下文,太长检索精度下降
最大块长度900 字符800 到 1200超过就二次切分,除非是原子单元
二次切分重叠60 字符40 到 80约等于目标长度的 15%
最小块长度80 字符50 到 120太短的块多是标题和一句话注释,噪声大
单文件最大块数200不设硬限超过说明这篇该拆分了

其中最小块长度这条是被噪声逼出来的。库里有大量只有一行内容的笔记,比如"某某命令加上某参数即可",切成块之后向量质量很差,因为它没有上下文,检索时容易莫名其妙地高分命中,占用宝贵的上下文位。现在的处理是:短于阈值的块不单独入索引,而是合并到相邻块里,作为附注保留。

3.3 元数据设计:让每一块记得自己从哪来

元数据是 llm_wiki 里最不起眼但最值钱的部分。每个块除正文外还带这些字段:

  • file_path:相对路径,用于展示出处和增量更新
  • heading_path:标题路径,如性能优化 > 连接池 > 参数配置
  • block_index:块在文件内的序号,用于回填上下文
  • mtime:文件修改时间,用于时效排序和冲突提示
  • fingerprint:块内容指纹,用于跳过未变化的块
  • tags:从笔记顶部的标签区解析出来的分类

heading_path在检索阶段会被拼进块的检索文本里,等于给每块加了自带的小标题,实测能把"答非所问"的比例压下去一大截。mtime的用法更隐蔽一些:当同一个问题检索到两段内容矛盾但标题路径相同的块时,我在上下文里按时间倒序排列,并在提示词里明确告诉模型"同一主题存在多个版本,优先采信较新的内容,如果冲突请指出"。这一条规则解决了我前面说的"旧配置误导人"的问题。

4. 向量化与索引构建

4.1 嵌入模型怎么选

嵌入模型的选择决定了检索的天花板,这一步省不得。我按四个维度筛:中文语义能力、向量维度、单条长度上限、调用成本和速度。

中文能力是首要的,因为笔记里中英混排、术语缩写很多,纯英文模型对"连接池""降级策略"这种词的理解明显偏弱。维度上我倾向中等维度,几百到一千维左右,维度太高会让本地索引文件迅速膨胀,检索也变慢;维度太低则区分度不够,相近主题的块容易糊在一起。单条长度上限要注意——如果模型只支持很短输入,那前面 400 字的块长就得往下压,两者是耦合的。

我自己用的是本地部署的中文语义嵌入模型,走本地推理有两个好处:一是批量重建索引不花钱,可以放心全量重跑;二是数据不出本地,个人笔记里有不少私密记录,这一点对我很重要。代价是首次加载慢、占用内存,但索引是离线跑的,这点代价完全值得。如果你更在意开箱速度,用托管接口也行,只是记得做好缓存,别重复调用。

4.2 批量嵌入、缓存与断点续跑

批量嵌入有三个必须做对的细节。

批量大小控制在 32 到 64 条。太小则调用次数多、开销大;太大则单次请求容易超时,一旦失败整批重来。我现在的做法是按 32 条一批,批内失败自动降级成逐条重试,这样单条异常文本不会拖垮整批。块级缓存靠指纹。建库前先读旧索引,用块指纹比对,只对变化的块重新嵌入。我的库更新频率大概是每天十几篇改动,增量更新耗时从全量的十几分钟降到几十秒。断点续跑靠事务性写入。每处理完一批就落盘一次,中途崩了下次从断点继续,不要攒到最后一次性写。

# 建库主流程(伪代码,函数按职责抽象) def build_index(wiki_root, store): old = store.load_block_fingerprints() # 已有块指纹 files = scan_markdown(wiki_root, exclude=["99-inbox"]) pending = [] for f in files: text = read_text(f.path) blocks = split_by_heading(text, target=400, max_len=900) for b in blocks: if len(b.content) < 80: # 过短块合并,不入索引 continue fp = fingerprint(b.content) if old.get(b.id) == fp: continue # 未变化,跳过 pending.append((b, fp)) for batch in chunks(pending, size=32): texts = [f"{b.heading_path}\n{b.content}" for b, _ in batch] vectors = embed_with_retry(texts) # 失败降级为逐条 for (b, fp), v in zip(batch, vectors): store.upsert(b, fp, v) # 每批落盘一次

这段代码里最值得说的是texts那一行:送进嵌入模型的不是纯正文,而是"标题路径 + 正文"。多花的这点长度换来的是块自带语义标签,检索准确率的提升在实测里相当明显,属于性价比最高的一处改动。

4.3 增量更新:别每次都全量重建

全量重建只在两种情况下做:换嵌入模型、改切块规则。其余时间一律走增量。

判断变化不能只看修改时间,因为复制文件、批量重命名都会改时间但内容没变。我的做法是双条件:修改时间变了才去算内容指纹,指纹变了才重建。这样既避免了每次读全部文件的开销,又不会因为时间戳抖动做无用功。

还有一条容易忽略的:删除也要同步。文件被删或者重命名之后,旧块必须从索引里清掉,否则它会一直以"幽灵内容"的形态被检索出来。我在块表里保留了文件路径字段,扫描结束后对比"库里有的文件"和"磁盘上有的文件",差集直接批量删除。

注意:重命名文件时一定要走"删旧建新"两条路径一起处理。我吃过一次亏——把一篇笔记从一个目录挪到另一个目录,光看内容指纹没变就跳过了重建,结果索引里存的还是旧路径,点开引用找不到文件,排查了好一阵。

5. 检索层:粗排、精排与预算

5.1 混合检索为什么必要

纯向量检索有一个稳定的失败模式:它擅长语义相似,不擅长精确匹配。问"某个配置项叫什么名字"这类问题时,向量检索经常给出语义相近但字面无关的块,真正包含那个参数名的那块反而排在后面。

所以我用向量 + 关键词双路召回,再用融合排序合并。向量路负责"意思对得上",关键词路负责"字面必须出现"。融合用倒数排名法,两路各出前若干条,按排名的倒数加权合并,同一个块被两路同时命中就会自然靠前。这个组合对个人笔记特别合适,因为笔记里既有关键词密度高的配置清单,也有大段叙述性的思考记录,单一检索方式覆盖不全。

关键词这一路要注意中文分词和标点处理。早期的故障是:我搜"参数 配置",笔记里写的是"参数、配置"(用了顿号),关键词路一条都没命中。后来在索引阶段把标点统一做了归一化,把全角半角、各种分隔符都规整成空格,这个问题才彻底消失。

5.2 重排与相似度阈值

粗排召回 20 到 30 条候选后,加一层交叉重排。向量检索是分别编码问题和块再算距离,重排模型是把问题和块放在一起看,精度高得多,速度也慢得多,所以只对候选集做。

这一步的必要性在于,我的笔记库里有大量"标题相似但角度不同"的内容,比如同一主题下既有"选型对比"又有"上线后问题记录",向量距离很近,粗排分不出高下。重排能把真正回答问题的那块顶上来,实测把 top-3 命中率往上提了一大截。

阈值这块要特别提醒:不要照抄别人的数字。不同嵌入模型的相似度分布差异很大,我用的模型余弦相似度普遍偏高,0.6 才算勉强相关;换成别的模型可能 0.4 就已经很好了。正确做法是拿一批自己的问题跑一遍,看正确块和无关块的分数分布,取一个能把两者分开的位置。我做的是动态阈值加保底条数:分数低于阈值就丢弃,但至少保留 3 条,避免一个问题因为阈值过严导致上下文为空。

5.3 上下文拼装与 token 预算

拼上下文时我遵守三条规则。

按分数排序,但不完全按分数截断。同一篇笔记的相邻块有时会同时命中,这时候我把它们按块序号拼回连续段落,读起来连贯,模型也不容易误解。总长度硬性封顶。上下文预算是提前算好的,超出就按分数从低往高丢,绝不让它把输出空间挤掉。每块带编号和出处。格式上给每块前面加一个引用标记,后面附上文件路径和标题路径,这样模型引用时能直接对上。

预算怎么算?举个例子:假定模型上下文窗口 8000 token,中文场景下把 1 个汉字粗略当 1 token(保守估计)。系统提示 300,问题描述 100,历史对话 600(我保留最近两轮),输出预留 1000,剩下来给检索上下文的是 6000。按每块 400 到 500 token 算,可以放 8 到 10 块。所以我实际取的是重排后前 8 块,不够就用保底条数补,超了就砍到 6 块。这个数字不是拍脑袋来的,是算出来的,改模型或改块长之后要重新算一遍。

6. 生成层:让答案可溯源

6.1 提示词骨架

提示词我改了十几版,最后收敛成一个很朴素的骨架,不含花哨的思维链引导,因为那些东西在检索问答里收益很小,还容易让模型跑偏。骨架大致是四段:

第一段交代身份和边界——"你是一个基于用户个人笔记作答的助手,只能使用提供的片段"。第二段交代规则——"每个结论后标注引用编号;片段里没有信息就明说没有;同一主题存在多版本时优先采信较新内容并说明冲突"。第三段贴上下文,每块带编号和出处。第四段是问题。

关键在边界要说得非常死。我用过很客气的措辞,比如"请尽量基于以下内容回答",结果模型频繁补充自己的通用知识,答案看着很完善,但和我笔记里的实际结论不一致,这种错误最难发现。改成"只能使用""没有就说没有"这类硬约束之后,编造情况明显减少。

6.2 引用与"不知道"的处理

引用编号的规则要明确到格式层面:正文里用方括号数字标注,回答末尾按编号列出出处,每条出处包含文件路径和标题路径。这样我在编辑器里能直接定位过去。

"不知道"的处理是这套系统里我最看重的部分。我给它设了三档:完全没有相关片段,直接回答"笔记里没有相关内容",并列出最接近的两三个标题供我确认;有片段但不足以支撑结论,回答"以下内容基于片段推测,可能不完整",并说明缺什么;有明确片段,正常作答。

三档机制带来的最大好处是信任校准。用得久了,我会知道看到"推测"两个字就要去核对原文,看到正常作答基本可以直接采信。这比让模型硬答一个看起来很完整的答案有价值得多。

6.3 后处理与展示

生成完了还有几步后处理,都是小细节但影响使用体验。

引用校验:检查正文里出现的编号是否都在上下文中存在,出现了不存在的编号就把这条引用标记为异常,提示我人工看一眼,这通常是模型编造引用的信号。路径补全:把相对路径补成可点击的本地路径,点一下就能跳转到编辑器对应位置。答案回写:把这次问答存成一条记录,附上问题和引用,攒一段时间之后我就知道自己的笔记库哪些主题被反复问、哪些主题回答质量差,反过来指导我补笔记。

历史问题的缓存我也做了。同一个问题短期内重复提问,直接返回上次结果,省一次完整链路。注意缓存键要包含索引版本号,索引更新后旧缓存自动失效,否则会拿到基于过期索引的答案。

7. 从零跑通最小可用版本

7.1 环境与目录准备

最小版本不需要复杂基础设施,一台日常用的机器就够。需要准备的东西只有四样:Python 运行环境(3.10 以上,主要是类型标注方便)、一个本地嵌入模型(走本地推理或者托管接口都行)、一个轻量向量索引(小规模下用文件存向量加内存暴力检索就够了,几千个块完全跑得动)、一个对话模型接口。

目录我建议这样分:

llm_wiki/ ├── wiki/ # 你的笔记库,被索引的对象 ├── index/ # 索引产物:块表 + 向量文件 ├── scripts/ │ ├── build.py # 建库 │ └── ask.py # 提问 └── logs/ # 每次问答的留档

先别上数据库。我一开始就引入了一个完整的关系型数据库,结果光建表写迁移就花了两天,真正的问题一个没解决。几千个块的规模,块表存成一个结构化文件、向量存成一个大数组文件,启动时全部读进内存,检索走矩阵乘法,速度完全够用。等确实卡了再换。

7.2 建库脚本拆解

建库脚本做四件事:扫描、切块、嵌入、落盘。前面 4.2 已经给过骨架,这里补几个实操中必须处理的分支。

编码兜底。笔记来源杂,偶尔混进非 UTF-8 的文件,读取时要显式指定编码并在失败时记录到日志跳过,不要让一个坏文件中断整轮建库。超长块裁剪。如果某个块因为保护代码块而变得极长,超过嵌入模型的输入上限,要在嵌入前裁到上限并记一条警告,否则接口会直接报错。空块过滤。切完之后正文只有空白或只有标题的块直接丢,它们只会污染索引。

落盘格式我建议拆成两个文件:块表存结构化文本(方便直接打开看,调试时极其有用),向量存二进制(体积小、读取快)。两个文件用块 ID 顺序对齐,别搞成两个独立字典再去查。

提示:建库完成后一定要打印一份统计——文件数、块数、平均块长、跳过的块数、警告条数。这份统计是排查问题的第一手材料。我有一次发现块数比预期少了一半,就是靠统计里的"跳过的块数"定位到最小块长度阈值设得太高。

7.3 查询脚本拆解

查询链路是五步:问题预处理、双路召回、融合重排、上下文拼装、调用生成。

问题预处理这一步很多人会跳过,但它有用:把问题里的指代词处理掉("那个参数"这种,如果带了对话历史就补全,没有就原样保留并提示)、去掉问句里的冗余语气词、把全角标点归一化。做完这些再进检索,命中率会好一些。

融合重排之后有个去重动作:同一篇笔记的多个块如果内容高度重叠,只保留分数最高的一个,避免上下文被同一篇笔记的相似段落占满。去重标准用块指纹前缀比对或者简单的文本相似度都行。

# 查询主流程(伪代码) def ask(question, index, history=None): q = normalize(question) vec_hits = index.search_vector(embed([q])[0], top_k=25) kw_hits = index.search_keyword(tokenize(q), top_k=25) fused = rrf_merge([vec_hits, kw_hits]) # 倒数排名融合 reranked = rerank(q, fused[:30])[:8] # 精排取前 8 kept = [b for b in reranked if b.score >= THRESHOLD] or reranked[:3] kept = dedupe_by_doc(kept) ctx = build_context(kept, budget=6000) # token 预算封顶 prompt = render_prompt(ctx, q, history) answer = call_model(prompt) return verify_citations(answer, kept), kept

verify_citations这一步不能省,它返回的异常标记是我判断"这次回答能不能信"的第一道闸。

7.4 参数估算与标定方法

参数不是抄来的,是标定出来的。我用的方法很土但有效:准备一套自己的测试问题集,大概 30 条,覆盖几种类型——精确查参数(考关键词能力)、概念解释(考语义能力)、跨文件综合(考召回广度)、明确不存在的主题(考拒答能力)。每条问题我手工标好"应该命中哪个文件的哪一节"。

然后每次改参数,跑一遍这 30 条,统计三个数:top-3 命中率(正确答案在重排后前三块的占比)、上下文有效率(拼进去的块里真正相关的占比)、拒答正确率(对不存在主题正确说出"没有"的占比)。三个数一起看,避免只优化一个指标把另一个搞崩。

举例说明一次真实的调整过程:我把目标块长从 600 降到 400,top-3 命中率涨了、上下文有效率也涨了,但拒答正确率掉了——因为块变短之后,一堆语义相近的无关块分数被抬起来,接近阈值,模型看到"有点像"的内容就不肯说没有了。最后加了"保底条数但必须有分数下限"的规则才平衡回来。这就是为什么必须一起看三个指标,只盯命中率会做出错误的取舍。

8. 踩坑记录与排查速查表

8.1 效果类问题

效果类问题最难受,因为链路长、原因多。我的排查顺序固定:先看检索出来的块,再看模型回答。把每次的检索结果打日志,问题一出,先确认"正确的那块到底有没有被捞出来"。

如果没捞出来,问题在检索层:切块碎了、嵌入文本没带标题路径、关键词分词失败、阈值太高。如果捞出来了但回答错了,问题在生成层:上下文被无关块挤占、提示词边界不清、块顺序混乱、模型没看懂表格。这个二分法能省掉大量瞎猜。

8.2 工程类问题

工程类问题相对好办,都是有明确症状的。下面这张表是我这两个月记下来的高频故障,按出现频率排。

症状可能原因处理方式
引用点开找不到文件文件被重命名,索引未更新扫描阶段比对文件差集,删旧建新
全量建库跑一半中断单条文本超长或编码异常显式指定编码、超长裁剪、批内失败降级逐条
同一问题答案前后不一致索引已更新但缓存未失效缓存键加入索引版本号
表格类问题答得支离破碎切块把表格劈开表格和代码块设为原子单元
只有一行的小笔记频繁高分短块噪声抬高最小块长度,短块合并进相邻块
中文标点导致关键词搜不到标点未归一化索引阶段统一全角半角与分隔符
问题涉及多个主题时召回偏窄单路检索+固定 top-k双路召回 + 按主题拆解子问题各召回一次

最后一条值得展开说。多主题问题是我后来才意识到的坑,比如"我上次换机器时改了哪些配置、踩了哪些坑",这实际上是两个子问题。单次检索只能命中其中一个方向,另一个方向的内容就漏了。我现在的做法是问题里出现明显的并列结构时,先做一次简单拆解,每个子问题各召回一次,再合并去重。实现上很土,正则加关键词匹配就能识别"和""以及""还有"这类连接词,但效果提升明显。

8.3 我怎么判断一次改动是好是坏

最后说点方法论层面的东西,这是我认为比任何参数都重要的部分。

每次只改一个变量。我早期犯的错是一次改三处:换嵌入模型、调块长、改提示词,然后效果变好了,但我根本不知道是哪一处起了作用,后面想复现或者回滚都无从下手。现在严格一次一改,跑测试集,看三个指标,记录到日志里。

留一份基线快照。每次大规模改动前把索引文件和参数配置复制一份,改完发现不对能立刻切回去。这件事听起来笨,救过我两次。

别追求漂亮指标。我有一阵子沉迷把 top-3 命中率往上顶,最后发现提升都是靠把块切得越来越碎换来的,命中率好看了,但拼进上下文的块全是碎片,模型读起来没头没尾,回答质量反而降了。最终指标应该是"我愿不愿意采信这个答案",测试集只是辅助。

我在实际使用中的体会是,llm_wiki 这类工具的价值不在于省下几次搜索的时间,而在于它逼着我把笔记写得更结构化。因为按标题切块、按标题路径检索,标题写得含糊的笔记检索效果肉眼可见地差,用了一阵子之后,我自然而然地开始把小节标题写成"结论式"的短句,而不是"杂项""其他"这种糊弄式的词。这个副作用带来的收益,说实话说比检索本身还大。如果你也想动手,建议先把切块和检索跑通、把日志打全,生成层最后再调,因为前两层的问题不解决,提示词写得再漂亮也是白费。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询