優(yōu)實(shí)戰(zhàn))
做RAG或者指令微調(diào)項(xiàng)目的人幾乎沒有沒跟 few-shot prompt 打過交道的。多數(shù)人的默認(rèn)動(dòng)作是把示例一股腦塞進(jìn) prompt 模板跑一兩次覺得還行等示例庫漲到幾十上百條問題就來了token 預(yù)算爆炸、模型注意力被無關(guān)示例稀釋、響應(yīng)延遲變高、賬單也跟著變厚。LangChain 里的示例選擇器ExampleSelector就是為解決這個(gè)問題設(shè)計(jì)的組件。它不是一個(gè)獨(dú)立功能模塊而是 few-shot prompt 體系里專門負(fù)責(zé)“在把 prompt 提交給模型之前從示例庫中挑選最優(yōu)子集”的決策器。目前最常用的四條技術(shù)路線分別是按長(zhǎng)度截?cái)嗟?Length 策略、按語義相似度排序的 Similarity 策略、平衡相關(guān)性與多樣性的 MMR 策略以及純文本重疊度打分的 NGram 策略。很多人只知道它們存在卻不太清楚各自底層怎么算、參數(shù)怎么調(diào)、適合什么場(chǎng)景。這篇文章適合三類人剛接觸 LangChain、對(duì)選擇器只有模糊概念的初學(xué)者已經(jīng)在用 Similarity 但效果不如預(yù)期、想換 MMR 試試的開發(fā)者以及想在離線環(huán)境下做示例篩選、不想引入重型向量組件的工程人員。我會(huì)把四種策略的原理、實(shí)操代碼、參數(shù)細(xì)節(jié)和踩過的坑一次講清楚。1. 示例選擇器到底解決了什么問題1.1 少樣本提示的規(guī)模困境先看沒有選擇器時(shí)的 few-shot 流程。每個(gè)示例都會(huì)被格式化成一個(gè)“輸入-輸出”對(duì)然后依次拼接進(jìn)最終的 prompt 模板。示例數(shù)量少時(shí)模型能很好地跟著示例風(fēng)格走示例一旦多起來整條 prompt 結(jié)構(gòu)會(huì)變得臃腫模型在長(zhǎng)上下文中對(duì)末尾指令的遵循度會(huì)顯著下降這背后有明確的注意力機(jī)制原因——上下文越長(zhǎng)中間部分的有效注意力越容易被稀釋。更現(xiàn)實(shí)的是成本每次請(qǐng)求都把全部示例算一遍 token按 token 計(jì)費(fèi)的接口里這部分開銷幾乎是純浪費(fèi)。Length 策略解決的是 token 預(yù)算問題。Similarity、MMR、NGram 解決的是“從這么多示例中選哪些最有用”的問題。想清楚這一點(diǎn)你就明白它們并不是互相排斥的競(jìng)品而是同一決策鏈路里不同環(huán)節(jié)的組件。例如可以先讓 Length 控制最終 prompt 的總體規(guī)模再用 Similarity 或 MMR 從語義上挑選高質(zhì)量子集也可以先做語義召回再按長(zhǎng)度做二次裁切。不同策略可以組合疊加只是 LangChain 默認(rèn)給的是單個(gè)選擇器接口組合使用時(shí)通常需要自己在外部編排。1.2 接口設(shè)計(jì)選擇器如何嵌入 few-shot 鏈路LangChain 里所有選擇器都繼承同一個(gè)抽象基類 BaseExampleSelector核心方法只有兩個(gè)select_examples(input_variables) 負(fù)責(zé)接收用戶輸入的變量字典、返回選中的示例列表add_example(example) 負(fù)責(zé)向示例庫中追加新樣本。這個(gè)設(shè)計(jì)非??酥撇还艿讓邮窍蛄繖z索、長(zhǎng)度計(jì)算還是自定義算法對(duì)外暴露的都是同一套接口。接入 FewShotPromptTemplate 時(shí)只需要把原來的examples參數(shù)替換成example_selector其余模板結(jié)構(gòu)不用動(dòng)。這個(gè)設(shè)計(jì)最有價(jià)值的地方在于它把“示例庫有多大”和“本次請(qǐng)求用哪些示例”徹底解耦。示例庫可以持續(xù)增長(zhǎng)prompt 模板保持穩(wěn)定選擇器內(nèi)部自行取舍。如果你想快速驗(yàn)證一個(gè)自定義的篩選算法繼承基類、實(shí)現(xiàn) select_examples然后立刻替換內(nèi)置方案即可完全不用重寫 prompt 的解析邏輯。后面我會(huì)專門寫一個(gè)自定義選擇器的完整示例把這條路走一遍。2. 四種策略的底層邏輯與差異2.1 Length 策略不感知語義的容量控制閥LengthBasedExampleSelector 的算法非常直白按示例在數(shù)組中的順序逐個(gè)將示例通過 example_prompt 格式化累加已選文本的總長(zhǎng)度一旦超過 max_length 就停止。它不關(guān)心示例內(nèi)容與用戶問題之間有沒有語義關(guān)聯(lián)也不做任何排序調(diào)整本質(zhì)是一個(gè)純粹的容量控制閥。這里有一個(gè)容易踩坑的點(diǎn)max_length 的單位。LangChain 內(nèi)部通過 get_token_ids 函數(shù)來估算長(zhǎng)度默認(rèn)實(shí)現(xiàn)會(huì)把文本按空白字符切分統(tǒng)計(jì)切分結(jié)果的片段數(shù)。對(duì)英文來說這約等于單詞數(shù)量還算可用對(duì)中文來說整句話沒有空格會(huì)被算成“一個(gè)片段”導(dǎo)致 max_length 幾乎不可能觸發(fā)截?cái)嗍纠俣嘁矔?huì)被全部拼進(jìn)去。解決思路是給 LengthBasedExampleSelector 傳入自定義的 get_token_ids 函數(shù)例如把中文字符數(shù)或模型真實(shí) tokenizer 的結(jié)果作為統(tǒng)計(jì)單位。使用 Length 策略的典型場(chǎng)景有兩類。第一類是模型上下文窗口很小比如一些輕量級(jí)模型輸入上限只有 2k token需要硬性保證 prompt 不超限。第二類是成本敏感型業(yè)務(wù)希望把每次請(qǐng)求的 token 數(shù)壓在一個(gè)穩(wěn)定預(yù)算內(nèi)。它的優(yōu)點(diǎn)是零模型依賴、速度快、結(jié)果可預(yù)測(cè)缺點(diǎn)是選出來的示例完全不管相關(guān)性可能把最沒用的幾條留在 prompt 里。2.2 Similarity 策略語義檢索直出 top-kSemanticSimilarityExampleSelector 做的事情本質(zhì)上就是一次 top-k 語義檢索。它把所有示例文本用 embedding 模型轉(zhuǎn)成向量用戶輸入到來時(shí)也做同樣的向量化然后計(jì)算余弦相似度取相似度最高的 k 個(gè)示例返回。這個(gè)策略非常直觀效果在大多數(shù)問答場(chǎng)景里都優(yōu)于隨機(jī)抽取和長(zhǎng)度截?cái)?。但它的隱性成本在兩個(gè)方面。第一是 embedding 模型的選型和向量庫的持久化LangChain 默認(rèn)使用內(nèi)存向量庫 InMemoryVectorStore每次進(jìn)程重啟都要重新計(jì)算全部示例的 embedding如果示例有幾萬條初始化耗時(shí)不可忽視。常見緩解方案是把向量庫持久化到磁盤或者給 embedding 結(jié)果做本地緩存。第二是 Similarity 完全不處理多樣性。10 個(gè)候選示例里可能有 8 個(gè)在表達(dá)同一個(gè)意思取了 top-k 等于只覆蓋了一個(gè)角度模型學(xué)到的東西非常有限。這也是很多人從 Similarity 轉(zhuǎn)向 MMR 的直接原因。還有一點(diǎn)值得注意Similarity 選擇器在構(gòu)造時(shí)會(huì)一次性 embedding 所有示例。若在調(diào)用 select_examples 之前調(diào)用了 add_example 追加新樣本不同版本的行為不完全一致有些實(shí)現(xiàn)不會(huì)自動(dòng)更新向量庫導(dǎo)致新樣本在檢索中“隱身”。所以追加樣本后最好重新構(gòu)建選擇器或者確認(rèn)你所用版本的 add_example 覆蓋了向量庫更新邏輯。2.3 MMR 策略給相似度加上多樣性約束MaxMarginalRelevanceExampleSelector 是對(duì) Similarity 的重要擴(kuò)展。它引入了 fetch_k 參數(shù)先從示例集中取出與用戶輸入相似度最高的 fetch_k 個(gè)候選再在候選集合內(nèi)部進(jìn)行重排讓最終返回的 k 個(gè)示例既與問題相關(guān)彼此之間又盡量不同。這個(gè)“相關(guān)與多樣”的權(quán)衡由一個(gè) lambda_mult 參數(shù)控制取值范圍在 0 到 1 之間。理解 MMR 的關(guān)鍵是那個(gè) lambda_mult。它可以近似看成對(duì)目標(biāo)函數(shù)的加權(quán)最終得分 ≈ (1 - lambda_mult) × 相關(guān)性得分 - lambda_mult × 與已選示例的最大相似度。lambda_mult 等于 1 時(shí)MMR 退化為純相似度選擇多樣性權(quán)重為零lambda_mult 等于 0 時(shí)它傾向于選“跟已選示例都不像”的樣本哪怕相關(guān)性弱一些。這個(gè)參數(shù)不是越大越好實(shí)際測(cè)試?yán)镂彝ǔ7旁?0.5 到 0.7 之間效果最穩(wěn)。如果設(shè)置到 0.8 以上多樣性懲罰幾乎可以忽略設(shè)置到 0.2 以下選出來的示例可能跟當(dāng)前問題完全不搭邊。還有一個(gè)極容易忽略的細(xì)節(jié)fetch_k 必須大于 k。如果 fetch_k 等于 k候選集合和最終返回集合大小一樣MMR 的重排就沒有任何操作空間多樣性約束形同虛設(shè)。我見過不少人在代碼里把 fetch_k 和 k 都設(shè)成 2然后跑來問為什么 MMR 效果和 Similarity 沒區(qū)別原因就在這里。2.4 NGram 策略無模型依賴的字面重疊打分NGramOverlapExampleSelector 是四種策略里唯一不需要 embedding 模型就能實(shí)現(xiàn)“相似度排序”的選項(xiàng)。它把示例文本和用戶輸入都切分成 n-gram也就是連續(xù) n 個(gè)基本單元組成的片段計(jì)算兩者之間的重疊比例作為得分然后設(shè)置一個(gè) threshold把低于閾值的示例過濾掉最終返回得分最高的 k 個(gè)。這個(gè)思路在離線部署、隱私敏感、或者不想引入外部模型依賴的場(chǎng)景下非常好用。它最大的局限是字面重疊不等于語義相似。同義詞改寫、語序調(diào)整、抽象表達(dá)都會(huì)讓得分大幅下降所以它更適合術(shù)語固定、格式規(guī)范的垂直領(lǐng)域比如設(shè)備故障碼查詢、合同條款匹配、產(chǎn)品型號(hào)參數(shù)說明這類任務(wù)。在這些場(chǎng)景里字面重疊恰恰是比語義向量更可靠的信號(hào)。NGram 有幾個(gè)關(guān)鍵參數(shù)需要理解。n 控制 n-gram 的粒度n 越小越能匹配短片段但誤報(bào)也越多n 越大越嚴(yán)格但對(duì)長(zhǎng)文本的要求也越高。ngram_delimiter 用來控制切分單元英文分詞場(chǎng)景通常用空格或標(biāo)點(diǎn)中文連續(xù)字場(chǎng)景可以設(shè)為空字符串直接按字符滑動(dòng)窗口。threshold 則控制過濾強(qiáng)度設(shè)得太低等于沒過濾設(shè)得太高可能直接返回空列表需要結(jié)合示例庫的具體文本分布去調(diào)整。3. 實(shí)操用統(tǒng)一示例集跑通四種選擇器3.1 準(zhǔn)備示例集和公共模板為了對(duì)比差異我準(zhǔn)備了一份非常簡(jiǎn)單的電商文案示例集每條示例包含商品名、風(fēng)格和文案三個(gè)字段。這個(gè)場(chǎng)景足夠直觀方便你在本地快速驗(yàn)證結(jié)果差異。from langchain_core.prompts import PromptTemplate, FewShotPromptTemplate examples [ {product: 紙纖手機(jī)殼, style: 極簡(jiǎn), text: 輕若無物還原裸機(jī)手感}, {product: 商務(wù)保溫杯, style: 商務(wù), text: 24小時(shí)保溫會(huì)議不斷檔}, {product: 運(yùn)動(dòng)耳機(jī), style: 運(yùn)動(dòng), text: IPX5防水跑步甩不掉}, {product: 電競(jìng)鍵盤, style: 電競(jìng), text: 全鍵熱插拔RGB隨光而動(dòng)}, {product: 便攜榨汁杯, style: 居家, text: 30秒出汁一沖就干凈}, {product: 桌面雙屏支架, style: 辦公, text: 升降折疊頸椎不被綁架}, ] example_prompt PromptTemplate( input_variables[product, style, text], template商品: {product}\n風(fēng)格: {style}\n文案: {text}, )注意選擇器內(nèi)部需要借助 example_prompt 把示例格式化后才能計(jì)算長(zhǎng)度或重疊度所以 example_prompt 是 Length 和 NGram 兩個(gè)選擇器的必傳參數(shù)。Similarity 和 MMR 因?yàn)樽呦蛄炕粡?qiáng)制要求 example_prompt但加上之后示例向量會(huì)包含模板結(jié)構(gòu)信息檢索效果通常更穩(wěn)。3.2 Length 與 NGram零依賴的兩個(gè)實(shí)現(xiàn)Length 選擇器的構(gòu)建很簡(jiǎn)單。把 examples 和 example_prompt 傳進(jìn)去設(shè)置 max_length 即可。from langchain_core.example_selectors import LengthBasedExampleSelector length_selector LengthBasedExampleSelector( examplesexamples, example_promptexample_prompt, max_length120, ) prompt FewShotPromptTemplate( example_selectorlength_selector, example_promptexample_prompt, prefix你是一名電商文案策劃。, suffix商品: {input}\n風(fēng)格: {style}\n文案:, input_variables[input, style], ) result prompt.format(input降噪耳機(jī), style通勤) print(result)輸出里示例會(huì)按原始順序被拼接。max_length 120 意味著當(dāng)已選示例的累計(jì) token 數(shù)超過 120 時(shí)后續(xù)示例被截?cái)?。這里有一個(gè)我在 2.1 提到的坑默認(rèn)的 token 估算對(duì)中文不友好。實(shí)測(cè)中文場(chǎng)景下建議把自定義 tokenizer 傳進(jìn)去例如按字符數(shù)統(tǒng)計(jì)def char_tokenizer(text: str) - list[str]: return list(text) length_selector_cn LengthBasedExampleSelector( examplesexamples, example_promptexample_prompt, max_length120, get_token_idschar_tokenizer, )NGram 選擇器的構(gòu)建也類似只是多了一個(gè) threshold 和 n 參數(shù)。from langchain_core.example_selectors import NGramOverlapExampleSelector ngram_selector NGramOverlapExampleSelector( examplesexamples, example_promptexample_prompt, threshold0.1, n2, ) selected ngram_selector.select_examples({input: 運(yùn)動(dòng)時(shí)防水不掉}, {style: 運(yùn)動(dòng)}) print(selected)這里 select_examples 接收的是和最終 prompt 變量同名的字典。我傳入了兩個(gè)鍵一個(gè)是用戶問題的 input另一個(gè)是風(fēng)格字段 style。NGram 會(huì)從這些變量中提取文本與示例格式化后的文本計(jì)算重疊度。如果閾值設(shè)得過高可能出現(xiàn)空列表所以我習(xí)慣先設(shè)一個(gè)較小的值觀察分布再逐步調(diào)大。3.3 Similarity 與 MMR向量化方案落地Similarity 和 MMR 在 LangChain 里的構(gòu)建方式高度一致都依賴 embedding 和 vectorstore。為了確保示例代碼能在本地直接跑通這里先用確定性假向量 DeterministicFakeEmbedding 演示。它在測(cè)試環(huán)境里非常有用不依賴網(wǎng)絡(luò)也不下載模型文件。實(shí)際項(xiàng)目可以換成 HuggingFaceEmbeddings 或其他 embedding 服務(wù)。from langchain_core.embeddings import DeterministicFakeEmbedding from langchain_core.example_selectors import SemanticSimilarityExampleSelector from langchain_core.vectorstores import InMemoryVectorStore embeddings DeterministicFakeEmbedding(size32) similarity_selector SemanticSimilarityExampleSelector.from_examples( examplesexamples, embeddingsembeddings, vectorstoreInMemoryVectorStore, k2, ) selected similarity_selector.select_examples({input: 想找一款防水耐用的運(yùn)動(dòng)耳機(jī)}) print(selected)from_examples 是一個(gè)便捷類方法它會(huì)自動(dòng)完成“把示例列表逐條 embedding、寫入向量庫”的全過程。如果你已經(jīng)有持久化的向量庫也可以直接傳入 vectorstore 實(shí)例避免每次啟動(dòng)都重新計(jì)算。真實(shí)環(huán)境里我建議先把示例集存成 JSON 或數(shù)據(jù)庫表啟動(dòng)服務(wù)時(shí)檢測(cè)向量庫是否存在不存在才做 embedding 初始化。MMR 的構(gòu)建方式幾乎一樣只是多了 fetch_k 和 lambda_mult。from langchain_core.example_selectors import MaxMarginalRelevanceExampleSelector mmr_selector MaxMarginalRelevanceExampleSelector.from_examples( examplesexamples, embeddingsembeddings, vectorstoreInMemoryVectorStore, k2, fetch_k4, lambda_mult0.6, ) selected mmr_selector.select_examples({input: 想找一款防水耐用的運(yùn)動(dòng)耳機(jī)}) print(selected)同樣的用戶輸入Similarity 返回的可能是“運(yùn)動(dòng)耳機(jī)”和另一個(gè)與運(yùn)動(dòng)耳機(jī)向量最近似的樣本MMR 在 fetch_k 足夠大時(shí)會(huì)在相關(guān)性允許的范圍內(nèi)盡量讓兩個(gè)示例分屬不同風(fēng)格例如一條運(yùn)動(dòng)耳機(jī)、一條便攜榨汁杯。這種差異在真實(shí)業(yè)務(wù)中往往決定了模型輸出角度的豐富程度。3.4 自定義選擇器一個(gè)最小但完整的標(biāo)簽過濾示例內(nèi)置選擇器再靈活也不如自己寫一個(gè)來得踏實(shí)。BaseExampleSelector 接口只有兩個(gè)方法我寫了一個(gè)按難度標(biāo)簽過濾的選擇器用來演示自定義算法如何接入 few-shot 鏈路。from langchain_core.example_selectors.base import BaseExampleSelector class DifficultySelector(BaseExampleSelector): def __init__(self, examples): self.examples examples def add_example(self, example): self.examples.append(example) def select_examples(self, input_variables): max_difficulty input_variables.get(max_difficulty, 5) selected [ ex for ex in self.examples if ex.get(difficulty, 0) max_difficulty ] return selected使用時(shí)只需要把 examples 換成帶 difficulty 字段的數(shù)據(jù)然后傳給 FewShotPromptTemplate。注意 select_examples 的參數(shù)是一個(gè)字典里面可能有用戶輸入、可能有控制參數(shù)這些約定完全由你自己定。我在實(shí)際項(xiàng)目里曾經(jīng)用類似思路寫過“按數(shù)據(jù)類型過濾”“按輸出格式過濾”“輪詢打散”三種自定義選擇器替換的時(shí)候幾乎零成本。需要留意的一點(diǎn)是自定義選擇器的 add_example 是抽象方法必須實(shí)現(xiàn)。如果示例庫是靜態(tài)的方法體里留一個(gè)簡(jiǎn)單的 append 就夠了但絕不能刪掉否則實(shí)例化會(huì)直接報(bào)錯(cuò)。3.5 參數(shù)調(diào)優(yōu)時(shí)必須知道的三處關(guān)鍵細(xì)節(jié)第一max_length 估算要匹配你的實(shí)際 tokenizer。這點(diǎn)在上面已經(jīng)反復(fù)強(qiáng)調(diào)真正上線時(shí)不要依賴 LangChain 默認(rèn)的空格切分估算要么自定義 get_token_ids要么直接用 len(tokenizer.encode(text)) 確保單位一致。第二fetch_k 至少要明顯大于 k。MMR 的整個(gè)價(jià)值都建立在候選集大于返回集這個(gè)前提之上。fetch_k 通常設(shè)成 k 的 2 到 3 倍比如 k2 時(shí) fetch_k 設(shè) 4 或 6。設(shè)得太大也會(huì)導(dǎo)致 MMR 在“候選池比選”時(shí)的計(jì)算量上升但對(duì)于幾百條示例的規(guī)模完全不是問題。第三threshold 的初始值寧低勿高。NGram 的 threshold 如果設(shè)太高容易得到空列表校驗(yàn)過的工程人員都知道空示例集傳入 few-shot prompt 不會(huì)報(bào)錯(cuò)但模型輸出會(huì)明顯失去 few-shot 的規(guī)范約束問題排查起來反而更難。我建議第一次跑先設(shè) 0 或負(fù)值把所有示例得分打印出來觀察分布后再確定閾值。4. 橫向?qū)Ρ扰c選型建議4.1 四種策略差異對(duì)照表為了讓你一眼看清差異我把四種策略的核心信號(hào)、依賴條件和適用邊界整理成一張表。這張表是我在實(shí)際選型時(shí)反復(fù)用到的速查參考。策略核心信號(hào)外部依賴主要優(yōu)勢(shì)主要局限建議場(chǎng)景Length文本長(zhǎng)度無控制 prompt 體積零成本不感知語義隨機(jī)性高上下文窗口小、成本敏感Similarity語義向量余弦相似度embedding 模型語義匹配準(zhǔn)部署簡(jiǎn)單忽略多樣性需向量庫問答知識(shí)庫、意圖匹配MMR語義相似度 多樣懲罰embedding 模型兼顧相關(guān)和多樣參數(shù)多需調(diào)優(yōu)示例覆蓋多個(gè)角度時(shí)NGram字面 n-gram 重疊度無離線可用結(jié)果可解釋無法處理同義改寫術(shù)語固定、格式規(guī)范從這張表能看出一個(gè)規(guī)律越偏向語義的方案效果上限越高但外部依賴和運(yùn)行成本也越高越偏向字面規(guī)則的方案越輕量但能處理的表達(dá)變化也越少。選型本質(zhì)上是在效果、成本、可維護(hù)性之間做權(quán)衡不存在某一種策略在所有場(chǎng)景下都最優(yōu)。4.2 不同場(chǎng)景下的選擇建議如果你是剛起步的小項(xiàng)目示例只有二三十條我的建議是直接用 Length 或者直接用全部示例沒必要折騰向量庫。這個(gè)階段模型體積小、數(shù)據(jù)量少引入 embedding 帶來的收益有限反而增加部署復(fù)雜度。當(dāng)示例庫規(guī)模超過一百條或者語言多樣化明顯時(shí)再用 Similarity 也不遲。它是四種策略里性價(jià)比最高的一個(gè)實(shí)現(xiàn)簡(jiǎn)單、理解直觀絕大多數(shù)業(yè)務(wù)用它已經(jīng)足夠。只有當(dāng)你不止一次發(fā)現(xiàn)“Similarity 選出的示例翻來覆去都是同一個(gè)風(fēng)格”時(shí)才有必要升級(jí)到 MMR。對(duì)離線環(huán)境或不允許調(diào)用外部模型的場(chǎng)景不要猶豫直接用 NGram它是唯一能穩(wěn)定落地又不依賴外部服務(wù)的基線方案。還有一個(gè)容易忽略的場(chǎng)景模板結(jié)構(gòu)非常固定的任務(wù)例如實(shí)體抽取、信息格式化。這種情況下示例之間的差異往往集中在極少數(shù)字段里NGram 的穩(wěn)定性和可解釋性反而比 embedding 更可靠。我用 NGram 做過一次法律法規(guī)條款的輔助檢索復(fù)述效果比同參數(shù)的語義檢索更穩(wěn)因?yàn)榉晌谋镜脑~匯高度固定字面重疊本身就是強(qiáng)信號(hào)。4.3 選擇器與記憶、緩存模塊的協(xié)同選擇器不是孤立工作的。在一個(gè)完整的 LangChain 鏈路里它通常與記憶模塊、緩存模塊同時(shí)存在。記憶模塊保留的是歷史對(duì)話狀態(tài)選擇器關(guān)注的是當(dāng)前這一輪需要哪些示例兩者互不干擾。一個(gè)常見的設(shè)計(jì)是歷史對(duì)話經(jīng)過記憶模塊壓縮后寫入 context示例選擇器根據(jù)最新用戶輸入從靜態(tài)示例庫中挑選 few-shot 示例最后一起拼入 prompt。緩存方面如果示例庫規(guī)模很大建議給選擇器的檢索結(jié)果加一層 LRU 緩存。因?yàn)橥挥脩舻妮斎胪鶗?huì)在短時(shí)間窗口內(nèi)反復(fù)觸發(fā)相同的向量檢索白做 embedding 計(jì)算非常浪費(fèi)。ManyChat 類的對(duì)話應(yīng)用里我見過把“用戶問題的 embedding 向量”作為緩存鍵的做法命中率可以做到相當(dāng)可觀。要提醒的是緩存鍵如果包含當(dāng)時(shí)的時(shí)間戳緩存就沒有意義了這一點(diǎn)經(jīng)常被忽略。5. 常見問題與排查技巧實(shí)錄5.1 選擇器返回空列表最常見的原因是 NGram 的 threshold 設(shè)得過高所有示例的分都低于閾值。排查方法很簡(jiǎn)單把 threshold 臨時(shí)調(diào)成負(fù)值打印所有示例的得分看看真實(shí)分布再調(diào)整閾值。另一個(gè)原因是字段匹配錯(cuò)誤select_examples 的字典鍵名與 example_prompt 中存在差異導(dǎo)致格式化出來的文本為空計(jì)算任何相似度都得不到有效結(jié)果。這種情況下建議先把傳入的 input_variables 和 example_prompt 直接拼出來打印肉眼檢查一遍。5.2 本地 embedding 模型初始化卡頓使用 HuggingFaceEmbeddings 或類似本地模型時(shí)首次調(diào)用會(huì)觸發(fā)模型權(quán)重下載。生產(chǎn)環(huán)境如果網(wǎng)絡(luò)受限構(gòu)建選擇器時(shí)很容易超時(shí)。解決方式是提前把模型下載到本地緩存目錄或者干脆打包進(jìn)鏡像。我經(jīng)歷過幾次線上服務(wù)冷啟動(dòng)卡在加載詞表和權(quán)重上后來完全離線部署才穩(wěn)定下來。如果你用的是遠(yuǎn)端 embedding 服務(wù)記得給初始化過程加好超時(shí)和重試不能在啟動(dòng)時(shí)一次性全量 embedding 幾萬條示例那會(huì)讓服務(wù)健康檢查直接失敗。5.3 MMR 結(jié)果越選越偏用戶輸入是“運(yùn)動(dòng)耳機(jī)”MMR 卻選出了一條“便攜榨汁杯”和一條“桌面支架”看起來非常離譜。出現(xiàn)這種情況多半是 lambda_mult 設(shè)置太低多樣性懲罰壓過了相關(guān)性。把這組參數(shù)代入公式你會(huì)發(fā)現(xiàn)只要候選與已選樣本相似度很低哪怕它與問題相關(guān)性一般也能拿到較高排名。排查時(shí)先把 lambda_mult 調(diào)到 0.8 以上確認(rèn)結(jié)果回到正常語義軌道再緩慢往下調(diào)找到相關(guān)性和多樣性的平衡點(diǎn)。每次調(diào)整都要結(jié)合真實(shí) prompt 效果去看不要只看向量距離。5.4 中文下 NGram 得分失靈英文按空格分詞中文沒有天然詞邊界這是 NGram 在中文場(chǎng)景最常見的坑。默認(rèn)的 ngram_delimiter 設(shè)成空格時(shí)中文整句話被當(dāng)成一個(gè)巨型 n-gram 塊重疊率往往趨近于 0。我的做法是把 n 設(shè)為 2 或 3、ngram_delimiter 設(shè)為空字符串讓它按連續(xù)字滑窗。還有一個(gè)小技巧做 n-gram 之前先去掉標(biāo)點(diǎn)和助詞避免把“的、地、得、了”這類高頻字帶進(jìn)特征。這個(gè)預(yù)處理對(duì)中文場(chǎng)景的得分穩(wěn)定性提升非常明顯。5.5 向量庫里的“幽靈示例”這個(gè)問題我遇到過兩次。第一次是往 Similarity 選擇器里 add_example 后新示例在檢索結(jié)果中始終不出現(xiàn)。翻源碼發(fā)現(xiàn)部分版本的 add_example 只更新了內(nèi)部示例列表沒有同步更新向量庫。所以項(xiàng)目里如果存在動(dòng)態(tài)追加樣本的需求建議不要用選擇器實(shí)例自帶的 add_example而是自己維護(hù)示例庫源表每次追加后重新構(gòu)建選擇器或直接操作向量庫寫入向量索引。第二種“幽靈示例”是反方向的選擇了持久化向量庫但向量庫里殘留著歷史舊示例刪除時(shí)沒有同步清理導(dǎo)致檢索結(jié)果里出現(xiàn)已經(jīng)廢棄的樣本。定期的向量庫重建成全量任務(wù)但我建議在關(guān)鍵業(yè)務(wù)節(jié)點(diǎn)上至少保留一條全量重建的腳本路徑。最后再分享一個(gè)我自己用的套路項(xiàng)目初期不要急著上 MMR。先用 Length 把 prompt 壓到可控范圍效果差再升級(jí)到 Similarity只有當(dāng)你明確觀察到“選出來的示例重復(fù)度高、彼此覆蓋”的時(shí)候再引入 fetch_k 和 lambda_mult 去調(diào) MMR。這條升級(jí)路徑不是越復(fù)雜越好而是按需添加復(fù)雜度。另外NGram 在離線環(huán)境里的價(jià)值被大多數(shù)人嚴(yán)重低估它給無法使用向量化手段的場(chǎng)景提供了一個(gè)可靠、可復(fù)現(xiàn)、可解釋的 baseline。離線跑通一版再跟在線語義方案做結(jié)果對(duì)比往往能幫你發(fā)現(xiàn)真正影響效果的到底是不是示例選擇策略本身。