<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh"><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://leesanyee.me/feed.xml" rel="self" type="application/atom+xml"/><link href="https://leesanyee.me/" rel="alternate" type="text/html" hreflang="zh"/><updated>2026-08-06T14:54:14+00:00</updated><id>https://leesanyee.me/feed.xml</id><title type="html">leesanyee</title><subtitle>LeeSanYee 的技术博客，记录 ML/NLP、C++、Linux 等方向的学习笔记与思考。 </subtitle><entry><title type="html">Graph RAG with Milvus —— 纯向量库造图的多跳推理</title><link href="https://leesanyee.me/blog/2026/GraphRAG-with-Milvus/" rel="alternate" type="text/html" title="Graph RAG with Milvus —— 纯向量库造图的多跳推理"/><published>2026-08-06T14:00:00+00:00</published><updated>2026-08-06T14:00:00+00:00</updated><id>https://leesanyee.me/blog/2026/GraphRAG-with-Milvus</id><content type="html" xml:base="https://leesanyee.me/blog/2026/GraphRAG-with-Milvus/"><![CDATA[<h1 id="graph-rag-with-milvus--纯向量库造图的多跳推理">Graph RAG with Milvus —— 纯向量库造图的多跳推理</h1> <h2 id="要解决的问题">要解决的问题</h2> <p>传统 RAG 在<strong>多跳问题</strong>上翻车：比如「欧拉（Euler）的老师（Johann）的儿子（Daniel）做了哪些贡献？」。</p> <p>普通相似度检索的原理是「查询和段落的语义贴近」，但答案藏在<strong>隔着两跳的关系</strong>里：</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Euler ←是学生→ Johann ←是儿子→ Daniel ←贡献→ 流体力学
</code></pre></div></div> <p>查询和 Daniel 的段落语义上离得很远，naive RAG 直接搜不到。</p> <p>答案：<strong>Graph RAG</strong> —— 用实体关系连成图，沿着图走多跳。但常见的 GraphRAG（如微软版）要装<strong>图数据库</strong>。这个 notebook 的创新是：<strong>只用向量库（Milvus）就做出图推理</strong>，不装图库。</p> <h2 id="核心思想不存图算图">核心思想：不存图，算图</h2> <p>整个系统的灵魂一句话：<strong>把「图」拆成 3 个向量集合，图结构不存，查询时用邻接矩阵现算多跳。</strong></p> <h2 id="入库3-个向量集合--邻接映射">入库：3 个向量集合 + 邻接映射</h2> <h3 id="原始数据段落--三元组">原始数据：段落 + 三元组</h3> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"passage"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Jakob Bernoulli (1654–1705): ... 他做了X贡献 ..."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"triplets"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">[</span><span class="s2">"Jakob Bernoulli"</span><span class="p">,</span><span class="w"> </span><span class="s2">"was the older brother of"</span><span class="p">,</span><span class="w"> </span><span class="s2">"Johann Bernoulli"</span><span class="p">],</span><span class="w">
    </span><span class="p">[</span><span class="s2">"Jakob Bernoulli"</span><span class="p">,</span><span class="w"> </span><span class="s2">"is known for"</span><span class="p">,</span><span class="w"> </span><span class="s2">"the Bernoulli numbers"</span><span class="p">]</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div> <h3 id="拆成-3-类">拆成 3 类</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">entities</span>  <span class="o">=</span> <span class="p">[]</span>   <span class="c1"># 实体：三元组的 subject 和 object
</span><span class="n">relations</span> <span class="o">=</span> <span class="p">[]</span>   <span class="c1"># 关系：三元组拼成一句话
</span><span class="n">passages</span>  <span class="o">=</span> <span class="p">[]</span>   <span class="c1"># 原文段落
</span>
<span class="k">for</span> <span class="n">triplet</span> <span class="ow">in</span> <span class="n">triplets</span><span class="p">:</span>
    <span class="n">entities</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">triplet</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>   <span class="c1"># subject
</span>    <span class="n">entities</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">triplet</span><span class="p">[</span><span class="mi">2</span><span class="p">])</span>   <span class="c1"># object
</span>    <span class="n">relations</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">triplet</span><span class="p">))</span>  <span class="c1"># "Jakob Bernoulli was the older brother..."
</span></code></pre></div></div> <p><strong>关键</strong>：关系不是存结构化 (subject, predicate, object)，而是<strong>拼成自然语言句子</strong>再嵌入，这样向量才有语义。</p> <h3 id="建-2-张邻接映射表这才是图">建 2 张邻接映射表（这才是「图」）</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">entityid_2_relationids</span> <span class="o">=</span> <span class="nf">defaultdict</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span>  <span class="c1"># 实体 → 参与哪些关系
</span><span class="n">relationid_2_passageids</span> <span class="o">=</span> <span class="nf">defaultdict</span><span class="p">(</span><span class="nb">list</span><span class="p">)</span> <span class="c1"># 关系 → 出自哪个段落
</span>
<span class="n">entityid_2_relationids</span><span class="p">[</span><span class="n">entities</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">triplet</span><span class="p">[</span><span class="mi">0</span><span class="p">])].</span><span class="nf">append</span><span class="p">(</span><span class="n">relation_id</span><span class="p">)</span>
<span class="n">entityid_2_relationids</span><span class="p">[</span><span class="n">entities</span><span class="p">.</span><span class="nf">index</span><span class="p">(</span><span class="n">triplet</span><span class="p">[</span><span class="mi">2</span><span class="p">])].</span><span class="nf">append</span><span class="p">(</span><span class="n">relation_id</span><span class="p">)</span>  <span class="c1"># 两端都连
</span><span class="n">relationid_2_passageids</span><span class="p">[</span><span class="n">relation_id</span><span class="p">].</span><span class="nf">append</span><span class="p">(</span><span class="n">passage_id</span><span class="p">)</span>
</code></pre></div></div> <p><strong>图从没作为图存过</strong> —— 就两张 Python dict 记录「实体↔关系」「关系↔段落」谁连着谁。</p> <h3 id="3-个向量集合milvus-标准的-embedding-入库">3 个向量集合（Milvus）—— 标准的 embedding 入库</h3> <p>3 个列表各自 embedding 进对应集合，跟普通 RAG 入库没区别：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nf">create_milvus_collection</span><span class="p">(</span><span class="sh">"</span><span class="s">entity_collection</span><span class="sh">"</span><span class="p">)</span>   <span class="c1"># 实体向量
</span><span class="nf">create_milvus_collection</span><span class="p">(</span><span class="sh">"</span><span class="s">relation_collection</span><span class="sh">"</span><span class="p">)</span> <span class="c1"># 关系向量
</span><span class="nf">create_milvus_collection</span><span class="p">(</span><span class="sh">"</span><span class="s">passage_collection</span><span class="sh">"</span><span class="p">)</span>  <span class="c1"># 段落向量
</span>
<span class="k">def</span> <span class="nf">milvus_insert</span><span class="p">(</span><span class="n">collection_name</span><span class="p">,</span> <span class="n">text_list</span><span class="p">):</span>
    <span class="k">for</span> <span class="n">row_id</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">text_list</span><span class="p">),</span> <span class="mi">512</span><span class="p">):</span>
        <span class="n">batch_texts</span> <span class="o">=</span> <span class="n">text_list</span><span class="p">[</span><span class="n">row_id</span> <span class="p">:</span> <span class="n">row_id</span> <span class="o">+</span> <span class="mi">512</span><span class="p">]</span>
        <span class="n">batch_embeddings</span> <span class="o">=</span> <span class="n">embedding_model</span><span class="p">.</span><span class="nf">embed_documents</span><span class="p">(</span><span class="n">batch_texts</span><span class="p">)</span>  <span class="c1"># 文本→向量
</span>        <span class="n">batch_data</span> <span class="o">=</span> <span class="p">[{</span><span class="sh">"</span><span class="s">id</span><span class="sh">"</span><span class="p">:</span> <span class="n">id_</span><span class="p">,</span> <span class="sh">"</span><span class="s">text</span><span class="sh">"</span><span class="p">:</span> <span class="n">text</span><span class="p">,</span> <span class="sh">"</span><span class="s">vector</span><span class="sh">"</span><span class="p">:</span> <span class="n">vector</span><span class="p">}</span> <span class="p">...]</span>
        <span class="n">milvus_client</span><span class="p">.</span><span class="nf">insert</span><span class="p">(</span><span class="n">collection_name</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">batch_data</span><span class="p">)</span>

<span class="nf">milvus_insert</span><span class="p">(</span><span class="n">relation_col_name</span><span class="p">,</span> <span class="n">relations</span><span class="p">)</span>  <span class="c1"># 关系列表 → relation 向量库
</span><span class="nf">milvus_insert</span><span class="p">(</span><span class="n">entity_col_name</span><span class="p">,</span>   <span class="n">entities</span><span class="p">)</span>   <span class="c1"># 实体列表 → entity 向量库
</span><span class="nf">milvus_insert</span><span class="p">(</span><span class="n">passage_col_name</span><span class="p">,</span>  <span class="n">passages</span><span class="p">)</span>   <span class="c1"># 段落列表 → passage 向量库
</span></code></pre></div></div> <p>每条记录 <code class="language-plaintext highlighter-rouge">{id, text, vector}</code>：<code class="language-plaintext highlighter-rouge">id</code> = 在列表里的下标，<code class="language-plaintext highlighter-rouge">text</code> = 原文，<code class="language-plaintext highlighter-rouge">vector</code> = embedding。</p> <p><strong>注意</strong>：向量库是「3 列表分别 embedding」来的，<strong>不是从 dict 转的</strong>。dict 和向量库是两条平行线，都来自同一批 3 列表，dict 只记连接、向量库存向量，靠 <strong>id 对齐</strong>。</p> <h2 id="邻接矩阵把-dict-变成能算的矩阵">邻接矩阵：把 dict 变成能算的矩阵</h2> <p>查询前把「实体-关系」dict 转成稀疏矩阵：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">entity_relation_adj</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="nf">zeros</span><span class="p">((</span><span class="nf">len</span><span class="p">(</span><span class="n">entities</span><span class="p">),</span> <span class="nf">len</span><span class="p">(</span><span class="n">relations</span><span class="p">)))</span>
<span class="n">entity_relation_adj</span><span class="p">[</span><span class="n">entity_id</span><span class="p">,</span> <span class="n">entityid_2_relationids</span><span class="p">[</span><span class="n">entity_id</span><span class="p">]]</span> <span class="o">=</span> <span class="mi">1</span>
<span class="n">entity_relation_adj</span> <span class="o">=</span> <span class="nf">csr_matrix</span><span class="p">(</span><span class="n">entity_relation_adj</span><span class="p">)</span>  <span class="c1"># 稀疏省内存
</span></code></pre></div></div> <p><code class="language-plaintext highlighter-rouge">entity_relation_adj[i][j]=1</code> ⟺ 实体 i 参与了关系 j。</p> <p><strong>矩阵乘法 = 图遍历</strong>（核心魔法）：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 1跳：A @ A.T
</span><span class="n">entity_adj_1_degree</span> <span class="o">=</span> <span class="n">entity_relation_adj</span> <span class="o">@</span> <span class="n">entity_relation_adj</span><span class="p">.</span><span class="n">T</span>
<span class="c1"># N跳：乘 N 次
</span><span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="n">target_degree</span> <span class="o">-</span> <span class="mi">1</span><span class="p">):</span>
    <span class="n">entity_adj_target_degree</span> <span class="o">=</span> <span class="n">entity_adj_target_degree</span> <span class="o">@</span> <span class="n">entity_adj_1_degree</span><span class="p">.</span><span class="n">T</span>
</code></pre></div></div> <p>直觉：<code class="language-plaintext highlighter-rouge">A @ A.T</code> 里，实体 i 和 k 若连到同一关系 j，乘积为 1 —— 说明 i、k 隔一跳相连。<strong>乘一次 = 扩一跳</strong>。</p> <h2 id="查询向量召回--矩阵展开--llm-过滤--回段落">查询：向量召回 → 矩阵展开 → LLM 过滤 → 回段落</h2> <h3 id="-双路向量召回找图的入口种子">① 双路向量召回（找「图的入口种子」）</h3> <p>查询进来，两条路并行去向量库搜，各得一个种子：</p> <p><strong>路 1：实体路</strong> —— NER 提实体，搜实体库</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">query_ner_list</span> <span class="o">=</span> <span class="p">[</span><span class="sh">"</span><span class="s">Euler</span><span class="sh">"</span><span class="p">]</span>            <span class="c1"># NER 从查询里抽出实体名
# query_ner_list = ner(query)         # 实际做法，这里写死省事
</span><span class="n">query_ner_emb</span> <span class="o">=</span> <span class="n">embedding_model</span><span class="p">.</span><span class="nf">embed_query</span><span class="p">(</span><span class="sh">"</span><span class="s">Euler</span><span class="sh">"</span><span class="p">)</span>
<span class="n">entity_search</span> <span class="o">=</span> <span class="n">milvus_client</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="n">entity_collection</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">query_ner_emb</span><span class="p">,</span> <span class="n">limit</span><span class="o">=</span><span class="mi">3</span><span class="p">)</span>
</code></pre></div></div> <p>目的：精确定位到查询里<strong>明确提到的实体节点</strong>（如 “Euler”）。</p> <p><strong>路 2：关系路</strong> —— 整句查询，搜关系库</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">query_emb</span> <span class="o">=</span> <span class="n">embedding_model</span><span class="p">.</span><span class="nf">embed_query</span><span class="p">(</span><span class="sh">"</span><span class="s">What contribution did the son of Euler</span><span class="sh">'</span><span class="s">s teacher make?</span><span class="sh">"</span><span class="p">)</span>
<span class="n">relation_search</span> <span class="o">=</span> <span class="n">milvus_client</span><span class="p">.</span><span class="nf">search</span><span class="p">(</span><span class="n">relation_collection</span><span class="p">,</span> <span class="n">data</span><span class="o">=</span><span class="n">query_emb</span><span class="p">,</span> <span class="n">limit</span><span class="o">=</span><span class="mi">3</span><span class="p">)</span>
</code></pre></div></div> <p>目的：整句语义去匹配<strong>意图接近的关系</strong>（如「师生/父子」这类模式）。</p> <p><strong>为什么两路都要</strong>：实体是「点」，关系是「线」。多跳问题光有点（Euler）不够，还得有线（谁是谁的老师/儿子）。实体路抓锚点，关系路抓结构，一路漏了另一路补。</p> <blockquote> <p>NER = Named Entity Recognition，命名实体识别，从文本里抽出人名/地名/机构名这类专有名词。</p> </blockquote> <h3 id="-邻接矩阵多跳展开">② 邻接矩阵多跳展开</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># 从召回的关系出发，扩出所有邻居关系
</span><span class="k">for</span> <span class="n">hit_id</span> <span class="ow">in</span> <span class="n">召回的关系</span><span class="p">:</span>
    <span class="n">expanded_from_relation</span> <span class="o">|=</span> <span class="n">relation_adj_target_degree</span><span class="p">[</span><span class="n">hit_id</span><span class="p">].</span><span class="nf">nonzero</span><span class="p">()</span>

<span class="c1"># 从召回的实体出发，扩出相关的所有关系
</span><span class="k">for</span> <span class="n">hit_id</span> <span class="ow">in</span> <span class="n">召回的实体</span><span class="p">:</span>
    <span class="n">expanded_from_entity</span> <span class="o">|=</span> <span class="n">entity_relation_adj_target_degree</span><span class="p">[</span><span class="n">hit_id</span><span class="p">].</span><span class="nf">nonzero</span><span class="p">()</span>

<span class="n">relation_candidate_ids</span> <span class="o">=</span> <span class="n">expanded_from_relation</span> <span class="o">|</span> <span class="n">expanded_from_entity</span>  <span class="c1"># 并集
</span></code></pre></div></div> <p>这一步是<strong>图遍历</strong>：拿到种子实体/关系，用矩阵查出它们一跳（或多跳）能连到的所有关系。</p> <h3 id="-llm-rerankcot-挑关系">③ LLM rerank（CoT 挑关系）</h3> <p>候选关系太多，用 LLM 聪明地挑 3 条，带 one-shot 例子 + 思维链：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">rerank_res</span> <span class="o">=</span> <span class="n">rerank_chain</span><span class="p">.</span><span class="nf">invoke</span><span class="p">({</span><span class="sh">"</span><span class="s">question</span><span class="sh">"</span><span class="p">:</span> <span class="n">query</span><span class="p">,</span> <span class="sh">"</span><span class="s">relation_des_str</span><span class="sh">"</span><span class="p">:</span> <span class="n">候选关系</span><span class="p">})</span>
</code></pre></div></div> <p>LLM 理解「欧拉的老师是谁 → 谁是他儿子 → 儿子做了啥」，选中关键关系。</p> <h3 id="-映射回段落生成答案">④ 映射回段落，生成答案</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="n">relation_id</span> <span class="ow">in</span> <span class="n">rerank_relation_ids</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">passage_id</span> <span class="ow">in</span> <span class="n">relationid_2_passageids</span><span class="p">[</span><span class="n">relation_id</span><span class="p">]:</span>  <span class="c1"># 靠 dict 找原文
</span>        <span class="k">if</span> <span class="n">passage_id</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">final_passages</span><span class="p">:</span>
            <span class="n">final_passages</span><span class="p">.</span><span class="nf">append</span><span class="p">(</span><span class="n">passages</span><span class="p">[</span><span class="n">passage_id</span><span class="p">])</span>
</code></pre></div></div> <p>选中关系 → 用 <code class="language-plaintext highlighter-rouge">relationid_2_passageids</code> 找回段落 → 喂 LLM 生成。</p> <h2 id="完整流程">完整流程</h2> <p>入库是<strong>两条平行线</strong>：向量线（3列表→embedding→3向量库）和图线（3列表→数id→2张邻接dict），靠 id 对齐，dict 不是向量库的来源。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>入库: 文本 → 三元组
   ├─ 向量线: 实体/关系/段落 3列表 → 各自embedding → 3个Milvus向量库
   └─ 图线:   同一批3列表 → 数id → 2张邻接dict（只记连接，不产生向量）
                                 │ 查询时 dict 转稀疏邻接矩阵

查询: 查询文本
   ├─ 路1: NER提实体("Euler") → embed → 搜【实体库】 → 种子实体 id
   ├─ 路2: 整句embed → 搜【关系库】 → 种子关系 id
   └─ 两路种子 → 邻接矩阵扩N跳 → 候选关系集
      → LLM CoT rerank 挑3条关键关系
      → 靠 relationid_2_passageids 找回段落 → 生成答案
</code></pre></div></div> <p>双路召回：实体路（NER 抽名词搜实体库）+ 关系路（整句搜关系库），各提供一个「图的入口种子」。<strong>召回找入口，矩阵做蔓延</strong>，然后 LLM 挑关系、dict 回段落。</p> <h2 id="效果对比">效果对比</h2> <p>同一个多跳问题，naive RAG 和本方法：</p> <table> <thead> <tr> <th> </th> <th>naive RAG</th> <th>Graph RAG with Milvus</th> </tr> </thead> <tbody> <tr> <td>召回到</td> <td>欧拉本人、Johann 的段落</td> <td>欧拉 + <strong>Daniel（答案所在段）</strong></td> </tr> <tr> <td>答案</td> <td>“I don’t know”</td> <td>Daniel 贡献流体力学/概率/统计</td> </tr> </tbody> </table> <p>naive RAG 失败原因：查询与 Daniel 段落语义距离远，表面关键词对不上。GraphRAG 靠矩阵展开 + rerank 沿着关系链找到了答案。</p> <h2 id="技术要点">技术要点</h2> <ul> <li><strong>只用向量库</strong>做出图推理，省掉图数据库</li> <li><strong>稀疏矩阵</strong>做多跳展开，向量化快、省内存，可扩展到数千实体毫秒级</li> <li><strong>双路召回</strong>（实体 + 关系）提供冗余，一路漏了另一路补</li> <li><strong>LLM CoT rerank</strong> 模拟图的社区过滤，比纯相似度更懂多跳意图</li> </ul> <h2 id="一句话总结">一句话总结</h2> <blockquote> <p>Graph RAG with Milvus = <strong>向量召回找入口 + 邻接矩阵现算多跳 ≈ 图遍历 + LLM rerank 过滤 ≈ 社区</strong>。用「一个向量库」拼出伪图推理，省掉图数据库，代价是这些都得现算。</p> </blockquote> <h2 id="参考">参考</h2> <ul> <li>仓库：https://github.com/NirDiamant/RAG_Techniques</li> <li>Milvus / Zilliz Cloud：https://cloud.zilliz.com</li> </ul>]]></content><author><name></name></author><category term="RAG"/><category term="rag"/><category term="graphrag"/><category term="milvus"/><category term="multi-hop"/><category term="knowledge-graph"/><summary type="html"><![CDATA[Graph RAG with Milvus —— 纯向量库造图的多跳推理]]></summary></entry><entry><title type="html">Hierarchical Indices 层级索引 —— 先粗后细的两级检索</title><link href="https://leesanyee.me/blog/2026/Hierarchical-Indices/" rel="alternate" type="text/html" title="Hierarchical Indices 层级索引 —— 先粗后细的两级检索"/><published>2026-08-06T13:30:00+00:00</published><updated>2026-08-06T13:30:00+00:00</updated><id>https://leesanyee.me/blog/2026/Hierarchical-Indices</id><content type="html" xml:base="https://leesanyee.me/blog/2026/Hierarchical-Indices/"><![CDATA[<h1 id="hierarchical-indices-层级索引--先粗后细的两级检索">Hierarchical Indices 层级索引 —— 先粗后细的两级检索</h1> <h2 id="要解决的问题">要解决的问题</h2> <p>普通 RAG 用的是 <strong>flat index</strong>（扁平索引）：所有 chunk 一股脑塞进向量库，查询时全局相似度搜索。文档大或语料多时，flat 检索有两个问题：</p> <ol> <li><strong>效率低</strong>：每次都要扫全部 chunk。</li> <li><strong>丢失上下文</strong>：chunk 太小，脱离了它在文档里的位置，可能召回不相关的东西。</li> </ol> <p>Hierarchical Indices 的做法：<strong>建两级索引</strong> —— 先存文档级摘要，再存细节 chunk。检索时「先粗后细」：先扫摘要定位到相关文档，再进到该文档的 chunk 里挖细节。</p> <h2 id="两级索引怎么建">两级索引怎么建</h2> <p>key：<strong>摘要和 chunk 共享 page 元数据</strong>，这是两级能串起来的关键。</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>文档(按页) → 摘要向量库(summary)  ← 存每页摘要
           → chunk向量库(detailed) ← 存每页拆出的细节块
                    │
        两边都带 metadata["page"]  —— 靠页码把两级连起来
</code></pre></div></div> <h3 id="-生成摘要异步--限流">① 生成摘要（异步 + 限流）</h3> <p>用 <code class="language-plaintext highlighter-rouge">load_summarize_chain</code> 的 <code class="language-plaintext highlighter-rouge">map_reduce</code> 逐页摘要，配合 asyncio 并发 + 指数退避处理限流：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="k">def</span> <span class="nf">summarize_doc</span><span class="p">(</span><span class="n">doc</span><span class="p">):</span>
    <span class="n">summary_output</span> <span class="o">=</span> <span class="k">await</span> <span class="nf">retry_with_exponential_backoff</span><span class="p">(</span><span class="n">summary_chain</span><span class="p">.</span><span class="nf">ainvoke</span><span class="p">([</span><span class="n">doc</span><span class="p">]))</span>
    <span class="n">summary</span> <span class="o">=</span> <span class="n">summary_output</span><span class="p">[</span><span class="sh">'</span><span class="s">output_text</span><span class="sh">'</span><span class="p">]</span>
    <span class="k">return</span> <span class="nc">Document</span><span class="p">(</span>
        <span class="n">page_content</span><span class="o">=</span><span class="n">summary</span><span class="p">,</span>
        <span class="n">metadata</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">source</span><span class="sh">"</span><span class="p">:</span> <span class="n">path</span><span class="p">,</span> <span class="sh">"</span><span class="s">page</span><span class="sh">"</span><span class="p">:</span> <span class="n">doc</span><span class="p">.</span><span class="n">metadata</span><span class="p">[</span><span class="sh">"</span><span class="s">page</span><span class="sh">"</span><span class="p">],</span> <span class="sh">"</span><span class="s">summary</span><span class="sh">"</span><span class="p">:</span> <span class="bp">True</span><span class="p">}</span>
    <span class="p">)</span>

<span class="c1"># 分批处理，每批5个，避免撞限流
</span><span class="n">batch_size</span> <span class="o">=</span> <span class="mi">5</span>
<span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="nf">len</span><span class="p">(</span><span class="n">documents</span><span class="p">),</span> <span class="n">batch_size</span><span class="p">):</span>
    <span class="n">batch_summaries</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="nf">gather</span><span class="p">(</span><span class="o">*</span><span class="p">[</span><span class="nf">summarize_doc</span><span class="p">(</span><span class="n">doc</span><span class="p">)</span> <span class="k">for</span> <span class="n">doc</span> <span class="ow">in</span> <span class="n">batch</span><span class="p">])</span>
    <span class="n">summaries</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="n">batch_summaries</span><span class="p">)</span>
    <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="nf">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>   <span class="c1"># 批间暂停
</span></code></pre></div></div> <h3 id="-生成-chunk-并打标">② 生成 chunk 并打标</h3> <p>细节 chunk 用 <code class="language-plaintext highlighter-rouge">RecursiveCharacterTextSplitter</code> 切，metadata 里标记 <code class="language-plaintext highlighter-rouge">summary: False</code> 和 <code class="language-plaintext highlighter-rouge">page</code>：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">chunk</span><span class="p">.</span><span class="n">metadata</span><span class="p">.</span><span class="nf">update</span><span class="p">({</span>
    <span class="sh">"</span><span class="s">chunk_id</span><span class="sh">"</span><span class="p">:</span> <span class="n">i</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">summary</span><span class="sh">"</span><span class="p">:</span> <span class="bp">False</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">page</span><span class="sh">"</span><span class="p">:</span> <span class="nf">int</span><span class="p">(</span><span class="n">chunk</span><span class="p">.</span><span class="n">metadata</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="sh">"</span><span class="s">page</span><span class="sh">"</span><span class="p">,</span> <span class="mi">0</span><span class="p">))</span>
<span class="p">})</span>
</code></pre></div></div> <h3 id="-两个向量库">③ 两个向量库</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">summary_vectorstore</span><span class="p">,</span> <span class="n">detailed_vectorstore</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="p">.</span><span class="nf">gather</span><span class="p">(</span>
    <span class="nf">create_vectorstore</span><span class="p">(</span><span class="n">summaries</span><span class="p">),</span>
    <span class="nf">create_vectorstore</span><span class="p">(</span><span class="n">detailed_chunks</span><span class="p">)</span>
<span class="p">)</span>
</code></pre></div></div> <h2 id="层级检索先粗后细">层级检索：先粗后细</h2> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">retrieve_hierarchical</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">summary_vectorstore</span><span class="p">,</span> <span class="n">detailed_vectorstore</span><span class="p">,</span>
                          <span class="n">k_summaries</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span> <span class="n">k_chunks</span><span class="o">=</span><span class="mi">5</span><span class="p">):</span>
    <span class="c1"># 第一级：扫摘要定位相关文档
</span>    <span class="n">top_summaries</span> <span class="o">=</span> <span class="n">summary_vectorstore</span><span class="p">.</span><span class="nf">similarity_search</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="n">k_summaries</span><span class="p">)</span>

    <span class="n">relevant_chunks</span> <span class="o">=</span> <span class="p">[]</span>
    <span class="k">for</span> <span class="n">summary</span> <span class="ow">in</span> <span class="n">top_summaries</span><span class="p">:</span>
        <span class="c1"># 第二级：只在该摘要对应的页码里挖细节 chunk
</span>        <span class="n">page_number</span> <span class="o">=</span> <span class="n">summary</span><span class="p">.</span><span class="n">metadata</span><span class="p">[</span><span class="sh">"</span><span class="s">page</span><span class="sh">"</span><span class="p">]</span>
        <span class="n">page_filter</span> <span class="o">=</span> <span class="k">lambda</span> <span class="n">metadata</span><span class="p">:</span> <span class="n">metadata</span><span class="p">[</span><span class="sh">"</span><span class="s">page</span><span class="sh">"</span><span class="p">]</span> <span class="o">==</span> <span class="n">page_number</span>
        <span class="n">page_chunks</span> <span class="o">=</span> <span class="n">detailed_vectorstore</span><span class="p">.</span><span class="nf">similarity_search</span><span class="p">(</span>
            <span class="n">query</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="n">k_chunks</span><span class="p">,</span> <span class="nb">filter</span><span class="o">=</span><span class="n">page_filter</span>
        <span class="p">)</span>
        <span class="n">relevant_chunks</span><span class="p">.</span><span class="nf">extend</span><span class="p">(</span><span class="n">page_chunks</span><span class="p">)</span>

    <span class="k">return</span> <span class="n">relevant_chunks</span>
</code></pre></div></div> <p><strong>流程</strong>：</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>用户查询 → 扫夏摘要库 → 找到相关文档(页码) → 限定该页 → 扫chunk库 → 返回细节块
</code></pre></div></div> <p>关键点：第二级用 <code class="language-plaintext highlighter-rouge">page_filter</code> <strong>按页码过滤</strong>，只从第一级命中的文档页里取 chunk。这样细节不会跑偏到别的章节。</p> <h2 id="对比-flat-index">对比 flat index</h2> <table> <thead> <tr> <th>维度</th> <th>Flat Index</th> <th>Hierarchical Index</th> </tr> </thead> <tbody> <tr> <td>索引</td> <td>一层，全 chunk</td> <td>两层：摘要 + chunk</td> </tr> <tr> <td>首次检索</td> <td>全局扫 chunk</td> <td>先扫摘要（数据量小）</td> </tr> <tr> <td>上下文</td> <td>chunk 孤立</td> <td>摘要提供文档级上下文</td> </tr> <tr> <td>效率</td> <td>大语料慢</td> <td>先粗筛，快</td> </tr> <tr> <td>适用</td> <td>小文档/小语料</td> <td>大文档/大语料</td> </tr> </tbody> </table> <h2 id="技术细节亮点">技术细节亮点</h2> <ul> <li><strong>持久化</strong>：向量库 <code class="language-plaintext highlighter-rouge">save_local</code> 存盘，避免每次重算： <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">summary_store</span><span class="p">.</span><span class="nf">save_local</span><span class="p">(</span><span class="sh">"</span><span class="s">../vector_stores/summary_store</span><span class="sh">"</span><span class="p">)</span>
<span class="n">detailed_store</span><span class="p">.</span><span class="nf">save_local</span><span class="p">(</span><span class="sh">"</span><span class="s">../vector_stores/detailed_store</span><span class="sh">"</span><span class="p">)</span>
</code></pre></div> </div> </li> <li><strong>限流处理</strong>：<code class="language-plaintext highlighter-rouge">retry_with_exponential_backoff</code> + 分批 + <code class="language-plaintext highlighter-rouge">asyncio.sleep</code>，是调用外部 LLM 摘要时的工程标配。</li> <li><strong>异步</strong>：用 <code class="language-plaintext highlighter-rouge">asyncio.to_thread</code> + <code class="language-plaintext highlighter-rouge">asyncio.gather</code> 并发建两个向量库，I/O 密集场景提速。</li> </ul> <h2 id="一句话总结">一句话总结</h2> <blockquote> <p>Hierarchical Index = <strong>摘要粗筛定位 + 页码过滤细取</strong>。用两级索引换取「效率 + 上下文保真」，适合大文档/大语料，代价是入库时要多跑一遍摘要。</p> </blockquote> <h2 id="参考">参考</h2> <ul> <li>仓库：https://github.com/NirDiamant/RAG_Techniques</li> </ul>]]></content><author><name></name></author><category term="RAG"/><category term="rag"/><category term="hierarchical-index"/><category term="retrieval"/><category term="summarization"/><summary type="html"><![CDATA[Hierarchical Indices 层级索引 —— 先粗后细的两级检索]]></summary></entry><entry><title type="html">HyDE 与 HyPE —— 假设检索技术的两个方向</title><link href="https://leesanyee.me/blog/2026/HyDE-HyPE-Hypothetical-Retrieval/" rel="alternate" type="text/html" title="HyDE 与 HyPE —— 假设检索技术的两个方向"/><published>2026-08-06T13:00:00+00:00</published><updated>2026-08-06T13:00:00+00:00</updated><id>https://leesanyee.me/blog/2026/HyDE-HyPE-Hypothetical-Retrieval</id><content type="html" xml:base="https://leesanyee.me/blog/2026/HyDE-HyPE-Hypothetical-Retrieval/"><![CDATA[<h1 id="hyde-与-hype--假设检索技术的两个方向">HyDE 与 HyPE —— 假设检索技术的两个方向</h1> <h2 id="要解决的问题">要解决的问题</h2> <p>普通 RAG 检索时，用户查询是<strong>短问句</strong>，库里存的是<strong>长文档</strong>。两者的向量分布在 embedding 空间里相隔很远 —— 论文里叫 <strong>query-document style mismatch</strong>。短问句直接去匹配长文档，经常匹配不准。</p> <p>HyDE 和 HyPE 是解决这个「风格鸿沟」的两个方向。核心思路一致：<strong>用 LLM 生成一个「假的东西」，让查询和存储的形态对齐</strong>。但方向恰好相反。</p> <h2 id="hyde查询时生成假文档">HyDE：查询时生成假文档</h2> <p>HyDE = Hypothetical Document Embedding（假设文档嵌入）。</p> <p><strong>核心</strong>：查询时，先让 LLM 根据问题生成一个<strong>假装已经答好的文档</strong>，再用这个假文档去向量库检索。</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">HyDERetriever</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">files_path</span><span class="p">,</span> <span class="n">chunk_size</span><span class="o">=</span><span class="mi">500</span><span class="p">,</span> <span class="n">chunk_overlap</span><span class="o">=</span><span class="mi">100</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">hyde_prompt</span> <span class="o">=</span> <span class="nc">PromptTemplate</span><span class="p">(</span>
            <span class="n">input_variables</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="s">query</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">chunk_size</span><span class="sh">"</span><span class="p">],</span>
            <span class="n">template</span><span class="o">=</span><span class="sh">"""</span><span class="s">Given the question </span><span class="sh">'</span><span class="s">{query}</span><span class="sh">'</span><span class="s">, generate a hypothetical document
            that directly answers this question. The document should be detailed and in-depth.
            the document size has be exactly {chunk_size} characters.</span><span class="sh">"""</span><span class="p">,</span>
        <span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">hyde_chain</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">hyde_prompt</span> <span class="o">|</span> <span class="n">self</span><span class="p">.</span><span class="n">llm</span>

    <span class="k">def</span> <span class="nf">retrieve</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">3</span><span class="p">):</span>
        <span class="n">hypothetical_doc</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">generate_hypothetical_document</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>  <span class="c1"># 先生成假文档
</span>        <span class="n">similar_docs</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">vectorstore</span><span class="p">.</span><span class="nf">similarity_search</span><span class="p">(</span><span class="n">hypothetical_doc</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="n">k</span><span class="p">)</span>  <span class="c1"># 用假文档检索
</span>        <span class="k">return</span> <span class="n">similar_docs</span><span class="p">,</span> <span class="n">hypothetical_doc</span>
</code></pre></div></div> <p><strong>流程</strong>：</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>用户问题 → LLM 生成假文档(假设的答案) → 用假文档去 FAISS 检索 → 返回真实 chunk
</code></pre></div></div> <p>关键设定：prompt 里要求假文档长度 <code class="language-plaintext highlighter-rouge">exactly {chunk_size}</code>，让假文档和库里的 chunk <strong>长度形态一致</strong>，向量更贴近。</p> <ul> <li><strong>时机</strong>：查询时（在线）</li> <li><strong>成本</strong>：每次查询都要调 LLM 生成，有额外开销</li> <li><strong>匹配</strong>：文档 ↔ 假文档</li> </ul> <h2 id="hype入库时生成假问题">HyPE：入库时生成假问题</h2> <p>HyPE = Hypothetical Prompt Embeddings（假设提示词嵌入）。</p> <p><strong>核心</strong>：与 HyDE 反过来 —— 入库时，让 LLM 对每个 chunk <strong>猜几个「用户可能会问的问题」</strong>，把这些问题向量存进库，而不是存 chunk 原文向量。</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">generate_hypothetical_prompt_embeddings</span><span class="p">(</span><span class="n">chunk_text</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="n">question_chain</span> <span class="o">=</span> <span class="n">question_gen_prompt</span> <span class="o">|</span> <span class="n">llm</span> <span class="o">|</span> <span class="nc">StrOutputParser</span><span class="p">()</span>
    <span class="n">questions</span> <span class="o">=</span> <span class="n">question_chain</span><span class="p">.</span><span class="nf">invoke</span><span class="p">({</span><span class="sh">"</span><span class="s">chunk_text</span><span class="sh">"</span><span class="p">:</span> <span class="n">chunk_text</span><span class="p">}).</span><span class="nf">replace</span><span class="p">(</span><span class="sh">"</span><span class="se">\n\n</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="se">\n</span><span class="sh">"</span><span class="p">).</span><span class="nf">split</span><span class="p">(</span><span class="sh">"</span><span class="se">\n</span><span class="sh">"</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">chunk_text</span><span class="p">,</span> <span class="n">embedding_model</span><span class="p">.</span><span class="nf">embed_documents</span><span class="p">(</span><span class="n">questions</span><span class="p">)</span>
</code></pre></div></div> <p>入库时<strong>一对多</strong>：一个 chunk 生成 N 个问题向量，但都指向同一个 chunk，所以一个 chunk 被存 N 次。</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">chunks_with_embedding_vectors</span> <span class="o">=</span> <span class="p">[(</span><span class="n">chunk</span><span class="p">.</span><span class="n">page_content</span><span class="p">,</span> <span class="n">vec</span><span class="p">)</span> <span class="k">for</span> <span class="n">vec</span> <span class="ow">in</span> <span class="n">vectors</span><span class="p">]</span>
<span class="n">vector_store</span><span class="p">.</span><span class="nf">add_embeddings</span><span class="p">(</span><span class="n">chunks_with_embedding_vectors</span><span class="p">)</span>
</code></pre></div></div> <p><strong>流程</strong>：</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>chunk → LLM 猜N个问题 → 分别embed → N个问题向量(都指向同一chunk) → FAISS
用户查询 → embed → 问题↔问题匹配 → 映射回chunk原文
</code></pre></div></div> <ul> <li><strong>时机</strong>：入库时（离线）</li> <li><strong>成本</strong>：一次性，入库多花；查询零额外开销</li> <li><strong>匹配</strong>：问题 ↔ 问题</li> </ul> <h2 id="对比">对比</h2> <table> <thead> <tr> <th>维度</th> <th>HyDE</th> <th>HyPE</th> </tr> </thead> <tbody> <tr> <td>生成时机</td> <td>查询时（在线）</td> <td>入库时（离线）</td> </tr> <tr> <td>生成什么</td> <td>假装好的<strong>答案文档</strong></td> <td>用户会问的<strong>假设问题</strong></td> </tr> <tr> <td>成本位置</td> <td>每次查询都花</td> <td>入库一次性花</td> </tr> <tr> <td>匹配方式</td> <td>文档 ↔ 假文档</td> <td>问题 ↔ 问题</td> </tr> <tr> <td>扩展性</td> <td>查询越忙越贵</td> <td>查询阶段零开销，可扩展</td> </tr> </tbody> </table> <h2 id="一句话总结">一句话总结</h2> <p>HyDE 和 HyPE 是<strong>同一个思路的两个方向</strong>：都用 LLM 弥合「用户怎么问」和「文档怎么存」之间的风格鸿沟。</p> <ul> <li><strong>HyDE</strong> 把查询变成假文档，改的是<strong>查询侧</strong>。</li> <li><strong>HyPE</strong> 把文档变成假问题，改的是<strong>存储侧</strong>。</li> </ul> <p>HyPE 把成本挪到入库的一次性开销，换来查询时的零额外成本，这是它「可扩展」的最大卖点。HyPE 论文宣称检索精度最高可提升 42 个百分点。</p> <h2 id="参考">参考</h2> <ul> <li>HyPE 预印本：https://papers.ssrn.com/sol3/papers.cfm?abstract_id=5139335</li> <li>仓库：https://github.com/NirDiamant/RAG_Techniques</li> </ul>]]></content><author><name></name></author><category term="RAG"/><category term="rag"/><category term="hyde"/><category term="hype"/><category term="retrieval"/><category term="query-expansion"/><summary type="html"><![CDATA[HyDE 与 HyPE —— 假设检索技术的两个方向]]></summary></entry><entry><title type="html">二叉树刷题总结</title><link href="https://leesanyee.me/blog/2026/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%88%B7%E9%A2%98%E6%80%BB%E7%BB%93/" rel="alternate" type="text/html" title="二叉树刷题总结"/><published>2026-08-05T05:23:03+00:00</published><updated>2026-08-05T05:23:03+00:00</updated><id>https://leesanyee.me/blog/2026/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%88%B7%E9%A2%98%E6%80%BB%E7%BB%93</id><content type="html" xml:base="https://leesanyee.me/blog/2026/%E4%BA%8C%E5%8F%89%E6%A0%91%E5%88%B7%E9%A2%98%E6%80%BB%E7%BB%93/"><![CDATA[<h1 id="二叉树笔记总结">二叉树笔记总结</h1> <h2 id="一两种思维模式">一、两种思维模式</h2> <ol> <li><strong>遍历二叉树，外部变量实现</strong></li> <li><strong>分解为子问题</strong></li> </ol> <p>&gt; 快排类似于<strong>前序遍历</strong>，归并类似于<strong>后序遍历</strong></p> <hr/> <h2 id="二前中后序遍历的区别">二、前/中/后序遍历的区别</h2> <p><strong>核心：遍历位置决定它们能根据多少信息做判断</strong></p> <table> <thead> <tr> <th>遍历方式</th> <th>可用信息</th> </tr> </thead> <tbody> <tr> <td><strong>前序</strong></td> <td>上一次传入的信息</td> </tr> <tr> <td><strong>中序</strong></td> <td>传入的信息 + 左子树</td> </tr> <tr> <td><strong>后序</strong></td> <td>左右子树 + 传参</td> </tr> </tbody> </table> <hr/> <h2 id="三以树的视角看-dp--回溯--dfs">三、以树的视角看 DP / 回溯 / DFS</h2> <table> <thead> <tr> <th>算法</th> <th>本质</th> <th>关注点</th> </tr> </thead> <tbody> <tr> <td><strong>DP</strong></td> <td>分治</td> <td>整棵子树</td> </tr> <tr> <td><strong>回溯</strong></td> <td>遍历</td> <td>树枝（路径）</td> </tr> <tr> <td><strong>DFS</strong></td> <td>遍历</td> <td>单个节点</td> </tr> </tbody> </table> <hr/> <h2 id="四解题核心思路">四、解题核心思路</h2> <h3 id="1-节点视角">1. 节点视角</h3> <p>&gt; 如果单独抽出一个二叉树节点，它需要做什么事？需要在什么时候（前/中/后序）做？</p> <h3 id="2-dfs--路径类问题">2. DFS / 路径类问题</h3> <ul> <li>递归到底部</li> <li>保存类似路径的，使用递归</li> <li>用一个变量保存路径</li> </ul> <h3 id="3-最近公共祖先lca">3. 最近公共祖先（LCA）</h3> <p>使用递归思路，将问题拆分： 永find(root,val1,val2) 若根节点val=val1或val2，那根节点就是lca 左右子树各找到一个，那root还是祖先 只在一侧找到，结果在那一侧，返回（子树，val1，val2）</p> <h3 id="常见题型">常见题型</h3> <p>在dfs中，一般为遍历和分解问题等题型 在bfs中，显然只能遍历</p> <h3 id="代码">代码</h3> <p>dfs</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 二叉树的遍历框架</span>
<span class="kt">void</span> <span class="nf">traverse</span><span class="p">(</span><span class="n">TreeNode</span><span class="o">*</span> <span class="n">root</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">root</span> <span class="o">==</span> <span class="nb">nullptr</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="c1">// 前序位置</span>
    <span class="n">traverse</span><span class="p">(</span><span class="n">root</span><span class="o">-&gt;</span><span class="n">left</span><span class="p">);</span>
    <span class="c1">// 中序位置</span>
    <span class="n">traverse</span><span class="p">(</span><span class="n">root</span><span class="o">-&gt;</span><span class="n">right</span><span class="p">);</span>
    <span class="c1">// 后序位置</span>
<span class="p">}</span>
</code></pre></div></div> <p>bfs</p> <div class="language-cpp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">levelOrderTraverse</span><span class="p">(</span><span class="n">TreeNode</span><span class="o">*</span> <span class="n">root</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">root</span> <span class="o">==</span> <span class="nb">nullptr</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span><span class="p">;</span>
    <span class="p">}</span>
    <span class="n">queue</span><span class="o">&lt;</span><span class="n">TreeNode</span><span class="o">*&gt;</span> <span class="n">q</span><span class="p">;</span>
    <span class="n">q</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">root</span><span class="p">);</span>
    <span class="c1">// 记录当前遍历到的层数（根节点视为第 1 层）</span>
    <span class="kt">int</span> <span class="n">depth</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

    <span class="k">while</span> <span class="p">(</span><span class="o">!</span><span class="n">q</span><span class="p">.</span><span class="n">empty</span><span class="p">())</span> <span class="p">{</span>
        <span class="kt">int</span> <span class="n">sz</span> <span class="o">=</span> <span class="n">q</span><span class="p">.</span><span class="n">size</span><span class="p">();</span>
        <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">sz</span><span class="p">;</span> <span class="n">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
            <span class="n">TreeNode</span><span class="o">*</span> <span class="n">cur</span> <span class="o">=</span> <span class="n">q</span><span class="p">.</span><span class="n">front</span><span class="p">();</span>
            <span class="n">q</span><span class="p">.</span><span class="n">pop</span><span class="p">();</span>
            <span class="c1">// 访问 cur 节点，同时知道它所在的层数</span>
            <span class="n">cout</span> <span class="o">&lt;&lt;</span> <span class="s">"depth = "</span> <span class="o">&lt;&lt;</span> <span class="n">depth</span> <span class="o">&lt;&lt;</span> <span class="s">", val = "</span> <span class="o">&lt;&lt;</span> <span class="n">cur</span><span class="o">-&gt;</span><span class="n">val</span> <span class="o">&lt;&lt;</span> <span class="n">endl</span><span class="p">;</span>

            <span class="c1">// 把 cur 的左右子节点加入队列</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">cur</span><span class="o">-&gt;</span><span class="n">left</span> <span class="o">!=</span> <span class="nb">nullptr</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">q</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">cur</span><span class="o">-&gt;</span><span class="n">left</span><span class="p">);</span>
            <span class="p">}</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">cur</span><span class="o">-&gt;</span><span class="n">right</span> <span class="o">!=</span> <span class="nb">nullptr</span><span class="p">)</span> <span class="p">{</span>
                <span class="n">q</span><span class="p">.</span><span class="n">push</span><span class="p">(</span><span class="n">cur</span><span class="o">-&gt;</span><span class="n">right</span><span class="p">);</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="n">depth</span><span class="o">++</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="Leetcode"/><category term="Treenode"/><category term="dfs"/><category term="bfs"/><summary type="html"><![CDATA[二叉树笔记总结]]></summary></entry><entry><title type="html">RAG 技术体系化分类——从 Pipeline 阶段到失败模式</title><link href="https://leesanyee.me/blog/2026/RAG-techniques-pipeline-categorization/" rel="alternate" type="text/html" title="RAG 技术体系化分类——从 Pipeline 阶段到失败模式"/><published>2026-08-02T09:30:00+00:00</published><updated>2026-08-02T09:30:00+00:00</updated><id>https://leesanyee.me/blog/2026/RAG-techniques-pipeline-categorization</id><content type="html" xml:base="https://leesanyee.me/blog/2026/RAG-techniques-pipeline-categorization/"><![CDATA[<h1 id="rag-技术体系化分类从-pipeline-阶段到失败模式">RAG 技术体系化分类——从 Pipeline 阶段到失败模式</h1> <h2 id="一核心观点">一、核心观点</h2> <p>所有 RAG 技术本质上都在优化同一个目标：</p> <blockquote> <p><strong>在成本、延迟、质量、可控性之间取得平衡，让 LLM 拿到最合适、最相关、最可控的上下文。</strong></p> </blockquote> <p>传统的线性流水线可以抽象为：</p> <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Query → Query处理 → Embedding → 检索 → 重排/压缩 → 上下文组装 → LLM生成
</code></pre></div></div> <p>不同技术是在这个漏斗的不同位置做优化，但很多先进技术已经<strong>跨层</strong>甚至形成<strong>反馈循环</strong>。</p> <hr/> <h2 id="二按-pipeline-阶段分类">二、按 Pipeline 阶段分类</h2> <h3 id="1-query-层查询理解与改写">1. Query 层：查询理解与改写</h3> <p>这些技术不改知识库，只改用户 query 或查询策略：</p> <table> <thead> <tr> <th>技术</th> <th>核心做法</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">query_transformations</code></td> <td>query 重写、扩展、消歧</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">HyDe</code></td> <td>让 LLM 生成假设答案/文档，再用它去检索</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">HyPE</code></td> <td>生成假设 prompt 增强 query 表示</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">memorag</code></td> <td>利用历史记忆改写/增强 query</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">adaptive_retrieval</code></td> <td>对 query 分类后选择不同检索策略</td> </tr> </tbody> </table> <p><strong>Insight</strong>：这一层优化的不是”检索本身”，而是让 query 更接近索引内容的语义分布。</p> <hr/> <h3 id="2-encoding--表示层文档如何被嵌入">2. Encoding / 表示层：文档如何被嵌入</h3> <p>这些技术改变”文档以什么形式进入向量空间”：</p> <table> <thead> <tr> <th>技术</th> <th>核心做法</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">semantic_chunking</code></td> <td>按语义边界切分</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">proposition_chunking</code></td> <td>用 LLM 拆成原子化命题再嵌入</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">multi_model_rag_with_captioning</code></td> <td>图片/表格用 VLM 生成文字摘要后统一嵌入</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">multi_model_rag_with_colpali</code></td> <td>直接用 ColPali 对页面图像做视觉-语言嵌入</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">document_augmentation</code></td> <td>给文档片段生成问题，把问题也嵌入</td> </tr> </tbody> </table> <p><strong>Insight</strong>：</p> <ul> <li><code class="language-plaintext highlighter-rouge">proposition_chunking</code> 做语义粒度精细化，代价是索引膨胀。</li> <li><code class="language-plaintext highlighter-rouge">document_augmentation</code> 做查询-文档对齐。</li> <li>ColPali 是端到端视觉检索，captioning 是间接文本检索。</li> </ul> <hr/> <h3 id="3-indexing--索引结构层">3. Indexing / 索引结构层</h3> <p>这些技术改变知识库的组织方式：</p> <table> <thead> <tr> <th>技术</th> <th>核心做法</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">graph_rag</code> / <code class="language-plaintext highlighter-rouge">graphrag_with_milvus</code> / <code class="language-plaintext highlighter-rouge">graph_rag_local_attribution</code> / <code class="language-plaintext highlighter-rouge">Microsoft_GraphRag</code></td> <td>抽取实体关系，构建知识图谱</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">hierarchical_indices</code></td> <td>构建层级索引</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">raptor</code></td> <td>递归聚类+摘要，构建树状多层次索引</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">contextual_chunk_headers</code></td> <td>给 chunk 加文档上下文头再嵌入</td> </tr> </tbody> </table> <p><strong>Insight</strong>：GraphRAG 和 RAPTOR 都在解决”全局综合类问题”，前者用图+社区摘要，后者用树状语义聚类。</p> <hr/> <h3 id="4-retrieval--检索策略层">4. Retrieval / 检索策略层</h3> <p>这些技术改变”怎么从索引里把内容捞出来”：</p> <table> <thead> <tr> <th>技术</th> <th>核心做法</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">fusion_retrieval</code></td> <td>稠密向量 + 稀疏关键词融合</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">dartboard</code></td> <td>相关性 + 多样性选择</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">adaptive_retrieval</code></td> <td>根据 query 类型动态选择策略</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">retrieval_with_feedback_loop</code></td> <td>根据用户反馈调整文档相关分</td> </tr> </tbody> </table> <p><strong>Insight</strong>：<code class="language-plaintext highlighter-rouge">dartboard</code> 解决结果冗余，<code class="language-plaintext highlighter-rouge">fusion_retrieval</code> 解决向量检索对关键词不敏感的问题。</p> <hr/> <h3 id="5-post-retrieval--上下文组装层">5. Post-Retrieval / 上下文组装层</h3> <p>这些技术发生在”捞出候选文档后，送给 LLM 前”：</p> <table> <thead> <tr> <th>技术</th> <th>核心做法</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">reranking</code></td> <td>用 cross-encoder 重排</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">contextual_compression</code></td> <td>压缩/提取最相关部分</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">context_enrichment_window_around_chunk</code></td> <td>给 chunk 加前后窗口</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">relevant_segment_extraction</code></td> <td>把离散 chunk 重组成连续 segment</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">explainable_retrieval</code></td> <td>给检索结果生成相关性解释</td> </tr> </tbody> </table> <p><strong>Insight</strong>：<code class="language-plaintext highlighter-rouge">context_enrichment_window</code> 是”小块+周边”，<code class="language-plaintext highlighter-rouge">relevant_segment_extraction</code> 是”基于相关性拼成大块”，方向不同。</p> <hr/> <h3 id="6-generation--llm-交互层">6. Generation / LLM 交互层</h3> <p>这些技术重点在 LLM 如何消费检索结果：</p> <table> <thead> <tr> <th>技术</th> <th>核心做法</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">simple_rag</code> / <code class="language-plaintext highlighter-rouge">simple_rag_with_llamaindex</code></td> <td>Baseline</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">simple_csv_rag</code> / <code class="language-plaintext highlighter-rouge">json_rag</code></td> <td>结构化数据 RAG</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">local_rag_huggingface_faiss</code></td> <td>本地模型 + FAISS</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">reliable_rag</code></td> <td>相关性过滤 + 幻觉检测 + 来源高亮</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">self_rag</code></td> <td>动态决定检索、评估检索相关性、评估生成支撑度</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">crag</code></td> <td>评估检索质量，动态选择本地知识/网络搜索/两者结合</td> </tr> </tbody> </table> <p><strong>Insight</strong>：<code class="language-plaintext highlighter-rouge">self_rag</code> 和 <code class="language-plaintext highlighter-rouge">crag</code> 是”检索-生成闭环”，LLM 不再只是最后一步。</p> <hr/> <h3 id="7-系统级--agentic--反馈层">7. 系统级 / Agentic / 反馈层</h3> <p>这些技术跨多个阶段，是整体架构设计：</p> <table> <thead> <tr> <th>技术</th> <th>核心做法</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">Agentic_RAG</code></td> <td>query reformulation + parser + reranker + GLM + LMUnit</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">adaptive_retrieval</code></td> <td>query 分类 + 策略路由</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">retrieval_with_feedback_loop</code></td> <td>用户反馈 → relevance 调整 → 索引微调</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">self_rag</code> / <code class="language-plaintext highlighter-rouge">crag</code></td> <td>自评估、自纠错</td> </tr> </tbody> </table> <hr/> <h2 id="三按失败模式选技术">三、按失败模式选技术</h2> <p>比阶段分类更实用的是”失败模式”视角：</p> <table> <thead> <tr> <th>失败模式</th> <th>典型症状</th> <th>对应技术</th> </tr> </thead> <tbody> <tr> <td>Query 表达不充分</td> <td>问题太短/歧义</td> <td>query_transformations, HyDe, HyPE, memorag</td> </tr> <tr> <td>文档切分丢失语义</td> <td>答案跨 chunk</td> <td>semantic_chunking, proposition_chunking, contextual_chunk_headers</td> </tr> <tr> <td>向量检索漏匹配</td> <td>关键词检索差</td> <td>fusion_retrieval, document_augmentation</td> </tr> <tr> <td>检索结果冗余</td> <td>top-k 重复</td> <td>dartboard</td> </tr> <tr> <td>缺少全局/关系推理</td> <td>需要跨文档综合</td> <td>graph_rag, raptor, hierarchical_indices</td> </tr> <tr> <td>上下文过长/噪声多</td> <td>LLM 被干扰</td> <td>contextual_compression, relevant_segment_extraction</td> </tr> <tr> <td>检索质量不可信</td> <td>检索到无关文档</td> <td>reranking, reliable_rag, self_rag, crag</td> </tr> <tr> <td>多模态内容无法检索</td> <td>PDF 含图/表</td> <td>multi_model_rag_with_captioning, multi_model_rag_with_colpali</td> </tr> <tr> <td>系统不能持续改进</td> <td>同样错误反复出现</td> <td>retrieval_with_feedback_loop</td> </tr> <tr> <td>生成 hallucination</td> <td>答案脱离原文</td> <td>reliable_rag, self_rag, crag</td> </tr> </tbody> </table> <hr/> <h2 id="四最重要的三个洞察">四、最重要的三个洞察</h2> <ol> <li> <p><strong>跨层技术是常态</strong> 真正有效的方案往往跨多个阶段。例如 RAPTOR 同时改了 indexing（树）和 retrieval（分层），Self-RAG 同时改了 retrieval 和 generation。</p> </li> <li> <p><strong>代价转移而非消除</strong> GraphRAG 把成本从在线移到离线，HyDe 把成本从检索后移到检索前，document_augmentation 用索引膨胀换检索精度。RAG 优化本质上是<strong>成本在时间轴和空间轴上的重新分配</strong>。</p> </li> <li> <p><strong>优化对象不是”检索质量”，而是”对 LLM 生成的边际贡献”</strong> 检索到的 chunk 相关性高，不一定对最终答案最有帮助。有时需要多样性、反事实证据或结构化关系。</p> </li> </ol> <hr/>]]></content><author><name></name></author><category term="RAG"/><category term="rag"/><summary type="html"><![CDATA[RAG 技术体系化分类——从 Pipeline 阶段到失败模式]]></summary></entry><entry><title type="html">MemoRAG 记忆增强型 RAG 总结</title><link href="https://leesanyee.me/blog/2026/memorag-summary/" rel="alternate" type="text/html" title="MemoRAG 记忆增强型 RAG 总结"/><published>2026-08-02T09:00:00+00:00</published><updated>2026-08-02T09:00:00+00:00</updated><id>https://leesanyee.me/blog/2026/memorag-summary</id><content type="html" xml:base="https://leesanyee.me/blog/2026/memorag-summary/"><![CDATA[<h2 id="1-一句话理解">1. 一句话理解</h2> <p>MemoRAG 在标准 RAG 的检索阶段之前，增加了一个<strong>记忆模型</strong>。这个模型先对文档库做一次压缩记忆，查询时基于记忆生成更精确的检索线索，从而提高模糊、隐式、长上下文查询的检索质量。</p> <h2 id="2-解决什么痛点">2. 解决什么痛点？</h2> <p>标准 RAG 直接对 query 做向量检索，遇到以下情况效果不好：</p> <ul> <li>query 有歧义或指代（如”那件事后来怎么样了？”）</li> <li>query 需要结合文档背景才能理解</li> <li>长文档中信息分散，需要全局上下文</li> <li>用户问题与文档答案在字面上不匹配</li> </ul> <p>MemoRAG 通过记忆模型让检索器”提前知道文档里有什么”。</p> <h2 id="3-与-query-transformation-的关系">3. 与 Query Transformation 的关系</h2> <table> <thead> <tr> <th> </th> <th>Query Transformation</th> <th>MemoRAG</th> </tr> </thead> <tbody> <tr> <td>输入</td> <td>只有 query</td> <td>query + 文档记忆</td> </tr> <tr> <td>输出</td> <td>一个改写后的 query</td> <td>多个检索线索/子问题</td> </tr> <tr> <td>是否依赖文档内容</td> <td>否</td> <td>是</td> </tr> <tr> <td>成本</td> <td>低</td> <td>较高（需先建记忆）</td> </tr> </tbody> </table> <p>可以把 MemoRAG 理解为”带全局上下文的 Query Rewriting”。</p> <h2 id="4-核心流程">4. 核心流程</h2> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>文档
    ↓
切分成 chunks
    ↓
Memory Model 抽取/压缩记忆
    ↓
记忆存入向量库
    ↓

用户 query
    ↓
在记忆中检索相关主题
    ↓
生成 text spans + surrogate queries
    ↓
用这些线索检索原始文档
    ↓
LLM 生成最终答案
</code></pre></div></div> <h2 id="5-本-notebook-中的简化实现">5. 本 notebook 中的简化实现</h2> <h3 id="51-记忆结构topic-details-pairs">5.1 记忆结构：topic-details pairs</h3> <p>用 LLM 从每个文档 chunk 中提取结构化键值对：</p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"pairs"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"topic"</span><span class="p">:</span><span class="w"> </span><span class="s2">"气候变暖对生物多样性的影响"</span><span class="p">,</span><span class="w"> </span><span class="nl">"details"</span><span class="p">:</span><span class="w"> </span><span class="s2">"气温上升导致许多物种因栖息地丧失而面临灭绝。"</span><span class="w"> </span><span class="p">},</span><span class="w">
    </span><span class="p">{</span><span class="w"> </span><span class="nl">"topic"</span><span class="p">:</span><span class="w"> </span><span class="s2">"海洋酸化"</span><span class="p">,</span><span class="w"> </span><span class="nl">"details"</span><span class="p">:</span><span class="w"> </span><span class="s2">"海洋吸收过量二氧化碳，导致酸化加剧。"</span><span class="w"> </span><span class="p">}</span><span class="w">
  </span><span class="p">]</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div> <h3 id="52-建记忆">5.2 建记忆</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">memorize</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">document</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="p">.</span><span class="n">chat</span><span class="p">.</span><span class="n">completions</span><span class="p">.</span><span class="nf">create</span><span class="p">(</span>
        <span class="n">model</span><span class="o">=</span><span class="sh">"</span><span class="s">qwen-turbo</span><span class="sh">"</span><span class="p">,</span>
        <span class="n">messages</span><span class="o">=</span><span class="p">[...],</span>
        <span class="n">response_format</span><span class="o">=</span><span class="p">{</span><span class="sh">"</span><span class="s">type</span><span class="sh">"</span><span class="p">:</span> <span class="sh">"</span><span class="s">json_object</span><span class="sh">"</span><span class="p">}</span>
    <span class="p">)</span>
    <span class="n">pairs</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_parse_into_pairs</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
    <span class="c1"># 存入 FAISS
</span></code></pre></div></div> <h3 id="53-查询时生成检索线索">5.3 查询时生成检索线索</h3> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">create_retrieval_queries</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">query</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="c1"># 1. 在 memory store 中检索相关 topic-details
</span>    <span class="n">results</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">store</span><span class="p">.</span><span class="nf">similarity_search_with_score</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">10</span><span class="p">)</span>

    <span class="c1"># 2. 生成 text spans（关键词/片段线索）
</span>    <span class="c1"># 3. 生成 surrogate queries（替代/子问题）
</span>
    <span class="k">return</span> <span class="n">text_spans</span> <span class="o">+</span> <span class="n">surrogate_queries</span> <span class="o">+</span> <span class="p">[</span><span class="n">query</span><span class="p">]</span>
</code></pre></div></div> <h2 id="6-memory-model-一般是什么">6. Memory Model 一般是什么？</h2> <table> <thead> <tr> <th>类型</th> <th>例子</th> <th>特点</th> </tr> </thead> <tbody> <tr> <td>长上下文 LLM</td> <td>Qwen2-7B、Mistral-7B</td> <td>直接读完整数据库，生成压缩记忆</td> </tr> <tr> <td>轻量级专用模型</td> <td>memorag-qwen2-7b-inst</td> <td>针对记忆任务微调</td> </tr> <tr> <td>结构化记忆库</td> <td>本 notebook 的 FAISS topic-details</td> <td>工程简化版，更易部署</td> </tr> <tr> <td>kv-cache 压缩</td> <td>原始论文方案</td> <td>把长文档压缩成 key-value 表示</td> </tr> </tbody> </table> <h2 id="7-成本分析">7. 成本分析</h2> <table> <thead> <tr> <th>技术</th> <th>索引阶段 LLM 调用</th> <th>查询阶段额外开销</th> </tr> </thead> <tbody> <tr> <td>标准 RAG</td> <td>0</td> <td>无</td> </tr> <tr> <td>MemoRAG</td> <td>每个 chunk 1 次</td> <td>记忆检索 + 生成线索</td> </tr> <tr> <td>GraphRAG</td> <td>每个 chunk 多次</td> <td>图展开/社区综合</td> </tr> </tbody> </table> <p>MemoRAG 比 GraphRAG 便宜，但比标准 RAG 贵。适合查询频繁、需要更好检索质量的场景。</p> <h2 id="8-优缺点">8. 优缺点</h2> <h3 id="优点">优点</h3> <ol> <li>提高模糊/隐式查询的检索效果；</li> <li>复用标准 RAG 检索链路；</li> <li>实现比 GraphRAG 简单；</li> <li>适合长文档和对话式 RAG。</li> </ol> <h3 id="缺点">缺点</h3> <ol> <li>索引阶段需要额外 LLM 调用；</li> <li>记忆质量依赖 LLM 抽取能力；</li> <li>topic 粒度需要调优；</li> <li>不是真正的跨文档推理，只是增强检索线索。</li> </ol> <h2 id="9-实践注意事项">9. 实践注意事项</h2> <ol> <li><strong>文档一定要切分后再 memorize</strong>，不要整篇传入；</li> <li><strong>OpenAI 兼容接口必须设置 <code class="language-plaintext highlighter-rouge">base_url</code></strong>，否则默认连 OpenAI 官方；</li> <li><strong>chunk size 建议 2000-4000 token</strong>，太小会导致 topic 太碎，太大会超时；</li> <li><strong>给 memory model 设置 timeout 和 retry</strong>，长文本生成可能较慢。</li> </ol> <h2 id="10-核心-insight">10. 核心 Insight</h2> <p>MemoRAG 的核心价值不是”改写问题”，而是”让检索拥有全局上下文”：</p> <ul> <li>普通 Query Rewriting：把用户的话翻得更清楚；</li> <li>MemoRAG：像读过整本书的助手，不仅翻译问题，还告诉你要查哪几个关键词。</li> </ul> <h2 id="11-相关技术">11. 相关技术</h2> <ul> <li>[[query-transformations-summary]]：不带记忆的 query 改写</li> <li>[[raptor-summary]]：用主题树组织文档</li> <li>[[microsoft-graphrag-summary]]：用知识图谱组织文档</li> <li>[[proposition-chunking-summary]]：把文档拆成原子化事实</li> </ul>]]></content><author><name></name></author><category term="RAG"/><category term="rag"/><category term="memorag"/><category term="memory"/><category term="query-rewriting"/><summary type="html"><![CDATA[1. 一句话理解]]></summary></entry><entry><title type="html">Microsoft GraphRAG 基于知识图谱的 RAG 总结</title><link href="https://leesanyee.me/blog/2026/microsoft-graphrag-summary/" rel="alternate" type="text/html" title="Microsoft GraphRAG 基于知识图谱的 RAG 总结"/><published>2026-08-01T13:00:00+00:00</published><updated>2026-08-01T13:00:00+00:00</updated><id>https://leesanyee.me/blog/2026/microsoft-graphrag-summary</id><content type="html" xml:base="https://leesanyee.me/blog/2026/microsoft-graphrag-summary/"><![CDATA[<h2 id="1-一句话理解">1. 一句话理解</h2> <p>Microsoft GraphRAG 是一种把文档先转换成<strong>知识图谱</strong>，再基于图谱中的实体、关系和社区进行检索与回答的 RAG 方案。它擅长处理需要跨文档连接信息、全局综合理解的复杂查询。</p> <h2 id="2-解决什么痛点">2. 解决什么痛点？</h2> <p>传统 RAG 把文档切成 chunk 后做向量检索，擅长回答”某段文字说了什么”，但不擅长：</p> <ul> <li><strong>连接分散信息</strong>：比如”Elon Musk 创立/收购了哪些公司？它们之间有什么关系？”</li> <li><strong>全局理解</strong>：比如”这篇文章的主题是什么？整体趋势如何？”</li> <li><strong>复杂综合推理</strong>：需要把多个来源的信息拼凑起来</li> </ul> <p>GraphRAG 通过知识图谱把实体和关系显式建模，解决这些问题。</p> <h2 id="3-索引阶段流程">3. 索引阶段流程</h2> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>原始文本
    ↓
Text Chunking（文本切分）
    ↓
Element Extraction（LLM 提取实体和关系）
    ↓
Graph Construction（构建知识图谱）
    ↓
Community Detection（发现社区/簇）
    ↓
Community Summarization（为每个社区生成摘要）
</code></pre></div></div> <h3 id="各步骤说明">各步骤说明</h3> <table> <thead> <tr> <th>步骤</th> <th>作用</th> </tr> </thead> <tbody> <tr> <td><strong>Chunking</strong></td> <td>把长文本切分成 manageable 的小块</td> </tr> <tr> <td><strong>Element Extraction</strong></td> <td>LLM 识别实体（如 Elon Musk、Tesla）和关系（如 founded、CEO of）</td> </tr> <tr> <td><strong>Graph Construction</strong></td> <td>实体作为节点，关系作为边，构建知识图谱</td> </tr> <tr> <td><strong>Community Detection</strong></td> <td>用 Leiden 等算法发现紧密相关的节点簇</td> </tr> <tr> <td><strong>Community Summarization</strong></td> <td>为每个社区生成摘要，供全局搜索使用</td> </tr> </tbody> </table> <h2 id="4-查询阶段两种搜索模式">4. 查询阶段：两种搜索模式</h2> <h3 id="41-local-search局部搜索">4.1 Local Search（局部搜索）</h3> <p>针对<strong>具体实体</strong>的查询，展开该实体的邻居节点和相关概念。</p> <p>示例：</p> <blockquote> <p>“What and how many companies and subsidiaries founded by Elon Musk?”</p> </blockquote> <h3 id="42-global-search全局搜索">4.2 Global Search（全局搜索）</h3> <p>针对<strong>整体语料</strong>的查询，综合多个社区摘要给出宏观回答。</p> <p>示例：</p> <blockquote> <p>“What are the major accomplishments of Elon Musk?”</p> </blockquote> <h2 id="5-代码实现要点">5. 代码实现要点</h2> <p>这个 notebook 使用 GraphRAG 官方 CLI 工具：</p> <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 初始化配置</span>
python <span class="nt">-m</span> graphrag.index <span class="nt">--init</span> <span class="nt">--root</span> data/graphrag

<span class="c"># 建索引</span>
python <span class="nt">-m</span> graphrag.index <span class="nt">--root</span> ./data/graphrag

<span class="c"># 查询</span>
python <span class="nt">-m</span> graphrag.query <span class="nt">--root</span> ./data/graphrag <span class="nt">--method</span> <span class="nb">local</span> <span class="s2">"query"</span>
</code></pre></div></div> <h3 id="配置">配置</h3> <ul> <li>LLM：<code class="language-plaintext highlighter-rouge">gpt-4o</code></li> <li>Embedding：<code class="language-plaintext highlighter-rouge">text-embedding-3-large</code></li> <li>支持 Azure OpenAI 或 OpenAI</li> </ul> <blockquote> <p>注意：按照 <code class="language-plaintext highlighter-rouge">.claude.md</code>，这个 notebook <strong>没有被迁移到阿里云百炼</strong>，因为它明确依赖 Azure OpenAI/OpenAI。</p> </blockquote> <h3 id="数据源">数据源</h3> <p>notebook 用 BeautifulSoup 抓取 Wikipedia 上 Elon Musk 的词条，保存为 <code class="language-plaintext highlighter-rouge">data/elon.md</code>，再喂给 GraphRAG 建索引。</p> <h2 id="6-与传统-rag-的对比">6. 与传统 RAG 的对比</h2> <table> <thead> <tr> <th> </th> <th>传统 RAG</th> <th>GraphRAG</th> </tr> </thead> <tbody> <tr> <td>检索单元</td> <td>文本 chunk</td> <td>实体、关系、社区</td> </tr> <tr> <td>擅长问题</td> <td>“某段文字说了什么”</td> <td>“这些实体有什么关系”</td> </tr> <tr> <td>索引成本</td> <td>低（embedding 一次）</td> <td>高（大量 LLM 调用）</td> </tr> <tr> <td>查询成本</td> <td>低</td> <td>中到高</td> </tr> <tr> <td>全局理解</td> <td>弱</td> <td>强</td> </tr> <tr> <td>可解释性</td> <td>低（黑盒相似度）</td> <td>较高（可追溯关系）</td> </tr> </tbody> </table> <h2 id="7-主要局限">7. 主要局限</h2> <ol> <li><strong>索引成本极高</strong>：每个 chunk 都要调 LLM 提取实体和关系；</li> <li><strong>对 LLM 质量敏感</strong>：实体关系提取错误会传播到整个图谱；</li> <li><strong>不适合简单事实查询</strong>：如”BLEU 是多少”用传统 RAG 更快更便宜；</li> <li><strong>图规模管理</strong>：文档量大时图谱可能很庞大，成本上升。</li> </ol> <h2 id="8-适用场景">8. 适用场景</h2> <ul> <li>人物关系网分析</li> <li>企业架构与股权关系</li> <li>医学文献中的疾病-药物-症状关系</li> <li>法律案例中的主体关系</li> <li>任何实体关系密集、需要跨文档综合的语料</li> </ul> <h2 id="9-核心-insight">9. 核心 Insight</h2> <p>GraphRAG 的本质是把”语义检索”升级成”关系检索”：</p> <ul> <li>向量 RAG 回答”哪段文字和我的问题语义最像”；</li> <li>GraphRAG 回答”哪些实体通过什么关系连接在一起”。</li> </ul> <h2 id="10-与-raptor-的对比">10. 与 RAPTOR 的对比</h2> <ul> <li><strong>RAPTOR</strong>：用树的层级抽象解决”宏观 vs 细节”的问题；</li> <li><strong>GraphRAG</strong>：用图的节点关系解决”连接 vs 综合”的问题。</li> </ul> <p>两者甚至可以结合：先用 GraphRAG 找到相关实体和社区，再在 RAPTOR 树中定位这些实体的详细描述。</p> <h2 id="11-相关技术">11. 相关技术</h2> <ul> <li>[[raptor-summary]]：用主题树组织文档</li> <li>[[proposition-chunking-summary]]：把文档拆成原子化事实</li> <li>[[query-transformations-summary]]：改写问题提高检索匹配度</li> <li>[[colpali-multimodal-rag-qa]]：多模态图像检索</li> </ul>]]></content><author><name></name></author><category term="RAG"/><category term="rag"/><category term="graphrag"/><category term="knowledge-graph"/><category term="microsoft"/><summary type="html"><![CDATA[1. 一句话理解]]></summary></entry><entry><title type="html">Multimodal RAG with Captioning 图像描述型多模态 RAG 总结</title><link href="https://leesanyee.me/blog/2026/multimodal-rag-with-captioning-summary/" rel="alternate" type="text/html" title="Multimodal RAG with Captioning 图像描述型多模态 RAG 总结"/><published>2026-07-31T13:00:00+00:00</published><updated>2026-07-31T13:00:00+00:00</updated><id>https://leesanyee.me/blog/2026/multimodal-rag-with-captioning-summary</id><content type="html" xml:base="https://leesanyee.me/blog/2026/multimodal-rag-with-captioning-summary/"><![CDATA[<h2 id="1-一句话理解">1. 一句话理解</h2> <p>Multimodal RAG with Captioning 不是直接检索图片，而是<strong>先用多模态模型把图片/表格生成文字描述（caption），再把 caption 和原始文本一起存入向量库做传统文本检索</strong>。</p> <h2 id="2-核心思想">2. 核心思想</h2> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PDF 页面
    ├──→ 提取文字 ───────────┐
    └──→ 提取图片 ─→ Gemini 生成 caption ─┘
                              ↓
                    统一变成文本块
                              ↓
                    Cohere 嵌入 → Chroma 向量库
                              ↓
                    文本检索 → Cohere LLM 生成答案
</code></pre></div></div> <p>所有非文本内容都被多模态模型”看懂”后”写成文字”，最终全部走传统文本 RAG 链路。</p> <h2 id="3-完整流程">3. 完整流程</h2> <h3 id="31-pdf-解析">3.1 PDF 解析</h3> <p>使用 PyMuPDF（fitz）同时提取：</p> <ul> <li>每页文字</li> <li>页内所有图片</li> </ul> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">with</span> <span class="n">fitz</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="sh">'</span><span class="s">attention_is_all_you_need.pdf</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">pdf_file</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">page_number</span> <span class="ow">in</span> <span class="nf">range</span><span class="p">(</span><span class="nf">len</span><span class="p">(</span><span class="n">pdf_file</span><span class="p">)):</span>
        <span class="n">page</span> <span class="o">=</span> <span class="n">pdf_file</span><span class="p">[</span><span class="n">page_number</span><span class="p">]</span>
        <span class="n">text</span> <span class="o">=</span> <span class="n">page</span><span class="p">.</span><span class="nf">get_text</span><span class="p">().</span><span class="nf">strip</span><span class="p">()</span>
        <span class="c1"># 提取图片并保存
</span></code></pre></div></div> <h3 id="32-图片-captioning">3.2 图片 Captioning</h3> <p>用 Gemini-1.5-flash 看图并生成适合检索的摘要：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">response</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="nf">generate_content</span><span class="p">([</span><span class="n">image</span><span class="p">,</span>
    <span class="sh">"</span><span class="s">You are an assistant tasked with summarizing tables, images and text for retrieval. ...</span><span class="sh">"</span><span class="p">])</span>
</code></pre></div></div> <p>例如一张 BLEU 分数表可能被描述为：</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Table showing BLEU scores for Transformer base and big models. The base model achieves 27.3 BLEU.
</code></pre></div></div> <h3 id="33-统一嵌入和检索">3.3 统一嵌入和检索</h3> <p>把文字和图片描述统一成 <code class="language-plaintext highlighter-rouge">Document</code>，一起存入 Chroma：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">vectorstore</span> <span class="o">=</span> <span class="n">Chroma</span><span class="p">.</span><span class="nf">from_documents</span><span class="p">(</span>
    <span class="n">documents</span><span class="o">=</span><span class="n">doc_splits</span> <span class="o">+</span> <span class="n">img_splits</span><span class="p">,</span>
    <span class="n">embedding</span><span class="o">=</span><span class="n">embedding_model</span><span class="p">,</span>
<span class="p">)</span>
</code></pre></div></div> <h3 id="34-问答">3.4 问答</h3> <p>检索回文字或 caption，交给 LLM 生成答案：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">docs</span> <span class="o">=</span> <span class="n">retriever</span><span class="p">.</span><span class="nf">invoke</span><span class="p">(</span><span class="n">query</span><span class="p">)</span>
<span class="n">generation</span> <span class="o">=</span> <span class="n">rag_chain</span><span class="p">.</span><span class="nf">invoke</span><span class="p">({</span><span class="sh">"</span><span class="s">documents</span><span class="sh">"</span><span class="p">:</span> <span class="n">docs</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="n">page_content</span><span class="p">,</span> <span class="sh">"</span><span class="s">question</span><span class="sh">"</span><span class="p">:</span> <span class="n">query</span><span class="p">})</span>
</code></pre></div></div> <h2 id="4-与-colpali-路线的对比">4. 与 ColPali 路线的对比</h2> <table> <thead> <tr> <th> </th> <th>ColPali 路线</th> <th>Captioning 路线</th> </tr> </thead> <tbody> <tr> <td>图片处理方式</td> <td>直接当图像检索</td> <td>先生成文字描述</td> </tr> <tr> <td>检索方式</td> <td>视觉特征 + late interaction</td> <td>文本 embedding</td> </tr> <tr> <td>索引内容</td> <td>base64 图片 + 视觉特征</td> <td>文字 + 图片描述</td> </tr> <tr> <td>检索粒度</td> <td>整页</td> <td>文本 chunk / 单个 caption</td> </tr> <tr> <td>生成阶段输入</td> <td>原始图片</td> <td>检索到的文字/caption</td> </tr> <tr> <td>优点</td> <td>保留完整视觉信息</td> <td>复用成熟文本 RAG 链路</td> </tr> <tr> <td>缺点</td> <td>索引大、需要多模态 LLM 读图</td> <td>caption 质量决定检索上限</td> </tr> </tbody> </table> <h2 id="5-优缺点">5. 优缺点</h2> <h3 id="优点">优点</h3> <ol> <li><strong>复用文本 RAG 基础设施</strong>：不需要特殊多模态向量库；</li> <li><strong>检索粒度更细</strong>：可以精确到某段文字或某张图片；</li> <li><strong>索引体积小</strong>：不存原始图片；</li> <li><strong>生成阶段更简单</strong>：给 LLM 的是文字，无需多模态模型。</li> </ol> <h3 id="缺点">缺点</h3> <ol> <li><strong>Caption 质量是瓶颈</strong>：描述错误会传递到检索和生成；</li> <li><strong>丢失视觉细节</strong>：复杂图表、布局、颜色信息会被简化；</li> <li><strong>无法处理纯视觉问题</strong>：如”图中红色曲线代表什么”；</li> <li><strong>额外 LLM 调用成本</strong>：每张图片都要生成 caption。</li> </ol> <h2 id="6-选型建议">6. 选型建议</h2> <table> <thead> <tr> <th>场景</th> <th>推荐方案</th> </tr> </thead> <tbody> <tr> <td>需要复用现有文本 RAG 系统</td> <td>Captioning</td> </tr> <tr> <td>图片内容可被文字较好描述</td> <td>Captioning</td> </tr> <tr> <td>需要精确到图表中的某个数值</td> <td>两者皆可</td> </tr> <tr> <td>需要保留布局、颜色、视觉关系</td> <td>ColPali</td> </tr> <tr> <td>扫描件/手写/复杂排版为主</td> <td>ColPali</td> </tr> <tr> <td>追求实现简单、成本低</td> <td>Captioning</td> </tr> </tbody> </table> <h2 id="7-实践注意事项">7. 实践注意事项</h2> <ol> <li><strong>Caption 和原始文本混合检索时</strong>，建议给 caption 加前缀标记，如 <code class="language-plaintext highlighter-rouge">IMAGE_CAPTION: ...</code>，便于区分来源；</li> <li><strong>不要只取 top-1</strong>：生产环境建议 <code class="language-plaintext highlighter-rouge">k=3~5</code>；</li> <li><strong>Caption 提示词很关键</strong>：要指导模型生成”适合检索”的描述，而不是泛泛而谈。</li> </ol> <h2 id="8-核心-insight">8. 核心 Insight</h2> <p>Captioning 路线的本质是用 LLM 做”有损的多模态→文本转换”：</p> <ul> <li>优势是把多模态问题降维成文本问题，复用成熟技术栈；</li> <li>代价是图片信息经过 LLM 压缩后，会丢失原始视觉细节；</li> <li>它不是”真正理解图片”，而是”让图片能被文本检索理解”。</li> </ul> <h2 id="9-相关技术">9. 相关技术</h2> <ul> <li>[[colpali-multimodal-rag-qa]]：直接基于视觉特征检索页面图像</li> <li>[[proposition-chunking-summary]]：把文档拆成原子化事实</li> <li>[[query-transformations-summary]]：改写问题提高检索匹配度</li> </ul>]]></content><author><name></name></author><category term="RAG"/><category term="rag"/><category term="multimodal"/><category term="captioning"/><category term="vision"/><summary type="html"><![CDATA[1. 一句话理解]]></summary></entry><entry><title type="html">掌握 ColPali 多模态 RAG 需要回答的 8 个问题</title><link href="https://leesanyee.me/blog/2026/colpali-multimodal-rag-qa/" rel="alternate" type="text/html" title="掌握 ColPali 多模态 RAG 需要回答的 8 个问题"/><published>2026-07-30T14:30:00+00:00</published><updated>2026-07-30T14:30:00+00:00</updated><id>https://leesanyee.me/blog/2026/colpali-multimodal-rag-qa</id><content type="html" xml:base="https://leesanyee.me/blog/2026/colpali-multimodal-rag-qa/"><![CDATA[<h2 id="一句话总结">一句话总结</h2> <p>ColPali 不是传统 RAG 的替代品，而是处理<strong>视觉密集型文档</strong>的专用工具。它把 PDF 页面当作图像直接检索，再由多模态 LLM 读懂返回的页面图像。</p> <h2 id="1-colpali-是为了解决什么痛点">1. ColPali 是为了解决什么痛点？</h2> <p>传统文本 RAG 处理 PDF 时通常先 OCR 提取文字，但会丢失或破坏：</p> <ul> <li>页面布局和视觉结构</li> <li>表格、图表、公式的原始形式</li> <li>手写内容、扫描件、复杂排版</li> </ul> <p>ColPali 让检索器直接”看”PDF 页面图像，绕过 OCR，保留完整的视觉信息。</p> <h2 id="2-colpali-和普通的-text-embedding-模型有什么区别">2. ColPali 和普通的 text embedding 模型有什么区别？</h2> <table> <thead> <tr> <th> </th> <th>text embedding</th> <th>ColPali</th> </tr> </thead> <tbody> <tr> <td>输入</td> <td>文本</td> <td>页面图像 + 文本 query</td> </tr> <tr> <td>输出</td> <td>单个向量</td> <td>多组 token-patch 相似度</td> </tr> <tr> <td>匹配方式</td> <td>余弦相似度</td> <td>late interaction（后期交互）</td> </tr> <tr> <td>粒度</td> <td>整个文档/段落</td> <td>页面级别</td> </tr> <tr> <td>优势</td> <td>轻量、成熟</td> <td>保留视觉布局、不依赖 OCR</td> </tr> </tbody> </table> <p>普通 embedding 把 query 和文档各压缩成一个向量；ColPali 保留 query token 和图像 patch 之间的细粒度对应关系。</p> <h2 id="3-什么是-late-interaction后期交互">3. 什么是 Late Interaction（后期交互）？</h2> <p>Late Interaction 是 ColBERT 提出的思想，也被 ColPali 继承：</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>普通双塔模型：
query → [一个向量]    doc → [一个向量]    → 算一次相似度

Late Interaction：
query token1 ─┐
query token2 ─┼→ 分别和 doc 的每个 patch 算相似度
query token3 ─┘
                → 聚合所有 token-patch 相似度得到最终分数
</code></pre></div></div> <p>好处：能定位到文档中具体哪个区域和 query 相关，比如”左下角的表格”。</p> <h2 id="4-代码里为什么要把-pdf-页面存成-base64">4. 代码里为什么要把 PDF 页面存成 base64？</h2> <p>因为 ColPali 检索返回的是<strong>最相关页面的索引/id</strong>，而不是页面内容。要让人或多模态 LLM 看到这一页，必须从索引里把原始图像还原出来。</p> <p>base64 就是原始页面图像的编码形式。入库时 <code class="language-plaintext highlighter-rouge">store_collection_with_index=True</code> 会把每页图片以 base64 存进索引；检索时用 <code class="language-plaintext highlighter-rouge">base64.b64decode()</code> 解码回图片。</p> <h2 id="5-colpali-检索回来的是什么能直接当答案吗">5. ColPali 检索回来的是什么？能直接当答案吗？</h2> <p>检索回来的是<strong>页面图像</strong>（以 base64 编码），不能直接当答案。</p> <p>需要再经过一步：把解码后的图片传给多模态 LLM（如 Gemini、Qwen-VL），让 LLM 看图并回答问题。</p> <p>完整链路：</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ColPali 负责"找哪一页" → 多模态 LLM 负责"读懂这一页"
</code></pre></div></div> <h2 id="6-colpali-最适合什么场景">6. ColPali 最适合什么场景？</h2> <p>适合以下文档类型：</p> <ul> <li>学术论文（包含公式、图表、架构图）</li> <li>财报/年报（大量表格和可视化）</li> <li>产品说明书/手册（图文混排）</li> <li>扫描件/手写笔记</li> <li>任何 OCR 效果差或版式复杂的文档</li> </ul> <p>如果文档是纯文字且版式简单，传统文本 RAG 更便宜、更成熟。</p> <h2 id="7-colpali-的主要局限是什么">7. ColPali 的主要局限是什么？</h2> <ol> <li><strong>检索粒度是页</strong>：无法精确到某一段文字；</li> <li><strong>索引体积大</strong>：要存图片 base64 和视觉特征；</li> <li><strong>依赖多模态 LLM</strong>：检索后必须再调一次大模型读图；</li> <li><strong>不支持跨页推理</strong>：答案分散在多页时需要额外处理；</li> <li><strong>计算成本高</strong>：视觉编码比文本编码慢且贵。</li> </ol> <h2 id="8-整个-pipeline-里各组件分别负责什么">8. 整个 pipeline 里各组件分别负责什么？</h2> <table> <thead> <tr> <th>组件</th> <th>职责</th> </tr> </thead> <tbody> <tr> <td><code class="language-plaintext highlighter-rouge">RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2")</code></td> <td>加载 ColPali 检索模型</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">RAG.index(...)</code></td> <td>把 PDF 每页建索引，同时存 base64 图片</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">RAG.search(query, k=1)</code></td> <td>用文本 query 检索最相关的页面</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">base64.b64decode(...)</code></td> <td>把检索结果还原成图片字节</td> </tr> <tr> <td><code class="language-plaintext highlighter-rouge">Gemini / Qwen-VL</code></td> <td>读取图片并生成最终答案</td> </tr> </tbody> </table> <h2 id="核心-insight">核心 Insight</h2> <p>掌握 ColPali 的标志是：能清晰地区分三个层次：</p> <ol> <li><strong>检索层</strong>（ColPali）：用视觉特征找页面；</li> <li><strong>存储层</strong>（base64 + 向量索引）：保存页面图像和特征；</li> <li><strong>生成层</strong>（多模态 LLM）：读懂返回的图片并回答问题。</li> </ol> <p>ColPali 只负责<strong>找图</strong>，看懂图的是后面的多模态 LLM。</p> <h2 id="相关技术">相关技术</h2> <ul> <li>[[raptor-summary]]：用主题树组织文档</li> <li>[[proposition-chunking-summary]]：把文档拆成原子化事实</li> <li>[[query-transformations-summary]]：改写问题提高检索匹配度</li> </ul>]]></content><author><name></name></author><category term="RAG"/><category term="rag"/><category term="colpali"/><category term="multimodal"/><category term="retrieval"/><summary type="html"><![CDATA[一句话总结]]></summary></entry><entry><title type="html">Proposition Chunking 命题化切分总结</title><link href="https://leesanyee.me/blog/2026/proposition-chunking-summary/" rel="alternate" type="text/html" title="Proposition Chunking 命题化切分总结"/><published>2026-07-30T13:00:00+00:00</published><updated>2026-07-30T13:00:00+00:00</updated><id>https://leesanyee.me/blog/2026/proposition-chunking-summary</id><content type="html" xml:base="https://leesanyee.me/blog/2026/proposition-chunking-summary/"><![CDATA[<h2 id="1-一句话理解">1. 一句话理解</h2> <p>Proposition Chunking 不是简单地改写句子，而是<strong>把文档拆解成原子化、自包含的事实单元</strong>，让每个检索单元只表达一个明确事实，从而提高被相关 query 命中的概率。</p> <h2 id="2-核心动机">2. 核心动机</h2> <p>传统 chunking 关注的是”切多大”，Proposition Chunking 关注的是”切多细”。</p> <p>向量检索的效果很大程度上取决于检索单元的语义纯度：</p> <ul> <li>长句/长段落包含多个事实，向量会被稀释；</li> <li>代词和指代让单独句子语义不完整；</li> <li>过渡词、修饰语占用 embedding 空间但不贡献事实。</li> </ul> <p>Proposition Chunking 用 LLM 把文本变成一条条独立事实，解决这些问题。</p> <h2 id="3-完整流程">3. 完整流程</h2> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>原始文档
    ↓
RecursiveCharacterTextSplitter 切 chunk（控制 LLM 输入长度）
    ↓
LLM 把每个 chunk 拆成多个 propositions
    ↓
LLM 对每个 proposition 从 4 个维度打分
    ↓
低于阈值的命题丢弃
    ↓
命题嵌入向量库（FAISS）
    ↓
检索并对比：命题检索 vs 原始 chunk 检索
</code></pre></div></div> <h2 id="4-什么是好的-proposition">4. 什么是好的 Proposition？</h2> <p>好的命题满足：</p> <ol> <li><strong>表达单一事实</strong>：一个命题一个 claim；</li> <li><strong>自包含</strong>：无需上下文即可理解；</li> <li><strong>用全称不用代词</strong>：避免 He/It/This 等模糊指代；</li> <li><strong>包含必要细节</strong>：时间、地点、限定词；</li> <li><strong>一个主谓关系</strong>：不含复杂从句或连词。</li> </ol> <p>示例：</p> <div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>原文：
Brian Chesky, co-founder of Airbnb, shared his experience of being advised to run the company in a traditional managerial style, which led to poor outcomes. He eventually found success by adopting a different approach, influenced by how Steve Jobs managed Apple.

命题：
- Brian Chesky is a co-founder of Airbnb.
- Brian Chesky was advised to run Airbnb in a traditional managerial style.
- Running Airbnb in a traditional managerial style led to poor outcomes.
- Brian Chesky adopted a different approach to running Airbnb.
- Steve Jobs' management style at Apple influenced Brian Chesky's approach.
</code></pre></div></div> <h2 id="5-质量检查">5. 质量检查</h2> <p>每个命题从四个维度 1–10 打分：</p> <ul> <li><strong>Accuracy</strong>：是否忠实反映原文；</li> <li><strong>Clarity</strong>：是否无需上下文即可理解；</li> <li><strong>Completeness</strong>：是否包含必要细节；</li> <li><strong>Conciseness</strong>：是否简洁不冗余。</li> </ul> <p>低于阈值（如 7 分）的命题会被丢弃。</p> <h2 id="6-为什么第一步不直接用句子切分">6. 为什么第一步不直接用句子切分？</h2> <p>用 <code class="language-plaintext highlighter-rouge">。</code> 切句子确实语义边界清晰，但直接作为 LLM 输入有问题：</p> <table> <thead> <tr> <th>问题</th> <th>说明</th> </tr> </thead> <tbody> <tr> <td>长句超窗</td> <td>法律/学术文本中一个句子可能几百 token</td> </tr> <tr> <td>短句浪费</td> <td>大量短句导致 LLM 调用次数爆炸</td> </tr> <tr> <td>上下文丢失</td> <td>相邻句子的指代关系被切断</td> </tr> </tbody> </table> <p><code class="language-plaintext highlighter-rouge">RecursiveCharacterTextSplitter</code> 默认会优先按段落、行、空格切分，只在必要时才切断句子，是在<strong>语义连贯性和 token 预算之间取折中</strong>。</p> <p>如果想让句子优先不被切断，可以自定义分隔符：</p> <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">text_splitter</span> <span class="o">=</span> <span class="n">RecursiveCharacterTextSplitter</span><span class="p">.</span><span class="nf">from_tiktoken_encoder</span><span class="p">(</span>
    <span class="n">chunk_size</span><span class="o">=</span><span class="mi">200</span><span class="p">,</span>
    <span class="n">chunk_overlap</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span>
    <span class="n">separators</span><span class="o">=</span><span class="p">[</span><span class="sh">"</span><span class="se">\n\n</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">。</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">！</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">？</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="se">\n</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s"> </span><span class="sh">"</span><span class="p">,</span> <span class="sh">""</span><span class="p">]</span>
<span class="p">)</span>
</code></pre></div></div> <h2 id="7-与原始-chunk-检索的对比">7. 与原始 Chunk 检索的对比</h2> <table> <thead> <tr> <th>维度</th> <th>命题检索</th> <th>原始 chunk 检索</th> </tr> </thead> <tbody> <tr> <td>精确性</td> <td>高</td> <td>中</td> </tr> <tr> <td>简洁性</td> <td>高</td> <td>中</td> </tr> <tr> <td>上下文丰富度</td> <td>低</td> <td>高</td> </tr> <tr> <td>全面性</td> <td>低</td> <td>高</td> </tr> <tr> <td>叙事连贯性</td> <td>中（可能碎片化）</td> <td>高</td> </tr> <tr> <td>信息过载</td> <td>低</td> <td>高</td> </tr> <tr> <td>适用场景</td> <td>快速事实查询</td> <td>复杂理解型查询</td> </tr> </tbody> </table> <h2 id="8-成本与收益的权衡">8. 成本与收益的权衡</h2> <p>Proposition Chunking 的代价：</p> <ul> <li>每个 chunk 都要调用 LLM 生成命题；</li> <li>每个命题都要调用 LLM 打分；</li> <li>索引构建成本高。</li> </ul> <p>收益：</p> <ul> <li>在线检索更精确；</li> <li>检索单元直接对应事实；</li> <li>特别适合问答型 RAG。</li> </ul> <h2 id="9-与-query-rewriting-的关系">9. 与 Query Rewriting 的关系</h2> <ul> <li><strong>Query Rewriting</strong>：改的是<strong>问题</strong>，让问题更容易匹配文档；</li> <li><strong>Proposition Chunking</strong>：改的是<strong>文档</strong>，让文档更容易匹配问题。</li> </ul> <p>两者方向相反，目标一致。理想情况下可以组合使用。</p> <h2 id="10-一句话总结">10. 一句话总结</h2> <blockquote> <p>Proposition Chunking = 把文档拆成原子化事实，让检索从”匹配文本块”升级为”匹配事实”。</p> </blockquote> <h2 id="11-相关技术">11. 相关技术</h2> <ul> <li>[[query-transformations-summary]]：改问题来提高检索匹配度</li> <li>[[raptor-summary]]：用主题树组织文档</li> <li>[[contextual-compression-summary]]：检索后再压缩噪声</li> </ul>]]></content><author><name></name></author><category term="RAG"/><category term="rag"/><category term="chunking"/><category term="proposition"/><category term="retrieval"/><summary type="html"><![CDATA[1. 一句话理解]]></summary></entry></feed>