GitHub 仓库 :https://github.com/zhoubyte/java-ai
如果你觉得这个项目有价值,欢迎 Star ⭐ 和关注,持续分享 AI 工程化实践!
一、传统向量 RAG 的五大根本性缺陷
要理解 PageIndex 为什么能颠覆传统 RAG,必须先深入理解向量 RAG 的底层缺陷------不是"调参能解决的小问题",而是范式层面的根本性矛盾。
1.1 查询-知识空间不匹配(Query-Knowledge Mismatch)
向量检索的核心假设是:与查询语义最相似的文本 = 最相关的文本。但这个假设在绝大多数真实场景中不成立。
用户提问表达的是需求意图 ,而非文本内容。二者之间存在本质差异:
| 用户问题 | 语义最相似的文本 | 真正相关的文本 |
|---|---|---|
| "网络安全事件损失了多少" | "网络安全防护投入 2.3 亿元" | "DDoS 攻击导致直接经济损失 850 万元" |
| "公司偿债能力如何" | "公司债务规模达 120 亿元" | "流动比率 2.35,速动比率 2.01" |
| "违约赔偿怎么算" | "合同概述中提到违约条款" | "第 8.3 条 违约赔偿计算方式" |
根因:Embedding 模型编码的是语义相似性(semantic similarity),而非逻辑相关性(logical relevance)或领域重要性(domain-specific importance)。向量空间中距离近,只代表"措辞像",不代表"能回答这个问题"。
1.2 语义相似 ≠ 实际相关(The Vibe Retrieval Problem)
这是向量 RAG 在专业领域文档中最致命的问题。在金融财报、法律合同、技术手册等场景中,大量文本片段语义高度相近,但实际含义天差地别:
同一份财报中的三段文本:
A. "2024年公司营收同比增长12.3%,净利润增长8.7%" ← 2024年数据
B. "2025年公司营收同比增长18.7%,净利润增长22.1%" ← 2025年数据
C. "2023年公司营收同比增长9.5%,净利润增长5.2%" ← 2023年数据
三段文本的向量距离极近(措辞几乎相同),但只有 B 能回答"2025年业绩如何"
向量检索无法区分这种关键的相关性差异------它不知道用户问的是哪一年。人类专家能理解"2025年"这个时间约束,向量不能。
1.3 硬切分破坏语义与上下文完整性(Hard Chunking Fragmentation)
传统 RAG 为适配 Embedding 模型的输入长度限制,必须将文档切分为固定大小的文本块(通常 512-1024 tokens)。这种"硬切分"会:
切断完整语义单元:
原始文档:
3.1 营收概览
公司2025年实现营业收入328.6亿元,同比增长18.7%。
其中,智能云服务142.5亿元(占比43.4%),AI解决方案89.2亿元(占比27.1%),
数据安全产品56.8亿元(占比17.3%),技术咨询40.1亿元(占比12.2%)。
切块后:
Chunk 1: "...营业收入328.6亿元,同比增长18.7%。其中,智能云服务142.5亿元"
Chunk 2: "(占比43.4%),AI解决方案89.2亿元(占比27.1%),数据安全产品56.8亿元"
→ 数字与含义被截断,"142.5亿元"和"占比43.4%"分离到两个 chunk
丢失跨章节引用:
文档中的引用关系:
"如第4.1节所述的网络安全事件,详见附录A的损失明细表"
切块后:
Chunk A: "如第4.1节所述的网络安全事件" ← 引用来源
Chunk B: "详见附录A的损失明细表" ← 引用目标
→ 引用链断裂,LLM 无法沿着引用追踪到目标内容
切片大小的两难困境:
- 切片过小(512 tokens)→ 上下文割裂,语义碎片化
- 切片过大(4096+ tokens)→ 单个切片语义过于分散,Embedding 表征模糊,召回精度下降
无论怎么调 chunk_size 和 overlap,都无法从根本上解决这个矛盾------因为问题出在"切分"这个动作本身。
1.4 无法整合对话历史(Independent Query Processing)
向量检索将每个用户查询视为独立事件,无法利用对话上下文:
用户:公司2025年营收是多少?
系统:328.6亿元,同比增长18.7%。
用户:那前一年呢? ← 指代"2024年营收"
系统:❌ 向量检索用"前一年"做 embedding,可能返回任何关于"之前"的内容
人类专家能理解"那前一年呢"指的是"2024年营收",因为记住了上一轮对话。向量检索没有这个能力------每个 query 都是孤立的。
1.5 检索过程不可解释(Black Box Retrieval)
向量检索是一个黑箱过程:
- 无法解释为什么返回了某个 chunk 而非另一个
- 无法提供检索路径和推理依据
- 在金融、法律、医疗等合规要求高的领域,不可解释性是致命缺陷
当系统返回错误答案时,你只能看到"它返回了这些 chunk",但不知道为什么选了这些、为什么没选那些。调试向量 RAG 是出了名的困难------你面对的是一个 1536 维空间中的黑箱。
二、PageIndex 的核心设计理念
2.1 AlphaGo 启发:搜索树替代记忆
PageIndex 团队用了一个精妙的类比:AlphaGo。
AlphaGo 不是通过记忆所有棋局位置来下棋的------它通过搜索树(Monte Carlo Tree Search)来推理每一步的最优走法。它不问"哪个位置和当前局面最相似",而是推理"走这一步之后,局势会怎样发展"。
PageIndex 把同样的思想应用到文档检索:
| AlphaGo | PageIndex |
|---|---|
| 棋盘状态树 | 文档层级树 |
| 每一步推理选择落子位置 | 每一步推理选择导航节点 |
| 不依赖记忆所有棋局 | 不依赖向量索引所有文本块 |
| 搜索 + 推理 | 搜索 + 推理 |
核心范式转换:从"哪个文本块和查询最相似"(匹配范式)转向"沿着文档结构推理,哪条路径通向答案"(推理范式)。
2.2 模拟人类专家的阅读行为
一个人类专家在查阅一份 200 页的年度报告时,绝不会把它切成 400 个碎片然后逐个比对相似度。他会:
- 看目录 → 快速了解文档结构,锁定可能相关的章节
- 推理判断 → "网络安全事件损失"应该在"风险因素"章节下
- 深入阅读 → 进入"4.1 网络安全风险",提取具体数字
- 评估充分性 → 答案是否完整?需要继续查找吗?
PageIndex 把这个人类阅读过程形式化为两个阶段:离线索引构建 和在线推理检索。
三、两阶段架构深度解析
3.1 阶段一:离线索引构建(Indexing Pipeline)
PDF / Markdown 文档
│
▼
┌─────────────┐
│ 结构解析 │ 提取文档的天然层级结构(目录、章节、标题)
└──────┬──────┘
│
▼
┌─────────────┐
│ 层级树构建 │ 将扁平段落列表还原为嵌套树结构
└──────┬──────┘
│
▼
┌─────────────┐
│ 摘要生成 │ LLM 为每个节点生成语义摘要(自底向上)
└──────┬──────┘
│
▼
DocumentTree(可序列化的 JSON 索引)
3.1.1 结构解析:保留文档天然骨架
PageIndex 的文档解析不是"切块",而是提取文档的天然层级结构。文档的章节、标题、页码本身就是人类作者精心组织的导航线索------为什么要丢弃它们?
PDF 解析(基于 PDFBox 3.x):
java
// 识别两种标题模式
private static final Pattern HEADING_PATTERN =
Pattern.compile("^(\\d+(\\.\\d+)*)\\s+(.+)$"); // "1.2.3 概述" → level=3
private static final Pattern CHAPTER_PATTERN =
Pattern.compile("^(第[一二三四五六七八九十百千]+[章节篇部])\\s+(.+)$"); // "第三章 财务分析" → level=1
逐页提取文本,通过正则识别标题行,推断层级深度。关键细节:
- 数字编号 :通过点号数量推断层级(
1.2.3有 2 个点 → level=3) - 中文章节 :
第X章/节统一映射为 level=1 - 跨页合并:同一章节内容分布在相邻页时自动合并内容和页码范围
Markdown 解析:
java
// # 的数量直接映射层级深度
private static final Pattern HEADING_PATTERN = Pattern.compile("^(#{1,6})\\s+(.+)$");
// ## 3.1 营收概览 → level=2, title="3.1 营收概览"
解析结果输出为 ParsedSection 列表------一个保留标题、层级、页码和内容的中间数据结构:
java
public class ParsedSection {
private String title; // 段落标题
private int level; // 层级(对应 # 的数量或 PDF 章节深度)
private int pageStart; // 起始页码
private int pageEnd; // 结束页码
private String content; // 段落文本内容
}
3.1.2 层级树构建:栈算法还原嵌套结构
解析器输出的是扁平的段落列表,需要还原为嵌套的树结构。核心算法使用栈来维护当前路径:
java
private TreeNode buildHierarchy(List<ParsedSection> sections) {
TreeNode root = TreeNode.builder().nodeId("0").title("Document Root").level(0).build();
Deque<TreeNode> stack = new ArrayDeque<>();
stack.push(root);
for (ParsedSection section : sections) {
TreeNode node = TreeNode.builder()
.nodeId(String.valueOf(nodeCounter++))
.title(section.getTitle())
.level(section.getLevel())
.content(section.getContent())
.build();
// 弹出栈中层级 >= 当前段落的节点,找到正确的父节点
while (stack.size() > 1 && stack.peek().getLevel() >= section.getLevel()) {
stack.pop();
}
stack.peek().addChild(node);
stack.push(node);
}
return root;
}
算法执行过程示例:
输入段落列表:
[1] 董事长致辞 (level=1)
[2] 公司概况 (level=1)
[3] 财务分析 (level=1)
[3.1] 营收概览 (level=2)
[3.2] 成本与费用 (level=2)
[4] 风险因素 (level=1)
栈的变化过程:
初始: [Root]
处理 [1]: pop(none, level=1 < Root的0) → Root.addChild([1]) → [Root, [1]]
处理 [2]: pop([1], level=1 >= 1) → Root.addChild([2]) → [Root, [2]]
处理 [3]: pop([2], level=1 >= 1) → Root.addChild([3]) → [Root, [3]]
处理 [3.1]: pop(none, level=2 > [3]的1) → [3].addChild([3.1]) → [Root, [3], [3.1]]
处理 [3.2]: pop([3.1], level=2 >= 2) → [3].addChild([3.2]) → [Root, [3], [3.2]]
处理 [4]: pop([3.2], level=1 < 2), pop([3], level=1 >= 1) → Root.addChild([4]) → [Root, [4]]
最终树结构:
Root
├── [1] 董事长致辞
├── [2] 公司概况
├── [3] 财务分析
│ ├── [3.1] 营收概览
│ └── [3.2] 成本与费用
└── [4] 风险因素
3.1.3 摘要生成:自底向上的语义浓缩
树结构构建完成后,需要为每个节点生成摘要。摘要的质量直接决定推理导航的准确性。PageIndex 采用自底向上策略:
java
private void generateSummariesRecursive(TreeNode node, ChatClient chatClient) {
// 第一步:叶节点用 LLM 根据原文生成摘要
if (node.getContent() != null && !node.getContent().isBlank()) {
String summary = generateSummary(chatClient, node.getTitle(), node.getContent());
node.setSummary(summary);
}
// 第二步:递归处理子节点(保证子节点摘要先生成)
for (TreeNode child : node.getChildren()) {
generateSummariesRecursive(child, chatClient);
}
// 第三步:非叶节点由子节点摘要聚合
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
if (node.getSummary() == null || node.getSummary().isBlank()) {
node.setSummary(buildChildrenSummary(node));
}
}
}
为什么自底向上?
叶节点有原始文本内容,LLM 可以生成高质量摘要。非叶节点没有自己的内容(只有子节点),如果直接让 LLM 生成摘要,需要传入所有子节点的完整文本,token 消耗巨大。自底向上策略让父节点摘要由子节点摘要聚合而成,既保证了信息完整性,又控制了 token 消耗。
叶节点摘要 vs 父节点摘要的区别:
叶节点 [4.1] 网络安全风险:
LLM 生成摘要 → "2025年8月DDoS攻击导致直接经济损失850万元,
公司投入2.3亿元升级安全防护体系"
父节点 [4] 风险因素:
由子节点摘要聚合 → "Contains sections: 网络安全风险 - DDoS攻击损失850万...;
市场竞争风险 - 华为云阿里云竞争...;
政策合规风险 - GDPR数据安全法...;
人才流失风险 - 核心研发离职率8.2%..."
父节点的聚合摘要天然包含了所有子节点的信息概要,为推理导航提供了完整的"路标"------LLM 在根节点就能看到整棵树的全景。
3.1.4 最终索引结构
构建完成后的 DocumentTree 可以序列化为 JSON 持久化存储:
json
{
"docId": "annual_report_2025",
"docName": "星辰科技2025年度报告",
"totalPages": 35,
"createdAt": "2026-05-17T10:30:00",
"root": {
"nodeId": "0",
"title": "Document Root",
"summary": "Contains sections: 董事长致辞 - ...; 公司概况 - ...; 财务分析 - ...; 风险因素 - ...; 战略规划 - ...",
"children": [
{
"nodeId": "1",
"title": "1. 董事长致辞",
"level": 1,
"pageStart": 1,
"pageEnd": 2,
"summary": "2025年营收328.6亿同比增长18.7%,净利润52.3亿增长22.1%,2026年将加大AI和量子计算投入",
"content": "2025年是星辰科技集团发展历程中..."
},
{
"nodeId": "4",
"title": "4. 风险因素",
"level": 1,
"pageStart": 12,
"pageEnd": 18,
"summary": "Contains sections: 网络安全风险 - DDoS攻击损失850万; 市场竞争风险 - ...; ...",
"children": [
{
"nodeId": "5",
"title": "4.1 网络安全风险",
"level": 2,
"pageStart": 12,
"pageEnd": 14,
"summary": "2025年8月DDoS攻击导致直接经济损失850万元,公司投入2.3亿元升级安全防护体系",
"content": "2025年8月,集团云服务平台遭受一次DDoS攻击..."
}
]
}
]
}
}
3.2 阶段二:在线推理检索(Retrieval Pipeline)
这是 PageIndex 的灵魂------用 LLM 推理替代向量匹配的检索过程。
用户问题
│
▼
┌──────────────────────────────────────────────┐
│ 迭代推理循环 │
│ │
│ ┌─────────┐ ┌──────────┐ ┌─────────┐ │
│ │ 读取 │───▶│ 推理选择 │───▶│ 深入 │ │
│ │ 子节点 │ │ 最相关 │ │ 子节点 │ │
│ │ 摘要 │ │ 子节点 │ │ │ │
│ └─────────┘ └──────────┘ └────┬────┘ │
│ ▲ │ │
│ │ 未到叶节点 │ │
│ └──────────────────────────────┘ │
│ │ │
│ 到达叶节点 │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 提取内容 │ │
│ │ 生成答案 │ │
│ └────┬─────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ 评估充分性│ │
│ └──────────┘ │
└──────────────────────────────────────────────┘
│
▼
QueryResult(答案 + 推理路径 + 引用信息)
3.2.1 推理导航的核心机制
每一步导航,系统将当前节点的子节点摘要(而非原文)展示给 LLM,让其推理选择最相关的子节点:
java
private NavigationDecision navigateNode(ChatClient chatClient, String query,
TreeNode node, int step) {
// 构建子节点信息列表------只传摘要,不传原文
StringBuilder childrenInfo = new StringBuilder();
for (TreeNode child : node.getChildren()) {
childrenInfo.append(String.format(
"- Node ID: %s | Title: %s | Pages: %d-%d | Summary: %s\n",
child.getNodeId(), child.getTitle(),
child.getPageStart(), child.getPageEnd(),
child.getSummary() != null ? child.getSummary() : "N/A"
));
}
String prompt = """
You are a document navigation agent. Your task is to find the most
relevant section in a document tree to answer a user's question.
User Question: %s
Current Node: [%s] %s
Available Child Nodes:
%s
Based on the user's question and the summaries of the child nodes,
which child node is most likely to contain the answer?
Respond in the following format:
REASONING: <your step-by-step reasoning about which node to select and why>
SELECTED_NODE: <the Node ID of the most relevant child node, or "NONE">
""".formatted(query, node.getNodeId(), node.getTitle(), childrenInfo);
String response = chatClient.prompt().user(prompt).call().content();
return parseNavigationResponse(response, step, node);
}
这个设计的关键洞察:
-
只传摘要,不传原文------每次导航的 LLM 输入只有几百 token(几个子节点的摘要),而非几千 token 的原文。这极大降低了推理成本。
-
结构化输出 ------要求 LLM 以
REASONING: ... SELECTED_NODE: ...格式输出,既获得了推理过程(可解释),又获得了决策结果(可执行)。 -
LLM 推理替代向量匹配------LLM 能理解"损失金额"和"网络安全事件"的因果关系,能理解"偿债能力"应该看"资产负债"章节而非"营收"章节。这是向量相似度永远做不到的。
3.2.2 完整推理过程示例
以"网络安全事件损失了多少?"为例,展示完整的推理导航过程:
Step 1:根节点导航
输入给 LLM 的信息:
User Question: 网络安全事件损失了多少?
Current Node: [0] Document Root
Available Child Nodes:
- Node ID: 1 | Title: 1. 董事长致辞 | Summary: 2025年营收328.6亿...
- Node ID: 2 | Title: 2. 公司概况 | Summary: 成立于2008年...
- Node ID: 3 | Title: 3. 财务分析 | Summary: Contains sections: 营收概览...
- Node ID: 4 | Title: 4. 风险因素 | Summary: Contains sections: 网络安全风险 - DDoS攻击损失850万; 市场竞争风险...
- Node ID: 5 | Title: 5. 战略规划 | Summary: Contains sections: AI原生战略...
LLM 输出:
REASONING: The question asks about losses from a network security incident.
"风险因素" section contains a child about "网络安全风险" which mentions
DDoS attack losses. This is the most relevant section.
SELECTED_NODE: 4
决策:NAVIGATE → 进入节点 [4]
Step 2:风险因素节点导航
输入给 LLM 的信息:
User Question: 网络安全事件损失了多少?
Current Node: [4] 4. 风险因素
Available Child Nodes:
- Node ID: 5 | Title: 4.1 网络安全风险 | Summary: 2025年8月DDoS攻击导致直接经济损失850万元...
- Node ID: 6 | Title: 4.2 市场竞争风险 | Summary: 华为云、阿里云竞争加剧...
- Node ID: 7 | Title: 4.3 政策合规风险 | Summary: GDPR、数据安全法...
- Node ID: 8 | Title: 4.4 人才流失风险 | Summary: 核心研发离职率8.2%...
LLM 输出:
REASONING: The question specifically asks about network security incident
losses. Node 4.1 "网络安全风险" explicitly mentions "DDoS攻击导致直接
经济损失850万元", which directly answers the question.
SELECTED_NODE: 5
决策:NAVIGATE → 进入节点 [5]
Step 3:到达叶节点,提取内容
节点 [5] 是叶节点(无子节点),提取其原始内容:
"2025年8月,集团云服务平台遭受一次DDoS攻击,持续时间约4小时...
此次事件导致直接经济损失约850万元..."
调用 LLM 基于提取内容生成最终答案
Step 4:评估答案充分性
LLM 评估:问题"网络安全事件损失了多少"与答案"直接经济损失约850万元"
是否充分?→ YES
最终输出:
answer: "根据4.1节网络安全风险部分,2025年8月的DDoS攻击导致直接经济损失约850万元。"
reasoningPath: [Step1 → Step2 → Step3]
sufficient: true
3.2.3 对比:向量 RAG 会怎么做?
同一个问题,传统向量 RAG 的处理过程:
1. 将"网络安全事件损失了多少"编码为向量
2. 在向量数据库中搜索最相似的 Top-K chunks
3. 可能返回的 chunks:
✓ "DDoS攻击导致直接经济损失约850万元" (相关,但可能排在第3-4位)
✗ "公司投入2.3亿元升级安全防护体系" (语义相似,但答非所问)
✗ "网络安全防护能力达到国家等保三级标准" (语义相似,但答非所问)
✗ "2024年网络安全事件造成损失320万元" (年份错误!)
4. LLM 基于这些 chunks 生成答案 → 可能引用错误年份或混淆"投入"和"损失"
向量检索无法区分"损失"和"投入"的区别,也无法区分不同年份的同类事件。PageIndex 的推理导航能精准区分------因为它理解"损失"和"投入"的语义差异,理解时间约束。
3.2.4 安全机制:防止推理失控
推理式检索引入了新的风险------LLM 可能在树中"迷路"。PageIndex 通过多重安全机制防止推理失控:
java
private static final int MAX_DEPTH = 5; // 最大搜索深度
private static final int MAX_ITERATIONS = 10; // 最大迭代次数
public QueryResult query(DocumentTree tree, String query) {
TreeNode current = tree.getRoot();
int step = 0;
while (current != null && step < MAX_ITERATIONS) {
step++;
// 到达叶节点 → 提取内容,终止导航
if (current.isLeaf()) {
relevantNodes.add(current);
break;
}
// LLM 推理选择子节点
NavigationDecision decision = navigateNode(chatClient, query, current, step);
// LLM 判断无相关子节点 → 终止导航
if (decision.selectedNodeId == null) {
break;
}
// 选中节点不存在(LLM 幻觉)→ 终止导航
TreeNode selectedChild = findChildById(current, decision.selectedNodeId);
if (selectedChild == null) {
break;
}
current = selectedChild;
}
// ...
}
三种终止条件:到达叶节点、LLM 判断无相关节点、安全阈值触发。
3.2.5 可追溯的推理路径
PageIndex 的每次查询都返回完整的推理路径,这是传统 RAG 无法提供的:
java
public class QueryResult {
private String query; // 用户原始问题
private String answer; // LLM 生成的最终答案
private List<ReasoningStep> reasoningPath; // 推理路径(每一步的决策过程)
private List<String> referencedNodeIds; // 引用的节点 ID
private List<String> referencedSections; // 引用的章节(含页码)
private boolean sufficient; // 答案是否充分
public static class ReasoningStep {
private int step; // 步骤序号
private String nodeId; // 当前所在节点
private String title; // 当前节点标题
private String reasoning; // LLM 的推理过程
private String decision; // 决策:NAVIGATE / EXTRACT / STOP
}
}
为什么可追溯性如此重要?
- 调试:当答案错误时,可以精确定位是哪一步推理出了问题
- 合规:金融、法律领域要求答案可审计、可解释
- 信任:用户能看到"系统为什么给出这个答案",而非黑箱输出
- 优化:基于推理路径分析,可以针对性优化摘要质量或 Prompt 设计
四、Java 实现的技术选型与设计决策
4.1 技术栈
| 技术 | 版本 | 选型理由 |
|---|---|---|
| Spring Boot | 3.x | 生态成熟,Spring AI 集成便捷 |
| Spring AI + Ollama | - | 本地 LLM 推理,零 API 成本,数据不出域 |
| Apache PDFBox | 3.0.5 | 纯 Java PDF 解析,3.x API 更现代 |
| Lombok | - | 减少 POJO 样板代码 |
| Jackson | - | 树结构 JSON 序列化/反序列化 |
4.2 为什么选择 Spring AI + Ollama
PageIndex 的核心依赖是 LLM,技术选型需要考虑:
- 本地部署:企业文档涉及敏感数据,不能调用外部 API
- 多模型支持:摘要生成和推理导航可以用不同模型
- 成本控制:导航步骤用小模型(qwen3:0.6b),答案生成用大模型
Spring AI 的 ChatClient 抽象让模型切换只需改配置,无需改代码。Ollama 提供本地推理能力,一行命令即可切换模型。
4.3 配置设计
yaml
spring:
ai:
ollama:
base-url: http://localhost:11434 # Ollama 服务地址
chat:
options:
model: qwen3:0.6b # 聊天模型
temperature: 0.3 # 低温度保证推理稳定性
pageindex:
max-reasoning-depth: 5 # 推理树搜索最大深度
max-reasoning-iterations: 10 # 单次查询最大推理迭代次数
summary-max-length: 2000 # 生成摘要时截取的最大文本长度
content-max-length: 3000 # 生成答案时截取的最大内容长度
temperature: 0.3 是关键参数------推理导航需要确定性输出,高温度会导致 LLM 在相同输入下做出不同选择,破坏检索稳定性。
五、成本分析:推理式检索真的更贵吗?
直觉上,每次查询需要 3-5 次 LLM 调用,成本应该更高。但实际计算并非如此:
5.1 单次查询 Token 消耗对比
| 环节 | 向量 RAG | PageIndex |
|---|---|---|
| Query Embedding | ~50 tokens | 无 |
| 向量检索 | 0 tokens(数据库操作) | 无 |
| 导航 Step 1 | 无 | ~300 tokens(5个子节点摘要) |
| 导航 Step 2 | 无 | ~200 tokens(4个子节点摘要) |
| 导航 Step 3 | 无 | ~150 tokens(叶节点内容) |
| 答案生成 | ~2000 tokens(Top-K chunks) | ~1500 tokens(精准内容) |
| 充分性评估 | 无 | ~100 tokens |
| 总计 | ~2050 tokens | ~2250 tokens |
Token 消耗基本持平,但 PageIndex 的输入质量远高于向量 RAG------传入的是精准定位的内容,而非"可能相关"的 chunks。
5.2 基础设施成本对比
| 成本项 | 向量 RAG | PageIndex |
|---|---|---|
| Embedding 模型 | 需要(GPU / API 费用) | 不需要 |
| 向量数据库 | Pinecone / Milvus / Weaviate | 不需要 |
| 索引更新 | 文档变更需重新 Embedding | 只需重建受影响的子树 |
| 运维 | 向量库集群管理、备份、扩缩容 | 无额外基础设施 |
5.3 准确率带来的隐性成本
向量 RAG 返回错误答案的代价往往被忽视------用户需要反复追问、人工核实,甚至做出错误决策。PageIndex 在 FinanceBench 上达到 98.7% 准确率(传统向量 RAG 仅 30-50%),这意味着:
- 更少的追问轮次
- 更少的人工核实
- 更高的用户信任度
六、PageIndex vs 传统 RAG 全维度对比
| 维度 | 传统向量 RAG | PageIndex |
|---|---|---|
| 检索范式 | 匹配(找最相似的) | 推理(找最相关的) |
| 文档处理 | 固定大小切块 | 保留天然层级结构 |
| 依赖组件 | Embedding 模型 + 向量数据库 | 仅需 LLM |
| 可解释性 | 黑盒(为什么返回这些块?) | 完整推理路径可追溯 |
| 跨章节问题 | 容易丢失上下文 | 天然支持层级遍历 |
| 对话上下文 | 每次查询独立 | 推理路径可传递上下文 |
| 跨引用追踪 | 无法处理 | 可沿引用导航 |
| 精度 | 受语义歧义影响(30-50%) | 推理理解因果关系(98.7%) |
| 基础设施 | 向量数据库 + Embedding 服务 | 仅 LLM 服务 |
| 适用场景 | 短文档、无结构文本 | 长文档、有层级结构的文档 |
七、API 使用指南
7.1 构建索引
bash
# 从文件构建(支持 PDF / Markdown)
curl -X POST "http://localhost:8081/pageindex/pageindex/index?filePath=/path/to/report.pdf"
# 从文本内容构建(直接传入 Markdown 格式文本)
curl -X POST "http://localhost:8081/pageindex/pageindex/index/text?title=年度报告" \
-H "Content-Type: text/plain" \
--data-binary @report.md
7.2 推理式查询
bash
curl -X POST "http://localhost:8081/pageindex/pageindex/query?docId=xxx&query=网络安全事件损失了多少"
返回结果包含完整的推理路径:
json
{
"query": "网络安全事件损失了多少",
"answer": "根据4.1节网络安全风险部分,2025年8月的DDoS攻击导致直接经济损失约850万元。",
"reasoningPath": [
{
"step": 1,
"nodeId": "0",
"title": "Document Root",
"reasoning": "The question asks about losses from a network security incident. '风险因素' section contains a child about '网络安全风险' which mentions DDoS attack losses.",
"decision": "NAVIGATE"
},
{
"step": 2,
"nodeId": "4",
"title": "4. 风险因素",
"reasoning": "Node 4.1 '网络安全风险' explicitly mentions 'DDoS攻击导致直接经济损失850万元', which directly answers the question.",
"decision": "NAVIGATE"
},
{
"step": 3,
"nodeId": "5",
"title": "4.1 网络安全风险",
"reasoning": "Reached leaf node containing the relevant content",
"decision": "EXTRACT"
}
],
"referencedSections": ["4.1 网络安全风险 (pp.12-14)"],
"sufficient": true
}
7.3 查看文档大纲
bash
curl "http://localhost:8081/pageindex/pageindex/index/{docId}/outline"
7.4 索引管理
bash
# 列出所有已索引文档
curl "http://localhost:8081/pageindex/pageindex/indices"
# 删除索引
curl -X DELETE "http://localhost:8081/pageindex/pageindex/index/{docId}"
八、快速开始
前置条件
- JDK 17+
- Ollama 已安装并运行
启动步骤
bash
# 1. 拉取模型
ollama pull qwen3:0.6b
# 2. 启动 Ollama 服务
ollama serve
# 3. 编译项目
cd java-ai
./mvnw compile -pl pageindex-demo
# 4. 运行
./mvnw spring-boot:run -pl pageindex-demo
5 分钟体验
项目内置了一份中文案例文档------星辰科技2025年度报告,包含完整的财务分析、风险因素、战略规划等章节,非常适合体验 PageIndex 的推理式检索能力。
bash
# 构建索引
curl -X POST "http://localhost:8081/pageindex/pageindex/index/text?title=星辰科技2025年度报告" \
-H "Content-Type: text/plain" \
--data-binary @pageindex-demo/src/main/resources/documents/星辰科技2025年度报告.md
# 用返回的 docId 进行查询
curl -X POST "http://localhost:8081/pageindex/pageindex/query?docId=<your-doc-id>&query=公司2025年研发投入了多少"
九、局限性与未来方向
当前已知局限
| 局限 | 影响 | 严重程度 |
|---|---|---|
| 单路径导航 | 每次只选择一个子节点,可能遗漏兄弟节点中的相关信息 | 中 |
| 无回溯机制 | LLM 选错方向后无法回退 | 高 |
| 内存存储 | 索引保存在内存中,重启丢失 | 中 |
| PDF 解析 | 复杂排版(多栏、表格、图片)解析质量有限 | 中 |
| 单文档检索 | 当前仅支持单文档查询,不支持跨文档推理 | 低 |
未来计划
- 多路径探索:允许 LLM 同时选择多个子节点,扩大召回范围
- 回溯策略:当答案不充分时自动回溯到上一层,选择其他路径
- 持久化存储:将索引保存到数据库(MongoDB / PostgreSQL JSONB)
- 增量更新:文档变更时只重建受影响的子树,而非整棵树
- 混合检索:在推理导航的基础上,结合向量检索作为补充------先用向量粗筛,再用推理精排
- 跨文档推理:支持在多棵文档树之间进行关联推理
- 对话记忆:将对话历史注入推理 Prompt,实现指代消解和上下文延续
十、总结
PageIndex 代表了 RAG 系统的一个范式转换:
传统 RAG: 文档 → 切块 → 向量化 → 相似度匹配 → 拼接上下文 → 生成答案
PageIndex: 文档 → 结构解析 → 层级树 + 摘要 → 推理导航 → 精准提取 → 生成答案
核心差异不在于技术细节,而在于检索哲学:
- 传统 RAG 问的是"哪个文本块和查询最像"------这是一个匹配问题
- PageIndex 问的是"沿着文档结构推理,哪条路径通向答案"------这是一个推理问题
当文档有结构、问题有深度、答案要精准时,推理永远比匹配更可靠。
GitHub 仓库 :https://github.com/zhoubyte/java-ai
Star ⭐ 关注,持续更新 AI 工程化实践!