級RAG系統(tǒng)實戰(zhàn):Haystack與LangGraph的檢索優(yōu)化與編排指南)
1. 從零搭建生產(chǎn)級 RAG 的整體設(shè)計思路1.1 為什么單靠向量檢索撐不起生產(chǎn)環(huán)境很多人第一次接觸 RAG腦子里想的都是“把文檔切塊、丟進向量庫、檢索 Top-K、拼進 Prompt”跑個 Demo 感覺效果還行就以為大功告成。但真到了生產(chǎn)環(huán)境問題會一個接一個冒出來用戶問“上季度的退貨政策跟這季度有什么區(qū)別”向量檢索返回的全是單段政策文本模型根本沒法做對比用戶問“幫我查一下訂單 12345 的物流狀態(tài)”檢索器壓根不知道要去調(diào)接口只會從知識庫里瞎找一段相似文本糊弄過去。這就是樸素 RAG 的瓶頸它把“檢索”等同于“語義相似度匹配”但真實業(yè)務(wù)里的信息需求遠不止“找一段相似的話”。有些問題需要跨文檔聚合有些需要實時數(shù)據(jù)有些需要多步推理有些需要精確匹配結(jié)構(gòu)化字段。單靠一個向量索引就像拿一把螺絲刀去修整輛車——不是不能用是場景一復(fù)雜就歇菜。所以生產(chǎn)級 RAG 的設(shè)計思路必須從“單點檢索”升級為“流水線編排”。Haystack 負(fù)責(zé)把檢索、排序、生成這些環(huán)節(jié)標(biāo)準(zhǔn)化成可插拔的組件LangGraph 負(fù)責(zé)把這些組件編排成有狀態(tài)、有分支、有循環(huán)的圖結(jié)構(gòu)。兩者配合才能覆蓋從簡單問答到復(fù)雜 Agent 推理的全譜系需求。1.2 Haystack 和 LangGraph 各自扮演什么角色Haystack 的定位是檢索流水線的標(biāo)準(zhǔn)化框架。它把文檔存儲、檢索器、排序器、Prompt 構(gòu)建、生成器這些環(huán)節(jié)抽象成統(tǒng)一的組件接口你可以像搭積木一樣替換其中任何一塊。比如今天用 BM25 做稀疏檢索明天換成 Embedding 做稠密檢索后天加一個 Cross-Encoder 做重排序Pipeline 的骨架不用動只換組件就行。這種設(shè)計在需要快速迭代檢索策略的階段特別省事。LangGraph 的定位是有狀態(tài) Agent 流程的編排引擎。它把整個 RAG 流程建模成一張圖節(jié)點是具體的操作檢索、生成、工具調(diào)用、條件判斷邊是節(jié)點之間的流轉(zhuǎn)邏輯。關(guān)鍵在于它支持狀態(tài)傳遞和條件分支你可以在圖里維護一個共享的 State 對象每個節(jié)點讀寫這個 State根據(jù)當(dāng)前 State 的內(nèi)容決定下一步走哪條路。這就讓“先判斷問題類型再決定走檢索還是走工具調(diào)用”這種邏輯變得非常自然。兩者結(jié)合的方式通常是用 Haystack 構(gòu)建底層的檢索和生成組件用 LangGraph 把這些組件包裝成節(jié)點編排成完整的 Agent 流程。Haystack 管“怎么查得準(zhǔn)”LangGraph 管“什么時候查、查完之后干什么”。1.3 生產(chǎn)級 RAG 的四個核心模塊拆解把生產(chǎn)級 RAG 拆開來看核心模塊可以歸為四塊檢索層負(fù)責(zé)從知識庫中召回候選文檔。生產(chǎn)環(huán)境通常需要混合檢索稀疏 稠密再加一層重排序來提升精度。工具層負(fù)責(zé)處理檢索解決不了的問題比如實時數(shù)據(jù)查詢、結(jié)構(gòu)化計算、外部 API 調(diào)用。工具層的關(guān)鍵是工具合約的設(shè)計——每個工具接受什么參數(shù)、返回什么格式、什么條件下觸發(fā)都要定義清楚。上下文工程層負(fù)責(zé)把檢索結(jié)果、工具返回、對話歷史、系統(tǒng)指令組裝成最終送給 LLM 的上下文。這一步直接決定生成質(zhì)量但很多人恰恰在這里偷懶。編排層負(fù)責(zé)根據(jù)用戶輸入動態(tài)決定走哪條路徑。簡單問題直接檢索生成復(fù)雜問題可能需要多輪檢索、工具調(diào)用、甚至自我反思。下面這張表可以幫你快速判斷自己的 RAG 系統(tǒng)目前處于哪個階段階段檢索方式工具調(diào)用上下文處理編排邏輯Demo 級單路向量檢索無直接拼接 Top-K線性流程可用級混合檢索 重排序少量硬編碼簡單截斷條件分支生產(chǎn)級自適應(yīng)檢索策略標(biāo)準(zhǔn)化工具合約動態(tài)上下文組裝有狀態(tài)圖編排2. 檢索層的深度優(yōu)化與 Haystack 組件選型2.1 混合檢索的落地細(xì)節(jié)稀疏與稠密怎么配合純向量檢索有個致命弱點對精確匹配不敏感。用戶搜“RFC 7231”向量模型可能返回一堆講 HTTP 協(xié)議的文檔但就是找不到那個編號對應(yīng)的具體章節(jié)。反過來純 BM25 又對語義改寫無能為力用戶問“怎么讓網(wǎng)頁加載更快”BM25 可能匹配不到“前端性能優(yōu)化”這種文檔?;旌蠙z索的思路是兩路并行召回然后融合排序。Haystack 里實現(xiàn)起來不復(fù)雜from haystack import Pipeline from haystack.components.retrievers import InMemoryBM25Retriever, InMemoryEmbeddingRetriever from haystack.components.joiners import DocumentJoiner pipeline Pipeline() pipeline.add_component(bm25_retriever, InMemoryBM25Retriever(document_storestore)) pipeline.add_component(embedding_retriever, InMemoryEmbeddingRetriever(document_storestore)) pipeline.add_component(joiner, DocumentJoiner(sort_byscore, join_modereciprocal_rank_fusion)) pipeline.connect(bm25_retriever.documents, joiner.documents) pipeline.connect(embedding_retriever.documents, joiner.documents)這里的關(guān)鍵參數(shù)是join_mode。reciprocal_rank_fusionRRF是我實測下來最穩(wěn)的融合策略它不依賴兩路檢索的原始分?jǐn)?shù)可比性只看排名。具體公式是score Σ 1/(k rank)k 通常取 60。這樣即使 BM25 的分?jǐn)?shù)范圍是 0-20向量相似度是 0-1融合后也不會出現(xiàn)某一路被另一路壓制的情況。注意兩路檢索的 Top-K 不要設(shè)成一樣的。BM25 建議取 20-30向量檢索取 10-15。原因是 BM25 的召回率高但精度低多召回一些讓后面的重排序去篩向量檢索精度相對高取太多反而引入噪聲。2.2 重排序模型的選擇與性能權(quán)衡混合檢索之后候選文檔可能有 30-50 篇直接塞給 LLM 既浪費 Token 又稀釋關(guān)鍵信息。重排序的作用就是在這批候選里挑出真正相關(guān)的 3-5 篇。Haystack 支持的重排序模型主要有兩類Cross-Encoder 類比如cross-encoder/ms-marco-MiniLM-L-6-v2把 query 和 document 拼在一起送進模型打分。精度高但速度慢適合候選集不大的場景。Late Interaction 類比如 ColBERT 風(fēng)格的模型預(yù)先計算文檔的 token 級向量查詢時做 MaxSim 操作。速度快但需要額外的索引存儲。我一般建議如果候選集在 50 篇以內(nèi)直接用 Cross-Encoder延遲增加 100-200ms 但精度提升明顯。如果候選集上百考慮先用輕量模型粗篩到 20 篇再用 Cross-Encoder 精排。from haystack.components.rankers import TransformersSimilarityRanker ranker TransformersSimilarityRanker( modelcross-encoder/ms-marco-MiniLM-L-6-v2, top_k5, score_threshold0.3 )score_threshold這個參數(shù)值得說一下。設(shè)太低會引入不相關(guān)文檔設(shè)太高可能把邊緣相關(guān)的也過濾掉。我的經(jīng)驗是先在驗證集上畫一條 Precision-Recall 曲線找到 F1 最高的閾值然后稍微往下調(diào) 0.05給召回留點余量。2.3 文檔切分策略固定長度 vs 語義切分文檔切分看似簡單實則影響巨大。固定長度切分比如每 512 token 一刀切實現(xiàn)簡單但容易把一段完整的論述攔腰截斷檢索時兩半都不完整。語義切分的思想是沿著文檔的自然邊界切比如按段落、按標(biāo)題層級、按句子邊界。Haystack 提供了DocumentSplitter組件支持按 word、sentence、passage 等粒度切分from haystack.components.preprocessors import DocumentSplitter splitter DocumentSplitter( split_bysentence, split_length5, split_overlap1 )split_overlap是重疊窗口設(shè)成 1 表示相鄰塊之間有一句話的重疊。這個重疊很重要因為很多問題的答案恰好跨越兩個塊的邊界有重疊才能保證至少有一個塊包含完整信息。對于結(jié)構(gòu)化文檔比如 Markdown 或 HTML更好的做法是按標(biāo)題層級切分把每個小節(jié)作為一個獨立的塊同時保留標(biāo)題路徑作為元數(shù)據(jù)。這樣檢索時可以用標(biāo)題路徑做過濾比如“只在‘退款政策’這一節(jié)里搜”。3. 工具合約設(shè)計讓 LLM 知道什么時候該調(diào)工具3.1 工具合約的三要素Key、Query、Value工具合約的本質(zhì)是告訴 LLM 三件事我是誰Key、我在找什么Query、我能提供什么Value。這三者缺一不可。很多人在定義工具時只寫了功能描述比如“查詢訂單狀態(tài)”但沒告訴 LLM 這個工具需要什么參數(shù)、參數(shù)格式是什么、返回結(jié)果長什么樣。結(jié)果 LLM 要么不敢調(diào)要么調(diào)了之后不知道怎么處理返回值。一個完整的工具合約應(yīng)該包含工具名稱簡短、唯一、語義明確比如query_order_status而不是tool_1。功能描述一句話說明這個工具做什么什么場景下應(yīng)該用。參數(shù)定義每個參數(shù)的名稱、類型、是否必填、取值范圍、示例值。返回格式返回值的結(jié)構(gòu)最好給出示例。觸發(fā)條件明確什么情況下應(yīng)該調(diào)用這個工具什么情況下不應(yīng)該。在 LangGraph 里工具通常定義成帶類型注解的函數(shù)然后用tool裝飾器包裝from langchain_core.tools import tool tool def query_order_status(order_id: str) - dict: 查詢指定訂單的當(dāng)前物流狀態(tài)。 適用場景用戶詢問某個具體訂單的配送進度、預(yù)計到達時間。 不適用場景用戶詢問退貨政策、支付問題等非物流問題。 Args: order_id: 訂單編號格式為 10 位數(shù)字字符串例如 1234567890 Returns: {status: 運輸中, location: 杭州轉(zhuǎn)運中心, eta: 2024-01-15} # 實際調(diào)用內(nèi)部 API return internal_api.get_order_status(order_id)3.2 工具調(diào)用的觸發(fā)判斷規(guī)則、模型還是混合LLM 判斷是否調(diào)用工具有三種常見策略純規(guī)則匹配用關(guān)鍵詞或正則判斷。比如用戶輸入包含“訂單號”就觸發(fā)訂單查詢工具。優(yōu)點是快、可控缺點是覆蓋不全用戶說“我買的東西到哪了”就匹配不到。純模型判斷把工具列表和用戶輸入一起送給 LLM讓 LLM 輸出該調(diào)哪個工具。優(yōu)點是靈活缺點是可能誤判而且每次都要消耗 Token?;旌喜呗韵扔靡?guī)則做粗篩縮小工具候選集再讓模型在候選集里做精判。這是我在生產(chǎn)環(huán)境最常用的方式。比如先判斷用戶輸入是否包含數(shù)字 ID如果有就把所有需要 ID 參數(shù)的工具篩出來再讓模型選具體調(diào)哪個。在 LangGraph 里這個判斷邏輯可以做成一個獨立節(jié)點def route_decision(state): user_input state[user_input] # 粗篩是否包含訂單號模式 if re.search(r\d{10}, user_input): return order_tools # 粗篩是否涉及政策類問題 if any(kw in user_input for kw in [政策, 規(guī)則, 條款]): return retrieval # 默認(rèn)走檢索 return retrieval3.3 工具返回結(jié)果的格式化與注入工具返回的結(jié)果不能直接塞進上下文需要做格式化。原因有兩個一是原始返回可能包含大量無關(guān)字段浪費 Token二是 LLM 對結(jié)構(gòu)化數(shù)據(jù)的理解能力有限需要轉(zhuǎn)成自然語言或簡潔的 JSON。我的做法是給每個工具定義一個format_output函數(shù)把原始返回轉(zhuǎn)成適合 LLM 閱讀的格式def format_order_status(raw: dict) - str: return ( f訂單當(dāng)前狀態(tài){raw[status]}\n f最新位置{raw[location]}\n f預(yù)計到達{raw[eta]} )然后在工具節(jié)點里調(diào)用這個格式化函數(shù)把結(jié)果寫入 State 的tool_results字段。后續(xù)的生成節(jié)點從 State 里讀取格式化后的結(jié)果拼進 Prompt。實操心得工具返回結(jié)果里如果有時間戳、ID 這類信息建議保留原始值的同時加一個自然語言解釋。比如eta: 2024-01-15可以格式化成預(yù)計到達2024年1月15日LLM 生成回答時不容易搞錯格式。4. 上下文工程把正確的東西放在正確的位置4.1 上下文窗口的分配策略LLM 的上下文窗口是有限資源怎么分配直接決定生成質(zhì)量。一個典型的 RAG 請求上下文里通常包含四部分系統(tǒng)指令、對話歷史、檢索結(jié)果、用戶當(dāng)前問題。我的分配原則是系統(tǒng)指令固定占用 10-15%放在最前面。這部分包含角色定義、輸出格式要求、安全約束等。對話歷史動態(tài)占用 20-30%只保留最近 N 輪。如果歷史太長用摘要壓縮。檢索結(jié)果占用 40-50%這是核心信息不能省。用戶問題占用 5-10%放在最后面緊挨著生成位置。這個分配不是死的要根據(jù)任務(wù)類型調(diào)整。比如工具調(diào)用場景檢索結(jié)果可以少一些給工具返回留空間純問答場景檢索結(jié)果可以占到 60%。4.2 檢索結(jié)果的去重、壓縮與排序檢索回來的文檔塊經(jīng)常有重復(fù)內(nèi)容比如同一段話在不同塊里各出現(xiàn)一次。直接拼進去不僅浪費 Token還會讓 LLM 誤以為這個信息特別重要。去重的簡單做法是用 MinHash 或 SimHash 計算文檔塊的指紋相似度超過閾值的只保留一個。Haystack 的DocumentJoiner其實已經(jīng)做了一部分去重但它是基于文檔 ID 的對內(nèi)容重復(fù)無能為力。壓縮的思路是抽取式壓縮對每個文檔塊只保留與 query 最相關(guān)的句子??梢杂靡粋€輕量模型比如 MiniLM給每個句子打分取 Top-3 句子拼成壓縮后的塊。這樣能把 500 token 的塊壓到 150 token 左右信息密度大幅提升。排序方面除了相關(guān)性分?jǐn)?shù)還可以考慮多樣性。如果 Top-5 文檔全部來自同一份文件信息覆蓋面可能不夠。可以用 MMRMaximal Marginal Relevance算法在相關(guān)性和多樣性之間做平衡def mmr_select(docs, query_embedding, lambda_param0.7, top_k5): selected [] candidates docs.copy() while len(selected) top_k and candidates: mmr_scores [] for doc in candidates: relevance cosine_sim(doc.embedding, query_embedding) redundancy max([cosine_sim(doc.embedding, s.embedding) for s in selected], default0) mmr_scores.append(lambda_param * relevance - (1 - lambda_param) * redundancy) best_idx np.argmax(mmr_scores) selected.append(candidates.pop(best_idx)) return selectedlambda_param設(shè)成 0.7 表示更看重相關(guān)性設(shè)成 0.5 表示相關(guān)性和多樣性各占一半。具體取值要看業(yè)務(wù)場景政策問答類可以偏相關(guān)性調(diào)研類可以偏多樣性。4.3 對話歷史的管理截斷、摘要還是向量化多輪對話里歷史信息的管理是個頭疼問題。全保留會爆窗口全丟棄會丟失上下文。三種策略各有適用場景截斷只保留最近 N 輪。簡單粗暴適合歷史信息價值不高的場景比如客服問答。摘要用 LLM 把歷史對話壓縮成一段摘要。保留關(guān)鍵信息但增加一次 LLM 調(diào)用有延遲成本。向量化把歷史對話存進向量庫每輪根據(jù)當(dāng)前問題檢索相關(guān)歷史。適合長對話場景但實現(xiàn)復(fù)雜度最高。我通常用滑動窗口 摘要的混合策略最近 3 輪保留原文更早的對話用 LLM 壓縮成一段 200 字以內(nèi)的摘要放在系統(tǒng)指令后面。這樣既保留了近期細(xì)節(jié)又不至于丟失遠期關(guān)鍵信息。def manage_history(history, max_recent3): if len(history) max_recent: return history recent history[-max_recent:] older history[:-max_recent] summary llm_summarize(older) return [{role: system, content: f歷史對話摘要{summary}}] recent5. LangGraph 編排把檢索、工具、生成串成有狀態(tài)的圖5.1 狀態(tài)定義與節(jié)點劃分LangGraph 的核心是 State。整個圖共享一個 State 對象每個節(jié)點讀取 State、執(zhí)行操作、寫回 State。State 的定義決定了圖的表達能力。一個典型的 RAG Agent State 包含這些字段from typing import TypedDict, Annotated from langgraph.graph import add_messages class RAGState(TypedDict): messages: Annotated[list, add_messages] # 對話消息 user_input: str # 當(dāng)前用戶輸入 retrieved_docs: list # 檢索到的文檔 tool_results: list # 工具調(diào)用結(jié)果 route: str # 路由決策 final_answer: str # 最終回答Annotated[list, add_messages]這個寫法表示 messages 字段用add_messages函數(shù)做更新新消息會追加而不是覆蓋。這是 LangGraph 里管理對話歷史的推薦方式。節(jié)點劃分的原則是單一職責(zé)每個節(jié)點只做一件事。常見的節(jié)點包括classify判斷用戶意圖決定路由retrieve執(zhí)行檢索call_tool執(zhí)行工具調(diào)用generate生成最終回答reflect檢查生成結(jié)果是否需要修正5.2 條件邊與循環(huán)讓流程會拐彎LangGraph 的條件邊是實現(xiàn)動態(tài)路由的關(guān)鍵。你可以在節(jié)點執(zhí)行完后根據(jù) State 的內(nèi)容決定下一步走哪個節(jié)點from langgraph.graph import StateGraph, END def route_after_classify(state): if state[route] tool: return call_tool elif state[route] retrieve: return retrieve else: return generate graph StateGraph(RAGState) graph.add_node(classify, classify_node) graph.add_node(retrieve, retrieve_node) graph.add_node(call_tool, tool_node) graph.add_node(generate, generate_node) graph.add_conditional_edges(classify, route_after_classify) graph.add_edge(retrieve, generate) graph.add_edge(call_tool, generate) graph.add_edge(generate, END)循環(huán)的典型場景是自我反思生成節(jié)點輸出答案后用一個檢查節(jié)點判斷答案是否完整、是否引用了不存在的來源。如果不合格回到檢索節(jié)點重新檢索最多循環(huán) 2-3 次。def should_retry(state): if state.get(retry_count, 0) 2: return end if state.get(quality_check) fail: return retrieve return end graph.add_conditional_edges(check, should_retry, {retrieve: retrieve, end: END})注意循環(huán)一定要設(shè)上限否則可能陷入死循環(huán)。我一般設(shè) 2 次重試超過就直接返回當(dāng)前最好的結(jié)果并在回答里注明“信息可能不完整”。5.3 流式輸出與中間狀態(tài)的可觀測性生產(chǎn)環(huán)境里用戶等 5 秒才看到完整回答是不可接受的。LangGraph 支持流式輸出可以在每個節(jié)點執(zhí)行完后立即推送中間狀態(tài)for event in graph.stream({user_input: 查詢訂單1234567890}, stream_modeupdates): for node_name, output in event.items(): print(f[{node_name}] {output})stream_modeupdates表示每個節(jié)點執(zhí)行完后推送增量更新。你可以根據(jù)節(jié)點名稱給用戶展示不同的提示比如“正在檢索知識庫...”、“正在查詢訂單系統(tǒng)...”、“正在生成回答...”。這種反饋能顯著提升用戶體驗??捎^測性方面建議在每個節(jié)點里加日志記錄把輸入、輸出、耗時都打出來。LangGraph 配合 LangSmith 可以做全鏈路追蹤但即使不用 LangSmith自己寫個簡單的日志裝飾器也能滿足基本需求import time def log_node(func): def wrapper(state): start time.time() result func(state) elapsed time.time() - start print(f{func.__name__} 耗時 {elapsed:.2f}s, 輸入: {state.get(user_input, )[:50]}) return result return wrapper6. 常見問題與排查技巧實錄6.1 檢索召回率低從 Query 改寫入手用戶的問題往往和文檔里的表述不一致。用戶問“怎么退錢”文檔里寫的是“退款流程”。這種詞匯鴻溝是召回率低的主要原因。解決辦法是Query 改寫在檢索之前先用 LLM 把用戶問題改寫成多個不同表述的查詢分別檢索后合并結(jié)果。def rewrite_query(user_input): prompt f把下面的問題改寫成3個不同表述的檢索查詢每行一個 原問題{user_input} 要求保持語義不變使用不同的關(guān)鍵詞和句式。 return llm.generate(prompt).split(\n)實測下來Query 改寫能把召回率提升 15-25%代價是增加一次 LLM 調(diào)用和多次檢索。如果延遲敏感可以只改寫一次生成 2-3 個變體。6.2 工具調(diào)用誤觸發(fā)閾值與白名單LLM 有時候會“過度熱情”明明不需要調(diào)工具也去調(diào)。比如用戶只是問“你們支持退貨嗎”LLM 可能觸發(fā)訂單查詢工具??刂普`觸發(fā)的手段有幾個提高觸發(fā)閾值在工具描述里明確寫“僅當(dāng)用戶提供了具體訂單號時才調(diào)用此工具”。加白名單某些工具只在特定路由下才暴露給 LLM。比如訂單工具只在route order時才加入工具列表。后置校驗工具調(diào)用前檢查參數(shù)是否合法比如訂單號是否符合格式不符合就直接返回錯誤提示而不是真的調(diào)接口。def validate_tool_call(tool_name, args): if tool_name query_order_status: if not re.match(r^\d{10}$, args.get(order_id, )): return False, 訂單號格式不正確請?zhí)峁?0位數(shù)字訂單號 return True, None6.3 上下文超長截斷策略與優(yōu)先級上下文超長是 RAG 系統(tǒng)最常見的報錯。處理策略的核心是優(yōu)先級排序哪些內(nèi)容必須保留哪些可以丟。我的優(yōu)先級順序是系統(tǒng)指令不可丟用戶當(dāng)前問題不可丟工具返回結(jié)果如果本輪有工具調(diào)用檢索結(jié)果中分?jǐn)?shù)最高的 2-3 篇最近 2 輪對話歷史更早的對話摘要檢索結(jié)果中分?jǐn)?shù)較低的篇目可丟實現(xiàn)上可以先計算各部分 Token 數(shù)然后從低優(yōu)先級開始砍直到總 Token 數(shù)低于模型上限的 80%留 20% 給生成。問題現(xiàn)象可能原因排查方向解決手段召回率低Query 與文檔表述不一致檢查檢索日志中的 query 和命中文檔Query 改寫、混合檢索工具誤觸發(fā)工具描述不夠明確查看觸發(fā)時的用戶輸入和工具參數(shù)加白名單、后置校驗上下文超長檢索結(jié)果過多或歷史太長統(tǒng)計各部分 Token 占比優(yōu)先級截斷、摘要壓縮生成答案不相關(guān)檢索結(jié)果噪聲大檢查重排序分?jǐn)?shù)分布提高重排序閾值、加多樣性循環(huán)不終止重試條件太寬松檢查循環(huán)計數(shù)和退出條件設(shè)最大重試次數(shù)6.4 生成質(zhì)量不穩(wěn)定溫度、Prompt 與 Few-shot同樣的檢索結(jié)果LLM 有時生成得很好有時胡言亂語。這通常和三個因素有關(guān)溫度參數(shù)RAG 場景建議溫度設(shè) 0.1-0.3太高容易發(fā)揮太低容易死板。我一般用 0.2。Prompt 結(jié)構(gòu)把檢索結(jié)果放在 Prompt 中間用戶問題放在最后系統(tǒng)指令放在最前。這種“三明治”結(jié)構(gòu)比把所有內(nèi)容混在一起效果好。Few-shot 示例在 Prompt 里加 1-2 個“問題-檢索結(jié)果-回答”的示例能顯著提升輸出格式的穩(wěn)定性。示例要選有代表性的覆蓋不同的問題類型。PROMPT_TEMPLATE 你是一個知識庫助手。根據(jù)下面的參考資料回答用戶問題。 如果參考資料中沒有相關(guān)信息直接說“根據(jù)現(xiàn)有資料無法回答”不要編造。 參考資料 {context} 用戶問題{question} 回答要求 1. 只使用參考資料中的信息 2. 引用來源時注明文檔標(biāo)題 3. 回答簡潔不超過200字 示例 問題退貨需要幾天 參考資料[退款政策] 退貨申請審核通過后3-5個工作日內(nèi)退款到賬。 回答根據(jù)退款政策退貨申請審核通過后3-5個工作日內(nèi)退款到賬。 7. 從可用到好用幾個容易被忽略的優(yōu)化點7.1 緩存策略哪些環(huán)節(jié)可以緩存RAG 流水線里檢索和生成是兩個最耗時的環(huán)節(jié)。合理的緩存能大幅降低延遲。Embedding 緩存同一段文本的 Embedding 結(jié)果不會變可以緩存。用文本的哈希值做 key避免重復(fù)計算。檢索結(jié)果緩存如果兩個用戶問了相似的問題檢索結(jié)果可能高度重疊??梢杂?query 的語義哈希做 key緩存 Top-K 文檔 ID。生成結(jié)果緩存完全相同的 query context 組合可以直接返回緩存答案。但要注意 context 可能因為文檔更新而變化緩存要設(shè) TTL。實操心得緩存粒度不要太細(xì)否則命中率低也不要太粗否則容易返回過期結(jié)果。我一般對 Embedding 做永久緩存對檢索結(jié)果做 1 小時緩存對生成結(jié)果做 10 分鐘緩存。7.2 降級方案當(dāng)檢索或工具不可用時怎么辦生產(chǎn)環(huán)境必須有降級方案。檢索服務(wù)掛了、工具 API 超時了系統(tǒng)不能直接報錯要給用戶一個合理的回復(fù)。檢索降級如果向量檢索不可用自動切到 BM25如果都不可用返回“知識庫暫時不可用請稍后重試”。工具降級如果工具調(diào)用超時返回“查詢超時請稍后重試或聯(lián)系人工客服”。生成降級如果 LLM 服務(wù)不可用返回檢索到的原始文檔片段讓用戶自己看。降級邏輯可以用 LangGraph 的條件邊實現(xiàn)在節(jié)點里捕獲異常把錯誤信息寫入 State然后路由到降級節(jié)點。7.3 評測體系怎么知道系統(tǒng)變好了還是變壞了沒有評測的優(yōu)化都是瞎猜。RAG 系統(tǒng)的評測至少要看三個指標(biāo)檢索命中率正確答案是否在檢索結(jié)果里??梢杂萌斯?biāo)注的問答對來測。生成忠實度生成的答案是否完全基于檢索結(jié)果有沒有編造??梢杂?NLI 模型自動判斷。端到端滿意度用戶對最終回答的評分??梢杂命c贊/點踩按鈕收集。我習(xí)慣在每次修改檢索策略或 Prompt 后跑一遍固定的評測集50-100 個問題對比修改前后的指標(biāo)變化。評測集要覆蓋不同類型的問題事實型、對比型、多跳推理型、工具調(diào)用型。這套東西搭起來之后RAG 系統(tǒng)才算真正從“能跑”變成“能扛”。Haystack 和 LangGraph 的組合給了足夠的靈活性但靈活性也意味著更多的決策點。每個決策點都需要根據(jù)業(yè)務(wù)場景做權(quán)衡沒有一刀切的最優(yōu)解。我自己的經(jīng)驗是先把檢索做扎實再把工具合約定義清楚最后在編排層做精細(xì)化控制。順序反了后面會越調(diào)越亂。