我當時腦子一熱,增加權重 all_candidates[content]['hit_count'] += 1 all_candidates[content]['best_score'] = max( all_candidates[content]['best_score'],为部 hit.score ) # 綜合評分 :命中次數 * 最高得分 candidates = list(all_candidates.values()) for c in candidates: c['combined_score'] = c['hit_count'] * c['best_score'] candidates.sort(key=lambda x: x['combined_score'], reverse=True) return candidates[:top_k]
查詢擴展這招特別好用 。但收獲也很大 。记录
二 、大模到成的踩邊生成邊輸出 retrieved_docs = await asyncio.to_thread( self.retriever.retrieve_with_rerank,实战 self.collection_name, question, 20, 5 ) prompt = PromptBuilder.build(question, retrieved_docs, question_type=auto) # 使用流式API stream = self.llm_client.chat.completions.create( model=self.llm_model, messages=[{ role: user, content: prompt}], temperature=0.3, stream=True # 開啟流式 ) for chunk in stream: if chunk.choices[0].delta.content: yield chunk.choices[0].delta.content
流式輸出這一點特別重要 。附上這套係統目前的从被一些核心指標:
- 日均查詢量:200+次
- 平均響應時間:2.3秒(開啟流式後首字符延遲約0.8秒)
- 用戶滿意度(通過回答後的點讚/點踩收集):約72%
- 無法回答的比例:約22%(這部分會定期分析 ,## 必須遵守的骂不门規則{ cls.CORE_INSTRUCTIONS}{ f## 回答格式{ extra_instructions} if extra_instructions else }## 參考資料{ context}## 用戶問題{ query}請回答 : return prompt @classmethod def _classify_question(cls, query: str) -> str: 簡單的問題分類(基於關鍵詞) procedure_keywords = [怎麽做, 如何操作, 步驟, 流程, 怎樣] concept_keywords = [是什麽, 什麽是, 定義, 解釋, 區別] troubleshoot_keywords = [為什麽, 報錯, 失敗, 異常, 問題, 故障] query_lower = query.lower() if any(kw in query_lower for kw in procedure_keywords): return procedure elif any(kw in query_lower for kw in concept_keywords): return concept elif any(kw in query_lower for kw in troubleshoot_keywords): return troubleshoot else: return general @classmethod def _format_context(cls, context_docs: list) -> str: parts = [] for i, doc in enumerate(context_docs, 1): source = doc.get('context_path', '未知來源') parts.append(f【資料{ i} ,確認 Seconds_Behind_Master = 02.3 停止從庫複製STOP SLAVE;RESET SLAVE ALL;## 3. 回滾方案如果切換失敗 ,靠谱坑全現在這套係統已經成了部門的为部標配工具,按標題
、记录我還想分享一個很重要的大模到成的踩經驗:不要試圖在一個Prompt裏塞太多指令
。要專業、
class PromptBuilder: Prompt構建器:根據問題類型動態調整 # 核心指令——任何情況都必須包含 CORE_INSTRUCTIONS = 1. 隻使用參考資料中的信息回答 ,支持手動觸發單篇重入庫 - 用戶提問時 ,
四 、優化指令可以根據問題類型動態調整 。也可以換成其他的) self.llm_client = OpenAI(base_url=llm_base_url, api_key=llm_api_key) self.llm_model = llm_model self.collection_name = knowledge_base def ingest_documents(self, documents: List[Dict]): 文檔入庫 documents格式 :[{ title: 文檔標題, content: 文檔內容, source: 來源}] print(f開始處理 { len(documents)} 篇文檔...) all_chunks = [] for doc in documents: # 在內容前加上標題,做起來挺麻煩的。直接把所有文檔切成小塊。隻輸出類別名稱:- knowledge_query:查詢知識庫信息(如詢問流程、如果前麵的切分和檢索做得不好,用Milvus做向量數據庫。強製切分(但盡量在段落邊界) content_so_far = '\n'.join(current_content) if len(content_so_far) > self.max_chunk_size: chunk_text = content_so_far.strip() chunks.append({ 'content': chunk_text, 'headers': dict(current_headers), 'context_path': self._build_context_path(current_headers) }) current_content = [] # 別忘了最後一段 if current_content: chunk_text = '\n'.join(current_content).strip() if len(chunk_text) >= self.min_chunk_size: chunks.append({ 'content': chunk_text, 'headers': dict(current_headers), 'context_path': self._build_context_path(current_headers) }) return chunks def _build_context_path(self, headers: Dict) -> str: 構建層級路徑 ,用戶檢索到的可能是過時信息 。
三、
這個問題說起來簡單,來源:{ source}】\n{ doc['content']}) return \n\n\n\n.join(parts)
五 、生成多個變體 ,心理上就不會覺得那麽慢了 。選好適用場景
RAG適合有明確知識庫 、要求標注來源 # 格式化上下文,這是讓大模型幫寫代碼。跟我們公司的實際流程八竿子打不著 。老員工翻文檔找答案,
後來我改成了基於語義結構的切分策略 :
import refrom typing import List, Dictclass SmartDocumentSplitter: 語義感知的文檔切分器 核心思路 :尊重文檔的原有結構 ,後來的解決辦法:
- 上線前先梳理高頻問題,接下來就是把檢索到的內容和用戶問題一起喂給大模型了。讓它基於這些材料生成答案
聽起來不複雜對吧?我當時也是這麽想的 ,但它有兩個致命弱點:
第一,2. 如果參考資料中沒有相關信息 ,把一些相似度尚可的文檔標題列出來 ,它給你返回一堆包含服務器的文檔,專門幫助員工查找和理解公司內部文檔。結果第一版上線三天就被罵下來了——用戶問我們的MySQL主從切換流程是什麽,
有人問 :幫我寫個SQL 。要標注來源 、而是會基於它學過的通用知識,
引導用戶自己去看
七、後來的故事還算圓滿。## 參考資料{ context}## 對話曆史{ history_text if history_text else (這是對話的開始)}## 當前問題用戶:{ query}## 回答準則1. 優先根據參考資料回答,轉成向量存起來
教訓三:文檔更新的同步問題
知識庫的文檔是會更新的 。有問題歡迎評論區交流!## 你的工作準則1. **隻根據提供的參考資料回答問題**,## 參考資料{ context}## 用戶問題{ query}## 回答要求請根據上述參考資料回答用戶問題。做創意),
有一次用戶問:MySQL切換前需要做哪些檢查?係統返回的文檔片段是這樣的:
確認沒有正在執行的大事務- 通知相關業務方 ,知識有截止日期。限製回答範圍、用於調試 }) # 第二階段
:重排序 rerank_scores = self._compute_rerank_scores(query, [c['content'] for c in candidates]) for i, score in enumerate(rerank_scores): candidates[i]['rerank_score'] = score # 按重排序分數排序 candidates.sort(key=lambda x: x['rerank_score'], reverse=True) return candidates[:final_top_k] def _compute_rerank_scores(self, query: str, documents: list) -> list: 計算query和每個文檔的相關性分數 scores = [] with torch.no_grad(): for doc in documents: # Reranker的輸入格式是 [query, document] inputs = self.reranker_tokenizer( [[query, doc]], padding=True, truncation=True, max_length=512, return_tensors='pt' ) outputs = self.reranker_model(**inputs) score = outputs.logits.squeeze().item() scores.append(score) return scores def retrieve_with_query_expansion(self, collection_name: str, query: str, llm_client, top_k: int = 5): 進階技巧
:查詢擴展 用大模型改寫用戶問題,更坑的是,給你編一個看起來很合理但其實是錯的答案
。文檔的持續更新
、也能知道它屬於哪個章節 context = f[文檔路徑:{ chunk['context_path']}]\n\n return context + chunk['content']# 實際使用示例splitter = SmartDocumentSplitter(max_chunk_size=800)chunks = splitter.split_markdown(sample_text)print(f切分後共 { len(chunks)} 個片段\n)for i, chunk in enumerate(chunks): print(f=== Chunk { i+1} ===) print(f路徑:{ chunk['context_path']}) print(f內容預覽 :{ chunk['content'][:150]}...) print()
這樣切出來的效果就好多了 。每行輸出一個改寫結果 ,要簡潔 、請指出差異並說明各自的適用場景。
RAG(Retrieval-Augmented Generation,重排序、確認切換時間窗口 ## 2. 切換步驟 ### 2.1 在主庫執行隻讀設置 SET GLOBAL read_only = 1; ### 2.2 等待從庫完全同步 在從庫執行 SHOW SLAVE STATUS,要友好、讓大模型理解上下文 context_parts = [] for i, doc in enumerate(context_docs, 1): source = doc.get('context_path', '未知來源') context_parts.append(f【資料{ i},但實際上 ,確保至少這些問題能回答好
起因是這樣的——我們部門負責維護一套內部知識庫係統 ,架構設計、真正相關的那篇可能隻排在第3或第4位 ,overlap=50 ,
3. Prompt工程真的是門手藝
同樣的檢索結果,
後來我的做法是 :區分核心指令和優化指令,段落等語義邊界切分 def __init__(self, max_chunk_size=800, min_chunk_size=100): self.max_chunk_size = max_chunk_size self.min_chunk_size = min_chunk_size def split_markdown(self, text: str) -> List[Dict]: 針對Markdown文檔的切分 保持標題層級結構 ,
幾點核心總結 :
1. RAG不是萬能的 ,它不知道你們公司上周發布的新規範,全部加起來可能要好幾秒 。最終穩定下來的Prompt是這樣的 :
def build_rag_prompt(query: str, context_docs: list, include_sources: bool = True) -> str: 生產環境使用的Prompt模板 關鍵設計
:明確角色定位、用戶體驗就很差,期間又踩了不少坑 ,會一本正經地胡說八道
。不要編造2. 資料中沒有的信息
,先根據問題檢索出最相關的文檔片段看起來沒毛病是吧?但實際用起來問題大了 。設置chunk_size=500,

最後,對於那些格式不規範的老文檔(沒有清晰的標題結構) ,但內容完全是它自己編的 !——這根本不是知識庫問答,可能就漏掉了最關鍵的信息。識別標題和內容 lines = text.split('\n') current_content = [] for line in lines: # 檢測Markdown標題 header_match = re.match(r'^(#{ 1,3})\s+(.+)$', line) if header_match: # 遇到新標題 ,明確說無法找到相關信息3. 標注信息來源【資料X】 # 操作類問題的額外指令 PROCEDURE_INSTRUCTIONS = 回答格式要求:- 按步驟編號列出(第一步 、問一個問題要等半天。比如用戶問數據庫掛了怎麽辦 ,希望這篇文章能幫你少踩一些坑。
from transformers import AutoModelForSequenceClassification, AutoTokenizerimport torchclass EnhancedRetriever: 增強版檢索器 :向量檢索 + 重排序 def __init__(self, vector_store: VectorStore): self.vector_store = vector_store # 加載重排序模型(BGE Reranker效果不錯) self.reranker_tokenizer = AutoTokenizer.from_pretrained('BAAI/bge-reranker-base') self.reranker_model = AutoModelForSequenceClassification.from_pretrained('BAAI/bge-reranker-base') self.reranker_model.eval() def retrieve_with_rerank(self, collection_name: str, query: str, initial_top_k: int = 20, final_top_k: int = 5): 兩階段檢索 : 1. 向量檢索召回 initial_top_k 個候選 2. 用重排序模型精排,但向量相似度可能並不高。用詞差異很大。方便持續優化 eval_prompt = f請評估以下問答的質量。答案可追溯的場景。讓它照著資料回答 。先幫它把參考資料找出來,格式如【資料1】,用戶的各種奇葩輸入 、還有人問 :在嗎?——我也不知道他想幹啥。關鍵詞匹配的那種 ,回答的結構不夠清晰 。第一條檢查項確認從庫同步狀態正常被切到了上一個chunk裏。當參考資料裏確實沒有答案時
,
教訓一:用戶的問題千奇百怪
我們在設計時假設用戶會問MySQL怎麽做主從切換這種正常問題。
問題二:沒有引導大模型說明信息來源。下一步就是向量化和檢索了。領導在季度會上還專門表揚了一回 。
這就是所謂的大模型幻覺問題,3. 回答時請標注信息來源,我花了將近三周時間重構了整個方案,已經是質的飛躍了 。為啥?因為搜索太爛了
,我發現了一個讓人抓狂的現象——用戶的口語化提問和文檔的正式表述之間存在巨大的語義鴻溝
