Link Search Menu Expand Document

在法律文件的 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 DBFAISS / Qdrant / Weaviate + metadata filter
檢索策略Hybrid(向量 + BM25)+ reranker
特殊需求可加入 QA pair 或摘要索引作輔助

如你有具體的法律文件樣本(例如某國的民法、合同樣板),我也可以協助你設計一個具體的 chunking/embedding/index pipeline。是否需要我幫你設計一個初步的 demo pipeline?

三合一引擎

Table of contents

目標

你這個做法「將條文內容、摘要與關鍵詞分開 embed」其實是非常合理的策略,對於細節檢索與語義理解都有幫助。但為了讓 query_enginechat_engine 能正確整合這些資訊,有幾個做法與建議可以參考,下面我依照你提到的「如何整合」與「是否需要控制分流」這兩點來說明:


1. 三種資訊分別嵌入,但屬於同一 Node,並用 metadata 區分

這是最常見的做法:
你可以對於同一條法規,建立三個 node,每個 node 代表一種內容(條文原文 / 摘要 / 關鍵詞),在 metadata 中加入 type: 條文type: 摘要type: 關鍵詞

範例 metadata:

{
  "母法名稱": "水污染防治法",
  "子法名稱": "放流水標準",
  "條": "第5條",
  "type": "摘要"
}

2. 讓 retriever 按照 type 進行篩選與排序(可加分流)

你可以在 retrieverquery_engine 使用 filters 來指定只查詢哪一類型的內容。例如:

retriever = vector_index.as_retriever(filters=MetadataFilters(filters=[
    ExactMatchFilter(key="type", value="條文")
]))

或者:建立多個 retriever:

  • law_text_retriever
  • summary_retriever
  • keywords_retriever

然後用自訂的 query_routerquery_analyzer 來決定該把 query 導去哪一個 retriever。


3. 進階:建立一個自訂的 RouterQueryEngine

如果你使用 llama_indexRouterQueryEngine,可以根據使用者 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,讓你能:

  1. 匯入三組資料(條文、摘要、關鍵詞)
  2. 各自建立 index 與 retriever
  3. 透過 RouterQueryEngine 自動分流回答
  4. 用簡單的網頁介面輸入查詢指令、得到回應

✅ 檔案結構建議:

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 的串接

太好了,我這就幫你加入:


✅ 功能擴充目標

  1. 使用本機的 Ollama 模型(例如 llama3-taide 等)進行查詢分流與回答
  2. 支援 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 檢查選單(可選)

app.py

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)