PageIndex:用推理替代向量的下一代 RAG 架构

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 个碎片然后逐个比对相似度。他会:

  1. 看目录 → 快速了解文档结构,锁定可能相关的章节
  2. 推理判断 → "网络安全事件损失"应该在"风险因素"章节下
  3. 深入阅读 → 进入"4.1 网络安全风险",提取具体数字
  4. 评估充分性 → 答案是否完整?需要继续查找吗?

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);
}

这个设计的关键洞察

  1. 只传摘要,不传原文------每次导航的 LLM 输入只有几百 token(几个子节点的摘要),而非几千 token 的原文。这极大降低了推理成本。

  2. 结构化输出 ------要求 LLM 以 REASONING: ... SELECTED_NODE: ... 格式输出,既获得了推理过程(可解释),又获得了决策结果(可执行)。

  3. 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,技术选型需要考虑:

  1. 本地部署:企业文档涉及敏感数据,不能调用外部 API
  2. 多模型支持:摘要生成和推理导航可以用不同模型
  3. 成本控制:导航步骤用小模型(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 工程化实践!

相关推荐
Honey Ro3 小时前
从“开卷考试”到“精准喂饭”,工业级 RAG 检索质量的演进与调优血泪史
深度学习·自然语言处理·rag
带刺的坐椅3 小时前
Solon Flow 实战:用 50 行 YAML 实现一个请假审批流(含中断恢复、并行网关、条件分支)
java·solon·工作流·审批流·流程编排
张二娃同学3 小时前
02_C语言数据类型_整型浮点型字符型一次讲清楚
android·java·c语言
程序猿乐锅3 小时前
【Tilas|第九篇】登录认证功能实现
java·笔记·tlias
optimistic_chen3 小时前
【AI Agent 全栈开发】RAG(检索增强生成)
java·linux·运维·人工智能·ai编程·rag
我的xiaodoujiao3 小时前
API 接口自动化测试详细图文教程学习系列19--添加封装其他的方法
开发语言·python·学习·测试工具·pytest
@蔓蔓喜欢你3 小时前
CSS Grid布局完全指南:构建复杂布局的利器
人工智能·ai
xiami_world3 小时前
2026年团队AI工具栈架构指南:ChatGPT + Codex + AI白板智能体工程化落地方案
人工智能·ai·信息可视化·aigc·流程图
Fzuim3 小时前
Claude Code AskUserQuestion 交互式提问机制深度解析
ai·agent·claude code