Skip to content

Agentic RAG

Ciao1019 edited this page Sep 1, 2026 · 3 revisions

Petrichor Agentic RAG:从 Markdown 到可追溯回答

Petrichor 的 RAG 不是“切片后做一次向量 Top-K”。它同时保留原文分片、用户问法、产品内 Wiki 和文档目录四种知识表示,再让 Agent 按问题类型执行 Search / Outline / Read。 这样既能回答精确事实,也能处理跨文章比较、章节归纳和多跳概念关系,并且最终证据仍可追溯到 原文或 Wiki 页面。

Note

术语说明

本文的“产品内 Wiki”指 Petrichor 从知识库文章编译出的知识层;它与仓库的 GitHub Wiki 文档栏不是同一个系统。

推荐阅读路径

1. 一张图看完整流程

flowchart TB
  source["Markdown 文章<br/>编辑器 · API · PDF"]
  build["单篇构建知识"]
  ingest["更新 Wiki"]
  vectorize["生成向量"]

  chunks["原文分片<br/>标题路径 + 正文"]
  questions["推荐问题<br/>原文的检索别名"]
  semantic["语义 Wiki<br/>实体 · 概念 · 关系"]
  outline["PageIndex<br/>标题树 + 章节摘要"]

  query["用户问题"]
  search["混合 Search<br/>Vector · BM25 · Wiki · RRF"]
  route["Outline<br/>结构导航"]
  read["Read / Read Many<br/>按需深读"]
  evidence["Evidence + Trace"]
  answer["可追溯回答"]

  source --> build
  source --> ingest
  source --> vectorize
  build --> chunks
  build --> questions
  build --> semantic
  ingest --> outline
  chunks --> vectorize
  questions --> vectorize

  query --> search
  query --> route
  chunks --> search
  questions --> search
  semantic --> search
  outline --> route
  search --> read
  route --> read
  read --> evidence --> answer
Loading

Important

两条不可突破的证据边界

  1. Search 只找候选。 Agent 必须显式 Read,候选正文才会进入 Evidence。
  2. 推荐问题只是检索别名。 命中后仍映射回原始 chunkId,模型生成的问题不会成为事实来源。

2. 四种知识表示分别解决什么问题

知识表示 核心职责 读取定位
原文分片 保存标题路径与 Markdown 正文,回答精确事实、步骤、代码和引用 chunkId
推荐问题 补齐用户问法与原文措辞之间的差异;命中后回到同一原文分片 chunkId
Wiki 页面 聚合实体、概念、关系与多篇文章贡献,支持语义导航和多跳关联 pageKey
PageIndex 保存章节层级与摘要,处理“有哪些章节”“按顺序总结”等结构问题 nodeKey / chunkId

原文负责事实,Wiki 负责语义导航,目录负责结构。三者不是互相替代的索引副本。

3. 从文件到 Markdown 源文章

知识构建的标准输入是 petrichor_kb_article.content_md

  • 在编辑器中创建或修改 Markdown;
  • 通过 REST / MCP / Agent Skill 创建或更新文章;
  • 导入 PDF:可提取页直接转成 Markdown,扫描页由独立视觉 Worker 按页转写,全部页面完成后合并 成一篇文章。

保存文章或完成 PDF 导入并不等于已经建立 RAG 索引。文章需要执行一次“构建知识”;需要 PageIndex 目录时再执行“更新 Wiki”;配置了 Embedding 模型后还应补齐向量。

4. 单篇“构建知识”做了什么

入口是 POST /api/kb/knowledge/build。请求进入 API 进程内的有界队列,同一用户、知识库和文章 正在运行时会复用原任务,不重复编译。默认最多同时构建 8 篇文章,队列容量 128,单任务超时 15 分钟;问题生成和页面生成还有各自的批次并发,但所有文章共享一个全局模型信号量。

任务的核心阶段如下:

flowchart LR
  load["读取文章与编译说明书"] --> chunk["确定性结构切片"]
  chunk --> parallel{"并行"}
  parallel --> questions["分片问题生成"]
  parallel --> extract["整篇实体 / 概念 / 关系抽取"]
  questions --> pages["全局目录规划<br/>分批生成 Wiki 页面"]
  extract --> pages
  pages --> tx["事务提交<br/>分片 · 索引 · Wiki · 链接 · 来源"]
  tx --> vectors["补原文分片向量<br/>best-effort"]
Loading

问题生成与整篇抽取并行执行;只有页面、链接和索引准备完整后,结果才会在同一事务中提交。

4.1 编译说明书

每个知识库可保存一份 compile-guide,用于告诉编译器领域、读者、抽取偏好、目录约定和页面 写法。它会追加到问题生成、候选抽取、目录规划和页面生成的系统提示中,但不能覆盖固定 JSON 输出协议。HTML 注释和只有标题没有正文的小节不会进入提示词。

详情见 知识可移植性文档

5. Markdown 如何切片

切片由确定性 Go 代码完成,不依赖模型,也不是固定 token 窗口。当前算法版本为 2。

5.1 参数

参数 当前值 作用
目标预算 1200 普通小节合并接近该规模时结束当前分片
小尾段阈值 400 短尾段优先并入同一顶层主题的相邻分片
标称上限 3200 超长内容进入回退切分;完整代码围栏可略超上限
超长重叠 320 只用于超长小节的相邻窗口
单篇上限 120 超出后停止生成后续分片并返回警告

这些数值是切片算法的文本预算,不是模型 tokenizer 的 token 数。

5.2 三阶段算法

阶段一:解析标题结构

  • 规范化换行并识别 Markdown h1h6
  • 用标题栈保存完整 headingPath
  • 围栏代码块中的 # 不会被误判为标题;
  • 标题前导语以文章标题作为定位名称。

例如 安装 > Docker Compose > HTTPS 会完整保存在分片元数据中。检索时文章标题和这条标题 路径都会进入索引文本,避免“正文只有命令、上下文全在标题里”时召回失败。

阶段二:合并短小节

算法按原顺序贪心合并相邻小节,但遵守两条边界:

  1. 不跨一级主题合并;
  2. 不为凑长度突破标称上限。

不足 400 的小尾段会优先并入同一一级主题的后一个分组,其次并入前一个分组。多个标题合并后, 如果某个小节占正文至少 60%,用它作为分片标题;否则锚定第一个有实质内容的小节,避免短标题 污染分片身份。

阶段三:切分超长小节

超长小节在窗口后半段按以下顺序寻找断点:

  1. 空行;
  2. 换行;
  3. 中英文句末标点;
  4. 实在找不到再用窗口边界。

断点不会落进代码围栏。如果标称边界位于围栏内,会优先保留完整代码块。相邻超长窗口保留 约 320 的上下文,并尽量把新窗口起点对齐到下一行。

5.3 为什么不直接固定长度切片

  • 标题路径被保留,回答可以显示“文章 › 章节 › 子章节”;
  • 配置段、步骤列表和代码块不容易被拦腰切断;
  • 普通章节没有无意义的全局 overlap,减少重复候选;
  • 分片算法可版本化,文章正文哈希或算法版本变化时会标记为 stale。

当前 120 片上限只限制分片/问题索引。整篇 Wiki 抽取走独立上下文预算,因此超长文档的 Wiki 候选与可逐片召回范围可能不同,构建结果会明确返回截断警告。

6. 推荐问题:给每个分片增加“用户问法”

切片完成后,每批最多 4 片、约 4000 文本预算,模型为每片生成恰好 3 个问题。问题必须能仅靠 该分片回答,不能输出答案。缺项、无效 JSON 或模型失败时,系统会补成模板问题,例如:

  • “这一节主要讲了什么?”
  • “这一节有哪些关键结论?”
  • “如何理解并应用这一节?”

每个问题独立进入 source_type = question 的词面与向量索引,但外键仍指向原始分片。这样用户问 “怎么部署”可以命中正文写作“生产安装”的章节,同时回答阶段仍读取原文,而不是读取合成问题。

7. 产品内 Wiki 如何构建

“Wiki”包含两条互补路径,理解这一点很重要。

7.1 “构建知识”:语义 Wiki 与知识图谱

单篇构建不会按分片逐片抽实体,而是把整篇文档交给候选抽取器:

  1. 生成文档摘要;
  2. 抽取有实质内容支撑的实体和概念,通常 5–20 个、最多 24 个;
  3. 抽取候选之间有原文依据的关系;
  4. 对照已有页面复用稳定 pageKey,避免同一对象重复建页;
  5. 为整批候选规划最多两级、可复用的浅目录;
  6. 每批 4 页生成独立 Markdown,正文中的相关对象使用 [[pageKey|显示名]]
  7. 写入 source / entity / concept 页面、页面链接、来源引用和知识库索引页。

整篇输入上限为 72000 个 Unicode 字符。超限时保留约 62% 文首和 38% 文尾,并显式插入省略 标记。页面生成失败会退回候选摘要页;候选抽取失败时仍可保存原文分片和 source 页面。

同一实体或概念可聚合多篇文章的贡献。重新构建某篇文章前,系统先移除它的旧贡献,再写入新 贡献;没有其它来源和人工底稿的孤立生成页会被清理。最终事务同时提交分片、索引、Wiki、链接 与来源引用,失败时不会留下半套新知识。

7.2 “更新 Wiki”:文章 source 页与 PageIndex 目录

POST /api/kb/wiki/ingest 面向整个知识库:

  • 按 source hash 跳过未变化文章;
  • 生成文章级 source 页摘要、要点、实体名称和推荐问题;
  • h1–h6 建立 PageIndex 目录树;
  • 最多 40 个有正文节点时用一次模型调用生成章节摘要,更多时使用本地摘要;
  • best-effort 为目录树节点补向量。

这条路径主要服务 knowledge.outline 和存量 Tree 召回,不会替代“构建知识”产生的现代分片、 问题索引与 entity / concept 页面。

“完全重建”会清空产品内 Wiki 页面、链接、来源引用和目录树,但保留人工编译说明书;现代文章 分片仍在。随后它只重建 source 页和 PageIndex 树,因此若要恢复 entity / concept 语义页面, 还需要对相关文章重新执行“构建知识”。

8. 词面索引与向量索引

8.1 索引文本

每条原文或问题索引的 embedding_text 都按以下顺序拼接,并截到 4000 字符:

文章标题
一级标题
二级标题
当前分片正文或推荐问题

原文和问题分开存储,使两种表示可以独立召回、再统一映射到分片。

8.2 中文 BM25

PostgreSQL 内置 parser 不适合直接切中文,所以应用层使用同一套中英文词元规则:

  • 英文和数字按词;
  • 连续中文使用 2 字滑窗;
  • 查询词元去重;
  • PostgreSQL 先尝试 GIN search_vector 收窄候选,未命中时退回 ILIKE 词元模式;
  • 最多取 400 个候选,在 Go 中计算真正的 BM25,而不是把 ts_rank 当最终分数。

应用层 BM25 对标题、路径摘要、正文使用 3 / 2 / 1 的字段权重,并做 IDF 与长度归一化。

8.3 Embedding 的两阶段一致性

构建事务提交后会 best-effort 补原文分片向量。Wiki 页的“生成向量”操作继续按严格顺序执行:

  1. 当前知识库所有原文分片向量 ready;
  2. 再生成推荐问题向量。

每批最多 64 条,单次每阶段最多处理 2000 条。检索只使用与当前 Embedding 模型、维度和版本完全 一致且状态为 ready 的向量;切换模型或维度后旧向量不会被误用,而会进入待重算状态。

向量不可用不阻断构建,BM25、Wiki 和目录路径仍可工作。

9. 问答时如何召回

以下是登录后站内助手和外部 Agent 文档搜索复用的主召回链路。

flowchart LR
  query["原问题 + 子查询"]
  query --> cv["原文 Vector"]
  query --> cb["原文 BM25"]
  query --> qv["问题 Vector"]
  query --> qb["问题 BM25"]
  query --> wiki["Wiki 词面检索"]
  cv --> rrf["RRF 融合"]
  cb --> rrf
  qv --> rrf
  qb --> rrf
  wiki --> rrf
  rrf --> balance["文章级平衡"]
  balance --> rerank["本地重排"]
  rerank --> diversity["去重 + 多样性控制"]
  diversity --> candidates["Search 候选"]
Loading

9.1 查询改写

简单问题保持原样。较长、包含多个分句或连接词的问题会用确定性规则拆成最多 3 个子查询;调用方 也可以显式传入 subQueries。连同原问题,总查询数最多 4 个。这里不额外调用 LLM,避免简单问题 被“改写坏”。

9.2 五路现代召回

对每个查询执行:

  1. 原文分片向量 Top 10;
  2. 推荐问题向量 Top 10;
  3. 原文分片 BM25 Top 10;
  4. 推荐问题 BM25 Top 10;
  5. Wiki 页面词面检索 Top 10。

如果原文和推荐问题两路词面检索都为空,还会按文章标题兜底。限定到单篇文章时不搜索 Wiki 页面, 避免跨文章概念页污染当前文章范围。

9.3 RRF 融合

不同召回源的原始分数不可直接比较,因此使用 Reciprocal Rank Fusion:

RRF(candidate) = Σ 1 / (60 + rank + 1)

相同 chunkId 的原文与问题命中会合并,多路命中的排名贡献全部保留,但候选展示和后续重排优先 使用原文内容。融合最多保留 30 个候选。

9.4 文章级筛选与本地重排

为避免一个热门长文占满结果,系统先做文章级平衡:

  • 默认选择最多 3 篇文章;
  • 每篇最多带 4 个候选进入下一阶段;
  • 文章得分综合该文前三个候选和多路命中数;
  • 候选按文章轮询交错,而不是先塞满第一篇。

前 10 个候选再按原始问题做本地重排:标题、路径/摘要、正文分别使用递减权重,完整问题命中高于 单词命中。当前是确定性本地重排,不依赖外部 reranker 服务,因此不会增加一次模型请求。

本地重排后的候选先执行多样性过滤:标题相同或词元 Jaccard 相似度达到 0.88 时去重,默认每篇 最多保留 3 个。若结果还未达到 limit,再从融合后的文章候选补齐,同时继续遵守单篇数量上限; 最终 limit 最大为 20。

9.5 存量降级路径

只有现代分片、问题和 Wiki 三类全部未命中时,才启用存量 PageIndex Tree:

  1. Tree 向量;
  2. Tree BM25;
  3. 若限定知识库且快速召回仍为空,或问题复杂度为 complex,让模型浏览最多 240 个目录节点。

旧 Tree 不会与现代结果同时抢排名,避免粗粒度目录节点遮住精确分片。

10. Search / Outline / Read:Agentic RAG 的核心

Search:先定位,不泄洪上下文

knowledge.search 返回标题、路径、摘要、召回来源和 chunkId / pageKey / nodeKey 等定位符, 不返回完整正文。Agent 可根据问题和候选决定深读 1 个还是多个章节。

Outline:结构问题走目录,不把章节打散

相似度召回擅长“哪一段最像”,不擅长:

  • “这篇文档有哪些章节讨论缓存?”
  • “按章节顺序总结安装流程”;
  • “比较第二章和第五章的结论”。

knowledge.outline 优先读取 PageIndex 树;没有树时,从已构建分片的 headingPath 和推荐问题重建 轻量目录。Agent 拿到目录后选择章节,再用定位符 Read。

Read:正文变成 Evidence 的唯一入口

knowledge.read / knowledge.read_many 可读取:

  • 原始分片;
  • 产品内 Wiki 页面及其出链、入链;
  • PageIndex 节点或子树;
  • 必要时整篇源文章。

简单任务一次批量深读最多 2 项,其它复杂度最多 4 项。读取结果由统一工具执行器归一化成 Evidence,进入上下文预算、Trace 和最终质量门。搜索摘要本身不会冒充完整证据。

Agent 为什么比固定 RAG 链更灵活

Main Agent 的 Soft Router 只给出策略提示,不裁剪能力。它可以根据问题动态决定:

  • 简单事实:Search → Read → Answer;
  • 结构总结:Outline → Read Many → Answer;
  • 概念多跳:Wiki Search / Read → 沿 links 深读关联页;
  • 跨文章比较:多子查询 Search → 文章平衡 → 并行深读;
  • 站内资料不足:改写查询,或在配置允许时加载 Research Skill;
  • 复杂任务:生成计划并委派子 Agent,同时受工具、token、时间和深度预算限制。

工具调用、Evidence、计划和停止原因都会进入 Agent Run / Trace;最终只生成一组回答文本,避免把 过程推理误当答案。运行时、安全和预算详见 Agent Runtime

11. 降级、失败与一致性

触发条件 保底行为
Embedding 未配置或调用失败 继续使用 BM25、Wiki、文章标题和目录召回
原文或问题某一路 SQL 失败 写入 diagnostics.degraded,其它召回源继续
Wiki 检索失败 原文与推荐问题召回继续
现代召回全空 启用存量 Tree Vector / BM25 / 模型目录导航
全部召回为空 建议改写查询;配置允许时可加载 Research Skill
推荐问题生成失败 每片补足 3 个模板问题并返回 warning
Wiki 页面生成失败 回退到候选摘要页,并继续维护来源引用
构建事务失败 保留旧知识,不提交半成品
向量补写失败 词面索引继续可用;失败向量可再次补写
API 重启 丢失排队或执行中的知识构建;已提交数据不受影响

召回诊断会记录各路候选 key、融合结果、入选文章、去重项、重排策略、耗时和降级原因,并进入 Trace,便于区分“资料确实不存在”和“某个召回源故障”。

12. 知识新鲜度

文章构建会保存:

  • 源文章标题 + 正文的 SHA-256;
  • chunkAlgorithmVersion
  • buildVersion
  • 构建时间和来源引用。

文章正文变化、切片算法升级或构建版本落后时,分片面板和 Wiki Lint 会标记 stale / outdated。 重新执行单篇“构建知识”会替换该文分片并更新它对聚合 Wiki 页的贡献,不需要清空整个知识库。

13. 三种问答入口不要混淆

登录后站内助手

  • 接口/api/assistant/chat
  • 范围:当前用户有权访问的知识库
  • 运行方式:完整 Agent Runtime 与本文主召回管线

外部 Agent REST / MCP

  • 范围:API Key scope 允许的数据
  • 运行方式:文档搜索复用主混合召回;读写继续受 scope 和审计约束

公开问答

  • 接口/api/public/qa/chat
  • 范围:无密码、未过期的公开分享文章及其可达 Wiki 页
  • 运行方式:最多 8 步只读工具循环,并执行独立的公开可见性过滤

Warning

公开问答不会越过分享边界读取私有分片,也不会把登录用户的完整 RAG 索引暴露给匿名访客。

14. 推荐操作顺序

  1. 在“AI 模型配置”绑定 CHAT 模型;需要语义召回时再绑定 EMBEDDING 模型。
  2. 创建、导入或更新 Markdown 文章。
  3. 如有领域规则,先在 Wiki 页保存“编译说明书”。
  4. 对文章执行“构建知识”,检查切片、推荐问题和 warning。
  5. 在 Wiki 页执行“更新 Wiki”,补齐 source 页和 PageIndex 目录。
  6. 反复点击“生成向量”,直到原文分片和推荐问题均 ready。
  7. 运行结构检查,处理 stale source、断链和旧构建版本。
  8. 在助手中分别用精确事实、结构总结和跨文章问题验收。

15. 配置与代码入口

知识构建并发来自 apps/api/config.toml

[knowledge_build]
concurrency = 8
queue_size = 128
question_batch_concurrency = 8
page_batch_concurrency = 8
model_concurrency = 64

主要实现按职责分布如下,避免用超长代码路径撑宽表格:

知识构建

  • 切片与推荐问题:apps/api/internal/kb/kb-workflow.go
  • 整篇候选、目录规划与页面生成:apps/api/internal/kb/kb-workflow_extraction.go
  • 构建事务与多文章 Wiki 聚合:apps/api/internal/kb/wiki-build.go
  • 内存任务队列与并发:apps/api/internal/kb/wiki-build-job.go
  • 分片与问题向量:apps/api/internal/kb/wiki-chunk-index.go
  • source 页与 PageIndex:apps/api/internal/kb/wiki-ingest.gowiki-tree.go

召回与 Agent 工具

  • 混合召回:apps/api/internal/assistantsvc/knowledge_recall.go
  • Wiki、Tree、重排与多样性:apps/api/internal/assistantsvc/knowledge_recall_wiki.go
  • Search / Read:apps/api/internal/assistantsvc/tools_knowledge.go
  • Outline:apps/api/internal/assistantsvc/outline_tools.go

继续阅读

  • Agent Runtime:了解循环、预算、Evidence、Trace 与安全边界。
  • 工具协议:查看工具命名、确认票据和统一输出格式。
  • 外部客户端接入:把 Petrichor 接入 Claude Code、Codex 或 Cursor。
  • 运维手册:部署、探针、Worker、指标和发布检查。

Clone this wiki locally