在法律文件的 RAG(Retrieval-Augmented Generation)系統中,如何建立 index(索引) 是決定檢索效果與最終生成品質的核心之一。由於法律文件通常篇幅長、用詞嚴謹、結構清晰,建立 index 時需特別考慮語義的準確性、上下文的完整性與檢索的效率。
下面是一些 法律文件中最佳的 Indexing 策略與建議:
📌 一、分段(Chunking)策略
✅ 最佳選擇:基於結構的分段
法律文件通常有明確的結構(如條款、節、章),例如:
第1章 總則
第2條 合同成立
第3條 當事人義務
做法:
利用正則表達式提取這些結構(如:
第[一二三四五\d]+[章條節])每一段 chunk 為一個 index entry,保留其原始標題與內容
加入元資料:條文號、文件名稱、日期等
優點:
語義完整、無截斷
容易追蹤來源與可解釋性高
❌ 不建議:固定 token 數 chunking(例如每 500 tokens)
容易造成語義截斷(尤其法律邏輯複雜,條文之間有依賴)
難以回溯來源條文,降低可信度
📌 二、嵌入(Embedding)模型選擇
✅ 推薦模型:
法律專用嵌入模型(如:
intfloat/e5-base-v2,sentence-transformers/all-mpnet-base-v2)中文法律場景: 可用 Chinese Legal-BERT 或訓練嵌入模型(如與
Lawformer配合)
✅ 嵌入內容建議:
條文 + 標題 + 上下文摘要
加入欄位如【文件類型】【頒布機構】【生效日期】等 metadata,提高檢索的針對性
📌 三、向量索引工具選擇
常見向量資料庫:
FAISS(快速、本地部署)
Weaviate(支援 metadata filter)
Qdrant / Milvus(高性能、大規模)
對於法律文件的實務應用,建議用:
Hierarchical Index + Metadata filter
- 比如:先根據【文件名稱】【條文章節】篩選 → 再向量檢索 → 再 rerank
 
📌 四、Retrieval 結合 Ranker
單靠 semantic embedding 常常不足以排出最相關的條文。
最佳實踐:
Hybrid retrieval:
結合 dense vector(語義)+ BM25(字面匹配)雙引擎
例如:先用 BM25 縮小範圍,再用向量 rerank
加 reranker(如 BGE-Reranker)進行語義排序
- 可顯著提升準確率與生成質量
 
📌 五、加入 Prompt-aware chunking(可選進階)
如果目標問題偏好提取具體答案(如“某條法律是否規定了 xxx”),可以考慮:
將原始 chunk 與“可能問題”生成 QA-pair 加入 index
增加檢索的問題-答案對齊能力(即 Dense Passage Retrieval)
✅ 總結:法律文件 RAG Index 建議方案
| 項目 | 建議 | 
|---|---|
| Chunk 方法 | 基於條文、章節分段(結構化 chunk) | 
| Embedding | 法律領域專用嵌入模型(中英文區分) | 
| Metadata | 條號、標題、法律名稱、生效日期、機構 | 
| Vector DB | FAISS / Qdrant / Weaviate + metadata filter | 
| 檢索策略 | Hybrid(向量 + BM25)+ reranker | 
| 特殊需求 | 可加入 QA pair 或摘要索引作輔助 | 
如你有具體的法律文件樣本(例如某國的民法、合同樣板),我也可以協助你設計一個具體的 chunking/embedding/index pipeline。是否需要我幫你設計一個初步的 demo pipeline?
三合一引擎
Table of contents
目標
你這個做法「將條文內容、摘要與關鍵詞分開 embed」其實是非常合理的策略,對於細節檢索與語義理解都有幫助。但為了讓 query_engine 或 chat_engine 能正確整合這些資訊,有幾個做法與建議可以參考,下面我依照你提到的「如何整合」與「是否需要控制分流」這兩點來說明:
1. 三種資訊分別嵌入,但屬於同一 Node,並用 metadata 區分
這是最常見的做法:
 你可以對於同一條法規,建立三個 node,每個 node 代表一種內容(條文原文 / 摘要 / 關鍵詞),在 metadata 中加入 type: 條文、type: 摘要、type: 關鍵詞。
範例 metadata:
{
  "母法名稱": "水污染防治法",
  "子法名稱": "放流水標準",
  "條": "第5條",
  "type": "摘要"
}
2. 讓 retriever 按照 type 進行篩選與排序(可加分流)
你可以在 retriever 或 query_engine 使用 filters 來指定只查詢哪一類型的內容。例如:
retriever = vector_index.as_retriever(filters=MetadataFilters(filters=[
    ExactMatchFilter(key="type", value="條文")
]))
或者:建立多個 retriever:
law_text_retrieversummary_retrieverkeywords_retriever
然後用自訂的 query_router 或 query_analyzer 來決定該把 query 導去哪一個 retriever。
3. 進階:建立一個自訂的 RouterQueryEngine
如果你使用 llama_index 的 RouterQueryEngine,可以根據使用者 query 中的語意自動選擇適合的 retriever:
from llama_index.query_engine.router_query_engine import RouterQueryEngine
from llama_index.selectors import LLMSingleSelector
query_engine = RouterQueryEngine(
    selector=LLMSingleSelector.from_defaults(),
    query_engine_tools=[
        QueryEngineTool.from_defaults(query_engine=law_text_engine, description="查詢條文"),
        QueryEngineTool.from_defaults(query_engine=summary_engine, description="查詢摘要"),
        QueryEngineTool.from_defaults(query_engine=keyword_engine, description="查詢關鍵詞")
    ]
)
4. 是否合併嵌入?
另一種選擇是:將條文、摘要與關鍵詞合併為同一 node 的 text,這樣就只需要一個 embedding,但你就失去了細部控制與來源判別的彈性:
node_text = f"條文:{law_text}\n摘要:{summary}\n關鍵詞:{keywords}"
結論整理:
| 作法 | 優點 | 缺點 | 
|---|---|---|
| 分三個 node,metadata 標明 type | 易控制、可查詢不同面向 | 多筆資料,需分類 | 
| 同一 node 合併 text | 建立方便、embedding 簡單 | 無法分流查詢,彈性差 | 
| 多個 retriever + Router | 精準分流,支援不同問法 | 實作稍複雜,但靈活 | 
如果你想,我可以幫你依照你目前的 embedding 結構,設計 retriever 路由邏輯或是 metadata 結構。你希望使用哪一種整合方式呢?(分 node + 分流、還是合併 node?)
Router方案
太好了!我這邊先幫你做一個簡單、可擴充的模組化範例架構,搭配 Streamlit,讓你能:
- 匯入三組資料(條文、摘要、關鍵詞)
 - 各自建立 index 與 retriever
 - 透過 
RouterQueryEngine自動分流回答 - 用簡單的網頁介面輸入查詢指令、得到回應
 
✅ 檔案結構建議:
law_query_demo/
├── app.py                  # Streamlit 主程式
├── data/
│   ├── laws.json
│   ├── summaries.json
│   └── keywords.json
├── index_builder.py        # index 建立與載入
├── router_engine.py        # RouterQueryEngine 初始化
└── utils.py                # 公用工具
  ✅ index_builder.py
from llama_index.core import VectorStoreIndex, Document
def load_documents(json_path):
    import json
    with open(json_path, 'r', encoding='utf-8') as f:
        data = json.load(f)
    return [Document(text=item["text"]) for item in data]
def build_index(json_path):
    docs = load_documents(json_path)
    return VectorStoreIndex.from_documents(docs)
  ✅ router_engine.py
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.query_engine.router_query_engine import RouterQueryEngine, QueryEngineTool
from llama_index.core.selectors import LLMSingleSelector
from index_builder import build_index
def init_router_engine():
    law_index = build_index("data/laws.json")
    summary_index = build_index("data/summaries.json")
    keyword_index = build_index("data/keywords.json")
    law_engine = law_index.as_query_engine(similarity_top_k=3)
    summary_engine = summary_index.as_query_engine(similarity_top_k=3)
    keyword_engine = keyword_index.as_query_engine(similarity_top_k=3)
    tools = [
        QueryEngineTool.from_defaults(law_engine, description="查詢完整法條原文"),
        QueryEngineTool.from_defaults(summary_engine, description="查詢法條摘要"),
        QueryEngineTool.from_defaults(keyword_engine, description="查詢關鍵詞相關資訊"),
    ]
    return RouterQueryEngine(
        selector=LLMSingleSelector.from_defaults(),
        query_engine_tools=tools,
    )
  ✅ app.py (Streamlit)
import streamlit as st
from router_engine import init_router_engine
st.set_page_config(page_title="法規查詢小助手", layout="wide")
st.title("法規查詢小助手(RouterQueryEngine 版)")
query = st.text_input("請輸入你的問題:", value="第5條放流水標準是什麼?")
if query:
    with st.spinner("查詢中..."):
        router_engine = init_router_engine()
        response = router_engine.query(query)
        st.markdown("### 回覆內容")
        st.write(response)
  ✅ data/ 資料格式範例(JSON)
每一類型是一個 JSON 陣列:
  laws.json
[
    {"text": "第5條:放流水不得超過標準..."},
    {"text": "第6條:有關懲罰規定如下..."}
]
  summaries.json
[
    {"text": "第5條的摘要是針對工廠廢水的標準..."},
    {"text": "第6條摘要說明了罰則與排放責任..."}
]
  keywords.json
[
    {"text": "關鍵詞:放流水、懲罰、罰則、污水處理"}
]
這樣你就可以依據不同來源的嵌入內容自動應答,還能方便擴充與測試。
 需要我也幫你加入 Ollama local LLM 的串接,或是 metadata filter 的版本嗎?
Ollama local LLM 的串接
太好了,我這就幫你加入:
✅ 功能擴充目標
- 使用本機的 Ollama 模型(例如 llama3-taide 等)進行查詢分流與回答
 - 支援 metadata-based filtering,例如只查某章、某條、某附件等
 
✅ 修改項目說明
1. 改用本機 Ollama 當作 LLM 模型
更新 router_engine.py:
from llama_index.llms import Ollama
llm = Ollama(model="llama3-taide", temperature=0.1)
然後在 RouterQueryEngine 中加上:
return RouterQueryEngine(
    selector=LLMSingleSelector.from_defaults(llm=llm),
    query_engine_tools=tools,
    llm=llm
)
2. 支援 metadata filtering(精準比對)
你的資料在建立時要包含 metadata,例如:
{"text": "第5條:放流水不得超標...", "metadata": {"條": "第5條", "附件名稱": "放流水標準表"}}
建立 index 時(./index_builder.py)記得傳入 metadata:
from llama_index.core.schema import Document
def load_documents(json_path):
    import json
    with open(json_path, 'r', encoding='utf-8') as f:
        data = json.load(f)
    docs = [Document(text=item["text"], metadata=item.get("metadata", {})) for item in data]
    return docs
建立 QueryEngine 時,改成可以支援 filter:
from llama_index.core.retrievers import VectorIndexRetriever
from llama_index.core.query_engine import RetrieverQueryEngine
from llama_index.core.indices.vector_store import VectorStoreIndex
retriever = VectorIndexRetriever(index=index, filters={"條": "第5條"})
engine = RetrieverQueryEngine(retriever=retriever, llm=llm)
你可以再加上自動判斷 metadata 的工具(我之前幫你寫過 extract_keywords_from_query() 這類的函式,可以結合進來)。
3. Streamlit 畫面增加 metadata 檢查選單(可選)
if st.checkbox("啟用 Metadata 精準篩選"):
    selected_tiao = st.text_input("指定條文(例如:第5條)")
    selected_fujian = st.text_input("指定附件名稱")
    filters = {}
    if selected_tiao:
        filters["條"] = selected_tiao
    if selected_fujian:
        filters["附件名稱"] = selected_fujian
    response = router_engine.query(query, filters=filters)
else:
    response = router_engine.query(query)