☰
Khoj 如何索引长篇 Markdown 文档:以《Undergraduation》为样本的解析、切分与向量化全流程解析
2026/10/11 18:46:56 网站建设 项目流程
  • 人工智能
  • AI 应用
  • 大模型
  • RAG
  • 后端
  • AI Agent

【免费下载链接】khoj

Your AI second brain. Self-hostable. Get answers from the web or your docs. Build custom agents, schedule automations, do deep research. Turn any online or local LLM into your personal, autonomous AI (gpt, claude, gemini, llama, qwen, mistral). Get started - free.

项目地址:https://gitcode.com/GitHub_Trending/kh/khoj
点击查看免费下载

本篇指南围绕 Khoj 仓库中真实存在的长文样本tests/data/markdown/undergraduation.markdown(Paul Graham 的经典随笔《Undergraduation》)展开,完整讲解 Khoj 将长篇 Markdown 笔记转化为可检索、可对话的向量化知识条目的完整链路。读完本文,你将掌握 Khoj 的 Markdown 内容管线如何按标题结构递归切分、如何按 token 上限做二次分割、如何为每个条目生成带行号溯源能力的 URI,以及如何通过哈希去重实现增量索引,并能够将这套方法论直接应用到自己的个人知识库建设中。

一、样本文档在仓库中的角色与内容概览

undergraduation.markdown位于 tests/data/markdown/ 目录,同目录下还有what_i_worked_on.markdown、having_kids.markdown、how_y_combinator_started.markdown等一批 Paul Graham 随笔,以及一份用于解析测试的 main_readme.md。从仓库结构可以推断,这批文件是Khoj Markdown 解析器的真实测试语料:它们形态各异——有纯 H1 长文(如本篇)、有嵌套多级标题的文档(如main_readme.md),恰好覆盖了不同 Markdown 结构对索引管线带来的挑战。

从内容上看,《Undergraduation》是一篇约 160 行的长篇随笔,发布于 2005 年,讨论"计算机专业本科生在大学期间如何成长为优秀的程序员"。它的 Markdown 结构极具代表性:

  • 只有一个顶级标题# Undergraduation;
  • 正文用**Hacking**、**Math**、**Everything**、**Jobs**、**Grad School**、**Notes**等粗体行充当小节标题,而非##二级标题;
  • 全文约 1100 个英文单词,远超 256 个 token 的索引上限。

这种"有且仅有一个 H1、内部全靠粗体分段"的文档形态,在真实个人笔记与博客归档中非常常见。而 Khoj 的 MarkdownToEntries 正是专门为这类文档设计的解析器,接下来我们沿着它的调用链逐层拆解。

二、索引入口:从上传文件到 Markdown 条目提取

Khoj 的 Markdown 内容索引并不是零散散落在各处的逻辑,而是一条清晰的三段式管线,其入口在 api_content.py 的 indexer 接口:

  1. 文件归类:上传的文件按扩展名归类到index_files字典,其中"markdown"键专门收集 Markdown 文件内容,映射关系为"文件名 → 文件文本";
  2. 进入配置器:组装成IndexerInput后调用 configure_content。其中 Markdown 分支的代码非常简洁:
# 摘自 src/khoj/routers/helpers.py if (search_type == state.SearchType.All.value or search_type == state.SearchType.Markdown.value) and files.get("markdown"): logger.info("💎 Setting up search for markdown notes") text_search.setup(MarkdownToEntries, files.get("markdown"), regenerate=regenerate, user=user)
  1. 统一处理入口:text_search.setup 最终调用MarkdownToEntries().process(files=files, user=user, regenerate=regenerate),返回"新增条目数 / 删除条目数"两个统计值。

也就是说,无论你是通过 Web 上传、桌面客户端同步,还是未来通过 API 推送,Markdown 文件最终都会汇聚到同一个处理类MarkdownToEntries。该类的process()方法(见 markdown_to_entries.py)内部做三件事:提取条目 → 按 token 切分 → 增量更新向量库。其中max_tokens = 256是本管线的关键常量,下面逐一展开。

三、按标题结构递归切分:Heading 祖先链机制

MarkdownToEntries.extract_markdown_entries(源码位置)负责把"文件名 → 文本"的字典逐文件交给process_single_markdown_file处理。这是整个解析器的核心,逻辑可以概括为两条规则:

规则一:内容很小或没有更深层标题时,整段存为一个条目。判断条件如下:

if len(TextToEntries.tokenizer(markdown_content_with_ancestry)) <= max_tokens or not re.search( rf"^#{{{len(ancestry) + 1},}}\s", markdown_content, flags=re.MULTILINE ): # 保存为单个 entry,记录起始行号

注意这里的tokenizer在 text_to_entries.py 中实现,本质就是text.split()——按空白分词。因此max_tokens=256实际含义是"256 个以空白分隔的单词",而非严格的语言模型 token 数。这是一个值得注意的实现细节:Khoj 用单词数近似 token 数,换取解析速度与确定性。

规则二:存在更深层标题时,按标题级别递归拆分。拆分通过正则(?=[#]{N} .+?)找到当前层级的下一个标题,把内容切成多个 section,然后带着"标题祖先链"(ancestry: Dict[int, str],键为标题级别,值为标题文本)递归进入下一层。每个 section 记录其在原文件中的起始行号start_line,行号指向标题行本身。

那么,像undergraduation.markdown这种只有 H1、内部全是**Hacking**这类粗体文本的文档会怎样?答案是:它命中规则一——正则^#{2,}\s在全文找不到任何二级标题,因此整个文档会被当作一个原始条目(raw为全文)保存下来,再进入下一阶段的 token 级切分。这正是该测试语料的工程价值所在:它验证并覆盖了"无子标题长文"这一分支路径。同一目录下的main_readme.md则因为存在## Dependencies、## Installation等多级嵌套标题,走的是递归拆分路径,对应测试 test_line_number_tracking_in_recursive_split。

四、Token 级二次切分:长文如何被拆成可嵌入的块

《Undergraduation》约 1100 词,远超 256 词上限,单条目无法直接交给 embedding 模型。于是process()紧接着调用split_entries_by_max_tokens(源码位置),用 LangChain 的RecursiveCharacterTextSplitter做二次切分。核心参数值得逐条说明:

参数取值作用
chunk_sizemax_tokens(默认 256)每个块的单词数上限
separators["\n\n", "\n", "!", "?", ".", " ", "\t", ""]切分优先级:段落 > 换行 > 感叹句 > 问句 > 句号 > 空格 > 字符,先按粗粒度切,切不动再降级
keep_separatorTrue保留分隔符,避免句子被拦腰截断
chunk_overlap0块之间不重叠,配合增量哈希去重

切分后,除第一个块外,后续每个块都会前置拼上原条目 heading 的最后 100 个字符作为上下文前缀(snipped_heading = entry.heading[-100:]),因为对于大文档,"这条内容来自哪个文件/标题"对检索模型至关重要。随后通过remove_long_words丢弃超过 500 字符的超长单词(防止 URL 或 base64 破坏块质量),并通过clean_field清除\0等非法字符。

对《Undergraduation》而言,一次切分大约会产生 4~5 个 256 词以内的块,每块都携带# tests/data/markdown/undergraduation.markdown这个文件级前缀(详见下一节),从而保证块与源文档的归属关系在向量检索中不会丢失。

五、Entry 生成与行号溯源:每个知识块都有"档案"

切分完成后,convert_markdown_entries_to_maps(源码位置)把原始字符串组装成结构化的Entry对象。这里有两个关键设计:

1. 编译文本以文件名作为顶级标题。每个 entry 的compiled字段会拼接# {文件名}\n前缀,使得"检索结果来自哪个文件"这一信息直接进入 embedding 编码。heading 为空的纯文本段落,前缀退化为仅含文件名一行。

2. URI 携带精确行号。对本地文件,URI 格式为:

file:///data/web/disk1/git_repo/GitHub_Trending/kh/khoj/tests/data/markdown/undergraduation.markdown#line=1

行号从条目在源文件中的实际起始行计算;对以http(s)://开头的"文件"(Khoj 也支持直接索引 URL),则保留 URL 本身。切分器在生成子块时,还会通过在 raw 文本中查找子块的实际位置来重新计算#line=行号(见 text_to_entries.py 中 line 84-109),保证每个切分块的行号都指向真实内容而非块首。

这一设计在测试中被严格验证:test_line_number_tracking_in_recursive_split 会逐条断言:URI 中的行号指向的文件行,必须能匹配该 entryraw的首个非标题行。这保证了你在 Khoj 中点开检索结果时,能直接跳转到笔记原文中的准确位置——这正是"AI 第二大脑"类工具最核心的溯源体验。

六、增量索引与向量化:哈希去重驱动的知识同步

最后一步update_embeddings(源码位置)解决"文件更新后如何只重建变化的部分":

  1. 哈希建档:对每个条目的compiled字段做 MD5 哈希(hash_func,见 text_to_entries.py),并按文件聚合;
  2. 差异识别:查询数据库已有哈希,hashes_to_process = hashes_for_file - existing_entry_hashes,只有新增部分才生成 embedding;regenerate=True时则先清空该类型全部旧条目;
  3. 批量入库:embedding 按min(200, num)批量写入DbEntry,同时记录file_type=markdown、file_source=computer(见 markdown_to_entries.py)。DbEntry.EntryType.MARKDOWN的定义在 database/models;
  4. 反向清理:对每个文件,将数据库中已不存在于当前哈希集合的条目删除;若客户端传入的某文件内容为空字符串"",则视为删除标记,整文件条目连同文件对象一并清除;
  5. 日期索引:顺带用DateFilter抽取条目中的日期,写入EntryDates,为后续按日期过滤检索(date_filter)铺路。

对《Undergraduation》这类静态随笔,首次索引会全量入库;之后即使你反复重新同步同一文件,哈希相同的块会被直接跳过,实现零成本幂等。新增一个段落时,只有受影响的块会被重新向量化。

七、从测试语料看工程启示:如何为个人知识库准备 Markdown

tests/data/markdown/这批语料(连同 test_markdown_to_entries.py 中从无标题、单标题、多标题到非递增标题级别的全覆盖测试)揭示了为知识库工具准备笔记的三条实用建议:

  1. 善用标题层级:Khoj 会以标题为锚点组织条目并保留祖先链。用规范的#/##/###层级(而非粗体替代)组织长文,能让检索返回更精准的段落级命中,且行号定位更精确。
  2. 控制单段粒度:解析以 256 词为块上限,段落切分优先于句子切分。因此"一段一意"的写作习惯天然有利于被完整索引,过长的段落会被强制在句号处切开。
  3. 合理处理废弃内容:同步时把已删除文件的内容置空即可触发 Khoj 的整文件清理机制,无需额外调用删除接口。

结语

从tests/data/markdown/undergraduation.markdown这一篇长文样本出发,我们完整走通了 Khoj 的 Markdown 索引链路:入口configure_content → text_search.setup、标题递归切分与祖先链、RecursiveCharacterTextSplitter的 token 级切分、带#line=行号溯源的 Entry 生成,以及 MD5 哈希驱动的增量向量化。这套管线对"无子标题长文""多级嵌套文档""URL 文件"等形态各异的 Markdown 都有明确的处理分支,并配有专门的解析测试(tests/test_markdown_to_entries.py)与真实语料(tests/data/markdown/)持续验证。理解了这些内部机制,你在规划自己的笔记结构、批量导入历史文档时,就能让 Khoj 的检索与对话能力发挥出最大价值。

  • 人工智能
  • AI 应用
  • 大模型
  • RAG
  • 后端
  • AI Agent

【免费下载链接】khoj

Your AI second brain. Self-hostable. Get answers from the web or your docs. Build custom agents, schedule automations, do deep research. Turn any online or local LLM into your personal, autonomous AI (gpt, claude, gemini, llama, qwen, mistral). Get started - free.

项目地址:https://gitcode.com/GitHub_Trending/kh/khoj
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

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

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

立即咨询