三合一引擎
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_retriever
summary_retriever
keywords_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)