📌 本文重點
- Naive RAG 上線後會遇到治理與時效問題
- 將 RAG 拆成索引 / 治理 / 拼接三層
- 用 metadata 做權限、版本、有效期與追溯
- 打通 log 與 trace,讓回答可審計可監控
在專案一開始,我們多半用一個 向量庫 + chunk + 單次檢索 的 Naive RAG 搭起 Demo,看起來效果不錯。但一旦上到真正的多人、多租戶、活資料(會更新、會過期)場景,很快就會踩到幾個痛點:
- 回答引用了 已刪除或過期的文件(時效問題)
- 同一問題在不同時間、不同使用者之間 答案不一致(治理問題)
- 回答內容 無法清楚追溯來源,審計與合規做不到(可追溯性問題)
Context Governance / Context Engineering 要解決的,就是:不要只問「怎麼多抓幾個 chunk」;要開始問「LLM 在推理時到底可以看見什麼」,並把這件事做成一個可控、可監控的架構層。
重點說明:從檢索到治理的三層拆分
1. 檢索層拆成三層:索引 / 治理 / 拼接
典型 Naive RAG:
- embed(text_chunks) → 向量庫
- query → 相似度搜尋 → top_k chunks → 拼成 prompt
升級成 Context Governance 之後,建議拆成三層:
- 索引層(Index Layer)
- 向量索引:文件內容的 semantic embedding
-
結構化索引:metadata / 權限 / version / valid_from / valid_to / tenant_id 等,存在關聯式 DB 或專門的索引(如
OpenSearchfilter,Postgres JSONB) -
治理層(Governance Layer)
- 權限過濾:
per-tenant、per-user、per-role - 時效與版本控制:只允許在有效期內、最新版本的文件進入 context
-
來源標註:在 context 裡保留
doc_id / version / source_system / updated_at,為後續 trace 和審計打基礎 -
拼接層(Context Assembly Layer)
- 根據任務(
QA/summarization/agent工單處理)選擇不同 prompt template - 根據 context policy(例如:最多
N個來源、每個來源最多M個 chunk、必須包含最新公告等)來決定如何拼接
這樣做的核心好處:
- 可控:你能明確說出「這個回答看到了哪些資料」
- 可審計:任何回答都能追溯到具體版本的文件
- 可演進:未來換向量庫、換模型、換圖資料結構,只要維持治理層的 contract 即可
💡 關鍵: 把 RAG 拆成索引、治理、拼接三層,可以在不動底層基礎設施的前提下,持續優化與替換任一層實作。
2. 何時只用 Vector,何時要 GraphRAG / Schema
向量搜尋擅長回答:「跟這段話最像的是什麼?」,但在以下情境常常不夠:
- 跨多個實體、關係的複雜查詢(例如:
- 「這客戶過去一年所有 ticket 中,和某功能相關的主要痛點是什麼?」)
- 知識本身有明確的 schema / 關係(產品 → 版本 →
bug→ 修補公告) - 需要 聚合 / 推理 而不是單純摘錄段落
這時考慮引入:
- GraphRAG / 知識圖譜:用圖結構顯式表示實體關係,向量只負責節點 / 邊的語義近似
- 業務 schema:在
DB或graph DB中建模Customer -[reported]-> Ticket -[relates_to]-> Feature等結構,RAG 只從圖或 DB 中抓「已整理好」的資料進 context
實務建議:
- FAQ、文件查詢、規範查詢:優先 純 vector + metadata filter
- 關係推理、跨文件關聯、流程追蹤:優先 GraphRAG / schema + vector(vector 幫你找到節點,圖幫你沿著關係走)
3. 在 RAG pipeline 中引入治理:我們需要哪些欄位?
最小可用治理模型,可以從以下 metadata 開始:
tenant_id:租戶隔離visibility/allowed_roles:權限version/is_latest:版本valid_from/valid_to:有效期source/doc_id/chunk_id:來源追蹤
治理層的業務邏輯就是:
- 只選
is_latest = true且now BETWEEN valid_from AND valid_to的文件 - 只選
tenant_id與當前請求相符,且角色有權限的文件 - 把上述 metadata 一起帶入 context,並在回答後產生 answer trace
💡 關鍵: 把
tenant_id、版本與有效期變成硬性 filter,可以大幅降低資料外洩與用到過期文件的風險。
實作範例:Python / TypeScript 實戰片段
以下不是完整專案,而是幾個關鍵點:版本與有效期控制、per-tenant 權限過濾、answer trace、監控與日誌。
1. Python:檢索層 + 治理層
假設向量存在 pgvector,metadata 在同一張表的 JSONB 裡:
# pseudo: Python + asyncpg + pgvector
from datetime import datetime
from typing import List, Dict
async def search_governed_chunks(
db, *, query_embedding: list[float], tenant_id: str, user_roles: list[str],
top_k: int = 8
) -> List[Dict]:
now = datetime.utcnow()
sql = """
SELECT
id, doc_id, chunk_id, content, metadata,
1 - (embedding <=> $1::vector) AS score
FROM documents
WHERE
metadata->>'tenant_id' = $2
AND (metadata->>'is_latest')::boolean = true
AND (metadata->>'valid_from')::timestamptz <= $3
AND (metadata->>'valid_to')::timestamptz >= $3
AND (
metadata->>'visibility' = 'public'
OR EXISTS (
SELECT 1 FROM jsonb_array_elements_text(metadata->'allowed_roles') r
WHERE r = ANY($4::text[])
)
)
ORDER BY embedding <=> $1::vector
LIMIT $5
"""
rows = await db.fetch(sql, query_embedding, tenant_id, now, user_roles, top_k)
return [
{
"doc_id": r["doc_id"],
"chunk_id": r["chunk_id"],
"content": r["content"],
"metadata": r["metadata"],
"score": r["score"],
}
for r in rows
]
上面這段把 向量相似度 + 權限 + 版本 + 有效期 一次做掉,這就是「治理層」的核心入口。
2. TypeScript:拼接層 + answer trace
假設你用 Node.js + OpenAI API:
// pseudo: TypeScript + OpenAI
import { OpenAI } from "openai";
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
type RetrievedChunk = {
content: string;
metadata: {
doc_id: string;
chunk_id: string;
version: string;
source: string;
updated_at: string;
};
};
function buildContext(chunks: RetrievedChunk[]): {
contextText: string;
trace: any[];
} {
const trace = chunks.map((c) => c.metadata);
const contextText = chunks
.map(
(c, idx) =>
`[[SOURCE ${idx + 1}]]\n` +
`doc_id=${c.metadata.doc_id} version=${c.metadata.version} ` +
`source=${c.metadata.source} updated_at=${c.metadata.updated_at}\n` +
`${c.content}\n`
)
.join("\n\n");
return { contextText, trace };
}
export async function answerWithTrace(
question: string,
chunks: RetrievedChunk[],
) {
const { contextText, trace } = buildContext(chunks);
const prompt = `你是企業知識庫助理,只能根據給定的資料來源回答。\n
如果無法從資料中找到答案,請明確回答「目前資料不足以回答」。\n
=== 已檢索資料 ===\n${contextText}\n
=== 問題 ===\n${question}`;
const completion = await openai.chat.completions.create({
model: "gpt-4.1-mini",
messages: [{ role: "user", content: prompt }],
});
const answer = completion.choices[0]?.message?.content ?? "";
return { answer, trace };
}
這裡的重點:
- context 內嵌了
doc_id / version / source / updated_at,LLM 的回答自然可以參照這些標籤 - 回傳結果中附帶
trace,可以直接存進answers表,實現問答可追溯
3. 監控與日誌設計(Python 片段)
最低限度,你應該記:
- 請求
id、tenant_id、user_id query、top_k、實際使用的 chunk 數- 每個 chunk 的
doc_id / version / score - LLM
model/latency/token數
import logging, time
logger = logging.getLogger("rag")
async def rag_pipeline(request_id: str, tenant_id: str, user_id: str, query: str):
t0 = time.time()
# 1. embed
embedding = await embed_query(query)
# 2. retrieve + governance
chunks = await search_governed_chunks(
db, query_embedding=embedding, tenant_id=tenant_id,
user_roles=["user"], top_k=8
)
# 3. assemble + call LLM
answer, trace = await call_llm_with_trace(query, chunks)
latency = time.time() - t0
logger.info(
"RAG_REQUEST", extra={
"request_id": request_id,
"tenant_id": tenant_id,
"user_id": user_id,
"query": query,
"retrieved_count": len(chunks),
"trace": trace,
"latency_ms": int(latency * 1000),
}
)
return answer
有了這層 log,你可以:
- 回頭調查「為何某次回答錯」——看他用了哪個版本的哪份文件
- 做品質分析——哪些 doc/source 經常被引用,哪些很少被用到
💡 關鍵: 系統性記錄
request → used_docs與 latency,能同時支撐故障排查與長期品質優化。
建議與注意事項:常見坑與最佳實踐 Checklist
常見坑
- 只相信 embedding 相似度
- 沒有權限 / 版本 / 時效 filter,導致過時、無權資料進 context
- 不處理刪除 / 更新
- 檔案在主系統被刪掉或更新,向量庫沒同步,LLM 繼續引用舊版本
- 忽略權限與租戶隔離
- 向量庫是共享的,但檢索時沒過濾
tenant_id或roles,直接變資料外洩風險 - 多索引源混用不標註
- 搜尋結果來自「公告 / 內部 wiki / 工單系統」,但沒有來源標籤,LLM 很難判斷權威度,使用者也無從分辨
可落地的最佳實踐 Checklist
- 索引層
-
- [ ] 每個 chunk 至少包含:
tenant_id,doc_id,chunk_id,version,is_latest,valid_from,valid_to,source - [ ] 有明確的 upsert pipeline,文件更新時同步更新 metadata 與向量
- [ ] 每個 chunk 至少包含:
- 治理層
-
- [ ] 檢索
API強制 filtertenant_id+is_latest+ 有效期 - [ ] 權限使用 白名單策略(角色不在
allowed_roles就直接排除) - [ ] 支援
per-tenant可配置的 context policy(top_k上限、允許的source列表)
- [ ] 檢索
- 拼接層
-
- [ ] 不同任務(
QA/summarization/agent)使用不同 prompt template - [ ] 在 context 中保留清楚的 來源標註,並鼓勵 LLM 在回答中引用
- [ ] 不同任務(
- 監控與審計
-
- [ ] 每個回答都存
request → used_docs (doc_id, version)的 trace - [ ] 有簡單 dashboard 監控:平均
retrieved_count/top_k命中度 / 各source的引用率
- [ ] 每個回答都存
做到以上,你的系統就不是「會講話的向量搜尋」,而是有 治理能力 的企業級 RAG:回答可控、資料可審計、行為可監控,也比較能撐過 Demo 以後的那一階段——真正上線的環境。
🚀 你現在可以做的事
- 審視現有 RAG pipeline,補上
tenant_id、版本、有效期等必要 metadata 欄位- 在搜尋
SQL或檢索層實作權限 + 時效 +is_latest的強制 filter- 為每次回答記錄
request → used_docstrace,並在 log 或 DB 裡建立最小可用的審計表格


發佈留言