Link Search Menu Expand Document

三合一引擎

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)