-
Notifications
You must be signed in to change notification settings - Fork 12
Agentic RAG
Petrichor 的 RAG 不是“切片后做一次向量 Top-K”。它同时保留原文分片、用户问法、产品内 Wiki 和文档目录四种知识表示,再让 Agent 按问题类型执行 Search / Outline / Read。 这样既能回答精确事实,也能处理跨文章比较、章节归纳和多跳概念关系,并且最终证据仍可追溯到 原文或 Wiki 页面。
Note
术语说明
本文的“产品内 Wiki”指 Petrichor 从知识库文章编译出的知识层;它与仓库的 GitHub Wiki 文档栏不是同一个系统。
- 第一次了解 Petrichor:先读 完整流程、四种知识表示 和 Search / Outline / Read。
- 准备部署或调参:重点读 知识构建、Markdown 切片、索引 和 配置入口。
- 正在排查召回质量:直接看 问答召回、降级策略 和 知识新鲜度。
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
Important
两条不可突破的证据边界
- Search 只找候选。 Agent 必须显式 Read,候选正文才会进入 Evidence。
-
推荐问题只是检索别名。 命中后仍映射回原始
chunkId,模型生成的问题不会成为事实来源。
| 知识表示 | 核心职责 | 读取定位 |
|---|---|---|
| 原文分片 | 保存标题路径与 Markdown 正文,回答精确事实、步骤、代码和引用 | chunkId |
| 推荐问题 | 补齐用户问法与原文措辞之间的差异;命中后回到同一原文分片 | chunkId |
| Wiki 页面 | 聚合实体、概念、关系与多篇文章贡献,支持语义导航和多跳关联 | pageKey |
| PageIndex | 保存章节层级与摘要,处理“有哪些章节”“按顺序总结”等结构问题 |
nodeKey / chunkId
|
原文负责事实,Wiki 负责语义导航,目录负责结构。三者不是互相替代的索引副本。
知识构建的标准输入是 petrichor_kb_article.content_md:
- 在编辑器中创建或修改 Markdown;
- 通过 REST / MCP / Agent Skill 创建或更新文章;
- 导入 PDF:可提取页直接转成 Markdown,扫描页由独立视觉 Worker 按页转写,全部页面完成后合并 成一篇文章。
保存文章或完成 PDF 导入并不等于已经建立 RAG 索引。文章需要执行一次“构建知识”;需要 PageIndex 目录时再执行“更新 Wiki”;配置了 Embedding 模型后还应补齐向量。
入口是 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"]
问题生成与整篇抽取并行执行;只有页面、链接和索引准备完整后,结果才会在同一事务中提交。
每个知识库可保存一份 compile-guide,用于告诉编译器领域、读者、抽取偏好、目录约定和页面
写法。它会追加到问题生成、候选抽取、目录规划和页面生成的系统提示中,但不能覆盖固定 JSON
输出协议。HTML 注释和只有标题没有正文的小节不会进入提示词。
详情见 知识可移植性文档。
切片由确定性 Go 代码完成,不依赖模型,也不是固定 token 窗口。当前算法版本为 2。
| 参数 | 当前值 | 作用 |
|---|---|---|
| 目标预算 | 1200 |
普通小节合并接近该规模时结束当前分片 |
| 小尾段阈值 | 400 |
短尾段优先并入同一顶层主题的相邻分片 |
| 标称上限 | 3200 |
超长内容进入回退切分;完整代码围栏可略超上限 |
| 超长重叠 | 320 |
只用于超长小节的相邻窗口 |
| 单篇上限 |
120 片 |
超出后停止生成后续分片并返回警告 |
这些数值是切片算法的文本预算,不是模型 tokenizer 的 token 数。
- 规范化换行并识别 Markdown
h1到h6; - 用标题栈保存完整
headingPath; - 围栏代码块中的
#不会被误判为标题; - 标题前导语以文章标题作为定位名称。
例如 安装 > Docker Compose > HTTPS 会完整保存在分片元数据中。检索时文章标题和这条标题
路径都会进入索引文本,避免“正文只有命令、上下文全在标题里”时召回失败。
算法按原顺序贪心合并相邻小节,但遵守两条边界:
- 不跨一级主题合并;
- 不为凑长度突破标称上限。
不足 400 的小尾段会优先并入同一一级主题的后一个分组,其次并入前一个分组。多个标题合并后, 如果某个小节占正文至少 60%,用它作为分片标题;否则锚定第一个有实质内容的小节,避免短标题 污染分片身份。
超长小节在窗口后半段按以下顺序寻找断点:
- 空行;
- 换行;
- 中英文句末标点;
- 实在找不到再用窗口边界。
断点不会落进代码围栏。如果标称边界位于围栏内,会优先保留完整代码块。相邻超长窗口保留 约 320 的上下文,并尽量把新窗口起点对齐到下一行。
- 标题路径被保留,回答可以显示“文章 › 章节 › 子章节”;
- 配置段、步骤列表和代码块不容易被拦腰切断;
- 普通章节没有无意义的全局 overlap,减少重复候选;
- 分片算法可版本化,文章正文哈希或算法版本变化时会标记为 stale。
当前 120 片上限只限制分片/问题索引。整篇 Wiki 抽取走独立上下文预算,因此超长文档的 Wiki 候选与可逐片召回范围可能不同,构建结果会明确返回截断警告。
切片完成后,每批最多 4 片、约 4000 文本预算,模型为每片生成恰好 3 个问题。问题必须能仅靠 该分片回答,不能输出答案。缺项、无效 JSON 或模型失败时,系统会补成模板问题,例如:
- “这一节主要讲了什么?”
- “这一节有哪些关键结论?”
- “如何理解并应用这一节?”
每个问题独立进入 source_type = question 的词面与向量索引,但外键仍指向原始分片。这样用户问
“怎么部署”可以命中正文写作“生产安装”的章节,同时回答阶段仍读取原文,而不是读取合成问题。
“Wiki”包含两条互补路径,理解这一点很重要。
单篇构建不会按分片逐片抽实体,而是把整篇文档交给候选抽取器:
- 生成文档摘要;
- 抽取有实质内容支撑的实体和概念,通常 5–20 个、最多 24 个;
- 抽取候选之间有原文依据的关系;
- 对照已有页面复用稳定
pageKey,避免同一对象重复建页; - 为整批候选规划最多两级、可复用的浅目录;
- 每批 4 页生成独立 Markdown,正文中的相关对象使用
[[pageKey|显示名]]; - 写入 source / entity / concept 页面、页面链接、来源引用和知识库索引页。
整篇输入上限为 72000 个 Unicode 字符。超限时保留约 62% 文首和 38% 文尾,并显式插入省略 标记。页面生成失败会退回候选摘要页;候选抽取失败时仍可保存原文分片和 source 页面。
同一实体或概念可聚合多篇文章的贡献。重新构建某篇文章前,系统先移除它的旧贡献,再写入新 贡献;没有其它来源和人工底稿的孤立生成页会被清理。最终事务同时提交分片、索引、Wiki、链接 与来源引用,失败时不会留下半套新知识。
POST /api/kb/wiki/ingest 面向整个知识库:
- 按 source hash 跳过未变化文章;
- 生成文章级 source 页摘要、要点、实体名称和推荐问题;
- 按
h1–h6建立 PageIndex 目录树; - 最多 40 个有正文节点时用一次模型调用生成章节摘要,更多时使用本地摘要;
- best-effort 为目录树节点补向量。
这条路径主要服务 knowledge.outline 和存量 Tree 召回,不会替代“构建知识”产生的现代分片、
问题索引与 entity / concept 页面。
“完全重建”会清空产品内 Wiki 页面、链接、来源引用和目录树,但保留人工编译说明书;现代文章 分片仍在。随后它只重建 source 页和 PageIndex 树,因此若要恢复 entity / concept 语义页面, 还需要对相关文章重新执行“构建知识”。
每条原文或问题索引的 embedding_text 都按以下顺序拼接,并截到 4000 字符:
文章标题
一级标题
二级标题
当前分片正文或推荐问题
原文和问题分开存储,使两种表示可以独立召回、再统一映射到分片。
PostgreSQL 内置 parser 不适合直接切中文,所以应用层使用同一套中英文词元规则:
- 英文和数字按词;
- 连续中文使用 2 字滑窗;
- 查询词元去重;
- PostgreSQL 先尝试 GIN
search_vector收窄候选,未命中时退回ILIKE词元模式; - 最多取 400 个候选,在 Go 中计算真正的 BM25,而不是把
ts_rank当最终分数。
应用层 BM25 对标题、路径摘要、正文使用 3 / 2 / 1 的字段权重,并做 IDF 与长度归一化。
构建事务提交后会 best-effort 补原文分片向量。Wiki 页的“生成向量”操作继续按严格顺序执行:
- 当前知识库所有原文分片向量 ready;
- 再生成推荐问题向量。
每批最多 64 条,单次每阶段最多处理 2000 条。检索只使用与当前 Embedding 模型、维度和版本完全
一致且状态为 ready 的向量;切换模型或维度后旧向量不会被误用,而会进入待重算状态。
向量不可用不阻断构建,BM25、Wiki 和目录路径仍可工作。
以下是登录后站内助手和外部 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 候选"]
简单问题保持原样。较长、包含多个分句或连接词的问题会用确定性规则拆成最多 3 个子查询;调用方
也可以显式传入 subQueries。连同原问题,总查询数最多 4 个。这里不额外调用 LLM,避免简单问题
被“改写坏”。
对每个查询执行:
- 原文分片向量 Top 10;
- 推荐问题向量 Top 10;
- 原文分片 BM25 Top 10;
- 推荐问题 BM25 Top 10;
- Wiki 页面词面检索 Top 10。
如果原文和推荐问题两路词面检索都为空,还会按文章标题兜底。限定到单篇文章时不搜索 Wiki 页面, 避免跨文章概念页污染当前文章范围。
不同召回源的原始分数不可直接比较,因此使用 Reciprocal Rank Fusion:
RRF(candidate) = Σ 1 / (60 + rank + 1)
相同 chunkId 的原文与问题命中会合并,多路命中的排名贡献全部保留,但候选展示和后续重排优先
使用原文内容。融合最多保留 30 个候选。
为避免一个热门长文占满结果,系统先做文章级平衡:
- 默认选择最多 3 篇文章;
- 每篇最多带 4 个候选进入下一阶段;
- 文章得分综合该文前三个候选和多路命中数;
- 候选按文章轮询交错,而不是先塞满第一篇。
前 10 个候选再按原始问题做本地重排:标题、路径/摘要、正文分别使用递减权重,完整问题命中高于 单词命中。当前是确定性本地重排,不依赖外部 reranker 服务,因此不会增加一次模型请求。
本地重排后的候选先执行多样性过滤:标题相同或词元 Jaccard 相似度达到 0.88 时去重,默认每篇
最多保留 3 个。若结果还未达到 limit,再从融合后的文章候选补齐,同时继续遵守单篇数量上限;
最终 limit 最大为 20。
只有现代分片、问题和 Wiki 三类全部未命中时,才启用存量 PageIndex Tree:
- Tree 向量;
- Tree BM25;
- 若限定知识库且快速召回仍为空,或问题复杂度为
complex,让模型浏览最多 240 个目录节点。
旧 Tree 不会与现代结果同时抢排名,避免粗粒度目录节点遮住精确分片。
knowledge.search 返回标题、路径、摘要、召回来源和 chunkId / pageKey / nodeKey 等定位符,
不返回完整正文。Agent 可根据问题和候选决定深读 1 个还是多个章节。
相似度召回擅长“哪一段最像”,不擅长:
- “这篇文档有哪些章节讨论缓存?”
- “按章节顺序总结安装流程”;
- “比较第二章和第五章的结论”。
knowledge.outline 优先读取 PageIndex 树;没有树时,从已构建分片的 headingPath 和推荐问题重建
轻量目录。Agent 拿到目录后选择章节,再用定位符 Read。
knowledge.read / knowledge.read_many 可读取:
- 原始分片;
- 产品内 Wiki 页面及其出链、入链;
- PageIndex 节点或子树;
- 必要时整篇源文章。
简单任务一次批量深读最多 2 项,其它复杂度最多 4 项。读取结果由统一工具执行器归一化成 Evidence,进入上下文预算、Trace 和最终质量门。搜索摘要本身不会冒充完整证据。
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。
| 触发条件 | 保底行为 |
|---|---|
| Embedding 未配置或调用失败 | 继续使用 BM25、Wiki、文章标题和目录召回 |
| 原文或问题某一路 SQL 失败 | 写入 diagnostics.degraded,其它召回源继续 |
| Wiki 检索失败 | 原文与推荐问题召回继续 |
| 现代召回全空 | 启用存量 Tree Vector / BM25 / 模型目录导航 |
| 全部召回为空 | 建议改写查询;配置允许时可加载 Research Skill |
| 推荐问题生成失败 | 每片补足 3 个模板问题并返回 warning |
| Wiki 页面生成失败 | 回退到候选摘要页,并继续维护来源引用 |
| 构建事务失败 | 保留旧知识,不提交半成品 |
| 向量补写失败 | 词面索引继续可用;失败向量可再次补写 |
| API 重启 | 丢失排队或执行中的知识构建;已提交数据不受影响 |
召回诊断会记录各路候选 key、融合结果、入选文章、去重项、重排策略、耗时和降级原因,并进入 Trace,便于区分“资料确实不存在”和“某个召回源故障”。
文章构建会保存:
- 源文章标题 + 正文的 SHA-256;
-
chunkAlgorithmVersion; -
buildVersion; - 构建时间和来源引用。
文章正文变化、切片算法升级或构建版本落后时,分片面板和 Wiki Lint 会标记 stale / outdated。 重新执行单篇“构建知识”会替换该文分片并更新它对聚合 Wiki 页的贡献,不需要清空整个知识库。
-
接口:
/api/assistant/chat - 范围:当前用户有权访问的知识库
- 运行方式:完整 Agent Runtime 与本文主召回管线
- 范围:API Key scope 允许的数据
- 运行方式:文档搜索复用主混合召回;读写继续受 scope 和审计约束
-
接口:
/api/public/qa/chat - 范围:无密码、未过期的公开分享文章及其可达 Wiki 页
- 运行方式:最多 8 步只读工具循环,并执行独立的公开可见性过滤
Warning
公开问答不会越过分享边界读取私有分片,也不会把登录用户的完整 RAG 索引暴露给匿名访客。
- 在“AI 模型配置”绑定
CHAT模型;需要语义召回时再绑定EMBEDDING模型。 - 创建、导入或更新 Markdown 文章。
- 如有领域规则,先在 Wiki 页保存“编译说明书”。
- 对文章执行“构建知识”,检查切片、推荐问题和 warning。
- 在 Wiki 页执行“更新 Wiki”,补齐 source 页和 PageIndex 目录。
- 反复点击“生成向量”,直到原文分片和推荐问题均 ready。
- 运行结构检查,处理 stale source、断链和旧构建版本。
- 在助手中分别用精确事实、结构总结和跨文章问题验收。
知识构建并发来自 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.go、wiki-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、指标和发布检查。