AI Agent 開發特訓營:短期實現智能自動化
在企業追求數位轉型的當下,智能 Agent 已成為提升營運效率的關鍵技術。本次特訓營專注於 AI Agent 開發,迅速帶您掌握從 LLM 到自動化 Agent 的完整技術,幫助企業打造真正能解決實際
內容簡介
作者介紹
適合人群
你將會學到什麼
購買須知
-
第一章節 LLM背後的架構 -transformer
介紹transformer歷史與架構
-
AI Agent第一堂直播內容
-
課程大綱
生成式AI實戰精修班:從理論到企業級應用 為什麼應該要學 LLM based AI Agent Application? 🚀 為什麼你應該學 LLM-Based AI Agent?— 迎接 AI 變革時代! 隨著 GPT-4、Claude 3、Gemini 1.5 等大語言模型(LLM)的快速發展,AI Agent(智能代理) 已經不只是個概念,而是正在顛覆 商業應用、軟體開發、知識管理、決策支持 等領域。 你是否還停留在傳統的 AI 思維? 👉 傳統 AI 主要透過 規則、特定演算法、固定流程 來解決問題,但這樣的系統通常很難靈活適應新的環境。 👉 LLM-Based AI Agent 則能夠自主學習、適應不同場景,並且透過「類人思維」來執行複雜任務,成為真正的 智能助手。 🔍 那麼,為什麼你應該學習 LLM-Based AI Agent 建構? 讓我們從幾個核心優勢來分析: 1️⃣ LLM-Based AI Agent vs. 傳統 AI:優勢在哪裡? 傳統 AI 🏛 LLM-Based AI Agent 🚀 學習成本 需要大量標註數據、特定演算法訓練 LLM 已經訓練好,可以直接透過 Prompt Engineering 和微調來適應不同任務 適應能力 只能解決特定問題,變化時需重新開發 可根據不同情境快速調整,能夠處理多種任務 推理能力 依賴明確規則與結構化數據 可透過 Chain-of-Thought、Tree-of-Thoughts 進行類人推理 時間成本 需要花費大量時間設計特定規則 能夠快速迭代、優化任務執行策略 迭代速度 每次修改都需要重新訓練模型 透過 Prompt 設計或 API 調整,即時變更行為 應用場景 主要用於特定領域(如影像辨識、推薦系統) 廣泛應用於客服、數據分析、軟體開發、決策支持等領域 開發工具 需使用 TensorFlow、PyTorch 等框架 可直接使用 LangChain、LlamaIndex、AutoGPT、LangGraph 等工具 總結來說,LLM-Based AI Agent 提供了更靈活、更智慧、更高效的解決方案,並且能夠幫助開發者 大幅降低 AI 系統開發成本。 2️⃣ 為什麼現在就是學習 AI Agent 最好的時機? ✅ 市場需求爆炸式增長:企業正大規模採用 AI 來提升生產力,AI Agent 需求激增。 ✅ 技術門檻下降:過去你可能需要懂深度學習,但現在透過 LangChain、LlamaIndex、AutoGPT,你已經可以輕鬆建構 AI Agent。 ✅ 超強個人生產力提升:你可以訓練一個 AI 代理人來幫你處理日常任務,如自動回覆郵件、寫程式、分析數據等。 ✅ 開放生態系統發展迅速:Hugging Face、OpenAI API、Anthropic API 等提供現成的 LLM,可以直接用來開發 AI Agent。 如果你現在開始學習,你將站在這場 AI 革命的最前線! 3️⃣ 你需要掌握哪些工具來建構 AI Agent? 🔹 LangChain - 最流行的 AI Agent 框架,幫助 LLM 連接不同數據來源、記憶、決策。 🔹 LlamaIndex - 最適合用來做 RAG(檢索增強生成),提升 AI 的查詢與回應能力。 🔹 AutoGPT / BabyAGI - 讓 AI 自己決定接下來要做什麼,打造完全自主的 AI Agent。 🔹 LangGraph - 用於構建多代理系統(Multi-Agent),能夠讓多個 AI 協作完成複雜任務。 🔹 FastAPI + OpenAI API - 如果你要把 AI Agent 部署成 Web 服務,這是最簡單的方式。 🔹 Vector Databases(如 Qdrant, Pinecone, Weaviate) - 幫助 AI 記住長期資訊,提升回應準確性。 這些工具的結合,將讓你 快速開發、測試、部署 AI Agent,並將它應用到 真實世界的問題中。 4️⃣ LLM-Based AI Agent 的核心應用場景 🔹 智慧客服:建立 AI 客服,24/7 回答客戶問題,減少人力成本。 🔹 AI 自動化工作流:讓 AI 代理人幫助你處理數據分析、文件整理、報告生成等工作。 🔹 AI 研究助手:讓 AI 幫你總結論文、提取關鍵資訊,提高研究效率。 🔹 金融分析 & 投資決策:讓 AI 幫你分析市場趨勢、推薦投資機會。 🔹 軟體開發 & 代碼生成:透過 AI 幫助撰寫、優化程式碼,大幅提升開發效率。 🔹 AI 自主決策系統:如 AI 交易系統、AI 招聘助手、AI 產品推薦引擎。 這些應用正在快速滲透各個行業,早學習早受益! 🚀 如何開始你的 AI Agent 學習之旅? 1️⃣ 掌握 LLM 基礎(學習 Prompt Engineering, OpenAI API, Hugging Face) 2️⃣ 學習 AI Agent 框架(LangChain、LlamaIndex、AutoGPT、LangGraph) 3️⃣ 動手實作 AI Agent(從簡單的聊天機器人開始,再進階到 RAG 或多代理系統) 4️⃣ 應用到真實專案(搭建一個幫助你提高生產力的 AI 助理,或優化企業流程) 5️⃣ 持續關注技術發展(AI 進步極快,時刻學習新技術,保持競爭力!) 📢 結論:現在開始學習,未來就是你的! 🌟 LLM-Based AI Agent 是人工智慧領域最具變革性的技術之一,它正在徹底改變我們與 AI 的互動方式。 🌟 現在學習這項技術,將讓你在未來的 AI 就業市場中立於不敗之地! 🌟 不管是提升個人生產力,還是打造智能企業解決方案,AI Agent 都是未來趨勢,你不能錯過! 👉 立即開始學習!讓 AI 代理人成為你的超級助理,讓你在這場 AI 革命中領先一步! 🚀 預計30小時密集課程,打造您的AI技術核心競爭力 課程大綱 Day 1:LLM基礎與RAG入門 Hour 1: LLM基礎架構與原理 LLM發展史 Transformer架構詳解 模型評估與優化 Hour 2: Prompt Engineering進階技術 基礎提示技術 Chain-of-Thought設計 提示優化技巧 Hour 3: RAG基礎與實作 RAG基礎概念 文檔處理與索引 基礎檢索策略 Day 2:RAG進階技術實戰 Hour 4: 進階檢索技術 HyDE技術詳解 Fusion檢索策略 自適應檢索 Hour 5: 上下文優化與壓縮 上下文增強技術 壓縮技術 分塊策略優化 Hour 6: 可靠性與可解釋性 可靠性增強 重排序機制 可解釋性分析 Day 3:AI Agent系統開發 Hour 7: 基礎Agent架構與開發 Agent基礎框架 記憶增強機制 自我提升機制 Hour 8: 專業領域Agent開發 學術任務Agent 客服支援Agent 數據分析Agent Hour 9: 多Agent協作系統 協作系統架構 團隊協作案例 特殊應用場景 Day 4:自動化數據獲取與處理 Hour 10: FireCrawl爬蟲框架 FireCrawl基礎架構 高級爬蟲策略 數據存儲優化 Hour 11: 智能數據處理 Jina框架整合 LlamaIndex集成 重排序機制 Hour 12: 特殊數據源處理 YouTube數據獲取 電商數據處理 自動化搜索系統 Day 5:企業級應用整合 Hour 13: 企業級智能助理系統 災害管理系統 智能商業助手 內容智能 Hour 14: 創意內容生成 智能寫作系統 視覺創意生成 播客內容製作 Hour 15: 企業應用整合 項目管理系統 銷售分析系統 系統監控與分析 課程亮點總結:為什麼選擇這門課? 🚀 從0到企業級應用的完整旅程 5天密集課程,從LLM基礎到企業級應用的全方位培訓 每個概念都配備實戰代碼,確保學以致用 完整項目實戰,建立個人作品集 💡 最新技術棧整合 整合RAG、Agent、爬蟲等熱門技術 使用業界領先框架:LangGraph、LlamaIndex、Jina 實戰企業級應用開發 🛠️ 實用性極強的技能培養 15個真實場景應用開發 50+實戰練習和案例分析 完整的代碼庫和開發文檔 🌟 差異化競爭優勢 獨特的多Agent協作系統開發經驗 企業級應用架構設計能力 自動化數據處理完整解決方案 📈 職業發展加速器 掌握AI領域最搶手的技能組合 建立個人技術競爭力 對接企業實際需求 💪 強大的學習支持 詳細的課程講義和代碼示例 實戰項目指導 技術問題解答 🎯 適合人群 想要轉型AI開發的工程師 需要提升技術棧的AI從業者 希望掌握企業級AI應用開發的技術主管 立即加入我們,開啟您的AI技術進階之旅!在這個AI快速發展的時代,讓我們一起構建下一代智能應用,創造無限可能!
-
LLM基礎架構與原理-Transformer架構
深入探討大型語言模型(LLM):解碼AI語言的未來 在人工智慧(AI)領域,大型語言模型(Large Language Models,LLM)正引領著一場語言理解與生成的革命。這些模型透過深度學習技術,特別是基於Transformer架構的神經網路,展現了前所未有的語言處理能力。本文將深入探討LLM的核心技術、發展歷程、應用場景,以及面臨的挑戰。 什麼是大型語言模型(LLM)? 大型語言模型是透過在海量文本資料上訓練而成的深度學習模型,旨在理解、生成和推理人類語言。它們能夠自動產生連貫且有意義的文本,回答問題,進行機器翻譯,甚至創作詩歌和故事。這些能力使得LLM在自然語言處理(NLP)領域中扮演著關鍵角色。 LLM的關鍵技術:Transformer架構 Transformer架構是LLM的核心技術之一。由Vaswani等人在2017年提出的Transformer模型,透過自注意力機制(Self-Attention Mechanism),有效地捕捉文本中不同詞彙之間的關聯性。這使得模型能夠理解長距離的語境依賴,並提高訓練效率。 代表性的大型語言模型 GPT系列(Generative Pre-trained Transformer) 由OpenAI開發的GPT系列模型,特別是GPT-3,以其強大的文本生成能力而聞名。GPT-3擁有1750億個參數,能夠在少量範例甚至無範例的情況下完成各種語言任務,如寫作、問答和程式碼生成。 Reference :https://baijiahao.baidu.com/s?id=1784331347640868625 說到多少個參數就想提到一點(補充): “”“ 是的,現在許多 80B(80 billion parameters)級別的模型 在性能上都超越了 GPT-3(175B)。這主要是因為模型架構、訓練數據、推理優化、激活稀疏性等技術的進步,使得較小的模型能夠達到甚至超越更大模型的表現。 為什麼 80B 級別模型能比 GPT-3(175B)強? 1. 更高品質的數據: • 訓練數據的過濾、去重與清理,使模型能夠在相對較小的參數規模下學習到更有效的知識。 • 混合專家模型(MoE, Mixture of Experts) 技術能夠讓模型專注於不同的領域,提高推理效率。 2. 更好的架構設計: • 例如 DeepMind 的 Chinchilla(70B)和 Google DeepMind 的 Gemini 1.5 Pro(80B MoE),在設計上優於 GPT-3,實現更好的效能。 • SFT(Supervised Fine-Tuning)與 RLHF(Reinforcement Learning from Human Feedback) 的改進,使得新一代 80B 級別模型在對話、推理和專業知識方面更強。 3. 計算優化: • 更高效的注意力機制(FlashAttention、Grouped Query Attention) 使得 80B 級別模型能夠在計算資源相對有限的情況下達到更好的推理效能。 • 例如 Mistral 7B 和 Mixtral 8x7B(MoE, 80B 激活 12B) 在某些 NLP 任務上已經比 GPT-3 更強。 4. 更好的對齊(Alignment): • GPT-4、Claude 3、Gemini 1.5、Mistral 系列 等模型都採用了更強的對齊技術,使得他們在推理、長文本處理和常識問答上遠超 GPT-3。 目前比 GPT-3(175B)更強的 80B 級別模型 模型名稱參數量特點與優勢Claude 3 Opus約 >100B(未知)在大多數基準測試上超越 GPT-4Gemini 1.5 Pro80B MoE(激活 16B)超強的長上下文能力(>1M tokens)Mistral 8x7B80B(激活 12B)主要用於高效推理,優化對話性能DeepSeek V267B訓練數據更廣泛,在多語言和編程任務上表現出色GPT-4 Turbo?(未公開)效能優化,推理速度比 GPT-4 更快且強 總結 「大 ≠ 強」,GPT-3 雖然參數多達 175B,但訓練技術相對落後。而現今的 80B 級別 模型透過 更優的架構、更好的數據、更強的計算優化,在推理、對話、寫作、程式碼等任務上已經全面超越 GPT-3。 未來的趨勢很可能是: • 小而精的模型(如 7B、13B、30B) 透過 MoE 或知識壓縮,達到與 GPT-3 同等甚至更強的表現。 • 大而強的模型(如 Claude 3、GPT-5、Gemini 2) 持續提升推理能力,進入 AGI 階段。 所以 一個「精調過的 80B」已經能打敗 GPT-3(175B),而現今的 GPT-4、Claude 3 這些「超大模型」更是遠遠超越 GPT-3! ”“” BERT系列(Bidirectional Encoder Representations from Transformers) Google推出的BERT模型專注於語言理解任務。與GPT不同,BERT採用雙向訓練方式,同時考慮上下文資訊,對於問答系統和文本分類等任務表現出色。 此處應插入BERT模型架構圖,強調其雙向編碼器和遮罩語言模型的訓練方式。 BERT 的關鍵技術 遮罩語言模型(Masked Language Model, MLM) 訓練時隨機遮罩(Mask)部分單詞,讓模型學會根據上下文預測被遮擋的詞。 下一句預測(Next Sentence Prediction, NSP) 訓練時提供兩個句子,讓模型判斷它們是否相鄰,幫助學習句子間的關聯。 這些技術使 BERT 在 問答系統、文本分類、命名實體識別(NER)等 NLP 任務 上表現卓越。 BERT 架構圖示意圖 T5模型(Text-To-Text Transfer Transformer) T5是Google提出的統一文本到文本框架,將所有NLP任務轉化為文本生成問題。例如,翻譯任務被表述為「將英文翻譯成中文」。這種統一的視角使得T5在多任務學習中表現優異。 T5:統一的文本到文本框架 T5(Text-To-Text Transfer Transformer)是 Google 提出的 統一 NLP 框架,將所有 NLP 任務視為「文本到文本」的問題。例如: 翻譯:「將英文翻譯成中文:Hello, world! → 你好,世界!」 摘要:「對以下內容生成摘要:...」 問答:「問題:世界上最高的山是什麼? 文章:珠穆朗瑪峰是世界上海拔最高的山。 答案:珠穆朗瑪峰」 這種統一的方法讓 T5 在 多任務學習、文本生成、知識遷移 等領域展現極強的泛化能力。 LLM的應用場景 大型語言模型在各個領域都有廣泛的應用,包括: 對話系統與聊天機器人:如ChatGPT,能夠與使用者進行自然且連貫的對話。 機器翻譯:自動將一種語言翻譯成另一種語言,提升跨語言交流效率。 內容生成:自動撰寫文章、報告,甚至詩歌和故事,輔助內容創作者。 問答系統:根據使用者的問題,從資料庫中檢索並生成準確的答案。 文本摘要:從長篇文章中提取關鍵資訊,生成簡短摘要,方便快速了解內容。 程式碼生成: 代表 - Copilot, Codium. LLM面臨的挑戰 儘管LLM展現了強大的能力,但仍面臨以下挑戰: 高昂的計算資源需求:訓練和部署LLM需要大量的計算資源,對硬體設施要求高。 資料偏見:模型可能從訓練資料中學習到偏見,導致生成的內容存在偏見或歧視。 缺乏解釋性:由於模型結構複雜,其決策過程難以解釋,影響在關鍵領域的應用。 結語 大型語言模型正以前所未有的速度推動著人工智慧的發展。隨著技術的不斷進步,LLM將在更多領域展現其價值,為人類生活帶來深遠的影響。然而,我們也需要正視其面臨的挑戰,確保這項技術以負責任且公平的方式發展。
-
LLM架構與原理完整進階探討
在人工智慧的浩瀚星空中,Transformer架構如同一顆璀璨的新星,為大型語言模型(LLM)的發展帶來了革命性的突破。然而,這顆新星是否真的是驅動LLM進化的核心力量?還是其他因素在背後默默支撐?讓我們一起踏上這段探索之旅,揭開Transformer背後的神秘面紗。 Transformer的魅力與挑戰 Transformer架構自問世以來,憑藉其獨特的注意力機制,讓模型能夠捕捉詞彙之間的複雜關聯,深入理解語言的精髓。這種能力使其在機器翻譯、文本摘要等任務中大放異彩。然而,正如每顆明星都有其陰影面,Transformer也面臨著自身的挑戰。 優勢: 並行計算:不同於傳統的循環神經網路(RNN),Transformer能同時處理序列中的所有詞彙,大幅提升訓練速度。 長程依賴:其注意力機制能有效捕捉長距離詞彙間的關聯,讓模型對長文本的理解更加透徹。 靈活性:透過調整架構和訓練數據,Transformer能適應多種自然語言處理任務。 挑戰: 資源需求:龐大的參數量使得訓練Transformer需要大量的計算資源,這對許多研究機構和企業而言是一大門檻。 解釋性不足:模型的決策過程如同黑箱,難以解釋其內部運作,影響在關鍵領域的應用。 數據依賴:對大量高品質數據的需求,可能導致模型產生偏見或泛化能力不足的問題。 LLM的進化之路:不止於Transformer 雖然Transformer為LLM的發展鋪平了道路,但單靠它並不足以支撐整個架構。模型的性能還取決於訓練數據的質量、參數的調整,以及優化策略的應用。此外,其他技術如高效的編碼器-解碼器架構、知識增強技術等,也在提升LLM的性能和應用範圍中扮演著重要角色。 未來展望:尋找新的可能性 隨著科技的日新月異,研究者們開始探索超越Transformer的替代技術。卷積神經網路(CNN)的改良、圖神經網路(GNN)的應用,以及記憶網路(Memory Networks)的進化,都是潛在的方向。這些新興技術可能帶來更精簡的模型架構,降低訓練成本,同時提升模型的泛化能力。 Transformer如同AI世界中的璀璨明星,為LLM的發展帶來了前所未有的突破。然而,真正的力量源泉在於多種技術的協同作用。未來,我們需要持續探索新的技術,並將其與現有架構相結合,才能推動LLM邁向更高的巔峰。 1. 簡介 (Introduction) 想像有一位博覽群書、懂各種領域的「AI 大師」坐在你面前,無論是中文、英文、法文,或是任何一種語言的疑難雜症,它都能在短短幾秒內為你給出答案。這聽起來是不是很神奇? 其實,這就是所謂的 大型語言模型(LLM, Large Language Model) 的概念。 LLM 通常預先在大量文本上進行訓練,例如 Common Crawl、Wikipedia 等等。它可以用來做文本的摘要、翻譯、生成等任務,也能對多種語言進行處理。它所依託的核心技術包括: 1. Transformer 模型架構:由編碼器(Encoder)與解碼器(Decoder)組成,掌握文字序列與上下文關係。 2. Attention 機制:讓模型能夠捕捉文本中遠距離的依存關係。 3. 自回歸式(Autoregressive)文字生成:模型依據先前產生的詞彙,一步一步生成後續的文字。 有名的 LLM 例子包括:OpenAI 的 GPT-3、GPT-4、Meta 的 LLaMA 2,以及 Google 的 Gemini。這些超強大模型正在顛覆搜索引擎、自然語言處理、醫療、機器人以及程式碼生成等各行各業。 來源: [klu.ai] 1.1 LLM 是如何構 建與訓練的? (How are LLMs built and trained?) 構建和訓練 LLM 是一個繁複的過程,基本流程如下: 1. 資料蒐集:從書籍、網路、社群媒體等取得大量文本資料,並做初步的清洗和格式化。 2. 模型架構:通常採用深度神經網路(Transformer、GPT、BERT…)並配備數十億參數。 3. 訓練:使用強大的運算資源(如 GPU、TPU),透過優化演算法讓模型在大量語料中學習如何「預測下一個字」。 4. 擴充:擴大資料、參數量及運算能力,讓模型逼近人類在語言理解和生成上的表現。 LLMOps 則專注在 部署、監控、維運 LLM,包括模型的版本控制、水平擴充與效能調校等等。 來源: [klu.ai] 1.2 背景:Transformer 之前的世界 (The Pre-Transformer Era) 在 Transformer 橫空出世前,學界常用的主流方法是 RNN(Recurrent Neural Network)以及 CNN(Convolutional Neural Network)。儘管它們在處理序列資料上取得相當成就,卻仍有一些限制性,使得大部分人開始尋覓新的解決方案。 1.2.1 多層感知器 (MLPs) —— 早期深度學習的代表 概述 多層感知器(MLPs, Multi-Layer Perceptrons)是深度學習最基本的模型之一,也是最早期成功的人工神經網路(ANN)架構。MLP 由多個全連接層(Fully Connected Layers)組成,每層神經元透過加權輸入、激活函數(Activation Function)進行變換,最後通過輸出層進行預測。這種架構在圖像分類、語音識別、基本的自然語言處理(NLP)等早期應用中發揮了重要作用。 架構 MLPs 的主要組成部分包括: • 輸入層(Input Layer):接受原始數據輸入(如圖像像素值、文本特徵向量等)。 • 隱藏層(Hidden Layers):通常由多個全連接層組成,每層神經元使用激活函數(如 ReLU、Sigmoid、Tanh)進行非線性變換。 • 輸出層(Output Layer):負責最終的分類或回歸結果輸出。 特性與優勢 1. 強大的非線性建模能力:MLP 透過多層隱藏層與非線性激活函數,能夠學習複雜的映射函數,比單純的線性模型更具表達能力。 2. 適用於結構化數據:MLP 在處理數字特徵、表格數據(tabular data)等結構化數據時表現良好。 3. 普遍適用的基本模型:MLP 是神經網路的基礎架構,可作為許多複雜深度學習模型(如 CNN、RNN、Transformer)的組成部分。 限制與挑戰 儘管 MLP 在許多領域中有不錯的表現,但它存在以下幾個主要問題: 1. 參數量龐大,容易過擬合 • MLP 需要對每一層的所有神經元進行全連接,因此當網路層數增多時,參數量會迅速增長,導致訓練成本提高,並且容易出現過擬合(Overfitting)。 2. 無法有效捕捉序列資訊 • MLP 在每一層中都是獨立的全連接運算,無法保留序列資訊。例如,在自然語言處理(NLP)或時序數據分析(如股市預測)中,MLP 無法學習數據的時間依賴性,這使得它難以處理長序列數據。相比之下,循環神經網路(RNN)或 Transformer 更適合這類任務。 3. 無法有效利用空間結構資訊 • MLP 的全連接層使得其難以直接利用圖像、語音等數據的空間結構。例如,在計算機視覺中,卷積神經網路(CNN)透過局部感受野(Receptive Field)來捕捉局部特徵,而 MLP 則無法直接做到這一點。 MLP 在現代深度學習中的角色 儘管傳統的 MLP 受到上述限制,但近年來一些新的研究重新探索了 MLP 架構的潛力,例如: • MLP-Mixer(2021):利用 MLP 來取代傳統的卷積操作,在某些視覺任務上達到了接近 CNN 和 Transformer 的效果。 • gMLP(2021):透過門控機制(Gated MLP)來增強 MLP 的表達能力,使其能夠在一定程度上捕捉序列資訊。 • FFN(Feed-Forward Network)在 Transformer 中的應用:現代 Transformer(如 GPT-4、BERT)中仍然包含 MLP 結構,作為前饋神經網路(Feed-Forward Network, FFN)的一部分。 結論 MLP 是深度學習發展的基礎,雖然其原始形式在許多應用場景下已被 CNN、RNN、Transformer 取代,但它仍然是許多現代架構的一部分,特別是在處理結構化數據時仍然具有競爭力。此外,近年的研究如 MLP-Mixer 和 gMLP 也讓 MLP 在深度學習領域重新受到關注。 [Credits: deeprevision] 1.2.2 循環神經網路 (RNNs) RNN 則能透過「循環機制」把先前狀態的資訊帶到下一個時間步,理論上可以捕捉長序列依存關係。 [插圖:Credits: experfy] 然而,一旦序列過長,梯度消失 或 梯度爆炸 問題就會變得嚴重。再加上 RNN 需要逐步處理資料,無法同時並行運算,導致在大型資料集上的訓練速度往往緩慢。 1.2.3 長短期記憶單元 (LSTM) LSTM 加入了「記憶體單元(Cell state)」與「門(Gate)」機制,用「選擇性記住或忘記」的方式來改良 RNN 的長序列依存問題。雖然 LSTM 解決了一部分問題,但若序列更長、或需要更高級特徵,依舊會出現 效能不彰的情形。 補充: 長短期記憶網路(LSTM, Long Short-Term Memory)詳解 上面這張圖顯示了一個 LSTM(Long Short-Term Memory) 單元的內部結構,它是一種 改進版的循環神經網絡(RNN),能夠有效解決 長期依賴問題(Long-Term Dependency Problem),在處理序列數據(如語言建模、語音識別、時間序列預測)時表現出色。 1. 為什麼需要 LSTM? 傳統 RNN 的問題: • RNN 主要依賴隱藏狀態(hidden state)來存儲過去的信息,但當序列過長時,舊信息會因 梯度消失(vanishing gradient)或梯度爆炸(exploding gradient) 而逐漸遺失,導致難以學習長距離依賴關係。 LSTM 的解決方案: • LSTM 引入了 記憶單元(Cell State, ) 和 門控機制(Gate Mechanism),透過這些設計來選擇性保留或丟棄信息,使得神經網絡能夠更有效地學習長期依賴。 2. LSTM 的核心結構 LSTM 的核心由以下三個「門」組成: (1) 忘記門(Forget Gate, ) • 負責決定 應該丟棄多少舊資訊,確保不必要的信息不會影響後續學習。 • 公式:(課程就不說那麼複雜) • (Sigmoid 函數):將輸出壓縮到 ,1 表示完全保留,0 表示完全丟棄。 • 和 是學習參數。 (2) 輸入門(Input Gate, ) • 負責決定 應該更新多少新資訊,讓新的輸入資訊影響記憶單元。 • 公式:(課程就不說那麼複雜) • 控制新訊息 如何影響記憶單元。 • 候選記憶資訊: • 是新的候選資訊,範圍在 ,可以存儲新的信息。 (3) 輸出門(Output Gate, ) • 負責決定 該輸出多少資訊到下一個時間步,也就是 當前時間步的隱藏狀態()。 • 公式:(課程就不說那麼複雜) • 決定 記憶單元 的哪些部分要輸出。 • 記憶單元的資訊經過 激活函數處理: • 作為當前時間步的輸出,並傳遞到下一個 LSTM 單元。 3. LSTM 記憶更新流程 (1) 忘記舊資訊 • 會決定是否保留之前時間步的資訊: (2) 記憶新資訊 • 候選記憶資訊 透過輸入門 決定要加進多少新資訊。 (3) 決定輸出 • 由輸出門 控制當前時間步的輸出資訊。 4. LSTM vs. 傳統 RNN 記憶能力 只保留短期記憶 能保留長期記憶 梯度消失問題 嚴重 改善 適用於長序列 否 是 結構 單純的 遞迴 引入 和門控機制 RNN 記憶能力:只保留短期記憶, 改善 適用於長序列:否, 結構:遞迴 LSTM 記憶能力 :能保留長期記憶, 改善 適用於長序列:是, 結構:引入 和門控機制 5. LSTM 的應用 LSTM 目前在許多 序列數據相關應用 中被廣泛使用,包括: • 自然語言處理(NLP): • 文字生成(如 ChatGPT 的基礎模型) • 機器翻譯(Google Translate 早期版本) • 語音識別(Siri、Google Assistant) • 時間序列分析: • 股票市場預測 • 氣象預測 • 音樂生成、影片分析: • 音樂旋律生成 • 影片中物體追蹤 6. 總結 LSTM 是 RNN 的改進版本,透過「忘記門、輸入門、輸出門」來控制信息流動,使其能夠有效地記住長序列的資訊,解決了 RNN 中的梯度消失問題。在 NLP、語音識別、時間序列分析等領域發揮了巨大作用。 🔹 關鍵點回顧: • LSTM 透過 記憶單元 保留長期資訊。 • 三個 門控機制(忘記門、輸入門、輸出門) 控制資訊流動。 • 相較於 RNN,LSTM 解決了梯度消失問題,更適合長序列任務。 這就是 LSTM 的完整解析!🎯 1.2.4 注意力機制 (Attention Mechanism) 在 RNN/LSTM 中引入 注意力機制(Attention),可以讓模型在翻譯或生成時,特別關注(focus)句子中的某些位置。然而,這依舊受制於必須逐字(或逐時間步)處理的限制,平行化程度不佳。 Attention Mechanism補充: 簡單易懂的 RNN/LSTM + 注意力機制解釋 在 RNN 或 LSTM 中,我們希望模型能夠處理 一整段句子或一連串時間序列數據,例如翻譯一段話或根據過去的股市走勢預測未來價格。但 RNN/LSTM 有兩個主要問題: 1. 逐步處理(Sequential Processing):模型必須一個接一個地讀取單詞或數據點,無法一次處理所有資訊,導致訓練與推理速度較慢,難以平行運算(parallelization)。 2. 長距離依賴問題(Long-term Dependency):當輸入的句子很長時,前面的資訊可能會被逐漸遺忘或影響變小,導致模型難以記住重要的內容。 為什麼需要「注意力機制(Attention)」? 想像你在閱讀一篇文章,當你讀到一個句子時,你不會只記住最後一個單詞,而是會根據句子的內容,把重點放在 「對當前理解最有幫助的單詞」 上。例如: 🔹 句子:「貓跳到屋頂上,然後優雅地跳下來。」 🔹 問題:「貓最後在哪裡?」 🔹 如果只記住最後幾個單詞:你可能記得「跳下來」,但不知道「從哪裡跳下來」。 🔹 如果使用注意力機制:模型會特別關注「屋頂」這個詞,因為這與「跳下來」的目標位置有關。 如何在 RNN/LSTM 中使用「注意力機制」? 注意力機制的核心思想是: 👉 在每個時間步(Time Step),讓模型「動態地選擇」哪些過去的單詞/資訊是最重要的,而不是只依賴最後的隱藏狀態。 💡 步驟簡單解釋: 1. 計算關鍵性(Attention Score):對當前輸出來說,過去每個時間步的重要性不同,因此計算「現在這一刻該關注哪些時間步」。 2. 加權求和(Weighted Sum):將重要的資訊加權平均,讓模型能夠更精確地獲取有價值的資訊。 3. 用於預測下一個單詞(或輸出):讓模型能夠根據「最相關的資訊」來做出更準確的決策。 仍然存在的問題: 即使加入了注意力機制,RNN/LSTM 還是必須逐步處理序列(例如翻譯一整段句子時,模型仍需「一個單詞一個單詞地生成」),這導致計算無法完全平行化,處理長句子時仍然會比較慢。 這也是為什麼後來 Transformer(如 GPT、BERT)取代了 RNN/LSTM —— 因為它完全依賴「自注意力機制(Self-Attention)」,可以一次性處理整個序列並且支持高度平行化運算,大幅提升了效率! 🚀 總結 ✅ RNN/LSTM 的問題: • 逐步處理,無法平行化 • 長序列可能會忘記重要信息 ✅ 注意力機制的解決方案: • 讓模型學習「關注」哪些部分比較重要,而不是只依賴最後的隱藏狀態 ✅ 仍然有的問題: • 仍然需要逐步運算,效率不如 Transformer 這也是為什麼 Transformer 取代了 RNN/LSTM,成為現在主流的深度學習架構! 💡 說到那麼多的記憶力機制加上注意力機制,又讓我想要延伸議題思考!! 那現在最火紅的Agent他又是怎麼記憶的?跟LSTM或Transformer的注意力機制好像聽起來有著微妙的關聯?又說不上哪裡有關聯是嗎? 沒關係我來寫一篇延伸思考跟大家討論一下...: 注意力機制(Attention) 與 AI 代理人(Agent) 的長短期記憶確實有關聯,尤其是在當前 AI 自主代理(Autonomous Agents) 越來越受關注的背景下。這不僅是一個技術性的話題,還能夠連結到 如何讓 AI 代理人具備更強的記憶能力與決策能力。如果要寫一個「延伸思考(Extended Thinking)」的部分,這會是一個很棒的方向。 🔍 延伸思考:從注意力機制到 AI 代理人的記憶設計 現代 AI 代理人,如 AutoGPT、BabyAGI、LangGraph Agents,通常需要: 1. 短期記憶(Short-term Memory):即時處理當前的任務,例如對話機器人回應當前的問題。 2. 長期記憶(Long-term Memory):記住過去的交互與經驗,以便未來決策能夠基於先前學習的資訊。 這與 注意力機制在 RNN/LSTM 中的角色相似: • 在 LSTM 中,記憶單元(Cell State, )決定長期資訊的保留與更新,類似於代理人的「長期知識庫」。 • 注意力機制(Attention)則負責動態選擇當前時刻最重要的資訊,類似於代理人每次決策時所「提取」的關鍵知識。 📌 1. AI 代理人的「注意力」:如何選擇關鍵資訊 在 AI 代理人架構中,關鍵問題之一是 如何從大量的記憶中檢索最相關的資訊,這與注意力機制的核心概念一致: • 在 LSTM 內部,注意力機制允許模型「關注」最重要的時間步,而不是僅依賴最終隱藏狀態(h_t ))。 • 在 AI 代理人中,我們希望它能從過去記憶中檢索出最相關的資訊來輔助決策(這與 RAG(檢索增強生成)技術息息相關)。 • 現代 AI 代理人的記憶系統(如 LangGraph)通常結合 向量數據庫(Vector Database) 來檢索過去的對話或行為,類似於 注意力機制在 Transformer 中的 Query-Key-Value 計算。 📌 2. 記憶的「動態更新」:如何平衡長短期記憶 在 LSTM 中,透過「忘記門(Forget Gate)」,模型能夠決定哪些資訊要遺忘,哪些要保留。這與 AI 代理人需要的「記憶管理」非常相似: • 如果 AI 代理人無法遺忘無關資訊,記憶會變得臃腫,影響檢索效率。 • 如果 AI 代理人遺忘太快,它就無法累積知識,影響長期規劃能力。 • 目前的解決方案包括: • 設計「記憶管理策略」,類似 LSTM 的門控機制(例如,LangGraph 讓代理人動態決定何時保留/遺忘某些知識)。 • 使用「檢索增強生成(RAG)」技術,讓代理人能夠檢索最相關的歷史記錄,而非存儲所有資訊。 這使得 AI 代理人能夠像人類一樣: ✅ 記住重要資訊,遺忘不必要細節 ✅ 動態選擇最適合當前決策的知識點 📌 3. 注意力機制與 AI 代理人的「專注力」 另一個有趣的關聯點是:如何讓 AI 代理人「專注於當前的關鍵任務」,而不被過多的記憶干擾? 在 Transformer 架構中,自注意力(Self-Attention) 能夠根據當前的上下文動態調整權重,確保模型不會被無關的資訊干擾。 同樣地,AI 代理人也可以: • 透過動態記憶檢索,確保每次行動都只使用最相關的知識,而不被大量過去資訊影響決策。 • 類似人類的「專注力」,確保 AI 代理人不會因為長期記憶太多而影響短期決策。 這讓 AI 代理人在長期任務(例如自動化研究、持續學習)時更加智能,並能夠根據當前的需求快速適應不同的場景。 📝 結論:從 RNN/LSTM 到 AI 代理人的記憶架構 🔹 LSTM 的記憶單元(Cell State)→ AI 代理人的長期記憶(Knowledge Base) 🔹 注意力機制(Attention)→ AI 代理人的知識檢索(RAG, Vector DB) 🔹 忘記門(Forget Gate)→ AI 代理人的記憶管理策略(Memory Pruning) 🔹 自注意力(Self-Attention)→ AI 代理人的專注力(Focused Task Execution) 👉 AI 代理人的長短期記憶機制,與 LSTM+Attention 有相似的設計哲學,只是應用場景不同。 👉 現代 AI 代理人透過 RAG、LangGraph、記憶管理技術,正在進化成更強大的「具備長短期記憶能力」的自主智能體! 這樣的探討可以作為你文章的 「延伸思考」,讓讀者理解從 RNN/LSTM 到 AI 代理人的技術演進。🔥 1.2.5 卷積神經網路 (CNNs) CNN 可以並行計算,且對區域性資訊敏感,但處理太長的序列(要關注到句子遠端的詞彙)時,仍顯得力不從心。 這時候,Transformer 便應運而生。 1.3 走進 Transformer (Introducing Transformers) Transformer 主打兩個重點: 1. 放棄了 RNN/CNN 的序列性運算,改用 自注意力(Self-Attention)。 2. 訓練可並行化,速度顯著提升。 讓我們先回顧三篇重要的先驅論文: 1. Sequence to Sequence Learning with Neural Networks 2. Neural Machine Translation by Jointly Learning to Align and Translate 3. Attention is All You Need 1.4 為什麼「Attention is All You Need」? (Why “Attention is All You Need”?) 1. 用自注意力取代 LSTM:使得訓練可並行化,大幅加快模型訓練速度。 2. 穩定的架構:加入多頭注意力(Multi-Head Attention)、位置編碼(Positional Encoding)等元素,使模型能不斷擴充。 3. 穩定的超參數:原始 Transformer 以 6 層編碼器及 6 層解碼器在多種 NLP 任務上都極具穩定性,使得後續研究廣泛沿用。 2. Transformer 架構 (Transformer Architecture) Transformer 的核心概念:給定一段文字(Prompt),模型預測下一個最有可能的詞彙。它可同時捕捉近距與遠距的相依關係。 Transformer 採用 「編碼器-解碼器」(Encoder-Decoder) 模式。編碼器負責將輸入序列壓縮成含上下文資訊的向量表示,解碼器根據這些表示一步步生成新的序列(譬如翻譯成另一種語言)。 2.1 嵌入 編碼器是 Transformer 架構的基本組成部分。編碼器的主要功能是將輸入的標記轉換為上下文表示。與早期獨立處理 token 的模型不同,Transformer 編碼器會捕捉相對於整個序列的每個 token 的上下文。 其結構組成如下: 文字輸入被分成更小的單位,稱為標記,可以是單字或子單字。這些標記被轉換成稱為嵌入的數字向量,用於捕捉單字的語義。 2.1.1 輸入嵌入 假設您想使用 Transformer 模型產生文字。您可以新增以下提示:“Data visualization empowers users to”。此輸入需要轉換成模型可以理解和處理的格式。這就是嵌入的作用所在:它將文字轉換為模型可以使用的數字表示形式。要將提示轉換為嵌入,我們需要 對輸入進行標記, 取得 token 嵌入, 添加位置信息,最後 將標記和位置編碼相加以獲得最終的嵌入。 讓我們看看每個步驟是如何完成的。 擴展嵌入層視圖,顯示輸入提示如何轉換為向量表示。該過程涉及(1)標記化,(2)標記嵌入,(3)位置編碼,以及(4)最終嵌入。 嵌入僅發生在最底部的編碼器。編碼器首先使用嵌入層將輸入標記(單字或子單字)轉換為向量。這些嵌入捕獲標記的語義含義並將其轉換為數字向量。 所有編碼器都會收到一個向量列表,每個向量的大小為 512(固定大小)。在底部編碼器中,這將是詞嵌入,但在其他編碼器中,這將是位於它們正下方的編碼器的輸出。 步驟 3:位置編碼 編碼器的工作流程。位置編碼 |圖片來源:Datacamp 圖片來源:Vaclar Kosar 自注意力機制最好的部分是它能夠根據特定詞語所使用的上下文產生動態上下文嵌入。此外,自註意力機制的一個主要優點是它允許並行計算所有上下文嵌入,從而可以快速處理大型文件。然而,這種並行處理有一個明顯的缺點:自注意力模組無法捕捉句子中單字的順序。例如,考慮以下兩句話: 拉維殺死了獅子。 獅子殺死了拉維。 如果您將這兩個句子都經過自我注意力模組,它將無法區分它們,因為它沒有捕捉到單字的順序。 這種限制可能會導致意義上的誤解,因為該模組將包含不同順序的相同單字的句子視為相同的句子。為了解決這個問題,Transformer 架構中引入了位置編碼。位置編碼提供有關句子中單字順序的信息,確保模型理解該序列並保持正確的上下文。 驟 4:最終嵌入 最後,我們將標記和位置編碼相加以獲得最終的嵌入表示。這種組合表示既捕獲了標記的語義含義,也捕獲了它們在輸入序列中的位置。 2.2 編碼器 編碼器層堆疊 編碼器的工作流程。編碼器層堆疊 |圖片來源:Datacamp Transformer 編碼器由一堆相同的層組成(原始 Transformer 模型中為 6 個)。這個數字是透過實驗獲得的,可以為各種任務提供最佳結果。 編碼器層用於將所有輸入序列轉換為連續的、抽象的表示,並封裝從整個序列中學習到的資訊。此層包含兩個子模組: 多頭注意力機制。 一個完全連接的網路。 但是這些區塊如何協同工作呢?編碼器區塊的實際架構包括附加元件,例如添加層和範數層以及殘差連接。這些確保資訊流在通過每個區塊時保持順暢。 輸入資料(通常是一批句子)進入第一個編碼器區塊,經過處理,然後輸出移動到下一個編碼器區塊。這個過程在所有六個編碼器區塊中持續進行,最終輸出被傳遞給解碼器。每個區塊以類似的方式處理數據,使得整個架構高度高效且結構化。 簡而言之,它結合了每個子層周圍的殘差連接,然後進行層規範化。 2.2.1 多頭自註意力 自註意力機制使模型能夠專注於輸入序列的相關部分,從而能夠捕捉資料中的複雜關係和依賴關係。讓我們看看如何一步一步計算這種自我注意力。 查詢、鍵和值矩陣 每個 token 的 embedding 向量轉換成三個向量:Query (Q)、Key (K)、 Value (V)。這些向量是透過將輸入嵌入矩陣與 Q、K 和 V 的學習權重矩陣相乘而得出的。 查詢(Q)是您在搜尋引擎欄中輸入的搜尋文字。這是您想要“查找更多資訊”的令牌。 鍵(K)是搜尋結果視窗中每個網頁的標題。它表示查詢可以關注的可能的標記。 價值(V)是網頁所顯示的實際內容。一旦我們將適當的搜尋字詞(查詢)與相關結果(鍵)匹配,我們就想要得到最相關頁面的內容(值)。 透過使用這些 QKV 值,模型可以計算注意力分數,這決定了每個標記在產生預測時應該獲得多少關注。 第一個自註意力模組使模型能夠從整個序列中捕獲上下文資訊。查詢、鍵和值不是執行單一的注意函數,而是線性投影 h 次。在每個查詢、鍵和值的投影版本上,注意機制並行執行,產生 h 維輸出值。 詳細架構如下: 矩陣乘法 (MatMul) — 查詢與鍵的點積 一旦查詢、鍵和值向量通過線性層,就會在查詢和鍵之間執行點積矩陣乘法,從而建立分數矩陣。 分數矩陣決定了每個單字對其他單字的強調程度。因此,每個單字都會根據同一時間步長內的其他單字被分配一個分數。分數越高,表示關注度越高。 該程序有效地將查詢映射到其對應的鍵。 編碼器的工作流程。注意機制——矩陣乘法。 降低注意力分數的大小 然後透過將分數除以查詢和鍵向量的維度的平方根來縮小分數。實施此步驟是為了確保梯度更穩定,因為值的乘積可能會導致過大的影響。 編碼器的工作流程。降低注意力分數。 |圖片來源:Datacamp 將 Softmax 應用於調整後的分數 隨後,將softmax函數應用於調整後的分數以獲得注意力權重。這會導致機率值的範圍從 0 到 1。 編碼器的工作流程。 Softmax 調整分數。 將 Softmax 結果與值向量結合 注意力機制的下一步是將來自softmax函數的權重與值向量相乘,得到輸出向量。 在這個過程中,只有那些呈現高softmax分數的單字才會被保留。最後,此輸出向量輸入到線性層進行進一步處理。 編碼器的工作流程。將 Softmax 結果與值向量結合。 |圖片來源:Datacamp 最後我們就得到了Attention機制的輸出! 那麼,你可能想知道為什麼它被稱為多頭注意力? 請記住,在整個過程開始之前,我們會將查詢、鍵和值分解 h 次。這個過程被稱為自我注意力,在每個較小的階段或「頭部」中分別發生。每個頭部都獨立發揮其魔力,產生一個輸出向量。 這個集合經過最後的線性層,很像一個微調其集體性能的過濾器。這裡的美妙之處在於每個頭部學習的多樣性,透過強大而多方面的理解豐富了編碼器模型。 2.2.2 歸一化和殘差連接 編碼器的工作流程。多頭注意力機制 (Multi-Head Attention) 後的歸一化與殘差連結。 |圖片來源:Datacamp 編碼器層中的每個子層後面都有一個標準化步驟。此外,每個子層輸出都添加到其輸入(殘差連接)中,以幫助緩解梯度消失問題,從而允許更深的模型。前饋神經網路之後也將重複這個過程。 2.2.3 前饋神經網絡 編碼器的工作流程。前饋神經網路子層。 歸一化殘差輸出的旅程繼續進行,因為它會透過逐點前饋網絡,這是進一步細化的關鍵階段。 將此網路想像為兩個線性層,其中 ReLU 激活層位於它們之間,充當橋樑。一旦經過處理,輸出就會踏上一條熟悉的路徑:它會循環回來並與逐點前饋網路的輸入合併。 這次重聚之後,又將進行新一輪的正常化,確保一切都調整好,為下一步行動同步進行。 2.2.4 編碼器的輸出 最後的編碼器層的輸出是一組向量,每個向量都以豐富的上下文理解來表示輸入序列。然後將此輸出用作 Transformer 模型中解碼器的輸入。 這種仔細的編碼為解碼器鋪平了道路,引導它在解碼時注意輸入中的正確單字。 想像建造一座塔,你可以在其中堆疊 N 個編碼器層。此堆疊中的每一層都有機會探索和學習注意力的不同方面,就像知識層一樣。這不僅使理解多樣化,而且可以顯著增強變壓器網路的預測能力。 2.3 解碼器 解碼器的作用在於製作文字序列。與編碼器類似,解碼器配備了一組類似的子層。它擁有兩個多頭注意層、一個逐點前饋層,並在每個子層之後結合了殘差連接和層規範化。 編碼器的全域結構。 |圖片來源:Datacamp 這些組件的功能類似於編碼器的層,但有一個不同之處:解碼器中的每個多頭注意層都有其獨特的使命。 解碼器過程的最後涉及一個線性層,作為分類器,頂部有一個softmax函數來計算不同單字的機率。 Transformer 解碼器具有專門設計的結構,透過逐步解碼編碼資訊來產生此輸出。 值得注意的是,解碼器以自回歸的方式運行,並以起始標記啟動其進程。它巧妙地使用先前生成的輸出列表作為其輸入,並與來自編碼器的富含初始輸入的注意力資訊的輸出相結合。 這種連續的解碼過程持續到解碼器到達一個關鍵時刻:產生一個標記,標誌著其輸出創建結束。 2.3.1 輸出嵌入 在解碼器的起始行,該過程與編碼器的過程相同。在這裡,輸入首先經過一個嵌入層 2.3.2 位置編碼 嵌入之後,就像解碼器一樣,輸入經過位置編碼層。該序列旨在產生位置嵌入。 然後,這些位置嵌入被引導到解碼器的第一個多頭注意層,在那裡精確計算特定於解碼器輸入的注意分數。 2.3.3 解碼器層的堆疊 解碼器由一組相同的層組成(原始 Transformer 模型中為 6 個)。每層都有三個主要子組件: 1. 掩蔽自註意力機制 這類似於編碼器中的自註意力機制,但有一個關鍵的區別:它防止位置關注後續位置,這意味著序列中的每個單字不受未來標記的影響。 例如,在計算單字「are」的注意力分數時,重要的是「are」不會被序列中的後續單字「you」所影響。 解碼器的工作流程。第一個多頭注意力遮罩。 這種掩蔽確保特定位置的預測只能依賴該位置之前已知的輸出。 2. 編碼器-解碼器多頭注意力或交叉注意力 在解碼器的第二個多頭注意層中,我們看到編碼器和解碼器元件之間的獨特相互作用。在這裡,編碼器的輸出既充當查詢又充當鍵的角色,而解碼器的第一個多頭注意層的輸出則充當值。 這種設定有效地將編碼器的輸入與解碼器的輸入對齊,使解碼器能夠識別並強調編碼器輸入中最相關的部分。 隨後,第二層多頭注意力的輸出將透過逐點前饋層進行細化,從而進一步增強處理。 解碼器的工作流程。編碼器-解碼器注意力機制。 在這個子層中,查詢來自前一個解碼器層,而鍵和值來自編碼器的輸出。這使得解碼器中的每個位置都能專注於輸入序列中的所有位置,從而有效地將來自編碼器的信息與解碼器中的信息整合在一起。 3.前饋神經網絡 與編碼器類似,每個解碼器層都包含一個完全連接的前饋網絡,分別且相同地應用於每個位置。 2.3.4 線性分類器和 Softmax 產生輸出機率 資料通過變換器模型的旅程最終會通過最後的線性層,該層起到分類器的作用。 這個分類器的大小與所涉及的類別的總數(詞彙表中包含的單字數)相對應。例如,在有 1000 個不同類別代表 1000 個不同單字的場景中,分類器的輸出將是一個包含 1000 個元素的陣列。 然後將該輸出引入到 softmax 層,將其轉換為一系列機率分數,每個分數介於 0 和 1 之間。 解碼器的工作流程。 Transformer 的最終輸出。 2.3.5 規範化和殘差連接 每個子層(掩蔽自註意力、編碼器-解碼器注意力、前饋網路)後面都有一個標準化步驟,每個子層還包括一個周圍的殘差連接。 2.3.6 解碼器的輸出 最後一層的輸出被轉換成預測序列,通常透過線性層然後是softmax來產生詞彙的機率。 解碼器在其操作流程中,將新產生的輸出合併到其不斷增長的輸入清單中,然後繼續解碼過程。這個循環重複進行,直到模型預測出一個特定的標記,表示完成。 預測機率最高的標記被指定為結論類,通常由結束標記表示。 再次記住,解碼器不僅限於單層。它可以由 N 層構成,每一層都基於從編碼器及其前一層接收的輸入來建構。這種分層架構允許模型分散其焦點並在其註意力頭中提取不同的注意力模式。 這種多層次的方法可以顯著增強模型的預測能力,因為它對不同的注意力組合有了更細緻的理解。 最終的架構與此類似(來自原始論文) Transformers 的原始結構。 3. LLM 架構 機器學習 (ML) 中的架構是指模型的神經元和層的排列。它就像一個藍圖,概述了模型如何從數據中學習。不同的架構捕捉資料中的不同關係,在訓練期間強調特定的組件。因此,架構會影響模型所擅長的任務及其所產生的輸出的品質。 3.1 Seq-2-Seq 模型(編碼器-解碼器) 圖片來源:Abhinav Kimothi 編碼器-解碼器由兩個元件組成: 編碼器-接受輸入資料並將其轉換為抽象的連續表示,以捕獲輸入的主要特徵。 解碼器-在吸收先前的輸出的同時,將連續表示轉換成可理解的輸出。 編碼和解碼過程使得模型能夠透過更有效的資料表示來處理複雜的語言任務,從而幫助模型做出連貫的回應。 這種雙過程架構在機器翻譯(將同一句話從一種語言轉換為另一種語言)和文字摘要(總結文字中的相同關鍵點)等生成任務中表現出色,在這些任務中,在生成輸出之前理解整個輸入至關重要。但是,由於需要先處理整體輸入,因此推理速度可能會較慢。 LLM 範例: 谷歌的 — T5、Flan-UL2和Flan-T5。 Meta 的BART。 3.2 自動編碼模型(僅限編碼器) 圖片來源:Abhinav Kimothi 流行的 BERT(“用於語言理解的深度雙向變壓器的預訓練”,2018)和 RoBERTa(“一種穩健優化的 BERT 預訓練方法”,2018)等模型使用僅編碼器架構將輸入轉換為豐富的、情境化的表示,而無需直接生成新序列。 例如,BERT 使用兩種創新方法在大量文字語料庫上進行預訓練:掩蔽語言建模 (MLM) 和下一句預測。 MLM 的工作原理是隱藏句子中的隨機標記,並訓練模型根據上下文預測這些標記。透過這種方式,模型可以理解左右上下文中單字之間的關係。這種「雙向」理解對於需要強大語言理解能力的任務至關重要,例如句子分類(例如情緒分析)或填補缺失的單字。 但與可以解釋和產生文字的編碼器-解碼器模型不同,它們本身無法產生長文字序列。他們更注重解釋輸入。 LLM 範例: Google 的BERT和ALBERT。 Meta 的RoBERTa。 微軟的DeBERTa。 3.3 自回歸模型(僅限解碼器) 圖片來源:Abhinav Kimothi 僅解碼器架構根據先前的上下文產生輸入序列的下一部分。與基於編碼器的模型不同,它們無法理解整個輸入,但擅長產生下一個可能的單字。因此,僅解碼器模型在輸出上更加“有創意”和“開放”。 這種逐個標記的輸出生成對於文字生成任務(如創意寫作、對話生成和故事完成)非常有效。 LLM 範例: Open AI 的GPT-4和GPT-3系列。 Google 的PaLM和Bard。 Meta 的LLaMA和OPT。 3.4 專家混合(MoE) Mistral 8x7B 等模型所採用的 MoE 與傳統的 Transformer 模型不同,它建立在這樣一個觀察基礎之上:單一整體語言模型可以分解為更小的、專門的子模型。在模型之間分配任務(例如切換輸入標記)的門控網路協調這些子模型,這些子模型專注於輸入資料的各個方面。 這種方法可以實現擴展(高效計算和資源分配)和多樣化技能,使 MoE 能夠出色地處理具有不同要求的複雜任務。該架構的整個目的是提高 LLM 參數的數量,而不會相應增加計算費用。 那麼,Mistral 8x7B 算是 LLM 嗎?儘管其架構與 transformer 模型存在差異,但由於以下幾個原因,它仍然有資格成為 LLM: 模型大小:其龐大的規模和參數數量(1870 億個參數)使其在複雜性和容量方面可與其他 LLM 相媲美。 預訓練:與其他 LLM 一樣,Mistral 8x7B 透過無監督學習技術在綜合資料集上進行預訓練,從而能夠理解和模仿類似人類的語言模式。 多才多藝:它展現了完成各種任務的熟練程度,展現了法學碩士的廣泛能力。 適應性:與其他 LLM 一樣,Mistral 8x7B 也可以針對特定任務進行微調,從而提高效能。 4. 推理 現在我們已經介紹了 Transformer 的架構並詳細了解了其元件,現在是時候討論如何實際執行推理了。我們已經訓練了我們的變換器模型,現在,在預測階段,架構的行為與訓練期間略有不同。 訓練和推理之間的主要區別: 訓練:訓練 Transformer 模型以學習輸入資料中的模式和關係(例如語言建模、翻譯等)。 推理:使用訓練好的 Transformer 模型進行預測,例如生成文本、翻譯語言或對文本進行分類。 在推理過程中,主要的差異發生在解碼器中。與訓練不同,在訓練中我們已經知道整個輸出句子,並且可以一次將所有標記傳遞給解碼器,使其以非自回歸的方式運行,而在推理過程中,我們沒有完整的輸出句子。因此,解碼器必須以自回歸的方式一次一個單字地產生翻譯。它使用每個先前預測的單字來幫助預測序列中的下一個單字。這個過程持續直到模型產生整個翻譯的句子。 圖片來源:Umar Jamil 推理過程中的資料流是: 輸入序列被轉換為嵌入(帶有位置編碼)並輸入到編碼器。 編碼器堆疊對此進行處理並產生輸入序列的編碼表示。 我們不使用目標序列,而是使用僅帶有句首標記的空序列。這被轉換成嵌入(帶有位置編碼)並輸入到解碼器。 解碼器堆疊將其與編碼器堆疊的編碼表示一起處理,以產生目標序列的編碼表示。 輸出層將其轉換為單字機率並產生輸出序列。 我們將輸出序列的最後一個字作為預測詞。該單字現在被填充到我們的解碼器輸入序列的第二個位置,該序列現在包含一個句子起始標記和第一個單字。 返回步驟#3。和以前一樣,將新的解碼器序列輸入模型。然後取出輸出的第二個字並將其附加到解碼器序列。重複此動作直到預測句子結束的標記。請注意,由於編碼器序列在每次迭代中都不會改變,因此我們不必每次都重複步驟 #1 和 #2 推理流程,在第一個時間步之後。圖片來源:Ketan Doshi 4.1 推理技術 Transformer 可用來推理,方法是每次輸入標記序列。編碼器用於處理輸入序列並產生嵌入序列。解碼器用於每次產生一個標記的輸出序列。解碼器使用編碼器輸出和先前產生的標記來產生下一個標記。 Transformer 可以使用不同的策略進行推理,例如貪婪搜尋和束搜尋。 4.1.1 貪婪搜索 貪婪搜尋是一種簡單的推理策略。在每個時間步驟中,解碼器根據其模型產生具有最高機率的標記。重複此過程直到解碼器產生序列結束標記。貪婪搜尋速度很快,但容易陷入局部最優。 貪婪搜索,來源:d2l.ai 4.1.2 集束搜索 集束搜尋是一種更複雜的推理策略。在每個時間步驟中,解碼器都會產生一束標記,其中束大小是一個超參數。然後,解碼器從光束中選擇前 k 個標記,並繼續從每個標記中產生標記。重複此過程直到解碼器產生序列結束標記。集束搜尋比貪婪搜尋慢,但可以產生更多樣化和準確的輸出。 束搜尋的過程(束大小=2;輸出序列的最大長度=3)。候選輸出序列為A、C、AB、CE、ABD、CED。來源:d2l.ai 還有其他可與 Transformer 一起使用的推理策略,例如採樣和核採樣。這些策略可用於產生更具創造性和多樣化的產出。 5. Transformer 推理優化 Transformer 模型以其自註意力機製而聞名,對於 NLP 和電腦視覺等任務至關重要。推理,即這些模型根據未知資料產生預測的階段,需要大量的計算資源。 影響 transformer 推理的一個關鍵因素是浮點運算 (FLOP) 的數量。每一層都涉及矩陣乘法,對於像 GPT-3 這樣的大型模型,每個 token 的 FLOP 可能高達數萬億。為了減少計算開銷,使用了鍵值(KV)緩存,允許模型重用先前計算的注意向量,從而加快自回歸解碼。 記憶體使用是另一個限制因素,GPT-3 等模型需要超過 200 GB 的記憶體。量化和並行等技術有助於更有效地管理這些資源,但變壓器推理通常仍然受到記憶體的限制,其中記憶體頻寬限制了計算速度。 5.1 Transformer 架構與推理流程 Transformer 的核心架構是基於自註意力機制和一系列堆疊層,每個層包含注意力和前饋網路。在推理過程中,Transformer 應用預先訓練的參數進行預測,通常在 GPT 等自迴歸模型中逐一標記地進行。 推理涉及幾個矩陣運算,特別是每個注意層中的矩陣向量乘法。對於每個新標記,模型透過將輸入嵌入與學習到的權重矩陣相乘來計算查詢(Q)、鍵(K)和值(V)向量。 注意力機制透過將查詢與轉置的鍵矩陣相乘、將結果縮放至維度大小的平方根並應用 softmax 函數來計算相關性分數。這個過程使得模型能夠衡量序列中每個標記的重要性。雖然這些矩陣乘法非常有效,但它們的計算成本很高,特別是在像 GPT-3 或 LLaMA 這樣的大型模型中,其中每個注意力頭每個 token 執行數十億次 FLOP。 5.2 Transformer 推理的階段:預填和解碼 Transformer 推理分為兩個關鍵階段:預先填入和解碼。這些階段決定了模型如何處理輸入標記並產生輸出標記,每個階段對效能有不同的影響。 1. 預填階段:在預填階段,模型並行處理整個輸入序列,將標記轉換為鍵值對。此階段計算密集但高度可並行,從而實現高效的 GPU 利用率。運算主要涉及矩陣-矩陣乘法,從而允許 GPU 同時處理多個標記。 Prefill 在批次方面表現出色,可以同時處理大量數據,從而最大限度地減少延遲。 2. 解碼階段:解碼階段較受記憶體限制且具有順序性,會逐一產生標記。每個新標記都依賴先前產生的標記,需要矩陣向量乘法,與預填充階段的平行特性相比,這會未充分利用 GPU。順序過程引入了延遲瓶頸,使得該階段的速度明顯變慢,尤其是在 GPT-3 這樣的大型模型中。 鍵值快取機制的說明,展示如何儲存先前計算的鍵和值以避免在推理過程中進行重複計算 |圖片來源:PremAI 鍵值(KV)快取是解碼階段的關鍵最佳化。透過儲存先前計算的鍵值矩陣,該模型避免了重新計算,從而將複雜性從二次降低到線性。 鍵值快取機制的圖示,展示了快取值如何減少解碼階段的運算負載 6. Transformer 推理中的挑戰 大型 Transformer 模型,尤其是像 GPT-3 這樣的大型語言模型 (LLM),由於其大小和計算要求,在推理過程中帶來了許多挑戰。這些挑戰圍繞著記憶體限制、延遲以及記憶體限制和計算限制操作之間的平衡。 1. 記憶體和運算需求:推理期間儲存模型權重和鍵值 (KV) 快取需要大量記憶體。像 GPT-3 這樣的大型模型具有 1750 億個參數,通常需要超過 200 GB 的記憶體。此外,KV 快取大小隨著序列長度和批次大小線性增長,進一步增加了記憶體負擔。例如,具有 70 億個參數和 4096 序列長度的 LLaMA 模型僅為 KV 快取就會消耗約 2 GB 的記憶體。 2. 順序令牌產生中的延遲:延遲是一個關鍵問題,特別是在解碼階段,其中令牌一次產生一個。每個新標記都依賴前一個標記,這會導致順序操作無法充分利用 GPU 的運算能力。即使高度最佳化的模型也會受到記憶體頻寬瓶頸的影響,並且隨著序列長度的增加,這種瓶頸問題會變得更加明顯。 3. 平衡批次大小和效能:更大的批次大小可以提高 GPU 利用率,尤其是在預填階段,但它們受到記憶體容量的限制。增加批次大小有助於最大化吞吐量,但僅限於系統受到記憶體限制的程度。除此之外,系統可能會遇到收益遞減的情況,因為記憶體頻寬開始限制進一步的效能提升。 4. 記憶體受限與計算受限操作的權衡: Transformer 推理在記憶體受限操作和計算受限操作之間交替進行。在解碼階段,矩陣向量乘法通常受記憶體限制,而預填矩陣-矩陣運算往往受計算限制。有效優化批次大小、KV 快取管理和精確度(例如 FP16、INT8)對於減少延遲和確保高效的 GPU 使用至關重要。 7. 更快推理的最佳化技術 隨著 GPT-3、LLaMA 和其他大型語言模型 (LLM) 等轉換器模型不斷擴展,最佳化技術對於管理與推理相關的增加的記憶體、計算負載和延遲變得至關重要。透過應用量化、鍵值 (KV) 快取、推測解碼、批次和平行等技術,開發人員可以顯著提高推理效能。 7.1 量化 量化前後的值的分佈,說明了從浮點數到 INT8 的轉換過程中的捨入和異常值剪切過程。 量化降低了模型權重和激活的精度,從而實現了更快的計算和更低的記憶體使用量。模型可以使用 8 位元(INT8)甚至更低的精度,而不是依賴 32 位元或 16 位元浮點精度,從而減少記憶體頻寬並允許模型更有效地處理更大的批次或更長的序列。 記憶體頻寬利用率 (MBU) 公式顯示記憶體頻寬如何限制效能,其中參數數量、每個參數的位元組數和每秒令牌數都會影響推理速度 例如,將 INT8 量化應用於 GPT-3 可以將記憶體需求減少高達 50%,直接導致推理過程中的延遲更低、吞吐量更高。量化對於面臨頻寬限制的記憶體受限模型特別有用。 將模型權重從 FP16 量化為 INT8 可以減少記憶體使用量,並透過降低記憶體頻寬需求來提高推理效能。 7.2 鍵值(KV)緩存 在自迴歸模型中,每個新的標記產生都需要存取所有先前的標記。隨著序列長度的增加,這會導致計算量呈現二次方增加。 KV 快取透過儲存先前標記的鍵和值張量來緩解這種情況,從而允許模型重複使用它們而無需重新計算。 KV 快取的大小隨著 token、layers 和 Attention Heads 的數量線性增長。例如,在 LLaMA 7B 模型中,4096 個標記的序列長度將需要大約 2 GB 的記憶體用於 KV 快取。這種優化顯著減少了解碼階段的計算負荷,提高了速度和記憶體效率。 7.3 推測解碼 推測解碼是一種高級優化技術,透過並行化令牌生成來減少延遲。推測解碼不是等待每個標記按順序處理,而是使用較小的草稿模型來預測前面的幾個標記,並使用主模型驗證預測。如果預測準確,則接受;如果不是,則將其丟棄。 這種方法允許並行執行,從而減少了產生令牌所需的整體時間,同時保持了準確性。它對於即時應用程式(例如聊天機器人)特別有用,因為快速回應時間至關重要。 7.4 批次 批次處理是一種優化變壓器推理的簡單但強大的技術。透過同時處理多個輸入,批次處理可以提高 GPU 的使用率,因為模型權重的記憶體成本在多個請求之間共享。然而,批次受到可用記憶體的限制,特別是在具有長序列的模型中。 傳統批次處理的一個挑戰是,批次內的不同請求可能會產生不同數量的輸出令牌。這可能會導致效率低下,因為所有請求都必須等待運行時間最長的請求完成。為了解決這個問題,正在進行的批次允許系統立即從批次中逐出已完成的請求,從而釋放資源以用於新請求。 7.5 硬體優化:並行性 硬體優化,特別是張量並行和管道並行,對於擴展大型模型至關重要。這些方法將計算負載分配到多個 GPU 上,使得系統能夠處理超出單一 GPU 記憶體容量的模型。 · 張量並行:此技術將模型的參數分散到多個 GPU 上,使它們能夠並行處理相同輸入的不同部分。張量並行對於注意層特別有效,其中不同的注意頭可以獨立計算。 · 管道並行:這種方法將模型分成連續的區塊,每個區塊由不同的 GPU 處理。管道並行減少了每個 GPU 的記憶體佔用,使更大的模型能夠有效運作。但是,在等待來自前幾個階段的資料時,它會在 GPU 之間引入一些空閒時間。 這兩種平行形式對於管理 GPT-3 和 LLaMA 等大型模型都至關重要,因為這些模型的記憶體和運算需求通常超出單一 GPU 的能力。 7.6 FlashAttention 和記憶體效率 另一個關鍵的進步是 FlashAttention,它透過減少資料載入和儲存在記憶體中的次數來優化記憶體存取模式。 FlashAttention 利用 GPU 記憶體層次結構更有效率地執行運算、融合操作並最大限度地減少資料移動。該技術可以減少記憶體浪費並實現更大的批量大小,從而顯著提高速度,尤其是在序列長度較大的模型中。 8. 推理效能基準測試 GPT-3 和 LLaMA 的基準測試說明了這些最佳化的影響。對於 GPT-3,與未優化的模型相比,KV 快取和量化的組合已被證明可以將推理時間減少多達 60%,吞吐量達到每秒數百個代幣。在 LLaMA 中,平行技術的使用確保即使是最大的模型(如 LLaMA 65B)也能保持高吞吐量,同時控制延遲。 這些優化使兩種模型能夠有效擴展,確保它們能夠處理現實世界的應用,從長上下文生成到即時回應,同時顯著減少計算和記憶體需求。 8.1 Transformer 推理的趨勢 隨著 Transformer 模型的規模和複雜性不斷增長,最佳化推理對於滿足實際應用的需求至關重要。下一波創新的重點是有效擴展變壓器模型、改善記憶體管理和利用先進的硬體功能。以下是影響變壓器推理未來的一些最具影響力的趨勢。 8.1.1 使用分頁和 FlashAttention 進行記憶體優化 一個關鍵趨勢是透過 PagedAttention 和 FlashAttention 等技術優化記憶體使用。在目前的推理過程中,模型通常會過度配置記憶體來處理最大可能的序列長度,從而導致效率低下。PagedAttention透過僅在需要時分配記憶體來解決這個問題,將鍵值 (KV) 快取分成更小的區塊並根據需要取得。 FlashAttention透過優化計算順序並減少記憶體和計算單元之間的資料移動進一步提高了記憶體效率。透過融合操作和利用 GPU 記憶體層次結構,FlashAttention 可以顯著減少記憶體浪費並實現更大的批次大小和更快的處理速度。這些進步對於擴展大型模型同時保持高效能至關重要。 8.1.2 多查詢和分組查詢注意 優化注意力機製本身是另一個重要趨勢。多重查詢注意力(MQA)和分組查詢注意力(GQA)是兩種在保持模型效能的同時減少記憶體佔用的變體。在 MQA 中,所有頭共享相同的鍵值對,這樣在保持準確性的同時減少了 KV 快取的大小。 分組查詢注意力 (GQA) 在 MQA 和傳統多頭注意力之間取得平衡,它對分組頭使用共享鍵值對。這種方法在保持高效能的同時進一步減少了記憶體使用量,使其對於像 LLaMA 2 70B 這樣的長上下文模型特別有用。 8.1.3 並行性:張量與序列 並行性仍然是擴展大型模型的核心策略。張量並行將模型層劃分為獨立的區塊,可跨多個 GPU 進行處理,從而減輕單一裝置的記憶體負擔。此方法對於注意力頭和前饋層非常有效,其中並行處理可以顯著提高效率。 vSequence 並行透過在序列維度上拆分 LayerNorm 和 Dropout 等操作進一步提高了記憶體效率。這減少了記憶體開銷,特別是對於長序列任務,並允許模型更有效地擴展。 8.1.4.即時應用的推測推理 對於即時應用,推測推理提供了一種減少延遲的創新方法。透過使用較小的草稿模型來預測未來的多個標記,推測推理可以實現並行執行。然後,主模型會驗證草稿令牌,然後決定接受或丟棄它們。 9.處理大型資料集 在大型資料集上訓練 Transformer 面臨獨特的挑戰,需要採取謹慎的策略: 9.1 高效率的資料載入與預處理 並行資料載入:利用 TensorFlowtf.data或 PyTorch等DataLoader多執行緒框架可以加快資料預處理速度。 混洗和批次:適當的混洗可以防止過度擬合任何特定的資料順序,而批次可以確保高效的 GPU 利用率。 9.2 分散式訓練 資料並行:模型在多個 GPU 上複製,每個 GPU 處理不同的小批量數據,並聚合梯度。 模型並行:模型本身分佈在多個 GPU 上,對於無法放入單一裝置記憶體的大型模型很有用。 9.3 混合精度訓練 利用 16 位元(半精度)浮點運算而不是 32 位元浮點運算可以顯著減少記憶體使用量並加快計算速度,而不會對準確度產生重大影響。 9.4 梯度累積 適用於批次大小受 GPU 記憶體限制的場景。在執行優化器步驟之前,梯度會在多個較小的批次中累積。 9.5 檢查點和恢復 在訓練期間定期保存模型狀態有助於在發生故障時從最後一個檢查點恢復,也可用於提前停止和微調的目的。 9.6 數據增強和採樣 資料增強技術增加了資料的可變性,有助於泛化。 智慧採樣策略確保模型不會過度擬合常見模式,而忽略罕見但重要的模式。 10. 結論 本學習文章深入探討大型語言模型 (LLM) 的開發和最佳化,首先概述其建構和訓練方法。它追溯了從 Transformer 之前模型到 Transformer 的變革性引入的演變,強調了關鍵的「注意力就是你所需要的一切」概念。 這篇的核心剖析了 Transformer 架構,詳細介紹了其嵌入層、編碼器-解碼器互動和自註意力機制等元件。它探索了各種 LLM 架構,包括 Seq-2-Seq、AutoEncoding 和 AutoRegressive 模型,闡明了它們獨特的功能。 對於實際部署,該學習文章研究了推理策略、最佳化技術(如量化、KV 快取、推測解碼和硬體並行性),並提供了 GPT-3 和 LLaMA 等真實範例。效能基準、記憶體最佳化和平行技術凸顯了高效 Transformer 推理的趨勢。 最後,文章討論如何處理大型資料集,討論了高效的資料處理、分散式訓練、混合精度技術和資料增強策略。本綜合指南將深入介紹如何有效地建置、最佳化和部署 LLM。
-
-
章節2 Prompt Engineering
-
AI Agent 開發特訓營:短期實現智能自動化-第二堂
-
AI Agent重要概念入門探討
AI Agent概念探討由淺入深!! Ref: 李宏毅教授 請fork我的存放上靠教材的github Repo:網址:https://github.com/kevin801221/LLMs_Amazing_courses_Langchain_LlamaIndex git clone https://github.com/kevin801221/LLMs_Amazing_courses_Langchain_LlamaIndex.git 下載後進入2024To2025_Agent_paper_docs/agentic-ai-presentation 這個資料夾後跑slide服務 : python -m http.server 8000 就可以run 在8000 porthttp://localhost:8000
-
Playwright 介紹 智能爬文的第一步
colab code: https://colab.research.google.com/drive/1Nu68l_1Xf5Xf7uN2iX_TsIyFtZCbEAKr#scrollTo=rJBQljicLT-B 使用 Playwright 進行網頁自動化:從基礎到實戰 1. Playwright 簡介 Playwright 是由 Microsoft 開發的強大瀏覽器自動化工具,支持 Chromium、Firefox 和 WebKit。相較於 Selenium,它擁有更高效的 API,適用於網頁測試、自動化數據抓取、機器人流程自動化(RPA)等場景。 2. 安裝 Playwright 在使用 Playwright 之前,需要先安裝相關的 Python 套件。 pip install playwright termcolor 接著,安裝 Playwright 的瀏覽器環境: playwright install 3. 簡單的網頁自動化案例 我們將使用 Playwright 瀏覽 https://jina.ai,並抓取頁面標題與特定標籤的內容。 3.1 Python 代碼示例 from playwright.sync_api import sync_playwright from termcolor import colored def run(playwright): # 啟動 Chromium 瀏覽器,非無頭模式(可以看到瀏覽器執行過程) browser = playwright.chromium.launch(headless=False) page = browser.new_page() page.goto("https://jina.ai") # 取得頁面標題 print(f"Page Title is: {colored(page.title(), 'cyan')}" ) # 抓取特定標籤的內容 ud_header = page.locator('.ud-heading-serif-xxl').all_text_contents() for subheader in ud_header: print(f"Jina Subheader: {colored(subheader, 'cyan')}") # 關閉瀏覽器 browser.close() # 使用 Playwright 執行網頁自動化 with sync_playwright() as playwright: run(playwright) 3.2 代碼解析 啟動 Playwright: sync_playwright():同步執行 Playwright。 playwright.chromium.launch(headless=False):啟動 Chromium 瀏覽器,headless=False 表示顯示瀏覽器介面。 打開新分頁並瀏覽網址: page = browser.new_page() 創建新分頁。 page.goto("https://jina.ai") 訪問目標網站。 抓取頁面資訊: page.title() 取得頁面標題。 page.locator('.ud-heading-serif-xxl').all_text_contents() 抓取 .ud-heading-serif-xxl 選擇器對應的所有文字內容。 輸出結果: termcolor.colored(text, 'cyan') 讓輸出更具可讀性。 關閉瀏覽器: browser.close() 釋放資源,確保瀏覽器正確關閉。 4. 可能遇到的錯誤與解決方案 4.1 無法找到特定選擇器 如果 page.locator('.ud-heading-serif-xxl') 找不到對應內容,可能是: 選擇器名稱已更改,需使用開發者工具(F12)檢查 HTML 結構。 目標元素是動態載入的,需使用 page.wait_for_selector(selector) 等待元素加載。 4.2 瀏覽器未安裝 如果執行時遇到類似 Error: browser not installed,請執行: playwright install 來安裝對應瀏覽器。 5. 進一步學習與優化 5.1 反思與優化方向 異步執行:可使用 async 提高效率。 數據存儲:將抓取內容存入 CSV、JSON 或資料庫。 自動化測試:結合 pytest-playwright 進行測試。 動態交互:如自動填寫表單、點擊按鈕等。 5.2 可選挑戰 爬取其他網站:嘗試修改 URL,抓取不同網站的數據。 處理 JavaScript 渲染的頁面:使用 page.wait_for_load_state("domcontentloaded") 確保 DOM 加載完成。 自動登入功能:嘗試登錄帳號,並抓取登入後的內容。 6. 結論 本文介紹了 Playwright 的基礎使用方式,並提供了一個抓取網頁標題與內容的示例。透過這些知識,你可以開始使用 Playwright 進行自動化測試、數據抓取,甚至機器人流程自動化。
-
🚀 從零開始打造 RAG:深入理解 LlamaIndex 組件用llamaindex框架深入探討
這是一篇針對 LlamaIndex 的 RAG(檢索增強生成) 教學,將深入探討如何從零開始構建 RAG,並理解其內部機制。這份指南將著重於低層次的實作,避免直接使用高階封裝的函式,幫助你更好地理解 RAG 的每個核心組件,並自由組合來適應不同需求。 Optional:llamaindex 和 Langchain ?? 可參考我的medium有寫一篇簡單概述的https://kilong31442.medium.com/langchain-v-s-llamaindex-2fcfcbb36a47 🚀 從零開始打造 RAG:深入理解 LlamaIndex 組件 📌 介紹 這篇文章將指引你如何 從零開始構建 RAG(Retrieval-Augmented Generation),完全不依賴高階封裝的方法,而是透過最基礎的 LLM、嵌入模型、提示詞(prompt)與向量索引(vector store) 來打造一個可擴展的 RAG 系統。 我們將逐步構建 RAG 的核心組件,包括: 資料攝取(Ingestion) 向量檢索(Vector Retrieval) 回應合成(Response Synthesis) 混合搜尋(Hybrid Search) 問答與評估(QA & Evaluation) 高級檢索技術(Fusion Retrieval, Router) 結構化數據查詢(Text-to-SQL, Text-to-Pandas) 這些內容來自 LlamaIndex 的官方指南,你可以隨時參考完整文件:👉 官方文件 1️⃣ 構建資料攝取管道(Ingestion Pipeline) 📌 Ingestion from Scratch 資料攝取是 RAG 的第一步,我們需要將非結構化數據轉換為可檢索的向量格式,並存入向量數據庫。本節將示範如何 手動處理數據並建立索引,不使用 VectorStoreIndex.from_documents 等高階函式。 2️⃣ 構建向量檢索(Vector Retrieval) 📌 Vector Retrieval from Scratch 在這一部分,我們將學習如何 查詢向量數據庫,並建立自己的檢索系統,不依賴 VectorStoreIndex.as_retriever()。我們將探討: 如何計算相似度(Cosine / dot-product) 如何篩選與排序檢索結果 3️⃣ 使用開源技術構建 RAG(Open-Source Components) 📌 Open Source RAG 想要完全不依賴 OpenAI API,打造 本地端的 RAG 系統?這一部分將展示如何使用 開源 LLM、開源嵌入模型(如 sentence-transformers)、以及本地向量數據庫(如 FAISS 或 Qdrant)。 4️⃣ 從零開始構建簡易向量數據庫(Vector Store) 📌 Vector Store from Scratch 本節展示如何從基礎開始構建向量數據庫,使其具備 密集搜索(Dense Search)與元數據篩選(Metadata Filtering) 的能力。這對於想要理解向量數據庫的內部機制的工程師特別有幫助! 5️⃣ 構建回應合成(Response Synthesis) 📌 Response Synthesis from Scratch 當檢索到相關資訊後,LLM 需要根據這些內容生成回答。本節將探討: 如何有效利用檢索到的內容 如何處理 上下文溢出(Context Overflow) 如何 提供來源引用(Source Citation) 如何 進行非同步調用(Async Calls) 來提升效率 6️⃣ 建立 LLM 生成內容的評估機制(Evaluation) 📌 Evaluation from Scratch 這一部分將教你如何評估 RAG 系統的回應,確保其: 準確性(Correctness) 忠實度(Faithfulness) 我們將探討如何使用 LLM 建立 自定義評估模組,以確保系統輸出的可靠性。 7️⃣ 進階 RAG 技術(Advanced RAG) 📌 Building Advanced RAG from Scratch 如果你想進一步提升 RAG 的性能,這一部分將探討進階技術,包括: 混合搜尋(Hybrid Search) 查詢重寫(Query Rewriting) 檢索融合(Fusion Retrieval) 多步檢索(Multi-Step Retrieval) 8️⃣ 混合檢索(Hybrid Search) 📌 Building Hybrid Search from Scratch 混合檢索結合了 稀疏檢索(Sparse Retrieval, 如 BM25)與密集檢索(Dense Retrieval, 如 FAISS 向量搜索),能夠提升查詢結果的相關性。本節將示範如何構建自己的 Hybrid Search 模組。 9️⃣ 構建智慧路由(Router) 📌 Router from Scratch 除了基本的 RAG,LLM 也可以用來進行 決策與路由。這一部分展示如何構建 智能查詢路由器,自動決定: 是否要檢索數據 應該使用哪種類型的檢索 如何處理不同類型的問題 🔟 構建融合檢索(Fusion Retrieval) 📌 Fusion Retrieval from Scratch 本節將展示如何構建 進階檢索技術,包括: 查詢重寫(Query Rewriting) 動態檢索(Dynamic Retrieval) 檢索結果集成(Ensemble Retrieval) 這對於需要 更靈活檢索策略 的企業應用特別有幫助。 1️⃣1️⃣ 在結構化數據上進行問答(QA over Structured Data) 📌 Text-to-SQL from Scratch📌 Text-to-Pandas from Scratch 除了非結構化文本,LlamaIndex 也支援 結構化與半結構化數據(如 SQL 資料庫與 Pandas DataFrame)。這一部分將示範: 如何從零開始構建 Text-to-SQL 如何將 LLM 與 Pandas 整合,進行數據查詢 🎯 結論 這篇指南幫助你理解如何 從底層開始構建 RAG,讓你不再只是使用封裝好的高階函式,而是能夠 深入理解 RAG 的每個核心組件,並根據需求打造自己的 AI 工作流。 如果你對這些內容感興趣,歡迎深入閱讀 LlamaIndex 官方文件**,或關注更多的 **RAG 技術發展! 🚀 你的 RAG 之旅,現在開始! 🚀
-
🚀RAG深入探討組件(1)~(3)
這篇教學將指引你如何 從零開始構建數據攝取(Data Ingestion)流程,將文檔處理為向量並存入向量數據庫。我們不使用 LlamaIndex 的高階封裝,而是直接構建數據處理管道,幫助你了解內部細節。 完整教學的 官方文件:👉 LlamaIndex 官方文件 🚀 1.從零開始構建數據攝取管道(Data Ingestion) 📌 目標 在這篇教學中,我們將學習如何構建 數據攝取管道,將文本轉換為向量嵌入,並存入 Pinecone 向量數據庫。 主要步驟: 載入文檔 拆分文本 構建節點(Nodes) 提取元數據(Metadata, 選擇性) 生成嵌入向量 插入向量數據庫 查詢向量數據庫 1️⃣ 環境準備 我們需要安裝以下 Python 套件: pip install llama-index-embeddings-openai pip install llama-index-vector-stores-pinecone pip install llama-index-llms-openai pip install llama-index pip install python-dotenv pinecone-client pymupdf 🔑 設定 API Keys 你需要: Pinecone API Key(申請免費帳號) OpenAI API Key(取得 API 金鑰) 建立一個 .env 檔案來儲存 API Key: import os from dotenv import load_dotenv dotenv_path = "env" with open(dotenv_path, "w") as f: f.write('PINECONE_API_KEY="<your_api_key>"\n') f.write('OPENAI_API_KEY="<your_api_key>"\n') load_dotenv(dotenv_path=dotenv_path) 2️⃣ 建立 Pinecone 向量數據庫 我們將使用 Pinecone 作為向量儲存庫。 from pinecone import Pinecone, Index, ServerlessSpec api_key = os.environ["PINECONE_API_KEY"] pc = Pinecone(api_key=api_key) index_name = "llamaindex-rag-fs" # 創建索引(如不存在) if index_name not in pc.list_indexes().names(): pc.create_index( index_name, dimension=1536, # text-embedding-ada-002 的維度 metric="euclidean", spec=ServerlessSpec(cloud="aws", region="us-east-1"), ) pinecone_index = pc.Index(index_name) # 清除索引(可選) pinecone_index.delete(deleteAll=True) 🌟 建立 LlamaIndex 的 VectorStore from llama_index.vector_stores.pinecone import PineconeVectorStore vector_store = PineconeVectorStore(pinecone_index=pinecone_index) 3️⃣ 載入文檔 下載 LLaMA2 論文 PDF,並解析文本: import fitz file_path = "./data/llama2.pdf" doc = fitz.open(file_path) 4️⃣ 使用文本拆分器 我們使用 LlamaIndex 內建的 SentenceSplitter,將長文本拆分為小段落: from llama_index.core.node_parser import SentenceSplitter text_parser = SentenceSplitter(chunk_size=1024) text_chunks = [] doc_idxs = [] for doc_idx, page in enumerate(doc): page_text = page.get_text("text") cur_text_chunks = text_parser.split_text(page_text) text_chunks.extend(cur_text_chunks) doc_idxs.extend([doc_idx] * len(cur_text_chunks)) 5️⃣ 手動構建節點 LlamaIndex 的 TextNode 是一種低階數據抽象,我們手動創建節點並添加元數據: from llama_index.core.schema import TextNode nodes = [] for idx, text_chunk in enumerate(text_chunks): node = TextNode(text=text_chunk) nodes.append(node) # 測試:印出第一個節點內容 print(nodes[0].get_content(metadata_mode="all")) 6️⃣ (可選)提取元數據 如果想要提取 標題與關鍵問題,我們可以使用 LlamaIndex 的 MetadataExtractor: from llama_index.core.extractors import ( QuestionsAnsweredExtractor, TitleExtractor, ) from llama_index.core.ingestion import IngestionPipeline from llama_index.llms.openai import OpenAI llm = OpenAI(model="gpt-3.5-turbo") extractors = [ TitleExtractor(nodes=5, llm=llm), QuestionsAnsweredExtractor(questions=3, llm=llm), ] pipeline = IngestionPipeline(transformations=extractors) nodes = await pipeline.arun(nodes=nodes, in_place=False) # 測試:印出第一個節點的元數據 print(nodes[0].metadata) 7️⃣ 生成嵌入向量 我們使用 OpenAI 的 text-embedding-ada-002 模型來生成向量: from llama_index.embeddings.openai import OpenAIEmbedding embed_model = OpenAIEmbedding() for node in nodes: node_embedding = embed_model.get_text_embedding( node.get_content(metadata_mode="all") ) node.embedding = node_embedding 8️⃣ 將節點插入向量數據庫 vector_store.add(nodes) 9️⃣ 查詢向量數據庫 數據攝取完成後,我們可以開始查詢: from llama_index.core import VectorStoreIndex, StorageContext index = VectorStoreIndex.from_vector_store(vector_store) query_engine = index.as_query_engine() query_str = "Can you tell me about the key concepts for safety finetuning?" response = query_engine.query(query_str) print(str(response)) 🎯 總結 這篇教學展示了 從零開始構建數據攝取管道 的完整流程,幫助你: 載入文檔(PDF) 拆分文本(Sentence Splitter) 建立節點(TextNode) 提取元數據(TitleExtractor, QuestionsExtractor) 生成向量嵌入(OpenAI Embeddings) 插入向量數據庫(Pinecone) 執行查詢(VectorStoreIndex) 🚀 現在,你已經可以獨立構建 RAG 系統的數據攝取模組了! 🚀 這篇教學將指引你 從零開始構建檢索(Retrieval)系統,我們將不依賴高階封裝的檢索方法,而是逐步實作: 建立向量數據庫(Pinecone) 載入文檔並將其轉換為向量 手動生成查詢嵌入 查詢向量數據庫 解析結果 封裝成自訂檢索器(Retriever) 整合檢索器與查詢引擎 完整教學的 官方文件:👉 LlamaIndex 官方文件 🚀 2.從零開始構建檢索系統(Retrieval) 📌 目標 本教程將展示如何手動構建一個檢索系統,並將其整合到 LlamaIndex 的查詢引擎中。我們將使用 Pinecone 作為向量存儲庫,並學習如何: 生成查詢嵌入(Query Embeddings) 執行不同檢索模式(密集、稀疏、混合) 解析檢索結果 封裝為自訂檢索器(Retriever) 1️⃣ 環境準備 安裝所需的 Python 套件: pip install llama-index-readers-file pymupdf pip install llama-index-vector-stores-pinecone pip install llama-index-embeddings-openai pip install llama-index 🔑 設定 API Keys 你需要: Pinecone API Key(申請免費帳號) OpenAI API Key(取得 API 金鑰) import os from dotenv import load_dotenv dotenv_path = "env" with open(dotenv_path, "w") as f: f.write('PINECONE_API_KEY="<your_api_key>"\n') f.write('OPENAI_API_KEY="<your_api_key>"\n') load_dotenv(dotenv_path=dotenv_path) 2️⃣ 建立 Pinecone 向量數據庫 from pinecone import Pinecone, Index, ServerlessSpec api_key = os.environ["PINECONE_API_KEY"] pc = Pinecone(api_key=api_key) dataset_name = "quickstart" # 創建索引(如不存在) if dataset_name not in pc.list_indexes().names(): pc.create_index( dataset_name, dimension=1536, # text-embedding-ada-002 的維度 metric="euclidean", spec=ServerlessSpec(cloud="aws", region="us-east-1"), ) pinecone_index = pc.Index(dataset_name) # 清除索引(可選) pinecone_index.delete(deleteAll=True) 🌟 建立 LlamaIndex 的 VectorStore from llama_index.vector_stores.pinecone import PineconeVectorStore vector_store = PineconeVectorStore(pinecone_index=pinecone_index) 3️⃣ 載入文檔並轉換為向量 下載 LLaMA2 論文 PDF,並解析文本: !mkdir data !wget --user-agent "Mozilla" "https://arxiv.org/pdf/2307.09288.pdf" -O "data/llama2.pdf" from pathlib import Path from llama_index.readers.file import PyMuPDFReader loader = PyMuPDFReader() documents = loader.load(file_path="./data/llama2.pdf") 📌 載入文檔至向量存儲 我們使用 高階方法 VectorStoreIndex.from_documents 來加速載入數據: from llama_index.core import VectorStoreIndex from llama_index.core.node_parser import SentenceSplitter from llama_index.core import StorageContext splitter = SentenceSplitter(chunk_size=1024) storage_context = StorageContext.from_defaults(vector_store=vector_store) index = VectorStoreIndex.from_documents( documents, transformations=[splitter], storage_context=storage_context ) 4️⃣ 生成查詢嵌入 from llama_index.embeddings.openai import OpenAIEmbedding embed_model = OpenAIEmbedding() query_str = "Can you tell me about the key concepts for safety finetuning?" query_embedding = embed_model.get_query_embedding(query_str) 5️⃣ 查詢向量數據庫 我們支援 不同檢索模式: default(密集檢索 Dense) sparse(稀疏檢索 Sparse) hybrid(混合檢索 Hybrid) from llama_index.core.vector_stores import VectorStoreQuery query_mode = "default" # 可切換成 "sparse" 或 "hybrid" vector_store_query = VectorStoreQuery( query_embedding=query_embedding, similarity_top_k=2, mode=query_mode ) query_result = vector_store.query(vector_store_query) print(query_result) 6️⃣ 解析檢索結果 我們將結果封裝成 **NodeWithScore**,包含檢索到的內容與分數: from llama_index.core.schema import NodeWithScore from typing import Optional nodes_with_scores = [] for index, node in enumerate(query_result.nodes): score: Optional[float] = None if query_result.similarities is not None: score = query_result.similarities[index] nodes_with_scores.append(NodeWithScore(node=node, score=score)) 輸出檢索到的內容: from llama_index.core.response.notebook_utils import display_source_node for node in nodes_with_scores: display_source_node(node, source_length=1000) 7️⃣ 封裝成自訂檢索器(Retriever) from llama_index.core import QueryBundle from llama_index.core.retrievers import BaseRetriever from typing import Any, List class PineconeRetriever(BaseRetriever): """自訂檢索器,對 Pinecone 向量數據庫進行查詢""" def __init__( self, vector_store: PineconeVectorStore, embed_model: Any, query_mode: str = "default", similarity_top_k: int = 2, ) -> None: """初始化參數""" self._vector_store = vector_store self._embed_model = embed_model self._query_mode = query_mode self._similarity_top_k = similarity_top_k super().__init__() def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]: """檢索處理""" if query_bundle.embedding is None: query_embedding = self._embed_model.get_query_embedding(query_bundle.query_str) else: query_embedding = query_bundle.embedding vector_store_query = VectorStoreQuery( query_embedding=query_embedding, similarity_top_k=self._similarity_top_k, mode=self._query_mode, ) query_result = self._vector_store.query(vector_store_query) nodes_with_scores = [] for index, node in enumerate(query_result.nodes): score: Optional[float] = None if query_result.similarities is not None: score = query_result.similarities[index] nodes_with_scores.append(NodeWithScore(node=node, score=score)) return nodes_with_scores 初始化檢索器: retriever = PineconeRetriever(vector_store, embed_model, query_mode="default", similarity_top_k=2) retrieved_nodes = retriever.retrieve(query_str) for node in retrieved_nodes: display_source_node(node, source_length=1000) 8️⃣ 整合查詢引擎 from llama_index.core.query_engine import RetrieverQueryEngine query_engine = RetrieverQueryEngine.from_args(retriever) response = query_engine.query(query_str) print(str(response)) 🎯 總結 這篇指南展示了 如何從零開始構建檢索系統,幫助你掌握: 生成查詢嵌入 執行不同模式的向量檢索 解析檢索結果 封裝為自訂檢索器 整合查詢引擎 🚀 現在,你已經可以獨立打造 RAG 系統的檢索模組! 🚀 好的,我會根據你的需求撰寫一篇完整的 「完全開源的 RAG 系統」 部落格文章,並在適當位置加入 Optional 章節,幫助讀者更深入理解 RAG 的運作機制,並提供額外的優化技巧。 這篇文章不僅涵蓋 LlamaIndex 官方指南的內容,還會補充: 可選的進階技巧(Optional Sections) 最佳實踐建議 額外的擴展選項 🚀 3.從零開始構建 完全開源 RAG 系統 📌 介紹 大多數 RAG(Retrieval-Augmented Generation)系統依賴 OpenAI API 或 雲端向量數據庫,這雖然方便,但在某些應用場景下,我們可能需要 完全開源的替代方案,例如: 隱私要求高的應用(如醫療或法律) 離線運行(On-Premise) 降低 API 成本 對 LLM、向量存儲和檢索技術有更深理解的需求 在這篇文章中,我們將使用 完全開源的技術堆疊 來構建 RAG 系統: 嵌入模型:Sentence Transformers 向量存儲:PostgreSQL + pgvector LLM(大型語言模型):Llama 2(透過 llama.cpp) 這篇教學將涵蓋: 數據攝取(Data Ingestion) 向量存儲(Vector Storage) 檢索管道(Retrieval Pipeline) 查詢處理(Query Processing) 1️⃣ 環境準備 🛠️ 安裝必要套件: pip install llama-index-readers-file pymupdf pip install llama-index-vector-stores-postgres pip install llama-index-embeddings-huggingface pip install llama-index-llms-llama-cpp pip install psycopg2-binary pgvector asyncpg "sqlalchemy[asyncio]" greenlet 🔑 設定 PostgreSQL 安裝 PostgreSQL & pgvector Linux / Mac:brew install postgresql brew services start postgresql **安裝 pgvector**:psql -d postgres -c "CREATE EXTENSION IF NOT EXISTS vector;" 建立用戶與資料庫:CREATE ROLE my_user WITH LOGIN PASSWORD 'my_password'; ALTER ROLE my_user SUPERUSER; CREATE DATABASE vector_db; 2️⃣ 嵌入模型(Embedding Model) 我們將使用 Hugging Face 的 BAAI/bge-small-en 模型來計算文本嵌入: from llama_index.embeddings.huggingface import HuggingFaceEmbedding embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en") ✅ Optional: 為何選擇 bge-small-en? 訓練良好,適合向量檢索 相較於 text-embedding-ada-002,它是 完全開源 的 若需更高性能,可以嘗試 BAAI/bge-large-en 3️⃣ 載入 Llama 2 作為 LLM 我們使用 llama.cpp 來載入 Llama 2-13B: from llama_index.llms.llama_cpp import LlamaCPP model_url = "https://huggingface.co/TheBloke/Llama-2-13B-chat-GGUF/resolve/main/llama-2-13b-chat.Q4_0.gguf" llm = LlamaCPP( model_url=model_url, model_path=None, temperature=0.1, max_new_tokens=256, context_window=3900, model_kwargs={"n_gpu_layers": 1}, verbose=True, ) ✅ Optional: 可以使用哪些開源 LLM? Llama 3(即將推出) Mistral 7B Falcon 40B ChatGLM 3 4️⃣ 建立 PostgreSQL 向量存儲 import psycopg2 from llama_index.vector_stores.postgres import PGVectorStore db_name = "vector_db" host = "localhost" password = "my_password" port = "5432" user = "my_user" conn = psycopg2.connect( dbname="postgres", host=host, password=password, port=port, user=user, ) conn.autocommit = True with conn.cursor() as c: c.execute(f"DROP DATABASE IF EXISTS {db_name}") c.execute(f"CREATE DATABASE {db_name}") vector_store = PGVectorStore.from_params( database=db_name, host=host, password=password, port=port, user=user, table_name="llama2_paper", embed_dim=384, ) ✅ Optional: 其他開源向量存儲 Chroma Weaviate Qdrant 5️⃣ 數據攝取管道 1. 載入文檔 from llama_index.readers.file import PyMuPDFReader loader = PyMuPDFReader() documents = loader.load(file_path="./data/llama2.pdf") 2. 拆分文本 from llama_index.core.node_parser import SentenceSplitter text_parser = SentenceSplitter(chunk_size=1024) text_chunks = [] doc_idxs = [] for doc_idx, doc in enumerate(documents): cur_text_chunks = text_parser.split_text(doc.text) text_chunks.extend(cur_text_chunks) doc_idxs.extend([doc_idx] * len(cur_text_chunks)) 3. 轉換為節點 from llama_index.core.schema import TextNode nodes = [] for idx, text_chunk in enumerate(text_chunks): node = TextNode(text=text_chunk) nodes.append(node) 4. 生成嵌入 for node in nodes: node.embedding = embed_model.get_text_embedding(node.get_content(metadata_mode="all")) 5. 插入向量存儲 vector_store.add(nodes) 6️⃣ 檢索管道 1. 生成查詢嵌入 query_str = "How does Llama 2 compare to other models?" query_embedding = embed_model.get_query_embedding(query_str) 2. 執行向量檢索 from llama_index.core.vector_stores import VectorStoreQuery vector_store_query = VectorStoreQuery( query_embedding=query_embedding, similarity_top_k=2, mode="default" ) query_result = vector_store.query(vector_store_query) 3. 解析結果 from llama_index.core.schema import NodeWithScore nodes_with_scores = [ NodeWithScore(node=node, score=query_result.similarities[i]) for i, node in enumerate(query_result.nodes) ] 7️⃣ 整合查詢引擎 from llama_index.core.query_engine import RetrieverQueryEngine retriever = VectorDBRetriever(vector_store, embed_model) query_engine = RetrieverQueryEngine.from_args(retriever, llm=llm) response = query_engine.query(query_str) print(str(response)) ✅ Optional: 如何提升檢索性能? 使用 Hybrid Search(混合檢索) 結合 BM25 作關鍵字匹配 嘗試 Query Expansion(查詢擴展) 🎯 結論 我們構建了一個 完全開源 的 RAG 系統,無需依賴 OpenAI 或商業 API,適合:✅ 高隱私場景✅ 離線應用✅ 客製化需求 🚀 現在,你可以打造自己的開源 RAG 系統了! 🚀 這是一個 關鍵的 Optional 章節,我會將它納入前面「完全開源的 RAG 系統」部落格中的 Optional 部分,作為「進階檢索技術 - 簡易 Hybrid Search」。這樣讀者在學完基本的向量檢索後,可以更進一步學習如何將 向量檢索(Semantic Search)與關鍵字檢索(Keyword Lookup) 結合,提高檢索的準確度與靈活性。 ✅ Optional: 進階檢索技術 - 簡易 Hybrid Search 在標準的 RAG 流程中,我們通常使用 向量檢索(Vector Retrieval),即: 對 Query 進行嵌入(Embedding) 從向量存儲中檢索 Top-K 相似結果 傳遞給 LLM 進行回答生成 這種方法的優勢是: 能夠理解語意相似性 可以檢索到未必完全匹配關鍵字的內容 但它也有一些問題: 無法處理 精確匹配(Exact Match) 可能忽略關鍵字的重要性 在數據量少時,語意相似度可能不準確 🚀 解決方案:Hybrid Search Hybrid Search(混合檢索) 結合了: 向量檢索(Semantic Search) 關鍵字檢索(Keyword Lookup) 這樣我們可以在查詢時: 若關鍵字命中(Keyword Match),則優先返回完全匹配的內容 若關鍵字沒有命中,則回退至語意檢索(Vector Retrieval) 可使用「AND」與「OR」條件 來調整查詢模式 這種方式特別適合:✅ 企業內部知識庫(Enterprise KB)✅ FAQ 檢索系統✅ 結構化數據的 RAG 應用 🛠 1️⃣ Hybrid Search 的基本架構 這裡我們使用 LlamaIndex 來建立 兩種不同的索引: 向量索引(VectorStoreIndex) - 用來進行語意檢索 關鍵字索引(SimpleKeywordTableIndex) - 用來進行關鍵字匹配 from llama_index.core import SimpleDirectoryReader, StorageContext from llama_index.core import SimpleKeywordTableIndex, VectorStoreIndex # 讀取文檔 documents = SimpleDirectoryReader("./data/paul_graham").load_data() # 轉換為節點 nodes = Settings.node_parser.get_nodes_from_documents(documents) # 建立存儲上下文 storage_context = StorageContext.from_defaults() storage_context.docstore.add_documents(nodes) # 建立向量索引 & 關鍵字索引 vector_index = VectorStoreIndex(nodes, storage_context=storage_context) keyword_index = SimpleKeywordTableIndex(nodes, storage_context=storage_context) 🔍 2️⃣ 自訂 Hybrid Search 檢索器 我們自訂一個 CustomRetriever: 先使用向量檢索 再使用關鍵字檢索 使用 AND 或 OR 模式來決定如何合併結果 from llama_index.core import QueryBundle from llama_index.core.schema import NodeWithScore from llama_index.core.retrievers import ( BaseRetriever, VectorIndexRetriever, KeywordTableSimpleRetriever, ) from typing import List class CustomRetriever(BaseRetriever): """自訂混合檢索 Retriever""" def __init__( self, vector_retriever: VectorIndexRetriever, keyword_retriever: KeywordTableSimpleRetriever, mode: str = "AND", # 可選 "AND" 或 "OR" ) -> None: """初始化參數""" self._vector_retriever = vector_retriever self._keyword_retriever = keyword_retriever if mode not in ("AND", "OR"): raise ValueError("Invalid mode.") self._mode = mode super().__init__() def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]: """執行檢索""" vector_nodes = self._vector_retriever.retrieve(query_bundle) keyword_nodes = self._keyword_retriever.retrieve(query_bundle) vector_ids = {n.node.node_id for n in vector_nodes} keyword_ids = {n.node.node_id for n in keyword_nodes} combined_dict = {n.node.node_id: n for n in vector_nodes} combined_dict.update({n.node.node_id: n for n in keyword_nodes}) if self._mode == "AND": retrieve_ids = vector_ids.intersection(keyword_ids) else: retrieve_ids = vector_ids.union(keyword_ids) retrieve_nodes = [combined_dict[rid] for rid in retrieve_ids] return retrieve_nodes ⚙️ 3️⃣ 建立 Hybrid Query Engine 我們將檢索器嵌入 RetrieverQueryEngine,並測試查詢: from llama_index.core import get_response_synthesizer from llama_index.core.query_engine import RetrieverQueryEngine # 建立向量檢索器 vector_retriever = VectorIndexRetriever(index=vector_index, similarity_top_k=2) # 建立關鍵字檢索器 keyword_retriever = KeywordTableSimpleRetriever(index=keyword_index) # 設定自訂混合檢索 custom_retriever = CustomRetriever(vector_retriever, keyword_retriever, mode="AND") # 定義 LLM 回應處理 response_synthesizer = get_response_synthesizer() # 建立查詢引擎 custom_query_engine = RetrieverQueryEngine( retriever=custom_retriever, response_synthesizer=response_synthesizer, ) 📝 4️⃣ 測試 Hybrid Search ✅ 測試 1:檢索與 YC 相關的內容 response = custom_query_engine.query("What did the author do during his time at YC?") print(response) 📌 結果 During his time at YC, the author worked on various projects, including writing essays and working on YC itself... ✅ 測試 2:關鍵字不匹配時的效果 response = custom_query_engine.query("What did the author do during his time at Yale?") print(response) 📌 結果 Empty Response 📢 Hybrid Search 能夠避免不相關的檢索結果! ✅ 測試 3:純向量檢索 vector_response = vector_query_engine.query("What did the author do during his time at Yale?") print(vector_response) 📌 結果 The context information does not provide any information about the author's time at Yale. 📢 向量檢索可能會產生與內容無關的回應,但 Hybrid Search 可避免這種情況! 🎯 結論 在這個 Optional 章節中,我們學習了: Hybrid Search 的必要性(解決語意檢索的精確匹配問題) 如何結合關鍵字索引與向量索引 如何自訂 Retriever 如何測試 Hybrid Search 📌 適用場景 內部文件檢索 FAQ 知識庫 法規與政策文件查詢 完整教學文件:👉 LlamaIndex 官方文件 🚀 現在,你的 RAG 系統已經支援 Hybrid Search 了! 🚀
-
🚀RAG 組件(4)~final
✅ Optional: 進階技術 - 從零開始打造向量存儲(Vector Store) 在 RAG 系統中,向量存儲庫(Vector Store)是檢索模組的 核心組件,負責存儲文件的向量嵌入,並在查詢時返回相似內容。在這個章節中,我們將: 手寫一個簡易的向量存儲 實作語意檢索(Semantic Search) 加入 Metadata Filtering 測試向量檢索與 Metadata 過濾 思考如何優化檢索性能 這將幫助你深入理解 向量數據庫的底層運作原理,並為你開發自己的向量檢索系統打下堅實基礎。 1️⃣ 環境準備 首先,我們需要安裝一些 必要的 Python 套件: pip install llama-index-readers-file pymupdf pip install llama-index-embeddings-openai 我們也需要下載測試數據: mkdir data wget --user-agent "Mozilla" "https://arxiv.org/pdf/2307.09288.pdf" -O "data/llama2.pdf" 2️⃣ 載入 PDF 文檔 我們使用 llama-index 來讀取 PDF 文檔,並將其解析為文本: from pathlib import Path from llama_index.readers.file import PyMuPDFReader loader = PyMuPDFReader() documents = loader.load(file_path="./data/llama2.pdf") 3️⃣ 將文檔轉換為節點(Nodes) 在 RAG 系統中,為了提高檢索的效果,我們通常會 將文本拆分成小塊,這樣可以讓向量檢索更準確: from llama_index.core.node_parser import SentenceSplitter node_parser = SentenceSplitter(chunk_size=256) nodes = node_parser.get_nodes_from_documents(documents) 4️⃣ 為每個節點生成嵌入向量 現在,我們使用 OpenAI 的 text-embedding-ada-002 來生成向量: from llama_index.embeddings.openai import OpenAIEmbedding embed_model = OpenAIEmbedding() for node in nodes: node_embedding = embed_model.get_text_embedding( node.get_content(metadata_mode="all") ) node.embedding = node_embedding 5️⃣ 建立一個簡單的 In-Memory 向量存儲 我們來 手寫一個 Python 版的 Vector Store,它可以: 新增(add) 節點 獲取(get) 節點 刪除(delete) 節點 查詢(query) 節點 📌 5.1 定義 Vector Store 基本架構 from typing import List, Dict, Any from llama_index.core.schema import BaseNode, TextNode class SimpleVectorStore: """一個簡單的 In-Memory 向量存儲""" def __init__(self): self.node_dict: Dict[str, BaseNode] = {} def add(self, nodes: List[BaseNode]) -> None: """新增節點""" for node in nodes: self.node_dict[node.node_id] = node def get(self, node_id: str) -> BaseNode: """獲取節點""" return self.node_dict.get(node_id, None) def delete(self, node_id: str) -> None: """刪除節點""" if node_id in self.node_dict: del self.node_dict[node_id] 📌 5.2 測試 Vector Store test_node1 = TextNode(id_="1", text="Hello, this is a test node.") test_node2 = TextNode(id_="2", text="This is another node for testing.") vector_store = SimpleVectorStore() vector_store.add([test_node1, test_node2]) print(vector_store.get("1")) # 測試獲取 vector_store.delete("2") print(vector_store.get("2")) # 測試刪除,應該輸出 None 6️⃣ 加入語意檢索(Semantic Search) 現在我們要讓這個向量存儲庫支援 語意檢索,即: 計算查詢向量與所有文件的相似度 取前 K 個最相似的文件 我們用 餘弦相似度(Cosine Similarity) 來計算向量之間的距離: import numpy as np from typing import Tuple def get_top_k_embeddings( query_embedding: List[float], doc_embeddings: List[List[float]], doc_ids: List[str], similarity_top_k: int = 5, ) -> Tuple[List[float], List]: """計算餘弦相似度,返回 Top-K 結果""" qembed_np = np.array(query_embedding) dembed_np = np.array(doc_embeddings) cos_sim = np.dot(dembed_np, qembed_np) / (np.linalg.norm(qembed_np) * np.linalg.norm(dembed_np, axis=1)) sorted_indices = np.argsort(cos_sim)[::-1][:similarity_top_k] sorted_similarities = cos_sim[sorted_indices] sorted_ids = [doc_ids[i] for i in sorted_indices] return sorted_similarities, sorted_ids 7️⃣ 加入 Metadata Filtering 有時候我們需要根據 Metadata(元數據) 來篩選文件: from llama_index.core.vector_stores import MetadataFilters def filter_nodes(nodes: List[BaseNode], filters: MetadataFilters) -> List[BaseNode]: """根據 Metadata 過濾節點""" filtered_nodes = [] for node in nodes: matches = True for f in filters.filters: if f.key not in node.metadata: matches = False continue if f.value != node.metadata[f.key]: matches = False continue if matches: filtered_nodes.append(node) return filtered_nodes 8️⃣ 測試查詢 現在我們來測試 向量檢索 + Metadata 過濾: vector_store = SimpleVectorStore() vector_store.add(nodes) # 加入節點 # 測試語意檢索 query_str = "Can you tell me about the key concepts for safety finetuning" query_embedding = embed_model.get_query_embedding(query_str) query_result = get_top_k_embeddings(query_embedding, [n.embedding for n in nodes], [n.node_id for n in nodes], similarity_top_k=2) for similarity, node_id in zip(query_result[0], query_result[1]): print(f"[Node ID {node_id}] Similarity: {similarity}\n") print(vector_store.get(node_id).get_content(metadata_mode="all")) print("\n----------------\n") 🎯 總結 在這個 Optional 章節中,我們: 從零開始構建了一個簡單的 In-Memory 向量存儲 實作了語意檢索(Semantic Search) 加入了 Metadata Filtering 測試了查詢效果 思考了如何優化檢索性能(FAISS, HNSW, Annoy) 📌 完整教學文件:👉 LlamaIndex 官方文件 從零開始構建回應合成:RAG 教學 簡介 檢索增強生成(RAG)管道是一種強大的方法,透過檢索相關文件來增強 LLM 的表現。其中,回應合成(Response Synthesis)是 RAG 系統的關鍵組件之一,負責將檢索到的節點(nodes)組合成最終答案。然而,當檢索的上下文超出模型的 token 限制時,有效的合成策略變得至關重要。 本教學將探討多種回應合成策略,並使用 LlamaIndex 與 OpenAI LLM 來實作。 你將學到的內容 使用簡單提示詞(prompt)進行回應合成 如何使用 建立與改進(Create and Refine) 策略處理上下文溢出 階層式摘要(Hierarchical Summarization) 的實作方法 使用非同步方式提升合成效率 將所有技術整合到簡單的查詢引擎中 [Optional] 如何優化回應合成效能 前置準備 請確保已安裝所需的 Python 套件: %pip install llama-index-readers-file pymupdf %pip install llama-index-vector-stores-pinecone %pip install llama-index-llms-openai %pip install llama-index 步驟 1:設置環境 我們將建立一個 Pinecone 向量存儲(vector store)並準備 LlamaIndex 組件。 載入數據 我們將下載範例 PDF 文件並使用 PyMuPDFReader 讀取它。 !mkdir data !wget --user-agent "Mozilla" "https://arxiv.org/pdf/2307.09288.pdf" -O "data/llama2.pdf" from pathlib import Path from llama_index.readers.file import PyMuPDFReader loader = PyMuPDFReader() documents = loader.load(file_path="./data/llama2.pdf") 建立 Pinecone 索引與檢索器 初始化 Pinecone 並創建一個向量存儲,設定 chunk 大小為 1024。 import pinecone import os api_key = os.environ["PINECONE_API_KEY"] pinecone.init(api_key=api_key, environment="us-west1-gcp") pinecone.create_index("quickstart", dimension=1536, metric="euclidean", pod_type="p1") pinecone_index = pinecone.Index("quickstart") pinecone_index.delete(deleteAll=True) from llama_index.vector_stores.pinecone import PineconeVectorStore from llama_index.core import VectorStoreIndex from llama_index.core.node_parser import SentenceSplitter from llama_index.core import StorageContext vector_store = PineconeVectorStore(pinecone_index=pinecone_index) splitter = SentenceSplitter(chunk_size=1024) storage_context = StorageContext.from_defaults(vector_store=vector_store) index = VectorStoreIndex.from_documents(documents, transformations=[splitter], storage_context=storage_context) retriever = index.as_retriever() 步驟 2:實作回應合成 基礎回應合成 我們從最基本的方法開始,將檢索到的節點全部放入單一提示詞中。 from llama_index.llms.openai import OpenAI from llama_index.core import PromptTemplate llm = OpenAI(model="text-davinci-003") qa_prompt = PromptTemplate( """ 以下是上下文資訊: --------------------- {context_str} --------------------- 根據上述上下文資訊,請回答問題。 問題: {query_str} 答案: """ ) 處理上下文溢出:建立與改進策略 若檢索的上下文過長,我們可以透過逐步改進來處理。 refine_prompt = PromptTemplate( """ 原始問題如下: {query_str} 我們已提供一個初步答案: {existing_answer} 我們可以利用以下新的上下文來改進這個答案(若需要)。 ------------ {context_str} ------------ 根據新的上下文,請改進原始答案。 若無需修改,請返回原始答案。 改進後的答案: """ ) 階層式摘要策略 透過遞歸方式將檢索的節點進行逐步合併。 def combine_results(texts, query_str, qa_prompt, llm, num_children=5): new_texts = [] for idx in range(0, len(texts), num_children): text_batch = texts[idx : idx + num_children] context_str = "\n\n".join([t for t in text_batch]) fmt_qa_prompt = qa_prompt.format(context_str=context_str, query_str=query_str) combined_response = llm.complete(fmt_qa_prompt) new_texts.append(str(combined_response)) if len(new_texts) == 1: return new_texts[0] else: return combine_results(new_texts, query_str, qa_prompt, llm, num_children=num_children) 步驟 3:整合所有技術 我們將檢索器、回應合成方法整合成一個查詢引擎。 class MyQueryEngine: def __init__(self, retriever, qa_prompt, llm, num_children=5): self.retriever = retriever self.qa_prompt = qa_prompt self.llm = llm self.num_children = num_children def query(self, query_str): retrieved_nodes = self.retriever.retrieve(query_str) return combine_results(retrieved_nodes, query_str, self.qa_prompt, self.llm, self.num_children) [Optional] 優化與改進回應合成效能 使用 非同步處理(Async Processing) 來加快查詢速度。 結合 分批處理(Batch Processing),提高模型推理效能。 微調提示詞(Prompt Engineering)以產生更精確的回應。 總結 本教學介紹了多種回應合成策略,並展示了如何在 RAG 管道中有效地處理大量檢索數據。 簡單合成法 適用於小型上下文 建立與改進策略 可用於逐步擴展答案 階層式摘要 幫助壓縮資訊並提高效率 非同步查詢與分批處理 可提升查詢效能 透過這些技術,您可以構建更強大的 RAG 系統! 從零開始構建回應合成與評估:RAG 教學 簡介 檢索增強生成(RAG)管道是一種強大的方法,透過檢索相關文件來增強 LLM 的表現。其中,回應合成(Response Synthesis)是 RAG 系統的關鍵組件之一,負責將檢索到的節點(nodes)組合成最終答案。此外,評估這些回應的質量也是一個重要步驟,可以確保生成的回應準確且忠於原始資訊。 本教學將探討多種回應合成與評估策略,並使用 LlamaIndex 與 OpenAI LLM 來實作。 你將學到的內容 使用簡單提示詞(prompt)進行回應合成 如何使用 建立與改進(Create and Refine) 策略處理上下文溢出 階層式摘要(Hierarchical Summarization) 的實作方法 正確性(Correctness) 和 忠誠度(Faithfulness) 評估方法 使用非同步方式提升合成效率 檢索評估(Retrieval Evaluation) 與 LlamaIndex 內建的評估模組 問題生成(Question Generation) 技術應用 將所有技術整合到簡單的查詢引擎中 [Optional] 如何優化回應合成效能 評估概念 評估與基準測試是 LLM 開發中的關鍵概念。要提升 LLM 應用(如 RAG 或 Agent)的表現,必須有方法來衡量其效果。 LlamaIndex 提供了多種評估模組,以測量生成結果的質量以及檢索結果的相關性。 回應評估(Response Evaluation) 正確性(Correctness):檢查生成的答案是否與參考答案一致(需要標註資料)。 語義相似性(Semantic Similarity):檢查生成的答案是否與參考答案在語義上相似(需要標註資料)。 忠誠度(Faithfulness):檢查生成的答案是否忠於檢索的上下文(避免幻覺)。 上下文相關性(Context Relevancy):檢查檢索到的內容是否與問題相關。 答案相關性(Answer Relevancy):檢查生成的答案是否與問題相關。 準則遵循(Guideline Adherence):檢查生成的答案是否符合特定準則。 問題生成(Question Generation) LlamaIndex 也可以利用現有的數據集自動生成問題,以進行更全面的評估。 檢索評估(Retrieval Evaluation) 我們也提供模組來獨立評估檢索結果。 核心步驟包括: 數據集生成:從非結構化文本生成 (問題, 上下文) 配對。 檢索評估:使用排名指標(如 MRR、hit-rate、precision)評估檢索效果。 LlamaIndex 提供的評估模組可直接用於上述步驟。 相關工具整合 LlamaIndex 也整合了社群評估工具,如: UpTrain Tonic Validate(包含可視化結果的 Web UI) DeepEval Ragas RAGChecker 使用模式 完整的使用細節請參考以下模式: 查詢評估模式(Query Eval Usage Pattern) 檢索評估模式(Retrieval Eval Usage Pattern) 模組與筆記本 有關如何使用這些評估組件,請參閱對應的模組指南。 RAG 系統的標註評估數據集(LabelledRagDataset) 有關如何使用標註數據集來評估 RAG 系統的詳細說明,請參閱專門的指南。 從零開始構建回應合成與評估:RAG 教學 簡介 檢索增強生成(RAG)管道是一種強大的方法,透過檢索相關文件來增強 LLM 的表現。其中,回應合成(Response Synthesis)是 RAG 系統的關鍵組件之一,負責將檢索到的節點(nodes)組合成最終答案。此外,評估這些回應的質量也是一個重要步驟,可以確保生成的回應準確且忠於原始資訊。 本教學將探討多種回應合成與評估策略,並使用 LlamaIndex 與 OpenAI LLM 來實作。 你將學到的內容 使用簡單提示詞(prompt)進行回應合成 如何使用 建立與改進(Create and Refine) 策略處理上下文溢出 階層式摘要(Hierarchical Summarization) 的實作方法 正確性(Correctness) 和 忠誠度(Faithfulness) 評估方法 使用非同步方式提升合成效率 將所有技術整合到簡單的查詢引擎中 [Optional] 如何優化回應合成效能 前置準備 請確保已安裝所需的 Python 套件: %pip install llama-index-readers-file pymupdf %pip install llama-index-vector-stores-pinecone %pip install llama-index-llms-openai %pip install llama-index 步驟 1:設置環境 我們將建立一個 Pinecone 向量存儲(vector store)並準備 LlamaIndex 組件。 載入數據 我們將下載範例 PDF 文件並使用 PyMuPDFReader 讀取它。 !mkdir data !wget --user-agent "Mozilla" "https://arxiv.org/pdf/2307.09288.pdf" -O "data/llama2.pdf" from pathlib import Path from llama_index.readers.file import PyMuPDFReader loader = PyMuPDFReader() documents = loader.load(file_path="./data/llama2.pdf") 建立 Pinecone 索引與檢索器 初始化 Pinecone 並創建一個向量存儲,設定 chunk 大小為 1024。 import pinecone import os api_key = os.environ["PINECONE_API_KEY"] pinecone.init(api_key=api_key, environment="us-west1-gcp") pinecone.create_index("quickstart", dimension=1536, metric="euclidean", pod_type="p1") pinecone_index = pinecone.Index("quickstart") pinecone_index.delete(deleteAll=True) from llama_index.vector_stores.pinecone import PineconeVectorStore from llama_index.core import VectorStoreIndex from llama_index.core.node_parser import SentenceSplitter from llama_index.core import StorageContext vector_store = PineconeVectorStore(pinecone_index=pinecone_index) splitter = SentenceSplitter(chunk_size=1024) storage_context = StorageContext.from_defaults(vector_store=vector_store) index = VectorStoreIndex.from_documents(documents, transformations=[splitter], storage_context=storage_context) retriever = index.as_retriever() 步驟 2:實作回應合成 基礎回應合成 我們從最基本的方法開始,將檢索到的節點全部放入單一提示詞中。 from llama_index.llms.openai import OpenAI from llama_index.core import PromptTemplate llm = OpenAI(model="text-davinci-003") qa_prompt = PromptTemplate( """ 以下是上下文資訊: --------------------- {context_str} --------------------- 根據上述上下文資訊,請回答問題。 問題: {query_str} 答案: """ ) 處理上下文溢出:建立與改進策略 若檢索的上下文過長,我們可以透過逐步改進來處理。 refine_prompt = PromptTemplate( """ 原始問題如下: {query_str} 我們已提供一個初步答案: {existing_answer} 我們可以利用以下新的上下文來改進這個答案(若需要)。 ------------ {context_str} ------------ 根據新的上下文,請改進原始答案。 若無需修改,請返回原始答案。 改進後的答案: """ ) 步驟 3:回應評估 正確性評估(Correctness Evaluation) 我們比較模型生成的答案與標準答案之間的相似度,並評分 1 到 5 分。 CORRECTNESS_SYS_TMPL = """ 你是一個專業的評估系統,負責評估問答機器人的準確性。 根據以下資訊進行評估: - 使用者問題 - 參考答案 - 生成答案 請評分 1 至 5 分,並解釋你的評分理由。 """ 忠誠度評估(Faithfulness Evaluation) 我們確保生成的答案是否忠於檢索到的內容,而非憑空捏造。 EVAL_TEMPLATE = PromptTemplate( """ 請判斷以下資訊是否受到上下文支持。 如果任何上下文支持此資訊,請回答 YES,否則回答 NO。 資訊: {query_str} 上下文: {context_str} 答案: """ ) 步驟 4:整合所有技術 我們將檢索器、回應合成與評估方法整合成一個查詢引擎。 class MyQueryEngine: def __init__(self, retriever, qa_prompt, llm, num_children=5): self.retriever = retriever self.qa_prompt = qa_prompt self.llm = llm self.num_children = num_children def query(self, query_str): retrieved_nodes = self.retriever.retrieve(query_str) return combine_results(retrieved_nodes, query_str, self.qa_prompt, self.llm, self.num_children) 總結 本教學介紹了回應合成與評估方法,確保 RAG 系統生成的答案既準確又忠於檢索內容。 回應合成:簡單合成、建立與改進、階層式摘要 評估方法:正確性評估、忠誠度評估 非同步處理與批量處理 提升查詢效能 不落格風格教學日誌:從零開始打造 Retrieval 系統 這篇教學日誌紀錄了如何建立一個標準的 Retriever 來對一個向量資料庫(Vector Database)進行檢索,以便透過相似度進行資料查詢。文中使用了 Pinecone 作為向量資料庫,並搭配 LlamaIndex(又稱 GPT Index)相關模組進行文件載入與檢索。 在這篇教學日誌中,你將會學到: 如何生成 Query Embedding 如何使用不同的檢索模式(Dense、Sparse、Hybrid)查詢向量資料庫 如何將查詢結果轉為 Nodes 如何將檢索流程封裝成自訂 Retriever 文末也會提供一些反向思考與進階課題,供同學參考與延伸學習。讓我們開始吧! 目錄 環境設定與安裝 建立 Pinecone Index 建立 PineconeVectorStore 載入文件 文件切割並載入 Vector Store 定義 Vector Retriever Step 1:生成 Query Embedding Step 2:查詢向量資料庫 Step-3:將結果轉成-nodes Step-4:將流程包裝成-retriever-類別 將 Retriever 插入 RetrieverQueryEngine 做回應生成 反向思考:為什麼要這樣做? Optional 進階教學 環境設定與安裝 如果你在 Colab 或其他環境上,請先執行下列指令來安裝所需套件(或確保已安裝): %pip install llama-index-readers-file pymupdf %pip install llama-index-vector-stores-pinecone %pip install llama-index-embeddings-openai !pip install llama-index 如環境中已安裝上述套件,可跳過此步驟。 建立 Pinecone Index 在下方程式碼中,會透過 Pinecone API 建立(或存取)我們想要的 Index。我們使用 text-embedding-ada-002 作為預設的嵌入模型,其維度是 1536。另外,如果之前的 Index 中已有內容,可以透過 pinecone_index.delete(deleteAll=True) 來刪除所有資料重新開始。 from pinecone import Pinecone, Index, ServerlessSpec import os api_key = os.environ["PINECONE_API_KEY"] # 確保你有設定這個環境變數 pc = Pinecone(api_key=api_key) # dimensions are for text-embedding-ada-002 dataset_name = "quickstart" if dataset_name not in pc.list_indexes().names(): pc.create_index( dataset_name, dimension=1536, metric="euclidean", spec=ServerlessSpec(cloud="aws", region="us-east-1"), ) pinecone_index = pc.Index(dataset_name) # [Optional] drop contents in index pinecone_index.delete(deleteAll=True) 建立 PineconeVectorStore 透過 PineconeVectorStore 包裝 pinecone_index,並存放在 StorageContext 中。這樣一來,我們就可以在 LlamaIndex 的高階 API 中直接使用此向量存儲。 from llama_index.vector_stores.pinecone import PineconeVectorStore vector_store = PineconeVectorStore(pinecone_index=pinecone_index) 載入文件 在這個範例中,我們載入了一份 PDF(llama2.pdf)檔案作為測試文件。透過 PyMuPDFReader 讀取 PDF 並轉為 Document 物件。 !mkdir data !wget --user-agent "Mozilla" "https://arxiv.org/pdf/2307.09288.pdf" -O "data/llama2.pdf" from pathlib import Path from llama_index.readers.file import PyMuPDFReader loader = PyMuPDFReader() documents = loader.load(file_path="./data/llama2.pdf") 文件切割並載入 Vector Store 這一步會透過 SentenceSplitter 以一定大小(chunk_size=1024)對文件進行切割,再將分段後的文件載入到向量存儲裡。 由於我們想更直觀地操作,在此直接利用 VectorStoreIndex.from_documents 進行高階封裝,將文件與向量儲存結合。 注意: 本文後半段的檢索過程則是 “從零開始” ,較少使用高階索引的功能,直接操作底層查詢。 from llama_index.core import VectorStoreIndex from llama_index.core.node_parser import SentenceSplitter from llama_index.core import StorageContext splitter = SentenceSplitter(chunk_size=1024) storage_context = StorageContext.from_defaults(vector_store=vector_store) index = VectorStoreIndex.from_documents( documents, transformations=[splitter], storage_context=storage_context ) 定義 Vector Retriever 下面示範如何自行撰寫 Retriever,並一步一步展開整個檢索流程。 預設的查詢字串: query_str = "Can you tell me about the key concepts for safety finetuning" Step 1:生成 Query Embedding 我們使用 OpenAIEmbedding 取得嵌入表徵(embedding),用來與向量資料庫中的內容進行相似度比對。 from llama_index.embeddings.openai import OpenAIEmbedding embed_model = OpenAIEmbedding() query_embedding = embed_model.get_query_embedding(query_str) Step 2:查詢向量資料庫 定義一個 VectorStoreQuery,其中包含我們的 query_embedding、要取回的節點數(similarity_top_k=2),以及檢索模式("default", "sparse", "hybrid" 皆可指定)。 # construct vector store query from llama_index.core.vector_stores import VectorStoreQuery query_mode = "default" # query_mode = "sparse" # query_mode = "hybrid" vector_store_query = VectorStoreQuery( query_embedding=query_embedding, similarity_top_k=2, mode=query_mode ) # returns a VectorStoreQueryResult query_result = vector_store.query(vector_store_query) query_result Step 3:將結果轉成 Nodes VectorStoreQueryResult 會回傳一串 Node 與對應的相似度(similarities)。以下將它們做成一個 NodeWithScore 陣列,方便後續操作或顯示。 from llama_index.core.schema import NodeWithScore from typing import Optional nodes_with_scores = [] for index, node in enumerate(query_result.nodes): score: Optional[float] = None if query_result.similarities is not None: score = query_result.similarities[index] nodes_with_scores.append(NodeWithScore(node=node, score=score)) # [Optional] 顯示看看結果 from llama_index.core.response.notebook_utils import display_source_node for node in nodes_with_scores: display_source_node(node, source_length=1000) Step 4:將流程包裝成 Retriever 類別 以下自訂了一個 PineconeRetriever 類別,繼承自 LlamaIndex 的 BaseRetriever。 只要提供 vector_store、embed_model、query_mode 和 similarity_top_k,就能建立出一個自訂的檢索器。 from llama_index.core import QueryBundle from llama_index.core.retrievers import BaseRetriever from typing import Any, List class PineconeRetriever(BaseRetriever): """Retriever over a pinecone vector store.""" def __init__( self, vector_store: PineconeVectorStore, embed_model: Any, query_mode: str = "default", similarity_top_k: int = 2, ) -> None: """Init params.""" self._vector_store = vector_store self._embed_model = embed_model self._query_mode = query_mode self._similarity_top_k = similarity_top_k super().__init__() def _retrieve(self, query_bundle: QueryBundle) -> List[NodeWithScore]: """Retrieve.""" if query_bundle.embedding is None: query_embedding = self._embed_model.get_query_embedding( query_bundle.query_str ) else: query_embedding = query_bundle.embedding vector_store_query = VectorStoreQuery( query_embedding=query_embedding, similarity_top_k=self._similarity_top_k, mode=self._query_mode, ) query_result = self._vector_store.query(vector_store_query) nodes_with_scores = [] for index, node in enumerate(query_result.nodes): score: Optional[float] = None if query_result.similarities is not None: score = query_result.similarities[index] nodes_with_scores.append(NodeWithScore(node=node, score=score)) return nodes_with_scores 接著,我們透過上面定義的 PineconeRetriever 建立一個檢索器,並嘗試使用它對剛才的 query_str 進行檢索: retriever = PineconeRetriever( vector_store, embed_model, query_mode="default", similarity_top_k=2 ) retrieved_nodes = retriever.retrieve(query_str) for node in retrieved_nodes: display_source_node(node, source_length=1000) 將 Retriever 插入 RetrieverQueryEngine 做回應生成 最後,我們將寫好的 PineconeRetriever 插入到 LlamaIndex 提供的 RetrieverQueryEngine 中,以生成最終回應。 提示:未來還可以替換成不同回應生成器、使用更複雜的 Template 或 Chain 等技術。 from llama_index.core.query_engine import RetrieverQueryEngine query_engine = RetrieverQueryEngine.from_args(retriever) response = query_engine.query(query_str) print(str(response)) 反向思考:為什麼要這樣做? 1. 為什麼要自訂 Retriever? • 直接使用高階 API (如 VectorStoreIndex.query)雖然方便,但有時候我們需要精細掌控查詢細節,或是想要在檢索前後做更多動作(如過濾文件、加入額外的檢索邏輯)。 2. 為什麼要把查詢與回應生成拆開? • 這樣可以輕易替換不同的檢索策略(或 Vector Store)而不影響回應生成邏輯,讓系統更具彈性。 3. 取得相似度有何用? • 相似度可以幫助我們理解檢索到的內容是否與查詢足夠相關;或在後續做融合(Reranking)等進階操作時使用。 Optional 進階教學 1. Sparse 或 Hybrid Search • 如果你的向量儲存工具(例如 Pinecone)支援稀疏向量,可以嘗試使用 "sparse" 或 "hybrid" 模式進行查詢,取得更豐富或多元的檢索結果。 2. 自訂相似度函數或指標 • 如果你想要以餘弦相似度 (cosine similarity) 或其他方式度量向量距離,可以調整 Pinecone Index 的 metric(或自行在程式中計算)。 3. 權重調整 • 在 Hybrid Search 情境下,可以對稀疏向量與 Dense 向量的權重做調整,以便產出更適合特定專案需求的檢索結果。 4. 文件更新與版本控管 • 若文件經常變動,如何有效地更新向量存儲與 Index?是否需要做版本控管或快照(snapshot)保留?這些都是在實際生產環境中常見的議題。 5. 查詢前的意圖識別 • 在一些進階應用中,若系統無法辨別使用者查詢的意圖,可以先經過一層意圖判斷後,選擇合適的 Retriever 或 Query 模式,進一步提升系統的品質。 希望透過上述步驟讓你對整個檢索流程與 Pinecone + LlamaIndex 的運作原理有更直觀的理解。也歡迎大家帶著問題與想法自行實驗並分享心得!
-
Felo - 最好用的AI Agent全方位no code工具!?來探討吧!
本文章將會針對 AI 初學者、行銷人員和開發者,並包含具體應用案例,讓不同背景的讀者都能輕鬆上手。 Felo AI 入門教學:輕鬆掌握多元 AI 搜尋引擎的魅力 現今網路資訊爆炸,Felo AI 應運而生,成為一款整合生成式 AI 的創新搜尋引擎。它由日本新創公司 Sparticle Inc. 開發,於 2024 年推出 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。Felo AI 最大的特色是跨語言的智慧搜尋,結合了尖端模型(例如 GPT-4)提供精準答案,同時清楚列出資訊來源供查證 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。不論你是 AI 初學者、行銷人員,或是開發者,Felo AI 都致力於用對話式介面和強大功能讓你用母語「搜尋全世界」,輕鬆獲取各領域的知識 (Felo Search 智慧搜尋與心智圖結合,提升使用體驗 | 原來可以這樣做沙龍) (Felo Search 智慧搜尋與心智圖結合,提升使用體驗 | 原來可以這樣做沙龍)。本篇將以輕鬆易懂的方式介紹 Felo AI 的基本功能、如何開始使用,以及針對不同背景讀者的應用案例、進階技巧與最佳實踐,幫助你快速上手這款全能 AI 工具。 Felo AI 的基本功能與特色 Felo AI 不只是傳統搜尋引擎的替代品,更像是一位智慧助理,可透過 ChatGPT 式對話回覆提問,同時提供可靠的資料來源佐證。以下是它的幾項核心功能與特色: 對話式搜尋:Felo AI 以聊天問答的形式呈現搜尋結果,就像和一個聰明的朋友對話,你只要輸入問題,系統能理解意圖並給出詳盡答案 (Felo Search 智慧搜尋與心智圖結合,提升使用體驗 | 原來可以這樣做沙龍)。相較一般搜尋只給連結,Felo AI 直接提供整合後的資訊,節省你點擊多個網頁的時間。 多語言支援與翻譯:跨語言能力是 Felo AI 的強項。你可以用母語發問,Felo AI 會搜尋全球資訊並自動將結果翻譯成你的語言 (Felo AI 與 SearchGPT 比較:跨語言學術搜尋... - 數位行銷學習|104學習精靈)。這打破語言障礙,非英語使用者也能無縫取得世界各地的知識 (Felo AI 與 SearchGPT 比較:跨語言學術搜尋... - 數位行銷學習|104學習精靈)。例如,用中文查詢英文論文時,Felo AI 可直接翻譯摘要內容,讓學術研究變得更簡單 (Felo AI 與 SearchGPT 比較:跨語言學術搜尋... - 數位行銷學習|104學習精靈)。其即時翻譯功能免去另開翻譯工具的麻煩,可直接讀懂外語網頁 (Felo AI 與 SearchGPT 比較:跨語言學術搜尋... - 數位行銷學習|104學習精靈)。 學術搜尋與知識庫:Felo AI 深耕學術領域,內建連結全球學術資料庫,支援搜尋論文並翻譯重點段落 (Felo AI 與 SearchGPT 比較:跨語言學術搜尋... - 數位行銷學習|104學習精靈)。這對研究人員或學生來說是一大福音,方便找到多國語言的學術資源。不僅如此,Felo AI 還提供知識組織工具,如「主題集」和「思維導圖」,幫助整理複雜資訊 (Felo AI 與 SearchGPT 比較:跨語言學術搜尋... - 數位行銷學習|104學習精靈)。相比傳統引擎只給結果列表,Felo AI 能將相關知識結構化,提升理解效率。 心智圖(思維導圖):最大亮點之一是自動心智圖功能。Felo AI 可將搜尋結果或對話內容生成視覺化的心智圖,讓資訊的關聯脈絡一目了然 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。透過思維導圖,使用者可以更容易掌握主題的全貌,特別適合釐清複雜概念 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。心智圖還支援編輯、下載,方便你加入自己的筆記或與他人分享 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。 來源透明與文件搜尋:每次 Felo AI 回答後,都會附上引用來源連結,方便你點擊查看原始內容 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。這種資訊來源透明度讓人更安心使用 AI 答案,也培養查證習慣。此外,Felo AI 內建網址摘要功能,只要貼上文章連結並輸入「summarize」摘要指令,就能快速獲得該頁面的重點概要 (Felo AI 與 SearchGPT 比較:跨語言學術搜尋... - 數位行銷學習|104學習精靈)。它也支援搜尋各種文件(PDF、Word、PPT 等),直接給出相關檔案結果,滿足多樣資訊需求 (Felo AI 與 SearchGPT 比較:跨語言學術搜尋... - 數位行銷學習|104學習精靈)。 無廣告、介面親和:為了提供專注的體驗,Felo AI 採用簡潔清爽的介面設計,幾乎沒有廣告干擾 (Felo Search 智慧搜尋與心智圖結合,提升使用體驗 | 原來可以這樣做沙龍)。而且完整支援繁體中文介面,包含選單、說明等,台灣使用者上手毫無障礙 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。介面同時提供淺色/深色模式切換,符合不同使用偏好 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。 以上這些功能使 Felo AI 成為「一站式」的智慧搜尋助手。例如,你可以直接用中文問「今年有什麼有趣的 AI 專案?」Felo AI 不僅會用對話形式彙整網上資訊回答,還可能自動生成一張相關專案的心智圖給你。對於想快速獲取知識又不想被資訊洪流淹沒的使用者而言,Felo AI 提供了高效率且直觀的解決方案。 如何註冊與開始使用 Felo AI 開始使用 Felo AI 非常簡單。你可以直接在瀏覽器中造訪官方網站(felo.ai),或下載行動 App(Android 與 iOS 皆有支援)進行體驗 (Felo Search 智慧搜尋與心智圖結合,提升使用體驗 | 原來可以這樣做沙龍)。以下是入門步驟: 註冊帳號:首次使用建議註冊一個免費帳號。點擊網站或 App 上的「登入/註冊」按鈕,可使用電子郵件快速註冊,或直接透過 Google、Apple 等第三方帳戶登入。註冊後方便保存你的搜尋歷史和主題收藏。不註冊也能直接試用基本搜尋功能,但登入帳號後才能充分體驗所有特色(如保存主題集)。 選擇搜尋模式:進入 Felo AI 主畫面後,你會看到一個大型的搜尋輸入框。介面語言會自動依據你的系統語言顯示(例如繁體中文)。在輸入框右側,有一個 「Pro」開關,代表是否使用進階模型搜尋。免費用戶每天擁有數次 Pro 模式的查詢額度,可用于更深入的提問。如果是一般簡單問題,關閉 Pro 開關即可進行快速搜尋(不消耗進階查詢額度)。建議先保留默認模式,待需要更詳細解答時再開啟 Pro 查詢。 (Felo Search 智慧搜尋與心智圖結合,提升使用體驗 | 原來可以這樣做沙龍)圖:Felo AI 網頁版的主畫面。畫面中央是對話式的搜尋欄,支持多語言提問;左側則有功能選單,包括對話搜尋、主題集和歷史記錄等。右上角提供登入/註冊入口,而下方醒目顯示「用您的語言搜尋世界」的標語,凸顯其跨語言搜尋特色。 進行第一次提問:在輸入框中鍵入你想查詢的問題或關鍵字。例如可以輸入「什麼是 Felo AI?」然後送出。幾秒鐘內,Felo AI 就會以對話形式給出答案,通常包含幾段文字解說和相關參考來源連結。在這個過程中,你彷彿直接在和 AI 交談,非常直覺。 查看回答與來源:閱讀 Felo AI 的回答內容。如果需要更深入細節,可以點擊答案中附上的【...†Lx-Ly】來源鏈接,跳轉閱讀原始文章或資料。這是 Felo AI 相較一般 chatbot 的優勢——它提供可追溯的來源,方便驗證資訊 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。 探索更多:在答案的下方,你可能會看到一些「相關問題」建議,或是一張「思維導圖」按鈕。你可以點擊這些來擴展學習。例如點擊心智圖按鈕,Felo AI 會把剛剛搜尋的主題繪製成一張樹狀圖,展示核心概念和延伸知識點,幫助你發現還沒想到的相關議題。 主題收藏:若這次搜尋涉及的內容你想留待日後參考,可以使用主題收藏功能(在搜尋結果頁面或回答旁邊通常有「加入主題」的按鈕)。將當前的結果儲存在一個自定義主題下,例如「學習Felo AI」,方便日後打開主題時瀏覽該主題下的所有相關內容 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。 初次體驗建議先從幾個有興趣的簡單問題開始,熟悉 Felo AI 的回答風格和功能版面。好消息是 Felo 提供了很慷慨的免費用量:根據官方資訊,免費方案除了無限次的一般快速搜尋外,每天還有約 5 次使用進階 GPT-4 等模型的專業搜尋額度,足夠滿足大部分日常需求 (Felo Search 智慧搜尋與心智圖結合,提升使用體驗 | 原來可以這樣做沙龍)。也就是說,不花錢也能享有 Felo AI 幾乎所有核心功能,包含全網搜尋、AI 對話、主題收藏等 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。當然,若你是重度使用者或對 GPT-4 等進階 AI 模型有大量需求,可以考慮訂閱 Felo Pro 計畫,以獲得每日數百次的進階查詢配額與更快的回應速度 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?) (Felo Search 智慧搜尋與心智圖結合,提升使用體驗 | 原來可以這樣做沙龍)。但對初學者而言,一開始使用免費版就絕對綽綽有餘。 在完成註冊和初步提問後,相信你已大致了解 Felo AI 的運作方式了。接下來,我們將介紹不同背景的使用者如何將 Felo AI 套用到實際應用場景中,發揮它的最大價值。 主要使用場景與應用案例 Felo AI 功能多元,適用於各種情境。以下將以三種類型的使用者為例,說明 Felo AI 能如何助你達成目標。 AI 初學者的學習夥伴 對於剛接觸人工智慧或相關領域的新手來說,Felo AI 是一位耐心又博學的老師。AI 初學者可以利用 Felo AI 來快速學習陌生概念、技術原理,或獲取最新趨勢資訊: 快速了解新知識:如果你聽說一個新名詞例如「生成對抗網路 (GAN)」,卻不清楚是什麼,不妨直接問 Felo AI:「GAN 是什麼?」。Felo AI 會給出簡明扼要的解釋,通常還會附上相關背景或應用說明,幫助你在幾分鐘內抓到重點。有用的是,回答底部還提供了深入閱讀的來源連結,方便你追蹤學術論文或科普文章以獲得更詳細資訊 (Felo Search 智慧搜尋與心智圖結合,提升使用體驗 | 原來可以這樣做沙龍)。這種答案+來源的形式,讓初學者能先獲得消化過的摘要,再自行研讀原文,學習效率大大提升。 多語言資源學習:很多 AI 知識儲存在英文資料中。以往英語不夠熟練的初學者可能被擋在門外,但 Felo AI 藉由強大的跨語言搜尋,直接將英文內容翻譯成中文給你 (Felo AI 與 SearchGPT 比較:跨語言學術搜尋... - 數位行銷學習|104學習精靈)。例如,你想學習一個 Python 機器學習函式庫的新函數,可以用中文詢問,Felo AI 會搜尋英文官方文件或國外討論,然後用中文回答重點,讓你無痛獲取英文世界的知識。必要時還可點開原文對照,逐步提升自己的英文閱讀能力。一來一往中,Felo AI 彷彿成了你的雙語教練。 結構化理解:初學新領域時,常覺得知識點雜亂無章。Felo AI 的思維導圖功能正好派上用場。每當你查詢一個較大的主題(如「深度學習演算法分類」),獲得回答後,不妨點擊「生成心智圖」。你將看到 Felo AI 把相關概念以樹狀圖形式列出,例如深度學習底下有哪些分支、每種演算法的關聯性如何等 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。這對視覺型學習者尤其有幫助——將零散資訊組織起來後,知識結構一目了然,加深記憶之餘也方便你找到後續進階的學習路徑。 循序對話探討:AI 初學者在自學過程中難免有許多連鎖疑問。Felo AI 支援上下文連貫的對話模式,讓你可以針對同一主題連續發問而不必每次重頭描述背景。例如你先問了「什麼是強化學習?」拿到基本答案後,接著追問「有沒有強化學習在遊戲中的成功案例?」Felo AI 會記住你前面的提問背景,直接給出相關案例的回答,省去你重複背景說明的麻煩 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。這種對話式的深入探討,宛如有個 24 小時待命的老師,陪你挖掘每一個好奇點。 總的來說,Felo AI 對 AI 初學者而言,就像一個智能學習助理。它友善地用你的語言解答問題,幫你跨越語言門檻取經全球知識,同時提供工具幫助理解和整理。在學習AI的路上,善用 Felo AI,你會發現自己可以更快掌握關鍵概念,也更有動力自行探索下去。 行銷人員的內容生成助手 對行銷、媒體或內容創作領域的人士來說,靈感與效率就是一切。Felo AI 恰好提供了一系列功能,幫助行銷人員在內容策劃、撰寫上如虎添翼: 內容靈感與文案產出:當你需要為產品撰寫宣傳文案或發想活動點子時,可以把 Felo AI 視作一個頭腦風暴夥伴。透過對話式提問,你可以要求它產生創意點子,例如:「幫我想三個強調環保的產品廣告標語」。得益於內建的 GPT-4 等大型模型,Felo AI 具備相當不錯的AI 寫作能力,可以輔助生成各類內容,包括撰寫文章段落、廣告標語、摘要重點甚至多語言翻譯等 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。這表示行銷人員在趕稿時,完全可以先讓 Felo AI 給出一版初稿,再加以人工潤色,極大提升效率。舉例而言,你可以請 Felo AI 「以輕鬆幽默的口吻介紹新品飲料」,它會產出一段草稿文字供你參考,為你的創作提供靈感火花 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?) (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。 社群趨勢調查:掌握市場和社群動態對行銷人員至關重要。Felo AI 整合了社群媒體搜尋功能,特別針對 Reddit、Twitter、小紅書等平台提供搜尋模式 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。你可以直接詢問「目前在討論度最高的手機相機功能是什麼?」Felo AI 會幫你搜羅 Reddit 上相關討論的重點意見,或抓取 Twitter 上的熱門話題摘要。這讓你不用逐個打開社群網站,就能一站掌握輿論趨勢 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?) (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。例如在新產品上市前,你可以透過 Felo AI 搜尋消費者對競品的評價亮點,迅速了解市場關注點並調整自己的行銷策略。 資料搜集與內容策展:在準備行銷提案或內容企劃時,常需要蒐集大量背景資料。Felo AI 的主題收藏功能非常實用——你可以將不同來源的搜尋結果彙整在一個主題檔案裡 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。假設你正在企劃一場活動,需要資料例如「Z 世代旅遊趨勢」「過去五年國旅統計」「競爭對手活動案例」等,只需各自搜索並將結果添加到「活動提案」主題集中。稍後你能在主題集中一次瀏覽所有相關資料,不必翻找歷史記錄,省時又便利。網址摘要功能同樣令內容策展更高效:面對一篇冗長的市場研究報告,只要把連結貼給 Felo AI,請它摘要重點,你很快就能取得報告的關鍵發現 (Felo AI 與 SearchGPT 比較:跨語言學術搜尋... - 數位行銷學習|104學習精靈)。 多國市場資訊:如果你的行銷業務涉足海外,Felo AI 的多語言優勢可以大展身手。比如你想了解日本市場某產品的口碑,可以用中文問「日本消費者如何評價XX產品?」,Felo AI 會搜尋日文網路上的評論並翻譯給你,整理出日本用戶的觀點摘要。這比起單純使用翻譯工具去讀日文網站有效許多,因為 Felo AI 幫你濃縮了重點意見。跨語言的市場調研在 Felo AI 上變得毫不費力,讓行銷團隊更容易取得國際視野。 總而言之,Felo AI 可作為行銷人員的內容生成與情報蒐集助手。無論是發想創意、撰寫文案,還是追蹤話題、整理資料,Felo AI 都能給予即時的支援與靈感。透過人機協作,行銷人員能更專注於創意本身,將繁瑣的資料收集與初稿撰寫交給 AI,加速產出高品質的行銷內容。 開發者的資訊探索利器 對軟體開發者和工程師而言,時間就是金錢。平時在解決技術問題、學習新工具時,常需要在網上搜尋文件或求助社群。Felo AI 為開發者提供了一套高效率的知識檢索工具,讓你更快找到答案並專注於寫程式本身: 技術問答與代碼範例:當你遇到程式錯誤或卡關,可以直接問 Felo AI,例如:「如何解決 Python 中的 MemoryError?」Felo AI 會自動搜尋 Stack Overflow、Reddit 等技術論壇上的相關討論,並以精簡的回答告訴你可能的原因和解決方案。同時列出的來源連結允許你點擊查看原始討論串,獲取更深入的背景(例如其他開發者的代碼片段或建議)。相比自己在論壇中大海撈針,Felo AI 幫你濃縮了眾多前輩的經驗,常常一兩句話就點出問題關鍵。對開發者來說,這種問答形式比傳統搜尋節省大量時間。 文件與 API 索引:開發新功能時難免需要翻閱技術文件(Documentation)。Felo AI 的文件搜尋能力可以直達目標——只要關鍵字夠明確,它能直接找到相關的官方文件頁面。例如你查詢「React useEffect 用法」,Felo AI 很可能直接給出 React 官方文件對 useEffect 鉤子的說明摘要,讓你快速了解用法細節。如果想看全文,只需點開引用來源即可跳轉官方手冊 (Felo AI 與 SearchGPT 比較:跨語言學術搜尋... - 數位行銷學習|104學習精靈)。更棒的是,若文件是英文的,你也可以請 Felo AI 幫忙翻譯其中關鍵段落,省去你自行理解翻譯的時間。多語言翻譯對開發者同樣適用,像是日本出的開源工具說明或法文博客文章,透過 Felo AI 都可跨語言檢索並閱讀重點內容。 範例代碼產生:除了搜尋現有知識外,Felo AI 還能運用其生成式 AI 模型,替你產生簡單的範例代碼。比如你問它「如何用 Python 實現快速排序?」Felo AI 有機會直接給出一段 Python 快速排序的示範程式碼,並附上講解步驟。當然,這些 AI 生成的代碼需要自行測試和斟酌,但作為參考範例能提供靈感。行內許多重複性的工具代碼(如格式轉換、API 調用樣板)都可以先問問 Felo AI,有時能節省手動翻文件或寫樣板的工夫。 主題研討與知識積累:對於大型專案的開發者團隊,Felo AI 的主題集功能也相當受用。如果你正在調研某項新技術,例如「微服務架構在金融業的應用」,可以建立一個該主題收藏,把平時找到的相關文章、論文、案例研究都存進去 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。團隊成員甚至可以共用這份主題集(將心智圖或重點摘要匯出分享),確保大家汲取同樣的資訊來源。長此以往,Felo AI 不僅解答了當下的疑難,還幫助開發者累積專屬的知識資料庫。下次遇到類似問題時,只要打開過往的主題收藏,就能迅速回顧並找到解決辦法。 可以看到,Felo AI 對開發者而言,更像是一款智能化的工程知識搜尋引擎。它把散落各處的技術經驗與文件智慧集中起來,跨語言、跨平台地提供給你。遇到Debug難題時,它是你的快速顧問;鑽研新技術時,它是你的資料嚮導。有了 Felo AI,相信開發者能把更多時間投入創造性工作,而不是困於漫長的搜尋與閱讀過程中。 進階功能與實用技巧 熟練掌握 Felo AI 的基本操作後,你還可以利用一些進階功能和技巧,進一步提升使用體驗: 主題收藏(Topic):如前文所述,將多個相關搜尋結果歸檔到「主題」是非常強大的功能。進階用法上,你可以為每個專案、每篇論文準備一個主題集,隨時把找到的資料加入其中。Felo AI 允許你在搜尋結果旁直接點選「加入主題」,新建或選擇既有主題分類 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。日後點開該主題時,其中累積的所有 Q&A 和連結都還在,相當於打造個人知識庫。建議定期整理主題集的內容,把無用的刪除、重要的加上註解,如此你的資訊庫會越來越精準好用。 AI 深度對話:Felo AI 不限於一問一答,還提供連貫對話的進階模式 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。在每次搜尋後,你都可以追問後續問題,AI 會記憶上下文。例如在主題收藏頁中,有一個「AI 對話」按鈕,點擊後你可以就該主題與 AI 展開連續對話 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。這對深入研究某個議題很有幫助——你可以圍繞主題反覆詢問不同角度,AI 的回答會逐步累積先前討論的知識,變得更加貼合你的探究方向。善用對話模式,可以挖掘出許多隱藏資訊或觀點,彌補單次提問的不足。 多元搜尋模式:在 Felo AI 的搜尋框附近,你可能注意到可選擇不同搜尋渠道或模式。例如全網搜尋(預設,一般性網路資源)、AI 對話(直接問 AI,不限定資料庫)、Reddit、X/Twitter、小紅書等社群 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。這些模式允許你針對不同來源進行定向搜尋。進階用戶可以根據需求切換:當你想獲取權威資料時,用全網搜尋或學術模式;想看社群風向時,切換 Reddit 或小紅書模式會很有效 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。例如做市場分析時,先用全網模式了解背景,再用社群模式抓取用戶真實反饋,兩相結合資訊更全面。熟悉各搜尋模式的特性,能讓你在 Felo AI 上如魚得水,精確找到最相關的答案。 思維導圖技巧:Felo AI 的心智圖除了自動生成外,你也可以做一些自定義操作。例如生成後的導圖支援節點編輯——你可以刪除某些不相關的分支,或加入你自行輸入的要點,以打造更貼合你理解的知識地圖 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。完成後,利用匯出功能將心智圖存為圖片帶走,插入到報告或簡報中,讓你的成果展示更加專業。熟練的用戶甚至會將多張心智圖組合,用於頭腦風暴會議的材料。一張由 AI 打底、人工補充修訂的思維導圖,往往能成為極佳的知識呈現工具。 內容生成與AI寫作:Felo AI 內建的 AI 模型不僅會回答問題,也可聽從指令生成內容。進階玩法上,你可以嘗試讓 Felo AI 幫你寫作:例如「請幫我寫一封邀請函給客戶,主旨是新品發布會。」或「替我將上述內容摘要成 100 字」。這類要求其實就是在調用 Felo AI 的AI 寫作功能 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。它可以產出草稿供你參考,節省你從零開始的時間。在翻譯方面也是如此,直接對 Felo 說「將上段文字翻譯成英文」,它會迅速給出譯文 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。這些生成類操作對 Felo AI 而言並非難事,充分利用可以提升你的工作效率。不過要注意的是,AI 生成內容有時措辭可能不完全符合你的期望,需要你再進行潤色修改以符合風格或細節正確。 跨平台與整合:別忘了,Felo AI 支援多平台使用。除了網頁介面,手機 App 方便你隨時查詢。而更有趣的是,在 Twitter(現稱為 X)上只要 @felosearch 發問,也能收到 Felo AI 的回答 ([ Felo - Search the world on the App Store ](https://apps.apple.com/us/app/felo-search-the-world/id6598782546#:~:text=edge%20RPA%20technology%20scours%20platforms,on%20Twitter%20with%20your%20questions))!這意味著你甚至不打開 App,也能直接在常用社群平台調用 Felo AI 的智慧。對重度使用者而言,可以將 Felo 設為預設搜尋引擎,在瀏覽器中直接以關鍵字觸發搜尋 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。總之,將 Felo AI 靈活嵌入你的日常工具鏈中,能讓 AI 如影隨形地提供幫助。 透過以上進階技巧,Felo AI 將不僅僅是一個Q&A工具,而會成為你日常工作流程的一部分。建議用戶抱著好奇心多嘗試,探索各種功能組合。例如,先用 Felo 找資料→存入主題集→生成心智圖整理→接著請 AI 寫作產出初稿,如此一條龍的操作將大大提高你的產出效率。隨著熟練度提高,你會不斷發現 Felo AI 更多貼心強大的用法。 最佳實踐與常見錯誤避免 為了充分發揮 Felo AI 的威力,並避免使用中的一些誤區,以下總結幾點最佳實踐和常見錯誤供參考: 1. 明確提問,逐步細化:儘量用清晰完整的句子向 Felo AI 提問。開始探索一個新主題時,先問大方向獲取概覽,再針對不清楚的部分提後續細節問題。切忌一上來丟一個模糊詞彙,這樣 AI 也難猜透你的需求。逐步細化問題的對話方式最能引導出有價值的答案。 2. 善用來源鏈接:Felo AI 每次答復都附帶了來源,不要忽視它們。最佳做法是在閱讀 AI 給的總結後,點開幾個關鍵來源快速瀏覽 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。這能驗證內容正確性,也可能從原文獲得額外背景。養成交叉查證的習慣,可以避免誤信 AI 生成的錯誤資訊。 3. 利用主題集整理知識:當你為某個專案或主題反覆搜索時,不要讓資訊散落在歷史紀錄中。正確做法是建立主題收藏,隨手把重要結果收入其中 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。日後複習時直接看主題集即可,省時省力。相反地,常見錯誤是每次需要用時再重新搜索,既浪費時間也可能漏掉之前找到的好資料。 4. 巧用心智圖與筆記:在進行學習或構思時,思維導圖能幫助整理思路。建議在獲得一定量資訊後就生成心智圖來梳理。如果發現導圖有不需要的部分,可以編輯精簡,使其成為你的專屬筆記圖表 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。避免只是看了一眼導圖就關閉,錯失將知識結構化的機會。 5. 留意免費額度與使用策略:Felo AI 免費版雖然功能完整,但每日進階查詢(Pro Search)有次數上限 (Felo Search 智慧搜尋與心智圖結合,提升使用體驗 | 原來可以這樣做沙龍)。最佳實踐是將簡單問題用一般模式解決,把寶貴的 Pro 次數留給需要深入分析的大哉問。常見的錯誤用法則是對任何小問題都開啟 GPT-4 模式詢問,不但浪費額度,有時過於龐大的模型反而回答未必更簡潔。瞭解自己需求的輕重,聰明分配查詢模式,才能持久享受免費用量。 6. 校對 AI 產生的內容:當你使用 Felo AI 進行內容創作(例如讓它寫段落或程式碼)後,務必親自校對修改。AI 給的初稿通常只是輔助參考 (2024最強AI搜尋引擎:Felo AI完整評測,為什麼它讓我放棄Perplexity?)。避免完全照單全收地使用 AI 文本,尤其在正式對外發布前,確認語氣、事實、細節都無誤才安心上線。把關內容品質仍然需要人工參與。 7. 保持隱私和安全:雖然 Felo AI 鼓勵你提出各種問題,但請避免在提問中輸入敏感個人資訊或機密資料。任何上傳網路的內容都有風險。使用 AI 工具時與使用其他線上服務一樣,注意保護個人隱私是基本常識。 8. 遇到問題善用說明:如果在使用過程中發現某功能不知如何操作,例如找不到心智圖按鈕、無法登入等,不妨查看 Felo 官方的常見問題(FAQ)或教學資源。官網部落格本身也有許多教程文章,可以搜尋關鍵詞找到對應說明(例如「心智圖教程」「AI 簡報功能」等) (如何使用 Felo AI 心智圖功能(教程) | Felo Search Blog)。善用這些學習資源,可以幫你更快掌握正確的用法,避開很多新手誤區。 最後提醒一點:AI 再強大也取代不了人的判斷。把 Felo AI 當作輔助工具而非絕對權威,保持批判思考,你才能用得既安心又高效。 經過上述介紹,相信你已對 Felo AI 有了相當全面的認識。這款結合了多語言搜尋、AI 對話與內容生成的全方位工具,對不同領域的使用者都相當友好。從回答日常疑問、學術輔助,到行銷創意發想、技術支援,Felo AI 都展現出強大的適應力和助益。如果你還沒嘗試過,不妨現在就動手體驗一下。在實際使用中探索功能,將理論轉化為實戰技能。可以預見,隨著 Felo AI 持續更新(開發團隊也不斷引入更新的 AI 模型與功能 (如何使用 Felo AI 心智圖功能(教程) | Felo Search Blog)),我們能做的事情只會越來越多。趕快善用這項工具,讓 AI 幫你輕鬆搜羅全世界的資訊,成為工作學習上的得力助手吧!
-
Promting 進階技巧整理
以下是我整理的一些promting 技巧, 上課前請各位去下載, 上課會直接講此投影片 以下是我對prompt coding+API調用的整理,放在我自己的教學Repo裏面。大家可以去fork https://github.com/kevin801221/prompt-eng-interactive-tutorial.git https://docs.google.com/presentation/d/1zCDJ33O4w7zT2053PojH1e7je7PrLQ0A501fidzO8RM/edit#slide=id.g31c41eee603_0_0
-
-
ch.2
-
AI Agent 開發特訓營:短期實現智能自動化-第三堂
-
有系統的整理AI Agent教學架構(可擴充)
探索 AI 代理人:設計與應用 系列文章 介紹(本篇) 從零開始建立 AI 代理人 AI 代理人框架 AI 代理人的類型 代理式設計模式 架構設計 多代理人系統 記憶與長期規劃 代理式 RAG 如何打造高效 AI 代理人 AI 代理人評估方法 目錄 1. 從 LLM 到 AI 代理人 1.1 傳統聊天機器人到 LLM 驅動的聊天機器人 1.2 LLM 聊天機器人的發展 1.3 從 LLM 聊天機器人到 RAG 代理人與 AI 代理人 2. 什麼是 AI 代理人? 2.1 AI 代理人的特徵 3. AI 代理人的核心組件 3.1 感知(感測器) 3.2 推理(處理器) 3.3 行動(執行器) 3.4 知識庫 3.5 學習能力 3.6 交互介面 4. AI 代理人如何與環境互動 4.1 感知階段 4.2 決策階段 4.3 行動階段 5. AI 代理人如何運作? 5.1 編排層(控制中心) 5.2 模型(大腦) 5.3 工具(執行手段) 6. ✅ 什麼情況適合使用 AI 代理人? / ⛔ 什麼情況應該避免? 7. AI 代理人的應用領域 8. 結論 LLM 與 AI 代理人的新時代 隨著大型語言模型(LLM)的發展,AI 技術取得了重大突破。這些強大的系統已經革新了自然語言處理,但當 LLM 與「代理性」(Agency)結合時,它們的真正潛力才得以釋放——能夠自主推理、規劃與行動。這正是 LLM 代理人(LLM Agents)的核心概念,也標誌著我們與 AI 互動方式的重大變革。Ref: letta 本篇文將全面介紹 AI 代理人,深入探討其特性、核心組件與類型,同時解析其發展歷程、挑戰及未來可能的發展方向。 從 LLM 到 AI 代理人 LLM 應用的演變速度驚人,其形態轉變是現代應用發展最快的領域之一。讓我們先來了解這個轉變的過程。mongodbmongodb 1.1 從傳統聊天機器人到 LLM 驅動的聊天機器人 聊天機器人並不新鮮,你可能在某些網站上與聊天機器人互動過,這在生成式 AI(Gen AI)這個術語出現之前就已經存在。傳統聊天機器人和現今 AI 驅動的對話代理人有著本質的不同。以下是它們的運作方式: 基於啟發式的回應 傳統聊天機器人依賴基於規則的邏輯(“如果-那麼”語句)。 只能處理預定義的規則,無法應對複雜或模糊的問題。 固定回應 回應是靜態且預定義的。 通常是透過檢測特定的關鍵詞或短語來觸發。 缺乏靈活性和對話深度。 人工轉接 會始終提供「轉接給人工」的選項來處理無法解決的問題。 複雜問題需要人工干預才能解決。1.2 LLM 驅動的聊天機器人介紹 ChatGPT 上線: 2022 年 11 月 30 日,OpenAI 推出了 ChatGPT,這是第一個主流的 LLM 應用,基於 GPT-3.5 技術。ChatGPT 保留了熟悉的聊天機器人界面,但背後運行的是強大的 LLM 技術,並且訓練自龐大的網路語料庫。 Transformer 架構: GPT(生成式預訓練變換器)基於 2017 年 Google 推出的 Transformer 架構,使用自注意力機制來分析輸入序列,能夠更深層次地理解上下文。 LLM 的能力: 與傳統聊天機器人不同,LLM 可以生成更具人類風格、情境相關且具有創新性的文本。其應用包括代碼生成、內容創作、增強的客戶服務等。 限制: 個性化:難以在長時間對話中維持一致且個性化的互動。 幻覺:可能生成事實上不正確但結構合理的回應,基於概率生成輸出,而非經過驗證的知識。 解決限制的方法: 像檢索增強生成(RAG)這樣的技術正在被探索,用以將輸出與可靠的外部數據進行對接。 這些進步旨在減少不準確性並提高 LLM 系統的穩定性。 1.3 從 LLM 驅動的聊天機器人到 RAG 聊天機器人與 AI 代理人 RAG 聊天機器人: 檢索增強生成(RAG)結合了外部數據檢索與 LLM 的能力,能生成準確且基於情境的回應。 知識來源: 非參數知識:來自外部來源(如互聯網或專有數據庫)的實時數據。 參數知識:LLM 訓練過程中內嵌的知識。 優勢: 減少幻覺現象,提供最新的信息,確保可驗證的回應。 提示工程: 例如上下文學習(一-shot、few-shot)、思維鏈(CoT)與 ReAct 等技術,通過引導 LLM 的推理與輸出生成來提高回應質量。AI 代理人 AI 代理人是從 LLM 演變而來,結合了工具使用、多步驟規劃和推理能力。 工具使用: LLM 能夠通過分析任務並通過結構化的模式(如 JSON)分配參數來調用程序定義的函數或 API。 環境: AI 代理人在迭代執行環境中運行,能夠根據反饋進行動態決策和持續調整。 代理式系統: 這些是具備自主決策能力的計算架構,能夠整合多個系統組件,做出決策並達成目標。 代理式 RAG: 將 LLM 的推理、工具使用和規劃能力與語義信息檢索相結合。 使系統能夠動態地分解任務、執行複雜查詢,並利用工具進行問題解決。從 LLM 驅動的聊天機器人到 RAG 聊天機器人和 AI 代理人的過渡,標誌著向更智能、適應性強、並能結合工具的系統轉變,這些系統能夠即時解決複雜的問題。 2. 什麼是 AI 代理人? AI 代理人是一種能夠通過感測器感知環境、處理信息並通過執行器對環境進行操作以達成特定目標的系統。可以把它當作一個數位實體,能觀察、思考並行動——就像人類與周圍環境互動一樣,但這是以編程的、有目的的方式進行的。 AI 代理人的概念建立在理性行為的基本理念之上:代理人應該採取最大化成功機會的行動來實現其既定目標。正是這種理性行為,使得 AI 代理人與簡單的回應式程序有所區別。 2.1 AI 代理人的特徵 AI 代理人具備幾個關鍵特徵: 自主性:無需人工干預,能獨立做出決策。 反應性與積極性行為:對環境變化作出反應,並採取積極措施以達成目標。 適應性:透過處理新資訊與經驗進行學習和演化。 目標導向:朝著預定目標或最佳化結果努力。 互動性:與其他代理人或人類進行溝通與協作。 持續性:持續運行,監控並回應動態環境。 3. AI 代理人的核心組件 AI 代理人本質上由以下組件構成: 感知 推理 行動 知識庫 學習 溝通介面 3.1 感知(感測器) 這些讓代理人能夠感知其環境。感知可以是物理感測器(如攝像頭、麥克風)或數位輸入(如數據流、用戶互動)。 3.2 推理(處理器) 代理人的“大腦”,負責處理來自感測器的信息並決定適當的行動。這個組件實現了代理人的決策算法,並保持必要的內部狀態。 AI 代理人使用各種決策機制,如基於規則的系統、專家系統和神經網絡,來做出明智的選擇並有效執行任務。 3.3 行動(執行器) 代理人通過執行行動來影響其環境,或簡單地使代理人能夠採取行動。這些行動可以是物理性的(如機器人手臂、揚聲器)或數位的(如數據庫更新、顯示輸出)。 3.4 知識庫 代理人用來做出決策的資訊庫,包含預設的知識和學習過的資訊。 3.5 學習 使代理人能夠通過數據和經驗不斷改善其性能。它使用如強化學習、監督學習和無監督學習等技術,隨著時間的推移提高 AI 代理人的表現。 3.6 溝通介面 允許代理人與其他代理人、系統或人類進行互動。 4. AI 代理人如何與環境互動 互動週期通常稱為「感知-規劃-行動」週期或「感知-行動」週期。讓我們以自駕車為例來理解每個階段: 感知-規劃-行動週期:以自駕車為例的智能系統互動模式 互動週期通常稱為「感知-規劃-行動」週期或「感知-行動」週期。讓我們以自駕車為例來理解每個階段: 什麼是感知-規劃-行動週期? 感知-規劃-行動(Perception-Planning-Action)週期是智能系統的核心運作模式,描述了系統如何持續地觀察環境、制定決策、執行動作,然後重複這個過程。這個週期是許多 AI 系統的基礎,從機器人到自駕車,再到智能代理都採用這種模式。 自駕車中的三個核心階段 1. 感知階段(Perception) 在感知階段,自駕車收集和處理來自環境的各種信息。 感測器系統 攝影機:捕捉視覺信息,識別車道線、交通標誌、行人和其他車輛 光達(LiDAR):生成周圍環境的3D點雲地圖,精確測量距離和物體形狀 雷達:檢測移動物體的速度和距離,在惡劣天氣中表現優異 超音波感測器:用於近距離檢測,特別是停車和低速行駛 GPS 和 IMU:提供位置和運動信息 資料處理 # 感知模組示例(偽代碼) class PerceptionModule: def process_sensor_data(self): # 攝影機資料處理 camera_data = self.camera.capture() detected_objects = self.object_detection(camera_data) # 光達資料處理 lidar_data = self.lidar.scan() point_cloud = self.process_point_cloud(lidar_data) # 雷達資料處理 radar_data = self.radar.detect() moving_objects = self.track_moving_objects(radar_data) # 融合多感測器資料 fused_data = self.sensor_fusion( detected_objects, point_cloud, moving_objects ) return self.create_world_model(fused_data) 感知輸出 靜態環境地圖:道路邊界、車道線、交通標誌 動態物體追蹤:其他車輛、行人、自行車的位置和速度 環境條件:天氣、光照、路面狀況 車輛狀態:當前位置、速度、方向 2. 規劃階段(Planning) 規劃階段基於感知到的信息制定行動策略,通常分為多個層次。 路線規劃(Route Planning) 全域路徑:從起點到終點的最優路線 考慮因素:交通狀況、道路限制、預估時間 行為規劃(Behavior Planning) 高層決策:是否變道、超車、停車、轉彎 交通規則遵循:紅綠燈、停止標誌、速度限制 情境理解:識別複雜的交通情況 運動規劃(Motion Planning) 軌跡生成:計算具體的行駛路徑 速度規劃:確定每個時刻的目標速度 避障規劃:動態避開障礙物 # 規劃模組示例(偽代碼) class PlanningModule: def plan_action(self, world_model, destination): # 行為規劃 behavior_decision = self.behavior_planner.decide( world_model, self.traffic_rules, self.current_goal ) # 運動規劃 if behavior_decision == "lane_change": trajectory = self.motion_planner.plan_lane_change( world_model, target_lane ) elif behavior_decision == "follow_road": trajectory = self.motion_planner.plan_lane_following( world_model, desired_speed ) elif behavior_decision == "stop": trajectory = self.motion_planner.plan_emergency_stop( world_model ) # 軌跡最佳化 optimized_trajectory = self.optimize_trajectory( trajectory, comfort_constraints, safety_constraints ) return optimized_trajectory 規劃輸出 目標軌跡:未來幾秒內的預定路徑 速度剖面:每個時間點的目標速度 控制指令:轉向角度、加速度、煞車力度 3. 行動階段(Action) 行動階段將規劃的決策轉化為實際的車輛控制指令。 控制系統 轉向控制:調整方向盤角度,確保車輛沿著規劃軌跡行駛 速度控制:控制油門和煞車,維持目標速度 穩定性控制:確保車輛在各種路面條件下保持穩定 執行器 電動轉向系統(EPS):精確控制車輛方向 電子煞車系統(EBS):提供精確的煞車力度 電子油門控制:調節發動機輸出功率 # 控制模組示例(偽代碼) class ControlModule: def execute_trajectory(self, planned_trajectory, current_state): # 計算控制誤差 lateral_error = self.calculate_lateral_error( planned_trajectory, current_state ) speed_error = self.calculate_speed_error( planned_trajectory, current_state ) # PID 控制器計算控制指令 steering_angle = self.lateral_controller.compute(lateral_error) throttle_brake = self.longitudinal_controller.compute(speed_error) # 安全檢查 steering_angle = self.safety_check_steering(steering_angle) throttle_brake = self.safety_check_speed(throttle_brake) # 發送控制指令 self.vehicle_interface.set_steering(steering_angle) self.vehicle_interface.set_throttle_brake(throttle_brake) return { "steering": steering_angle, "throttle_brake": throttle_brake, "timestamp": self.get_current_time() } 回饋機制 感測器回饋:監控執行結果 車輛狀態監控:確認控制指令正確執行 安全監控:檢測異常情況並觸發緊急措施 週期性運作 連續循環 這三個階段不是線性執行的,而是形成一個連續的循環: graph LR A[感知 Perception] --> B[規劃 Planning] B --> C[行動 Action] C --> A A1[收集感測器資料] --> A2[物體檢測與追蹤] A2 --> A3[環境建模] B1[路線規劃] --> B2[行為決策] B2 --> B3[軌跡生成] C1[控制指令計算] --> C2[執行器控制] C2 --> C3[狀態監控] 實時性要求 高頻率更新:通常以 10-100 Hz 的頻率運行 低延遲:從感知到行動的總延遲必須在毫秒級 並行處理:多個感測器和處理單元同時工作 實際運作示例 # 完整的自駕車控制循環 class AutonomousVehicle: def __init__(self): self.perception = PerceptionModule() self.planning = PlanningModule() self.control = ControlModule() self.safety_monitor = SafetyMonitor() def autonomous_driving_loop(self): while self.is_autonomous_mode_active(): try: # 感知階段 world_model = self.perception.process_sensor_data() # 安全檢查 if self.safety_monitor.is_emergency_situation(world_model): self.emergency_stop() continue # 規劃階段 planned_trajectory = self.planning.plan_action( world_model, self.destination ) # 行動階段 control_commands = self.control.execute_trajectory( planned_trajectory, self.get_current_state() ) # 記錄和監控 self.log_cycle_data(world_model, planned_trajectory, control_commands) # 等待下一個週期 self.wait_for_next_cycle() except Exception as e: self.handle_system_error(e) self.initiate_safe_mode() 挑戰與解決方案 技術挑戰 1. 感知挑戰 惡劣天氣條件:雨、雪、霧影響感測器性能 複雜環境:城市交通、施工區域、非標準路況 感測器融合:整合不同感測器的資料並處理衝突信息 解決方案: 多模態感測器組合 機器學習模型訓練 冗餘系統設計 2. 規劃挑戰 不確定性處理:其他駕駛員的不可預測行為 複雜情境:多車道變道、環形交叉路口 計算效率:在有限時間內找到最優解 解決方案: 機率推理和預測模型 分層規劃架構 啟發式演算法和機器學習 3. 控制挑戰 車輛動力學:不同車型的物理特性 路面條件:濕滑、顛簸路面的適應 硬體延遲:感測器和執行器的時間延遲 解決方案: 自適應控制演算法 模型預測控制 硬體最佳化 失效安全設計 感測器失效:自動切換到冗餘感測器 規劃失效:採用保守的預設策略 控制失效:緊急停車系統接管 其他應用領域 機器人系統 家用機器人:清潔、服務、陪伴機器人 工業機器人:製造、組裝、搬運機器人 探索機器人:太空、深海、危險環境探索 AI 代理系統 遊戲 AI:即時策略遊戲、角色扮演遊戲 智能助手:語音助手、聊天機器人 交易系統:演算法交易、風險管理 物聯網系統 智慧城市:交通管理、能源最佳化 智慧家居:環境控制、安全監控 農業自動化:精準農業、自動灌溉 未來發展趨勢 技術進步 邊緣計算:在感測器端進行預處理 5G 通訊:車聯網和即時資料交換 量子計算:解決複雜最佳化問題 系統整合 群體智能:多車協調和群體決策 基礎設施整合:智慧道路和車輛協同 雲端處理:大規模資料分析和模型更新 倫理和法律 決策透明度:可解釋的 AI 決策 責任歸屬:事故責任的法律框架 隱私保護:使用者資料的安全和隱私 總結 感知-規劃-行動週期是智能系統的核心運作模式,在自駕車等複雜系統中發揮著關鍵作用。通過持續的感知環境、智能規劃和精確執行,這些系統能夠在動態環境中安全可靠地運作。 隨著技術的不斷進步,我們可以期待看到更加智能、安全和高效的自主系統,它們將在各個領域為人類提供更好的服務和體驗。理解這個基本週期有助於我們設計和改進各種智能系統,從而推動人工智能技術的發展和應用。 4.1 感知階段 這是代理人“感知”的階段: 感測器 → 處理 → 狀態更新 代理人通過感測器接收輸入。 信息被處理並進行解釋。 根據新信息更新當前狀態。 4.2 決策階 這是代理人“思考”的階段: 當前狀態 + 目標 → 評估選項 → 選擇最佳行動 代理人評估可能的行動。 考慮目標和約束條件。 根據可用信息選擇最優行動。 4.3 行動階段 這是代理人“執行”的階段: 執行行動 → 觀察變化 → 開始新週期 通過執行器執行選擇的行動。 環境發生變化。 代理人通過感測器觀察結果,開始新一輪週期。 這個週期會持續重複,通常每秒會多次執行。這個週期的強大之處在於: 適應性:如果出現意外情況,代理人可以在下一個感知階段檢測到並相應地調整行動。 學習機會:代理人可以比較預測結果和實際結果,從而改進未來的決策。 目標導向行為:每個週期都將代理人推向其目標,同時遵守約束條件。 5. AI 代理人如何運作? 假設你的智能冰箱不僅在你缺少牛奶時重新訂購,還會根據你的瀏覽習慣建議改喝杏仁奶。這是有幫助還是有點不安?你來決定! 這就是 AI 代理人。 AI 代理人能夠理解人類語言(得益於 LLM),推理處理信息,規劃行動,並執行任務,無需不斷的人類介入。它們解決複雜問題,比起簡單的自動化工具,進步了很多。與基本的腳本不同,AI 代理人被整合到軟件系統中,能夠與環境進行複雜的互動。 AI 代理人與簡單自動化的區別 AI 代理人之所以不同,主要有兩個關鍵能力: 工具使用 規劃能力 你可能曾見過 ChatGPT 在基本數學問題上出錯。這是因為它僅根據訓練時的數據來回應。 同樣,如果我讓你計算 85 乘 65,作為人類,你可以直接回答(如果你知道答案),或者通過使用工具(比如計算器)來計算,對嗎? 你給代理人提供工具的使用權限也是一樣的。 第二個能力是規劃。 以相同的數學問題為例,只有當你知道乘法運算或知道如何將 85 和 65 與乘法這一參數輸入計算器時,你才會解決這個問題。 這就是規劃和推理的作用。 5.1 編排層(控制中心) 假設我想創建一個 AI 代理人會議排程器,我詢問排程器:「我想為所有學生舉辦一個網絡研討會」。 這將被視為觸發 AI 代理人的動作。 Orchestration layer | Credits: Himanshu Ramchandani 該查詢可以是文字、音頻、視頻或圖像。(你已經知道,無論數據的類型如何,它總是會轉換為數值,便於機器處理) 查詢將由編排層(即 AI 代理人的控制中心)處理。 編排層有四個主要功能: 記憶:維護整個互動過程中的記錄。 狀態:儲存整個過程的當前狀態。 推理:指導代理人的推理過程。 規劃:確定步驟,並決定接下來的步驟是什麼。 Credits: lyzr 5.2 模型(大腦) 模型是整個代理人的集中決策者。 它通常是像大型語言模型(LLM)這樣的 AI 模型。 Models in AI agents | Credits: Himanshu Ramchandani To understand the query, formulate a plan, and determine the next action, the model uses reasoning and logic frameworks like: ReAct(Reason + Act) ensures thoughtful and deliberate actionsChain-of-Thoughtreason through intermediate steps.Tree-of-Thoughtsexplores multiple paths to find the best solutionThe model determines what actions to take, and performs those actions using specific tools. 5.3 Tools (The Hands)Using tools the agent can interact with the external world. Like I told you, a calculator, APIs, web search, external databases, etc. 6. ✅ 何時使用代理人 / ⛔ 何時避免使用代理人 代理人在你需要讓 LLM 決定應用程序工作流程時非常有用。但它們往往是過度設計。關鍵問題是:我是否真的需要靈活的工作流程來高效解決當前的任務?如果預定的工作流程經常無法滿足需求,那意味著你需要更多的靈活性。 舉個例子:假設你正在開發一個處理顧客請求的應用程序,這是針對衝浪旅遊網站的顧客需求。 你可以預先知道,顧客的請求將屬於兩種情況之一(基於用戶選擇),並且你為這兩種情況有預定的工作流程: 想了解旅行資訊?⇒ 給他們提供搜索框以查詢你的知識庫 想與銷售人員聯繫?⇒ 讓他們填寫聯繫表格 如果這個確定性的工作流程能夠處理所有查詢,那麼當然只需要編寫代碼!這將為你提供一個 100% 可靠的系統,避免由於讓不可預測的 LLM 干預工作流程而引入錯誤。為了簡單和穩定,建議規範化處理,避免使用任何代理行為。 但是,如果無法那麼簡單地預先確定工作流程呢? 例如,某用戶想問:“我可以週一來,但我忘記了護照,可能會延遲到週三,能否在週二早上帶我和我的行李去衝浪並提供取消保險?”這個問題涉及很多因素,可能沒有任何預先確定的條件能夠解決這個請求。 如果預定的工作流程經常無法解決問題,那就意味著你需要更多的靈活性。 這時,代理式系統就能發揮作用。 在上面的例子中,你可以建立一個多步驟的代理人,讓它可以訪問天氣 API 來查詢天氣預報,使用 Google Maps API 計算旅行距離,查看員工的可用性,並且能在知識庫上運行 RAG 系統。 直到最近,計算機程序仍然限制於預定的工作流程,試圖通過大量的 if/else 條件來處理複雜性。它們專注於極為狹窄的任務,如“計算這些數字的總和”或“找到圖中最短的路徑”。但實際上,大多數現實生活中的任務(如我們的旅行例子)並不適合預定的工作流程。代理式系統讓程序能夠應對現實世界中各種複雜任務! 7. 應用領域 AI 代理人是多功能工具,可以在各個領域提高生產力、效率和智能。它們越來越多地被應用於日常應用和高端的高影響領域。 8. 結論 AI 代理人正在改變我們與技術的互動方式,提供前所未有的自主性、智能性和適應性。從簡單的反射型代理人到複雜的學習系統,它們在各行各業中被應用於解決複雜問題並提升人類能力。然而,構建有效的 AI 代理人也面臨挑戰,包括倫理問題、數據依賴性和可擴展性問題。 隨著 AI 技術的不斷發展,AI 代理人的未來充滿了巨大的潛力。通過聚焦於通用 AI、人機協作和倫理考量,我們可以創建出不僅能高效執行任務,還能與人類價值觀相契合並對社會作出積極貢獻的代理人。 AI 代理人是能夠感知、決策和行動以達成目標的自主系統。核心組件包括感測器、執行器、決策引擎和學習模塊。AI 代理人被應用於虛擬助手、自駕車和醫療等領域。通過理解基本原理並保持對進展的了解,我們可以利用 AI 代理人的力量推動創新,創造更好的未來。
-
Basic Langchain and RAG
直接進code學習 基礎 Langchain code for app llm chain : Chat models and prompts: retrievers : Build a semantic search engine over a PDF with document loaders, embedding models, and vector stores. classification : Classify text into categories or labels using chat models with structured outputs. extraction : Extract structured data from text and other unstructured media using chat models and few-shot examples. RAG : part1, PART2 code position: https://drive.google.com/drive/folders/1PTY3j1cgQ43LHTQazeMnWvKqNlgkG0lh?usp=share_link RAG from Scratch: https://drive.google.com/drive/folders/17qmUCpAj0KaBTHUT_2ZIahbbEkhRszjK?usp=share_link code RAG part1 : https://colab.research.google.com/drive/1WmsjaW0WHw8xi8GROG4EyN1GF4XQHy_b code RAG part2 : https://colab.research.google.com/drive/1wP2_p-z82DT0EijZzJYWrhJERuz4078K
-
Langgraph 重要基礎知識和應用
LangGraph 完整教學:從基礎到進階的 Agent 構建指南 什麼是 LangGraph? LangGraph 是一個用於構建可控 Agent 的低級編排框架。相比於 LangChain 提供的整合和可組合元件,LangGraph 專注於代理編排,提供可定製的架構、長期記憶體和人機協同功能,以可靠地處理複雜任務。 核心優勢 可靠性和可控性:通過審核檢查和人機協同審批來指導代理行動 低級且可擴展:使用完全描述性的低級基元構建自定義代理 一流的流媒體支援:支援逐個令牌流式處理和中間步驟流式處理 快速開始 安裝 pip install -U langgraph "langchain[anthropic]" 第一個簡單 Agent from langgraph.prebuilt import create_react_agent # 定義一個模擬的天氣資料抓取工具 def get_weather(city: str) -> str: """Get weather for a given city.""" return f"It's always sunny in {city}!" # 創建 Agent agent = create_react_agent( model="anthropic:claude-3-5-sonnet-latest", tools=[get_weather], prompt="You are a helpful assistant" ) # 執行 Agent response = agent.invoke({ "messages": [{"role": "user", "content": "what is the weather in sf"}] }) 核心概念與進階功能 1. 動態提示配置 LangGraph 支援靜態和動態兩種提示配置: 靜態提示: agent = create_react_agent( model="anthropic:claude-3-5-sonnet-latest", tools=[get_weather], prompt="Never answer questions about the weather." ) 動態提示: from langchain_core.messages import AnyMessage from langchain_core.runnables import RunnableConfig from langgraph.prebuilt.chat_agent_executor import AgentState def prompt(state: AgentState, config: RunnableConfig) -> list[AnyMessage]: user_name = config["configurable"].get("user_name") system_msg = f"You are a helpful assistant. Address the user as {user_name}." return [{"role": "system", "content": system_msg}] + state["messages"] agent = create_react_agent( model="anthropic:claude-3-5-sonnet-latest", tools=[get_weather], prompt=prompt ) 2. 多輪對話與記憶體 要實現多輪對話,需要配置檢查點程式(checkpointer): from langgraph.checkpoint.memory import MemorySaver checkpointer = MemorySaver() agent = create_react_agent( model="anthropic:claude-3-5-sonnet-latest", tools=[get_weather], checkpointer=checkpointer ) config = {"configurable": {"thread_id": "1"}} # 第一次對話 sf_response = agent.invoke( {"messages": [{"role": "user", "content": "what is the weather in sf"}]}, config ) # 第二次對話(會記住之前的內容) ny_response = agent.invoke( {"messages": [{"role": "user", "content": "what about new york?"}]}, config ) 3. 結構化回應 使用 response_format 參數生成符合特定架構的結構化回應: from pydantic import BaseModel class WeatherResponse(BaseModel): conditions: str agent = create_react_agent( model="anthropic:claude-3-5-sonnet-latest", tools=[get_weather], response_format=WeatherResponse ) response = agent.invoke({ "messages": [{"role": "user", "content": "what is the weather in sf"}] }) # 取得結構化回應 structured_data = response["structured_response"] 自訂 Agent 架構 基本圖形構建 from typing import Annotated from typing_extensions import TypedDict from langgraph.graph import StateGraph, START, END from langgraph.graph.message import add_messages class State(TypedDict): messages: Annotated[list, add_messages] # 建立圖形構建器 graph_builder = StateGraph(State) # 定義聊天機器人節點 def chatbot(state: State): return {"messages": [llm_with_tools.invoke(state["messages"])]} graph_builder.add_node("chatbot", chatbot) 添加工具節點 from langchain_core.messages import ToolMessage import json class BasicToolNode: """執行工具呼叫的節點""" def __init__(self, tools: list) -> None: self.tools_by_name = {tool.name: tool for tool in tools} def __call__(self, inputs: dict): if messages := inputs.get("messages", []): message = messages[-1] else: raise ValueError("No message found in input") outputs = [] for tool_call in message.tool_calls: tool_result = self.tools_by_name[tool_call["name"]].invoke( tool_call["args"] ) outputs.append( ToolMessage( content=json.dumps(tool_result), name=tool_call["name"], tool_call_id=tool_call["id"], ) ) return {"messages": outputs} tool_node = BasicToolNode(tools=[tool]) graph_builder.add_node("tools", tool_node) 條件邊(Conditional Edges) def route_tools(state: State): """根據工具呼叫情況決定下一步""" if isinstance(state, list): ai_message = state[-1] elif messages := state.get("messages", []): ai_message = messages[-1] else: raise ValueError(f"No messages found in input state: {state}") if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0: return "tools" return END # 添加條件邊 graph_builder.add_conditional_edges( "chatbot", route_tools, {"tools": "tools", END: END}, ) graph_builder.add_edge("tools", "chatbot") graph_builder.add_edge(START, "chatbot") # 編譯圖形 graph = graph_builder.compile() 進階功能 1. 持久化記憶體 同步版本 - SQLite Checkpointer from langgraph.checkpoint.memory import MemorySaver memory = MemorySaver() graph = graph_builder.compile(checkpointer=memory) 非同步版本 - AsyncSqliteSaver import asyncio from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver async def main(): db_path = "./production_chatbot_async.db" async with AsyncSqliteSaver.from_conn_string(db_path) as checkpointer: graph = graph_builder.compile(checkpointer=checkpointer) config = {"configurable": {"thread_id": "user_456"}} result = await graph.ainvoke( {"messages": [{"role": "user", "content": "Hi, I'm Bob"}]}, config=config ) print(result["messages"][-1].content) if __name__ == "__main__": asyncio.run(main()) 2. 人機協同控制(Human-in-the-Loop) from langchain_core.tools import tool from langgraph.types import Command, interrupt @tool def human_assistance(query: str) -> str: """Request assistance from a human.""" human_response = interrupt({"query": query}) return human_response["data"] # 設定工具 tools = [tool, human_assistance] llm_with_tools = llm.bind_tools(tools) def chatbot(state: State): message = llm_with_tools.invoke(state["messages"]) # 禁用並行工具調用以避免重複 assert len(message.tool_calls) <= 1 return {"messages": [message]} # 編譯圖形 memory = MemorySaver() graph = graph_builder.compile(checkpointer=memory) # 執行並處理中斷 config = {"configurable": {"thread_id": "1"}} # 開始對話 events = graph.stream( {"messages": [{"role": "user", "content": "I need expert guidance"}]}, config, stream_mode="values", ) # 當執行暫停時,提供人類回應 human_response = "We recommend using LangGraph for building reliable agents." human_command = Command(resume={"data": human_response}) # 恢復執行 events = graph.stream(human_command, config, stream_mode="values") 3. 自訂狀態欄位 from langchain_core.tools import InjectedToolCallId class State(TypedDict): messages: Annotated[list, add_messages] name: str birthday: str @tool def human_assistance( name: str, birthday: str, tool_call_id: Annotated[str, InjectedToolCallId] ) -> str: """Request human verification of information.""" human_response = interrupt({ "question": "Is this correct?", "name": name, "birthday": birthday, }) if human_response.get("correct", "").lower().startswith("y"): verified_name = name verified_birthday = birthday response = "Correct" else: verified_name = human_response.get("name", name) verified_birthday = human_response.get("birthday", birthday) response = f"Corrected: {human_response}" # 使用 Command 更新狀態 state_update = { "name": verified_name, "birthday": verified_birthday, "messages": [ToolMessage(response, tool_call_id=tool_call_id)], } return Command(update=state_update) 4. 時間旅行功能 LangGraph 的檢查點機制支援「時間旅行」,讓您可以回到對話的任何之前狀態: # 查看狀態歷史 for state in graph.get_state_history(config): print(f"訊息數量: {len(state.values['messages'])}") print(f"檢查點ID: {state.config.get('configurable', {}).get('checkpoint_id')}") # 從特定檢查點恢復 specific_config = { "configurable": { "thread_id": "1", "checkpoint_id": "特定的檢查點ID" } } # 從該時間點繼續執行 events = graph.stream(None, specific_config, stream_mode="values") 實際應用案例 完整的搜尋助手 from langchain_tavily import TavilySearch # 設定搜尋工具 tool = TavilySearch(max_results=2) tools = [tool] # 建立具有記憶體的搜尋助手 memory = MemorySaver() agent = create_react_agent( model="anthropic:claude-3-5-sonnet-latest", tools=tools, checkpointer=memory ) def stream_graph_updates(user_input: str): config = {"configurable": {"thread_id": "search_session"}} for event in agent.stream( {"messages": [{"role": "user", "content": user_input}]}, config ): for value in event.values(): print("Assistant:", value["messages"][-1].content) # 互動式對話循環 while True: try: user_input = input("User: ") if user_input.lower() in ["quit", "exit", "q"]: print("Goodbye!") break stream_graph_updates(user_input) except KeyboardInterrupt: break 視覺化與除錯 from IPython.display import Image, display # 視覺化圖形結構 try: display(Image(graph.get_graph().draw_mermaid_png())) except Exception: print("需要安裝額外的依賴來顯示圖形") 最佳實踐 記憶體管理:始終為需要多輪對話的應用程式配置檢查點程式 錯誤處理:在工具節點中添加適當的錯誤處理機制 狀態設計:仔細設計狀態結構,包含應用程式所需的所有資訊 人機協同:在關鍵決策點添加人類干預機制 測試與調試:使用 LangGraph Studio 進行視覺化調試 生態系統整合 LangSmith:提供代理評估和可觀察性 LangGraph 平台:專門為長時間運行的有狀態工作流構建的部署平台 LangGraph Studio:視覺化 IDE,支援工作流的交互和調試 總結 LangGraph 提供了一個強大而靈活的框架來構建複雜的 AI 代理系統。通過其狀態管理、檢查點機制和人機協同功能,您可以構建可靠、可控的智能應用程式。從簡單的聊天機器人到複雜的多代理系統,LangGraph 都能提供所需的工具和抽象。 開始您的 LangGraph 之旅,探索 AI 代理的無限可能!
-
Langgraph AgenticRAG basic
LangGraph Agentic RAG 完整教學:建構智能檢索代理系統 簡介 Agentic RAG(Retrieval-Augmented Generation)是一個強大的架構,結合了檢索增強生成和代理系統的概念。這個系統能夠智能地決定何時需要從知識庫中檢索資訊,何時可以直接回應用戶。本教學將逐步帶你了解如何使用 LangGraph 建構一個完整的 Agentic RAG 系統。 系統概述 核心功能 在這個教學中,我們將建立一個檢索代理,能夠: ✅ 智能判斷:自動決定是否需要從向量資料庫中檢索上下文 ✅ 相關性評估:評估檢索文件的相關性 ✅ 查詢最佳化:必要時重寫查詢以獲得更好的結果 ✅ 準確回答:基於檢索到的內容生成準確的回答 系統架構圖 graph TD A[開始] --> B[生成查詢或回應] B --> C{需要檢索?} C -->|是| D[檢索工具] C -->|否| E[結束] D --> F{文件相關?} F -->|是| G[生成答案] F -->|否| H[重寫問題] H --> B G --> E 環境設置 安裝必要套件 pip install -U --quiet langgraph "langchain[openai]" langchain-community langchain-text-splitters 設置 API 金鑰 import getpass import os def _set_env(key: str): if key not in os.environ: os.environ[key] = getpass.getpass(f"{key}: ") _set_env("OPENAI_API_KEY") 步驟 1:文件預處理 1.1 載入網路文件 使用 WebBaseLoader 從指定的 URL 載入文件: from langchain_community.document_loaders import WebBaseLoader # 定義要載入的文件 URL urls = [ "https://lilianweng.github.io/posts/2024-11-28-reward-hacking/", "https://lilianweng.github.io/posts/2024-07-07-hallucination/", "https://lilianweng.github.io/posts/2024-04-12-diffusion-video/", ] # 載入所有文件 docs = [WebBaseLoader(url).load() for url in urls] print(f"成功載入 {len(docs)} 個文件") 1.2 文件分割處理 將長文件分割成較小的區塊以便建立索引: from langchain_text_splitters import RecursiveCharacterTextSplitter # 展平文件列表 docs_list = [item for sublist in docs for item in sublist] # 配置文件分割器 text_splitter = RecursiveCharacterTextSplitter.from_tiktoken_encoder( chunk_size=100, # 每個區塊的大小 chunk_overlap=50 # 區塊間的重疊 ) # 執行分割 doc_splits = text_splitter.split_documents(docs_list) print(f"文件分割完成,共 {len(doc_splits)} 個區塊") 步驟 2:建立檢索工具 2.1 創建向量存儲 使用 OpenAI 嵌入模型創建記憶體向量存儲: from langchain_core.vectorstores import InMemoryVectorStore from langchain_openai import OpenAIEmbeddings # 初始化嵌入模型 embeddings = OpenAIEmbeddings() # 創建向量存儲 vectorstore = InMemoryVectorStore.from_documents( documents=doc_splits, embedding=embeddings ) # 創建檢索器 retriever = vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 5} # 返回最相似的 5 個文件 ) print("向量存儲建立完成") 2.2 包裝成檢索工具 將檢索器包裝成 LangChain 工具: from langchain.tools.retriever import create_retriever_tool retriever_tool = create_retriever_tool( retriever, "retrieve_blog_posts", "搜尋並返回關於 Lilian Weng 部落格文章的資訊。用於回答與機器學習、AI 研究相關的問題。", ) print("檢索工具創建完成") 步驟 3:定義圖形節點 3.1 生成查詢或回應節點 這是系統的主要決策節點,決定是否需要檢索外部資訊: from langgraph.graph import MessagesState from langchain.chat_models import init_chat_model # 初始化回應模型 response_model = init_chat_model("openai:gpt-4", temperature=0) def generate_query_or_respond(state: MessagesState): """ 呼叫模型生成回應。根據問題決定是否使用檢索工具。 如果問題需要特定的外部資訊,模型會選擇使用檢索工具。 如果問題可以直接回答,模型會直接生成回應。 """ print("🤖 正在分析查詢並決定是否需要檢索...") # 將檢索工具綁定到模型 model_with_tools = response_model.bind_tools([retriever_tool]) # 生成回應 response = model_with_tools.invoke(state["messages"]) return {"messages": [response]} 3.2 文件評分節點 評估檢索到的文件是否與查詢相關: from pydantic import BaseModel, Field from typing import Literal class GradeDocuments(BaseModel): """使用二元分數評分文件相關性""" binary_score: str = Field( description="相關性分數:'yes' 表示相關,'no' 表示不相關" ) # 初始化評分模型 grader_model = init_chat_model("openai:gpt-4", temperature=0) def grade_documents(state: MessagesState) -> Literal["generate_answer", "rewrite_question"]: """ 判斷檢索到的文件是否與問題相關 返回: - "generate_answer": 如果文件相關 - "rewrite_question": 如果文件不相關 """ print("📊 正在評估檢索文件的相關性...") # 提取問題和上下文 question = state["messages"][0].content last_message = state["messages"][-1] # 確保最後一條訊息包含檢索結果 if hasattr(last_message, 'content'): context = last_message.content else: context = str(last_message) # 評分提示 grading_prompt = f""" 用戶問題: {question} 檢索到的文件: {context} 請評估檢索到的文件是否與用戶問題相關。 如果文件包含回答問題所需的資訊,請回答 'yes'。 如果文件與問題不相關或無法提供有用資訊,請回答 'no'。 """ # 使用結構化輸出 structured_grader = grader_model.with_structured_output(GradeDocuments) score = structured_grader.invoke([{"role": "user", "content": grading_prompt}]) print(f"📋 相關性評分: {score.binary_score}") if score.binary_score == "yes": print("✅ 文件相關,準備生成答案") return "generate_answer" else: print("❌ 文件不相關,重寫問題") return "rewrite_question" 3.3 重寫問題節點 當檢索到的文件不相關時,重寫原始問題以改善檢索效果: def rewrite_question(state: MessagesState): """ 重寫原始用戶問題以改善檢索效果 """ print("✏️ 正在重寫問題以改善檢索效果...") messages = state["messages"] question = messages[0].content # 重寫提示 rewrite_prompt = f""" 原始問題: {question} 之前的檢索沒有找到相關資訊。請重寫這個問題,使其更適合從 Lilian Weng 的部落格文章中檢索資訊。 重寫要求: 1. 保持原始問題的核心意圖 2. 使用更具體的關鍵詞 3. 考慮 AI/ML 領域的術語 4. 讓問題更適合學術文章檢索 重寫後的問題: """ response = response_model.invoke([{"role": "user", "content": rewrite_prompt}]) print(f"🔄 重寫後的問題: {response.content}") return {"messages": [{"role": "user", "content": response.content}]} 3.4 生成答案節點 基於檢索到的相關內容生成最終答案: def generate_answer(state: MessagesState): """ 基於檢索到的上下文生成最終答案 """ print("📝 正在基於檢索內容生成答案...") # 提取問題和上下文 question = state["messages"][0].content last_message = state["messages"][-1] if hasattr(last_message, 'content'): context = last_message.content else: context = str(last_message) # 生成答案的提示 answer_prompt = f""" 基於以下檢索到的內容回答用戶問題。 用戶問題: {question} 檢索到的內容: {context} 請提供一個詳細、準確的答案。如果檢索內容不足以完全回答問題,請說明這一點。 答案: """ response = response_model.invoke([{"role": "user", "content": answer_prompt}]) print("✅ 答案生成完成") return {"messages": [response]} 步驟 4:組裝工作流程圖 4.1 建立圖形結構 from langgraph.graph import StateGraph, START, END from langgraph.prebuilt import ToolNode, tools_condition # 初始化狀態圖 workflow = StateGraph(MessagesState) # 添加節點 workflow.add_node("generate_query_or_respond", generate_query_or_respond) workflow.add_node("retrieve", ToolNode([retriever_tool])) workflow.add_node("rewrite_question", rewrite_question) workflow.add_node("generate_answer", generate_answer) print("🏗️ 工作流程節點添加完成") 4.2 定義邊和條件邊 # 添加起始邊 workflow.add_edge(START, "generate_query_or_respond") # 添加條件邊:決定是否需要檢索 workflow.add_conditional_edges( "generate_query_or_respond", tools_condition, { "tools": "retrieve", # 如果需要使用工具,去檢索節點 END: END, # 如果不需要工具,直接結束 }, ) # 添加條件邊:評估檢索結果 workflow.add_conditional_edges( "retrieve", grade_documents, { "generate_answer": "generate_answer", # 文件相關,生成答案 "rewrite_question": "rewrite_question", # 文件不相關,重寫問題 }, ) # 添加固定邊 workflow.add_edge("generate_answer", END) workflow.add_edge("rewrite_question", "generate_query_or_respond") print("🔗 工作流程邊定義完成") 4.3 編譯圖形 # 編譯工作流程圖 graph = workflow.compile() print("🎯 Agentic RAG 系統編譯完成!") 步驟 5:系統測試與運行 5.1 基本查詢測試 def test_agentic_rag(question: str): """測試 Agentic RAG 系統""" print(f"🔍 處理問題: {question}") print("=" * 50) # 執行查詢 for chunk in graph.stream( { "messages": [ { "role": "user", "content": question, } ] } ): for node, update in chunk.items(): print(f"📍 來自節點 '{node}' 的更新:") if "messages" in update and update["messages"]: last_message = update["messages"][-1] if hasattr(last_message, 'pretty_print'): last_message.pretty_print() else: print(last_message) print("-" * 30) # 測試不同類型的問題 test_questions = [ "Lilian Weng 對獎勵駭客的類型說了什麼?", "什麼是幻覺問題在大語言模型中?", "今天天氣如何?", # 這個問題不需要檢索 ] for question in test_questions: test_agentic_rag(question) print("\n" + "="*70 + "\n") 5.2 進階查詢範例 # 處理複雜查詢的完整示例 def run_complex_query(): """運行複雜查詢的範例""" complex_question = """ 請詳細解釋 Lilian Weng 文章中關於強化學習中獎勵駭客的主要觀點, 並說明這對 AI 安全有什麼影響? """ print("🚀 執行複雜查詢範例") print(f"問題: {complex_question}") print("=" * 70) # 執行查詢並收集結果 result = graph.invoke({ "messages": [ { "role": "user", "content": complex_question, } ] }) # 顯示最終答案 final_answer = result["messages"][-1].content print("🎯 最終答案:") print(final_answer) return result # 執行範例 complex_result = run_complex_query() 系統監控與除錯 添加詳細日誌 import logging from datetime import datetime # 設置日誌 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) def log_node_execution(node_name: str, state: MessagesState): """記錄節點執行狀態""" timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") message_count = len(state.get("messages", [])) logger.info(f"[{timestamp}] 節點 '{node_name}' 執行,當前訊息數: {message_count}") # 在每個節點函數中添加日誌記錄 def enhanced_generate_query_or_respond(state: MessagesState): log_node_execution("generate_query_or_respond", state) return generate_query_or_respond(state) # 類似地增強其他節點... 性能監控 import time class PerformanceMonitor: def __init__(self): self.start_time = None self.node_times = {} def start_timing(self): self.start_time = time.time() def record_node_time(self, node_name: str): if self.start_time: elapsed = time.time() - self.start_time self.node_times[node_name] = elapsed def get_summary(self): total_time = sum(self.node_times.values()) return { "total_time": total_time, "node_breakdown": self.node_times, "average_per_node": total_time / len(self.node_times) if self.node_times else 0 } # 使用範例 monitor = PerformanceMonitor() monitor.start_timing() # 執行查詢... result = graph.invoke({"messages": [{"role": "user", "content": "測試問題"}]}) # 顯示性能報告 performance_summary = monitor.get_summary() print(f"執行總時間: {performance_summary['total_time']:.2f} 秒") 最佳實踐與最佳化 1. 文件處理最佳化 # 進階文件分割策略 def create_optimized_splitter(content_type: str = "blog"): """根據內容類型創建最佳化的分割器""" if content_type == "blog": return RecursiveCharacterTextSplitter.from_tiktoken_encoder( chunk_size=200, chunk_overlap=50, separators=["\n\n", "\n", ". ", " ", ""] ) elif content_type == "academic": return RecursiveCharacterTextSplitter.from_tiktoken_encoder( chunk_size=500, chunk_overlap=100, separators=["\n\n", "\n", ". ", " ", ""] ) else: return RecursiveCharacterTextSplitter.from_tiktoken_encoder( chunk_size=300, chunk_overlap=75 ) # 智能文件標記 def add_metadata_to_documents(docs, source_info): """為文件添加元數據以改善檢索""" for doc in docs: doc.metadata.update({ "source_type": source_info.get("type", "unknown"), "domain": source_info.get("domain", "general"), "language": source_info.get("language", "en"), "creation_date": datetime.now().isoformat() }) return docs 2. 檢索策略改進 # 混合檢索策略 from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever def create_hybrid_retriever(doc_splits): """創建混合檢索器,結合語義和關鍵詞檢索""" # 語義檢索器 vectorstore = InMemoryVectorStore.from_documents( documents=doc_splits, embedding=OpenAIEmbeddings() ) semantic_retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) # 關鍵詞檢索器 keyword_retriever = BM25Retriever.from_documents(doc_splits) keyword_retriever.k = 3 # 組合檢索器 ensemble_retriever = EnsembleRetriever( retrievers=[semantic_retriever, keyword_retriever], weights=[0.7, 0.3] # 70% 語義,30% 關鍵詞 ) return ensemble_retriever # 使用混合檢索器 hybrid_retriever = create_hybrid_retriever(doc_splits) enhanced_retriever_tool = create_retriever_tool( hybrid_retriever, "enhanced_retrieve_blog_posts", "使用混合檢索策略搜尋 Lilian Weng 部落格文章" ) 3. 快取機制 from functools import lru_cache import hashlib class QueryCache: def __init__(self, max_size=100): self.cache = {} self.max_size = max_size def _hash_query(self, query: str) -> str: return hashlib.md5(query.encode()).hexdigest() def get(self, query: str): query_hash = self._hash_query(query) return self.cache.get(query_hash) def set(self, query: str, result): if len(self.cache) >= self.max_size: # 移除最舊的條目 oldest_key = next(iter(self.cache)) del self.cache[oldest_key] query_hash = self._hash_query(query) self.cache[query_hash] = result def clear(self): self.cache.clear() # 全域快取實例 query_cache = QueryCache() # 在檢索函數中使用快取 def cached_retrieve(query: str): # 檢查快取 cached_result = query_cache.get(query) if cached_result: print(f"🎯 快取命中: {query}") return cached_result # 執行檢索 result = retriever.get_relevant_documents(query) # 儲存到快取 query_cache.set(query, result) return result 擴展功能 1. 多語言支援 from langchain.schema import Document def detect_and_translate(text: str, target_language: str = "en"): """檢測文本語言並翻譯""" # 這裡可以整合翻譯服務 # 例如 Google Translate API 或 Azure Translator pass def multilingual_processing(docs: list[Document]): """多語言文件處理""" processed_docs = [] for doc in docs: # 檢測語言 detected_lang = "zh" # 簡化示例 # 如果不是英文,進行翻譯 if detected_lang != "en": translated_content = detect_and_translate(doc.page_content, "en") # 創建新文件,保留原始內容 new_doc = Document( page_content=translated_content, metadata={ **doc.metadata, "original_language": detected_lang, "original_content": doc.page_content } ) processed_docs.append(new_doc) else: processed_docs.append(doc) return processed_docs 2. 知識圖譜整合 class KnowledgeGraphRetriever: def __init__(self): self.entities = {} self.relationships = {} def extract_entities(self, text: str): """從文本中提取實體""" # 使用 NER 模型提取實體 # 這裡是簡化示例 pass def build_graph(self, documents): """構建知識圖譜""" for doc in documents: entities = self.extract_entities(doc.page_content) # 建立實體關係 # ... def graph_enhanced_retrieval(self, query: str): """基於知識圖譜的增強檢索""" # 1. 常規向量檢索 vector_results = retriever.get_relevant_documents(query) # 2. 提取查詢實體 query_entities = self.extract_entities(query) # 3. 圖譜擴展 related_entities = self.find_related_entities(query_entities) # 4. 組合結果 enhanced_results = self.combine_results(vector_results, related_entities) return enhanced_results 3. 用戶回饋學習 class FeedbackLearner: def __init__(self): self.feedback_data = [] def collect_feedback(self, query: str, response: str, rating: int, comments: str = ""): """收集用戶回饋""" feedback = { "query": query, "response": response, "rating": rating, "comments": comments, "timestamp": datetime.now().isoformat() } self.feedback_data.append(feedback) def analyze_feedback(self): """分析回饋數據""" if not self.feedback_data: return {} ratings = [f["rating"] for f in self.feedback_data] avg_rating = sum(ratings) / len(ratings) low_rated_queries = [ f for f in self.feedback_data if f["rating"] < 3 ] return { "average_rating": avg_rating, "total_feedback": len(self.feedback_data), "low_rated_count": len(low_rated_queries), "low_rated_queries": low_rated_queries } def improve_system(self): """基於回饋改進系統""" analysis = self.analyze_feedback() # 識別需要改進的查詢類型 problematic_queries = analysis.get("low_rated_queries", []) # 生成改進建議 suggestions = [] for query_data in problematic_queries: suggestion = { "original_query": query_data["query"], "issue": query_data["comments"], "improvement_needed": "檢索策略調整" } suggestions.append(suggestion) return suggestions # 使用回饋學習 feedback_learner = FeedbackLearner() # 模擬用戶回饋 feedback_learner.collect_feedback( "什麼是 reward hacking?", "獎勵駭客是指...", 4, "回答很好但可以更詳細" ) 部署與監控 1. 生產環境配置 from langchain.callbacks import get_openai_callback import os class ProductionRAGSystem: def __init__(self, config_path: str = None): self.config = self.load_config(config_path) self.setup_monitoring() self.setup_error_handling() def load_config(self, config_path): """載入生產環境配置""" default_config = { "model_name": "gpt-4", "temperature": 0, "max_tokens": 1000, "chunk_size": 200, "chunk_overlap": 50, "max_retrieval_documents": 5, "cache_enabled": True, "logging_level": "INFO" } if config_path and os.path.exists(config_path): # 載入外部配置檔案 pass return default_config def setup_monitoring(self): """設置監控""" self.metrics = { "total_queries": 0, "successful_queries": 0, "failed_queries": 0, "average_response_time": 0, "token_usage": 0 } def setup_error_handling(self): """設置錯誤處理""" self.error_handlers = { "retrieval_error": self.handle_retrieval_error, "generation_error": self.handle_generation_error, "timeout_error": self.handle_timeout_error } def handle_retrieval_error(self, error): """處理檢索錯誤""" print(f"檢索錯誤: {error}") return "抱歉,檢索系統暫時無法使用,請稍後再試。" def handle_generation_error(self, error): """處理生成錯誤""" print(f"生成錯誤: {error}") return "抱歉,回答生成過程中發生錯誤,請重新提問。" def handle_timeout_error(self, error): """處理超時錯誤""" print(f"超時錯誤: {error}") return "請求處理時間過長,請簡化您的問題後重試。" def process_query_with_monitoring(self, query: str): """帶監控的查詢處理""" start_time = time.time() self.metrics["total_queries"] += 1 try: with get_openai_callback() as cb: result = graph.invoke({ "messages": [{"role": "user", "content": query}] }) # 更新指標 self.metrics["successful_queries"] += 1 self.metrics["token_usage"] += cb.total_tokens response_time = time.time() - start_time self.update_average_response_time(response_time) return result except Exception as e: self.metrics["failed_queries"] += 1 error_type = type(e).__name__ if "retrieval" in str(e).lower(): return self.handle_retrieval_error(e) elif "timeout" in str(e).lower(): return self.handle_timeout_error(e) else: return self.handle_generation_error(e) def update_average_response_time(self, new_time): """更新平均回應時間""" total_successful = self.metrics["successful_queries"] current_avg = self.metrics["average_response_time"] # 計算新的平均值 self.metrics["average_response_time"] = ( (current_avg * (total_successful - 1) + new_time) / total_successful ) def get_health_status(self): """獲取系統健康狀態""" total_queries = self.metrics["total_queries"] success_rate = ( self.metrics["successful_queries"] / total_queries * 100 if total_queries > 0 else 0 ) return { "status": "healthy" if success_rate > 95 else "degraded", "success_rate": success_rate, "average_response_time": self.metrics["average_response_time"], "total_queries": total_queries, "token_usage": self.metrics["token_usage"] } # 初始化生產系統 production_system = ProductionRAGSystem() 2. API 服務化 from fastapi import FastAPI, HTTPException from pydantic import BaseModel import uvicorn app = FastAPI(title="Agentic RAG API", version="1.0.0") class QueryRequest(BaseModel): question: str user_id: str = None session_id: str = None class QueryResponse(BaseModel): answer: str confidence: float sources: list[str] processing_time: float @app.post("/query", response_model=QueryResponse) async def process_query(request: QueryRequest): """處理查詢請求""" try: start_time = time.time() # 執行 RAG 查詢 result = production_system.process_query_with_monitoring(request.question) processing_time = time.time() - start_time # 提取答案和來源 answer = result["messages"][-1].content if result["messages"] else "無法生成回答" # 計算信心分數(簡化版) confidence = 0.8 if len(answer) > 100 else 0.6 # 提取來源 sources = ["Lilian Weng Blog"] # 簡化版 return QueryResponse( answer=answer, confidence=confidence, sources=sources, processing_time=processing_time ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @app.get("/health") async def health_check(): """健康檢查端點""" return production_system.get_health_status() @app.get("/metrics") async def get_metrics(): """獲取系統指標""" return production_system.metrics # 啟動服務 if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=8000) 3. Docker 容器化 # Dockerfile FROM python:3.11-slim WORKDIR /app # 安裝系統依賴 RUN apt-get update && apt-get install -y \ gcc \ g++ \ && rm -rf /var/lib/apt/lists/* # 複製需求檔案 COPY requirements.txt . # 安裝 Python 依賴 RUN pip install --no-cache-dir -r requirements.txt # 複製應用程式碼 COPY . . # 暴露端口 EXPOSE 8000 # 設置環境變數 ENV PYTHONPATH=/app ENV PYTHONUNBUFFERED=1 # 啟動命令 CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] # docker-compose.yml version: '3.8' services: agentic-rag: build: . ports: - "8000:8000" environment: - OPENAI_API_KEY=${OPENAI_API_KEY} - LOG_LEVEL=INFO volumes: - ./logs:/app/logs - ./data:/app/data restart: unless-stopped redis: image: redis:alpine ports: - "6379:6379" volumes: - redis_data:/data restart: unless-stopped nginx: image: nginx:alpine ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf - ./ssl:/etc/nginx/ssl depends_on: - agentic-rag restart: unless-stopped volumes: redis_data: 測試與驗證 1. 單元測試 import unittest from unittest.mock import Mock, patch class TestAgenticRAG(unittest.TestCase): def setUp(self): """設置測試環境""" self.test_state = { "messages": [ {"role": "user", "content": "什麼是 reward hacking?"} ] } def test_generate_query_or_respond(self): """測試查詢生成節點""" with patch('langchain.chat_models.init_chat_model') as mock_model: # 模擬模型回應 mock_response = Mock() mock_response.content = "測試回應" mock_model.return_value.bind_tools.return_value.invoke.return_value = mock_response result = generate_query_or_respond(self.test_state) self.assertIn("messages", result) self.assertEqual(len(result["messages"]), 1) def test_grade_documents_relevant(self): """測試文件評分 - 相關情況""" test_state = { "messages": [ {"role": "user", "content": "什麼是 reward hacking?"}, Mock(content="Reward hacking 是指 AI 系統找到意外方法來最大化獎勵...") ] } with patch('langchain.chat_models.init_chat_model') as mock_model: mock_grader = Mock() mock_grader.binary_score = "yes" mock_model.return_value.with_structured_output.return_value.invoke.return_value = mock_grader result = grade_documents(test_state) self.assertEqual(result, "generate_answer") def test_grade_documents_irrelevant(self): """測試文件評分 - 不相關情況""" test_state = { "messages": [ {"role": "user", "content": "什麼是 reward hacking?"}, Mock(content="今天天氣很好...") ] } with patch('langchain.chat_models.init_chat_model') as mock_model: mock_grader = Mock() mock_grader.binary_score = "no" mock_model.return_value.with_structured_output.return_value.invoke.return_value = mock_grader result = grade_documents(test_state) self.assertEqual(result, "rewrite_question") # 運行測試 if __name__ == "__main__": unittest.main() 2. 整合測試 class IntegrationTest: def __init__(self): self.test_cases = [ { "question": "什麼是 reward hacking?", "expected_keywords": ["獎勵", "駭客", "AI", "強化學習"], "should_retrieve": True }, { "question": "今天天氣如何?", "expected_keywords": [], "should_retrieve": False }, { "question": "Lilian Weng 對 hallucination 的看法?", "expected_keywords": ["幻覺", "語言模型", "生成"], "should_retrieve": True } ] def run_integration_tests(self): """運行整合測試""" results = [] for i, test_case in enumerate(self.test_cases): print(f"執行測試案例 {i+1}: {test_case['question']}") try: # 執行查詢 result = graph.invoke({ "messages": [{"role": "user", "content": test_case["question"]}] }) # 驗證結果 answer = result["messages"][-1].content # 檢查是否包含預期關鍵詞 keywords_found = any( keyword.lower() in answer.lower() for keyword in test_case["expected_keywords"] ) test_result = { "test_case": i+1, "question": test_case["question"], "passed": keywords_found or not test_case["expected_keywords"], "answer_length": len(answer), "keywords_found": keywords_found } results.append(test_result) except Exception as e: results.append({ "test_case": i+1, "question": test_case["question"], "passed": False, "error": str(e) }) return results def generate_test_report(self, results): """生成測試報告""" passed_tests = sum(1 for r in results if r.get("passed", False)) total_tests = len(results) print(f"\n📊 測試報告") print(f"通過率: {passed_tests}/{total_tests} ({passed_tests/total_tests*100:.1f}%)") print("\n詳細結果:") for result in results: status = "✅ 通過" if result.get("passed", False) else "❌ 失敗" print(f" 測試 {result['test_case']}: {status}") if "error" in result: print(f" 錯誤: {result['error']}") # 執行整合測試 integration_test = IntegrationTest() test_results = integration_test.run_integration_tests() integration_test.generate_test_report(test_results) 3. 性能基準測試 import concurrent.futures import statistics class PerformanceBenchmark: def __init__(self, num_queries=100): self.num_queries = num_queries self.test_queries = [ "什麼是 reward hacking?", "解釋 AI hallucination 問題", "Diffusion model 的工作原理", "強化學習的挑戰", "語言模型的局限性" ] * (num_queries // 5) def single_query_benchmark(self, query): """單個查詢的基準測試""" start_time = time.time() try: result = graph.invoke({ "messages": [{"role": "user", "content": query}] }) end_time = time.time() return { "query": query, "response_time": end_time - start_time, "success": True, "answer_length": len(result["messages"][-1].content) } except Exception as e: return { "query": query, "response_time": time.time() - start_time, "success": False, "error": str(e) } def concurrent_benchmark(self, max_workers=5): """並發基準測試""" print(f"🚀 開始並發基準測試,{self.num_queries} 個查詢,{max_workers} 個並發") with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: futures = [ executor.submit(self.single_query_benchmark, query) for query in self.test_queries ] results = [] for future in concurrent.futures.as_completed(futures): result = future.result() results.append(result) if len(results) % 10 == 0: print(f"完成 {len(results)}/{self.num_queries} 個查詢") return results def analyze_results(self, results): """分析基準測試結果""" successful_results = [r for r in results if r["success"]] failed_results = [r for r in results if not r["success"]] if successful_results: response_times = [r["response_time"] for r in successful_results] answer_lengths = [r["answer_length"] for r in successful_results] analysis = { "total_queries": len(results), "successful_queries": len(successful_results), "failed_queries": len(failed_results), "success_rate": len(successful_results) / len(results) * 100, "avg_response_time": statistics.mean(response_times), "median_response_time": statistics.median(response_times), "min_response_time": min(response_times), "max_response_time": max(response_times), "avg_answer_length": statistics.mean(answer_lengths), "queries_per_second": len(successful_results) / sum(response_times) } return analysis else: return {"error": "所有查詢都失敗了"} def generate_benchmark_report(self, analysis): """生成基準測試報告""" print("\n📈 性能基準測試報告") print("=" * 50) print(f"總查詢數: {analysis['total_queries']}") print(f"成功查詢數: {analysis['successful_queries']}") print(f"成功率: {analysis['success_rate']:.2f}%") print(f"平均回應時間: {analysis['avg_response_time']:.2f} 秒") print(f"中位數回應時間: {analysis['median_response_time']:.2f} 秒") print(f"最快回應時間: {analysis['min_response_time']:.2f} 秒") print(f"最慢回應時間: {analysis['max_response_time']:.2f} 秒") print(f"平均答案長度: {analysis['avg_answer_length']:.0f} 字符") print(f"每秒查詢數 (QPS): {analysis['queries_per_second']:.2f}") # 執行性能基準測試 benchmark = PerformanceBenchmark(num_queries=50) benchmark_results = benchmark.concurrent_benchmark(max_workers=3) analysis = benchmark.analyze_results(benchmark_results) benchmark.generate_benchmark_report(analysis) 故障排除指南 常見問題與解決方案 1. 檢索質量問題 def diagnose_retrieval_issues(): """診斷檢索質量問題""" print("🔍 檢索質量診斷") # 測試查詢 test_query = "什麼是 reward hacking?" # 檢索文件 docs = retriever.get_relevant_documents(test_query) print(f"查詢: {test_query}") print(f"檢索到 {len(docs)} 個文件") # 分析檢索結果 for i, doc in enumerate(docs): print(f"\n文件 {i+1}:") print(f"內容長度: {len(doc.page_content)} 字符") print(f"內容預覽: {doc.page_content[:100]}...") # 檢查關鍵詞匹配 query_words = test_query.lower().split() doc_content = doc.page_content.lower() matches = [word for word in query_words if word in doc_content] print(f"關鍵詞匹配: {matches}") # 計算相似度分數(簡化版) similarity = len(matches) / len(query_words) print(f"相似度分數: {similarity:.2f}") # 執行診斷 diagnose_retrieval_issues() 2. 模型回應問題 def diagnose_model_issues(): """診斷模型回應問題""" print("🤖 模型回應診斷") # 測試不同溫度設置 temperatures = [0, 0.3, 0.7, 1.0] test_prompt = "請解釋什麼是人工智能。" for temp in temperatures: print(f"\n溫度設置: {temp}") test_model = init_chat_model("openai:gpt-4", temperature=temp) response = test_model.invoke([{"role": "user", "content": test_prompt}]) print(f"回應長度: {len(response.content)} 字符") print(f"回應預覽: {response.content[:100]}...") # 分析回應質量指標 sentences = response.content.split('。') avg_sentence_length = sum(len(s) for s in sentences) / len(sentences) print(f"平均句子長度: {avg_sentence_length:.1f} 字符") print(f"句子數量: {len(sentences)}") # 執行診斷 diagnose_model_issues() 3. 圖形流程問題 def diagnose_graph_flow(): """診斷圖形流程問題""" print("🔄 圖形流程診斷") # 追蹤每個節點的執行 class FlowTracker: def __init__(self): self.execution_log = [] def log_node_execution(self, node_name, input_data, output_data): self.execution_log.append({ "node": node_name, "timestamp": datetime.now().isoformat(), "input_size": len(str(input_data)), "output_size": len(str(output_data)) }) def print_execution_log(self): print("\n執行日誌:") for log_entry in self.execution_log: print(f" {log_entry['timestamp']}: 節點 '{log_entry['node']}'") print(f" 輸入大小: {log_entry['input_size']} 字符") print(f" 輸出大小: {log_entry['output_size']} 字符") # 使用流程追蹤器 flow_tracker = FlowTracker() # 測試查詢 test_query = "什麼是機器學習?" print(f"測試查詢: {test_query}") try: # 執行圖形並追蹤 for chunk in graph.stream({ "messages": [{"role": "user", "content": test_query}] }): for node_name, update in chunk.items(): flow_tracker.log_node_execution( node_name, {"messages": [{"role": "user", "content": test_query}]}, update ) print(f"✅ 節點 '{node_name}' 執行完成") flow_tracker.print_execution_log() except Exception as e: print(f"❌ 圖形執行失敗: {e}") flow_tracker.print_execution_log() # 執行診斷 diagnose_graph_flow() 總結 系統優勢 Agentic RAG 系統結合了智能決策和檢索增強生成的優勢,主要特點包括: 智能判斷能力 - 自動決定何時需要外部資訊 自我修正機制 - 能夠評估和改進檢索品質 靈活的工作流程 - 易於擴展和客製化 可觀測性 - 完整的監控和診斷能力 最佳實踐總結 文件處理 根據內容類型選擇合適的分割策略 為文件添加豐富的元數據 使用混合檢索策略提高準確性 模型配置 針對不同任務調整溫度參數 使用結構化輸出提高可靠性 實施適當的快取機制 系統監控 建立完整的指標收集系統 實施健康檢查和自動恢復 收集和分析用戶回饋 擴展性設計 模組化設計便於維護 支援水平擴展 實施適當的負載均衡 未來發展方向 技術改進 整合更先進的檢索技術 實施知識圖譜增強 添加多模態支援 用戶體驗 改善回應速度 提供更好的解釋性 支援對話式互動 企業級功能 增強安全和隱私保護 支援多租戶架構 實施完整的 RBAC 系統 資源與參考 LangGraph 官方文檔: https://langchain-ai.github.io/langgraph/ LangChain 文檔: https://docs.langchain.com/ OpenAI API 文檔: https://platform.openai.com/docs 向量資料庫比較: Pinecone, Weaviate, Chroma 希望這個完整的 Agentic RAG 教學能幫助您成功建立自己的智能檢索代理系統!如果您在實施過程中遇到任何問題,請參考故障排除指南或查閱相關文檔。
-
會議記錄專家SQLAgent
我用 LangGraph 做了個會說人話的 SQL Agent,再也不用寫查詢了 老闆:「欸,幫我查一下哪個部門最愛開會?」我:「好的」打開這個 Agent我:「哪個部門最愛開會?」Agent:「技術部開了 28 場會議,佔總數的 35%...」老闆:「...你怎麼這麼快?」我:😏 🤦♂️ 起因:又被 SQL 搞瘋了 你是不是也有過這種經歷: 手上有一堆會議記錄的 CSV,老闆突然要各種奇奇怪怪的數據分析。一下要「按部門統計」,一下要「找出會議之王」,一下又要「分析會議效率」。 每次都要: 打開 Excel/SQL 工具 想半天查詢語法 跑出結果發現格式不對 重新整理給老闆看 更慘的是,當老闆站在你旁邊等結果的時候... 那個壓力 😅 所以我想:能不能做一個會說人話的查詢工具? 直接問:「哪個部門最愛開會?」就得到答案,不用寫任何 SQL。 結果真的做出來了,而且比想像中還聰明。 💡 為什麼選 LangGraph 而不是普通 Agent? 剛開始我也想用 LangChain 的 ReActAgent 就好,簡單粗暴: # 看起來很簡單對吧? agent = create_react_agent(llm, tools, prompt) result = agent.invoke({"input": "哪個部門開會最多?"}) 但實際測試後發現一個嚴重問題:不可控。 有時候 Agent 會: 直接瞎猜答案(沒查資料庫) 跳過檢查表格結構的步驟 生成錯誤的 SQL 語法 回答跟問題完全無關的東西 這在 demo 時還好,但要給老闆用的話... 💀 LangGraph 的好處是可以強制執行工作流程: 必須先看資料庫有什麼表 ↓ 必須了解表格結構 ↓ 必須生成查詢 ↓ 必須檢查查詢安全性 ↓ 必須執行並格式化結果 每個步驟都不能跳過,可預測又可靠。 🛠️ 開工:設置環境 必要套件 pip install langchain langgraph langchain-openai langchain-community pip install pandas sqlite3 python-dotenv API Key 設置 去 OpenAI 申請一個 API key,然後: # 方法 1: 環境變數 export OPENAI_API_KEY="sk-your-key-here" # 方法 2: .env 檔案 echo "OPENAI_API_KEY=sk-your-key-here" > .env 測試資料準備 如果手邊沒有會議記錄,可以快速生成一些測試資料: import pandas as pd import random from datetime import datetime, timedelta def generate_meeting_data(n=100): departments = ['技術部', '行銷部', '人事部', '財務部', '產品部', '營運部'] meeting_types = ['專案會議', '週例會', '月報', 'Retro', '1on1', 'All hands'] hosts = ['Alice Chen', 'Bob Wang', 'Carol Liu', 'David Lin', 'Eve Wu'] priorities = ['高', '中', '低'] meetings = [] base_date = datetime.now() - timedelta(days=90) for i in range(n): meeting_date = base_date + timedelta(days=random.randint(0, 90)) duration = random.randint(30, 180) meeting = { '會議ID': f'MTG-{i+1:03d}', '會議主題': f'{random.choice(meeting_types)} - Week {i//7 + 1}', '會議類型': random.choice(meeting_types), '會議日期': meeting_date.strftime('%Y-%m-%d'), '開始時間': f'{random.randint(9,17)}:00', '結束時間': f'{random.randint(10,18)}:00', '會議時長_分鐘': duration, '主辦部門': random.choice(departments), '會議主持人': random.choice(hosts), '參與人數': random.randint(3, 20), '會議地點': f'{random.randint(1,5)}F-會議室{random.choice("ABCD")}', '會議優先級': random.choice(priorities), '會議狀態': random.choice(['已完成', '進行中', '已取消']), '會議費用': random.randint(0, 3000), '會議評分': round(random.uniform(2.0, 5.0), 1), '創建時間': meeting_date.strftime('%Y-%m-%d %H:%M:%S'), '最後更新時間': meeting_date.strftime('%Y-%m-%d %H:%M:%S') } meetings.append(meeting) return pd.DataFrame(meetings) # 生成測試資料 df = generate_meeting_data(100) df.to_csv('meeting_records.csv', index=False, encoding='utf-8') print(f"✅ 生成了 {len(df)} 筆測試資料") 🏗️ 核心架構:讓 Agent 有邏輯 整體設計思路 我設計的 SQL Agent 工作流程是這樣的: 用戶問問題 ↓ 列出所有資料表 (知道有什麼可以查) ↓ 獲取表格 schema (了解欄位結構) ↓ 生成 SQL 查詢 (根據問題和結構生成) ↓ 檢查查詢安全性 (避免危險操作) ↓ 執行查詢 (實際跑 SQL) ↓ 格式化結果 (讓答案更好看) ↓ 回傳給用戶 主要類別結構 import os import sqlite3 import pandas as pd import logging from typing import Literal from langchain_openai import ChatOpenAI from langchain_community.utilities import SQLDatabase from langchain_community.agent_toolkits import SQLDatabaseToolkit from langchain_core.messages import AIMessage, HumanMessage, SystemMessage from langgraph.graph import END, START, MessagesState, StateGraph from langgraph.prebuilt import ToolNode class MeetingSQLAgent: """會議記錄 SQL 查詢助手""" def __init__(self, csv_file_path: str, openai_api_key: str = None): # 設置 LLM if openai_api_key: os.environ["OPENAI_API_KEY"] = openai_api_key self.llm = ChatOpenAI( model="gpt-4o-mini", temperature=0, max_tokens=2000 ) # 建立資料庫 self._setup_database(csv_file_path) # 設置工具 self._setup_tools() # 建構工作流程 self._build_workflow() print("✅ SQL Agent 初始化完成") 資料庫設置 def _setup_database(self, csv_file_path): """將 CSV 轉換成 SQLite 資料庫""" try: # 讀取 CSV df = pd.read_csv(csv_file_path, encoding='utf-8') # 簡單的資料清理 df['會議日期'] = pd.to_datetime(df['會議日期'], errors='coerce') df['會議時長_分鐘'] = pd.to_numeric(df['會議時長_分鐘'], errors='coerce') df['會議評分'] = pd.to_numeric(df['會議評分'], errors='coerce') # 建立 SQLite 資料庫 self.db_path = "meeting_records.db" conn = sqlite3.connect(self.db_path) df.to_sql('meeting_records', conn, if_exists='replace', index=False) # 建立索引提升查詢效能 indexes = [ "CREATE INDEX IF NOT EXISTS idx_department ON meeting_records(主辦部門)", "CREATE INDEX IF NOT EXISTS idx_date ON meeting_records(會議日期)", "CREATE INDEX IF NOT EXISTS idx_host ON meeting_records(會議主持人)", "CREATE INDEX IF NOT EXISTS idx_type ON meeting_records(會議類型)" ] for idx_sql in indexes: conn.execute(idx_sql) conn.commit() conn.close() # 連接到 LangChain SQLDatabase self.db = SQLDatabase.from_uri(f"sqlite:///{self.db_path}") print(f"📊 成功載入 {len(df)} 筆會議記錄") except Exception as e: print(f"❌ 資料庫設置失敗: {e}") raise 🔧 工具設置:給 Agent 各種能力 def _setup_tools(self): """設置 SQL 相關工具""" toolkit = SQLDatabaseToolkit(db=self.db, llm=self.llm) self.tools = toolkit.get_tools() # 取得特定工具的參考 self.list_tables_tool = next(t for t in self.tools if t.name == "sql_db_list_tables") self.get_schema_tool = next(t for t in self.tools if t.name == "sql_db_schema") self.run_query_tool = next(t for t in self.tools if t.name == "sql_db_query") # 建立工具節點 self.get_schema_node = ToolNode([self.get_schema_tool], name="get_schema") self.run_query_node = ToolNode([self.run_query_tool], name="run_query") print("🔧 工具設置完成") 系統提示:給 Agent 個性和規則 這部分很重要,決定了 Agent 的行為模式: SYSTEM_PROMPT = """ 你是一個專門分析會議記錄的 SQL 助手。 ## 重要規則: 1. 我們使用 SQLite,不要用 MySQL 語法 (如 SHOW TABLES) 2. 中文欄位名要用反引號:`主辦部門`, `會議時長_分鐘` 3. 除非特別要求,否則限制結果:LIMIT 10 4. 絕對不要執行 DROP, DELETE, UPDATE 等危險操作 5. 查詢失敗時要給出有用的建議 ## 資料庫結構: meeting_records 表包含: - 基本資訊:會議ID, 會議主題, 會議類型, 會議日期 - 時間相關:開始時間, 結束時間, 會議時長_分鐘 - 人員相關:主辦部門, 會議主持人, 參與人數 - 評估相關:會議優先級, 會議狀態, 會議評分, 會議費用 ## 回答風格: - 用數據說話,提供具體數字 - 如果有趣的發現,主動提及 - 保持專業但友善的語調 記住:你的目標是幫助用戶快速獲得會議數據的洞察! """ 🎭 工作流程節點:每個步驟的實作 節點 1:列出資料表 def _list_tables(self, state: MessagesState): """列出資料庫中的所有表格""" tool_call = { "name": "sql_db_list_tables", "args": {}, "id": "list_tables_001", "type": "tool_call", } tool_call_message = AIMessage(content="", tool_calls=[tool_call]) tool_result = self.list_tables_tool.invoke(tool_call) response = AIMessage(f"📋 發現資料表:{tool_result.content}") return {"messages": [tool_call_message, tool_result, response]} 節點 2:獲取表格結構 def _call_get_schema(self, state: MessagesState): """強制呼叫 schema 工具獲取表格結構""" llm_with_tools = self.llm.bind_tools([self.get_schema_tool], tool_choice="any") instruction = SystemMessage(content=""" 請獲取 meeting_records 表的詳細結構資訊。 我們使用 SQLite,請用正確的語法獲取欄位資訊和樣本資料。 """) response = llm_with_tools.invoke([instruction] + state["messages"]) return {"messages": [response]} 節點 3:生成查詢 def _generate_query(self, state: MessagesState): """根據用戶問題生成 SQL 查詢""" system_message = SystemMessage(content=SYSTEM_PROMPT) llm_with_tools = self.llm.bind_tools([self.run_query_tool]) response = llm_with_tools.invoke([system_message] + state["messages"]) return {"messages": [response]} 節點 4:檢查查詢安全性 這一步很關鍵,避免執行危險的 SQL: def _check_query(self, state: MessagesState): """檢查 SQL 查詢的安全性和正確性""" safety_prompt = """ 作為 SQLite 專家,請檢查這個查詢是否安全且正確: 🚫 安全性檢查: - 是否使用了 SHOW 語句 (SQLite 不支援) - 是否有 DROP, DELETE, UPDATE 等危險操作 - 是否有 SQL 注入風險 ✅ 正確性檢查: - SQLite 語法是否正確 - 中文欄位名是否用反引號包圍 - 邏輯是否合理 如果有問題,請修正查詢。如果沒問題,保持原樣。 """ system_message = SystemMessage(content=safety_prompt) tool_call = state["messages"][-1].tool_calls[0] user_message = HumanMessage(content=f"請檢查:{tool_call['args']['query']}") llm_with_tools = self.llm.bind_tools([self.run_query_tool], tool_choice="any") response = llm_with_tools.invoke([system_message, user_message]) response.id = state["messages"][-1].id return {"messages": [response]} 節點 5:結果格式化 def _format_response(self, state: MessagesState): """格式化查詢結果,讓它更好看""" last_message = state["messages"][-1] if hasattr(last_message, 'content') and last_message.content: content = last_message.content # 處理錯誤訊息 if "Error" in content: formatted_content = f""" ❌ 查詢執行時出現錯誤: {content} 💡 建議: • 檢查問題描述是否清楚 • 試試更簡單的問題,如:「總共有多少場會議?」 • 確認要查詢的資料是否存在 """ else: # 美化正常結果 lines = content.split('\n') formatted_content = "📊 查詢結果:\n\n" for line in lines: if line.strip(): formatted_content += f"• {line.strip()}\n" formatted_message = AIMessage(content=formatted_content) return {"messages": [formatted_message]} return {"messages": []} 🔗 組裝工作流程 def _build_workflow(self): """建構 LangGraph 工作流程""" builder = StateGraph(MessagesState) # 添加所有節點 builder.add_node("list_tables", self._list_tables) builder.add_node("call_get_schema", self._call_get_schema) builder.add_node("get_schema", self.get_schema_node) builder.add_node("generate_query", self._generate_query) builder.add_node("check_query", self._check_query) builder.add_node("run_query", self.run_query_node) builder.add_node("format_response", self._format_response) # 定義工作流程 builder.add_edge(START, "list_tables") builder.add_edge("list_tables", "call_get_schema") builder.add_edge("call_get_schema", "get_schema") builder.add_edge("get_schema", "generate_query") builder.add_conditional_edges("generate_query", self._should_continue) builder.add_edge("check_query", "run_query") builder.add_edge("run_query", "format_response") builder.add_edge("format_response", END) # 編譯工作流程 self.agent = builder.compile() print("🔗 工作流程建構完成") def _should_continue(self, state: MessagesState) -> Literal[END, "check_query"]: """決定是否繼續執行""" messages = state["messages"] last_message = messages[-1] if not last_message.tool_calls: return END # 沒有工具呼叫,結束 else: return "check_query" # 有查詢需要檢查 主要查詢方法 def query(self, question: str) -> str: """執行查詢並返回結果""" try: print(f"🔍 處理問題:{question}") result = self.agent.invoke({ "messages": [HumanMessage(content=question)] }) final_message = result["messages"][-1] return final_message.content except Exception as e: return f"❌ 查詢失敗:{str(e)}" 🧪 實際測試:看看效果如何 基礎測試 def test_basic_queries(): """測試基本查詢功能""" # 初始化 Agent agent = MeetingSQLAgent("meeting_records.csv") test_cases = [ "總共有多少場會議?", "哪個部門開會最多?", "平均會議時間多長?", "誰主持最多會議?", "評分最高的 5 場會議是什麼?", "本月有多少場會議?" ] print("🧪 開始基礎測試...") for question in test_cases: print(f"\n❓ 問題:{question}") try: result = agent.query(question) print(f"✅ 回答:{result}") except Exception as e: print(f"❌ 失敗:{e}") print("-" * 50) if __name__ == "__main__": test_basic_queries() 進階測試 def test_complex_queries(): """測試複雜查詢""" agent = MeetingSQLAgent("meeting_records.csv") complex_questions = [ "分析各部門的會議效率(時長 vs 評分)", "找出可能需要優化的會議(時間長但評分低)", "比較不同會議類型的參與度", "統計每個月的會議趨勢", "找出最活躍的會議主持人 TOP 5" ] print("🔬 開始複雜查詢測試...") for question in complex_questions: print(f"\n🤔 複雜問題:{question}") result = agent.query(question) print(f"🧠 智能分析:{result}") print("=" * 60) 互動式測試 def interactive_test(): """互動式測試環境""" agent = MeetingSQLAgent("meeting_records.csv") print("🎯 歡迎使用會議記錄查詢助手!") print("💡 你可以用自然語言問任何關於會議數據的問題") print("🚪 輸入 'exit' 退出,'help' 查看範例") examples = [ "哪個部門最愛開會?", "會議平均多長時間?", "誰是開會之王?", "有什麼有趣的發現嗎?", "本週開了多少會議?" ] while True: user_input = input("\n❓ 你的問題:").strip() if user_input.lower() == 'exit': print("👋 掰掰!") break elif user_input.lower() == 'help': print("\n💡 範例問題:") for i, example in enumerate(examples, 1): print(f" {i}. {example}") continue elif not user_input: continue print("🤖 AI 思考中...") try: result = agent.query(user_input) print(f"📊 {result}") except Exception as e: print(f"💥 出錯了:{e}") # 執行互動測試 if __name__ == "__main__": interactive_test() 🐛 踩坑記錄:我遇到的問題 問題 1:SQLite vs MySQL 語法 錯誤現象: sqlite3.OperationalError: near "SHOW": syntax error [SQL: SHOW COLUMNS FROM meeting_records;] 原因:LLM 習慣生成 MySQL 語法,但我們用的是 SQLite 解決方案:在系統提示中明確指出使用 SQLite,並提供正確的語法範例: SQLITE_RULES = """ ⚠️ 重要:我們使用 SQLite,不是 MySQL! ❌ 錯誤語法: - SHOW TABLES - SHOW COLUMNS FROM table - LIMIT 0,10 ✅ 正確語法: - SELECT name FROM sqlite_master WHERE type='table' - PRAGMA table_info(table_name) - LIMIT 10 OFFSET 0 """ 問題 2:中文欄位名處理 錯誤現象: sqlite3.OperationalError: no such column: 主辦部門 解決方案:提醒 LLM 用反引號包圍中文欄位: CHINESE_FIELD_RULES = """ 🔤 中文欄位名稱處理: - 必須用反引號包圍:`主辦部門`, `會議時長_分鐘` - 不要用單引號或雙引號 - 欄位名稱要完全匹配,注意底線和空格 """ 問題 3:工作流程卡住 現象:Agent 有時會在某個節點卡住不動 排查方法: # 添加調試信息 def _debug_workflow_state(self, state, node_name): messages = state.get("messages", []) print(f"🔍 {node_name} 節點狀態:") print(f" 訊息數量: {len(messages)}") if messages: last_msg = messages[-1] print(f" 最後訊息類型: {type(last_msg)}") if hasattr(last_msg, 'tool_calls'): print(f" 工具呼叫: {len(last_msg.tool_calls) if last_msg.tool_calls else 0}") 解決方案:確保條件邊的邏輯正確,特別是 _should_continue 方法。 ⚡ 效能優化:讓它跑得更快 資料庫索引優化 def _create_performance_indexes(self, conn): """建立效能索引""" indexes = [ # 基礎索引 "CREATE INDEX IF NOT EXISTS idx_department ON meeting_records(`主辦部門`)", "CREATE INDEX IF NOT EXISTS idx_date ON meeting_records(`會議日期`)", "CREATE INDEX IF NOT EXISTS idx_host ON meeting_records(`會議主持人`)", # 複合索引 "CREATE INDEX IF NOT EXISTS idx_dept_date ON meeting_records(`主辦部門`, `會議日期`)", "CREATE INDEX IF NOT EXISTS idx_type_status ON meeting_records(`會議類型`, `會議狀態`)", # 數值範圍索引 "CREATE INDEX IF NOT EXISTS idx_duration ON meeting_records(`會議時長_分鐘`)", "CREATE INDEX IF NOT EXISTS idx_score ON meeting_records(`會議評分`)" ] for idx_sql in indexes: try: conn.execute(idx_sql) except Exception as e: print(f"⚠️ 索引建立失敗: {e}") conn.commit() print("🚀 效能索引建立完成") 查詢結果快取 from functools import lru_cache import hashlib class CachedMeetingSQLAgent(MeetingSQLAgent): """帶快取功能的 SQL Agent""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._query_cache = {} def _get_cache_key(self, question: str) -> str: """生成快取鍵""" return hashlib.md5(question.encode()).hexdigest() def query(self, question: str) -> str: """帶快取的查詢方法""" cache_key = self._get_cache_key(question) # 檢查快取 if cache_key in self._query_cache: print("⚡ 從快取回傳結果") return self._query_cache[cache_key] # 執行查詢 result = super().query(question) # 儲存到快取 self._query_cache[cache_key] = result return result LLM 呼叫優化 def _optimize_llm_calls(self): """優化 LLM 呼叫設定""" self.llm = ChatOpenAI( model="gpt-4o-mini", # 使用較快的模型 temperature=0, max_tokens=1500, # 減少 token 數量 timeout=30, # 設定超時 max_retries=2, # 減少重試次數 streaming=True # 啟用串流回應 ) 🛡️ 安全性:不讓 Agent 搞破壞 SQL 注入防護 ```pythondef _validate_query_safety(self, query: str) -> bool: """檢查查詢是否安全""" # 危險關鍵字檢查 dangerous_keywords = [ 'DROP', 'DELETE', 'UPDATE', 'INSERT',
-
-
RAG 組建的深入了解
想要進入真正的產品化的RAG應用,我們必續對其組件瞭如於心。所以這一張會全面瞭解RAG相關的組建,到AgenticRAG如何建構。
-
Langchain 詳細記憶方法整理
LangChain RAG Agent 記憶體管理完整教學指南 課程相關code我都上完到github空間中:請 git clone https://github.com/kevin801221/AgenticU-The-Modular-Teaching-Hub-for-Modern-LLM-Agent-Frameworks.git 去下載相關程式碼。(可以幫忙點個星星唷!) 目錄 前言與重要性 記憶體管理基礎概念 ConversationBufferMemory - 基礎完整記憶 ConversationBufferWindowMemory - 滑動視窗記憶 ConversationTokenBufferMemory - Token 限制記憶 ConversationSummaryMemory - 摘要記憶 ConversationSummaryBufferMemory - 混合摘要記憶 ConversationEntityMemory - 實體記憶 ConversationKGMemory - 知識圖譜記憶 VectorStoreRetrieverMemory - 向量檢索記憶 SQL 資料庫持久化記憶 實際應用場景與選擇建議 效能最佳化與注意事項 前言與重要性 在現代 AI 應用開發中,特別是構建能夠進行多輪對話的 RAG (Retrieval-Augmented Generation) 系統時,記憶體管理 是決定系統品質的關鍵因素之一。正如人類對話需要記住之前談論的內容才能保持連貫性,AI Agent 也需要某種形式的「記憶」來理解對話上下文,提供相關且一致的回應。 為什麼記憶體管理如此重要? 上下文連續性:確保 AI 能理解代詞指涉、話題延續等語言現象 個人化體驗:記住使用者偏好、歷史需求,提供客製化服務 效率最佳化:避免重複收集已知資訊,提升對話效率 一致性維持:確保 AI 在整個對話過程中保持一致的立場和知識狀態 LangChain 框架提供了豐富的記憶體管理機制,每種機制都針對不同的使用場景和技術限制進行了最佳化。本教學將深入探討這些機制的原理、實作和最佳實踐。 記憶體管理基礎概念 核心挑戰 在設計記憶體管理系統時,我們面臨幾個核心挑戰: Token 限制:大型語言模型都有上下文長度限制(如 GPT-4 的 8K 或 32K token) 計算成本:更長的上下文意味著更高的 API 調用成本 相關性:不是所有歷史對話都對當前問題有幫助 存儲效率:長期對話需要高效的存儲和檢索機制 LangChain 記憶體架構 LangChain 的記憶體系統基於以下核心組件: BaseMemory:所有記憶體類別的基礎抽象類別 ChatMessageHistory:負責實際的訊息存儲和檢索 Memory Variables:提供給 LLM 的上下文變數 # 基本記憶體介面 from abc import ABC, abstractmethod class BaseMemory(ABC): @abstractmethod def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]: """從記憶體載入相關變數""" pass @abstractmethod def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None: """將新的對話儲存到記憶體""" pass ConversationBufferMemory - 基礎完整記憶 概念說明 ConversationBufferMemory 是最直觀的記憶體實作,它將所有對話歷史完整地儲存在記憶體緩衝區中。這種方法確保了最完整的上下文保存,但可能導致 token 消耗過多的問題。 適用場景 短期對話:對話輪數較少(< 10 輪)的場景 開發測試:需要完整追蹤對話流程的開發階段 詳細分析:需要保留所有對話細節的應用 詳細實作 from langchain.memory import ConversationBufferMemory from langchain_openai import ChatOpenAI from langchain.chains import ConversationChain import logging # 設置日誌以追蹤記憶體使用情況 logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) # 初始化記憶體 memory = ConversationBufferMemory( return_messages=True, # 返回結構化的訊息對象 memory_key="history", # 在 prompt 中的變數名稱 human_prefix="Human", # 人類訊息的前綴 ai_prefix="Assistant", # AI 訊息的前綴 ) # 建立對話鏈 llm = ChatOpenAI( temperature=0, model_name="gpt-4o", max_tokens=1000 ) conversation = ConversationChain( llm=llm, memory=memory, verbose=True, # 顯示詳細的執行過程 ) # 模擬多輪對話 def simulate_conversation(): conversations = [ "你好,我想遠端開立一個銀行帳戶。該如何開始?", "我是台灣居民,有台灣身分證", "我需要準備哪些文件?", "開戶後多久可以使用網路銀行?" ] for i, user_input in enumerate(conversations, 1): logger.info(f"=== 對話輪次 {i} ===") response = conversation.predict(input=user_input) print(f"用戶: {user_input}") print(f"助手: {response}\n") # 檢查記憶體狀態 memory_vars = memory.load_memory_variables({}) logger.info(f"記憶體中的對話數: {len(memory_vars['history'])}") # 執行對話模擬 simulate_conversation() # 詳細檢查記憶體內容 print("=== 完整記憶體內容 ===") memory_content = memory.load_memory_variables({}) for i, message in enumerate(memory_content["history"]): print(f"{i+1}. {message.type}: {message.content}") 進階配置選項 # 自定義記憶體配置 custom_memory = ConversationBufferMemory( return_messages=True, memory_key="chat_history", input_key="question", # 指定輸入鍵 output_key="answer", # 指定輸出鍵 human_prefix="客戶", # 自定義前綴 ai_prefix="客服專員", ) # 手動操作記憶體 custom_memory.save_context( inputs={"question": "請問營業時間?"}, outputs={"answer": "我們的營業時間是週一到週五 9:00-17:00"} ) # 清空記憶體 custom_memory.clear() 優缺點分析 優點: 實作簡單,概念直觀 保留完整對話脈絡 無資訊遺失風險 缺點: Token 消耗可能快速增長 長對話可能超出模型限制 計算成本隨對話長度線性增長 ConversationBufferWindowMemory - 滑動視窗記憶 概念說明 滑動視窗記憶是對基礎緩衝記憶的改進,它只保留最近的 k 輪對話。這種方法在保持相關上下文的同時,有效控制了記憶體大小和 token 消耗。 核心原理 採用 FIFO (First In, First Out) 策略: 當對話輪數 ≤ k 時,保留所有對話 當對話輪數 > k 時,移除最舊的對話 始終維持記憶體中有最新的 k 輪對話 詳細實作 from langchain.memory import ConversationBufferWindowMemory from langchain_openai import ChatOpenAI from langchain.chains import ConversationChain class WindowMemoryAnalyzer: def __init__(self, window_size=3): self.memory = ConversationBufferWindowMemory( k=window_size, # 視窗大小 return_messages=True, memory_key="history" ) self.llm = ChatOpenAI(temperature=0, model_name="gpt-4o") self.conversation = ConversationChain( llm=self.llm, memory=self.memory, verbose=False ) self.conversation_count = 0 def chat(self, message): """進行對話並分析記憶體狀態""" self.conversation_count += 1 print(f"\n=== 對話 {self.conversation_count} ===") print(f"輸入: {message}") response = self.conversation.predict(input=message) print(f"回應: {response}") # 分析記憶體狀態 self._analyze_memory_state() return response def _analyze_memory_state(self): """分析並顯示當前記憶體狀態""" memory_vars = self.memory.load_memory_variables({}) history = memory_vars.get("history", []) print(f"記憶體狀態: 保留 {len(history)} 則訊息") for i, msg in enumerate(history): prefix = "🔵" if msg.type == "human" else "🤖" print(f" {prefix} {msg.content[:50]}...") def demonstrate_window_effect(self): """示範滑動視窗效果""" test_messages = [ "我叫 Alice,今年 25 歲", "我是軟體工程師", "我住在台北", "我喜歡看電影", "我最近在學習 AI", "請告訴我我的基本資訊" # 測試記憶體保留情況 ] for msg in test_messages: self.chat(msg) print("\n=== 最終記憶體內容 ===") final_memory = self.memory.load_memory_variables({}) for msg in final_memory["history"]: print(f"{msg.type}: {msg.content}") # 執行示範 analyzer = WindowMemoryAnalyzer(window_size=3) analyzer.demonstrate_window_effect() 動態視窗大小調整 class AdaptiveWindowMemory(ConversationBufferWindowMemory): """自適應視窗大小的記憶體""" def __init__(self, base_window_size=3, max_window_size=10): super().__init__(k=base_window_size, return_messages=True) self.base_window_size = base_window_size self.max_window_size = max_window_size self.importance_scores = [] def save_context(self, inputs, outputs): # 計算對話重要性(簡化範例) importance = self._calculate_importance(inputs, outputs) self.importance_scores.append(importance) # 根據重要性調整視窗大小 if importance > 0.8: # 高重要性對話 self.k = min(self.k + 1, self.max_window_size) elif importance < 0.3: # 低重要性對話 self.k = max(self.k - 1, self.base_window_size) super().save_context(inputs, outputs) def _calculate_importance(self, inputs, outputs): """計算對話重要性(示範用的簡化版本)""" # 實際應用中可以使用更複雜的評分機制 text = str(inputs) + str(outputs) keywords = ["重要", "關鍵", "問題", "解決", "幫助"] score = sum(1 for keyword in keywords if keyword in text) return min(score / len(keywords), 1.0) 最佳實踐 # 針對不同場景的視窗大小建議 WINDOW_SIZE_RECOMMENDATIONS = { "客服對話": 5, # 需要記住近期問題和回答 "教學輔導": 7, # 需要跟蹤學習進度 "閒聊機器人": 3, # 輕量級對話 "技術支援": 10, # 可能需要參考較多技術細節 "醫療諮詢": 8, # 需要記住症狀和病史資訊 } def create_optimized_window_memory(use_case, custom_size=None): """根據使用場景創建最佳化的視窗記憶體""" window_size = custom_size or WINDOW_SIZE_RECOMMENDATIONS.get(use_case, 5) return ConversationBufferWindowMemory( k=window_size, return_messages=True, memory_key="history" ) ConversationTokenBufferMemory - Token 限制記憶 概念說明 Token 緩衝記憶體是最精確的記憶體管理方式,它直接根據 token 數量而非對話輪數來管理記憶體。這種方法特別適合需要精確控制 API 成本和模型輸入長度的應用。 技術原理 Token 計算:使用 tiktoken 庫精確計算文本的 token 數量 動態截斷:當總 token 數超過限制時,從最舊的對話開始移除 實時監控:每次保存新對話時都會檢查 token 限制 詳細實作 from langchain.memory import ConversationTokenBufferMemory from langchain_openai import ChatOpenAI import tiktoken from typing import List, Dict class TokenMemoryManager: """Token 記憶體管理器""" def __init__(self, max_token_limit=1000, model_name="gpt-4o"): self.llm = ChatOpenAI(model_name=model_name, temperature=0) self.memory = ConversationTokenBufferMemory( llm=self.llm, max_token_limit=max_token_limit, return_messages=True, memory_key="history" ) # 初始化 tokenizer self.encoding = tiktoken.encoding_for_model(model_name) self.max_token_limit = max_token_limit def add_conversation(self, human_message: str, ai_message: str): """添加對話並分析 token 使用情況""" print(f"\n=== 添加新對話 ===") print(f"人類: {human_message}") print(f"AI: {ai_message}") # 計算新對話的 token 數 human_tokens = len(self.encoding.encode(human_message)) ai_tokens = len(self.encoding.encode(ai_message)) new_tokens = human_tokens + ai_tokens print(f"新對話 Token 數: {new_tokens} (人類: {human_tokens}, AI: {ai_tokens})") # 保存對話前的記憶體狀態 before_memory = self.memory.load_memory_variables({}) before_count = len(before_memory.get("history", [])) before_tokens = self._calculate_total_tokens(before_memory.get("history", [])) # 保存新對話 self.memory.save_context( inputs={"input": human_message}, outputs={"output": ai_message} ) # 保存對話後的記憶體狀態 after_memory = self.memory.load_memory_variables({}) after_count = len(after_memory.get("history", [])) after_tokens = self._calculate_total_tokens(after_memory.get("history", [])) # 分析變化 self._analyze_memory_change( before_count, after_count, before_tokens, after_tokens, new_tokens ) def _calculate_total_tokens(self, messages: List) -> int: """計算訊息列表的總 token 數""" total = 0 for message in messages: total += len(self.encoding.encode(message.content)) return total def _analyze_memory_change(self, before_count, after_count, before_tokens, after_tokens, new_tokens): """分析記憶體變化""" print(f"\n📊 記憶體分析:") print(f" 對話數量: {before_count} → {after_count}") print(f" Token 使用: {before_tokens} → {after_tokens}") print(f" Token 限制: {self.max_token_limit}") print(f" 使用率: {after_tokens/self.max_token_limit*100:.1f}%") if after_count < before_count + 1: removed_count = (before_count + 1) - after_count print(f" ⚠️ 移除了 {removed_count} 則舊對話以符合 Token 限制") if after_tokens + new_tokens > self.max_token_limit: print(f" ✂️ 觸發 Token 限制,進行記憶體截斷") def demonstrate_token_management(): """示範 Token 記憶體管理""" # 創建較小的 token 限制以便觀察截斷效果 manager = TokenMemoryManager(max_token_limit=500) # 模擬長對話 conversations = [ ("什麼是機器學習?", "機器學習是人工智慧的一個子領域,它使電腦能夠在沒有被明確程式設計的情況下學習和改進。"), ("深度學習和機器學習有什麼區別?", "深度學習是機器學習的一個子集,使用多層神經網路來模擬人腦的運作方式,能夠處理更複雜的模式識別任務。"), ("請解釋什麼是神經網路?", "神經網路是由互相連接的節點(神經元)組成的計算模型,這些節點模擬生物神經系統的工作方式。"), ("什麼是卷積神經網路?", "卷積神經網路(CNN)是一種特殊的神經網路架構,特別適合處理圖像資料,使用卷積層來提取特徵。"), ("請解釋反向傳播演算法", "反向傳播是訓練神經網路的核心演算法,它通過計算梯度來調整網路權重,從而最小化預測誤差。"), ("什麼是過擬合?", "過擬合是指模型在訓練資料上表現很好,但在新資料上表現不佳的現象,通常是因為模型過於複雜。") ] for human_msg, ai_msg in conversations: manager.add_conversation(human_msg, ai_msg) # 顯示最終記憶體狀態 print("\n" + "="*50) print("最終記憶體內容:") final_memory = manager.memory.load_memory_variables({}) for i, message in enumerate(final_memory["history"]): print(f"{i+1}. {message.type}: {message.content}") # 執行示範 demonstrate_token_management() 進階 Token 管理策略 class AdvancedTokenMemory(ConversationTokenBufferMemory): """進階 Token 記憶體管理""" def __init__(self, llm, max_token_limit, reserve_ratio=0.2): super().__init__(llm=llm, max_token_limit=max_token_limit, return_messages=True) self.reserve_ratio = reserve_ratio # 為新對話保留的 token 比例 self.priority_keywords = ["重要", "關鍵", "緊急", "問題"] def save_context(self, inputs, outputs): """智能保存上下文""" # 計算新對話的 token 數 new_content = str(inputs) + str(outputs) new_tokens = len(self.llm.get_num_tokens(new_content)) # 確保為新對話保留足夠空間 reserved_tokens = int(self.max_token_limit * self.reserve_ratio) available_tokens = self.max_token_limit - reserved_tokens # 如果需要,進行智能截斷 if self._get_current_token_count() + new_tokens > available_tokens: self._smart_truncate(available_tokens - new_tokens) super().save_context(inputs, outputs) def _smart_truncate(self, target_tokens): """智能截斷:優先保留重要對話""" messages = self.chat_memory.messages scored_messages = [] for i, msg in enumerate(messages): importance_score = self._calculate_importance(msg.content) scored_messages.append((i, msg, importance_score)) # 按重要性排序(降序) scored_messages.sort(key=lambda x: x[2], reverse=True) # 保留最重要的對話直到達到目標 token 數 kept_messages = [] current_tokens = 0 for _, msg, _ in scored_messages: msg_tokens = len(self.llm.get_num_tokens(msg.content)) if current_tokens + msg_tokens <= target_tokens: kept_messages.append(msg) current_tokens += msg_tokens # 按原始順序重新排列 kept_messages.sort(key=lambda x: messages.index(x)) self.chat_memory.messages = kept_messages def _calculate_importance(self, content): """計算內容重要性""" score = 0 content_lower = content.lower() # 關鍵字權重 for keyword in self.priority_keywords: score += content_lower.count(keyword) * 2 # 長度權重(較長的內容可能包含更多資訊) score += min(len(content) / 100, 3) # 問句權重(問題通常比較重要) if "?" in content or "?" in content: score += 1 return score ConversationSummaryMemory - 摘要記憶 概念說明 摘要記憶體使用 LLM 將冗長的對話歷史壓縮成簡潔的摘要,這種方法在保留關鍵資訊的同時大幅減少 token 消耗。特別適合需要長期記憶但對成本敏感的應用。 技術原理 逐步摘要:每次新增對話時,將所有歷史對話重新摘要 資訊壓縮:使用 LLM 的理解能力提取對話要點 上下文保持:摘要保留對話的核心脈絡和關鍵資訊 詳細實作 from langchain.memory import ConversationSummaryMemory from langchain_openai import ChatOpenAI from langchain.schema import BaseMessage import time class SummaryMemoryAnalyzer: """摘要記憶體分析器""" def __init__(self, model_name="gpt-4o"): self.llm = ChatOpenAI( model_name=model_name, temperature=0, max_tokens=500 ) # 自定義摘要提示 summary_prompt = """ 請將以下對話歷史總結成簡潔但完整的摘要,保留所有重要資訊和關鍵細節: 對話歷史: {history} 摘要應該: 1. 保留所有重要的事實和數據 2. 維持對話的邏輯脈絡 3. 突出關鍵決定和結論 4. 使用繁體中文 摘要: """ self.memory = ConversationSummaryMemory( llm=self.llm, return_messages=False, # 返回摘要文本而非訊息對象 memory_key="history", prompt_template=summary_prompt ) self.conversation_count = 0 self.token_usage = [] def add_conversation_turn(self, human_input: str, ai_response: str): """添加對話輪次並分析摘要效果""" self.conversation_count += 1 print(f"\n{'='*60}") print(f"對話輪次 {self.conversation_count}") print(f"{'='*60}") # 記錄添加前的狀態 before_summary = self.memory.load_memory_variables({}).get("history", "") before_tokens = len(self.llm.get_num_tokens(before_summary)) if before_summary else 0 print(f"🗣️ 人類: {human_input}") print(f"🤖 AI: {ai_response}") # 測量摘要生成時間 start_time = time.time() # 保存對話(觸發摘要生成) self.memory.save_context( inputs={"input": human_input}, outputs={"output": ai_response} ) end_time = time.time() summary_time = end_time - start_time # 記錄添加後的狀態 after_summary = self.memory.load_memory_variables({}).get("history", "") after_tokens = len(self.llm.get_num_tokens(after_summary)) # 分析摘要效果 self._analyze_summary_effect( before_summary, after_summary, before_tokens, after_tokens, summary_time ) # 記錄 token 使用情況 self.token_usage.append({ "turn": self.conversation_count, "tokens": after_tokens, "generation_time": summary_time }) def _analyze_summary_effect(self, before_summary, after_summary, before_tokens, after_tokens, summary_time): """分析摘要效果""" print(f"\n📋 摘要分析:") print(f" 摘要生成時間: {summary_time:.2f} 秒") print(f" Token 變化: {before_tokens} → {after_tokens}") if before_summary: compression_ratio = len(before_summary) / len(after_summary) if after_summary else 0 print(f" 文本壓縮比: {compression_ratio:.2f}x") print(f"\n📝 當前摘要:") print(f" {after_summary}") def demonstrate_summary_memory(self): """示範摘要記憶體的運作""" # 模擬一個詳細的客戶服務對話 conversations = [ ("我想申請信用卡,我的年收入是100萬台幣", "好的,年收入100萬符合我們的申請條件。請問您目前的職業是什麼?"), ("我是軟體工程師,在台積電工作已經3年了", "台積電是很好的公司。請問您目前有其他銀行的信用卡嗎?"), ("我有中國信託和玉山銀行的信用卡,都使用正常,沒有遲繳紀錄", "很好,良好的信用記錄對申請很有幫助。您希望申請哪種類型的信用卡?"), ("我經常出國出差,希望申請一張適合海外消費的信用卡", "我推薦我們的全球通信用卡,海外消費有2%現金回饋,而且免收海外交易手續費。"), ("聽起來不錯,申請流程需要多久?需要準備什麼文件?", "申請流程約7-10個工作天。您需要準備身分證、收入證明、工作證明,以及最近三個月的薪資單。"), ("好的,我現在可以線上申請嗎?", "可以的,我現在就為您開啟線上申請流程。請確認您的聯絡電話是否為09XX-XXX-XXX?") ] for human_msg, ai_msg in conversations: self.add_conversation_turn(human_msg, ai_msg) # 生成最終分析報告 self._generate_analysis_report() def _generate_analysis_report(self): """生成分析報告""" print(f"\n{'='*60}") print("摘要記憶體效能分析報告") print(f"{'='*60}") print(f"總對話輪次: {self.conversation_count}") if self.token_usage: avg_tokens = sum(usage["tokens"] for usage in self.token_usage) / len(self.token_usage) max_tokens = max(usage["tokens"] for usage in self.token_usage) min_tokens = min(usage["tokens"] for usage in self.token_usage) print(f"Token 使用統計:") print(f" 平均: {avg_tokens:.1f} tokens") print(f" 最大: {max_tokens} tokens") print(f" 最小: {min_tokens} tokens") avg_time = sum(usage["generation_time"] for usage in self.token_usage) / len(self.token_usage) print(f"平均摘要生成時間: {avg_time:.2f} 秒") # 估算成本效益 estimated_cost_without_summary = self.conversation_count * 50 * 0.00003 # 假設每輪50 tokens current_avg_tokens = avg_tokens if self.token_usage else 0 estimated_cost_with_summary = self.conversation_count * current_avg_tokens * 0.00003 print(f"\n💰 成本分析:") print(f" 不使用摘要預估成本: ${estimated_cost_without_summary:.6f}") print(f" 使用摘要實際成本: ${estimated_cost_with_summary:.6f}") print(f" 節省比例: {(1 - estimated_cost_with_summary/estimated_cost_without_summary)*100:.1f}%") # 執行摘要記憶體示範 analyzer = SummaryMemoryAnalyzer() analyzer.demonstrate_summary_memory() 自定義摘要策略 from langchain.prompts import PromptTemplate class CustomSummaryMemory(ConversationSummaryMemory): """自定義摘要記憶體""" def __init__(self, llm, summary_type="detailed"): # 定義不同類型的摘要模板 summary_templates = { "detailed": """ 請將以下對話歷史詳細總結,保留所有重要資訊: {history} 總結要求: 1. 保留所有數字、日期、名稱等具體資訊 2. 維持事件的時間順序 3. 突出重要決定和行動項目 4. 保留情感色彩和態度 詳細總結: """, "concise": """ 請將以下對話歷史簡潔總結,只保留核心要點: {history} 簡潔總結(50字內): """, "structured": """ 請將以下對話歷史結構化總結: {history} 結構化總結: 【參與者】: 【討論主題】: 【關鍵決定】: 【行動項目】: 【未解決問題】: """ } prompt_template = summary_templates.get(summary_type, summary_templates["detailed"]) prompt = PromptTemplate.from_template(prompt_template) super().__init__( llm=llm, prompt=prompt, return_messages=False ) # 使用示例 def compare_summary_types(): """比較不同摘要類型的效果""" llm = ChatOpenAI(model_name="gpt-4o", temperature=0) summary_types = ["detailed", "concise", "structured"] memories = {stype: CustomSummaryMemory(llm, stype) for stype in summary_types} # 測試對話 test_conversation = [ ("我需要在下週一前完成專案報告", "了解,下週一是12月15日。這個專案報告的主題是什麼?"), ("是關於AI在金融業的應用分析", "好的,這是個很有趣的主題。您需要包含哪些具體方面?"), ("我想分析機器學習在風險評估中的應用", "機器學習在風險評估確實很重要。建議您可以從信用評分、欺詐檢測等角度分析。") ] # 為每種摘要類型添加相同的對話 for stype, memory in memories.items(): print(f"\n=== {stype.upper()} 摘要類型 ===") for human_input, ai_response in test_conversation: memory.save_context( inputs={"input": human_input}, outputs={"output": ai_response} ) summary = memory.load_memory_variables({})["history"] print(f"摘要結果:\n{summary}") print(f"Token 數: {len(memory.llm.get_num_tokens(summary))}") compare_summary_types() ConversationSummaryBufferMemory - 混合摘要記憶 概念說明 混合摘要記憶體是最平衡的解決方案,它結合了完整記憶和摘要記憶的優點。系統會保留最近的完整對話(在 token 限制內),並將更早的對話內容進行摘要。 技術架構 from langchain.memory import ConversationSummaryBufferMemory from langchain_openai import ChatOpenAI import tiktoken from typing import List, Tuple class HybridMemorySystem: """混合記憶體系統""" def __init__(self, max_token_limit=800, model_name="gpt-4o"): self.llm = ChatOpenAI(model_name=model_name, temperature=0) self.memory = ConversationSummaryBufferMemory( llm=self.llm, max_token_limit=max_token_limit, return_messages=True, memory_key="history" ) self.encoding = tiktoken.encoding_for_model(model_name) self.max_token_limit = max_token_limit self.conversation_log = [] # 用於追蹤對話歷史 def process_conversation(self, human_input: str, ai_response: str): """處理對話並展示混合記憶體的運作""" conversation_id = len(self.conversation_log) + 1 self.conversation_log.append((human_input, ai_response)) print(f"\n{'='*70}") print(f"對話 {conversation_id}: 混合記憶體分析") print(f"{'='*70}") # 分析對話前的記憶體狀態 before_state = self._analyze_memory_state("處理前") # 保存新對話 self.memory.save_context( inputs={"input": human_input}, outputs={"output": ai_response} ) # 分析對話後的記憶體狀態 after_state = self._analyze_memory_state("處理後") # 比較變化 self._compare_memory_states(before_state, after_state, conversation_id) print(f"\n📄 當前記憶體內容:") self._display_current_memory() def _analyze_memory_state(self, stage: str) -> dict: """分析記憶體狀態""" memory_vars = self.memory.load_memory_variables({}) messages = memory_vars.get("history", []) # 分析摘要和完整訊息 summary_content = "" full_messages = [] for msg in messages: if hasattr(msg, 'type') and msg.type == "system": summary_content = msg.content else: full_messages.append(msg) # 計算 token 數 summary_tokens = len(self.encoding.encode(summary_content)) if summary_content else 0 full_message_tokens = sum( len(self.encoding.encode(msg.content)) for msg in full_messages ) total_tokens = summary_tokens + full_message_tokens return { "stage": stage, "summary_content": summary_content, "summary_tokens": summary_tokens, "full_messages": full_messages, "full_message_tokens": full_message_tokens, "total_tokens": total_tokens, "message_count": len(full_messages) } def _compare_memory_states(self, before: dict, after: dict, conversation_id: int): """比較記憶體狀態變化""" print(f"\n📊 記憶體狀態比較:") print(f" 摘要部分:") print(f" Token 數: {before['summary_tokens']} → {after['summary_tokens']}") if after['summary_tokens'] > before['summary_tokens']: print(f" 🔄 摘要已更新(+{after['summary_tokens'] - before['summary_tokens']} tokens)") print(f" 完整訊息部分:") print(f" 訊息數: {before['message_count']} → {after['message_count']}") print(f" Token 數: {before['full_message_tokens']} → {after['full_message_tokens']}") print(f" 總計:") print(f" Token 數: {before['total_tokens']} → {after['total_tokens']}") print(f" Token 限制: {self.max_token_limit}") print(f" 使用率: {after['total_tokens']/self.max_token_limit*100:.1f}%") # 檢查是否觸發摘要 if after['summary_tokens'] > before['summary_tokens']: print(f" ⚡ 觸發摘要機制:部分歷史對話被摘要") if after['message_count'] < len(self.conversation_log): summarized_count = len(self.conversation_log) - after['message_count'] print(f" 📝 {summarized_count} 輪對話已被摘要") def _display_current_memory(self): """顯示當前記憶體內容""" memory_vars = self.memory.load_memory_variables({}) messages = memory_vars.get("history", []) for i, msg in enumerate(messages): if hasattr(msg, 'type'): if msg.type == "system": print(f" 📋 [摘要] {msg.content}") elif msg.type == "human": print(f" 👤 [完整] {msg.content}") elif msg.type == "ai": print(f" 🤖 [完整] {msg.content}") def demonstrate_hybrid_memory(self): """示範混合記憶體的完整運作流程""" # 設計一個漸進複雜的對話場景 conversations = [ # 第1-3輪:建立基礎資訊(應該保持完整) ("我是張小明,想諮詢房屋貸款", "張小明您好!我很樂意協助您房屋貸款的諮詢。請問您看中的房屋價格大約是多少?"), ("房屋總價是1500萬,我有300萬頭期款", "好的,1500萬房屋,您有300萬頭期款,需要貸款1200萬。請問您目前的月收入是多少?"), ("我月收入12萬,太太月收入8萬", "您們夫妻合計月收入20萬,貸款1200萬是可行的。請問您目前的工作年資多久了?"), # 第4-6輪:深入細節(可能開始摘要) ("我在科技公司工作5年了,職位是資深工程師", "5年的穩定工作經驗很好。根據您的條件,可以申請到1.8%的優惠利率。"), ("1.8%的利率聽起來不錯,還款期限可以多長?", "最長可以貸款30年,以1200萬30年計算,月付金約4.2萬元。"), ("月付金4.2萬可以接受。還需要準備什麼文件?", "需要準備身分證、收入證明、工作證明、以及房屋相關文件。"), # 第7-9輪:最終確認(應該保持完整) ("文件我都可以準備,大概多久可以核貸?", "文件齊全後約7-10個工作天。我們也提供線上申請服務。"), ("太好了,我想現在就開始申請流程", "沒問題!我現在為您安排專員聯繫,開始正式申請流程。"), ("謝謝您的協助,請問專員什麼時候會聯繫我?", "專員會在今天下午3點前聯繫您,請保持電話暢通。") ] print("🎯 混合記憶體示範:房屋貸款諮詢對話") print("觀察摘要機制如何在token限制下運作...\n") for human_msg, ai_msg in conversations: self.process_conversation(human_msg, ai_msg) input("\n按 Enter 繼續下一輪對話...") # 最終狀態分析 self._generate_final_analysis() def _generate_final_analysis(self): """生成最終分析報告""" print(f"\n{'='*70}") print("混合記憶體最終分析報告") print(f"{'='*70}") final_state = self._analyze_memory_state("最終") print(f"📈 效能指標:") print(f" 總對話輪次: {len(self.conversation_log)}") print(f" 保留完整對話: {final_state['message_count']} 輪") print(f" 摘要對話: {len(self.conversation_log) - final_state['message_count']} 輪") print(f" 最終 Token 使用: {final_state['total_tokens']}/{self.max_token_limit}") print(f" 記憶體使用率: {final_state['total_tokens']/self.max_token_limit*100:.1f}%") # 計算記憶體效率 if len(self.conversation_log) > 0: retention_rate = final_state['message_count'] / len(self.conversation_log) print(f" 對話保留率: {retention_rate*100:.1f}%") print(f"\n🎯 混合記憶體優勢:") print(f" ✅ 保留最新對話的完整細節") print(f" ✅ 通過摘要維持長期記憶") print(f" ✅ 自動平衡記憶體使用") print(f" ✅ 適應不同對話長度") # 執行混合記憶體示範 hybrid_system = HybridMemorySystem(max_token_limit=600) hybrid_system.demonstrate_hybrid_memory() ConversationEntityMemory - 實體記憶 概念說明 實體記憶體專注於從對話中提取和管理關鍵實體(如人名、地名、組織等)及其相關資訊。這種記憶體特別適合需要追蹤多個實體狀態的複雜對話場景。 詳細實作 from langchain.memory import ConversationEntityMemory from langchain_openai import ChatOpenAI from langchain.schema import BaseMessage import json import re class EntityMemoryManager: """實體記憶體管理器""" def __init__(self, model_name="gpt-4o"): self.llm = ChatOpenAI(model_name=model_name, temperature=0) self.memory = ConversationEntityMemory( llm=self.llm, return_messages=True, memory_key="history" ) self.conversation_count = 0 self.entity_evolution = {} # 追蹤實體資訊的演化 def process_conversation(self, human_input: str, ai_response: str): """處理對話並分析實體提取""" self.conversation_count += 1 print(f"\n{'='*70}") print(f"對話 {self.conversation_count}: 實體記憶體分析") print(f"{'='*70}") print(f"👤 人類: {human_input}") print(f"🤖 AI: {ai_response}") # 記錄對話前的實體狀態 before_entities = self._get_current_entities() # 保存對話(觸發實體提取) self.memory.save_context( inputs={"input": human_input}, outputs={"output": ai_response} ) # 記錄對話後的實體狀態 after_entities = self._get_current_entities() # 分析實體變化 self._analyze_entity_changes(before_entities, after_entities) # 更新實體演化記錄 self._update_entity_evolution(after_entities) def _get_current_entities(self) -> dict: """獲取當前記憶體中的實體資訊""" # 通過 memory.entity_store 獲取實體資訊 return dict(self.memory.entity_store.store) def _analyze_entity_changes(self, before: dict, after: dict): """分析實體變化""" print(f"\n🔍 實體分析:") # 新增的實體 new_entities = set(after.keys()) - set(before.keys()) if new_entities: print(f" ➕ 新識別實體: {', '.join(new_entities)}") for entity in new_entities: print(f" 📝 {entity}: {after[entity]}") # 更新的實體 updated_entities = [] for entity in set(after.keys()) & set(before.keys()): if before[entity] != after[entity]: updated_entities.append(entity) if updated_entities: print(f" 🔄 更新的實體: {', '.join(updated_entities)}") for entity in updated_entities: print(f" 📝 {entity}:") print(f" 舊: {before[entity]}") print(f" 新: {after[entity]}") # 實體統計 print(f" 📊 實體統計:") print(f" 總實體數: {len(before)} → {len(after)}") print(f" 新增: {len(new_entities)}") print(f" 更新: {len(updated_entities)}") def _update_entity_evolution(self, current_entities: dict): """更新實體演化記錄""" for entity, info in current_entities.items(): if entity not in self.entity_evolution: self.entity_evolution[entity] = [] # 檢查是否有變化 if not self.entity_evolution[entity] or self.entity_evolution[entity][-1]["info"] != info: self.entity_evolution[entity].append({ "conversation": self.conversation_count, "info": info }) def query_entity(self, entity_name: str): """查詢特定實體的資訊""" print(f"\n🔍 查詢實體: {entity_name}") # 從記憶體查詢 memory_vars = self.memory.load_memory_variables({"input": f"告訴我關於{entity_name}的資訊"}) print(f"📋 記憶體中的相關資訊:") if "history" in memory_vars: for msg in memory_vars["history"]: if entity_name.lower() in msg.content.lower(): print(f" 💬 {msg.content}") # 顯示實體演化過程 if entity_name in self.entity_evolution: print(f"\n📈 {entity_name} 資訊演化:") for record in self.entity_evolution[entity_name]: print(f" 對話 {record['conversation']}: {record['info']}") def demonstrate_entity_memory(self): """示範實體記憶體的運作""" # 設計一個包含多個實體的複雜對話 conversations = [ ("我是王大明,在台積電工作", "王大明您好!很高興認識您。台積電是台灣的知名企業。"), ("我住在新竹市,已經在台積電工作3年了", "新竹市是科技重鎮,台積電的總部也在那裡。3年的工作經驗很不錯!"), ("我太太李小美是護理師,在新竹馬偕醫院上班", "護理師是很有意義的工作。新竹馬偕醫院在當地有很好的聲譽。"), ("我們有一個5歲的兒子叫王小華,在新竹市立幼兒園就讀", "5歲正是活潑好動的年紀!新竹市立幼兒園的教育品質如何?"), ("王小華很喜歡畫畫,李小美希望培養他的藝術天份", "培養孩子的興趣很重要。王小華有藝術天份,李小美的想法很好。"), ("台積電最近有升遷機會,我可能會調到台南廠區", "恭喜您有升遷機會!台南廠區是台積電的重要據點,不過這意味著要搬家嗎?"), ("是的,如果升遷成功,全家就要搬到台南。李小美也在找台南的醫院工作", "搬家是大事,不過台南的生活品質也很好。相信李小美能在台南找到合適的護理師工作。") ] print("🎯 實體記憶體示範:家庭搬遷決策對話") print("觀察系統如何提取和追蹤多個實體資訊...\n") for human_msg, ai_msg in conversations: self.process_conversation(human_msg, ai_msg) input("\n按 Enter 繼續...") # 實體查詢示範 print(f"\n{'='*70}") print("實體查詢示範") print(f"{'='*70}") entities_to_query = ["王大明", "李小美", "王小華", "台積電", "新竹市"] for entity in entities_to_query: self.query_entity(entity) input(f"\n按 Enter 查詢下一個實體...") # 生成實體關係圖 self._generate_entity_relationships() def _generate_entity_relationships(self): """生成實體關係分析""" print(f"\n{'='*70}") print("實體關係分析") print(f"{'='*70}") current_entities = self._get_current_entities() # 按類型分類實體 entity_categories = { "人物": [], "地點": [], "組織": [], "其他": [] } for entity, info in current_entities.items(): info_lower = info.lower() if any(keyword in info_lower for keyword in ["工作", "住", "兒子", "太太", "護理師"]): entity_categories["人物"].append((entity, info)) elif any(keyword in info_lower for keyword in ["市", "醫院", "幼兒園", "廠區"]): entity_categories["地點"].append((entity, info)) elif any(keyword in info_lower for keyword in ["公司", "企業", "醫院"]): entity_categories["組織"].append((entity, info)) else: entity_categories["其他"].append((entity, info)) for category, entities in entity_categories.items(): if entities: print(f"\n📂 {category}:") for entity, info in entities: print(f" 🏷️ {entity}: {info}") print(f"\n📈 實體記憶體效能:") print(f" 總實體數: {len(current_entities)}") print(f" 對話輪次: {self.conversation_count}") print(f" 平均每輪提取實體: {len(current_entities)/self.conversation_count:.1f}") # 執行實體記憶體示範 entity_manager = EntityMemoryManager() entity_manager.demonstrate_entity_memory() ConversationKGMemory - 知識圖譜記憶 概念說明 知識圖譜記憶體是最先進的記憶體管理方式,它不僅提取實體,還建立實體之間的關係網絡。這種結構化的知識表示方式特別適合複雜的推理和查詢任務。 詳細實作 from langchain_community.memory.kg import ConversationKGMemory from langchain_openai import ChatOpenAI import networkx as nx import matplotlib.pyplot as plt from typing import List, Tuple, Dict class KnowledgeGraphMemorySystem: """知識圖譜記憶體系統""" def __init__(self, model_name="gpt-4o"): self.llm = ChatOpenAI(model_name=model_name, temperature=0) self.memory = ConversationKGMemory( llm=self.llm, return_messages=True, memory_key="history" ) self.conversation_count = 0 self.knowledge_graph = nx.DiGraph() # 用於視覺化的圖 self.triplet_history = [] # 記錄三元組生成歷史 def process_conversation(self, human_input: str, ai_response: str): """處理對話並構建知識圖譜""" self.conversation_count += 1 print(f"\n{'='*80}") print(f"對話 {self.conversation_count}: 知識圖譜構建") print(f"{'='*80}") print(f"👤 人類: {human_input}") print(f"🤖 AI: {ai_response}") # 記錄對話前的知識狀態 before_kg = dict(self.memory.kg.get_store()) # 保存對話(觸發知識圖譜更新) self.memory.save_context( inputs={"input": human_input}, outputs={"output": ai_response} ) # 記錄對話後的知識狀態 after_kg = dict(self.memory.kg.get_store()) # 分析知識圖譜變化 new_triplets = self._analyze_kg_changes(before_kg, after_kg) # 更新本地圖結構 self._update_local_graph(new_triplets) # 顯示當前知識狀態 self._display_current_knowledge() def _analyze_kg_changes(self, before: dict, after: dict) -> List[Tuple[str, str, str]]: """分析知識圖譜變化""" print(f"\n🧠 知識圖譜分析:") new_triplets = [] # 檢查每個實體的知識變化 for entity, relations in after.items(): if entity not in before: print(f" ➕ 新實體: {entity}") # 解析該實體的所有關係 triplets = self._parse_relations(entity, relations) new_triplets.extend(triplets) elif before[entity] != relations: print(f" 🔄 更新實體: {entity}") # 找出新增的關係 old_relations = before[entity] new_info = relations.replace(old_relations, "").strip() if new_info: triplets = self._parse_relations(entity, new_info) new_triplets.extend(triplets) if new_triplets: print(f" 🔗 新增關係三元組:") for subject, predicate, obj in new_triplets: print(f" ({subject}) --[{predicate}]--> ({obj})") self.triplet_history.append({ "conversation": self.conversation_count, "triplet": (subject, predicate, obj) }) print(f" 📊 圖譜統計:") print(f" 實體數: {len(before)} → {len(after)}") print(f" 新增三元組: {len(new_triplets)}") return new_triplets def _parse_relations(self, entity: str, relations_text: str) -> List[Tuple[str, str, str]]: """從關係文本中解析三元組""" triplets = [] # 簡化的關係解析(實際應用中可能需要更複雜的NLP處理) import re # 匹配各種關係模式 patterns = [ (r'(\w+)\s*lives in\s*(\w+)', lambda m: (m.group(1), "lives_in", m.group(2))), (r'(\w+)\s*works at\s*(\w+)', lambda m: (m.group(1), "works_at", m.group(2))), (r'(\w+)\s*is\s*(\w+)', lambda m: (m.group(1), "is", m.group(2))), (r'(\w+)\s*has\s*(\w+)', lambda m: (m.group(1), "has", m.group(2))), (r'(\w+)\s*married to\s*(\w+)', lambda m: (m.group(1), "married_to", m.group(2))), (r'(\w+)\s*child of\s*(\w+)', lambda m: (m.group(1), "child_of", m.group(2))), ] for pattern, extractor in patterns: matches = re.finditer(pattern, relations_text, re.IGNORECASE) for match in matches: triplets.append(extractor(match)) return triplets def _update_local_graph(self, new_triplets: List[Tuple[str, str, str]]): """更新本地圖結構用於視覺化""" for subject, predicate, obj in new_triplets: self.knowledge_graph.add_edge(subject, obj, relation=predicate) def _display_current_knowledge(self): """顯示當前知識狀態""" current_kg = dict(self.memory.kg.get_store()) print(f"\n📚 當前知識庫:") for entity, info in current_kg.items(): print(f" 🏷️ {entity}: {info}") def query_knowledge(self, query: str): """查詢知識圖譜""" print(f"\n🔍 知識查詢: {query}") # 使用記憶體的查詢功能 memory_vars = self.memory.load_memory_variables({"input": query}) print(f"📋 查詢結果:") if "history" in memory_vars: for msg in memory_vars["history"]: print(f" 💭 {msg.content}") # 基於圖結構的查詢 self._graph_based_query(query) def _graph_based_query(self, query: str): """基於圖結構的查詢""" query_lower = query.lower() print(f"\n🕸️ 圖結構查詢:") # 查找相關節點 relevant_nodes = [node for node in self.knowledge_graph.nodes() if node.lower() in query_lower] if relevant_nodes: for node in relevant_nodes: print(f" 🎯 {node} 的關係:") # 出邊(該節點作為主語) for target in self.knowledge_graph.successors(node): edge_data = self.knowledge_graph.get_edge_data(node, target) relation = edge_data.get('relation', 'related_to') print(f" → {relation}: {target}") # 入邊(該節點作為賓語) for source in self.knowledge_graph.predecessors(node): edge_data = self.knowledge_graph.get_edge_data(source, node) relation = edge_data.get('relation', 'related_to') print(f" ← {relation}: {source}") def visualize_knowledge_graph(self): """視覺化知識圖譜""" if len(self.knowledge_graph.nodes()) == 0: print("知識圖譜為空,無法視覺化") return plt.figure(figsize=(12, 8)) # 使用spring布局 pos = nx.spring_layout(self.knowledge_graph, k=2, iterations=50) # 繪製節點 nx.draw_networkx_nodes(self.knowledge_graph, pos, node_color='lightblue', node_size=2000, alpha=0.7) # 繪製邊 nx.draw_networkx_edges(self.knowledge_graph, pos, edge_color='gray', arrows=True, arrowsize=20, alpha=0.6) # 繪製標籤 nx.draw_networkx_labels(self.knowledge_graph, pos, font_size=10, font_weight='bold') # 繪製邊標籤(關係) edge_labels = nx.get_edge_attributes(self.knowledge_graph, 'relation') nx.draw_networkx_edge_labels(self.knowledge_graph, pos, edge_labels, font_size=8) plt.title("知識圖譜視覺化", fontsize=16, fontweight='bold') plt.axis('off') plt.tight_layout() plt.show() print(f"\n📊 圖譜統計:") print(f" 節點數: {len(self.knowledge_graph.nodes())}") print(f" 邊數: {len(self.knowledge_graph.edges())}") print(f" 密度: {nx.density(self.knowledge_graph):.3f}") def demonstrate_kg_memory(self): """示範知識圖譜記憶體的完整功能""" # 設計一個複雜的企業組織對話 conversations = [ ("Alice Chen 是我們公司的 CEO", "了解,Alice Chen 擔任 CEO 的職位。"), ("Bob Wang 是技術總監,他直接向 Alice 匯報", "好的,Bob Wang 是技術總監,向 Alice Chen 匯報。"), ("我們有三個部門:工程部、市場部和財務部", "公司組織結構很清楚,有工程、市場和財務三個主要部門。"), ("Bob Wang 管理工程部,工程部有 15 個工程師", "Bob Wang 管理工程部,部門規模是 15 人。"), ("Carol Liu 是市場部主管,市場部在台北辦公室", "Carol Liu 負責市場部,辦公地點在台北。"), ("David Kim 是財務部主管,他之前在 Google 工作", "David Kim 負責財務部,有 Google 的工作背景。"), ("Alice Chen 和 Carol Liu 都是台大 MBA 畢業", "Alice Chen 和 Carol Liu 都有台大 MBA 的教育背景。"), ("公司總部在新竹,但市場部在台北辦公室", "公司總部位於新竹,但市場部設在台北辦公室。"), ] print("🎯 知識圖譜記憶體示範:企業組織結構對話") print("觀察系統如何構建複雜的關係網絡...\n") for human_msg, ai_msg in conversations: self.process_conversation(human_msg, ai_msg) input("\n按 Enter 繼續...") # 知識查詢示範 print(f"\n{'='*80}") print("知識查詢示範") print(f"{'='*80}") queries = [ "Alice Chen 是誰?", "誰管理工程部?", "台大 MBA 畢業的有哪些人?", "公司有哪些辦公室?", "Bob Wang 的工作職責是什麼?" ] for query in queries: self.query_knowledge(query) input(f"\n按 Enter 執行下一個查詢...") # 生成關係分析報告 self._generate_relationship_analysis() # 視覺化圖譜(如果環境支援) try: self.visualize_knowledge_graph() except ImportError: print("matplotlib 未安裝,跳過圖譜視覺化") def _generate_relationship_analysis(self): """生成關係分析報告""" print(f"\n{'='*80}") print("知識圖譜關係分析報告") print(f"{'='*80}") current_kg = dict(self.memory.kg.get_store()) print(f"📈 統計資訊:") print(f" 對話輪次: {self.conversation_count}") print(f" 識別實體: {len(current_kg)}") print(f" 生成三元組: {len(self.triplet_history)}") print(f" 圖節點數: {len(self.knowledge_graph.nodes())}") print(f" 圖邊數: {len(self.knowledge_graph.edges())}") print(f"\n🔗 關係類型分析:") relation_counts = {} for record in self.triplet_history: _, predicate, _ = record["triplet"] relation_counts[predicate] = relation_counts.get(predicate, 0) + 1 for relation, count in sorted(relation_counts.items(), key=lambda x: x[1], reverse=True): print(f" {relation}: {count} 次") print(f"\n🏆 知識圖譜記憶體優勢:") print(f" ✅ 結構化知識表示") print(f" ✅ 支援複雜關係查詢") print(f" ✅ 便於推理和分析") print(f" ✅ 可視覺化知識網絡") # 執行知識圖譜記憶體示範 kg_system = KnowledgeGraphMemorySystem() kg_system.demonstrate_kg_memory() VectorStoreRetrieverMemory - 向量檢索記憶 概念說明 向量檢索記憶體使用語意相似性而非時間順序來檢索相關的歷史對話。這種方法特別適合需要從大量歷史對話中找到語意相關內容的場景。 詳細實作 import faiss from langchain_openai.embeddings import OpenAIEmbeddings from langchain_community.vectorstores.faiss import FAISS from langchain.memory import VectorStoreRetrieverMemory from langchain.docstore import InMemoryDocstore from langchain.schema import Document import numpy as np from typing import List, Dict, Tuple class VectorMemorySystem: """向量檢索記憶體系統""" def __init__(self, embedding_dim=1536, top_k=3): self.embeddings_model = OpenAIEmbeddings() self.embedding_dim = embedding_dim self.top_k = top_k # 初始化 FAISS 向量資料庫 index = faiss.IndexFlatL2(embedding_dim) self.vectorstore = FAISS( self.embeddings_model, index, InMemoryDocstore({}), {} ) # 創建檢索器 self.retriever = self.vectorstore.as_retriever( search_kwargs={"k": top_k} ) # 初始化記憶體 self.memory = VectorStoreRetrieverMemory( retriever=self.retriever, memory_key="history", input_key="input" ) self.conversation_count = 0 self.all_conversations = [] # 保存所有對話用於分析 self.similarity_scores = [] # 記錄相似度分數 def add_conversation(self, human_input: str, ai_response: str, metadata: Dict = None): """添加對話到向量記憶體""" self.conversation_count += 1 print(f"\n{'='*70}") print(f"添加對話 {self.conversation_count}") print(f"{'='*70}") print(f"👤 人類: {human_input}") print(f"🤖 AI: {ai_response}") # 保存完整對話記錄 conversation_record = { "id": self.conversation_count, "human": human_input, "ai": ai_response, "metadata": metadata or {} } self.all_conversations.append(conversation_record) # 將對話保存到向量記憶體 self.memory.save_context( inputs={"input": human_input}, outputs={"output": ai_response} ) print(f"✅ 對話已添加到向量記憶體") print(f"📊 當前記憶體大小: {len(self.all_conversations)} 條對話") def query_similar_conversations(self, query: str, detailed_analysis=True): """查詢語意相似的對話""" print(f"\n{'='*70}") print(f"向量檢索查詢") print(f"{'='*70}") print(f"🔍 查詢: {query}") # 使用記憶體進行檢索 memory_result = self.memory.load_memory_variables({"input": query}) print(f"\n📋 檢索結果:") retrieved_docs = memory_result.get("history", []) if not retrieved_docs: print(" ❌ 未找到相關對話") return [] # 詳細分析檢索結果 if detailed_analysis: self._analyze_retrieval_results(query, retrieved_docs) return retrieved_docs def _analyze_retrieval_results(self, query: str, retrieved_docs: List[Document]): """分析檢索結果""" print(f"\n🔬 檢索分析:") print(f" 檢索到 {len(retrieved_docs)} 條相關對話") # 計算相似度分數 query_embedding = self.embeddings_model.embed_query(query) for i, doc in enumerate(retrieved_docs): print(f"\n 📄 結果 {i+1}:") print(f" 內容: {doc.page_content}") # 計算相似度 doc_embedding = self.embeddings_model.embed_query(doc.page_content) similarity = self._cosine_similarity(query_embedding, doc_embedding) print(f" 相似度: {similarity:.4f}") # 查找原始對話 original_conv = self._find_original_conversation(doc.page_content) if original_conv: print(f" 原始對話 ID: {original_conv['id']}") print(f" AI 回應: {original_conv['ai']}") def _cosine_similarity(self, vec1: List[float], vec2: List[float]) -> float: """計算餘弦相似度""" vec1 = np.array(vec1) vec2 = np.array(vec2) return np.dot(vec1, vec2) / (np.linalg.norm(vec1) * np.linalg.norm(vec2)) def _find_original_conversation(self, content: str) -> Dict: """根據內容找到原始對話""" for conv in self.all_conversations: if conv["human"] in content or content in conv["human"]: return conv return None def compare_with_chronological_memory(self, query: str): """與時序記憶體比較""" print(f"\n{'='*70}") print("向量檢索 vs 時序檢索比較") print(f"{'='*70}") print(f"🔍 查詢: {query}") # 向量檢索結果 print(f"\n🧠 向量檢索結果 (語意相似):") vector_results = self.query_similar_conversations(query, detailed_analysis=False) # 時序檢索結果(最近的對話) print(f"\n⏰ 時序檢索結果 (最近對話):") recent_conversations = self.all_conversations[-self.top_k:] for i, conv in enumerate(recent_conversations): print(f" 📄 最近第 {i+1} 條:") print(f" 人類: {conv['human']}") print(f" AI: {conv['ai']}") # 分析兩種方法的差異 self._analyze_retrieval_differences(query, vector_results, recent_conversations) def _analyze_retrieval_differences(self, query: str, vector_results: List[Document], recent_conversations: List[Dict]): """分析檢索方法差異""" print(f"\n📊 檢索方法比較:") # 提取向量檢索的對話ID vector_conv_ids = [] for doc in vector_results: original = self._find_original_conversation(doc.page_content) if original: vector_conv_ids.append(original["id"]) recent_conv_ids = [conv["id"] for conv in recent_conversations] print(f" 向量檢索對話 ID: {vector_conv_ids}") print(f" 時序檢索對話 ID: {recent_conv_ids}") # 計算重疊 overlap = set(vector_conv_ids) & set(recent_conv_ids) print(f" 重疊對話數: {len(overlap)}") if len(overlap) < len(vector_conv_ids): print(f" 💡 向量檢索找到了時序方法錯過的相關對話") # 語意相關性分析 query_embedding = self.embeddings_model.embed_query(query) print(f"\n📈 語意相關性分析:") # 向量檢索結果的平均相似度 vector_similarities = [] for doc in vector_results: doc_embedding = self.embeddings_model.embed_query(doc.page_content) similarity = self._cosine_similarity(query_embedding, doc_embedding) vector_similarities.append(similarity) # 時序檢索結果的平均相似度 recent_similarities = [] for conv in recent_conversations: conv_embedding = self.embeddings_model.embed_query(conv["human"]) similarity = self._cosine_similarity(query_embedding, conv_embedding) recent_similarities.append(similarity) if vector_similarities: avg_vector_sim = sum(vector_similarities) / len(vector_similarities) print(f" 向量檢索平均相似度: {avg_vector_sim:.4f}") if recent_similarities: avg_recent_sim = sum(recent_similarities) / len(recent_similarities) print(f" 時序檢索平均相似度: {avg_recent_sim:.4f}") def demonstrate_vector_memory(self): """示範向量記憶體的完整功能""" # 準備多樣化的對話數據 conversations = [ # 技術問題 ("如何優化 Python 程式的效能?", "可以使用 profiling 工具找出瓶頸,考慮使用 Cython 或 NumPy。"), ("資料庫查詢太慢了,有什麼解決方案?", "可以考慮添加索引、優化查詢語句、或使用快取。"), ("Docker 容器如何最佳化?", "減少映像檔層數、使用多階段建構、選擇輕量基礎映像檔。"), # 商業問題 ("如何提升客戶滿意度?", "重點關注客戶服務品質、產品品質和售後支援。"), ("公司營收成長策略有哪些?", "可以考慮市場擴張、產品創新、客戶留存和定價策略。"), ("如何管理遠端團隊?", "建立清楚的溝通機制、使用協作工具、定期視訊會議。"), # 學習問題 ("如何學習機器學習?", "從數學基礎開始,學習 Python,實作小專案,閱讀論文。"), ("英文口說如何進步?", "多練習對話、看英文影片、參加語言交換活動。"), ("如何培養程式設計思維?", "多練習演算法、參與開源專案、學習設計模式。"), # 生活問題 ("如何保持健康的作息?", "規律睡眠、適度運動、均衡飲食、減少壓力。"), ("如何規劃退休生活?", "提早開始儲蓄、投資理財、培養興趣愛好。"), ] print("🎯 向量記憶體示範:多領域問答系統") print("建立包含技術、商業、學習、生活等多個領域的對話記憶體...\n") # 添加所有對話 for human_msg, ai_msg in conversations: self.add_conversation(human_msg, ai_msg) print(f"\n✅ 已添加 {len(conversations)} 條對話到向量記憶體") # 測試各種查詢 test_queries = [ "程式效能問題", # 應該找到技術相關對話 "團隊管理", # 應該找到管理相關對話 "學習方法", # 應該找到學習相關對話 "如何提升速度", # 可能涵蓋技術和生活 "Python 相關問題" # 特定技術查詢 ] print(f"\n{'='*70}") print("語意檢索測試") print(f"{'='*70}") for query in test_queries: self.query_similar_conversations(query) input(f"\n按 Enter 繼續下一個查詢...") # 比較檢索方法 print(f"\n{'='*70}") print("檢索方法比較測試") print(f"{'='*70}") comparison_queries = ["效能優化", "團隊協作"] for query in comparison_queries: self.compare_with_chronological_memory(query) input(f"\n按 Enter 繼續...") # 生成分析報告 self._generate_vector_analysis_report() def _generate_vector_analysis_report(self): """生成向量記憶體分析報告""" print(f"\n{'='*70}") print("向量記憶體效能分析報告") print(f"{'='*70}") print(f"📈 統計資訊:") print(f" 總對話數: {len(self.all_conversations)}") print(f" 向量維度: {self.embedding_dim}") print(f" 檢索數量: {self.top_k}") # 分析對話主題分佈 topics = { "技術": ["Python", "資料庫", "Docker", "程式", "演算法"], "商業": ["客戶", "營收", "團隊", "管理", "策略"], "學習": ["學習", "進步", "培養", "練習"], "生活": ["健康", "作息", "退休", "生活"] } topic_counts = {topic: 0 for topic in topics.keys()} for conv in self.all_conversations: text = conv["human"] + " " + conv["ai"] for topic, keywords in topics.items(): if any(keyword in text for keyword in keywords): topic_counts[topic] += 1 break print(f"\n📂 主題分佈:") for topic, count in topic_counts.items(): percentage = count / len(self.all_conversations) * 100 print(f" {topic}: {count} 條 ({percentage:.1f}%)") print(f"\n🎯 向量記憶體優勢:") print(f" ✅ 語意相似性檢索") print(f" ✅ 跨時間範圍找到相關對話") print(f" ✅ 支援大規模對話存儲") print(f" ✅ 自動主題聚類效果") # 執行向量記憶體示範 vector_system = VectorMemorySystem() vector_system.demonstrate_vector_memory() SQL 資料庫持久化記憶 概念說明 對於需要跨會話保存、多用戶共享、或大規模存儲的應用,SQL 資料庫提供了可靠的持久化記憶體解決方案。LangChain 提供了與各種 SQL 資料庫的整合支援。 詳細實作 from langchain_community.chat_message_histories import SQLChatMessageHistory from langchain.memory import ConversationBufferMemory from langchain.schema import BaseMessage, HumanMessage, AIMessage import sqlite3 import json from datetime import datetime from typing import List, Dict, Optional class PersistentMemoryManager: """持久化記憶體管理器""" def __init__(self, database_url="sqlite:///chat_memory.db"): self.database_url = database_url self.session_memories = {} # 快取不同會話的記憶體 # 初始化資料庫 self._initialize_database() def _initialize_database(self): """初始化資料庫結構""" if "sqlite:///" in self.database_url: db_path = self.database_url.replace("sqlite:///", "") conn = sqlite3.connect(db_path) cursor = conn.cursor() # 創建擴展表用於會話元數據 cursor.execute(""" CREATE TABLE IF NOT EXISTS session_metadata ( session_id TEXT PRIMARY KEY, user_id TEXT, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, last_activity TIMESTAMP DEFAULT CURRENT_TIMESTAMP, conversation_count INTEGER DEFAULT 0, metadata TEXT ) """) conn.commit() conn.close() def create_session_memory(self, session_id: str, user_id: str = None, metadata: Dict = None) -> ConversationBufferMemory: """為特定會話創建記憶體""" print(f"\n{'='*60}") print(f"創建會話記憶體: {session_id}") print(f"{'='*60}") # 創建 SQL 訊息歷史 chat_history = SQLChatMessageHistory( session_id=session_id, connection=self.database_url ) # 創建記憶體實例 memory = ConversationBufferMemory( chat_memory=chat_history, return_messages=True, memory_key="history" ) # 快取記憶體實例 self.session_memories[session_id] = memory # 更新會話元數據 self._update_session_metadata(session_id, user_id, metadata) print(f"✅ 會話記憶體已創建並連接到資料庫") # 載入歷史對話 existing_messages = chat_history.messages if existing_messages: print(f"📚 載入了 {len(existing_messages)} 條歷史訊息") return memory def _update_session_metadata(self, session_id: str, user_id: str = None, metadata: Dict = None): """更新會話元數據""" if "sqlite:///" in self.database_url: db_path = self.database_url.replace("sqlite:///", "") conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute(""" INSERT OR REPLACE INTO session_metadata (session_id, user_id, last_activity, metadata) VALUES (?, ?, ?, ?) """, ( session_id, user_id, datetime.now().isoformat(), json.dumps(metadata or {}) )) conn.commit() conn.close() def add_conversation(self, session_id: str, human_message: str, ai_message: str) -> None: """向會話添加對話""" if session_id not in self.session_memories: print(f"⚠️ 會話 {session_id} 不存在,自動創建...") self.create_session_memory(session_id) memory = self.session_memories[session_id] print(f"\n📝 添加對話到會話 {session_id}:") print(f" 👤 人類: {human_message}") print(f" 🤖 AI: {ai_message}") # 保存對話 memory.save_context( inputs={"input": human_message}, outputs={"output": ai_message} ) # 更新會話統計 self._increment_conversation_count(session_id) print(f"✅ 對話已保存到資料庫") def _increment_conversation_count(self, session_id: str): """增加對話計數""" if "sqlite:///" in self.database_url: db_path = self.database_url.replace("sqlite:///", "") conn = sqlite3.connect(db_path) cursor = conn.cursor() cursor.execute(""" UPDATE session_metadata SET conversation_count = conversation_count + 1, last_activity = ? WHERE session_id = ? """, (datetime.now().isoformat(), session_id)) conn.commit() conn.close() def get_session_history(self, session_id: str, limit: Optional[int] = None) -> List[BaseMessage]: """獲取會話歷史""" print(f"\n🔍 查詢會話歷史: {session_id}") if session_id not in self.session_memories: self.create_session_memory(session_id) memory = self.session_memories[session_id] memory_vars = memory.load_memory_variables({}) messages = memory_vars.get("history", []) if limit: messages = messages[-limit:] print(f"📚 找到 {len(messages)} 條歷史訊息") for i, msg in enumerate(messages): print(f" {i+1}. {msg.__class__.__name__}: {msg.content[:50]}...") return messages def search_conversations(self, keyword: str, session_id: Optional[str] = None) -> List[Dict]: """搜尋包含關鍵字的對話""" print(f"\n🔍 搜尋關鍵字: '{keyword}'") if session_id: print(f" 限制會話: {session_id}") results = [] if "sqlite:///" in self.database_url: db_path = self.database_url.replace("sqlite:///", "") conn = sqlite3.connect(db_path) cursor = conn.cursor() # 構建查詢 query = """ SELECT session_id, message, type, created_at FROM message_store WHERE message LIKE ? """ params = [f"%{keyword}%"] if session_id: query += " AND session_id = ?" params.append(session_id) query += " ORDER BY created_at DESC" cursor.execute(query, params) rows = cursor.fetchall() for row in rows: results.append({ "session_id": row[0], "message": row[1], "type": row[2], "created_at": row[3] }) conn.close() print(f"📋 找到 {len(results)} 條包含關鍵字的訊息") for result in results[:5]: # 只顯示前5條 print(f" 會話 {result['session_id']}: {result['type']} - {result['message'][:50]}...") return results def get_session_statistics(self) -> Dict: """獲取所有會話的統計資訊""" print(f"\n📊 會話統計分析") stats = { "total_sessions": 0, "total_conversations": 0, "active_sessions": 0, "sessions_by_user": {}, "recent_activity": [] } if "sqlite:///" in self.database_url: db_path = self.database_url.replace("sqlite:///", "") conn = sqlite3.connect(db_path) cursor = conn.cursor() # 基本統計 cursor.execute("SELECT COUNT(*) FROM session_metadata") stats["total_sessions"] = cursor.fetchone()[0] cursor.execute("SELECT SUM(conversation_count) FROM session_metadata") result = cursor.fetchone()[0] stats["total_conversations"] = result if result else 0 # 按用戶統計 cursor.execute(""" SELECT user_id, COUNT(*), SUM(conversation_count) FROM session_metadata WHERE user_id IS NOT NULL GROUP BY user_id """) for row in cursor.fetchall(): stats["sessions_by_user"][row[0]] = { "sessions": row[1], "conversations": row[2] } # 最近活動 cursor.execute(""" SELECT session_id, user_id, last_activity, conversation_count FROM session_metadata ORDER BY last_activity DESC LIMIT 10 """) for row in cursor.fetchall(): stats["recent_activity"].append({ "session_id": row[0], "user_id": row[1], "last_activity": row[2], "conversation_count": row[3] }) conn.close() # 顯示統計結果 print(f" 總會話數: {stats['total_sessions']}") print(f" 總對話數: {stats['total_conversations']}") print(f" 用戶數: {len(stats['sessions_by_user'])}") if stats["sessions_by_user"]: print(f"\n👥 用戶活動:") for user_id, user_stats in stats["sessions_by_user"].items(): print(f" {user_id}: {user_stats['sessions']} 會話, {user_stats['conversations']} 對話") if stats["recent_activity"]: print(f"\n⏰ 最近活動:") for activity in stats["recent_activity"][:3]: print(f" 會話 {activity['session_id']}: {activity['conversation_count']} 對話 ({activity['last_activity']})") return stats def cleanup_old_sessions(self, days_threshold: int = 30): """清理舊會話""" print(f"\n🧹 清理 {days_threshold} 天前的舊會話") if "sqlite:///" in self.database_url: db_path = self.database_url.replace("sqlite:///", "") conn = sqlite3.connect(db_path) cursor = conn.cursor() # 查找舊會話 cursor.execute(""" SELECT session_id FROM session_metadata WHERE last_activity < datetime('now', '-{} days') """.format(days_threshold)) old_sessions = [row[0] for row in cursor.fetchall()] if old_sessions: print(f"找到 {len(old_sessions)} 個舊會話") # 刪除訊息 for session_id in old_sessions: cursor.execute("DELETE FROM message_store WHERE session_id = ?", (session_id,)) # 刪除元數據 cursor.execute(""" DELETE FROM session_metadata WHERE last_activity < datetime('now', '-{} days') """.format(days_threshold)) conn.commit() print(f"✅ 已清理 {len(old_sessions)} 個舊會話") else: print("沒有需要清理的舊會話") conn.close() def demonstrate_persistent_memory(self): """示範持久化記憶體的完整功能""" print("🎯 持久化記憶體示範:多用戶客服系統") print("模擬多個用戶的跨會話對話...\n") # 模擬多個用戶的對話 users_conversations = { "user_alice": [ ("我想查詢我的帳戶餘額", "您好 Alice!您的帳戶餘額是 $1,250.00"), ("上次轉帳記錄在哪裡查看?", "您可以在交易記錄頁面查看所有轉帳記錄"), ("我想設定自動扣款", "好的,我為您開啟自動扣款設定頁面") ], "user_bob": [ ("我的信用卡被盜刷了", "非常抱歉!我立即為您凍結信用卡"), ("需要重新申請新卡嗎?", "是的,我已為您申請新卡,5-7個工作天內寄達"), ("臨時額度可以提高嗎?", "基於您的信用記錄,可以臨時提高到 $5,000") ], "user_charlie": [ ("想了解投資理財產品", "我們有定期存款、基金、股票等多種理財產品"), ("風險等級如何分類?", "分為保守型、穩健型、積極型三個等級"), ("我適合哪種投資?", "根據您的風險承受能力,建議從穩健型開始") ] } # 第一輪:創建會話並添加對話 print("=" * 70) print("第一輪:創建用戶會話") print("=" * 70) for user_id, conversations in users_conversations.items(): session_id = f"{user_id}_session_1" # 創建會話 self.create_session_memory( session_id=session_id, user_id=user_id, metadata={"service_type": "banking", "priority": "normal"} ) # 添加對話 for human_msg, ai_msg in conversations: self.add_conversation(session_id, human_msg, ai_msg) input(f"\n{user_id} 會話已創建,按 Enter 繼續...") # 第二輪:模擬會話重新連接 print("\n" + "=" * 70) print("第二輪:重新連接會話(模擬跨會話持久化)") print("=" * 70) for user_id in users_conversations.keys(): session_id = f"{user_id}_session_1" print(f"\n重新連接 {user_id} 的會話...") # 重新創建記憶體(應該載入歷史對話) memory = self.create_session_memory(session_id, user_id) # 添加新對話 if user_id == "user_alice": self.add_conversation(session_id, "我剛才問的問題解決了嗎?", "是的,您的自動扣款已經設定完成!") elif user_id == "user_bob": self.add_conversation(session_id, "新卡什麼時候會到?", "預計3個工作天內送達,我們會簡訊通知您") elif user_id == "user_charlie": self.add_conversation(session_id, "我決定先開始定期存款", "很好的選擇!我為您推薦1年期定存,利率2.1%") # 第三輪:搜尋和分析功能 print("\n" + "=" * 70) print("第三輪:搜尋和分析功能") print("=" * 70) # 關鍵字搜尋 search_keywords = ["信用卡", "帳戶", "投資"] for keyword in search_keywords: self.search_conversations(keyword) input(f"\n搜尋 '{keyword}' 完成,按 Enter 繼續...") # 統計分析 self.get_session_statistics() print("\n" + "=" * 70) print("持久化記憶體示範完成") print("=" * 70) print("🎯 持久化記憶體優勢:") print(" ✅ 跨會話保存對話歷史") print(" ✅ 支援多用戶隔離") print(" ✅ 可靠的資料持久化") print(" ✅ 豐富的查詢和分析功能") print(" ✅ 適合生產環境部署") # 執行持久化記憶體示範 persistent_manager = PersistentMemoryManager() persistent_manager.demonstrate_persistent_memory() 實際應用場景與選擇建議 記憶體類型選擇指南 根據不同的應用場景,選擇合適的記憶體管理策略至關重要: def recommend_memory_type(use_case_info: Dict) -> str: """根據使用場景推薦記憶體類型""" conversation_length = use_case_info.get("avg_conversation_length", 10) user_count = use_case_info.get("user_count", 1) budget_sensitive = use_case_info.get("budget_sensitive", False) need_persistence = use_case_info.get("need_persistence", False) complex_relationships = use_case_info.get("complex_relationships", False) semantic_search = use_case_info.get("semantic_search", False) recommendations = [] # 基於場景特徵的推薦邏輯 if conversation_length <= 5 and not budget_sensitive: recommendations.append(("ConversationBufferMemory", "適合短對話,實作簡單")) if 5 < conversation_length <= 20: recommendations.append(("ConversationBufferWindowMemory", "滑動視窗控制記憶體大小")) if budget_sensitive or conversation_length > 20: recommendations.append(("ConversationTokenBufferMemory", "精確控制 token 使用")) recommendations.append(("ConversationSummaryBufferMemory", "平衡詳細度與成本")) if complex_relationships: recommendations.append(("ConversationKGMemory", "建立實體關係網絡")) recommendations.append(("ConversationEntityMemory", "追蹤實體資訊")) if semantic_search or conversation_length > 50: recommendations.append(("VectorStoreRetrieverMemory", "語意相似性檢索")) if need_persistence or user_count > 1: recommendations.append(("SQL持久化記憶體", "跨會話保存,多用戶支援")) return recommendations # 應用場景範例 scenarios = { "客服聊天機器人": { "avg_conversation_length": 15, "user_count": 1000, "budget_sensitive": True, "need_persistence": True, "complex_relationships": False, "semantic_search": False }, "個人AI助手": { "avg_conversation_length": 30, "user_count": 1, "budget_sensitive": False, "need_persistence": True, "complex_relationships": True, "semantic_search": True }, "教育輔導系統": { "avg_conversation_length": 25, "user_count": 100, "budget_sensitive": True, "need_persistence": True, "complex_relationships": True, "semantic_search": True } } print("🎯 記憶體類型選擇建議") print("=" * 60) for scenario, info in scenarios.items(): print(f"\n📋 場景: {scenario}") recommendations = recommend_memory_type(info) print(" 推薦記憶體類型:") for memory_type, reason in recommendations: print(f" ✅ {memory_type}: {reason}") 效能最佳化與注意事項 記憶體效能最佳化策略 class MemoryOptimizer: """記憶體效能最佳化器""" @staticmethod def optimize_token_usage(memory_type: str, conversation_data: List) -> Dict: """最佳化 token 使用策略""" total_tokens = sum(len(conv["human"]) + len(conv["ai"]) for conv in conversation_data) avg_conversation_tokens = total_tokens / len(conversation_data) if conversation_data else 0 recommendations = { "current_usage": total_tokens, "avg_per_conversation": avg_conversation_tokens, "optimization_strategies": [] } if memory_type == "ConversationBufferMemory": if total_tokens > 2000: recommendations["optimization_strategies"].extend([ "考慮改用 ConversationBufferWindowMemory", "設定適當的視窗大小(建議 5-10 輪)", "實作對話重要性評分,保留關鍵對話" ]) elif memory_type == "ConversationSummaryMemory": recommendations["optimization_strategies"].extend([ "最佳化摘要提示詞,確保關鍵資訊不遺失", "定期檢查摘要品質", "考慮使用更便宜的模型進行摘要" ]) elif memory_type == "VectorStoreRetrieverMemory": recommendations["optimization_strategies"].extend([ "調整檢索數量(k值)平衡相關性與成本", "使用本地向量資料庫減少 API 調用", "實作向量快取機制" ]) return recommendations @staticmethod def monitor_memory_performance(memory_instance, operation_count: int = 100): """監控記憶體效能""" import time import psutil import os print(f"\n🔍 記憶體效能監控") print(f"操作次數: {operation_count}") # 記錄開始狀態 start_time = time.time() start_memory = psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024 # MB # 模擬記憶體操作 for i in range(operation_count): memory_instance.save_context( inputs={"input": f"測試訊息 {i}"}, outputs={"output": f"回應 {i}"} ) # 每 10 次操作檢查一次 if (i + 1) % 10 == 0: current_memory = psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024 print(f" 第 {i+1} 次操作 - 記憶體使用: {current_memory:.1f} MB") # 記錄結束狀態 end_time = time.time() end_memory = psutil.Process(os.getpid()).memory_info().rss / 1024 / 1024 # 分析結果 total_time = end_time - start_time memory_increase = end_memory - start_memory avg_time_per_op = total_time / operation_count print(f"\n📊 效能分析結果:") print(f" 總執行時間: {total_time:.2f} 秒") print(f" 平均每次操作: {avg_time_per_op:.4f} 秒") print(f" 記憶體增長: {memory_increase:.1f} MB") print(f" 每次操作記憶體增長: {memory_increase/operation_count:.4f} MB") return { "total_time": total_time, "avg_time_per_operation": avg_time_per_op, "memory_increase_mb": memory_increase, "memory_per_operation_mb": memory_increase / operation_count } # 記憶體最佳化實踐建議 print("\n🎯 記憶體管理最佳實踐") print("=" * 60) best_practices = [ "定期監控 token 使用量,避免超出模型限制", "根據對話重要性選擇不同的記憶體策略", "實作記憶體快取機制,減少重複計算", "使用批次處理最佳化向量操作效能", "定期清理不需要的歷史對話", "監控記憶體使用情況,防止記憶體洩漏", "為不同用戶實作記憶體隔離", "設定合理的記憶體大小限制", "使用異步操作提升並發效能", "實作記憶體備份與恢復機制" ] for i, practice in enumerate(best_practices, 1): print(f"{i:2d}. {practice}") print(f"\n💡 記憶體選擇決策樹:") print("短期對話 (< 10輪) → ConversationBufferMemory") print("中期對話 (10-30輪) → ConversationBufferWindowMemory") print("長期對話 + 成本敏感 → ConversationSummaryBufferMemory") print("複雜關係追蹤 → ConversationEntityMemory / ConversationKGMemory") print("語意搜尋需求 → VectorStoreRetrieverMemory") print("多用戶 + 持久化 → SQL 資料庫記憶體") 結論 本教學詳細介紹了 LangChain 中各種記憶體管理機制的原理、實作和應用場景。每種記憶體類型都有其獨特的優勢和適用場景: ConversationBufferMemory:最簡單直觀,適合短期對話 ConversationBufferWindowMemory:平衡記憶體使用與對話品質 ConversationTokenBufferMemory:精確控制成本和 token 使用 ConversationSummaryMemory:長期對話的成本最佳化解決方案 ConversationEntityMemory:實體追蹤和管理 ConversationKGMemory:複雜關係建模和推理 VectorStoreRetrieverMemory:語意相似性檢索 SQL 持久化記憶體:生產級多用戶支援 選擇合適的記憶體管理策略需要考慮對話長度、成本預算、功能需求、用戶規模等多個因素。在實際應用中,往往需要結合多種記憶體類型,構建混合式的記憶體管理系統,以達到最佳的效能和用戶體驗。 通過本教學的學習和實踐,您應該能夠: 理解各種記憶體管理機制的工作原理 根據具體需求選擇合適的記憶體類型 實作和最佳化記憶體管理系統 監控和調整記憶體效能 處理大規模部署中的記憶體管理挑戰 記憶體管理是構建高品質 AI 應用的基礎,掌握這些技能將大大提升您的 RAG 系統開發能力。
-
RAG - Loaders 組件統整
LangChain 文件載入器實戰指南 課程相關code我都上完到github空間中:請 git clone https://github.com/kevin801221/AgenticU-The-Modular-Teaching-Hub-for-Modern-LLM-Agent-Frameworks.git 去下載相關程式碼。(可以幫忙點個星星唷!) 📚 參考資源與延伸閱讀 官方文檔 LangChain Document Loaders 官方文檔 LangChain Community Document Loaders 第三方服務與工具 Unstructured.io 官網 - 進階文件解析服務 LlamaParse 文檔 - LlamaIndex 文件解析工具 Upstage Document AI - 企業級文件處理解決方案 Beautiful Soup 文檔 - HTML/XML 解析工具 學術資源 arXiv.org - 學術預印本論文庫 Layout Parser GitHub - 文件版面分析工具 目錄 Document 物件基礎 PDF 文件處理 網頁內容載入 結構化資料處理 Office 文檔處理 學術資源載入 進階解析技術 效能最佳化 實際應用案例 Document 物件基礎 在 LangChain 中,所有文件載入器都會將原始資料轉換為標準的 Document 物件格式: from langchain.schema import Document # Document 物件包含兩個核心部分 doc = Document( page_content="文件的實際內容", # 主要文字內容 metadata={ # 元數據 "source": "file.pdf", "page": 1, "author": "作者名稱" } ) print(f"內容: {doc.page_content}") print(f"元數據: {doc.metadata}") 文件分析工具 def analyze_documents(docs: List[Document]): """分析文件集合的基本統計""" total_length = sum(len(doc.page_content) for doc in docs) print(f"📊 文件分析:") print(f" 文件數量: {len(docs)}") print(f" 總字符數: {total_length:,}") print(f" 平均長度: {total_length/len(docs):,.0f} 字符") # 分析 metadata 欄位 all_fields = set() for doc in docs: all_fields.update(doc.metadata.keys()) print(f" Metadata 欄位: {len(all_fields)} 種") for field in sorted(all_fields): count = sum(1 for doc in docs if field in doc.metadata) print(f" {field}: {count}/{len(docs)} 文件") PDF 文件處理 PDF 是最常見的文檔格式,LangChain 提供多種載入器選擇: 基本載入器比較 from langchain_community.document_loaders import ( PyPDFLoader, PyMuPDFLoader, UnstructuredPDFLoader, PDFPlumberLoader ) # 1. PyPDFLoader - 最基礎,速度快 loader = PyPDFLoader("document.pdf") docs = loader.load() # 2. PyMuPDFLoader - 速度最快,metadata 豐富 loader = PyMuPDFLoader("document.pdf") docs = loader.load() # 3. UnstructuredPDFLoader - 功能最強,可識別結構 loader = UnstructuredPDFLoader("document.pdf", mode="elements") docs = loader.load() # 4. PDFPlumberLoader - 表格提取最佳 loader = PDFPlumberLoader("document.pdf") docs = loader.load() 選擇建議 載入器 速度 功能 最適用場景 PyPDFLoader 快 基礎 一般文字提取 PyMuPDFLoader 最快 中等 大量 PDF 處理 UnstructuredPDFLoader 慢 最強 結構化內容分析 PDFPlumberLoader 中等 表格 包含表格的文件 進階 PDF 處理 def smart_pdf_loading(pdf_path: str, content_type: str = "auto"): """根據內容類型智能選擇 PDF 載入器""" # 自動檢測內容類型 if content_type == "auto": with open(pdf_path, 'rb') as f: sample = f.read(1024).decode('utf-8', errors='ignore') if 'table' in sample.lower() or '|' in sample: content_type = "table_heavy" elif len(sample.strip()) < 50: content_type = "scanned" else: content_type = "text" # 選擇載入器 if content_type == "table_heavy": loader = PDFPlumberLoader(pdf_path) elif content_type == "scanned": loader = PyPDFLoader(pdf_path, extract_images=True) elif content_type == "structured": loader = UnstructuredPDFLoader(pdf_path, mode="elements") else: loader = PyMuPDFLoader(pdf_path) # 預設選擇 return loader.load() 網頁內容載入 基本網頁載入 import bs4 from langchain_community.document_loaders import WebBaseLoader # 基本載入 loader = WebBaseLoader("https://example.com/article") docs = loader.load() # 指定解析範圍(只抓取文章內容) bs_kwargs = dict( parse_only=bs4.SoupStrainer("div", attrs={"class": ["post-content", "article-body"]}) ) loader = WebBaseLoader( web_paths=["https://example.com/article"], bs_kwargs=bs_kwargs ) docs = loader.load() 批次載入多個網頁 def batch_load_websites(urls: List[str], max_concurrent: int = 5): """批次載入多個網頁""" # 配置解析器 bs_kwargs = dict( parse_only=bs4.SoupStrainer(["article", "main", "div"], attrs={"class": ["content", "post", "article"]}) ) # 批次載入 loader = WebBaseLoader( web_paths=urls, bs_kwargs=bs_kwargs, requests_per_second=2 # 控制請求頻率 ) docs = loader.load() # 清理內容 cleaned_docs = [] for doc in docs: # 移除多餘空白 content = ' '.join(doc.page_content.split()) # 過濾太短的內容 if len(content) > 100: doc.page_content = content cleaned_docs.append(doc) return cleaned_docs 結構化資料處理 CSV 檔案處理 from langchain_community.document_loaders import CSVLoader import pandas as pd # 基本 CSV 載入 loader = CSVLoader( file_path="data.csv", source_column="title" # 指定來源欄位 ) docs = loader.load() # 使用 Pandas 進行進階處理 def process_csv_with_pandas(csv_path: str, content_template: str = None): """使用 Pandas 進行進階 CSV 處理""" df = pd.read_csv(csv_path) # 數據清理 df = df.dropna().drop_duplicates() docs = [] for _, row in df.iterrows(): # 自定義內容格式 if content_template: content = content_template.format(**row.to_dict()) else: content = "\n".join([f"{col}: {row[col]}" for col in df.columns]) metadata = { "source": csv_path, "row_index": row.name, **{f"field_{col}": row[col] for col in df.columns[:5]} # 前5個欄位作為 metadata } docs.append(Document(page_content=content, metadata=metadata)) return docs # 使用範例 docs = process_csv_with_pandas( "customers.csv", content_template="客戶: {name}, 年齡: {age}, 城市: {city}, 職業: {occupation}" ) Excel 檔案處理 from langchain_community.document_loaders import UnstructuredExcelLoader # 基本 Excel 載入 loader = UnstructuredExcelLoader("data.xlsx", mode="elements") docs = loader.load() # 處理多個工作表 def process_excel_workbook(excel_path: str): """處理包含多個工作表的 Excel 檔案""" import pandas as pd # 讀取所有工作表 excel_file = pd.ExcelFile(excel_path) all_docs = [] for sheet_name in excel_file.sheet_names: df = pd.read_excel(excel_path, sheet_name=sheet_name) # 將工作表轉換為文檔 content = f"工作表: {sheet_name}\n\n" content += df.to_string(index=False) metadata = { "source": excel_path, "sheet_name": sheet_name, "rows": len(df), "columns": len(df.columns) } all_docs.append(Document(page_content=content, metadata=metadata)) return all_docs Office 文檔處理 Word 文檔處理 from langchain_community.document_loaders import ( Docx2txtLoader, UnstructuredWordDocumentLoader ) # 簡單文字提取 loader = Docx2txtLoader("document.docx") docs = loader.load() # 結構化解析 loader = UnstructuredWordDocumentLoader("document.docx", mode="elements") docs = loader.load() # 分析文檔結構 def analyze_word_structure(docs: List[Document]): """分析 Word 文檔結構""" elements = {} for doc in docs: element_type = doc.metadata.get("category", "unknown") elements[element_type] = elements.get(element_type, 0) + 1 print("📄 文檔結構分析:") for element_type, count in elements.items(): print(f" {element_type}: {count}") PowerPoint 處理 from langchain_community.document_loaders import UnstructuredPowerPointLoader # 按投影片載入 loader = UnstructuredPowerPointLoader("presentation.pptx", mode="elements") docs = loader.load() # 組織投影片內容 def organize_powerpoint_content(docs: List[Document]): """組織 PowerPoint 內容""" slides = {} for doc in docs: # 假設 metadata 中有投影片編號 slide_num = doc.metadata.get("page_number", 1) if slide_num not in slides: slides[slide_num] = [] slides[slide_num].append(doc.page_content) # 重新組織為完整投影片 organized_docs = [] for slide_num in sorted(slides.keys()): content = "\n\n".join(slides[slide_num]) metadata = { "source": "presentation.pptx", "slide_number": slide_num, "element_count": len(slides[slide_num]) } organized_docs.append(Document(page_content=content, metadata=metadata)) return organized_docs 學術資源載入 arXiv 論文搜尋 from langchain_community.document_loaders import ArxivLoader # 基本搜尋 loader = ArxivLoader( query="large language models", load_max_docs=5 ) papers = loader.load() # 進階搜尋與分析 def search_and_analyze_papers(query: str, max_papers: int = 10): """搜尋並分析 arXiv 論文""" loader = ArxivLoader( query=query, load_max_docs=max_papers, load_all_available_meta=True ) papers = loader.load() # 分析論文 analysis = { "total_papers": len(papers), "avg_length": sum(len(p.page_content) for p in papers) // len(papers), "years": {}, "categories": {} } for paper in papers: # 分析發表年份 published = paper.metadata.get("Published", "") if published: year = published[:4] analysis["years"][year] = analysis["years"].get(year, 0) + 1 # 分析分類 categories = paper.metadata.get("categories", "").split() for cat in categories: analysis["categories"][cat] = analysis["categories"].get(cat, 0) + 1 return papers, analysis # 使用範例 papers, analysis = search_and_analyze_papers("transformer attention mechanism") print(f"搜尋結果: {analysis}") 文獻回顧生成 def create_literature_review(search_queries: List[str]): """創建文獻回顧""" all_papers = [] # 收集所有論文 for query in search_queries: loader = ArxivLoader(query=query, load_max_docs=5) papers = loader.load() all_papers.extend(papers) # 去重(基於標題) unique_papers = {} for paper in all_papers: title = paper.metadata.get("Title", "") if title not in unique_papers: unique_papers[title] = paper papers = list(unique_papers.values()) # 生成回顧內容 review_content = f"# 文獻回顧\n\n" review_content += f"**搜尋查詢**: {', '.join(search_queries)}\n" review_content += f"**論文數量**: {len(papers)}\n\n" # 按年份分組 papers_by_year = {} for paper in papers: published = paper.metadata.get("Published", "Unknown") year = published[:4] if published != "Unknown" else "Unknown" if year not in papers_by_year: papers_by_year[year] = [] papers_by_year[year].append(paper) # 生成年份摘要 for year in sorted(papers_by_year.keys(), reverse=True): review_content += f"## {year} 年論文 ({len(papers_by_year[year])} 篇)\n\n" for paper in papers_by_year[year][:3]: # 只顯示前3篇 title = paper.metadata.get("Title", "無標題") authors = paper.metadata.get("Authors", "無作者") summary = paper.metadata.get("Summary", "") review_content += f"### {title}\n" review_content += f"**作者**: {authors}\n" if summary: short_summary = summary[:200] + "..." if len(summary) > 200 else summary review_content += f"**摘要**: {short_summary}\n" review_content += "\n" metadata = { "document_type": "literature_review", "paper_count": len(papers), "search_queries": search_queries } return Document(page_content=review_content, metadata=metadata) 進階解析技術 第三方 AI 服務整合 # 使用 Upstage Document AI from langchain_community.document_loaders import UpstageDocumentParseLoader def parse_with_upstage(file_path: str, api_key: str): """使用 Upstage 解析文檔""" loader = UpstageDocumentParseLoader( file_path=file_path, api_key=api_key, output_format="text", # 或 "markdown" coordinates=True # 提取座標資訊 ) docs = loader.load() # 增強 metadata for doc in docs: doc.metadata.update({ "parsing_service": "upstage", "enhanced_extraction": True, "coordinate_info": doc.metadata.get("coordinates") is not None }) return docs # 使用 LlamaParse def parse_with_llamaparse(file_path: str, api_key: str): """使用 LlamaParse 解析文檔""" try: from llama_parse import LlamaParse parser = LlamaParse( api_key=api_key, result_type="markdown", verbose=False ) documents = parser.load_data(file_path) # 轉換為 LangChain Document docs = [] for i, doc in enumerate(documents): langchain_doc = Document( page_content=doc.text, metadata={ "source": file_path, "page": i + 1, "parsing_service": "llamaparse", "output_format": "markdown" } ) docs.append(langchain_doc) return docs except ImportError: raise ValueError("需要安裝 llama-parse: pip install llama-parse") 智能載入器選擇 def auto_select_loader(file_path: str, requirements: dict = None): """根據需求自動選擇最佳載入器""" import os file_ext = os.path.splitext(file_path)[1].lower() file_size = os.path.getsize(file_path) requirements = requirements or {} if file_ext == '.pdf': if requirements.get('extract_tables', False): return PDFPlumberLoader(file_path) elif requirements.get('preserve_structure', False): return UnstructuredPDFLoader(file_path, mode="elements") elif file_size > 50 * 1024 * 1024: # > 50MB return PyMuPDFLoader(file_path) # 最快 else: return PyPDFLoader(file_path) # 平衡 elif file_ext == '.docx': if requirements.get('preserve_structure', False): return UnstructuredWordDocumentLoader(file_path, mode="elements") else: return Docx2txtLoader(file_path) elif file_ext == '.csv': source_col = requirements.get('source_column') return CSVLoader(file_path, source_column=source_col) elif file_ext == '.txt': encoding = requirements.get('encoding', 'utf-8') return TextLoader(file_path, encoding=encoding) else: raise ValueError(f"不支援的檔案類型: {file_ext}") 效能最佳化 批次處理與快取 import hashlib import pickle import time from pathlib import Path from concurrent.futures import ThreadPoolExecutor, as_completed class DocumentProcessor: """文檔處理器(含快取功能)""" def __init__(self, cache_dir: str = "./cache"): self.cache_dir = Path(cache_dir) self.cache_dir.mkdir(exist_ok=True) def process_documents(self, file_paths: List[str], strategy: str = "parallel", max_workers: int = 4): """批次處理文檔""" if strategy == "parallel": return self._parallel_process(file_paths, max_workers) else: return self._sequential_process(file_paths) def _parallel_process(self, file_paths: List[str], max_workers: int): """並行處理""" results = {} with ThreadPoolExecutor(max_workers=max_workers) as executor: # 提交任務 future_to_file = { executor.submit(self._process_single_file, fp): fp for fp in file_paths } # 收集結果 for future in as_completed(future_to_file): file_path = future_to_file[future] try: docs = future.result() results[file_path] = docs print(f"✅ {Path(file_path).name}: {len(docs)} 文檔") except Exception as e: print(f"❌ {Path(file_path).name}: {e}") results[file_path] = [] return results def _process_single_file(self, file_path: str): """處理單個文件(含快取)""" # 檢查快取 cached = self._load_from_cache(file_path) if cached is not None: return cached # 實際處理 loader = auto_select_loader(file_path) docs = loader.load() # 保存快取 self._save_to_cache(file_path, docs) return docs def _cache_key(self, file_path: str) -> str: """生成快取鍵""" stat = Path(file_path).stat() key_data = f"{file_path}_{stat.st_size}_{stat.st_mtime}" return hashlib.md5(key_data.encode()).hexdigest() def _load_from_cache(self, file_path: str): """從快取載入""" cache_file = self.cache_dir / f"{self._cache_key(file_path)}.pickle" if cache_file.exists(): try: with open(cache_file, 'rb') as f: return pickle.load(f) except: cache_file.unlink() # 刪除損壞的快取 return None def _save_to_cache(self, file_path: str, docs: List[Document]): """保存到快取""" cache_file = self.cache_dir / f"{self._cache_key(file_path)}.pickle" try: with open(cache_file, 'wb') as f: pickle.dump(docs, f) except Exception as e: print(f"快取保存失敗: {e}") 記憶體監控 def monitor_memory_usage(func): """記憶體使用監控裝飾器""" def wrapper(*args, **kwargs): try: import psutil import os process = psutil.Process(os.getpid()) # 記錄開始狀態 start_memory = process.memory_info().rss / 1024 / 1024 # MB start_time = time.time() # 執行函數 result = func(*args, **kwargs) # 記錄結束狀態 end_memory = process.memory_info().rss / 1024 / 1024 # MB end_time = time.time() print(f"🔍 記憶體監控:") print(f" 開始: {start_memory:.1f} MB") print(f" 結束: {end_memory:.1f} MB") print(f" 增加: {end_memory - start_memory:.1f} MB") print(f" 時間: {end_time - start_time:.2f} 秒") return result except ImportError: print("⚠️ psutil 未安裝,無法監控記憶體") return func(*args, **kwargs) return wrapper # 使用範例 @monitor_memory_usage def process_large_documents(file_paths: List[str]): processor = DocumentProcessor() return processor.process_documents(file_paths, strategy="parallel") 實際應用案例 企業文檔管理系統 def enterprise_document_system(): """企業文檔管理系統範例""" # 文檔分類處理 document_configs = { "合約文件": { "file_types": [".pdf", ".docx"], "requirements": {"preserve_structure": True, "extract_tables": True}, "priority": "high" }, "技術文檔": { "file_types": [".pdf", ".md", ".txt"], "requirements": {"preserve_structure": True}, "priority": "medium" }, "財務報告": { "file_types": [".xlsx", ".pdf"], "requirements": {"extract_tables": True}, "priority": "high" } } processor = DocumentProcessor() # 按類別處理文檔 results = {} for doc_type, config in document_configs.items(): print(f"處理 {doc_type}...") # 這裡會是實際的文件路徑 file_paths = [] # 從文件系統或資料庫獲取 docs = processor.process_documents( file_paths, strategy="parallel" if config["priority"] == "high" else "sequential" ) results[doc_type] = docs return results 學術研究平台 def research_platform_pipeline(): """學術研究平台處理流程""" # 多源資料收集 research_sources = { "arxiv_papers": { "queries": ["machine learning", "natural language processing"], "max_papers": 20 }, "uploaded_papers": { "file_paths": [], # 用戶上傳的論文 "formats": [".pdf"] } } all_documents = [] # 處理 arXiv 論文 for query in research_sources["arxiv_papers"]["queries"]: loader = ArxivLoader( query=query, load_max_docs=research_sources["arxiv_papers"]["max_papers"] ) papers = loader.load() all_documents.extend(papers) # 處理上傳論文 processor = DocumentProcessor() uploaded_docs = processor.process_documents( research_sources["uploaded_papers"]["file_paths"] ) for docs in uploaded_docs.values(): all_documents.extend(docs) # 生成研究摘要 summary = create_literature_review([ doc.metadata.get("Title", "未知標題") for doc in all_documents[:5] ]) return { "total_papers": len(all_documents), "summary": summary, "documents": all_documents } 最佳實踐總結 載入器選擇指南 場景 推薦載入器 原因 大量 PDF 處理 PyMuPDFLoader 速度最快 表格重要的文檔 PDFPlumberLoader 表格提取最佳 需要保留結構 UnstructuredPDFLoader 結構識別能力強 網頁批次爬取 WebBaseLoader 內建並發支援 學術論文研究 ArxivLoader 專門的學術資源 結構化資料 CSVLoader + pandas 靈活的資料處理 效能最佳化策略 檔案預處理 檢查檔案完整性 統一檔案格式 過濾不必要的檔案 載入策略 小文件(<10個):順序處理 中等文件(10-100個):並行處理 大量文件(>100個):分批流式處理 記憶體管理 啟用快取避免重複處理 監控記憶體使用情況 大文件分塊處理 錯誤處理 實作重試機制 隔離問題文件 提供回退方案 常見問題解決 Q: PDF 文字提取不完整怎麼辦? def robust_pdf_extraction(pdf_path: str): """穩健的 PDF 文字提取""" loaders = [ PyMuPDFLoader(pdf_path), PyPDFLoader(pdf_path), UnstructuredPDFLoader(pdf_path, mode="single") ] for loader in loaders: try: docs = loader.load() if docs and len(docs[0].page_content.strip()) > 100: return docs except Exception as e: print(f"載入器 {loader.__class__.__name__} 失敗: {e}") continue raise ValueError("所有 PDF 載入器都失敗") Q: 如何處理大量小文件? def process_many_small_files(file_paths: List[str]): """處理大量小文件的最佳化方法""" # 分批處理避免記憶體問題 batch_size = 50 all_results = {} for i in range(0, len(file_paths), batch_size): batch = file_paths[i:i + batch_size] processor = DocumentProcessor() batch_results = processor.process_documents( batch, strategy="parallel", max_workers=8 ) all_results.update(batch_results) # 清理記憶體 del processor print(f"已處理 {min(i + batch_size, len(file_paths))}/{len(file_paths)} 文件") return all_results Q: 網頁載入被封鎖怎麼辦? def respectful_web_scraping(urls: List[str]): """遵守網站規則的網頁抓取""" import time import random headers = { 'User-Agent': 'Mozilla/5.0 (compatible; DocumentLoader/1.0)' } successful_docs = [] for url in urls: try: # 檢查 robots.txt(簡化版) if "/admin" in url or "/private" in url: print(f"跳過受保護的 URL: {url}") continue loader = WebBaseLoader( web_paths=[url], requests_kwargs={ 'headers': headers, 'timeout': 30 } ) docs = loader.load() successful_docs.extend(docs) print(f"✅ 成功載入: {url}") # 請求間隔 time.sleep(random.uniform(1, 3)) except Exception as e: print(f"❌ 載入失敗 {url}: {e}") continue return successful_docs 生產環境部署建議 小型系統(< 1000 文件) # 簡單配置 def small_scale_setup(): return { "loader_strategy": "基本載入器", "processing": "同步處理", "cache": "本地文件快取", "deployment": "單機部署" } 中型系統(1K-10K 文件) # 進階配置 def medium_scale_setup(): return { "loader_strategy": "智能載入器選擇", "processing": "並行 + 異步處理", "cache": "Redis 分佈式快取", "deployment": "容器化部署", "monitoring": "記憶體 + 效能監控" } 大型系統(> 10K 文件) # 企業級配置 def large_scale_setup(): return { "loader_strategy": "雲端 AI 服務 + 自定義載入器", "processing": "分佈式處理", "cache": "多層快取架構", "deployment": "微服務架構 + K8s", "monitoring": "全方位監控 + 告警", "scaling": "自動擴縮容" } 總結 🎯 關鍵要點 正確選擇載入器 根據文件格式和需求選擇 考慮速度、功能、準確性的平衡 實作載入器回退機制 效能最佳化 使用快取避免重複處理 根據文件量選擇處理策略 監控資源使用情況 錯誤處理 實作穩健的錯誤處理機制 提供多種載入器選擇 記錄詳細的處理日誌 🚀 進階應用 多模態處理: 結合文字、圖片、表格的統一處理 智能分類: 自動識別文件類型和最佳處理方法 實時處理: 支援文件上傳後的即時處理 API 整合: 與第三方 AI 服務的深度整合 📚 學習建議 從簡單開始: 先掌握基本載入器的使用 實際練習: 用自己的文件測試不同載入器 關注效能: 在實際專案中測試處理效能 保持更新: 關注新的載入器和最佳實踐 掌握文件載入器是構建高效 RAG 系統的基礎,選擇合適的工具和策略比使用最新技術更重要。記住,最佳的載入器是最適合你需求的載入器! 本指南基於 LangChain 官方文檔和實際專案經驗編寫,持續更新以反映最新的技術發展。
-
RAG 架構深度剖析 (2) 載入器組件 - Loaders (上)
LangChain 文件載入器完整實戰指南 課程相關code我都上完到github空間中:請 git clone https://github.com/kevin801221/AgenticU-The-Modular-Teaching-Hub-for-Modern-LLM-Agent-Frameworks.git 去下載相關程式碼。(可以幫忙點個星星唷!) 📚 參考資源與延伸閱讀 官方文檔 LangChain Document Loaders 官方文檔 LangChain Community Document Loaders Document 架構與 Metadata 處理 第三方服務與工具 Unstructured.io 官網 - 進階文件解析服務 LlamaParse 文檔 - LlamaIndex 文件解析工具 Upstage Document AI - 企業級文件處理解決方案 PyMuPDF 文檔 - 高效能 PDF 處理庫 Beautiful Soup 文檔 - HTML/XML 解析工具 學術資源 arXiv.org - 學術預印本論文庫 Layout Parser GitHub - 文件版面分析工具 Document AI Research Papers - 文件 AI 研究論文集 工具與庫 PDFPlumber GitHub - PDF 表格提取專用工具 Python-docx 文檔 - Word 文檔處理 OpenPyXL 文檔 - Excel 文件處理 Pandas 文檔 - 結構化資料處理 目錄 文件載入器基礎概念 PDF 文件處理詳解 網頁內容載入與爬取 結構化資料處理 Office 文檔處理 學術與研究資源載入 進階文件解析技術 自定義載入器開發 效能最佳化與批次處理 實際應用案例與最佳實踐 文件載入器基礎概念 Document 物件架構 在 LangChain 中,所有文件載入器都會將原始資料轉換為標準的 Document 物件格式。理解這個基礎架構對於有效使用各種載入器至關重要。 from langchain.schema import Document from typing import Dict, List, Any import json class DocumentAnalyzer: """Document 物件分析器""" def __init__(self): self.document_stats = {} def analyze_document(self, doc: Document, doc_id: str = None): """詳細分析 Document 物件""" doc_id = doc_id or f"doc_{len(self.document_stats)}" print(f"\n{'='*60}") print(f"Document 分析: {doc_id}") print(f"{'='*60}") # 分析 page_content content = doc.page_content content_stats = { "length": len(content), "lines": len(content.split('\n')), "words": len(content.split()), "characters": len(content), "has_chinese": any('\u4e00' <= char <= '\u9fff' for char in content), "has_english": any(char.isalpha() for char in content), "has_numbers": any(char.isdigit() for char in content), } print(f"📄 內容分析:") print(f" 文字長度: {content_stats['length']:,} 字符") print(f" 行數: {content_stats['lines']:,}") print(f" 詞數: {content_stats['words']:,}") print(f" 包含中文: {'✅' if content_stats['has_chinese'] else '❌'}") print(f" 包含英文: {'✅' if content_stats['has_english'] else '❌'}") print(f" 包含數字: {'✅' if content_stats['has_numbers'] else '❌'}") # 分析 metadata metadata = doc.metadata print(f"\n🏷️ Metadata 分析:") print(f" 欄位數量: {len(metadata)}") for key, value in metadata.items(): value_type = type(value).__name__ value_preview = str(value)[:50] + "..." if len(str(value)) > 50 else str(value) print(f" {key} ({value_type}): {value_preview}") # 儲存分析結果 self.document_stats[doc_id] = { "content_stats": content_stats, "metadata_fields": list(metadata.keys()), "metadata_count": len(metadata) } return content_stats, metadata def compare_documents(self, docs: List[Document], source_name: str = "Unknown"): """比較多個文件的特徵""" print(f"\n{'='*70}") print(f"文件集合分析: {source_name}") print(f"{'='*70}") if not docs: print("❌ 沒有文件可分析") return total_length = sum(len(doc.page_content) for doc in docs) total_words = sum(len(doc.page_content.split()) for doc in docs) # 統計 metadata 欄位 all_metadata_fields = set() for doc in docs: all_metadata_fields.update(doc.metadata.keys()) print(f"📊 集合統計:") print(f" 文件數量: {len(docs)}") print(f" 總字符數: {total_length:,}") print(f" 總詞數: {total_words:,}") print(f" 平均文件長度: {total_length/len(docs):,.0f} 字符") print(f" Metadata 欄位: {len(all_metadata_fields)} 種") print(f"\n📋 Metadata 欄位詳情:") for field in sorted(all_metadata_fields): field_count = sum(1 for doc in docs if field in doc.metadata) coverage = field_count / len(docs) * 100 print(f" {field}: {field_count}/{len(docs)} 文件 ({coverage:.1f}%)") # 文件長度分佈 lengths = [len(doc.page_content) for doc in docs] lengths.sort() print(f"\n📈 長度分佈:") print(f" 最短: {min(lengths):,} 字符") print(f" 最長: {max(lengths):,} 字符") print(f" 中位數: {lengths[len(lengths)//2]:,} 字符") return { "document_count": len(docs), "total_length": total_length, "average_length": total_length / len(docs), "metadata_fields": list(all_metadata_fields) } # 使用範例 analyzer = DocumentAnalyzer() # 創建範例文件 sample_docs = [ Document( page_content="這是一個範例文件,包含中文和English混合內容。", metadata={"source": "sample.txt", "page": 1, "author": "Test Author"} ), Document( page_content="This is an English document with some numbers: 123, 456.", metadata={"source": "sample.txt", "page": 2, "category": "example"} ) ] # 分析單個文件 analyzer.analyze_document(sample_docs[0], "sample_doc_1") # 比較文件集合 analyzer.compare_documents(sample_docs, "Sample Documents") PDF 文件處理詳解 多種 PDF 載入器比較分析 PDF 是最常見也是最複雜的文檔格式之一。不同的 PDF 載入器有各自的優勢和適用場景: from langchain_community.document_loaders import ( PyPDFLoader, PyMuPDFLoader, UnstructuredPDFLoader, PDFMinerLoader, PDFPlumberLoader ) import time from typing import List, Dict import matplotlib.pyplot as plt import seaborn as sns class PDFLoaderComparator: """PDF 載入器效能與功能比較器""" def __init__(self, pdf_path: str): self.pdf_path = pdf_path self.loaders = { "PyPDF": PyPDFLoader, "PyMuPDF": PyMuPDFLoader, "Unstructured": UnstructuredPDFLoader, "PDFMiner": PDFMinerLoader, "PDFPlumber": PDFPlumberLoader } self.results = {} def compare_all_loaders(self, detailed_analysis=True): """比較所有 PDF 載入器""" print(f"🔍 PDF 載入器綜合比較") print(f"檔案: {self.pdf_path}") print(f"{'='*80}") for loader_name, loader_class in self.loaders.items(): print(f"\n📖 測試 {loader_name}...") try: # 測量載入時間 start_time = time.time() if loader_name == "Unstructured": # Unstructured 支援多種模式 loader = loader_class(self.pdf_path, mode="single") else: loader = loader_class(self.pdf_path) docs = loader.load() end_time = time.time() load_time = end_time - start_time # 分析載入結果 result = self._analyze_loader_result( loader_name, docs, load_time, detailed_analysis ) self.results[loader_name] = result except Exception as e: print(f"❌ {loader_name} 載入失敗: {str(e)}") self.results[loader_name] = {"error": str(e)} # 生成比較報告 self._generate_comparison_report() def _analyze_loader_result(self, loader_name: str, docs: List, load_time: float, detailed: bool) -> Dict: """分析載入器結果""" if not docs: return {"error": "沒有載入到任何文件"} total_content_length = sum(len(doc.page_content) for doc in docs) total_words = sum(len(doc.page_content.split()) for doc in docs) # 收集所有 metadata 欄位 all_metadata_fields = set() for doc in docs: all_metadata_fields.update(doc.metadata.keys()) result = { "load_time": load_time, "document_count": len(docs), "total_content_length": total_content_length, "total_words": total_words, "avg_doc_length": total_content_length / len(docs), "metadata_fields": list(all_metadata_fields), "metadata_field_count": len(all_metadata_fields) } print(f" ⏱️ 載入時間: {load_time:.2f} 秒") print(f" 📄 文件數量: {len(docs)}") print(f" 📝 總內容長度: {total_content_length:,} 字符") print(f" 🔤 總詞數: {total_words:,}") print(f" 🏷️ Metadata 欄位: {len(all_metadata_fields)} 種") if detailed: print(f" 📋 Metadata 欄位詳情: {', '.join(all_metadata_fields)}") # 顯示第一個文件的內容預覽 if docs: preview = docs[0].page_content[:200].replace('\n', ' ') print(f" 👀 內容預覽: {preview}...") return result def _generate_comparison_report(self): """生成載入器比較報告""" print(f"\n{'='*80}") print("📊 PDF 載入器綜合比較報告") print(f"{'='*80}") # 過濾出成功的結果 successful_results = {name: result for name, result in self.results.items() if "error" not in result} if not successful_results: print("❌ 所有載入器都失敗了") return # 速度比較 print(f"\n⚡ 載入速度排名:") speed_ranking = sorted(successful_results.items(), key=lambda x: x[1]["load_time"]) for i, (name, result) in enumerate(speed_ranking, 1): print(f" {i}. {name}: {result['load_time']:.2f} 秒") # 內容提取量比較 print(f"\n📝 內容提取量排名:") content_ranking = sorted(successful_results.items(), key=lambda x: x[1]["total_content_length"], reverse=True) for i, (name, result) in enumerate(content_ranking, 1): print(f" {i}. {name}: {result['total_content_length']:,} 字符") # Metadata 豐富度比較 print(f"\n🏷️ Metadata 豐富度排名:") metadata_ranking = sorted(successful_results.items(), key=lambda x: x[1]["metadata_field_count"], reverse=True) for i, (name, result) in enumerate(metadata_ranking, 1): fields = result["metadata_fields"] print(f" {i}. {name}: {result['metadata_field_count']} 欄位 ({', '.join(fields[:3])}...)") # 推薦使用場景 print(f"\n🎯 使用場景推薦:") fastest = speed_ranking[0][0] most_content = content_ranking[0][0] most_metadata = metadata_ranking[0][0] print(f" 🚀 追求速度: {fastest}") print(f" 📖 內容完整性: {most_content}") print(f" 🏷️ 豐富 Metadata: {most_metadata}") return { "fastest": fastest, "most_content": most_content, "most_metadata": most_metadata, "successful_loaders": list(successful_results.keys()) } def test_special_pdf_features(self): """測試特殊 PDF 功能""" print(f"\n{'='*80}") print("🔬 特殊 PDF 功能測試") print(f"{'='*80}") # 測試 OCR 功能(PyPDF) print(f"\n📷 OCR 功能測試 (PyPDF):") try: loader = PyPDFLoader(self.pdf_path, extract_images=True) docs_with_ocr = loader.load() print(f" ✅ OCR 啟用成功,載入 {len(docs_with_ocr)} 個文件") except Exception as e: print(f" ❌ OCR 功能失敗: {e}") # 測試結構化解析(Unstructured) print(f"\n🏗️ 結構化解析測試 (Unstructured):") try: loader = UnstructuredPDFLoader(self.pdf_path, mode="elements") structured_docs = loader.load() # 分析元素類型 element_types = {} for doc in structured_docs: element_type = doc.metadata.get("category", "unknown") element_types[element_type] = element_types.get(element_type, 0) + 1 print(f" ✅ 結構化解析成功,識別出 {len(structured_docs)} 個元素") print(f" 📊 元素類型分佈:") for element_type, count in element_types.items(): print(f" {element_type}: {count}") except Exception as e: print(f" ❌ 結構化解析失敗: {e}") # 測試表格提取(PDFPlumber) print(f"\n📊 表格提取測試 (PDFPlumber):") try: loader = PDFPlumberLoader(self.pdf_path) docs = loader.load() # 檢查是否包含表格相關內容 table_indicators = ["table", "row", "column", "cell", "|", "---"] has_table_content = any( any(indicator in doc.page_content.lower() for indicator in table_indicators) for doc in docs ) print(f" {'✅' if has_table_content else '❓'} 表格內容檢測: {'發現' if has_table_content else '未發現'}可能的表格結構") except Exception as e: print(f" ❌ 表格提取測試失敗: {e}") # 使用範例 - 需要實際的 PDF 檔案 def demonstrate_pdf_comparison(): """示範 PDF 載入器比較""" # 注意:需要提供實際的 PDF 檔案路徑 pdf_path = "./data/sample.pdf" # 請替換為實際檔案路徑 try: comparator = PDFLoaderComparator(pdf_path) comparator.compare_all_loaders(detailed_analysis=True) comparator.test_special_pdf_features() except FileNotFoundError: print("❌ PDF 檔案不存在,請提供有效的 PDF 檔案路徑") # 創建模擬比較結果 print("\n📋 PDF 載入器特性比較表:") comparison_table = { "載入器": ["PyPDF", "PyMuPDF", "Unstructured", "PDFMiner", "PDFPlumber"], "速度": ["中等", "快速", "較慢", "中等", "較慢"], "文字提取": ["基礎", "優秀", "優秀", "優秀", "良好"], "表格支援": ["無", "有限", "良好", "無", "優秀"], "OCR支援": ["是", "否", "是", "否", "否"], "結構化解析": ["無", "有限", "優秀", "良好", "有限"], "Metadata": ["基礎", "豐富", "豐富", "中等", "豐富"] } import pandas as pd df = pd.DataFrame(comparison_table) print(df.to_string(index=False)) # 執行示範 demonstrate_pdf_comparison() 進階 PDF 處理技巧 class AdvancedPDFProcessor: """進階 PDF 處理器""" def __init__(self): self.processed_pdfs = {} def intelligent_pdf_loading(self, pdf_path: str, content_type: str = "auto") -> List[Document]: """智能 PDF 載入 - 根據內容類型選擇最佳載入器""" print(f"🧠 智能 PDF 載入: {pdf_path}") print(f"內容類型: {content_type}") # 根據內容類型選擇載入器 if content_type == "auto": content_type = self._detect_pdf_content_type(pdf_path) loader_map = { "text_heavy": PyPDFLoader, "structured": UnstructuredPDFLoader, "table_heavy": PDFPlumberLoader, "academic": PyMuPDFLoader, "scanned": lambda path: PyPDFLoader(path, extract_images=True) } loader_class = loader_map.get(content_type, PyPDFLoader) print(f"選擇載入器: {loader_class.__name__}") if content_type == "structured": loader = loader_class(pdf_path, mode="elements") else: loader = loader_class(pdf_path) docs = loader.load() # 後處理 processed_docs = self._post_process_pdf_docs(docs, content_type) print(f"✅ 成功載入 {len(processed_docs)} 個文件") return processed_docs def _detect_pdf_content_type(self, pdf_path: str) -> str: """檢測 PDF 內容類型""" print("🔍 檢測 PDF 內容類型...") # 使用快速載入器進行內容分析 try: loader = PyPDFLoader(pdf_path) sample_docs = loader.load() if not sample_docs: return "text_heavy" # 合併前幾頁內容進行分析 sample_content = " ".join( doc.page_content for doc in sample_docs[:3] ).lower() # 檢測模式 table_indicators = ["table", "row", "column", "|", "---", "cell"] structure_indicators = ["title", "heading", "section", "chapter"] academic_indicators = ["abstract", "introduction", "methodology", "reference", "citation"] table_score = sum(sample_content.count(indicator) for indicator in table_indicators) structure_score = sum(sample_content.count(indicator) for indicator in structure_indicators) academic_score = sum(sample_content.count(indicator) for indicator in academic_indicators) # 檢測是否為掃描文件(文字很少) if len(sample_content.strip()) < 100: detected_type = "scanned" elif table_score > 5: detected_type = "table_heavy" elif academic_score > 3: detected_type = "academic" elif structure_score > 5: detected_type = "structured" else: detected_type = "text_heavy" print(f"檢測結果: {detected_type}") print(f" 表格指標: {table_score}") print(f" 結構指標: {structure_score}") print(f" 學術指標: {academic_score}") return detected_type except Exception as e: print(f"⚠️ 檢測失敗,使用預設類型: {e}") return "text_heavy" def _post_process_pdf_docs(self, docs: List[Document], content_type: str) -> List[Document]: """PDF 文件後處理""" processed_docs = [] for doc in docs: # 清理內容 cleaned_content = self._clean_pdf_content(doc.page_content) # 增強 metadata enhanced_metadata = doc.metadata.copy() enhanced_metadata.update({ "content_type": content_type, "word_count": len(cleaned_content.split()), "char_count": len(cleaned_content), "processed_at": time.time() }) # 根據內容類型添加特殊處理 if content_type == "structured": enhanced_metadata["element_type"] = doc.metadata.get("category", "unknown") processed_doc = Document( page_content=cleaned_content, metadata=enhanced_metadata ) processed_docs.append(processed_doc) return processed_docs def _clean_pdf_content(self, content: str) -> str: """清理 PDF 內容""" import re # 移除多餘的空白和換行 content = re.sub(r'\n\s*\n', '\n\n', content) content = re.sub(r' +', ' ', content) # 移除頁眉頁腳(簡單的規則) lines = content.split('\n') cleaned_lines = [] for line in lines: line = line.strip() # 跳過可能是頁碼的行 if re.match(r'^\d+$', line): continue # 跳過太短的行(可能是頁眉頁腳) if len(line) < 3: continue cleaned_lines.append(line) return '\n'.join(cleaned_lines) def batch_process_pdfs(self, pdf_paths: List[str], max_workers: int = 4) -> Dict[str, List[Document]]: """批次處理多個 PDF 檔案""" import concurrent.futures from pathlib import Path print(f"📚 批次處理 {len(pdf_paths)} 個 PDF 檔案") print(f"使用 {max_workers} 個工作執行緒") results = {} with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor: # 提交所有任務 future_to_path = { executor.submit(self.intelligent_pdf_loading, pdf_path): pdf_path for pdf_path in pdf_paths } # 收集結果 for future in concurrent.futures.as_completed(future_to_path): pdf_path = future_to_path[future] try: docs = future.result() results[pdf_path] = docs print(f"✅ {Path(pdf_path).name}: {len(docs)} 個文件") except Exception as e: print(f"❌ {Path(pdf_path).name}: {e}") results[pdf_path] = [] print(f"\n📊 批次處理完成:") total_docs = sum(len(docs) for docs in results.values()) successful_files = len([path for path, docs in results.items() if docs]) print(f" 成功處理: {successful_files}/{len(pdf_paths)} 檔案") print(f" 總文件數: {total_docs}") return results # 使用範例 advanced_processor = AdvancedPDFProcessor() # 智能載入單個 PDF # docs = advanced_processor.intelligent_pdf_loading("./data/sample.pdf") # 批次處理多個 PDF # pdf_files = ["./data/pdf1.pdf", "./data/pdf2.pdf", "./data/pdf3.pdf"] # batch_results = advanced_processor.batch_process_pdfs(pdf_files) 網頁內容載入與爬取 進階網頁載入技術 import asyncio import aiohttp import bs4 from langchain_community.document_loaders import WebBaseLoader from langchain.schema import Document from typing import List, Dict, Optional, Callable import time import requests from urllib.parse import urljoin, urlparse import robots class AdvancedWebLoader: """進階網頁載入器""" def __init__(self, respect_robots=True, delay_between_requests=1.0): self.respect_robots = respect_robots self.delay_between_requests = delay_between_requests self.session = requests.Session() self.session.headers.update({ 'User-Agent': 'Mozilla/5.0 (compatible; LangChain WebLoader/1.0)' }) def load_with_custom_parser(self, url: str, parser_config: Dict) -> List[Document]: """使用自定義解析器載入網頁""" print(f"🌐 載入網頁: {url}") print(f"解析配置: {parser_config}") # 檢查 robots.txt if self.respect_robots and not self._check_robots_txt(url): raise ValueError(f"robots.txt 禁止訪問: {url}") # 構建 BeautifulSoup 解析器參數 bs_kwargs = self._build_bs_kwargs(parser_config) # 使用 WebBaseLoader loader = WebBaseLoader( web_paths=[url], bs_kwargs=bs_kwargs, header_template=parser_config.get("header_template"), requests_kwargs={ 'timeout': parser_config.get('timeout', 30), 'headers': parser_config.get('headers', {}) } ) docs = loader.load() # 後處理 processed_docs = self._post_process_web_docs(docs, parser_config) print(f"✅ 成功載入 {len(processed_docs)} 個文件") return processed_docs def _check_robots_txt(self, url: str) -> bool: """檢查 robots.txt 是否允許爬取""" try: parsed_url = urlparse(url) robots_url = f"{parsed_url.scheme}://{parsed_url.netloc}/robots.txt" response = self.session.get(robots_url, timeout=10) if response.status_code == 200: rp = robots.RobotFileParser() rp.set_url(robots_url) rp.read() return rp.can_fetch('*', url) return True # 如果沒有 robots.txt,假設允許 except Exception as e: print(f"⚠️ 無法檢查 robots.txt: {e}") return True def _build_bs_kwargs(self, parser_config: Dict) -> Dict: """構建 BeautifulSoup 解析器參數""" bs_kwargs = {} # 解析範圍限制 if 'parse_only' in parser_config: parse_only_config = parser_config['parse_only'] if isinstance(parse_only_config, dict): tag = parse_only_config.get('tag', 'div') attrs = parse_only_config.get('attrs', {}) bs_kwargs['parse_only'] = bs4.SoupStrainer(tag, attrs=attrs) elif isinstance(parse_only_config, str): # CSS 選擇器 bs_kwargs['parse_only'] = bs4.SoupStrainer(parse_only_config) # 解析器類型 parser = parser_config.get('parser', 'html.parser') bs_kwargs['features'] = parser return bs_kwargs def _post_process_web_docs(self, docs: List[Document], config: Dict) -> List[Document]: """網頁文件後處理""" processed_docs = [] for doc in docs: content = doc.page_content # 清理 HTML 內容 if config.get('clean_html', True): content = self._clean_html_content(content) # 提取特定資訊 if config.get('extract_links', False): links = self._extract_links(doc.page_content, doc.metadata.get('source', '')) doc.metadata['extracted_links'] = links # 內容過濾 min_length = config.get('min_content_length', 0) if len(content.strip()) < min_length: continue # 增強 metadata enhanced_metadata = doc.metadata.copy() enhanced_metadata.update({ 'content_length': len(content), 'word_count': len(content.split()), 'processed_at': time.time(), 'loader_config': config.get('name', 'custom') }) processed_doc = Document( page_content=content, metadata=enhanced_metadata ) processed_docs.append(processed_doc) return processed_docs def _clean_html_content(self, content: str) -> str: """清理 HTML 內容""" import re # 移除多餘的空白 content = re.sub(r'\s+', ' ', content) content = re.sub(r'\n\s*\n', '\n\n', content) # 移除常見的網頁雜訊 noise_patterns = [ r'Cookie', r'Privacy Policy', r'Terms of Service', r'Subscribe to newsletter', r'Follow us on', r'Share this article' ] for pattern in noise_patterns: content = re.sub(pattern, '', content, flags=re.IGNORECASE) return content.strip() def _extract_links(self, html_content: str, base_url: str) -> List[Dict]: """從 HTML 內容中提取連結""" soup = bs4.BeautifulSoup(html_content, 'html.parser') links = [] for link in soup.find_all('a', href=True): href = link['href'] text = link.get_text(strip=True) # 轉換為絕對 URL absolute_url = urljoin(base_url, href) links.append({ 'url': absolute_url, 'text': text, 'title': link.get('title', '') }) return links async def batch_load_async(self, urls: List[str], parser_configs: List[Dict], max_concurrent: int = 5) -> Dict[str, List[Document]]: """異步批次載入多個網頁""" print(f"🚀 異步載入 {len(urls)} 個網頁") print(f"最大並發數: {max_concurrent}") semaphore = asyncio.Semaphore(max_concurrent) results = {} async def load_single_url(url: str, config: Dict): async with semaphore: try: # 在這裡我們使用同步方法,實際應用中可能需要真正的異步 HTTP 客戶端 docs = await asyncio.to_thread( self.load_with_custom_parser, url, config ) results[url] = docs print(f"✅ {url}: {len(docs)} 個文件") except Exception as e: print(f"❌ {url}: {e}") results[url] = [] # 請求間延遲 await asyncio.sleep(self.delay_between_requests) # 創建任務 tasks = [] for i, url in enumerate(urls): config = parser_configs[i] if i < len(parser_configs) else parser_configs[0] tasks.append(load_single_url(url, config)) # 執行所有任務 await asyncio.gather(*tasks) print(f"\n📊 異步載入完成:") total_docs = sum(len(docs) for docs in results.values()) successful_urls = len([url for url, docs in results.items() if docs]) print(f" 成功載入: {successful_urls}/{len(urls)} 網頁") print(f" 總文件數: {total_docs}") return results def crawl_sitemap(self, sitemap_url: str, max_pages: int = 100) -> List[str]: """從 sitemap 提取 URL 列表""" print(f"🗺️ 解析 Sitemap: {sitemap_url}") try: loader = WebBaseLoader( web_paths=[sitemap_url], bs_kwargs={"features": "xml"} ) docs = loader.load() if not docs: raise ValueError("無法載入 sitemap") # 解析 XML 內容 soup = bs4.BeautifulSoup(docs[0].page_content, 'xml') # 提取 URL urls = [] for loc in soup.find_all('loc'): url = loc.get_text(strip=True) if url: urls.append(url) if len(urls) >= max_pages: break print(f"✅ 從 sitemap 提取 {len(urls)} 個 URL") return urls except Exception as e: print(f"❌ Sitemap 解析失敗: {e}") return [] # 預定義的解析器配置 PARSER_CONFIGS = { "blog_post": { "name": "blog_post", "parse_only": { "tag": "article", "attrs": {} }, "clean_html": True, "extract_links": True, "min_content_length": 100 }, "news_article": { "name": "news_article", "parse_only": { "tag": "div", "attrs": {"class": ["article-content", "post-content", "entry-content"]} }, "clean_html": True, "extract_links": False, "min_content_length": 200 }, "product_page": { "name": "product_page", "parse_only": { "tag": "div", "attrs": {"class": ["product-description", "product-details"]} }, "clean_html": True, "extract_links": True, "min_content_length": 50 }, "documentation": { "name": "documentation", "parse_only": { "tag": "main", "attrs": {} }, "clean_html": True, "extract_links": True, "min_content_length": 100, "headers": { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" } } } # 使用範例 def demonstrate_advanced_web_loading(): """示範進階網頁載入功能""" web_loader = AdvancedWebLoader( respect_robots=True, delay_between_requests=1.0 ) # 測試 URL 列表 test_urls = [ "https://lilianweng.github.io/posts/2023-06-23-agent/", "https://python.langchain.com/docs/modules/data_connection/document_loaders/", "https://docs.python.org/3/tutorial/" ] print("🎯 進階網頁載入示範") print("=" * 60) # 單個網頁載入示範 print("\n📄 單個網頁載入:") try: docs = web_loader.load_with_custom_parser( test_urls[0], PARSER_CONFIGS["blog_post"] ) if docs: print(f"載入成功: {len(docs)} 個文件") print(f"內容預覽: {docs[0].page_content[:200]}...") print(f"Metadata: {docs[0].metadata}") except Exception as e: print(f"載入失敗: {e}") # 異步批次載入示範 print("\n🚀 異步批次載入示範:") async def async_demo(): try: configs = [ PARSER_CONFIGS["blog_post"], PARSER_CONFIGS["documentation"], PARSER_CONFIGS["documentation"] ] results = await web_loader.batch_load_async( test_urls, configs, max_concurrent=2 ) for url, docs in results.items(): print(f"{url}: {len(docs)} 文件") except Exception as e: print(f"異步載入失敗: {e}") # 運行異步示範 try: asyncio.run(async_demo()) except Exception as e: print(f"異步示範失敗: {e}") # 執行示範 demonstrate_advanced_web_loading() 結構化資料處理 CSV 和資料庫載入器詳解 import pandas as pd import sqlite3 import json from langchain_community.document_loaders.csv_loader import CSVLoader from langchain_community.document_loaders import ( UnstructuredExcelLoader, DataFrameLoader ) from langchain.schema import Document from typing import List, Dict, Any, Optional, Callable import numpy as np class StructuredDataProcessor: """結構化資料處理器""" def __init__(self): self.processors = { 'csv': self._process_csv, 'excel': self._process_excel, 'dataframe': self._process_dataframe, 'json': self._process_json, 'sql': self._process_sql } def process_structured_data(self, data_source: str, data_type: str, processing_config: Dict = None) -> List[Document]: """處理結構化資料""" print(f"📊 處理結構化資料") print(f"資料來源: {data_source}") print(f"資料類型: {data_type}") config = processing_config or {} if data_type not in self.processors: raise ValueError(f"不支援的資料類型: {data_type}") processor = self.processors[data_type] docs = processor(data_source, config) # 統計分析 self._analyze_structured_data(docs, data_type) return docs def _process_csv(self, csv_path: str, config: Dict) -> List[Document]: """處理 CSV 檔案""" print(f"📄 處理 CSV: {csv_path}") # 基本載入 encoding = config.get('encoding', 'utf-8') source_column = config.get('source_column') loader = CSVLoader( file_path=csv_path, encoding=encoding, source_column=source_column ) basic_docs = loader.load() # 進階處理選項 if config.get('use_pandas', False): return self._process_csv_with_pandas(csv_path, config) # 增強 metadata enhanced_docs = [] for i, doc in enumerate(basic_docs): # 解析 CSV 行資料 row_data = self._parse_csv_row(doc.page_content) enhanced_metadata = doc.metadata.copy() enhanced_metadata.update({ 'row_number': i + 1, 'column_count': len(row_data), 'data_type': 'csv_row', 'has_missing_values': any(not value or value.strip() == '' for value in row_data.values()) }) enhanced_doc = Document( page_content=doc.page_content, metadata=enhanced_metadata ) enhanced_docs.append(enhanced_doc) return enhanced_docs def _process_csv_with_pandas(self, csv_path: str, config: Dict) -> List[Document]: """使用 Pandas 處理 CSV""" print("🐼 使用 Pandas 處理 CSV") # 讀取 CSV pd_kwargs = config.get('pandas_kwargs', {}) df = pd.read_csv(csv_path, **pd_kwargs) print(f"載入 DataFrame: {df.shape[0]} 行 x {df.shape[1]} 列") # 資料清理 if config.get('clean_data', True): df = self._clean_dataframe(df) # 根據處理模式生成文件 mode = config.get('mode', 'row_by_row') if mode == 'row_by_row': return self._dataframe_to_docs_by_row(df, config) elif mode == 'column_analysis': return self._dataframe_to_docs_by_column(df, config) elif mode == 'summary': return self._dataframe_to_summary_docs(df, config) else: raise ValueError(f"未知的處理模式: {mode}") def _clean_dataframe(self, df: pd.DataFrame) -> pd.DataFrame: """清理 DataFrame""" print("🧹 清理 DataFrame") original_shape = df.shape # 移除完全空白的行 df = df.dropna(how='all') # 處理缺失值 for column in df.columns: if df[column].dtype == 'object': df[column] = df[column].fillna('') else: df[column] = df[column].fillna(0) # 移除重複行 df = df.drop_duplicates() print(f"清理結果: {original_shape} -> {df.shape}") return df def _dataframe_to_docs_by_row(self, df: pd.DataFrame, config: Dict) -> List[Document]: """將 DataFrame 按行轉換為文件""" content_columns = config.get('content_columns', list(df.columns)) page_content_template = config.get('page_content_template') docs = [] for index, row in df.iterrows(): # 生成頁面內容 if page_content_template: page_content = page_content_template.format(**row.to_dict()) else: # 預設格式 content_parts = [] for col in content_columns: if col in row and pd.notna(row[col]) and str(row[col]).strip(): content_parts.append(f"{col}: {row[col]}") page_content = "\n".join(content_parts) # 生成 metadata metadata = { 'source': config.get('source', 'dataframe'), 'row_index': index, 'data_type': 'dataframe_row' } # 添加數值型資料統計 numeric_columns = df.select_dtypes(include=[np.number]).columns if len(numeric_columns) > 0: numeric_data = row[numeric_columns].to_dict() metadata['numeric_summary'] = { col: float(val) for col, val in numeric_data.items() if pd.notna(val) } # 添加分類型資料 categorical_columns = df.select_dtypes(include=['object']).columns if len(categorical_columns) > 0: categorical_data = row[categorical_columns].to_dict() metadata['categorical_data'] = { col: str(val) for col, val in categorical_data.items() if pd.notna(val) and str(val).strip() } doc = Document(page_content=page_content, metadata=metadata) docs.append(doc) return docs def _dataframe_to_docs_by_column(self, df: pd.DataFrame, config: Dict) -> List[Document]: """將 DataFrame 按列分析轉換為文件""" docs = [] for column in df.columns: series = df[column] # 生成列分析內容 analysis = self._analyze_series(series) page_content = f"Column Analysis: {column}\n" page_content += f"Data Type: {analysis['dtype']}\n" page_content += f"Non-null Count: {analysis['non_null_count']}\n" page_content += f"Unique Values: {analysis['unique_count']}\n" if analysis['dtype'] in ['int64', 'float64']: page_content += f"Mean: {analysis.get('mean', 'N/A')}\n" page_content += f"Std: {analysis.get('std', 'N/A')}\n" page_content += f"Min: {analysis.get('min', 'N/A')}\n" page_content += f"Max: {analysis.get('max', 'N/A')}\n" else: page_content += f"Most Common: {analysis.get('most_common', 'N/A')}\n" metadata = { 'source': config.get('source', 'dataframe'), 'column_name': column, 'data_type': 'column_analysis', 'analysis': analysis } doc = Document(page_content=page_content, metadata=metadata) docs.append(doc) return docs def _analyze_series(self, series: pd.Series) -> Dict: """分析 Pandas Series""" analysis = { 'dtype': str(series.dtype), 'non_null_count': series.count(), 'null_count': series.isnull().sum(), 'unique_count': series.nunique() } if series.dtype in ['int64', 'float64']: analysis.update({ 'mean': series.mean(), 'std': series.std(), 'min': series.min(), 'max': series.max(), 'median': series.median() }) else: if analysis['unique_count'] > 0: most_common = series.value_counts().head(3) analysis['most_common'] = most_common.to_dict() return analysis def _process_excel(self, excel_path: str, config: Dict) -> List[Document]: """處理 Excel 檔案""" print(f"📊 處理 Excel: {excel_path}") # 使用 UnstructuredExcelLoader mode = config.get('mode', 'single') loader = UnstructuredExcelLoader(excel_path, mode=mode) docs = loader.load() # 如果需要更詳細的處理,使用 pandas if config.get('detailed_processing', False): return self._process_excel_with_pandas(excel_path, config) return docs def _process_excel_with_pandas(self, excel_path: str, config: Dict) -> List[Document]: """使用 Pandas 詳細處理 Excel""" print("🐼 使用 Pandas 處理 Excel") # 讀取所有工作表 excel_file = pd.ExcelFile(excel_path) all_docs = [] sheet_names = config.get('sheet_names', excel_file.sheet_names) for sheet_name in sheet_names: print(f"處理工作表: {sheet_name}") df = pd.read_excel(excel_path, sheet_name=sheet_name) # 為這個工作表創建配置 sheet_config = config.copy() sheet_config['source'] = f"{excel_path}#{sheet_name}" # 處理工作表 if config.get('clean_data', True): df = self._clean_dataframe(df) sheet_docs = self._dataframe_to_docs_by_row(df, sheet_config) # 為每個文件添加工作表資訊 for doc in sheet_docs: doc.metadata['sheet_name'] = sheet_name doc.metadata['excel_file'] = excel_path all_docs.extend(sheet_docs) return all_docs def _parse_csv_row(self, content: str) -> Dict[str, str]: """解析 CSV 行內容""" import csv import io # 簡單的 CSV 行解析 reader = csv.DictReader(io.StringIO(content)) try: return next(reader) except: # 如果解析失敗,返回原始內容 return {'content': content} def _analyze_structured_data(self, docs: List[Document], data_type: str): """分析結構化資料載入結果""" print(f"\n📈 結構化資料分析結果:") print(f"資料類型: {data_type}") print(f"文件數量: {len(docs)}") if not docs: print("❌ 沒有載入任何文件") return # 內容長度統計 content_lengths = [len(doc.page_content) for doc in docs] print(f"內容長度統計:") print(f" 平均: {np.mean(content_lengths):.0f} 字符") print(f" 最短: {min(content_lengths)} 字符") print(f" 最長: {max(content_lengths)} 字符") # Metadata 分析 all_metadata_keys = set() for doc in docs: all_metadata_keys.update(doc.metadata.keys()) print(f"Metadata 欄位: {len(all_metadata_keys)} 種") for key in sorted(all_metadata_keys): count = sum(1 for doc in docs if key in doc.metadata) print(f" {key}: {count}/{len(docs)} 文件") # 使用範例 def demonstrate_structured_data_processing(): """示範結構化資料處理""" processor = StructuredDataProcessor() print("🎯 結構化資料處理示範") print("=" * 60) # 創建範例 CSV 資料 sample_data = { 'name': ['Alice', 'Bob', 'Charlie', 'Diana'], 'age': [25, 30, 35, 28], 'city': ['New York', 'London', 'Tokyo', 'Paris'], 'occupation': ['Engineer', 'Designer', 'Teacher', 'Doctor'] } df = pd.DataFrame(sample_data) csv_path = './sample_data.csv' df.to_csv(csv_path, index=False) print(f"✅ 創建範例 CSV: {csv_path}") # 基本 CSV 處理 print("\n📄 基本 CSV 處理:") basic_config = { 'encoding': 'utf-8', 'source_column': 'name' } try: basic_docs = processor.process_structured_data( csv_path, 'csv', basic_config ) if basic_docs: print(f"第一個文件內容: {basic_docs[0].page_content}") print(f"第一個文件 Metadata: {basic_docs[0].metadata}") except Exception as e: print(f"基本處理失敗: {e}") # 進階 Pandas 處理 print("\n🐼 進階 Pandas 處理:") pandas_config = { 'use_pandas': True, 'clean_data': True, 'mode': 'row_by_row', 'content_columns': ['name', 'age', 'city', 'occupation'], 'page_content_template': "Person: {name}, Age: {age}, City: {city}, Job: {occupation}" } try: pandas_docs = processor.process_structured_data( csv_path, 'csv', pandas_config ) if pandas_docs: print(f"Pandas 處理結果: {len(pandas_docs)} 文件") print(f"第一個文件內容: {pandas_docs[0].page_content}") except Exception as e: print(f"Pandas 處理失敗: {e}") # 列分析模式 print("\n📊 列分析模式:") column_config = { 'use_pandas': True, 'mode': 'column_analysis' } try: column_docs = processor.process_structured_data( csv_path, 'csv', column_config ) if column_docs: print(f"列分析結果: {len(column_docs)} 文件") for doc in column_docs[:2]: # 顯示前兩個 print(f"列分析: {doc.page_content[:100]}...") except Exception as e: print(f"列分析失敗: {e}") # 清理範例檔案 import os try: os.remove(csv_path) print(f"\n🧹 已清理範例檔案: {csv_path}") except: pass # 執行示範 demonstrate_structured_data_processing() Office 文檔處理 Word 和 PowerPoint 進階處理 ```pythonfrom langchain_community.document_loaders import ( Docx2txtLoader, UnstructuredWordDocumentLoader, UnstructuredPowerPointLoader)from langchain.schema import Documentimport zipfileimport xml.etree.ElementTree as ETfrom typing import List, Dict, Optionalimport re class OfficeDocumentProcessor: """Office
-
RAG 架構深度剖析 (1) 記憶組件 - Memory
RAG 中的記憶體管理 課程相關code我都上完到github空間中:請 git clone https://github.com/kevin801221/AgenticU-The-Modular-Teaching-Hub-for-Modern-LLM-Agent-Frameworks.git 去下載相關程式碼。(可以幫忙點個星星唷!) 上課時間會再帶真正的jupyter notebook 筆記,裡面將有完整的筆記和code.下面內容會是Memory的摘要內容。 本堂課會都先以Langchain的架構為主去建構RAG Agent, Agentic RAG system. 前言 在建構能夠進行多輪對話的 RAG (Retrieval-Augmented Generation) 應用時,「記憶體 (Memory)」是一個至關重要的組件。它負責儲存、追蹤和取回對話歷史,讓大型語言模型 (LLM) 能夠理解上下文,從而生成更連貫、更具相關性的回應。 LangChain 提供了多種記憶體管理機制,每種機制都有其獨特的運作方式和適用場景。本文件將根據 05-Memory 資料夾中的筆記本內容,詳細介紹幾種核心的記憶體類型。 1. ConversationBufferMemory 這是最基礎的記憶體類型,它會將所有對話歷史完整地儲存在一個緩衝區中,並在需要時將其全部傳遞給 LLM。 核心概念: 完整保存:不對對話內容做任何刪減或摘要,保留最完整的上下文。 簡單易用:實現方式非常直觀,適合快速開發和測試。 潛在問題: Token 限制:隨著對話變長,完整的歷史記錄可能會超過 LLM 的 Token 上下文視窗限制,導致錯誤或效能下降。 程式碼範例: from langchain.memory import ConversationBufferMemory from langchain_openai import ChatOpenAI from langchain.chains import ConversationChain # 初始化記憶體 memory = ConversationBufferMemory() # 建立對話鏈 llm = ChatOpenAI(temperature=0, model_name="gpt-4o") conversation = ConversationChain( llm=llm, memory=memory, ) # 進行對話 response = conversation.predict(input="你好,我想遠端開立一個銀行帳戶。該如何開始?") print(response) # 檢查記憶體中的歷史紀錄 print(memory.load_memory_variables({})["history"]) 2. ConversationBufferWindowMemory 為了緩解 ConversationBufferMemory 可能導致的 Token 過多問題,ConversationBufferWindowMemory 引入了「滑動視窗」的概念。它只會保留最近的 k 次對話互動。 核心概念: 滑動視窗:只儲存最近的 k 輪對話,舊的對話會被捨棄。 控制長度:有效防止記憶體無限增長,確保不會輕易超出 Token 限制。 程式碼範例: from langchain.memory import ConversationBufferWindowMemory # 只保留最近 2 次的對話互動 (k=2) memory = ConversationBufferWindowMemory(k=2, return_messages=True) memory.save_context(inputs={"human": "第一輪問題"}, outputs={"ai": "第一輪回答"}) memory.save_context(inputs={"human": "第二輪問題"}, outputs={"ai": "第二輪回答"}) memory.save_context(inputs={"human": "第三輪問題"}, outputs={"ai": "第三輪回答"}) # 此時,記憶體中只會保留第二輪和第三輪的對話 print(memory.load_memory_variables({})["history"]) 3. ConversationTokenBufferMemory 這種記憶體管理方式更加精確,它不是根據對話的「次數」,而是根據對話內容的「Token 數量」來決定何時需要刪減歷史紀錄。 核心概念: Token 長度限制:設定一個 max_token_limit,當對話歷史的總 Token 數超過此限制時,會從最舊的紀錄開始刪除。 精確控制:比 ConversationBufferWindowMemory 更能精準地控制傳遞給 LLM 的上下文長度。 程式碼範例: from langchain.memory import ConversationTokenBufferMemory from langchain_openai import ChatOpenAI llm = ChatOpenAI(model_name="gpt-4o-mini") # 將最大 Token 長度限制為 150 memory = ConversationTokenBufferMemory( llm=llm, max_token_limit=150, return_messages=True, ) # ... 多次儲存對話 ... # 檢查記憶體,只有總 Token 數在 150 以內的最新對話會被保留 print(memory.load_memory_variables({})["history"]) 4. ConversationSummaryMemory & ConversationSummaryBufferMemory 對於非常長的對話,即使只保留部分歷史也可能佔用大量 Token。ConversationSummaryMemory 提供了一種解決方案:將舊的對話內容進行「摘要」。 ConversationSummaryMemory**:會將所有**的對話歷史都摘要成一段精簡的文字。 **ConversationSummaryBufferMemory**:這是一種混合策略。它會保留最近的一部分對話原文(基於 Token 數量),並將更早的對話內容進行摘要。 核心概念: 對話摘要:利用 LLM 將冗長的對話歷史濃縮成精華摘要,大幅減少 Token 消耗。 **混合策略 (Buffer + Summary)**:兼顧了保留近期對話細節和控制整體上下文長度的需求。 程式碼範例 (ConversationSummaryBufferMemory): from langchain.memory import ConversationSummaryBufferMemory from langchain_openai import ChatOpenAI llm = ChatOpenAI() # 設定 Token 門檻為 200 memory = ConversationSummaryBufferMemory( llm=llm, max_token_limit=200, return_messages=True, ) # ... 多次儲- 存對話 ... # 當對話總 Token 超過 200 時,舊的對話會被摘要 # 最新的對話(在 200 Token 內)會被完整保留 print(memory.load_memory_variables({})["history"]) 5. ConversationEntityMemory & ConversationKGMemory 這兩種記憶體專注於從對話中提取和管理「實體 (Entity)」及其相關資訊。 **ConversationEntityMemory**:以「鍵值對」的形式儲存關於特定實體的資訊。例如,它可以記住 {"Amelia": "是一位屢獲殊榮的風景攝影師..."}。 ConversationKGMemory**:以「知識圖譜 (Knowledge Graph)」的形式儲存實體之間的關係**。它將資訊結構化為「主詞-關係-受詞」的三元組,例如 (Shelly Kim, lives in, Pangyo)。 核心概念: 實體提取:自動從對話中識別出人名、地名、組織等關鍵實體。 結構化儲存:將關於實體的零散資訊整理成結構化的格式,便於查詢和推理。 **關係網絡 (KG)**:ConversationKGMemory 能夠建立複雜的實體關係網絡,回答如「誰是 Shelly 的同事?」這類問題。 程式碼範例 (ConversationKGMemory): from langchain_community.memory.kg import ConversationKGMemory from langchain_openai import ChatOpenAI llm = ChatOpenAI(model_name="gpt-4o", temperature=0) memory = ConversationKGMemory(llm=llm, return_messages=True) memory.save_context( {"input": "這位是居住在板橋的 Shelly Kim。"}, {"output": "你好 Shelly,很高興認識你!"}, ) memory.save_context( {"input": "Shelly Kim 是我們公司的新設計師。"}, {"output": "太好了!歡迎加入我們的團隊。"}, ) # 檢索關於 "Shelly Kim" 的所有結構化資訊 print(memory.load_memory_variables({"input": "誰是 Shelly Kim?"})) # 輸出: {'history': [SystemMessage(content="On Shelly Kim: Shelly Kim lives in Pangyo. Shelly Kim is our company's new designer.")]} 6. VectorStoreRetrieverMemory 這種記憶體將對話歷史儲存在一個「向量資料庫 (Vector Store)」中。當需要取回記憶時,它不是按照時間順序,而是根據「語意相關性」來查詢最相關的對話片段。 核心概念: 向量化儲存:將每段對話轉換成向量 (Embedding) 並存入向量資料庫。 語意搜尋:根據新問題的語意,在資料庫中尋找最相似、最相關的歷史對話。 非時序性:不依賴對話的時間順序,非常適合需要從大量歷史中查找特定資訊的場景,如面試或客服對話。 程式碼範例: import faiss from langchain_openai.embeddings import OpenAIEmbeddings from langchain_community.vectorstores.faiss import FAISS from langchain.memory import VectorStoreRetrieverMemory # 初始化向量資料庫 (以 FAISS 為例) embeddings_model = OpenAIEmbeddings() embedding_size = 1536 index = faiss.IndexFlatL2(embedding_size) vectorstore = FAISS(embeddings_model, index, InMemoryDocstore({}), {}) retriever = vectorstore.as_retriever(search_kwargs={"k": 1}) # 初始化記憶體 memory = VectorStoreRetrieverMemory(retriever=retriever) # ... 儲存多段面試對話 ... # 查詢與 "主修" 相關的歷史對話 print(memory.load_memory_variables({"prompt": "面試者的主修是什麼?"})) 7. 使用資料庫儲存記憶 (以 SQLite 為例) 對於需要持久化儲存、跨會話共享記憶的應用,可以將對話歷史儲存在 SQL 資料庫中。SQLChatMessageHistory 提供了與 SQLAlchemy 相容的介面。 核心概念: 持久化儲存:對話歷史被保存在資料庫檔案中,不會因程式結束而消失。 會話管理:透過 session_id 來區分和管理不同使用者或不同對話的歷史紀錄。 靈活性:支援所有 SQLAlchemy 相容的資料庫 (如 SQLite, PostgreSQL, MySQL)。 程式碼範例: from langchain_community.chat_message_histories import SQLChatMessageHistory # 初始化,指定 session_id 和資料庫連線 chat_message_history = SQLChatMessageHistory( session_id="user123_conversation456", connection="sqlite:///sqlite.db" ) # 新增對話 chat_message_history.add_user_message("你好,我是 Heesun。") chat_message_history.add_ai_message("你好 Heesun!很高興認識你。") # 檢視儲存的訊息 print(chat_message_history.messages) 透過 RunnableWithMessageHistory,可以輕鬆地將這種持久化記憶與 LCEL (LangChain Expression Language) 鏈結合,實現可配置的、按需加載的對話歷史管理。
-
RAG 組件 - 分割器 (text splitter)
LangChain 文字分割器實戰指南 課程相關code我都上完到github空間中:請 git clone https://github.com/kevin801221/AgenticU-The-Modular-Teaching-Hub-for-Modern-LLM-Agent-Frameworks.git 去下載相關程式碼。(可以幫忙點個星星唷!) 📚 參考資源 官方文檔 LangChain Text Splitters 官方文檔 Text Splitters API 參考 相關工具與技術 OpenAI Embeddings - 語意分割所需的向量化模型 Tiktoken - OpenAI 的 Token 計算工具 Semantic Chunking 研究 - 語意分割的理論基礎 目錄 為什麼需要文字分割 分割器選擇指南 基礎文字分割器 遞迴文字分割器 專用格式分割器 語意分割器 進階技巧與最佳實踐 實際應用案例 為什麼需要文字分割 核心問題 在 RAG 系統中,文字分割是必要的,因為: Token 限制:LLM 有上下文長度限制(如 GPT-4 的 8K-32K tokens) 檢索精確性:小區塊比大文件更容易精確匹配查詢 成本控制:較小的上下文意味著更低的 API 調用成本 處理效率:向量化和相似性計算在小區塊上更快 分割的關鍵考量 # 基本分割參數 split_config = { "chunk_size": 1000, # 區塊大小(字符數) "chunk_overlap": 200, # 重疊字符數 "separator": "\n\n" # 分隔符 } # 重疊的重要性 """ 區塊 1: "...人工智能的發展歷程可以追溯到1950年代。" ↓ 200字符重疊 區塊 2: "1950年代,計算機科學家開始探索機器學習..." """ 分割器選擇指南 內容類型 推薦分割器 主要優勢 一般文字 RecursiveCharacterTextSplitter 智能保留語意結構 程式碼 RecursiveCharacterTextSplitter.from_language() 理解語法結構 Markdown MarkdownHeaderTextSplitter 保留標題層次 HTML HTMLHeaderTextSplitter 按標題標籤分割 JSON RecursiveJsonSplitter 保持JSON結構完整 高語意要求 SemanticChunker 基於語意相似性 基礎文字分割器 CharacterTextSplitter 最簡單的分割器,適合結構化程度較低的文字: from langchain_text_splitters import CharacterTextSplitter # 基本使用 text_splitter = CharacterTextSplitter( separator="\n\n", # 分隔符 chunk_size=300, # 區塊大小 chunk_overlap=50, # 重疊大小 length_function=len, # 長度計算函數 ) # 分割文字 docs = text_splitter.create_documents([long_text]) # 分割已有的文檔 split_docs = text_splitter.split_documents(existing_docs) 使用場景與注意事項 def analyze_split_quality(docs): """分析分割品質""" sizes = [len(doc.page_content) for doc in docs] print(f"📊 分割分析:") print(f" 總區塊數: {len(docs)}") print(f" 平均大小: {sum(sizes)/len(sizes):.0f} 字符") print(f" 最大區塊: {max(sizes)} 字符") print(f" 最小區塊: {min(sizes)} 字符") # 檢查是否有過小或過大的區塊 too_small = len([s for s in sizes if s < 50]) too_large = len([s for s in sizes if s > 2000]) if too_small > 0: print(f" ⚠️ {too_small} 個區塊過小 (<50 字符)") if too_large > 0: print(f" ⚠️ {too_large} 個區塊過大 (>2000 字符)") # 使用範例 docs = text_splitter.create_documents([sample_text]) analyze_split_quality(docs) 遞迴文字分割器 RecursiveCharacterTextSplitter 官方推薦的通用分割器,智能地按語意結構分割: from langchain_text_splitters import RecursiveCharacterTextSplitter # 基本配置 text_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, length_function=len, is_separator_regex=False, ) # 遞迴分割邏輯 default_separators = [ "\n\n", # 段落分割(優先) "\n", # 行分割 " ", # 詞分割 "" # 字符分割(最後手段) ] 自定義分隔符 def create_custom_splitter(content_type: str): """根據內容類型創建自定義分割器""" if content_type == "academic_paper": # 學術論文:按節和段落分割 separators = ["\n\n\n", "\n\n", "\n", ". ", " "] elif content_type == "legal_document": # 法律文件:按條款分割 separators = ["\n第", "\n(", "\n\n", "\n", " "] elif content_type == "technical_manual": # 技術手冊:按步驟和段落分割 separators = ["\n步驟", "\n##", "\n\n", "\n", " "] else: # 預設配置 separators = ["\n\n", "\n", " ", ""] return RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=separators ) # 使用範例 academic_splitter = create_custom_splitter("academic_paper") docs = academic_splitter.create_documents([paper_text]) Token 基礎分割 def create_token_based_splitter(model_name: str = "gpt-4"): """創建基於 Token 計算的分割器""" import tiktoken # 獲取對應模型的編碼器 try: encoding = tiktoken.encoding_for_model(model_name) except KeyError: encoding = tiktoken.get_encoding("cl100k_base") # 預設編碼 def token_len(text: str) -> int: """計算文字的 token 數量""" return len(encoding.encode(text)) return RecursiveCharacterTextSplitter( chunk_size=1000, # 1000 tokens chunk_overlap=200, # 200 tokens 重疊 length_function=token_len, is_separator_regex=False, ) # 使用範例 token_splitter = create_token_based_splitter("gpt-4") docs = token_splitter.create_documents([text]) 專用格式分割器 程式碼分割器 from langchain_text_splitters import Language, RecursiveCharacterTextSplitter # 支援的程式語言 supported_languages = [ Language.PYTHON, Language.JAVASCRIPT, Language.TYPESCRIPT, Language.JAVA, Language.CPP, Language.GO, Language.RUST, Language.MARKDOWN ] # Python 程式碼分割 python_splitter = RecursiveCharacterTextSplitter.from_language( language=Language.PYTHON, chunk_size=2000, chunk_overlap=200, ) # 分割 Python 程式碼 python_code = """ class DataProcessor: def __init__(self, data): self.data = data def process(self): # 處理資料的邏輯 return processed_data def main(): processor = DataProcessor(raw_data) result = processor.process() return result """ docs = python_splitter.create_documents([python_code]) # 檢查分割結果 for i, doc in enumerate(docs): print(f"區塊 {i+1}:") print(doc.page_content) print("-" * 40) Markdown 分割器 from langchain_text_splitters import MarkdownHeaderTextSplitter # 定義要分割的標題層級 headers_to_split_on = [ ("#", "Header 1"), # H1 標題 ("##", "Header 2"), # H2 標題 ("###", "Header 3"), # H3 標題 ] markdown_splitter = MarkdownHeaderTextSplitter( headers_to_split_on=headers_to_split_on, strip_headers=False, # 保留標題 ) # 範例 Markdown 文檔 markdown_doc = """ # 機器學習簡介 機器學習是人工智能的一個重要分支。 ## 監督學習 監督學習使用標記的資料進行訓練。 ### 分類問題 分類是預測離散標籤的任務。 ### 回歸問題 回歸是預測連續數值的任務。 ## 無監督學習 無監督學習從未標記的資料中學習模式。 """ # 執行分割 md_header_splits = markdown_splitter.split_text(markdown_doc) # 檢查結果和 metadata for split in md_header_splits: print(f"內容: {split.page_content[:50]}...") print(f"Metadata: {split.metadata}") print("-" * 50) JSON 分割器 from langchain_text_splitters import RecursiveJsonSplitter # JSON 資料範例 json_data = { "users": [ { "id": 1, "name": "Alice", "profile": { "age": 30, "city": "台北", "interests": ["閱讀", "旅行", "程式設計"] } }, { "id": 2, "name": "Bob", "profile": { "age": 25, "city": "高雄", "interests": ["音樂", "運動"] } } ], "metadata": { "version": "1.0", "created_at": "2024-01-01" } } # 創建 JSON 分割器 json_splitter = RecursiveJsonSplitter(max_chunk_size=300) # 分割 JSON json_chunks = json_splitter.split_json(json_data=json_data) # 檢查分割結果 for i, chunk in enumerate(json_chunks): print(f"JSON 區塊 {i+1}:") print(chunk.page_content) print("-" * 30) 語意分割器 SemanticChunker 基於語意相似性的智能分割器: from langchain_experimental.text_splitter import SemanticChunker from langchain_openai.embeddings import OpenAIEmbeddings # 創建語意分割器 embeddings = OpenAIEmbeddings() semantic_splitter = SemanticChunker( embeddings, breakpoint_threshold_type="percentile", # 閾值類型 breakpoint_threshold_amount=95 # 在相似度最低的5%處分割 ) # 不同的閾值類型 threshold_types = { "percentile": "百分位數 - 在相似度差異最大的地方分割", "standard_deviation": "標準差 - 基於統計分佈分割", "interquartile": "四分位距 - 基於中位數分割" } # 使用範例 long_article = """ 人工智能的發展可以分為幾個重要階段。早期的人工智能研究主要集中在符號推理和專家系統上。 機器學習的出現標誌著AI發展的新階段。通過從資料中學習模式,機器能夠在沒有明確程式設計的情況下改善性能。 深度學習進一步推動了AI的發展。神經網路的多層結構使得機器能夠學習更複雜的表示和模式。 如今,大型語言模型代表了AI的最新發展。這些模型能夠理解和生成人類語言,開啟了新的應用可能性。 """ docs = semantic_splitter.create_documents([long_article]) # 分析語意分割效果 def analyze_semantic_splits(docs): """分析語意分割效果""" print(f"🧠 語意分割分析:") print(f" 總區塊數: {len(docs)}") for i, doc in enumerate(docs): sentences = doc.page_content.split('。') print(f"\n 區塊 {i+1} ({len(doc.page_content)} 字符):") print(f" 句子數: {len([s for s in sentences if s.strip()])}") print(f" 內容: {doc.page_content[:100]}...") analyze_semantic_splits(docs) 語意分割進階配置 def create_adaptive_semantic_splitter(content_type: str): """根據內容類型創建自適應語意分割器""" configs = { "technical": { "threshold_type": "standard_deviation", "threshold_amount": 1.5, # 較嚴格的分割 "description": "技術文檔需要精確的語意邊界" }, "narrative": { "threshold_type": "percentile", "threshold_amount": 90, # 較寬鬆的分割 "description": "敘述性文字允許更長的語意區塊" }, "mixed": { "threshold_type": "interquartile", "threshold_amount": 1.25, # 中等分割 "description": "混合內容採用平衡策略" } } config = configs.get(content_type, configs["mixed"]) return SemanticChunker( embeddings=OpenAIEmbeddings(), breakpoint_threshold_type=config["threshold_type"], breakpoint_threshold_amount=config["threshold_amount"] ), config["description"] # 使用範例 tech_splitter, description = create_adaptive_semantic_splitter("technical") print(f"配置說明: {description}") 進階技巧與最佳實踐 混合分割策略 def hybrid_text_splitting(text: str, content_type: str = "mixed"): """混合分割策略:結合多種分割器的優勢""" from langchain.schema import Document # 第一階段:結構化分割 if "markdown" in content_type.lower(): # 先按標題分割 md_splitter = MarkdownHeaderTextSplitter([ ("#", "Header 1"), ("##", "Header 2"), ]) initial_docs = md_splitter.split_text(text) else: # 一般文字先做基本分割 initial_docs = [Document(page_content=text)] # 第二階段:遞迴細分 recursive_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, ) final_docs = [] for doc in initial_docs: if len(doc.page_content) > 1000: # 大區塊進一步分割 sub_docs = recursive_splitter.split_documents([doc]) final_docs.extend(sub_docs) else: final_docs.append(doc) return final_docs # 使用範例 hybrid_docs = hybrid_text_splitting(sample_text, "markdown") 分割品質評估 def evaluate_split_quality(docs: list, original_text: str): """評估分割品質""" # 基本統計 chunk_sizes = [len(doc.page_content) for doc in docs] total_chars = sum(chunk_sizes) # 計算資訊保留度 original_length = len(original_text) retention_rate = total_chars / original_length # 計算語意連貫性(簡化版) coherence_scores = [] for doc in docs: sentences = doc.page_content.split('。') if len(sentences) > 1: # 簡單評估:檢查是否有不完整的句子 incomplete = sum(1 for s in sentences if len(s.strip()) < 10) coherence = 1 - (incomplete / len(sentences)) coherence_scores.append(coherence) avg_coherence = sum(coherence_scores) / len(coherence_scores) if coherence_scores else 0 # 生成評估報告 report = { "總區塊數": len(docs), "平均區塊大小": sum(chunk_sizes) / len(chunk_sizes), "資訊保留率": f"{retention_rate:.2%}", "語意連貫性": f"{avg_coherence:.2%}", "大小分佈": { "最小": min(chunk_sizes), "最大": max(chunk_sizes), "標準差": (sum((x - sum(chunk_sizes)/len(chunk_sizes))**2 for x in chunk_sizes) / len(chunk_sizes))**0.5 } } return report # 使用範例 quality_report = evaluate_split_quality(docs, original_text) print("📊 分割品質評估:") for key, value in quality_report.items(): print(f" {key}: {value}") 動態分割參數調整 def adaptive_chunk_size(text: str, target_chunks: int = None): """根據文本長度自適應調整分割參數""" text_length = len(text) if target_chunks: # 根據目標區塊數計算大小 chunk_size = text_length // target_chunks chunk_size = max(500, min(chunk_size, 2000)) # 限制在合理範圍 else: # 根據文本長度自動調整 if text_length < 2000: chunk_size = 500 elif text_length < 10000: chunk_size = 1000 else: chunk_size = 1500 # 重疊大小為區塊大小的 15-20% chunk_overlap = int(chunk_size * 0.175) return { "chunk_size": chunk_size, "chunk_overlap": chunk_overlap, "estimated_chunks": text_length // (chunk_size - chunk_overlap) } # 使用範例 text_stats = adaptive_chunk_size(sample_text, target_chunks=10) print(f"建議參數: {text_stats}") # 應用建議參數 splitter = RecursiveCharacterTextSplitter(**text_stats) docs = splitter.create_documents([sample_text]) 實際應用案例 案例 1:技術文檔處理 def process_technical_documentation(docs): """處理技術文檔的專用流程""" # 1. 識別內容類型 def identify_content_type(text): if "```" in text and "def " in text: return "code_heavy" elif "# " in text and "## " in text: return "markdown" else: return "general" processed_docs = [] for doc in docs: content_type = identify_content_type(doc.page_content) # 2. 選擇適當的分割器 if content_type == "code_heavy": splitter = RecursiveCharacterTextSplitter.from_language( language=Language.PYTHON, chunk_size=1500, chunk_overlap=200 ) elif content_type == "markdown": # 先按標題分割,再遞迴細分 md_splitter = MarkdownHeaderTextSplitter([ ("##", "Section"), ("###", "Subsection") ]) md_splits = md_splitter.split_text(doc.page_content) recursive_splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=150 ) for split in md_splits: if len(split.page_content) > 1000: sub_splits = recursive_splitter.split_documents([split]) processed_docs.extend(sub_splits) else: processed_docs.append(split) continue else: splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200 ) # 3. 執行分割 splits = splitter.split_documents([doc]) processed_docs.extend(splits) return processed_docs 案例 2:多語言內容處理 def multilingual_text_splitting(text: str, primary_language: str = "zh"): """多語言文本分割""" import re # 語言特定的分隔符 separators_config = { "zh": ["\n\n", "\n", "。", ";", ",", " "], # 中文 "en": ["\n\n", "\n", ". ", "; ", ", ", " "], # 英文 "ja": ["\n\n", "\n", "。", "、", " "], # 日文 } # 檢測文本中的語言分佈 def detect_language_sections(text): # 簡化的語言檢測 chinese_chars = len(re.findall(r'[\u4e00-\u9fff]', text)) english_chars = len(re.findall(r'[a-zA-Z]', text)) japanese_chars = len(re.findall(r'[\u3040-\u309f\u30a0-\u30ff]', text)) total_chars = chinese_chars + english_chars + japanese_chars if total_chars == 0: return primary_language ratios = { "zh": chinese_chars / total_chars, "en": english_chars / total_chars, "ja": japanese_chars / total_chars } return max(ratios, key=ratios.get) detected_lang = detect_language_sections(text) separators = separators_config.get(detected_lang, separators_config["en"]) # 創建適合的分割器 splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200, separators=separators ) docs = splitter.create_documents([text]) # 為每個文檔添加語言資訊 for doc in docs: doc.metadata["detected_language"] = detected_lang doc.metadata["language_confidence"] = detect_language_sections(doc.page_content) return docs 案例 3:學術論文處理 def academic_paper_processing(paper_text: str): """學術論文專用處理流程""" # 1. 識別論文結構 sections = { "abstract": r"(abstract|摘要)", "introduction": r"(introduction|引言|前言)", "methodology": r"(methodology|method|方法)", "results": r"(results|結果)", "discussion": r"(discussion|討論)", "conclusion": r"(conclusion|結論)", "references": r"(references|參考文獻)" } # 2. 按節分割 section_docs = [] import re for section_name, pattern in sections.items(): matches = list(re.finditer(pattern, paper_text, re.IGNORECASE)) if matches: for match in matches: # 找到該節的開始位置 start = match.start() # 找到下一節的開始位置 next_section_start = len(paper_text) for next_pattern in sections.values(): next_matches = list(re.finditer(next_pattern, paper_text[start+100:], re.IGNORECASE)) if next_matches: next_section_start = min(next_section_start, start + 100 + next_matches[0].start()) section_text = paper_text[start:next_section_start].strip() if len(section_text) > 100: # 過濾太短的內容 doc = Document( page_content=section_text, metadata={ "section": section_name, "document_type": "academic_paper" } ) section_docs.append(doc) # 3. 對長節進一步分割 final_docs = [] for doc in section_docs: if len(doc.page_content) > 1500: # 根據節的類型選擇分割策略 if doc.metadata["section"] in ["methodology", "results"]: # 方法和結果部分可能包含較多技術細節 splitter = RecursiveCharacterTextSplitter( chunk_size=1200, chunk_overlap=200, separators=["\n\n", "\n", ". ", " "] ) else: # 其他部分使用標準分割 splitter = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=150 ) sub_docs = splitter.split_documents([doc]) # 保留原始節資訊 for sub_doc in sub_docs: sub_doc.metadata.update(doc.metadata) final_docs.extend(sub_docs) else: final_docs.append(doc) return final_docs 總結與最佳實踐 🎯 分割器選擇決策樹 文本類型判斷 ├── 一般文本 → RecursiveCharacterTextSplitter ├── 程式碼 → RecursiveCharacterTextSplitter.from_language() ├── Markdown → MarkdownHeaderTextSplitter + Recursive ├── JSON → RecursiveJsonSplitter ├── 高語意要求 → SemanticChunker └── 混合內容 → 混合策略 📏 參數設置指南 內容類型 chunk_size chunk_overlap 說明 短文檔 500-800 100-150 保持完整性 長文檔 1000-1500 200-300 平衡效率與質量 技術文檔 1200-2000 200-400 保留技術概念完整性 對話文本 300-600 50-100 保持對話流暢性 ⚡ 效能最佳化建議 預處理優化 清理多餘空白和特殊字符 統一換行符格式 移除不必要的元數據 分割策略優化 短文檔避免過度分割 長文檔採用分層分割 根據下游任務調整參數 記憶體管理 大文檔流式處理 及時釋放不需要的中間結果 使用生成器處理大量文檔 🚨 常見陷阱與解決方案 1. 過度分割問題 # ❌ 錯誤:chunk_size 設置過小 bad_splitter = RecursiveCharacterTextSplitter( chunk_size=100, # 太小了! chunk_overlap=50 ) # ✅ 正確:根據內容調整大小 good_splitter = RecursiveCharacterTextSplitter( chunk_size=800, # 適中的大小 chunk_overlap=100 ) 2. 重疊設置不當 # ❌ 錯誤:重疊太大或太小 bad_overlap = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=800 # 重疊 80%,太多了! ) # ✅ 正確:重疊 15-25% 比較適合 good_overlap = RecursiveCharacterTextSplitter( chunk_size=1000, chunk_overlap=200 # 重疊 20% ) 3. 忽略語意邊界 def check_semantic_boundaries(docs): """檢查是否在不當位置分割""" issues = [] for i, doc in enumerate(docs): content = doc.page_content # 檢查是否在句子中間分割 if not content.rstrip().endswith(('.', '!', '?', '。', '!', '?')): issues.append(f"區塊 {i+1} 可能在句子中間分割") # 檢查是否分割了重要概念 if content.count(',') > content.count('。') * 3: issues.append(f"區塊 {i+1} 可能分割了長句或列表") return issues # 使用範例 issues = check_semantic_boundaries(docs) if issues: print("⚠️ 發現潛在分割問題:") for issue in issues: print(f" {issue}") 📊 分割效果評估工具 def comprehensive_split_analysis(original_text: str, docs: list): """全面的分割效果分析""" analysis = {} # 1. 基礎統計 chunk_sizes = [len(doc.page_content) for doc in docs] analysis['basic_stats'] = { 'total_chunks': len(docs), 'avg_size': sum(chunk_sizes) / len(chunk_sizes), 'size_std': (sum((x - analysis['basic_stats']['avg_size'])**2 for x in chunk_sizes) / len(chunk_sizes))**0.5, 'min_size': min(chunk_sizes), 'max_size': max(chunk_sizes) } # 2. 內容保留分析 total_split_length = sum(chunk_sizes) analysis['content_retention'] = { 'original_length': len(original_text), 'total_split_length': total_split_length, 'retention_rate': total_split_length / len(original_text) } # 3. 分佈分析 size_ranges = { 'very_small': len([s for s in chunk_sizes if s < 200]), 'small': len([s for s in chunk_sizes if 200 <= s < 500]), 'medium': len([s for s in chunk_sizes if 500 <= s < 1000]), 'large': len([s for s in chunk_sizes if 1000 <= s < 1500]), 'very_large': len([s for s in chunk_sizes if s >= 1500]) } analysis['size_distribution'] = size_ranges # 4. 語意完整性檢查(簡化) incomplete_chunks = 0 for doc in docs: content = doc.page_content.strip() if not content.endswith(('.', '!', '?', '。', '!', '?', '\n')): incomplete_chunks += 1 analysis['semantic_integrity'] = { 'incomplete_chunks': incomplete_chunks, 'completion_rate': 1 - (incomplete_chunks / len(docs)) } return analysis # 使用範例並生成報告 def print_analysis_report(analysis): """印出分析報告""" print("📊 文字分割分析報告") print("=" * 50) # 基礎統計 stats = analysis['basic_stats'] print(f"\n📈 基礎統計:") print(f" 總區塊數: {stats['total_chunks']}") print(f" 平均大小: {stats['avg_size']:.0f} 字符") print(f" 大小標準差: {stats['size_std']:.0f}") print(f" 大小範圍: {stats['min_size']} - {stats['max_size']}") # 內容保留 retention = analysis['content_retention'] print(f"\n💾 內容保留:") print(f" 原始長度: {retention['original_length']:,} 字符") print(f" 分割後總長度: {retention['total_split_length']:,} 字符") print(f" 保留率: {retention['retention_rate']:.1%}") # 大小分佈 dist = analysis['size_distribution'] print(f"\n📊 大小分佈:") for size_range, count in dist.items(): percentage = count / stats['total_chunks'] * 100 print(f" {size_range}: {count} 個 ({percentage:.1f}%)") # 語意完整性 integrity = analysis['semantic_integrity'] print(f"\n🎯 語意完整性:") print(f" 不完整區塊: {integrity['incomplete_chunks']}") print(f" 完整性評分: {integrity['completion_rate']:.1%}") # 建議 print(f"\n💡 最佳化建議:") if integrity['completion_rate'] < 0.8: print(" • 考慮調整分隔符優先級") print(" • 增加 chunk_overlap 大小") if dist['very_small'] > stats['total_chunks'] * 0.2: print(" • 過小區塊過多,考慮增加 chunk_size") if dist['very_large'] > stats['total_chunks'] * 0.1: print(" • 過大區塊存在,考慮減少 chunk_size") if stats['size_std'] > stats['avg_size'] * 0.5: print(" • 區塊大小變異過大,考慮使用 SemanticChunker") # 完整使用範例 splitter = RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200) docs = splitter.create_documents([sample_text]) analysis = comprehensive_split_analysis(sample_text, docs) print_analysis_report(analysis) 🔧 實用工具函數 def optimize_split_parameters(text: str, target_chunk_count: int = None): """自動最佳化分割參數""" text_length = len(text) if target_chunk_count: optimal_chunk_size = text_length // target_chunk_count # 限制在合理範圍內 optimal_chunk_size = max(300, min(optimal_chunk_size, 2000)) else: # 根據文本長度自動調整 if text_length < 1000: optimal_chunk_size = 300 elif text_length < 5000: optimal_chunk_size = 800 elif text_length < 20000: optimal_chunk_size = 1200 else: optimal_chunk_size = 1500 optimal_overlap = int(optimal_chunk_size * 0.2) # 20% 重疊 return { 'chunk_size': optimal_chunk_size, 'chunk_overlap': optimal_overlap, 'estimated_chunks': text_length // (optimal_chunk_size - optimal_overlap) } def batch_process_with_different_splitters(texts: list): """批次測試不同分割器的效果""" splitters = { 'character': CharacterTextSplitter(chunk_size=1000, chunk_overlap=200), 'recursive': RecursiveCharacterTextSplitter(chunk_size=1000, chunk_overlap=200), 'semantic': SemanticChunker(OpenAIEmbeddings(), breakpoint_threshold_type="percentile") } results = {} for splitter_name, splitter in splitters.items(): results[splitter_name] = [] for i, text in enumerate(texts): try: docs = splitter.create_documents([text]) analysis = comprehensive_split_analysis(text, docs) results[splitter_name].append({ 'text_id': i, 'chunk_count': len(docs), 'avg_chunk_size': analysis['basic_stats']['avg_size'], 'completion_rate': analysis['semantic_integrity']['completion_rate'] }) except Exception as e: print(f"❌ {splitter_name} 處理文本 {i} 時出錯: {e}") results[splitter_name].append(None) # 生成比較報告 print("\n📊 分割器比較報告:") print("-" * 60) for splitter_name, result_list in results.items(): valid_results = [r for r in result_list if r is not None] if valid_results: avg_chunks = sum(r['chunk_count'] for r in valid_results) / len(valid_results) avg_size = sum(r['avg_chunk_size'] for r in valid_results) / len(valid_results) avg_completion = sum(r['completion_rate'] for r in valid_results) / len(valid_results) print(f"\n{splitter_name.upper()}:") print(f" 平均區塊數: {avg_chunks:.1f}") print(f" 平均區塊大小: {avg_size:.0f}") print(f" 平均完整性: {avg_completion:.1%}") return results 結論 🎯 關鍵要點總結 選擇合適的分割器 一般文本:RecursiveCharacterTextSplitter 程式碼:語言專用分割器 結構化內容:專用格式分割器 高語意要求:SemanticChunker 參數調整原則 chunk_size:根據內容類型和下游任務調整 chunk_overlap:通常設為 chunk_size 的 15-25% 分隔符:根據內容結構自定義 品質保證 定期評估分割效果 監控語意完整性 根據實際效果調整策略 🚀 進階發展方向 智能分割:結合 NLP 技術的更精確語意邊界識別 多模態分割:處理包含圖片、表格的複雜文檔 動態調整:根據檢索效果自動最佳化分割參數 領域特化:針對特定領域的專用分割策略 文字分割是 RAG 系統中的關鍵環節,正確的分割策略能顯著提升系統的檢索準確性和生成品質。選擇最適合的分割器和參數,並持續監控和最佳化,是建構高效 RAG 系統的重要基礎。 本指南基於 LangChain 官方文檔和實際專案經驗編寫,持續更新以反映最新的技術發展和最佳實踐。
-
RAG 組件 - 嵌入模型 (embedding model)
課程相關code我都上完到github空間中:請 git clone https://github.com/kevin801221/AgenticU-The-Modular-Teaching-Hub-for-Modern-LLM-Agent-Frameworks.git 去下載相關程式碼。(可以幫忙點個星星唷!) LangChain 嵌入模型實戰指南 📚 參考資源 官方文檔 LangChain Embeddings 官方文檔 Embedding Models 整合列表 重要工具與平台 OpenAI Embeddings API - 商業級嵌入服務 Hugging Face Model Hub - 開源嵌入模型庫 MTEB Leaderboard - 嵌入模型效能排行榜 Ollama - 本地模型部署工具 Sentence Transformers - 語句嵌入專用庫 研究與基準 CLIP 論文 - 多模態嵌入的基礎 E5 系列模型 - 指令式嵌入模型 BGE 系列模型 - 中英文嵌入模型 目錄 嵌入基礎概念 模型選擇指南 商業嵌入服務 開源嵌入模型 本地部署方案 多模態嵌入 效能最佳化 實際應用案例 嵌入基礎概念 什麼是嵌入? 嵌入(Embedding)是將文字、圖像等高維度資料映射到低維度向量空間的技術,讓電腦能理解語意關係。 # 嵌入的基本概念 text_1 = "貓是可愛的動物" text_2 = "狗是忠誠的寵物" text_3 = "今天天氣很好" # 轉換為向量後 vector_1 = [0.2, 0.8, 0.1, ...] # 動物相關 vector_2 = [0.3, 0.7, 0.2, ...] # 動物相關(相似) vector_3 = [0.9, 0.1, 0.8, ...] # 天氣相關(不相似) 核心應用場景 def demonstrate_embedding_applications(): """展示嵌入的主要應用""" applications = { "語意搜尋": { "描述": "找到意思相近但用詞不同的內容", "範例": "搜尋'汽車' → 找到'轎車'、'車輛'、'交通工具'" }, "文件分類": { "描述": "根據內容自動分類文檔", "範例": "新聞分類、郵件分類、產品分類" }, "推薦系統": { "描述": "找到相似的內容進行推薦", "範例": "相似文章推薦、產品推薦" }, "異常檢測": { "描述": "識別與正常內容差異很大的文字", "範例": "垃圾郵件檢測、內容審核" } } return applications 向量相似度計算 import numpy as np def calculate_similarity(vector1, vector2, method="cosine"): """計算向量相似度""" if method == "cosine": # 餘弦相似度:最常用,範圍 -1 到 1 dot_product = np.dot(vector1, vector2) norm_a = np.linalg.norm(vector1) norm_b = np.linalg.norm(vector2) return dot_product / (norm_a * norm_b) elif method == "euclidean": # 歐幾里得距離:越小越相似 return np.linalg.norm(vector1 - vector2) elif method == "dot_product": # 點積:需要向量已正規化 return np.dot(vector1, vector2) # 使用範例 def compare_texts_with_embeddings(embedding_model, texts): """比較文字的語意相似度""" # 獲取嵌入向量 vectors = embedding_model.embed_documents(texts) # 計算兩兩相似度 similarity_matrix = [] for i, vec1 in enumerate(vectors): row = [] for j, vec2 in enumerate(vectors): similarity = calculate_similarity(vec1, vec2) row.append(similarity) similarity_matrix.append(row) return similarity_matrix 模型選擇指南 選擇決策樹 考量因素 商業服務 開源模型 本地部署 成本 按使用量付費 免費 硬體成本 效能 通常最佳 中等到優秀 取決於硬體 隱私 資料上傳雲端 可控制 完全本地 維護 無需維護 需要更新 需要管理 客製化 有限 可微調 完全控制 模型比較表 模型類型 代表模型 維度 語言支援 最適場景 OpenAI text-embedding-3-large 3072 多語言 高品質商業應用 多語言E5 multilingual-e5-large 1024 100+ 語言 多語言場景 BGE bge-large-zh-v1.5 1024 中英文 中文優化 本地模型 nomic-embed-text 768 英文為主 隱私優先 商業嵌入服務 OpenAI Embeddings 最受歡迎的商業嵌入服務,提供優秀的效能和易用性: from langchain_openai import OpenAIEmbeddings import os # 基本配置 openai_embeddings = OpenAIEmbeddings( model="text-embedding-3-large", # 或 text-embedding-3-small api_key=os.getenv("OPENAI_API_KEY") ) # 基本使用 def basic_openai_embedding_usage(): """OpenAI 嵌入基本使用""" # 單個查詢 query = "什麼是機器學習?" query_vector = openai_embeddings.embed_query(query) # 多個文檔 documents = [ "機器學習是人工智能的一個分支。", "深度學習使用神經網路進行學習。", "自然語言處理專注於理解人類語言。" ] doc_vectors = openai_embeddings.embed_documents(documents) print(f"查詢向量維度: {len(query_vector)}") print(f"文檔向量數量: {len(doc_vectors)}") return query_vector, doc_vectors # 進階配置 def advanced_openai_config(): """OpenAI 嵌入進階配置""" # 調整向量維度(節省成本) small_dim_embeddings = OpenAIEmbeddings( model="text-embedding-3-large", dimensions=1536, # 原始 3072 壓縮到 1536 ) # 批次處理設置 batch_embeddings = OpenAIEmbeddings( model="text-embedding-3-large", chunk_size=1000, # 每批處理的文檔數 max_retries=3, # 重試次數 request_timeout=60 # 超時設置 ) return small_dim_embeddings, batch_embeddings # 成本計算 def calculate_openai_cost(text_list, model="text-embedding-3-large"): """計算 OpenAI 嵌入成本""" import tiktoken # 獲取對應的編碼器 encoding = tiktoken.encoding_for_model("gpt-4") # 通用編碼器 total_tokens = 0 for text in text_list: tokens = len(encoding.encode(text)) total_tokens += tokens # 價格表(2024年價格,請查看最新價格) prices = { "text-embedding-3-large": 0.00013 / 1000, # 每1K tokens "text-embedding-3-small": 0.00002 / 1000, "text-embedding-ada-002": 0.00010 / 1000 } cost = total_tokens * prices.get(model, prices["text-embedding-3-large"]) return { "first_run_time": time_1, "cached_run_time": time_2, "speedup": speedup, "cache_stats": stats } 批次處理最佳化 def batch_embedding_optimizer(): """批次嵌入最佳化""" class BatchEmbeddingProcessor: def __init__(self, embedding_model, batch_size=100): self.embedding_model = embedding_model self.batch_size = batch_size def process_large_dataset(self, documents): """處理大型文檔集合""" all_embeddings = [] total_batches = (len(documents) + self.batch_size - 1) // self.batch_size print(f"處理 {len(documents)} 個文檔,分為 {total_batches} 批次") for i in range(0, len(documents), self.batch_size): batch = documents[i:i + self.batch_size] batch_num = i // self.batch_size + 1 print(f"處理批次 {batch_num}/{total_batches}...") try: batch_embeddings = self.embedding_model.embed_documents(batch) all_embeddings.extend(batch_embeddings) except Exception as e: print(f"批次 {batch_num} 處理失敗: {e}") # 添加空向量作為佔位符 all_embeddings.extend([None] * len(batch)) return all_embeddings def process_with_progress(self, documents): """帶進度條的處理""" try: from tqdm import tqdm all_embeddings = [] for i in tqdm(range(0, len(documents), self.batch_size), desc="嵌入處理"): batch = documents[i:i + self.batch_size] batch_embeddings = self.embedding_model.embed_documents(batch) all_embeddings.extend(batch_embeddings) return all_embeddings except ImportError: print("tqdm 未安裝,使用基本處理...") return self.process_large_dataset(documents) return BatchEmbeddingProcessor # 記憶體監控 def monitor_embedding_memory(): """監控嵌入過程的記憶體使用""" import psutil import os def memory_monitor_decorator(func): def wrapper(*args, **kwargs): process = psutil.Process(os.getpid()) # 記錄開始狀態 start_memory = process.memory_info().rss / 1024 / 1024 # MB # 執行函數 result = func(*args, **kwargs) # 記錄結束狀態 end_memory = process.memory_info().rss / 1024 / 1024 # MB memory_increase = end_memory - start_memory print(f"記憶體使用: {start_memory:.1f} MB → {end_memory:.1f} MB") print(f"記憶體增長: {memory_increase:.1f} MB") return result return wrapper return memory_monitor_decorator 並行處理 import concurrent.futures import threading from typing import List, Callable class ParallelEmbeddingProcessor: """並行嵌入處理器""" def __init__(self, embedding_models: List, max_workers: int = 4): self.embedding_models = embedding_models self.max_workers = max_workers self.lock = threading.Lock() def process_with_multiple_models(self, documents: List[str]): """使用多個模型並行處理""" results = {} def process_with_model(model_info): model_name, model = model_info try: embeddings = model.embed_documents(documents) with self.lock: results[model_name] = { "embeddings": embeddings, "dimension": len(embeddings[0]) if embeddings else 0, "success": True } except Exception as e: with self.lock: results[model_name] = { "error": str(e), "success": False } # 並行執行 with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor: model_items = [(name, model) for name, model in self.embedding_models.items()] executor.map(process_with_model, model_items) return results def distributed_processing(self, documents: List[str], chunk_size: int = 1000): """分散式文檔處理""" # 將文檔分塊 document_chunks = [ documents[i:i + chunk_size] for i in range(0, len(documents), chunk_size) ] all_results = [] def process_chunk(chunk_info): chunk_idx, chunk = chunk_info # 選擇模型(輪詢) model_names = list(self.embedding_models.keys()) selected_model_name = model_names[chunk_idx % len(model_names)] selected_model = self.embedding_models[selected_model_name] try: embeddings = selected_model.embed_documents(chunk) return { "chunk_idx": chunk_idx, "embeddings": embeddings, "model_used": selected_model_name, "success": True } except Exception as e: return { "chunk_idx": chunk_idx, "error": str(e), "success": False } # 並行處理塊 with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_workers) as executor: chunk_items = [(i, chunk) for i, chunk in enumerate(document_chunks)] chunk_results = list(executor.map(process_chunk, chunk_items)) # 重新組合結果 chunk_results.sort(key=lambda x: x["chunk_idx"]) final_embeddings = [] for result in chunk_results: if result["success"]: final_embeddings.extend(result["embeddings"]) else: print(f"塊 {result['chunk_idx']} 處理失敗: {result['error']}") return final_embeddings 實際應用案例 案例 1:智能文檔搜尋系統 def intelligent_document_search_system(): """智能文檔搜尋系統""" import faiss import numpy as np class DocumentSearchEngine: def __init__(self, embedding_model): self.embedding_model = embedding_model self.documents = [] self.embeddings = [] self.index = None def add_documents(self, documents: List[str]): """添加文檔到搜尋引擎""" print(f"正在嵌入 {len(documents)} 個文檔...") # 批次處理嵌入 batch_processor = batch_embedding_optimizer() processor = batch_processor(self.embedding_model, batch_size=50) new_embeddings = processor.process_with_progress(documents) # 更新文檔和嵌入 self.documents.extend(documents) self.embeddings.extend(new_embeddings) # 重建索引 self._build_index() print(f"已添加 {len(documents)} 個文檔,總計 {len(self.documents)} 個") def _build_index(self): """建立 FAISS 索引""" if not self.embeddings: return # 轉換為 numpy 陣列 embeddings_array = np.array(self.embeddings).astype('float32') # 建立 FAISS 索引 dimension = embeddings_array.shape[1] self.index = faiss.IndexFlatIP(dimension) # 內積索引 # 正規化向量(用於餘弦相似度) faiss.normalize_L2(embeddings_array) # 添加向量到索引 self.index.add(embeddings_array) print(f"FAISS 索引已建立,維度: {dimension}") def search(self, query: str, top_k: int = 5): """搜尋相關文檔""" if not self.index: return [] # 嵌入查詢 query_embedding = self.embedding_model.embed_query(query) query_vector = np.array([query_embedding]).astype('float32') # 正規化查詢向量 faiss.normalize_L2(query_vector) # 搜尋 scores, indices = self.index.search(query_vector, top_k) # 組織結果 results = [] for i, (score, idx) in enumerate(zip(scores[0], indices[0])): if idx < len(self.documents): results.append({ "rank": i + 1, "document": self.documents[idx], "similarity": float(score), "document_id": int(idx) }) return results def get_similar_documents(self, doc_id: int, top_k: int = 5): """找到與指定文檔相似的其他文檔""" if doc_id >= len(self.embeddings) or not self.index: return [] # 獲取指定文檔的嵌入 doc_embedding = np.array([self.embeddings[doc_id]]).astype('float32') faiss.normalize_L2(doc_embedding) # 搜尋(排除自己) scores, indices = self.index.search(doc_embedding, top_k + 1) results = [] for score, idx in zip(scores[0], indices[0]): if idx != doc_id and idx < len(self.documents): results.append({ "document": self.documents[idx], "similarity": float(score), "document_id": int(idx) }) return results[:top_k] return DocumentSearchEngine # 使用示範 def demo_document_search(): """文檔搜尋示範""" # 選擇嵌入模型 embedding_model = setup_multilingual_e5() # 或其他模型 # 創建搜尋引擎 DocumentSearchEngine = intelligent_document_search_system() search_engine = DocumentSearchEngine(embedding_model) # 範例文檔 sample_documents = [ "人工智能是模擬人類智能的技術。它包括機器學習、深度學習等方法。", "機器學習是人工智能的重要分支,讓電腦能從資料中學習規律。", "深度學習使用多層神經網路,在圖像識別和語音處理方面表現優異。", "自然語言處理專注於讓電腦理解和生成人類語言。", "電腦視覺讓機器能夠識別和理解圖像內容。", "強化學習通過獎勵和懲罰機制訓練智能代理。", "資料科學結合統計學和電腦科學來分析大量資料。", "雲端運算提供彈性和可擴展的計算資源。", "區塊鏈是一種分散式記帳技術,確保資料安全和透明。", "物聯網連接各種設備,實現智能化管理。" ] # 添加文檔 search_engine.add_documents(sample_documents) # 搜尋測試 test_queries = [ "什麼是深度學習?", "AI 人工智能應用", "數據分析方法" ] for query in test_queries: print(f"\n🔍 搜尋: {query}") results = search_engine.search(query, top_k=3) for result in results: print(f" {result['rank']}. 相似度: {result['similarity']:.3f}") print(f" {result['document'][:60]}...") return search_engine 案例 2:多語言內容分類 def multilingual_content_classifier(): """多語言內容分類系統""" from sklearn.metrics.pairwise import cosine_similarity import numpy as np class MultilingualClassifier: def __init__(self, embedding_model): self.embedding_model = embedding_model self.categories = {} self.category_embeddings = {} def add_category(self, category_name: str, example_texts: List[str]): """添加分類類別""" # 獲取範例文字的嵌入 embeddings = self.embedding_model.embed_documents(example_texts) # 計算類別的中心向量 center_embedding = np.mean(embeddings, axis=0) self.categories[category_name] = example_texts self.category_embeddings[category_name] = center_embedding print(f"已添加類別 '{category_name}',包含 {len(example_texts)} 個範例") def classify_text(self, text: str, threshold: float = 0.5): """分類單個文字""" if not self.category_embeddings: return {"error": "沒有可用的分類類別"} # 獲取文字嵌入 text_embedding = self.embedding_model.embed_query(text) # 計算與各類別的相似度 similarities = {} for category, category_embedding in self.category_embeddings.items(): similarity = cosine_similarity( [text_embedding], [category_embedding] )[0][0] similarities[category] = similarity # 找到最佳匹配 best_category = max(similarities, key=similarities.get) best_score = similarities[best_category] # 檢查是否超過閾值 if best_score < threshold: return { "category": "未知", "confidence": best_score, "all_scores": similarities } return { "category": best_category, "confidence": best_score, "all_scores": similarities } def batch_classify(self, texts: List[str]): """批次分類多個文字""" results = [] for text in texts: result = self.classify_text(text) result["text"] = text results.append(result) return results def evaluate_classification(self, test_data: List[tuple]): """評估分類效能""" correct_predictions = 0 total_predictions = len(test_data) category_stats = {} for text, true_category in test_data: prediction = self.classify_text(text) predicted_category = prediction["category"] # 統計準確率 if predicted_category == true_category: correct_predictions += 1 # 分類別統計 if true_category not in category_stats: category_stats[true_category] = {"correct": 0, "total": 0} category_stats[true_category]["total"] += 1 if predicted_category == true_category: category_stats[true_category]["correct"] += 1 # 計算整體準確率 overall_accuracy = correct_predictions / total_predictions # 計算各類別準確率 category_accuracies = {} for category, stats in category_stats.items(): category_accuracies[category] = stats["correct"] / stats["total"] return { "overall_accuracy": overall_accuracy, "category_accuracies": category_accuracies, "total_samples": total_predictions, "correct_predictions": correct_predictions } return MultilingualClassifier # 使用示範 def demo_multilingual_classification(): """多語言分類示範""" # 創建分類器 embedding_model = setup_multilingual_e5() MultilingualClassifier = multilingual_content_classifier() classifier = MultilingualClassifier(embedding_model) # 定義分類類別和範例 categories_data = { "科技": [ "人工智能正在改變世界", "Machine learning algorithms are improving", "新的程式語言框架發布", "Quantum computing breakthrough announced", "區塊鏈技術的最新應用" ], "體育": [ "足球比賽結果公佈", "Basketball championship finals", "奧運會準備工作進行中", "Tennis tournament highlights", "游泳世界紀錄被打破" ], "美食": [ "新餐廳開業優惠活動", "Traditional cooking recipes shared", "健康飲食的重要性", "Food festival this weekend", "素食料理製作方法" ] } # 添加類別 for category, examples in categories_data.items(): classifier.add_category(category, examples) # 測試分類 test_texts = [ "深度學習模型的新突破", # 科技 "Basketball player wins MVP award", # 體育 "義大利麵的製作秘訣", # 美食 "這是一個混合的句子,包含 AI 和 pizza" # 混合 ] print("\n🏷️ 分類測試:") for text in test_texts: result = classifier.classify_text(text) print(f"\n文字: {text}") print(f"分類: {result['category']}") print(f"信心度: {result['confidence']:.3f}") return classifier 案例 3:語意相似度分析 def semantic_similarity_analyzer(): """語意相似度分析工具""" import matplotlib.pyplot as plt import seaborn as sns import pandas as pd class SimilarityAnalyzer: def __init__(self, embedding_model): self.embedding_model = embedding_model def analyze_text_groups(self, text_groups: dict): """分析文字群組間的相似度""" all_texts = [] group_labels = [] # 收集所有文字和標籤 for group_name, texts in text_groups.items(): all_texts.extend(texts) group_labels.extend([group_name] * len(texts)) # 獲取所有嵌入 embeddings = self.embedding_model.embed_documents(all_texts) # 計算相似度矩陣 similarity_matrix = cosine_similarity(embeddings) # 創建 DataFrame df = pd.DataFrame( similarity_matrix, index=[f"{label}_{i}" for i, label in enumerate(group_labels)], columns=[f"{label}_{i}" for i, label in enumerate(group_labels)] ) return df, all_texts, group_labels def visualize_similarity_heatmap(self, similarity_df, title="語意相似度熱圖"): """視覺化相似度熱圖""" plt.figure(figsize=(12, 10)) sns.heatmap( similarity_df, annot=True, cmap="YlOrRd", center=0, square=True, fmt='.2f' ) plt.title(title) plt.tight_layout() plt.show() def find_outliers(self, texts: List[str], threshold: float = 0.3): """找出語意上的異常值""" embeddings = self.embedding_model.embed_documents(texts) similarity_matrix = cosine_similarity(embeddings) # 計算每個文字與其他文字的平均相似度 avg_similarities = [] for i in range(len(texts)): # 排除自己 others = [similarity_matrix[i][j] for j in range(len(texts)) if i != j] avg_similarity = np.mean(others) avg_similarities.append(avg_similarity) # 找出異常值 outliers = [] for i, avg_sim in enumerate(avg_similarities): if avg_sim < threshold: outliers.append({ "text": texts[i], "avg_similarity": avg_sim, "index": i }) return sorted(outliers, key=lambda x: x["avg_similarity"]) def cluster_similar_texts(self, texts: List[str], n_clusters: int = 3): """將相似文字聚類""" from sklearn.cluster import KMeans embeddings = self.embedding_model.embed_documents(texts) # K-means 聚類 kmeans = KMeans(n_clusters=n_clusters, random_state=42) cluster_labels = kmeans.fit_predict(embeddings) # 組織結果 clusters = {} for i, label in enumerate(cluster_labels): if label not in clusters: clusters[label] = [] clusters[label].append({ "text": texts[i], "index": i }) return clusters return SimilarityAnalyzer # 使用示範 def demo_similarity_analysis(): """相似度分析示範""" # 創建分析器 embedding_model = setup_multilingual_e5() SimilarityAnalyzer = semantic_similarity_analyzer() analyzer = SimilarityAnalyzer(embedding_model) # 測試文字群組 text_groups = { "AI技術": [ "機器學習幫助解決複雜問題", "深度學習在圖像識別表現優異", "自然語言處理理解人類語言", "人工智能改變各個行業" ], "程式設計": [ "Python 是流行的程式語言", "JavaScript 用於網頁開發", "演算法優化提升程式效能", "開源軟體促進技術發展" ], "不相關": [ "今天天氣很好", "我喜歡吃義大利麵", "電影很精彩" ] } # 分析群組相似度 similarity_df, all_texts, group_labels = analyzer.analyze_text_groups(text_groups) print("📊 群組間相似度分析完成") print(f"分析了 {len(all_texts)} 個文字,分為 {len(text_groups)} 個群組") # 找出異常值 outliers = analyzer.find_outliers(all_texts, threshold=0.4) if outliers: print(f"\n🔍 發現 {len(outliers)} 個語意異常值:") for outlier in outliers: print(f" • {outlier['text']} (相似度: {outlier['avg_similarity']:.3f})") # 自動聚類 clusters = analyzer.cluster_similar_texts(all_texts, n_clusters=3) print(f"\n🔗 自動聚類結果 ({len(clusters)} 個群組):") for cluster_id, cluster_texts in clusters.items(): print(f"\n 群組 {cluster_id}:") for item in cluster_texts: print(f" • {item['text']}") return analyzer, similarity_df 總結與最佳實踐 🎯 嵌入模型選擇指南 def embedding_selection_guide(): """嵌入模型選擇指南""" selection_matrix = { "商業場景": { "高品質需求": "OpenAI text-embedding-3-large", "成本敏感": "OpenAI text-embedding-3-small", "企業部署": "Azure OpenAI Embeddings" }, "開源方案": { "多語言支援": "multilingual-e5-large-instruct", "中文優化": "bge-large-zh-v1.5", "輕量級": "all-MiniLM-L6-v2" }, "本地部署": { "隱私優先": "Ollama + nomic-embed-text", "資源限制": "GPT4All embeddings", "自定義": "本地 Hugging Face 模型" }, "特殊需求": { "多模態": "OpenCLIP", "程式碼": "code-specific models", "領域特化": "fine-tuned models" } } return selection_matrix # 效能對比工具 def comprehensive_model_comparison(): """全面的模型比較工具""" models_to_test = { "OpenAI-Large": OpenAIEmbeddings(model="text-embedding-3-large"), "OpenAI-Small": OpenAIEmbeddings(model="text-embedding-3-small"), "E5-Multilingual": setup_multilingual_e5(), "BGE-Chinese": setup_bge_chinese() } test_scenarios = { "中文文字": ["人工智能改變世界", "機器學習很重要"], "英文文字": ["AI is transforming industries", "Machine learning is crucial"], "混合語言": ["AI人工智能", "Machine learning機器學習"], "技術術語": ["深度學習神經網路", "自然語言處理NLP"] } results = {} for model_name, model in models_to_test.items(): results[model_name] = {} for scenario_name, texts in test_scenarios.items(): try: start_time = time.time() embeddings = model.embed_documents(texts) end_time = time.time() results[model_name][scenario_name] = { "success": True, "time": end_time - start_time, "dimension": len(embeddings[0]), "avg_processing_time": (end_time - start_time) / len(texts) } except Exception as e: results[model_name][scenario_name] = { "success": False, "error": str(e) } return results 💡 最佳實踐建議 模型選擇 考慮成本、效能、隱私三大因素 先用小模型驗證,再升級到大模型 多語言場景優選多語言專用模型 效能最佳化 實作嵌入快取減少重複計算 使用批次處理提升吞吐量 監控記憶體使用避免 OOM 生產部署 本地部署考慮硬體資源 雲端服務注意 API 限制 實作降級和重試機制 品質保證 定期評估嵌入品質 監控相似度分佈 根據業務場景調整模型 嵌入模型是 RAG 系統的核心組件,正確的選擇和最佳化能顯著提升系統效能。記住,最好的模型是最適合你需求的模型,而不一定是最新或最大的模型! 本指南基於 LangChain 官方文檔和實際專案經驗編寫,持續更新以反映最新的技術發展和最佳實踐。 "total_tokens": total_tokens, "estimated_cost": cost, "cost_per_1k_tokens": prices[model] * 1000 } ### 其他商業服務 ```python # Cohere Embeddings from langchain_cohere import CohereEmbeddings cohere_embeddings = CohereEmbeddings( model="embed-multilingual-v3.0", # 支援多語言 cohere_api_key=os.getenv("COHERE_API_KEY") ) # Azure OpenAI from langchain_openai import AzureOpenAIEmbeddings azure_embeddings = AzureOpenAIEmbeddings( azure_deployment="your-embedding-deployment", openai_api_version="2023-05-15", azure_endpoint="https://your-resource.openai.azure.com/", api_key=os.getenv("AZURE_OPENAI_API_KEY") ) 開源嵌入模型 Hugging Face 模型 最豐富的開源模型生態系統: from langchain_huggingface import HuggingFaceEmbeddings import torch # 檢查可用設備 device = "cuda" if torch.cuda.is_available() else "cpu" print(f"使用設備: {device}") # 多語言 E5 模型(推薦) def setup_multilingual_e5(): """設置多語言 E5 模型""" model_name = "intfloat/multilingual-e5-large-instruct" embeddings = HuggingFaceEmbeddings( model_name=model_name, model_kwargs={ "device": device, "trust_remote_code": True }, encode_kwargs={ "normalize_embeddings": True, # 正規化向量 "show_progress_bar": True # 顯示進度條 } ) return embeddings # BGE 中文模型 def setup_bge_chinese(): """設置 BGE 中文模型""" model_name = "BAAI/bge-large-zh-v1.5" embeddings = HuggingFaceEmbeddings( model_name=model_name, model_kwargs={"device": device}, encode_kwargs={"normalize_embeddings": True} ) return embeddings # 輕量級模型(適合 CPU) def setup_lightweight_model(): """設置輕量級模型""" model_name = "sentence-transformers/all-MiniLM-L6-v2" embeddings = HuggingFaceEmbeddings( model_name=model_name, model_kwargs={"device": "cpu"}, encode_kwargs={"normalize_embeddings": True} ) return embeddings # 模型效能測試 def benchmark_models(): """比較不同模型的效能""" import time models = { "E5-Large": setup_multilingual_e5(), "BGE-Chinese": setup_bge_chinese(), "MiniLM": setup_lightweight_model() } test_texts = [ "人工智能是未來科技的重要方向。", "Machine learning is transforming industries.", "自然語言處理讓機器理解人類語言。" ] results = {} for model_name, model in models.items(): start_time = time.time() try: vectors = model.embed_documents(test_texts) end_time = time.time() results[model_name] = { "success": True, "time": end_time - start_time, "dimension": len(vectors[0]), "vectors_count": len(vectors) } except Exception as e: results[model_name] = { "success": False, "error": str(e) } return results 模型下載與管理 def manage_huggingface_models(): """管理 Hugging Face 模型下載和快取""" from huggingface_hub import snapshot_download import os # 設置快取目錄 cache_dir = "./models_cache" os.makedirs(cache_dir, exist_ok=True) # 預下載模型 models_to_download = [ "intfloat/multilingual-e5-large-instruct", "BAAI/bge-large-zh-v1.5", "sentence-transformers/all-MiniLM-L6-v2" ] for model_name in models_to_download: print(f"下載模型: {model_name}") try: snapshot_download( repo_id=model_name, cache_dir=cache_dir, resume_download=True ) print(f"✅ {model_name} 下載完成") except Exception as e: print(f"❌ {model_name} 下載失敗: {e}") return cache_dir 本地部署方案 Ollama 本地部署 簡單易用的本地模型運行工具: from langchain_ollama import OllamaEmbeddings # 基本設置 def setup_ollama_embeddings(): """設置 Ollama 嵌入模型""" # 確保已安裝 Ollama 並拉取模型 # 命令: ollama pull nomic-embed-text embeddings = OllamaEmbeddings( model="nomic-embed-text", base_url="http://localhost:11434" # 預設 Ollama 地址 ) return embeddings # 檢查 Ollama 服務狀態 def check_ollama_status(): """檢查 Ollama 服務狀態""" import requests try: response = requests.get("http://localhost:11434/api/tags") if response.status_code == 200: models = response.json().get("models", []) return { "status": "running", "available_models": [model["name"] for model in models] } else: return {"status": "error", "message": "Service not responding"} except requests.ConnectionError: return {"status": "offline", "message": "Ollama not running"} # 自動設置 Ollama def auto_setup_ollama(): """自動設置 Ollama 環境""" import subprocess import time # 檢查狀態 status = check_ollama_status() if status["status"] == "offline": print("Ollama 未運行,請先啟動 Ollama 服務") return None # 檢查是否有嵌入模型 embedding_models = ["nomic-embed-text", "all-minilm"] available_models = status.get("available_models", []) for model in embedding_models: if model not in available_models: print(f"拉取模型: {model}") try: subprocess.run(["ollama", "pull", model], check=True) print(f"✅ {model} 安裝完成") break except subprocess.CalledProcessError: print(f"❌ {model} 安裝失敗") continue return OllamaEmbeddings(model=model) GPT4All 本地方案 from langchain_community.embeddings import GPT4AllEmbeddings def setup_gpt4all_embeddings(): """設置 GPT4All 嵌入模型""" embeddings = GPT4AllEmbeddings( model_name="all-MiniLM-L6-v2.gguf2.f16.gguf", gpt4all_kwargs={"allow_download": True} ) return embeddings # 效能測試 def test_local_performance(): """測試本地模型效能""" import time import psutil import os # 測試文字 test_docs = [ "人工智能正在改變世界。", "機器學習是AI的重要分支。", "深度學習模擬人腦神經網路。", "自然語言處理理解人類語言。", "電腦視覺讓機器看懂圖像。" ] * 20 # 重複以測試批次處理 models = { "Ollama": setup_ollama_embeddings(), "GPT4All": setup_gpt4all_embeddings() } results = {} for model_name, model in models.items(): print(f"測試 {model_name}...") # 記錄系統資源 process = psutil.Process(os.getpid()) start_memory = process.memory_info().rss / 1024 / 1024 # MB start_time = time.time() try: vectors = model.embed_documents(test_docs) end_time = time.time() end_memory = process.memory_info().rss / 1024 / 1024 # MB results[model_name] = { "success": True, "processing_time": end_time - start_time, "memory_usage": end_memory - start_memory, "throughput": len(test_docs) / (end_time - start_time), "dimension": len(vectors[0]) if vectors else 0 } except Exception as e: results[model_name] = { "success": False, "error": str(e) } return results 多模態嵌入 CLIP 模型應用 處理文字和圖像的統一嵌入空間: # 安裝 OpenCLIP: pip install open_clip_torch from langchain_experimental.open_clip import OpenCLIPEmbeddings import torch def setup_clip_embeddings(): """設置 CLIP 多模態嵌入""" clip_embeddings = OpenCLIPEmbeddings( model_name="ViT-B-32", checkpoint="openai" ) return clip_embeddings def multimodal_search_demo(): """多模態搜尋示範""" clip_model = setup_clip_embeddings() # 準備圖像路徑 image_paths = [ "./images/cat.jpg", "./images/dog.jpg", "./images/car.jpg", "./images/tree.jpg" ] # 準備搜尋查詢 text_queries = [ "可愛的寵物", "交通工具", "自然風景" ] try: # 獲取圖像嵌入 image_embeddings = clip_model.embed_image(image_paths) # 獲取文字嵌入 text_embeddings = clip_model.embed_documents(text_queries) # 計算相似度矩陣 similarities = [] for text_emb in text_embeddings: row = [] for img_emb in image_embeddings: similarity = calculate_similarity(text_emb, img_emb) row.append(similarity) similarities.append(row) # 找到最匹配的圖像 results = {} for i, query in enumerate(text_queries): best_match_idx = similarities[i].index(max(similarities[i])) results[query] = { "best_image": image_paths[best_match_idx], "similarity": similarities[i][best_match_idx] } return results except Exception as e: print(f"多模態搜尋失敗: {e}") return None # 自定義多模態應用 def custom_multimodal_app(): """自定義多模態應用""" def search_images_by_text(text_query, image_database): """根據文字搜尋圖像""" clip_model = setup_clip_embeddings() # 獲取查詢嵌入 query_embedding = clip_model.embed_query(text_query) # 獲取所有圖像嵌入 image_embeddings = clip_model.embed_image(image_database) # 計算相似度並排序 similarities = [] for i, img_emb in enumerate(image_embeddings): similarity = calculate_similarity(query_embedding, img_emb) similarities.append((i, similarity)) # 按相似度排序 similarities.sort(key=lambda x: x[1], reverse=True) return similarities[:5] # 返回前5個最相似的 def search_text_by_image(image_path, text_database): """根據圖像搜尋文字""" clip_model = setup_clip_embeddings() # 獲取圖像嵌入 image_embedding = clip_model.embed_image([image_path])[0] # 獲取文字嵌入 text_embeddings = clip_model.embed_documents(text_database) # 計算相似度 similarities = [] for i, text_emb in enumerate(text_embeddings): similarity = calculate_similarity(image_embedding, text_emb) similarities.append((i, similarity)) similarities.sort(key=lambda x: x[1], reverse=True) return similarities[:5] return search_images_by_text, search_text_by_image 效能最佳化 嵌入快取系統 ```pythonfrom langchain.embeddings import CacheBackedEmbeddingsfrom langchain.storage import LocalFileStore, InMemoryStoreimport hashlibimport json class AdvancedEmbeddingCache: """進階嵌入快取系統""" def __init__(self, cache_type="file", cache_dir="./embedding_cache"): self.cache_type = cache_type self.cache_dir = cache_dir if cache_type == "file": self.store = LocalFileStore(cache_dir) elif cache_type == "memory": self.store = InMemoryStore() else: raise ValueError("cache_type must be 'file' or 'memory'") def create_cached_embeddings(self, base_embeddings, namespace=None): """創建帶快取的嵌入模型""" namespace = namespace or base_embeddings.model return CacheBackedEmbeddings.from_bytes_store( base_embeddings, self.store, namespace=namespace ) def get_cache_stats(self): """獲取快取統計""" if self.cache_type == "file": import os cache_files = [] for root, dirs, files in os.walk(self.cache_dir): for file in files: file_path = os.path.join(root, file) cache_files.append({ "file": file, "size": os.path.getsize(file_path), "modified": os.path.getmtime(file_path) }) total_size = sum(f["size"] for f in cache_files) return { "cache_type": "file", "total_files": len(cache_files), "total_size_mb": total_size / (1024 * 1024), "cache_dir": self.cache_dir } else: # 記憶體快取統計 return { "cache_type": "memory", "entries": len(self.store.store) if hasattr(self.store, 'store') else 0 } def clear_cache(self, older_than_days=None): """清理快取""" if self.cache_type == "file" and older_than_days: import os import time current_time = time.time() cutoff_time = current_time - (older_than_days * 24 * 3600) removed_count = 0 for root, dirs, files in os.walk(self.cache_dir): for file in files: file_path = os.path.join(root, file) if os.path.getmtime(file_path) < cutoff_time: os.remove(file_path) removed_count += 1 return {"removed_files": removed_count} elif self.cache_type == "memory": if hasattr(self.store, 'store'): self.store.store.clear() return {"status": "memory_cache_cleared"} 快取使用示範 def caching_performance_demo(): """快取效能示範""" import time from langchain_openai import OpenAIEmbeddings # 創建快取系統 cache_system = AdvancedEmbeddingCache(cache_type="file") # 原始嵌入模型 base_embeddings = OpenAIEmbeddings(model="text-embedding-3-small") # 帶快取的嵌入模型 cached_embeddings = cache_system.create_cached_embeddings(base_embeddings) # 測試文檔 test_docs = [ "人工智能是現代科技的重要分支。", "機器學習幫助電腦從資料中學習。", "深度學習使用多層神經網路。", "自然語言處理讓機器理解語言。" ] * 10 # 重複文檔來測試快取效果 # 第一次運行(建立快取) print("第一次運行(建立快取)...") start_time = time.time() embeddings_1 = cached_embeddings.embed_documents(test_docs) time_1 = time.time() - start_time print(f"時間: {time_1:.2f} 秒") # 第二次運行(使用快取) print("第二次運行(使用快取)...") start_time = time.time() embeddings_2 = cached_embeddings.embed_documents(test_docs) time_2 = time.time() - start_time print(f"時間: {time_2:.2f} 秒") # 計算速度提升 speedup = time_1 / time_2 if time_2 > 0 else float('inf') print(f"速度提升: {speedup:.1f}x") # 快取統計 stats = cache_system.get_cache_stats() print(f"快取統計: {stats}") return {
-
RAG 重要組件 - 向量資料庫 (vector database)
RAG 中的向量儲存 (Vector Stores) 在 RAG (Retrieval-Augmented Generation) 流程中,向量儲存 (Vector Store) 扮演著至關重要的角色。在文件經過「載入 (Load)」、「分割 (Split)」和「嵌入 (Embed)」之後,所產生的向量化區塊 (Vectorized Chunks) 需要一個專門的資料庫來進行高效的儲存、管理和檢索。這就是向量儲存的核心功能。 本文件將根據 09-VectorStore 資料夾中的筆記本內容,介紹向量儲存的基本概念以及幾種主流的向量資料庫。 1. 為什麼需要向量儲存? 傳統的關聯式資料庫(如 SQL)是為結構化資料設計的,不適合儲存和查詢高維度的向量資料。向量儲存則專為此而生,它具備以下關鍵優勢: 高效的相似度搜尋:能夠在數百萬甚至數十億個向量中,快速找到與給定查詢向量最相似的向量。透過索引演算法(如 HNSW、IVF)可以將搜尋時間從線性降至對數級別。 可擴展性:能夠應對不斷增長的資料量,而不會顯著降低查詢效能。支援水平擴展和叢集部署,可處理 TB 級別的向量資料。 語意檢索:支援基於語意相似度的檢索,而不僅僅是關鍵字匹配,這對於 RAG 應用至關重要。能夠理解同義詞、上下文和概念關聯。 **中繼資料過濾 (Metadata Filtering)**:允許在進行向量搜尋的同時,根據文件的中繼資料(如標題、日期、來源、標籤)進行過濾,實現更精準的檢索。 多模態支援:能夠同時處理文字、圖像、音訊等不同類型的向量資料,實現跨模態搜尋。 向量儲存 vs 傳統資料庫對比 特性 傳統資料庫 向量資料庫 資料類型 結構化資料 高維向量 + 中繼資料 查詢方式 精確匹配、關鍵字 相似度搜尋、語意理解 索引方法 B-tree、Hash HNSW、IVF、LSH 查詢延遲 毫秒級 毫秒到秒級(取決於資料量) 適用場景 交易處理、報表 AI 搜尋、推薦系統 2. 核心操作 (CRUD & Search) LangChain 為各種向量儲存提供了一個統一的介面,主要包含以下操作: 2.1 資料寫入操作 add_documents(documents): 批次新增文件到向量儲存中。系統會自動對文件進行向量化並儲存相關中繼資料。 # 批次新增文件 db.add_documents([doc1, doc2, doc3]) upsert(vectors, ids, metadata): 更新插入操作,如果文件已存在則更新,否則新增。適用於需要動態更新知識庫的場景。 # 更新或插入特定 ID 的文件 db.upsert( vectors=[vector1, vector2], ids=["doc_1", "doc_2"], metadata=[{"source": "updated"}, {"source": "new"}] ) from_documents(documents, embedding, **kwargs): 類別方法,從文件列表直接建立向量儲存實例。 # 一次性建立向量儲存 db = VectorStore.from_documents( documents=docs, embedding=embeddings, persist_directory="./db" # 持久化路徑 ) 2.2 資料讀取與搜尋 similarity_search(query, k=4, filter=None): 核心搜尋功能,回傳最相似的 K 個文件。 # 基本相似度搜尋 results = db.similarity_search( query="人工智慧的應用", k=5, filter={"category": "technology"} # 中繼資料過濾 ) similarity_search_with_score(query, k=4): 除了回傳文件外,還包含相似度分數,有助於評估結果品質。 # 帶分數的搜尋 results_with_scores = db.similarity_search_with_score( query="機器學習演算法", k=3 ) for doc, score in results_with_scores: print(f"Score: {score:.3f} - {doc.page_content[:100]}") max_marginal_relevance_search(query, k=4, fetch_k=20): 最大邊際相關性搜尋,平衡相關性和多樣性。 # MMR 搜尋避免重複內容 diverse_results = db.max_marginal_relevance_search( query="深度學習", k=5, fetch_k=50, # 先取得更多候選 lambda_mult=0.7 # 平衡相關性(1.0)和多樣性(0.0) ) 2.3 資料管理操作 delete(ids): 根據文件 ID 刪除特定文件。對於需要動態維護知識庫的應用非常重要。 # 刪除過時文件 db.delete(ids=["outdated_doc_1", "outdated_doc_2"]) get(ids): 根據 ID 取得特定文件,用於驗證或偵錯。 # 取得特定文件 specific_docs = db.get(ids=["doc_1", "doc_2"]) 3. 常用的向量儲存方案 LangChain 整合了多種向量儲存方案,開發者可以根據應用需求、部署環境(本地 vs. 雲端)和成本來選擇。 本地端方案 (Local / Self-Hosted) 適合快速原型開發、小型應用或對資料隱私有嚴格要求的場景。 3.1 FAISS (Facebook AI Similarity Search) 由 Facebook 開發的高效能程式庫,非常適合在記憶體中進行快速的相似度搜尋。它輕量且易於整合,但本身不提供持久化儲存(需要額外實現)。 優勢: 極快的搜尋速度(純記憶體運算) 支援多種索引演算法(Flat、IVF、HNSW等) 豐富的量化選項以節省記憶體 支援 GPU 加速 劣勢: 需要手動實現持久化 不支援線上更新(需要重建索引) 單機限制,不支援分散式 適用場景: 快速原型開發 靜態資料集 對延遲要求極高的應用 資料量中等(百萬到千萬級別) from langchain_community.vectorstores import FAISS from langchain_openai import OpenAIEmbeddings import pickle # 建立或載入 FAISS 索引 embedding = OpenAIEmbeddings() # 方法一:從文件建立 db = FAISS.from_documents(docs, embedding) # 方法二:從已有向量建立 # db = FAISS.from_texts(texts, embedding, metadatas=metadatas) # 搜尋 results = db.similarity_search(query="小王子來自哪個星球?", k=3) # 持久化儲存 db.save_local("faiss_index") # 載入已儲存的索引 db_loaded = FAISS.load_local("faiss_index", embedding, allow_dangerous_deserialization=True) # 進階配置:使用不同的索引類型 # IndexFlatL2: 精確搜尋,適合小資料集 # IndexIVFFlat: 近似搜尋,適合大資料集 # IndexHNSWFlat: 圖形索引,平衡速度和精度 3.2 Chroma 一個開源的、專為 AI 應用設計的嵌入式資料庫。它簡單易用,支援本地持久化儲存和中繼資料過濾。 優勢: 原生支援持久化 優秀的中繼資料過濾功能 內建的資料版本控制 支援多種距離度量 輕量級,易於部署 劣勢: 相對較新,生態系統較小 大規模資料效能有限 單機部署限制 適用場景: 中小型應用(百萬級文件) 需要豐富中繼資料過濾 快速原型和開發 本地部署需求 from langchain_chroma import Chroma from langchain_openai import OpenAIEmbeddings import chromadb # 初始化持久化客戶端 client = chromadb.PersistentClient(path="./chroma_db") # 建立向量儲存 db = Chroma( client=client, collection_name="my_collection", embedding_function=OpenAIEmbeddings() ) # 新增文件 db.add_documents(docs) # 進階搜尋:結合中繼資料過濾 results = db.similarity_search( query="機器學習演算法", k=5, filter={"source": "academic_paper", "year": {"$gte": 2020}} ) # 使用 where 和 where_document 進行複雜過濾 results = db.similarity_search( query="深度學習", k=3, filter={ "$and": [ {"category": {"$eq": "AI"}}, {"difficulty": {"$in": ["intermediate", "advanced"]}} ] } ) # 資料統計 collection = client.get_collection("my_collection") print(f"Total documents: {collection.count()}") 3.3 PGVector PostgreSQL 的一個擴充套件,讓傳統的關聯式資料庫具備了儲存和查詢向量的能力。適合希望在現有 PostgreSQL 基礎設施上擴充向量搜尋功能的團隊。 優勢: 結合關聯式資料庫的 ACID 特性 強大的 SQL 查詢能力 成熟的生態系統和工具 支援複雜的混合查詢 優秀的資料一致性保證 劣勢: 向量搜尋效能不如專用向量資料庫 需要 PostgreSQL 運維知識 記憶體使用較高 適用場景: 已有 PostgreSQL 基礎設施 需要複雜的關聯查詢 強一致性要求 企業級應用 from langchain_postgres import PGVector from langchain_openai import OpenAIEmbeddings import os # 資料庫連線設定 CONNECTION_STRING = "postgresql+psycopg://user:password@localhost:5432/vectordb" # 建立向量儲存 db = PGVector( embeddings=OpenAIEmbeddings(), collection_name="documents", connection=CONNECTION_STRING, use_jsonb=True # 使用 JSONB 儲存中繼資料 ) # 從文件建立 db = PGVector.from_documents( documents=docs, embedding=OpenAIEmbeddings(), collection_name="my_docs", connection=CONNECTION_STRING ) # 複雜的 SQL 查詢結合向量搜尋 # 可以直接使用 SQL 進行複雜查詢 query_sql = """ SELECT document, cmetadata, embedding <-> %s as distance FROM langchain_pg_embedding WHERE cmetadata ->> 'category' = 'research' AND cmetadata ->> 'year' > '2020' ORDER BY embedding <-> %s LIMIT 5; """ # 使用 hybrid search 結合全文檢索 results = db.similarity_search( query="人工智慧倫理", k=5, filter={"category": "ethics", "language": "zh-TW"} ) 3.4 Elasticsearch 一個強大的分散式搜尋和分析引擎,除了傳統的全文檢索外,也支援向量相似度搜尋,適合需要混合搜尋(關鍵字 + 語意)的複雜場景。 優勢: 優秀的全文檢索能力 支援混合搜尋(BM25 + 向量) 強大的聚合和分析功能 成熟的叢集和分散式能力 豐富的生態系統 劣勢: 配置複雜,學習曲線陡峭 資源消耗較高 向量搜尋效能不如專用方案 適用場景: 需要混合搜尋能力 已有 Elasticsearch 基礎設施 大規模資料處理 複雜的搜尋和分析需求 from langchain_elasticsearch import ElasticsearchStore from langchain_openai import OpenAIEmbeddings # Elasticsearch 配置 es_config = { "hosts": ["http://localhost:9200"], "http_auth": ("username", "password"), # 如果需要認證 "verify_certs": False } # 建立向量儲存 db = ElasticsearchStore( embedding=OpenAIEmbeddings(), index_name="documents", es_connection=es_config ) # 混合搜尋:結合關鍵字和語意搜尋 hybrid_results = db.similarity_search( query="機器學習在醫療領域的應用", k=5, search_kwargs={ "query": { "bool": { "should": [ # 語意搜尋 { "knn": { "embedding": { "vector": query_vector, "k": 10 } } }, # 關鍵字搜尋 { "match": { "content": "機器學習 醫療" } } ] } } } ) # 使用聚合進行資料分析 aggregation_query = { "aggs": { "categories": { "terms": {"field": "metadata.category.keyword"} }, "avg_score": { "avg": {"script": "_score"} } } } 雲端託管方案 (Cloud-Hosted / Managed) 適合需要高可用性、可擴展性和無需自行維護基礎設施的生產環境應用。 3.5 Pinecone 一個完全託管的向量資料庫服務,專為高效能、低延遲的向量搜尋而設計,提供簡單的 API 和可擴展的基礎設施。 優勢: 完全託管,無需維護 極佳的搜尋效能和擴展性 簡單的 API 設計 內建的資料備份和災難恢復 支援實時更新 劣勢: 成本較高 供應商鎖定風險 對中繼資料查詢支援有限 適用場景: 生產環境應用 大規模向量搜尋(億級別) 需要高可用性 不想維護基礎設施 import os from langchain_pinecone import PineconeVectorStore from langchain_openai import OpenAIEmbeddings from pinecone import Pinecone # 初始化 Pinecone pc = Pinecone(api_key=os.getenv("PINECONE_API_KEY")) index_name = "my-langchain-index" # 建立索引(如果不存在) if index_name not in pc.list_indexes().names(): pc.create_index( name=index_name, dimension=1536, # OpenAI embeddings 維度 metric="cosine", spec=ServerlessSpec( cloud="aws", region="us-east-1" ) ) # 建立向量儲存 vectorstore = PineconeVectorStore( index=pc.Index(index_name), embedding=OpenAIEmbeddings() ) # 批次新增文件 vectorstore.add_documents(docs) # 進階搜尋配置 results = vectorstore.similarity_search( query="小王子和狐狸說了什麼?", k=3, filter={"source": {"$in": ["book1", "book2"]}} # 中繼資料過濾 ) # 使用命名空間進行資料隔離 vectorstore_ns = PineconeVectorStore( index=pc.Index(index_name), embedding=OpenAIEmbeddings(), namespace="project_a" # 資料隔離 ) # 統計資訊 index_stats = pc.Index(index_name).describe_index_stats() print(f"Total vectors: {index_stats['total_vector_count']}") 3.6 Qdrant 提供開源和雲端兩種版本,以其豐富的過濾功能和高效的查詢效能而聞名。 優勢: 豐富的過濾和搜尋選項 優秀的效能表現 支援多租戶架構 強大的 API 和 SDK 開源版本可自部署 劣勢: 相對較新的產品 文件和社群相對較小 某些高級功能需要付費版本 適用場景: 需要複雜過濾邏輯 多租戶應用 高效能要求 混合雲部署 from langchain_qdrant import QdrantVectorStore from langchain_openai import OpenAIEmbeddings from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams # 雲端版本 client = QdrantClient( url="https://your-cluster-url.qdrant.io", api_key="your-api-key" ) # 本地版本 # client = QdrantClient(host="localhost", port=6333) collection_name = "my_documents" # 建立集合(如果不存在) if not client.collection_exists(collection_name): client.create_collection( collection_name=collection_name, vectors_config=VectorParams( size=1536, # OpenAI embeddings 維度 distance=Distance.COSINE ) ) # 建立向量儲存 qdrant = QdrantVectorStore( client=client, collection_name=collection_name, embedding=OpenAIEmbeddings() ) # 複雜的過濾查詢 results = qdrant.similarity_search( query="人工智慧的未來發展", k=5, filter={ "must": [ {"key": "category", "match": {"value": "technology"}}, {"key": "year", "range": {"gte": 2020}} ], "should": [ {"key": "language", "match": {"value": "zh"}}, {"key": "language", "match": {"value": "en"}} ] } ) # 使用 payload 進行進階操作 qdrant.add_documents( documents=docs, ids=["doc1", "doc2"], payloads=[ {"category": "AI", "priority": "high", "tags": ["ML", "DL"]}, {"category": "Tech", "priority": "medium", "tags": ["Software"]} ] ) 3.7 Weaviate 同樣提供開源和雲端版本,支援圖形化資料模型和 GraphQL API,適合需要複雜資料關聯的應用。 優勢: 圖形資料模型支援 GraphQL API 內建的機器學習模組 多模態資料支援 強大的語意搜尋能力 劣勢: 學習曲線較陡峭 配置相對複雜 資源消耗較高 適用場景: 複雜的資料關聯需求 多模態搜尋 知識圖譜應用 GraphQL 生態系統 from langchain_weaviate import WeaviateVectorStore from langchain_openai import OpenAIEmbeddings import weaviate # 連線到 Weaviate client = weaviate.Client( url="https://your-weaviate-instance.weaviate.network", auth_client_secret=weaviate.AuthApiKey(api_key="your-api-key") ) # 本地 Weaviate # client = weaviate.Client("http://localhost:8080") # 建立向量儲存 db = WeaviateVectorStore( client=client, index_name="Document", text_key="content", embedding=OpenAIEmbeddings() ) # GraphQL 查詢 graphql_query = """ { Get { Document( nearText: { concepts: ["人工智慧"] } limit: 5 where: { path: ["category"] operator: Equal valueString: "technology" } ) { content category _additional { distance } } } } """ # 執行 GraphQL 查詢 result = client.query.raw(graphql_query) # 使用 Weaviate 的內建模組 # 支援自動分類、問答等 classification_result = client.query.get("Document", ["content"]) \ .with_near_text({"concepts": ["機器學習"]}) \ .with_additional(["classification"]) \ .with_limit(5) \ .do() 3.8 MongoDB Atlas Vector Search MongoDB 的雲端服務,將向量搜尋功能與其強大的 NoSQL 文件資料庫無縫整合,方便在現有資料上擴充 AI 功能。 優勢: 與 MongoDB 生態系統完美整合 強大的文件資料庫功能 支援複雜的聚合管道 成熟的分片和副本集 豐富的中繼資料查詢能力 劣勢: 向量搜尋功能相對較新 需要 Atlas 雲端服務 成本可能較高 適用場景: 已使用 MongoDB 的應用 需要複雜文件查詢 混合資料類型應用 需要強大的聚合能力 from langchain_mongodb import MongoDBAtlasVectorSearch from langchain_openai import OpenAIEmbeddings from pymongo import MongoClient # 連線到 MongoDB Atlas client = MongoClient("mongodb+srv://username:password@cluster.mongodb.net/") db = client["vector_database"] collection = db["documents"] # 建立向量搜尋索引(在 Atlas UI 中配置) # { # "fields": [ # { # "type": "vector", # "path": "embedding", # "numDimensions": 1536, # "similarity": "cosine" # }, # { # "type": "filter", # "path": "metadata.category" # } # ] # } # 建立向量儲存 vectorstore = MongoDBAtlasVectorSearch( collection=collection, embedding=OpenAIEmbeddings(), index_name="vector_index" ) # 複雜的聚合查詢 pipeline = [ { "$vectorSearch": { "index": "vector_index", "path": "embedding", "queryVector": query_vector, "numCandidates": 100, "limit": 10, "filter": {"metadata.category": "technology"} } }, { "$group": { "_id": "$metadata.author", "docs": {"$push": "$$ROOT"}, "avg_score": {"$avg": "$score"} } }, {"$sort": {"avg_score": -1}} ] results = list(collection.aggregate(pipeline)) 3.9 Neo4j 一個圖資料庫,除了儲存節點和關係外,也支援向量索引。這使得它能夠進行結合了圖遍歷和向量相似度搜尋的複雜查詢。 優勢: 強大的圖查詢能力 結合向量搜尋和圖遍歷 Cypher 查詢語言 優秀的關係資料處理 支援知識圖譜應用 劣勢: 學習曲線陡峭 不適合純向量搜尋場景 資源消耗較高 適用場景: 知識圖譜應用 複雜關係查詢 推薦系統 欺詐檢測 from langchain_community.vectorstores import Neo4jVector from langchain_openai import OpenAIEmbeddings from neo4j import GraphDatabase # 連線到 Neo4j uri = "bolt://localhost:7687" username = "neo4j" password = "password" # 建立向量儲存 vectorstore = Neo4jVector.from_documents( documents=docs, embedding=OpenAIEmbeddings(), url=uri, username=username, password=password, index_name="document_embeddings" ) # 複雜的 Cypher 查詢結合向量搜尋 cypher_query = """ CALL db.index.vector.queryNodes('document_embeddings', 5, $query_vector) YIELD node, score MATCH (node)-[:AUTHORED_BY]->(author:Author) MATCH (node)-[:BELONGS_TO]->(category:Category) WHERE category.name IN ['AI', 'Technology'] RETURN node.content, author.name, score ORDER BY score DESC """ # 使用圖遍歷增強檢索 graph_enhanced_query = """ // 找到與查詢最相似的文件 CALL db.index.vector.queryNodes('document_embeddings', 10, $query_vector) YIELD node as doc, score // 擴展到相關文件 MATCH (doc)-[:RELATED_TO]-(related_doc:Document) // 找到共同的主題 MATCH (doc)-[:HAS_TOPIC]->(topic:Topic)<-[:HAS_TOPIC]-(related_doc) RETURN doc, related_doc, topic, score ORDER BY score DESC LIMIT 5 """ # 執行圖增強查詢 with GraphDatabase.driver(uri, auth=(username, password)) as driver: with driver.session() as session: result = session.run( graph_enhanced_query, query_vector=query_embedding ) enhanced_results = [record for record in result] 4. 向量儲存作為檢索器 (Retriever) 在 LangChain 中,任何向量儲存都可以透過 .as_retriever() 方法轉換為一個**檢索器 (Retriever)**。檢索器是一個更通用的介面,它封裝了從資料來源(如向量儲存)取回文件的邏輯。 4.1 基本檢索器配置 # 基本檢索器 retriever = db.as_retriever( search_type="similarity", # 搜尋類型 search_kwargs={'k': 5} # 搜尋參數 ) # 使用檢索器 relevant_docs = retriever.invoke("關於馴養的意義") 4.2 進階檢索策略 Maximal Marginal Relevance (MMR) 在回傳結果時,不僅考慮與查詢的相似度,還考慮結果之間的多樣性,避免回傳過於相似的內容。 # MMR 檢索器 mmr_retriever = db.as_retriever( search_type="mmr", search_kwargs={ 'k': 5, # 最終回傳數量 'fetch_k': 20, # 候選文件數量 'lambda_mult': 0.7 # 多樣性參數 (0=最大多樣性, 1=最大相關性) } ) # MMR 演算法原理: # 1. 先取得 fetch_k 個最相似的候選文件 # 2. 從候選文件中選擇既相關又多樣的 k 個文件 # 3. 每次選擇時平衡相關性和與已選文件的差異性 Similarity Score Threshold 根據相似度分數過濾結果,只回傳超過特定閾值的文件。 # 基於分數閾值的檢索器 threshold_retriever = db.as_retriever( search_type="similarity_score_threshold", search_kwargs={ 'score_threshold': 0.8, # 分數閾值 'k': 10 # 最大回傳數量 } ) # 只有相似度分數 > 0.8 的文件才會被回傳 relevant_docs = threshold_retriever.invoke("機器學習演算法") Self-Querying Retriever 讓 LLM 根據使用者的自然語言問題,自動生成結構化的中繼資料過濾條件。 from langchain.retrievers.self_query.base import SelfQueryRetriever from langchain.chains.query_constructor.base import AttributeInfo from langchain_openai import ChatOpenAI # 定義中繼資料屬性 metadata_field_info = [ AttributeInfo( name="category", description="文件的類別,如 'technology', 'science', 'business'", type="string" ), AttributeInfo( name="year", description="文件發布的年份", type="integer" ), AttributeInfo( name="author", description="文件作者", type="string" ), AttributeInfo( name="difficulty", description="內容難度等級,如 'beginner', 'intermediate', 'advanced'", type="string" ) ] # 文件內容描述 document_content_description = "技術文章和研究論文的集合" # 建立自查詢檢索器 llm = ChatOpenAI(model="gpt-4", temperature=0) self_query_retriever = SelfQueryRetriever.from_llm( llm=llm, vectorstore=db, document_contents=document_content_description, metadata_field_info=metadata_field_info, verbose=True ) # 使用自然語言查詢 # LLM 會自動解析出過濾條件 results = self_query_retriever.invoke( "找出 2023 年後發布的高級機器學習文章" ) # 自動轉換為: query="機器學習", filter={"year": {"$gte": 2023}, "difficulty": "advanced"} Multi-Query Retriever 為單一使用者問題產生多個不同角度的查詢,提高檢索覆蓋率。 from langchain.retrievers.multi_query import MultiQueryRetriever # 建立多查詢檢索器 multiquery_retriever = MultiQueryRetriever.from_llm( retriever=db.as_retriever(), llm=ChatOpenAI(model="gpt-4", temperature=0) ) # 對於使用者問題 "AI 的倫理問題",可能會產生: # 1. "人工智慧倫理考量" # 2. "機器學習公平性問題" # 3. "AI 決策透明度" # 4. "演算法偏見處理" results = multiquery_retriever.invoke("AI 的倫理問題") Contextual Compression Retriever 對檢索到的文件進行壓縮和過濾,只保留與查詢最相關的部分。 from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor # 建立文件壓縮器 compressor = LLMChainExtractor.from_llm(ChatOpenAI(model="gpt-4")) # 建立壓縮檢索器 compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=db.as_retriever() ) # 檢索並壓縮結果 compressed_docs = compression_retriever.invoke("深度學習在醫療診斷中的應用") # 每個文件只保留與查詢相關的段落 4.3 組合檢索策略 Ensemble Retriever 結合多個不同的檢索器,利用各自的優勢。 from langchain.retrievers import EnsembleRetriever from langchain_community.retrievers import BM25Retriever # 建立 BM25 檢索器(關鍵字搜尋) bm25_retriever = BM25Retriever.from_documents(docs) bm25_retriever.k = 3 # 建立向量檢索器(語意搜尋) vector_retriever = db.as_retriever(search_kwargs={'k': 3}) # 組合檢索器 ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, vector_retriever], weights=[0.4, 0.6] # BM25: 40%, Vector: 60% ) # 結合關鍵字和語意搜尋的優勢 results = ensemble_retriever.invoke("機器學習模型評估指標") Parent Document Retriever 先檢索小區塊,然後回傳包含該區塊的較大文件段落。 from langchain.retrievers import ParentDocumentRetriever from langchain.storage import InMemoryStore from langchain.text_splitter import RecursiveCharacterTextSplitter # 建立兩個分割器 parent_splitter = RecursiveCharacterTextSplitter(chunk_size=2000) child_splitter = RecursiveCharacterTextSplitter(chunk_size=400) # 建立儲存 store = InMemoryStore() # 建立父文件檢索器 parent_retriever = ParentDocumentRetriever( vectorstore=db, docstore=store, child_splitter=child_splitter, parent_splitter=parent_splitter ) # 新增文件 parent_retriever.add_documents(docs) # 檢索時返回包含相關小區塊的大段落 results = parent_retriever.invoke("神經網路訓練技巧") 5. 向量儲存選擇指南 5.1 決策矩陣 考量因素 本地方案 雲端方案 推薦選擇 資料隱私 ✅ 完全控制 ⚠️ 依賴供應商政策 Chroma, PGVector 維運成本 💰 需要人力維護 💰💰 服務費用較高 FAISS (開發), Pinecone (生產) 擴展性 ⚠️ 硬體限制 ✅ 彈性擴展 Pinecone, Qdrant 整合難度 📚 需要學習 🚀 開箱即用 Chroma (簡單), Weaviate (複雜) 資料量 < 1M 文件 > 10M 文件 小: FAISS, 大: Pinecone 5.2 使用場景建議 快速原型開發 # 推薦:FAISS 或 Chroma # 理由:設置簡單,無需外部依賴 from langchain_community.vectorstores import FAISS from langchain_openai import OpenAIEmbeddings # 幾行程式碼即可開始 db = FAISS.from_documents(docs, OpenAIEmbeddings()) results = db.similarity_search("查詢內容") 生產環境應用 # 推薦:Pinecone 或 Qdrant # 理由:高可用性,專業維護 from langchain_pinecone import PineconeVectorStore import pinecone # 企業級向量搜尋 vectorstore = PineconeVectorStore( index=pinecone.Index("production-index"), embedding=OpenAIEmbeddings() ) 混合搜尋需求 # 推薦:Elasticsearch 或 MongoDB Atlas # 理由:同時支援關鍵字和語意搜尋 from langchain_elasticsearch import ElasticsearchStore # 結合 BM25 和向量搜尋 db = ElasticsearchStore( embedding=OpenAIEmbeddings(), index_name="hybrid_search" ) 複雜關係查詢 # 推薦:Neo4j 或 Weaviate # 理由:支援圖查詢和向量搜尋 from langchain_community.vectorstores import Neo4jVector # 知識圖譜 + 向量搜尋 vectorstore = Neo4jVector.from_documents( docs, OpenAIEmbeddings(), url="bolt://localhost:7687" ) 5.3 效能優化建議 索引優化 # FAISS 索引優化 import faiss # 對於大型資料集,使用 IVF 索引 nlist = 100 # 聚類數量 quantizer = faiss.IndexFlatL2(dimension) index = faiss.IndexIVFFlat(quantizer, dimension, nlist) # 訓練索引 index.train(training_vectors) index.add(vectors) # 搜尋參數調整 index.nprobe = 10 # 搜尋的聚類數量 快取策略 from functools import lru_cache import hashlib class CachedVectorStore: def __init__(self, vectorstore): self.vectorstore = vectorstore @lru_cache(maxsize=1000) def _cached_search(self, query_hash, k): return self.vectorstore.similarity_search(query_hash, k=k) def similarity_search(self, query, k=4): query_hash = hashlib.md5(query.encode()).hexdigest() return self._cached_search(query_hash, k) 批次處理 # 批次新增文件以提高效率 def batch_add_documents(vectorstore, documents, batch_size=100): for i in range(0, len(documents), batch_size): batch = documents[i:i + batch_size] vectorstore.add_documents(batch) print(f"已處理 {min(i + batch_size, len(documents))}/{len(documents)} 文件") 6. 最佳實務和常見陷阱 6.1 向量維度一致性 # 確保所有向量維度一致 embedding_model = OpenAIEmbeddings() dimension = len(embedding_model.embed_query("test")) print(f"向量維度: {dimension}") # 驗證向量維度 def validate_vectors(vectors): if not vectors: return True expected_dim = len(vectors[0]) return all(len(v) == expected_dim for v in vectors) 6.2 中繼資料設計 # 良好的中繼資料結構 metadata_template = { "source": "document_source.pdf", "page": 1, "section": "introduction", "category": "technology", "tags": ["AI", "machine learning"], "created_at": "2024-01-01", "updated_at": "2024-01-15", "language": "zh-TW", "difficulty": "intermediate", "author": "張三", "version": "1.0" } # 確保中繼資料可搜尋 def normalize_metadata(metadata): """標準化中繼資料格式""" normalized = {} for key, value in metadata.items(): if isinstance(value, str): normalized[key] = value.lower().strip() elif isinstance(value, list): normalized[key] = [str(v).lower().strip() for v in value] else: normalized[key] = value return normalized 6.3 向量品質監控 import numpy as np from sklearn.metrics.pairwise import cosine_similarity def monitor_vector_quality(vectorstore, test_queries): """監控向量搜尋品質""" results = {} for query in test_queries: # 檢索結果 docs = vectorstore.similarity_search_with_score(query, k=5) # 計算分數分佈 scores = [score for _, score in docs] results[query] = { "avg_score": np.mean(scores), "min_score": np.min(scores), "max_score": np.max(scores), "score_std": np.std(scores) } return results # 定期執行品質監控 test_queries = ["人工智慧", "機器學習", "深度學習"] quality_report = monitor_vector_quality(db, test_queries) 6.4 資料版本管理 class VersionedVectorStore: def __init__(self, base_vectorstore): self.base_store = base_vectorstore self.versions = {} self.current_version = "v1.0" def create_version(self, version_name, documents): """建立新版本""" # 為文件添加版本標籤 versioned_docs = [] for doc in documents: doc.metadata["version"] = version_name versioned_docs.append(doc) self.base_store.add_documents(versioned_docs) self.versions[version_name] = len(versioned_docs) self.current_version = version_name def search_version(self, query, version=None, k=4): """在特定版本中搜尋""" if version is None: version = self.current_version filter_condition = {"version": version} return self.base_store.similarity_search( query, k=k, filter=filter_condition ) 選擇合適的向量儲存是建構 RAG 應用的重要決策。你需要根據你的資料規模、查詢延遲要求、預算、部署環境以及是否需要進階過濾或混合搜尋功能來做出選擇。 記住,沒有「最好」的向量儲存,只有「最適合」你特定需求的解決方案。建議在小規模資料上測試多個選項,評估其效能、易用性和成本,然後再做出最終決定。
-
RAG 重要組件 - 檢索器 (Retriver)
RAG 中的檢索器 (Retrievers) 在 RAG (Retrieval-Augmented Generation) 流程中,檢索器 (Retriever) 是一個核心組件,其主要職責是根據使用者的查詢 (Query),從一個大規模的資料來源(通常是向量儲存)中「取回 (Retrieve)」最相關的文件區塊 (Chunks)。 與直接查詢向量儲存相比,檢索器提供了一個更抽象、更靈活的介面,並支援多種先進的檢索策略,以提升檢索結果的品質和多樣性。本文件將根據 10-Retriever 資料夾中的筆記本內容,介紹幾種關鍵的檢索器類型。 檢索器 vs 向量儲存對比 特性 向量儲存 檢索器 抽象層級 底層儲存 高層介面 功能範圍 基礎 CRUD 操作 複雜檢索策略 可組合性 有限 高度可組合 擴展性 依賴具體實現 統一介面,易擴展 適用場景 簡單搜尋需求 複雜檢索邏輯 1. 基礎:VectorStoreRetriever 這是最基礎的檢索器,它直接將一個向量儲存 (Vector Store) 封裝成檢索器介面。任何向量儲存都可以透過 .as_retriever() 方法輕鬆轉換。 1.1 核心搜尋類型 相似度搜尋 (Similarity Search) 預設的檢索方式,回傳與查詢向量最相似的 k 個文件。 from langchain_community.vectorstores import FAISS from langchain_openai import OpenAIEmbeddings # 建立基礎檢索器 db = FAISS.from_documents(docs, OpenAIEmbeddings()) basic_retriever = db.as_retriever( search_type="similarity", search_kwargs={"k": 5} ) # 執行檢索 results = basic_retriever.invoke("什麼是機器學習?") for doc in results: print(f"Source: {doc.metadata.get('source', 'unknown')}") print(f"Content: {doc.page_content[:100]}...") 最大邊際相關性 (Maximal Marginal Relevance, MMR) 在回傳結果時,不僅考慮文件與查詢的相關性,還會考慮文件之間的多樣性,避免回傳過於相似、冗餘的內容。 # MMR 參數詳解 mmr_retriever = db.as_retriever( search_type="mmr", search_kwargs={ "k": 5, # 最終回傳的文件數量 "fetch_k": 20, # 初始候選文件數量(越大越多樣,但計算成本越高) "lambda_mult": 0.7 # 平衡因子:1.0=純相關性,0.0=純多樣性 } ) # MMR 演算法流程示例 def explain_mmr_process(): """ MMR 演算法步驟: 1. 取得 fetch_k=20 個最相似的候選文件 2. 選擇與查詢最相似的文件作為第一個結果 3. 對於剩餘的 k-1 個位置: - 計算每個候選文件的 MMR 分數 - MMR = λ × 相關性分數 - (1-λ) × max(與已選文件的相似度) - 選擇 MMR 分數最高的文件 """ pass relevant_diverse_docs = mmr_retriever.invoke("深度學習的應用領域") 相似度分數過濾 (Similarity Score Threshold) 只回傳相似度分數高於某個閾值的結果,用於過濾掉低相關性的雜訊。 # 基於分數閾值的檢索 threshold_retriever = db.as_retriever( search_type="similarity_score_threshold", search_kwargs={ "score_threshold": 0.8, # 只回傳相似度 > 0.8 的文件 "k": 10 # 最大檢索數量 } ) # 只有高相關性的文件會被回傳 high_quality_results = threshold_retriever.invoke("PyTorch 訓練技巧") # 檢查結果品質 for doc, score in threshold_retriever.get_relevant_documents_with_score("PyTorch 訓練技巧"): print(f"Score: {score:.3f} - {doc.page_content[:50]}...") 1.2 進階配置選項 # 自訂檢索器配置 class CustomVectorStoreRetriever: def __init__(self, vectorstore, config): self.vectorstore = vectorstore self.config = config def retrieve(self, query): # 根據查詢長度調整檢索參數 if len(query.split()) <= 3: # 短查詢 k = self.config["short_query_k"] search_type = "similarity" else: # 長查詢 k = self.config["long_query_k"] search_type = "mmr" retriever = self.vectorstore.as_retriever( search_type=search_type, search_kwargs={"k": k} ) return retriever.invoke(query) # 使用自訂配置 config = { "short_query_k": 3, "long_query_k": 7 } custom_retriever = CustomVectorStoreRetriever(db, config) 2. 提升檢索品質的進階檢索器 2.1 MultiQueryRetriever 單一的使用者查詢可能因為用詞或觀點的限制,無法涵蓋所有相關的文件。MultiQueryRetriever 透過 LLM 從一個原始查詢自動生成多個不同角度的相似查詢,然後對每個查詢都進行檢索,最後將所有結果合併並去重。 運作流程詳解 from langchain.retrievers.multi_query import MultiQueryRetriever from langchain_openai import ChatOpenAI # 建立多查詢檢索器 llm = ChatOpenAI(model="gpt-4", temperature=0.3) multiquery_retriever = MultiQueryRetriever.from_llm( retriever=db.as_retriever(), llm=llm, parser_key="lines" # 解析生成查詢的方式 ) # 自訂查詢生成提示 from langchain.retrievers.multi_query import LineListOutputParser from langchain.prompts import PromptTemplate QUERY_PROMPT = PromptTemplate( input_variables=["question"], template="""你是一個 AI 語言模型助手。你的任務是生成5個不同版本的使用者問題, 以便從向量資料庫中檢索相關文件。透過對使用者問題產生多種視角, 你的目標是幫助使用者克服基於距離的相似度搜尋的一些限制。 原始問題: {question} 請提供這些替代問題,每行一個:""" ) # 使用自訂提示的多查詢檢索器 multiquery_retriever = MultiQueryRetriever( retriever=db.as_retriever(), llm_chain=llm, prompt=QUERY_PROMPT, parser=LineListOutputParser() ) # 示例:生成的查詢變體 original_query = "LangChain 的優點是什麼?" # 可能生成的查詢: # 1. "LangChain 有哪些主要功能和特色?" # 2. "使用 LangChain 開發有什麼好處?" # 3. "LangChain 框架的核心優勢是什麼?" # 4. "為什麼選擇 LangChain 而不是其他框架?" # 5. "LangChain 在 AI 應用開發中的價值?" results = multiquery_retriever.invoke(original_query) print(f"找到 {len(results)} 個相關文件") 自訂查詢生成策略 class DomainSpecificMultiQueryRetriever(MultiQueryRetriever): """針對特定領域的多查詢檢索器""" def __init__(self, retriever, llm, domain="general"): super().__init__(retriever, llm) self.domain = domain self.domain_prompts = { "medical": "生成醫學相關的替代查詢...", "legal": "生成法律相關的替代查詢...", "technical": "生成技術相關的替代查詢..." } def generate_queries(self, question): domain_prompt = self.domain_prompts.get(self.domain, "") enhanced_question = f"{domain_prompt}\n原始問題: {question}" return super().generate_queries(enhanced_question) # 技術領域專用檢索器 tech_multiquery = DomainSpecificMultiQueryRetriever( retriever=db.as_retriever(), llm=llm, domain="technical" ) 2.2 ContextualCompressionRetriever 有時候,即使是相關的文件區塊,也可能只有一小部分內容與查詢直接相關。ContextualCompressionRetriever 旨在解決這個問題,它會在檢索後對文件進行壓縮,只保留與查詢最相關的部分。 文件壓縮器類型 LLMChainExtractor 使用 LLM 判斷每個文件中的哪些句子與查詢相關,並只保留這些句子。 from langchain.retrievers import ContextualCompressionRetriever from langchain.retrievers.document_compressors import LLMChainExtractor from langchain_openai import ChatOpenAI # 建立 LLM 提取器 llm = ChatOpenAI(model="gpt-4", temperature=0) compressor = LLMChainExtractor.from_llm(llm) # 建立壓縮檢索器 compression_retriever = ContextualCompressionRetriever( base_compressor=compressor, base_retriever=db.as_retriever() ) # 執行壓縮檢索 compressed_docs = compression_retriever.invoke("深度學習在醫療診斷中的應用") # 比較原始和壓縮後的內容長度 for doc in compressed_docs: print(f"壓縮後長度: {len(doc.page_content)} 字符") print(f"內容: {doc.page_content}") print("-" * 50) EmbeddingsFilter 計算每個文件與查詢的嵌入相似度,只保留高於某個閾值的文件。 from langchain.retrievers.document_compressors import EmbeddingsFilter # 建立嵌入過濾器 embeddings = OpenAIEmbeddings() embeddings_filter = EmbeddingsFilter( embeddings=embeddings, similarity_threshold=0.76, # 相似度閾值 k=10 # 最多保留的文件數 ) # 組合多個壓縮器 from langchain.retrievers.document_compressors import DocumentCompressorPipeline # 建立壓縮管道:先用嵌入過濾,再用 LLM 提取 pipeline_compressor = DocumentCompressorPipeline( transformers=[embeddings_filter, compressor] ) pipeline_retriever = ContextualCompressionRetriever( base_compressor=pipeline_compressor, base_retriever=db.as_retriever() ) # 雙重壓縮的結果 highly_relevant_docs = pipeline_retriever.invoke("機器學習模型評估指標") 自訂壓縮器 from langchain.retrievers.document_compressors.base import BaseDocumentCompressor from typing import List, Sequence import re class KeywordHighlightCompressor(BaseDocumentCompressor): """突出顯示關鍵字並保留相關段落的壓縮器""" def __init__(self, keywords_per_query=5): self.keywords_per_query = keywords_per_query def compress_documents( self, documents: Sequence[Document], query: str ) -> List[Document]: # 提取查詢中的關鍵字 keywords = self._extract_keywords(query) compressed_docs = [] for doc in documents: # 找到包含關鍵字的句子 relevant_sentences = self._find_relevant_sentences( doc.page_content, keywords ) if relevant_sentences: compressed_content = "\n".join(relevant_sentences) compressed_doc = Document( page_content=compressed_content, metadata={**doc.metadata, "compression_ratio": len(compressed_content) / len(doc.page_content)} ) compressed_docs.append(compressed_doc) return compressed_docs def _extract_keywords(self, query: str) -> List[str]: # 簡單的關鍵字提取(實際應用中可使用更複雜的 NLP 技術) words = re.findall(r'\b\w+\b', query.lower()) return words[:self.keywords_per_query] def _find_relevant_sentences(self, text: str, keywords: List[str]) -> List[str]: sentences = re.split(r'[.!?]+', text) relevant = [] for sentence in sentences: if any(keyword in sentence.lower() for keyword in keywords): relevant.append(sentence.strip()) return relevant # 使用自訂壓縮器 custom_compressor = KeywordHighlightCompressor(keywords_per_query=3) custom_compression_retriever = ContextualCompressionRetriever( base_compressor=custom_compressor, base_retriever=db.as_retriever() ) 2.3 LongContextReorder 研究顯示,LLM 在處理長上下文時,對於開頭和結尾的資訊注意力最高,而中間的資訊容易被「遺忘」。LongContextReorder 是一個後處理步驟,它會將檢索到的文件列表進行重排序,將最相關的文件放在列表的開頭和結尾,而將次要的文件放在中間,以最大化 LLM 的注意力。 from langchain.document_transformers import LongContextReorder # 建立重排序檢索器 def create_reordered_retriever(base_retriever): reordering = LongContextReorder() def reordered_retrieve(query): # 先檢索文件 docs = base_retriever.invoke(query) # 重新排序以最佳化長上下文處理 reordered_docs = reordering.transform_documents(docs) return reordered_docs return reordered_retrieve # 使用重排序檢索器 base_retriever = db.as_retriever(search_kwargs={"k": 10}) reordered_retriever = create_reordered_retriever(base_retriever) # 注意力最佳化的結果 optimized_docs = reordered_retriever("複雜的多步驟機器學習流程") # 檢查重排序效果 print("重排序後的文件順序:") for i, doc in enumerate(optimized_docs): relevance = "高" if i < 2 or i >= len(optimized_docs) - 2 else "中" print(f"位置 {i+1}: 重要性={relevance} - {doc.page_content[:50]}...") 自訂重排序策略 class SmartReorder: """智能重排序策略""" def __init__(self, strategy="lost_in_middle"): self.strategy = strategy def reorder_documents(self, docs, query=None): if self.strategy == "lost_in_middle": return self._lost_in_middle_reorder(docs) elif self.strategy == "similarity_decay": return self._similarity_decay_reorder(docs) elif self.strategy == "interleaved": return self._interleaved_reorder(docs) else: return docs def _lost_in_middle_reorder(self, docs): """將最相關的文件放在開頭和結尾""" if len(docs) <= 2: return docs reordered = [] high_relevance = docs[:len(docs)//2] low_relevance = docs[len(docs)//2:] # 交替放置高相關性文件在開頭和結尾 for i, doc in enumerate(high_relevance): if i % 2 == 0: reordered.insert(0, doc) # 放在開頭 else: reordered.append(doc) # 放在結尾 # 低相關性文件放在中間 mid_point = len(reordered) // 2 for doc in low_relevance: reordered.insert(mid_point, doc) mid_point += 1 return reordered def _similarity_decay_reorder(self, docs): """基於相似度衰減的重排序""" # 實現基於相似度分數的動態重排序 return docs # 簡化實現 def _interleaved_reorder(self, docs): """交錯排列不同類型的文件""" # 根據文件類型或來源進行交錯排列 return docs # 簡化實現 # 使用智能重排序 smart_reorder = SmartReorder(strategy="lost_in_middle") def smart_reordered_retriever(query): docs = db.similarity_search(query, k=10) return smart_reorder.reorder_documents(docs, query) 3. 處理複雜文件結構的檢索器 3.1 ParentDocumentRetriever 在處理文件時,我們常常面臨一個兩難:小的文件區塊有利於精準的語意搜尋,但大的區塊能提供更完整的上下文。ParentDocumentRetriever 巧妙地解決了這個問題。 詳細運作流程 from langchain.retrievers import ParentDocumentRetriever from langchain.storage import InMemoryStore from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain_community.vectorstores import Chroma # 建立不同粒度的分割器 parent_splitter = RecursiveCharacterTextSplitter( chunk_size=2000, # 父區塊:2000字符 chunk_overlap=200, # 重疊部分 separators=["\n\n", "\n", " ", ""] ) child_splitter = RecursiveCharacterTextSplitter( chunk_size=400, # 子區塊:400字符 chunk_overlap=50, # 重疊部分 separators=["\n\n", "\n", " ", ""] ) # 建立儲存 vectorstore = Chroma( collection_name="child_chunks", embedding_function=OpenAIEmbeddings() ) store = InMemoryStore() # 儲存父區塊 # 建立父文件檢索器 parent_retriever = ParentDocumentRetriever( vectorstore=vectorstore, docstore=store, child_splitter=child_splitter, parent_splitter=parent_splitter, k=3 # 檢索的子區塊數量 ) # 新增文件 parent_retriever.add_documents(docs, ids=None) # 檢索流程示例 def demonstrate_parent_retrieval(query): """展示父文件檢索的詳細流程""" # 1. 在子區塊中搜尋 child_docs = vectorstore.similarity_search(query, k=3) print("找到的相關子區塊:") for i, doc in enumerate(child_docs): print(f"子區塊 {i+1}: {doc.page_content[:100]}...") # 2. 透過父文件檢索器取得完整上下文 parent_docs = parent_retriever.invoke(query) print("\n對應的父區塊:") for i, doc in enumerate(parent_docs): print(f"父區塊 {i+1} (長度: {len(doc.page_content)}): {doc.page_content[:200]}...") return parent_docs # 執行演示 results = demonstrate_parent_retrieval("深度學習的反向傳播演算法") 進階配置選項 class AdaptiveParentDocumentRetriever(ParentDocumentRetriever): """自適應的父文件檢索器""" def __init__(self, vectorstore, docstore, child_splitter, parent_splitter, **kwargs): super().__init__(vectorstore, docstore, child_splitter, parent_splitter, **kwargs) self.query_complexity_threshold = 10 # 查詢複雜度閾值 def invoke(self, query: str): # 根據查詢複雜度調整檢索策略 query_words = len(query.split()) if query_words > self.query_complexity_threshold: # 複雜查詢:檢索更多子區塊,回傳更多父區塊 self.k = 5 self.fetch_k = 20 else: # 簡單查詢:較少的檢索數量 self.k = 3 self.fetch_k = 10 return super().invoke(query) def get_relevant_documents_with_metadata(self, query: str): """回傳文件及其檢索中繼資料""" docs = self.invoke(query) enhanced_docs = [] for doc in docs: # 計算父區塊包含的子區塊數量 child_count = len([d for d in self.vectorstore.similarity_search(query, k=20) if d.metadata.get('parent_id') == doc.metadata.get('doc_id')]) doc.metadata['child_chunk_count'] = child_count doc.metadata['retrieval_method'] = 'parent_document' enhanced_docs.append(doc) return enhanced_docs # 使用自適應檢索器 adaptive_retriever = AdaptiveParentDocumentRetriever( vectorstore=vectorstore, docstore=store, child_splitter=child_splitter, parent_splitter=parent_splitter ) 3.2 MultiVectorRetriever 這個檢索器更進一步,它允許你為一份文件儲存多個不同的向量,這些向量可以代表文件的不同方面。 策略一:儲存摘要 (Summary) from langchain.retrievers.multi_vector import MultiVectorRetriever from langchain.storage import InMemoryStore from langchain_openai import ChatOpenAI import uuid # 建立多向量檢索器 vectorstore = Chroma(collection_name="summaries", embedding_function=OpenAIEmbeddings()) store = InMemoryStore() id_key = "doc_id" # 建立多向量檢索器 mv_retriever = MultiVectorRetriever( vectorstore=vectorstore, docstore=store, id_key=id_key ) # 生成文件摘要 llm = ChatOpenAI(model="gpt-4", temperature=0) def generate_summaries(docs): """為文件生成摘要""" summaries = [] doc_ids = [str(uuid.uuid4()) for _ in docs] summary_prompt = """ 請為以下文件提供一個簡潔的摘要,重點關注主要概念和關鍵資訊: 文件內容: {content} 摘要: """ for doc, doc_id in zip(docs, doc_ids): # 生成摘要 summary = llm.invoke(summary_prompt.format(content=doc.page_content)) # 建立摘要文件 summary_doc = Document( page_content=summary.content, metadata={**doc.metadata, id_key: doc_id} ) summaries.append(summary_doc) # 將原始文件存入 docstore store.mset([(doc_id, doc)]) return summaries, doc_ids # 生成並新增摘要 summaries, doc_ids = generate_summaries(docs) mv_retriever.vectorstore.add_documents(summaries) # 檢索:透過摘要找到完整文件 summary_results = mv_retriever.invoke("機器學習的監督學習方法") print(f"透過摘要檢索到 {len(summary_results)} 個完整文件") 策略二:假設性問題 (Hypothetical Questions) def generate_hypothetical_questions(docs, questions_per_doc=3): """為每個文件生成假設性問題""" question_docs = [] doc_ids = [str(uuid.uuid4()) for _ in docs] question_prompt = """ 基於以下文件內容,生成 {num_questions} 個這個文件可以回答的問題。 問題應該具體、明確,並涵蓋文件的主要內容。 文件內容: {content} 請用以下格式回應: 問題1: [問題內容] 問題2: [問題內容] 問題3: [問題內容] """ for doc, doc_id in zip(docs, doc_ids): # 生成問題 questions_response = llm.invoke( question_prompt.format( content=doc.page_content, num_questions=questions_per_doc ) ) # 解析生成的問題 questions = [ line.split(": ", 1)[1] for line in questions_response.content.split("\n") if line.strip() and ": " in line ] # 為每個問題建立文件 for question in questions: question_doc = Document( page_content=question, metadata={ **doc.metadata, id_key: doc_id, "type": "hypothetical_question" } ) question_docs.append(question_doc) # 將原始文件存入 docstore store.mset([(doc_id, doc)]) return question_docs # 建立基於問題的檢索器 question_vectorstore = Chroma( collection_name="questions", embedding_function=OpenAIEmbeddings() ) question_store = InMemoryStore() question_retriever = MultiVectorRetriever( vectorstore=question_vectorstore, docstore=question_store, id_key=id_key ) # 生成並新增假設性問題 hypothetical_questions = generate_hypothetical_questions(docs) question_retriever.vectorstore.add_documents(hypothetical_questions) # 檢索:透過問題匹配找到相關文件 question_results = question_retriever.invoke("如何調整學習率?") print(f"透過問題匹配找到 {len(question_results)} 個相關文件") 策略三:多視角向量化 def generate_multi_perspective_vectors(docs): """從多個視角對文件進行向量化""" perspectives = { "technical": "從技術實現的角度重新描述這個文件的內容", "conceptual": "從概念理解的角度總結這個文件的核心思想", "practical": "從實際應用的角度說明這個文件的實用價值", "educational": "從教學的角度解釋這個文件適合什麼程度的學習者" } multi_docs = [] doc_ids = [str(uuid.uuid4()) for _ in docs] for doc, doc_id in zip(docs, doc_ids): store.mset([(doc_id, doc)]) # 儲存原始文件 for perspective, prompt in perspectives.items(): # 從不同視角重新描述文件 perspective_content = llm.invoke( f"{prompt}:\n\n{doc.page_content}" ) perspective_doc = Document( page_content=perspective_content.content, metadata={ **doc.metadata, id_key: doc_id, "perspective": perspective } ) multi_docs.append(perspective_doc) return multi_docs # 建立多視角檢索器 multi_perspective_retriever = MultiVectorRetriever( vectorstore=Chroma(collection_name="multi_perspective", embedding_function=OpenAIEmbeddings()), docstore=store, id_key=id_key ) # 新增多視角文件 multi_perspective_docs = generate_multi_perspective_vectors(docs) multi_perspective_retriever.vectorstore.add_documents(multi_perspective_docs) # 檢索時會考慮所有視角 perspective_results = multi_perspective_retriever.invoke("初學者如何學習機器學習?") 組合策略的綜合檢索器 class ComprehensiveMultiVectorRetriever: """組合多種策略的綜合多向量檢索器""" def __init__(self, base_docs, llm): self.base_docs = base_docs self.llm = llm self.retrievers = {} self._setup_retrievers() def _setup_retrievers(self): # 建立不同策略的檢索器 strategies = ["summary", "questions", "perspectives"] for strategy in strategies: vectorstore = Chroma( collection_name=f"{strategy}_store", embedding_function=OpenAIEmbeddings() ) docstore = InMemoryStore() retriever = MultiVectorRetriever( vectorstore=vectorstore, docstore=docstore, id_key="doc_id" ) self.retrievers[strategy] = retriever # 根據策略生成對應的文件 if strategy == "summary": generated_docs = self._generate_summaries() elif strategy == "questions": generated_docs = self._generate_questions() elif strategy == "perspectives": generated_docs = self._generate_perspectives() retriever.vectorstore.add_documents(generated_docs) def retrieve_with_strategy(self, query, strategy="summary", k=3): """使用特定策略檢索""" return self.retrievers[strategy].invoke(query) def retrieve_ensemble(self, query, weights=None, k=3): """組合多個策略的結果""" if weights is None: weights = {"summary": 0.4, "questions": 0.4, "perspectives": 0.2} all_results = [] for strategy, weight in weights.items(): strategy_results = self.retrievers[strategy].invoke(query) # 為每個結果添加權重 for doc in strategy_results: doc.metadata["strategy"] = strategy doc.metadata["weight"] = weight all_results.append(doc) # 去重並排序 unique_results = self._deduplicate_and_rank(all_results) return unique_results[:k] def _deduplicate_and_rank(self, docs): """去重並根據綜合分數排序""" seen_ids = set() unique_docs = [] for doc in docs: doc_id = doc.metadata.get("doc_id") if doc_id not in seen_ids: seen_ids.add(doc_id) unique_docs.append(doc) return unique_docs # 使用綜合檢索器 comprehensive_retriever = ComprehensiveMultiVectorRetriever(docs, llm) # 使用不同策略檢索 summary_results = comprehensive_retriever.retrieve_with_strategy("深度學習", "summary") question_results = comprehensive_retriever.retrieve_with_strategy("深度學習", "questions") # 組合策略檢索 ensemble_results = comprehensive_retriever.retrieve_ensemble( "深度學習的實際應用", weights={"summary": 0.3, "questions": 0.5, "perspectives": 0.2} ) 4. 結合不同檢索策略:EnsembleRetriever 不同的檢索方法各有優劣。例如,傳統的關鍵字搜尋(如 BM25)擅長匹配精確的術語,而向量搜尋擅長捕捉語意相似度。EnsembleRetriever 可以將多個不同的檢索器(如一個 BM25Retriever 和一個 VectorStoreRetriever)組合起來。 4.1 基本 Ensemble 檢索 from langchain.retrievers import BM25Retriever, EnsembleRetriever from langchain_community.vectorstores import FAISS # 準備不同類型的檢索器 # 1. BM25 檢索器(關鍵字匹配) bm25_retriever = BM25Retriever.from_documents(docs) bm25_retriever.k = 5 # 2. 向量檢索器(語意相似度) faiss_vectorstore = FAISS.from_documents(docs, OpenAIEmbeddings()) faiss_retriever = faiss_vectorstore.as_retriever(search_kwargs={"k": 5}) # 3. 建立 Ensemble 檢索器 ensemble_retriever = EnsembleRetriever( retrievers=[bm25_retriever, faiss_retriever], weights=[0.5, 0.5] # 等權重組合 ) # 執行組合檢索 hybrid_results = ensemble_retriever.invoke("機器學習模型評估指標") # 比較不同檢索器的結果 print("BM25 檢索結果:") bm25_results = bm25_retriever.invoke("機器學習模型評估指標") for doc in bm25_results[:3]: print(f"- {doc.page_content[:100]}...") print("\n向量檢索結果:") vector_results = faiss_retriever.invoke("機器學習模型評估指標") for doc in vector_results[:3]: print(f"- {doc.page_content[:100]}...") print("\nEnsemble 檢索結果:") for doc in hybrid_results[:3]: print(f"- {doc.page_content[:100]}...") 4.2 Reciprocal Rank Fusion (RRF) 演算法詳解 def explain_rrf_algorithm(): """ Reciprocal Rank Fusion 演算法: 1. 對每個檢索器的結果進行排序 2. 為每個文件計算 RRF 分數: RRF(d) = Σ(1 / (k + rank_i(d))) 其中 k 是常數(通常為60),rank_i(d) 是文件 d 在檢索器 i 中的排名 3. 根據 RRF 分數重新排序所有文件 4. 回傳 top-k 結果 """ pass class CustomEnsembleRetriever: """自訂的 Ensemble 檢索器,實現更靈活的融合策略""" def __init__(self, retrievers, weights=None, fusion_method="rrf"): self.retrievers = retrievers self.weights = weights or [1.0] * len(retrievers) self.fusion_method = fusion_method def invoke(self, query, k=5): # 收集所有檢索器的結果 all_results = [] for i, retriever in enumerate(self.retrievers): results = retriever.invoke(query) for rank, doc in enumerate(results): all_results.append({ "doc": doc, "retriever_id": i, "rank": rank, "weight": self.weights[i] }) # 根據融合方法計算最終分數 if self.fusion_method == "rrf": return self._rrf_fusion(all_results, k) elif self.fusion_method == "weighted_sum": return self._weighted_sum_fusion(all_results, k) elif self.fusion_method == "borda_count": return self._borda_count_fusion(all_results, k) def _rrf_fusion(self, results, k, rrf_k=60): """Reciprocal Rank Fusion""" doc_scores = {} for result in results: doc_id = id(result["doc"]) # 使用物件 ID 作為唯一標識 if doc_id not in doc_scores: doc_scores[doc_id] = {"doc": result["doc"], "score": 0} # RRF 分數計算 rrf_score = result["weight"] / (rrf_k + result["rank"] + 1) doc_scores[doc_id]["score"] += rrf_score # 排序並回傳 top-k sorted_docs = sorted( doc_scores.values(), key=lambda x: x["score"], reverse=True ) return [item["doc"] for item in sorted_docs[:k]] def _weighted_sum_fusion(self, results, k): """加權求和融合""" doc_scores = {} max_rank = max(result["rank"] for result in results) for result in results: doc_id = id(result["doc"]) if doc_id not in doc_scores: doc_scores[doc_id] = {"doc": result["doc"], "score": 0} # 將排名轉換為分數(排名越高分數越高) rank_score = (max_rank - result["rank"]) / max_rank weighted_score = result["weight"] * rank_score doc_scores[doc_id]["score"] += weighted_score sorted_docs = sorted( doc_scores.values(), key=lambda x: x["score"], reverse=True ) return [item["doc"] for item in sorted_docs[:k]] def _borda_count_fusion(self, results, k): """Borda Count 融合""" doc_scores = {} # 計算每個檢索器的最大排名 retriever_max_ranks = {} for result in results: retriever_id = result["retriever_id"] if retriever_id not in retriever_max_ranks: retriever_max_ranks[retriever_id] = 0 retriever_max_ranks[retriever_id] = max( retriever_max_ranks[retriever_id], result["rank"] ) for result in results: doc_id = id(result["doc"]) if doc_id not in doc_scores: doc_scores[doc_id] = {"doc": result["doc"], "score": 0} # Borda Count 分數 max_rank = retriever_max_ranks[result["retriever_id"]] borda_score = result["weight"] * (max_rank - result["rank"]) doc_scores[doc_id]["score"] += borda_score sorted_docs = sorted( doc_scores.values(), key=lambda x: x["score"], reverse=True ) return [item["doc"] for item in sorted_docs[:k]] # 使用自訂 Ensemble 檢索器 custom_ensemble = CustomEnsembleRetriever( retrievers=[bm25_retriever, faiss_retriever], weights=[0.6, 0.4], # 更重視 BM25 結果 fusion_method="rrf" ) rrf_results = custom_ensemble.invoke("深度學習優化技術") 4.3 多檢索器的進階組合 # 建立更多樣化的檢索器組合 def create_diverse_ensemble(): # 1. 不同參數的向量檢索器 similarity_retriever = faiss_vectorstore.as_retriever( search_type="similarity", search_kwargs={"k": 5} ) mmr_retriever = faiss_vectorstore.as_retriever( search_type="mmr", search_kwargs={"k": 5, "fetch_k": 20, "lambda_mult": 0.7} ) # 2. 壓縮檢索器 compression_retriever = ContextualCompressionRetriever( base_compressor=EmbeddingsFilter( embeddings=OpenAIEmbeddings(), similarity_threshold=0.76 ), base_retriever=similarity_retriever ) # 3. 多查詢檢索器 multi_query_retriever = MultiQueryRetriever.from_llm( retriever=similarity_retriever, llm=ChatOpenAI(model="gpt-4", temperature=0.3) ) # 4. 建立多層 Ensemble diverse_ensemble = EnsembleRetriever( retrievers=[ bm25_retriever, # 關鍵字匹配 mmr_retriever, # 多樣性語意搜尋 compression_retriever, # 壓縮檢索 multi_query_retriever # 多查詢檢索 ], weights=[0.3, 0.3, 0.2, 0.2] ) return diverse_ensemble # 建立多樣化的檢索器 diverse_retriever = create_diverse_ensemble() # 測試不同複雜度的查詢 test_queries = [ "什麼是機器學習?", # 簡單查詢 "深度學習中的梯度消失問題及其解決方案", # 複雜查詢 "如何選擇合適的優化器和學習率?" # 實用查詢 ] for query in test_queries: print(f"\n查詢: {query}") results = diverse_retriever.invoke(query) print(f"找到 {len(results)} 個相關文件") for i, doc in enumerate(results[:2]): print(f" {i+1}. {doc.page_content[:80]}...") 5. 自訂檢索器開發 5.1 基礎自訂檢索器框架 from langchain.schema import BaseRetriever, Document from typing import List from abc import ABC, abstractmethod class CustomRetriever(BaseRetriever, ABC): """自訂檢索器的基礎類別""" def __init__(self, **kwargs): super().__init__(**kwargs) @abstractmethod def _get_relevant_documents(self, query: str) -> List[Document]: """實現具體的檢索邏輯""" pass def invoke(self, query: str) -> List[Document]: """統一的檢索介面""" return self._get_relevant_documents(query) class KeywordBoostRetriever(CustomRetriever): """基於關鍵字加權的檢索器""" def __init__(self, base_retriever, keyword_boost=2.0, **kwargs): super().__init__(**kwargs) self.base_retriever = base_retriever self.keyword_boost = keyword_boost self.important_keywords = [ "機器學習", "深度學習", "神經網路", "人工智慧", "演算法", "模型", "訓練", "優化" ] def _get_relevant_documents(self, query: str) -> List[Document]: # 基礎檢索 base_results = self.base_retriever.invoke(query) # 關鍵字加權 boosted_results = [] for doc in base_results: boost_score = 1.0 # 檢查重要關鍵字 for keyword in self.important_keywords: if keyword in doc.page_content.lower(): boost_score *= self.keyword_boost doc.metadata["boost_score"] = boost_score boosted_results.append(doc) # 根據加權分數重新排序 boosted_results.sort( key=lambda x: x.metadata.get("boost_score", 1.0), reverse=True ) return boosted_results # 使用關鍵字加權檢索器 keyword_retriever = KeywordBoostRetriever( base_retriever=faiss_retriever, keyword_boost=1.5 ) boosted_results = keyword_retriever.invoke("機器學習演算法比較") 5.2 時間感知檢索器 from datetime import datetime, timedelta import dateutil.parser class TimeAwareRetriever(CustomRetriever): """時間感知的檢索器,優先顯示最新內容""" def __init__(self, base_retriever, time_decay_factor=0.1, **kwargs): super().__init__(**kwargs) self.base_retriever = base_retriever self.time_decay_factor = time_decay_factor def _get_relevant_documents(self, query: str) -> List[Document]: base_results = self.base_retriever.invoke(query) current_time = datetime.now() time_weighted_results = [] for doc in base_results: # 嘗試從中繼資料獲取時間資訊 doc_time = self._extract_document_time(doc) if doc_time: # 計算時間衰減分數 time_diff = (current_time - doc_time).days time_score = max(0.1, 1.0 - (time_diff * self.time_decay_factor / 365)) else: time_score = 0.5 # 沒有時間資訊的預設分數 doc.metadata["time_score"] = time_score doc.metadata["document_age_days"] = time_diff if doc_time else None time_weighted_results.append(doc) # 根據時間分數排序 time_weighted_results.sort( key=lambda x: x.metadata.get("time_score", 0), reverse=True ) return time_weighted_results def _extract_document_time(self, doc: Document) -> datetime: """從文件中提取時間資訊""" time_fields = ["created_at", "published_date", "last_modified", "date"] for field in time_fields: if field in doc.metadata: try: return dateutil.parser.parse(doc.metadata[field]) except: continue return None # 使用時間感知檢索器 time_retriever = TimeAwareRetriever( base_retriever=faiss_retriever, time_decay_factor=0.05 # 較慢的時間衰減 ) recent_results = time_retriever.invoke("最新的深度學習技術") 5.3 使用者偏好檢索器 class UserPreferenceRetriever(CustomRetriever): """基於使用者偏好的個人化檢索器""" def __init__(self, base_retriever, user_profile=None, **kwargs): super().__init__(**kwargs) self.base_retriever = base_retriever self.user_profile = user_profile or { "preferred_topics": [], "difficulty_level": "intermediate", "language": "zh-TW", "content_type_preference": {"tutorial": 1.2, "research": 0.8, "news": 1.0} } def _get_relevant_documents(self, query: str) -> List[Document]: base_results = self.base_retriever.invoke(query) personalized_results = [] for doc in base_results: preference_score = self._calculate_preference_score(doc) doc.metadata["preference_score"] = preference_score personalized_results.append(doc) # 根據偏好分數排序 personalized_results.sort( key=lambda x: x.metadata.get("preference_score", 0), reverse=True ) return personalized_results def _calculate_preference_score(self, doc: Document) -> float: score = 1.0 # 主題偏好 doc_topics = doc.metadata.get("topics", []) for topic in self.user_profile["preferred_topics"]: if topic in doc_topics: score *= 1.3 # 難度等級匹配 doc_difficulty = doc.metadata.get("difficulty", "intermediate") if doc_difficulty == self.user_profile["difficulty_level"]: score *= 1.2 # 內容類型偏好 content_type = doc.metadata.get("content_type", "unknown") if content_type in self.user_profile["content_type_preference"]: score *= self.user_profile["content_type_preference"][content_type] return score def update_user_profile(self, **updates): """更新使用者偏好""" self.user_profile.update(updates) # 建立個人化檢索器 user_profile = { "preferred_topics": ["深度學習", "電腦視覺"], "difficulty_level": "advanced", "language": "zh-TW", "content_type_preference": {"research": 1.5, "tutorial": 1.0, "news": 0.7} } personalized_retriever = UserPreferenceRetriever( base_retriever=faiss_retriever, user_profile=user_profile ) personalized_results = personalized_retriever.invoke("卷積神經網路") 6. 檢索器效能評估和最佳化 6.1 檢索品質評估 import numpy as np from sklearn.metrics.pairwise import cosine_similarity class RetrievalEvaluator: """檢索器效能評估工具""" def __init__(self, embedding_model): self.embedding_model = embedding_model def evaluate_relevance(self, query, retrieved_docs, ground_truth_docs=None): """評估檢索相關性""" query_embedding = self.embedding_model.embed_query(query) # 計算與查詢的相似度 doc_similarities = [] for doc in retrieved_docs: doc_embedding = self.embedding_model.embed_query(doc.page_content) similarity = cosine_similarity( [query_embedding], [doc_embedding] )[0][0] doc_similarities.append(similarity) metrics = { "mean_similarity": np.mean(doc_similarities), "std_similarity": np.std(doc_similarities), "min_similarity": np.min(doc_similarities), "max_similarity": np.max(doc_similarities) } return metrics def evaluate_diversity(self, retrieved_docs): """評估檢索結果的多樣性""" if len(retrieved_docs) < 2: return {"diversity_score": 1.0} # 計算文件間的平均相似度 embeddings = [] for doc in retrieved_docs: embedding = self.embedding_model.embed_query(doc.page_content) embeddings.append(embedding) similarities = [] for i in range(len(embeddings)): for j in range(i+1, len(embeddings)): sim = cosine_similarity([embeddings[i]], [embeddings[j]])[0][0] similarities.append(sim) avg_similarity = np.mean(similarities) diversity_score = 1.0 - avg_similarity # 相似度越低,多樣性越高 return { "diversity_score": diversity_score, "avg_inter_document_similarity": avg_similarity } def comprehensive_evaluation(self, retriever, test_queries): """綜合評估檢索器效能""" results = {} for query in test_queries: retrieved_docs = retriever.invoke(query) relevance_metrics = self.evaluate_relevance(query, retrieved_docs) diversity_metrics = self.evaluate_diversity(retrieved_docs) results[query] = { **relevance_metrics, **diversity_metrics, "num_docs": len(retrieved_docs) } return results # 評估不同檢索器 evaluator = RetrievalEvaluator(OpenAIEmbeddings()) test_queries = [ "深度學習的基本概念", "機器學習模型的評估指標", "神經網路的優化技術" ] # 評估基礎檢索器 basic_results = evaluator.comprehensive_evaluation(faiss_retriever, test_queries) # 評估 Ensemble 檢索器 ensemble_results = evaluator.comprehensive_evaluation(ensemble_retriever, test_queries) # 比較結果 for query in test_queries: print(f"\n查詢: {query}") print(f"基礎檢索器 - 平均相似度: {basic_results[query]['mean_similarity']:.3f}") print(f"Ensemble 檢索器 - 平均相似度: {ensemble_results[query]['mean_similarity']:.3f}") print(f"基礎檢索器 - 多樣性: {basic_results[query]['diversity_score']:.3f}") print(f"Ensemble 檢索器 - 多樣性: {ensemble_results[query]['diversity_score']:.3f}") 6.2 A/B 測試框架 ```pythonimport randomfrom typing import Dict, List, Callable class RetrieverABTester: """檢索器 A/B 測試框架""" def __init__(self, evaluator): self.evaluator = evaluator self.test_results = {} def run_ab_test( self, retriever_a, retriever_b, test_queries: List[str], test_name: str = "ab_test" ): """執行 A/B 測試""" results_a = self.evaluator.comprehensive_evaluation(retriever_a, test_queries) results_b = self.evaluator.comprehensive_evaluation(retriever_b, test_queries) comparison = self._compare_results(results_a, results_b) self.test_results[test_name] = { "retriever_a": results_a, "retriever_b": results_b, "comparison": comparison } return comparison def _compare_results(self, results_a, results_b): """比較兩個檢索器的結果""" metrics = ["mean_similarity", "diversity_score"] comparison = {} for metric in metrics: a_scores = [results_a[q][metric] for q in results_a.keys()] b_scores = [results_b[q][metric] for q in results_b.keys()] comparison[metric] = { "a_avg": np.mean(a_scores), "b_avg": np.mean(b_scores), "improvement": (np.mean(b_scores) - np.mean(a_scores)) / np.mean(a_scores) * 100, "winner": "B" if np.mean(b_scores) > np.mean(a_scores) else "A" } return comparison def statistical_significance_test(self, test_name: str, metric: str = "mean_similarity"): """統計顯著性檢驗""" from scipy import stats if test_name not in self.test_results: raise ValueError(f"Test {test_name} not found") results_a = self.test_results[test_name]["retriever_a"] results_b = self.test_results[test_name]["retriever_b"] a_scores = [results_a[q][metric] for q in results_a.keys()] b_scores = [results_b[q][metric] for q in results_b.keys()]
-
RAG 重要組件 - 重排器(ReRanker)
RAG 中的重排器 (Rerankers) 在 RAG (Retrieval-Augmented Generation) 流程中,檢索器 (Retriever) 從大量文件中取回一批可能相關的文件區塊。然而,這些初步檢索到的結果不一定總是按「最相關」到「最不相關」的順序排列。重排器 (Reranker) 的作用就是在檢索之後、將文件傳遞給大型語言模型 (LLM) 之前,對這批文件進行重新排序,以提升最終結果的品質。 1. 為什麼需要重排? 傳統的向量相似度搜尋(如餘弦相似度)雖然能快速找到語意上相近的文件,但它有時無法完全捕捉查詢和文件之間的細微語意關係。例如,它可能無法區分「蘋果公司」和「蘋果水果」。 重排器透過一個更強大、但計算成本也更高的模型,對初步檢索到的少量文件(例如前 20-100 個)進行精細的相關性評分,從而: 提升精準度:將真正最相關的文件排在最前面 過濾雜訊:降低不相關文件對 LLM 生成答案的干擾 優化上下文:為 LLM 提供更高品質、更具相關性的上下文 2. Cross-Encoder 重排器 這是最常見也是效果最顯著的一種重排器。交叉編碼器 (Cross-Encoder) 會將「查詢」和「單個文件」成對地輸入到一個模型中(通常是 BERT 這類的 Transformer 模型)。 運作原理: 輸入配對:將 (查詢, 文件A), (查詢, 文件B)... 這樣的配對作為模型的輸入 深度交互:模型在內部對查詢和文件的 Token 進行深度的注意力交互 輸出相關性分數:模型最終輸出一個介於 0 和 1 之間的分數 重新排序:根據這個分數對所有初步檢索到的文件進行重新排序 程式碼範例: from langchain.retrievers.document_compressors import CrossEncoderReranker from langchain_community.cross_encoders import HuggingFaceCrossEncoder from langchain.retrievers import ContextualCompressionRetriever # 初始化一個 Hugging Face 上的 Cross-Encoder 模型 model = HuggingFaceCrossEncoder(model_name="BAAI/bge-reranker-base") # 建立重排器 reranker = CrossEncoderReranker(model=model, top_n=3) # 與基礎檢索器結合 compression_retriever = ContextualCompressionRetriever( base_compressor=reranker, base_retriever=retriever ) results = compression_retriever.invoke("你的查詢問題") 常用模型推薦: BAAI/bge-reranker-base:平衡效能和速度,支援中文 BAAI/bge-reranker-large:最高精度,速度較慢 cross-encoder/ms-marco-MiniLM-L-6-v2:英文專用,速度快 3. 第三方 API 型重排器 (Jina Reranker) 對於不希望在本地部署和維護模型的開發者,可以使用像 Jina AI 這樣提供重排服務的 API。 核心特點: 易於使用:只需 API 金鑰即可使用 多語言支援:通常提供強大的多語言重排模型 企業級服務:穩定性高,適合生產環境 程式碼範例: from langchain_jina import JinaRerank from langchain.retrievers import ContextualCompressionRetriever # 初始化 Jina Reranker jina_rerank = JinaRerank(top_n=3) # 與基礎檢索器結合 compression_retriever = ContextualCompressionRetriever( base_compressor=jina_rerank, base_retriever=retriever ) results = compression_retriever.invoke("你的查詢問題") 4. 輕量級本地重排器 (FlashRank) FlashRank 是一個專為速度和輕量化設計的開源重排器程式庫。 核心特點: 超輕量級:模型尺寸小,記憶體佔用低 速度極快:非常適合需要即時回應的應用場景 本地運行:完全在本地處理,保障資料隱私 程式碼範例: from langchain_community.document_transformers import FlashrankRerank from langchain.retrievers import ContextualCompressionRetriever # 初始化 FlashRank 重排器 flashrank_rerank = FlashrankRerank(top_n=3) # 與基礎檢索器結合 compression_retriever = ContextualCompressionRetriever( base_compressor=flashrank_rerank, base_retriever=retriever ) results = compression_retriever.invoke("你的查詢問題") 5. 選擇指南 5.1 快速決策 需求 推薦方案 原因 最高精度 Cross-Encoder (BGE-large) 深度語意理解 最快速度 FlashRank 輕量級最佳化 易於使用 Jina API 無需部署維護 成本敏感 FlashRank 開源免費 隱私要求 本地 Cross-Encoder 完全本地處理 平衡方案 Cross-Encoder (BGE-base) 精度與速度平衡 5.2 效能對比 方案 延遲 精度 成本 維護 FlashRank 極低 中等 免費 簡單 Cross-Encoder (base) 中等 高 免費 中等 Cross-Encoder (large) 高 極高 免費 中等 Jina API 中等* 高 付費 無 *包含網路延遲 6. 實施建議 6.1 基本實施步驟 評估現狀:測量目前檢索系統的基準效能 選擇方案:根據需求選擇合適的重排器 小規模測試:在測試資料上驗證改善效果 A/B 測試:在生產環境中進行對比測試 全面部署:根據測試結果決定是否全面採用 6.2 常見注意事項 延遲影響:重排會增加總體延遲,需要權衡精度提升與延遲增加 成本考量:API 服務按使用量計費,大規模使用需評估成本 降級策略:當重排服務不可用時,應能回退到基礎檢索結果 監控指標:追蹤重排前後的精度變化和使用者滿意度 6.3 簡單評估方法 # 簡單的重排效果評估 def evaluate_reranking(queries, documents, reranker): improvements = [] for query in queries: # 原始檢索結果 original_results = base_retriever.invoke(query) # 重排後結果 reranked_results = reranker.compress_documents(original_results, query) # 人工評估或自動指標計算 # improvement = calculate_improvement(original_results, reranked_results, query) # improvements.append(improvement) return improvements 結論 重排是提升 RAG 系統品質的重要技術。選擇合適的重排器需要考慮: 精度需求:是否需要最高的檢索精度 延遲要求:系統對回應時間的敏感度 成本預算:可接受的部署和運營成本 技術能力:團隊的技術實力和維護能力 建議從簡單方案開始(如 FlashRank),驗證重排的價值後,再根據實際需求選擇更複雜的方案。記住,最好的方案是最適合你具體需求的方案,而不是技術上最先進的方案。
-
Langgraph 基礎多代理人框架1 - Supervisor
建立專門的研究和數學代理人 使用預建的langgraph-supervisor來構建一個協調它們的監督者 從頭開始建立一個監督者 實施高級任務委派 pip install -U langgraph langgraph-supervisor langchain-tavily "langchain[openai]" import getpass import os def _set_if_undefined(var: str): if not os.environ.get(var): os.environ[var] = getpass.getpass(f"Please provide your {var}") _set_if_undefined("OPENAI_API_KEY") _set_if_undefined("TAVILY_API_KEY") 1.建立工作代理 研究代理將能夠使用 Tavily API 訪問網路搜尋工具 數學代理將可以使用簡單的數學工具(add,multiply,divide) Research agent¶ 研究代理 from langchain_tavily import TavilySearch web_search = TavilySearch(max_results=3) web_search_results = web_search.invoke("who is the mayor of NYC?") print(web_search_results["results"][0]["content"]) 要創建個別的工作代理,我們將使用 LangGraph 的預建代理。 from langgraph.prebuilt import create_react_agent research_agent = create_react_agent( model="openai:gpt-4.1", tools=[web_search], prompt=( "你是專業的研究助理代理人。\n\n" "【工作職責】\n" "1. 執行研究相關任務:\n" " - 信息檢索與收集\n" " - 資料分析與整理\n" " - 內容查證與驗證\n\n" "2. 工作限制:\n" " - 不處理數學計算任務\n" " - 不執行非研究類工作\n" " - 專注於使用提供的研究工具\n\n" "【回報要求】\n" "- 任務完成後直接向主管回報\n" "- 僅提供研究成果,不添加額外解釋\n" "- 保持回應專業、準確、簡潔" ), name="research_agent", ) 讓我們運行代理以驗證其行為是否如預期。 for chunk in research_agent.stream( {"messages": [{"role": "user", "content": "who is the mayor of NYC?"}]} ): pretty_print_messages(chunk) Update from node agent: ================================== Ai Message ================================== Name: research_agent Tool Calls: tavily_search (call_T4wrj7DKAG5hVtNVhjRYGdei) Call ID: call_T4wrj7DKAG5hVtNVhjRYGdei Args: query: current mayor of New York City Update from node tools: ================================= Tool Message ================================= Name: tavily_search {"query": "current mayor of New York City", "follow_up_questions": null, "answer": null, "images": [], "results": [{"title": "List of mayors of New York City - Wikipedia", "url": "https://en.wikipedia.org/wiki/List_of_mayors_of_New_York_City", "content": "The mayor of New York City is the chief executive of the Government of New York City, as stipulated by New York City's charter.The current officeholder, the 110th in the sequence of regular mayors, is Eric Adams, a member of the Democratic Party.. During the Dutch colonial period from 1624 to 1664, New Amsterdam was governed by the Director of New Netherland.", "score": 0.9039154, "raw_content": null}, {"title": "Office of the Mayor | Mayor's Bio | City of New York - NYC.gov", "url": "https://www.nyc.gov/office-of-the-mayor/bio.page", "content": "Mayor Eric Adams has served the people of New York City as an NYPD officer, State Senator, Brooklyn Borough President, and now as the 110th Mayor of the City of New York. He gave voice to a diverse coalition of working families in all five boroughs and is leading the fight to bring back New York City's economy, reduce inequality, improve", "score": 0.8405867, "raw_content": null}, {"title": "Eric Adams - Wikipedia", "url": "https://en.wikipedia.org/wiki/Eric_Adams", "content": "Eric Leroy Adams (born September 1, 1960) is an American politician and former police officer who has served as the 110th mayor of New York City since 2022. Adams was an officer in the New York City Transit Police and then the New York City Police Department (NYPD) for more than 20 years, retiring at the rank of captain.He served in the New York State Senate from 2006 to 2013, representing the", "score": 0.77731717, "raw_content": null}], "response_time": 1.31} Update from node agent: ================================== Ai Message ================================== Name: research_agent The current mayor of New York City is Eric Adams. Math agent def add(a: float, b: float): """將兩個數字相加 參數: a: 第一個數字 b: 第二個數字 返回: 兩數之和 """ return a + b def multiply(a: float, b: float): """將兩個數字相乘 參數: a: 第一個數字 b: 第二個數字 返回: 兩數之積 """ return a * b def divide(a: float, b: float): """將兩個數字相除 參數: a: 被除數 b: 除數 返回: 商值,如果除數為零則返回錯誤信息 """ if b == 0: return "錯誤:除數不能為零" return a / b math_agent = create_react_agent( model="openai:gpt-4.1", tools=[add, multiply, divide], prompt=( "你是數學計算專家代理。\n\n" "【工作職責】\n" "1. 數學運算:\n" " - 基礎算術運算(加、乘、除)\n" " - 精確計算數值\n" " - 處理數學相關查詢\n\n" "2. 工作限制:\n" " - 只處理數學計算任務\n" " - 不處理非數學相關工作\n" " - 專注使用提供的計算工具\n\n" "【回報要求】\n" "- 計算完成後直接回報主管\n" "- 僅提供計算結果\n" "- 不附加說明文字\n" "- 確保結果準確簡潔" ), name="math_agent", ) 讓我們運行數學代理: for chunk in math_agent.stream( {"messages": [{"role": "user", "content": "what's (3 + 5) x 7"}]} ): pretty_print_messages(chunk) Update from node agent: ================================== Ai Message ================================== Name: math_agent Tool Calls: add (call_LqyOrR1Ktr2LVLDlXpbWNAsp) Call ID: call_LqyOrR1Ktr2LVLDlXpbWNAsp Args: a: 3 b: 5 Update from node tools: ================================= Tool Message ================================= Name: add 8.0 Update from node agent: ================================== Ai Message ================================== Name: math_agent Tool Calls: multiply (call_IBXYtlEMdZrfDZ8g8bWC31pM) Call ID: call_IBXYtlEMdZrfDZ8g8bWC31pM Args: a: 8 b: 7 Update from node tools: ================================= Tool Message ================================= Name: multiply 56.0 Update from node agent: ================================== Ai Message ================================== Name: math_agent 56 2. 使用 langgraph-supervisor 建立監督者 from langgraph_supervisor import create_supervisor from langchain.chat_models import init_chat_model supervisor = create_supervisor( model=init_chat_model("openai:gpt-4.1"), agents=[research_agent, math_agent], prompt=( "你是專案經理,負責協調和管理專業團隊。\n\n" "【團隊架構】\n" "研究部門代理:\n" "• 職責:資訊研究、市場分析、數據收集\n" "• 適用:需要搜索、調查、分析的任務\n\n" "計算部門代理:\n" "• 職責:數學運算、數值計算、公式處理\n" "• 適用:需要計算、運算、數學分析的任務\n\n" "【管理準則】\n" "1. 任務分配:\n" " - 仔細分析任務需求\n" " - 選擇最適合的代理執行\n" " - 確保任務分配準確無誤\n\n" "2. 執行原則:\n" " - 串行執行,不並行調用\n" " - 監督但不干預執行過程\n" " - 專注於管理協調工作\n\n" "3. 目標導向:\n" " - 確保任務高效完成\n" " - 維護團隊協作順暢\n" " - 提供最佳解決方案" ), add_handoff_back_messages=True, output_mode="full_history", ).compile() from IPython.display import display, Image display(Image(supervisor.get_graph().draw_mermaid_png())) 研究代理將查找必要的 GDP 信息 數學代理將執行除法以找出紐約州 GDP 的百分比,根據要求 for chunk in supervisor.stream( { "messages": [ { "role": "user", "content": "find US and New York state GDP in 2024. what % of US GDP was New York state?", } ] }, ): pretty_print_messages(chunk, last_message=True) final_message_history = chunk["supervisor"]["messages"] Update from node supervisor: ================================= Tool Message ================================= Name: transfer_to_research_agent Successfully transferred to research_agent Update from node research_agent: ================================= Tool Message ================================= Name: transfer_back_to_supervisor Successfully transferred back to supervisor Update from node supervisor: ================================= Tool Message ================================= Name: transfer_to_math_agent Successfully transferred to math_agent Update from node math_agent: ================================= Tool Message ================================= Name: transfer_back_to_supervisor Successfully transferred back to supervisor Update from node supervisor: ================================== Ai Message ================================== Name: supervisor The US GDP in 2024 was $29.017 trillion, and New York State's GDP was $2.284 trillion. New York State accounted for approximately 7.87% of the total US GDP in 2024. 3. 從頭開始創建監督者 設置主管如何與個別代理溝通 創建監督代理人 將監督代理和工作代理合併為單一多代理圖。 設置代理通信 我們需要定義一種方法讓監督代理與工作代理進行溝通。在多代理架構中,實現這一點的一種常見方法是使用交接,讓一個代理將控制權交給另一個代理。交接允許您指定: destination: target agent to transfer to目的地:目標代理以轉移至 payload: information to pass to that agentpayload: 傳遞給該代理的信息 我們將通過交接工具實施交接,並將這些工具交給監督代理:當監督代理調用這些工具時,將把控制權交給工作代理,並將完整的消息歷史傳遞給該代理。 from typing import Annotated from langchain_core.tools import tool, InjectedToolCallId from langgraph.prebuilt import InjectedState from langgraph.graph import StateGraph, START, MessagesState from langgraph.types import Command def create_handoff_tool(agent_name: str, description: str = None): """創建任務交接工具""" name = f"transfer_to_{agent_name}" description = description or f"將任務交給 {agent_name}" @tool(name, description=description) def handoff_tool( state: Annotated[MessagesState, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId], ) -> Command: return Command( goto=agent_name, update={ **state, "messages": state["messages"] + [{ "role": "tool", "content": f"已轉交給 {agent_name}", "name": name, "tool_call_id": tool_call_id, }] }, graph=Command.PARENT, ) return handoff_tool # 交接工具 assign_to_research_agent = create_handoff_tool( "research_agent", "分配研究任務" ) assign_to_math_agent = create_handoff_tool( "math_agent", "分配計算任務" ) 創建監督代理人 然後,讓我們使用剛剛定義的交接工具來創建監督代理。我們將使用預建的create_react_agent: supervisor_agent = create_react_agent( model="openai:gpt-4.1", tools=[assign_to_research_agent, assign_to_math_agent], prompt=( "你是團隊主管,負責協調和管理專業代理團隊。\n\n" "【團隊成員】\n" "1. 研究代理:專門處理研究相關任務\n" " - 信息搜索、資料收集\n" " - 市場分析、競爭調研\n" " - 文獻查找、數據驗證\n\n" "2. 數學代理:專門處理計算相關任務\n" " - 數學運算、公式計算\n" " - 統計分析、數值處理\n" " - 財務計算、數據建模\n\n" "【工作原則】\n" "- 根據任務性質分配給合適的代理\n" "- 一次只委派給一個代理,確保專注執行\n" "- 作為主管只負責任務分配和協調\n" "- 不親自執行具體工作任務" ), name="supervisor", ) 創建多代理圖形 把這一切結合在一起,讓我們為整體多代理系統創建一個圖。我們將添加監督者和個別代理作為子圖節點。 from langgraph.graph import END # Define the multi-agent supervisor graph supervisor = ( StateGraph(MessagesState) # NOTE: `destinations` is only needed for visualization and doesn't affect runtime behavior .add_node(supervisor_agent, destinations=("research_agent", "math_agent", END)) .add_node(research_agent) .add_node(math_agent) .add_edge(START, "supervisor") # always return back to the supervisor .add_edge("research_agent", "supervisor") .add_edge("math_agent", "supervisor") .compile() ) 請注意,我們已經添加了從工作代理返回到監督者的明確邊緣——這意味著他們保證將控制權返回給監督者。如果您希望代理直接回應用戶(即將系統變成路由器,您可以移除這些邊緣)。 from IPython.display import display, Image display(Image(supervisor.get_graph().draw_mermaid_png())) for chunk in supervisor.stream( { "messages": [ { "role": "user", "content": "find US and New York state GDP in 2024. what % of US GDP was New York state?", } ] }, ): pretty_print_messages(chunk, last_message=True) final_message_history = chunk["supervisor"]["messages"] Update from node supervisor: ================================= Tool Message ================================= Name: transfer_to_research_agent Successfully transferred to research_agent Update from node research_agent: ================================== Ai Message ================================== Name: research_agent - US GDP in 2024: $29.017 trillion (nominal, current prices) - New York state GDP in 2024: $2.284 trillion - New York state's share of US GDP in 2024: 7.87% Update from node supervisor: ================================= Tool Message ================================= Name: transfer_to_math_agent Successfully transferred to math_agent Update from node math_agent: ================================== Ai Message ================================== Name: math_agent US GDP in 2024: $29.017 trillion New York state GDP in 2024: $2.284 trillion New York state's share of US GDP: 7.87% Update from node supervisor: ================================== Ai Message ================================== Name: supervisor US GDP in 2024 was $29.017 trillion. New York state GDP in 2024 was $2.284 trillion. New York state's GDP was about 7.87% of the US GDP. 完整檢視: for message in final_message_history: message.pretty_print() ================================ Human Message ================================= find US and New York state GDP in 2024. what % of US GDP was New York state? ================================== Ai Message ================================== Name: supervisor Tool Calls: transfer_to_research_agent (call_qxk9abrxWYQT6a9hPNpXiuM0) Call ID: call_qxk9abrxWYQT6a9hPNpXiuM0 Args: ================================= Tool Message ================================= Name: transfer_to_research_agent Successfully transferred to research_agent ================================== Ai Message ================================== Name: research_agent Tool Calls: tavily_search (call_A3cVm1BXDD8dYv6uLwO132gg) Call ID: call_A3cVm1BXDD8dYv6uLwO132gg Args: query: US GDP 2024 search_depth: advanced tavily_search (call_77JyoUYwGDXlRNKOwvQFUUYJ) Call ID: call_77JyoUYwGDXlRNKOwvQFUUYJ Args: query: New York state GDP 2024 search_depth: advanced ================================= Tool Message ================================= Name: tavily_search {"query": "US GDP 2024", "follow_up_questions": null, "answer": null, "images": [], "results": [{"url": "https://tradingeconomics.com/united-states/gdp-growth", "title": "United States GDP Growth Rate - Trading Economics", "content": "The US economy expanded an annualized 2.4% in Q4 2024, slightly higher than 2.3% in the previous estimates, primarily reflecting a downward revision to imports. Exports fell slightly less (-0.2% vs -0.5%) and imports declined more than initially anticipated (-1.9% vs -1.2%), leaving the contribution from net trade positive at 0.26 pp (vs 0.12 pp). Government expenditure also rose more (3.1% vs 2.9%) and fixed investment contracted less (-1.1% vs -1.4%), due to equipment (-8.7% vs -9%) while [...] The Gross Domestic Product (GDP) in the United States expanded 2.40 percent in the fourth quarter of 2024 over the previous quarter. GDP Growth Rate in the United States averaged 3.21 percent from 1947 until 2024, reaching an all time high of 35.20 percent in the third quarter of 2020 and a record low of -28.10 percent in the second quarter of 2020. This page provides the latest reported value for - United States GDP Growth Rate - plus previous releases, historical high and low, short-term [...] The Gross Domestic Product (GDP) in the United States expanded 2.40 percent in the fourth quarter of 2024 over the previous quarter. GDP Growth Rate in the United States is expected to be 0.90 percent by the end of this quarter, according to Trading Economics global macro models and analysts expectations. In the long-term, the United States GDP Growth Rate is projected to trend around 2.00 percent in 2026, according to our econometric models.\n%\n3Y5Y10YMAX\nExport API\nOK\nLoading...", "score": 0.9071234, "raw_content": null}, {"url": "https://www.bea.gov/data/gdp/gross-domestic-product", "title": "Gross Domestic Product | U.S. Bureau of Economic Analysis (BEA)", "content": "Real gross domestic product (GDP) increased at an annual rate of 2.4 percent in the fourth quarter of 2024 (October, November, and December), according to the third estimate released by the U.S. Bureau of Economic Analysis. In the third quarter, real GDP increased 3.1 percent. The increase in real GDP in the fourth quarter primarily reflected increases in consumer spending and government spending that were partly offset by a decrease in investment. Imports, which are a subtraction in the", "score": 0.9008183, "raw_content": null}, {"url": "https://www.nerdwallet.com/article/finance/gdp-report", "title": "GDP Report: Final Estimate Shows Growth Increased 2.4% in Q4 2024", "content": "NerdWallet's content is fact-checked for accuracy, timeliness and relevance. It undergoes a thorough review process involving writers and editors to ensure the information is as clear and complete as possible.\n\nAnna Helhoski\n\nRick VanderKnyff\n\nUpdated on April 7\n\nReal gross domestic product increased by an annual rate of 2.4% in the fourth quarter of 2024, according to the third estimate report released on March 27 by the Bureau of Economic Analysis. [...] The third estimate also showed that in 2024, the U.S. GDP grew 2.8% compared to a 2.9% increase in 2023.\n\nQ1 2025 GDP forecasts are negative\n\nA forecasting tool published by the Atlanta Federal Reserve shows negative growth for the first quarter of 2025; it expects gross domestic product (GDP) to contract by rate of -2.8%, according to data released on March 28. If the forecast is correct, it would be the first quarter where GDP declined since the first and second quarters of 2022. [...] GDP could be the next concern: On March 3, a forecasting tool published by the Atlanta Federal Reserve showed that GDP could contract by a rate of -2.8% in the first quarter of 2025. If GDP does indeed decline, it would be the first time since the first and second quarters of 2022.\n\nIn 2024, the U.S. GDP grew 2.8% compared to a 2.9% increase in 2023, according to a second estimate of real gross domestic product from the Bureau of Economic Analysis, released Feb. 27.", "score": 0.89053273, "raw_content": null}], "response_time": 1.78} ================================= Tool Message ================================= Name: tavily_search {"query": "New York state GDP 2024", "follow_up_questions": null, "answer": null, "images": [], "results": [{"url": "https://en.wikipedia.org/wiki/Economy_of_New_York_(state)", "title": "Economy of New York (state) - Wikipedia", "content": "Jump to content\nMain menu\nSearch\nDonate\nCreate account\nLog in\nPersonal tools\nToggle the table of contents\nEconomy of New York (state)\n1 language\nArticle\nTalk\nRead\nEdit\nView history\nTools\nFrom Wikipedia, the free encyclopedia\nThis article is about the overall economy of New York State. For the economy of New York City, see Economy of New York City.\nEconomy of New York\nNew York City, the economic capital of New York (state)\nStatistics\nGDP $2.3 trillion (2024)[1]\nGDP per capita $117,332 (2024)[2] [...] The economy of the State of New York is reflected in its gross state product in 2024 of $2.284 trillion, ranking third in size behind the larger states of California and Texas. If New York State were an independent nation, it would rank as the 10th largest economy in the world by nominal GDP. However, in 2019, the multi-state, New York City-centered metropolitan statistical area produced a gross metropolitan product (GMP) of $US2.0 trillion, ranking first nationally by a wide margin and would [...] Population below poverty line 13.6%[3]\nGini coefficient 0.5157 ± 0.0029 (2023)[4]\nLabour force 9,645,984 (2023)[5]\nUnemployment 4.4% (August 2024)[6]\nPublic finances\nRevenues $63.5 billion[7]\nExpenses $54.6 billion[8]", "score": 0.9511106, "raw_content": null}, {"url": "https://en.wikipedia.org/wiki/List_of_U.S._states_and_territories_by_GDP", "title": "List of U.S. states and territories by GDP - Wikipedia", "content": "GDP per capita also varied widely throughout the United States in 2024, with New York ($117,332), Massachusetts ($110,561), and Washington (state) ($108,468) recording the three highest GDP per capita figures in the U.S., while Mississippi ($53,061), Arkansas ($60,276), and West Virginia ($60,783) recorded the three lowest GDP per capita figures in the U.S. The District of Columbia, though, recorded a GDP per capita figure far higher than any U.S. state in 2024 at $263,220. [...] Overall, in the calendar year 2024, the United States' Nominal GDP at Current Prices totaled at $29.017 trillion, as compared to $25.744 trillion in 2022.\nThe three U.S. states with the highest GDPs were California ($4.080 trillion), Texas ($2.695 trillion), and New York ($2.284 trillion). The three U.S. states with the lowest GDPs were Vermont ($45.4 billion), Wyoming ($53.0 billion), and Alaska ($69.8 billion).", "score": 0.8947989, "raw_content": null}, {"url": "https://edc.nyc/sites/default/files/2025-01/NYCEDC-State-of-the-NYC-Economy-2024-v3.pdf", "title": "[PDF] State of the New York City Economy - NYCEDC", "content": "for talent faced a limited supply. STATE OF THE NEW YORK CITY ECONOMY 2024 / 21 STATE OF THE NEW YORK CITY ECONOMY 2024 / 22 After losing nearly a million jobs during the COVID-19 pandemic, New York City is now at record-high levels of private and total employment. The city’s gross city product (GCP) stands at $1.18 trillion as of 2023.24 While legacy sectors such as Finance and Insurance have continued to play a vital part in the city’s economic successes, emerging sectors like Tech, the Green [...] STATE OF THE NEW YORK CITY ECONOMY 2024 / 11 New York City’s economy is the largest in the nation, with $2 trillion in gross metropolitan product (GMP) for the metro area, representing 9% of the total US economy. As such, the city’s economy is closely intertwined with the broader national economic landscape, and US macroeconomic conditions play a significant role in shaping the economic backdrop for the city. National interest rates, inflation, gross domestic product (GDP), and employment [...] 1 Macro Trends STATE OF THE NEW YORK CITY ECONOMY 2024 / 12 Output Has Grown Faster in NYC than Nationally Since 2022 Actual and Projected US Real GDP and NYC Real GCP Growth Rates 2019 2020 2021 2022 2023 2024* 2025* NYC 2.6% -4.2% 5.5% 2.6% 2.8% 3.6% 1.9% US 2.5% -2.2% 5.8% 1.9% 2.5% 2.5% 1.4% Source: NYC OMB and US Bureau of Economic Analysis. Projections for 2024 and and 2025 from NYC OMB.", "score": 0.85797083, "raw_content": null}], "response_time": 0.63} ================================== Ai Message ================================== Name: research_agent - US GDP in 2024: $29.017 trillion (nominal, current prices) - New York state GDP in 2024: $2.284 trillion - New York state's share of US GDP in 2024: 7.87% ================================== Ai Message ================================== Name: supervisor Tool Calls: transfer_to_math_agent (call_m5ICqaoAtRXHWb8BI3638dJL) Call ID: call_m5ICqaoAtRXHWb8BI3638dJL Args: ================================= Tool Message ================================= Name: transfer_to_math_agent Successfully transferred to math_agent ================================== Ai Message ================================== Name: math_agent Tool Calls: divide (call_Sf7nvyvEgIaoDlaioPmkCdqz) Call ID: call_Sf7nvyvEgIaoDlaioPmkCdqz Args: a: 2.284 b: 29.017 ================================= Tool Message ================================= Name: divide 0.07871247889168417 ================================== Ai Message ================================== Name: math_agent US GDP in 2024: $29.017 trillion New York state GDP in 2024: $2.284 trillion New York state's share of US GDP: 7.87% ================================== Ai Message ================================== Name: supervisor US GDP in 2024 was $29.017 trillion. New York state GDP in 2024 was $2.284 trillion. New York state's GDP was about 7.87% of the US GDP. 您可以看到監督系統將所有個別代理的訊息(即它們的內部工具調用循環)附加到完整的訊息歷史中。這意味著在每次監督者回合中,監督代理都會看到這個完整的歷史。如果您想要更多控制: how inputs are passed to agents: you can use LangGraph Send() primitive to directly send data to the worker agents during the handoff. See the task delegation example below如何將輸入傳遞給代理:您可以使用 LangGraph Send() 原語在交接期間直接將數據發送給工作代理。請參閱下面的任務委派示例。 how agent outputs are added: you can control how much of the agent's internal message history is added to the overall supervisor message history by wrapping the agent in a separate node function:如何添加代理輸出:您可以通過將代理包裝在一個單獨的節點函數中來控制代理的內部消息歷史添加到整體監督者消息歷史中的程度: def call_research_agent(state): # return agent's final response, # excluding inner monologue response = research_agent.invoke(state) return {"messages": response["messages"][-1]} 4. 建立委派任務 到目前為止,個別代理依賴於解釋完整的消息歷史來確定他們的任務。另一種方法是要求主管明確制定任務。我們可以通過向handoff_tool函數添加task_description參數來實現。 from langgraph.types import Send def create_task_description_handoff_tool( *, agent_name: str, description: str | None = None ): name = f"transfer_to_{agent_name}" description = description or f"Ask {agent_name} for help." @tool(name, description=description) def handoff_tool( # this is populated by the supervisor LLM task_description: Annotated[ str, "Description of what the next agent should do, including all of the relevant context.", ], # these parameters are ignored by the LLM state: Annotated[MessagesState, InjectedState], ) -> Command: task_description_message = {"role": "user", "content": task_description} agent_input = {**state, "messages": [task_description_message]} return Command( goto=[Send(agent_name, agent_input)], graph=Command.PARENT, ) return handoff_tool assign_to_research_agent_with_description = create_task_description_handoff_tool( agent_name="research_agent", description="Assign task to a researcher agent.", ) assign_to_math_agent_with_description = create_task_description_handoff_tool( agent_name="math_agent", description="Assign task to a math agent.", ) supervisor_agent_with_description = create_react_agent( model="openai:gpt-4.1", tools=[ assign_to_research_agent_with_description, assign_to_math_agent_with_description, ], prompt=( "You are a supervisor managing two agents:\n" "- a research agent. Assign research-related tasks to this assistant\n" "- a math agent. Assign math-related tasks to this assistant\n" "Assign work to one agent at a time, do not call agents in parallel.\n" "Do not do any work yourself." ), name="supervisor", ) supervisor_with_description = ( StateGraph(MessagesState) .add_node( supervisor_agent_with_description, destinations=("research_agent", "math_agent") ) .add_node(research_agent) .add_node(math_agent) .add_edge(START, "supervisor") .add_edge("research_agent", "supervisor") .add_edge("math_agent", "supervisor") .compile() ) 我們在handoff_tool中使用Send()原語。這意味著每個工作代理僅看到Send有效負載的內容,而不是接收完整的supervisor圖狀態作為輸入。在此示例中,我們將任務描述作為單一的「人類」消息發送。 現在讓我們用相同的輸入查詢來運行它: for chunk in supervisor.stream( { "messages": [ { "role": "user", "content": "find US and New York state GDP in 2024. what % of US GDP was New York state?", } ] }, subgraphs=True, ): pretty_print_messages(chunk, last_message=True) Update from subgraph supervisor: Update from node agent: ================================== Ai Message ================================== Name: supervisor Tool Calls: transfer_to_research_agent (call_TtKzjGQBe4X9Xh0VzmjStVgZ) Call ID: call_TtKzjGQBe4X9Xh0VzmjStVgZ Args: Update from subgraph research_agent: Update from node agent: ================================== Ai Message ================================== Name: research_agent Tool Calls: tavily_search (call_AfeRYBJxJtmD4EKqifYcx8EI) Call ID: call_AfeRYBJxJtmD4EKqifYcx8EI Args: query: US GDP in 2024 search_depth: advanced tavily_search (call_n7Dn8QnDLu2ZpEDzswS2MOJ8) Call ID: call_n7Dn8QnDLu2ZpEDzswS2MOJ8 Args: query: New York state GDP in 2024 search_depth: advanced Update from subgraph research_agent: Update from node tools: ================================= Tool Message ================================= Name: tavily_search {"query": "New York state GDP in 2024", "follow_up_questions": null, "answer": null, "images": [], "results": [{"url": "https://en.wikipedia.org/wiki/Economy_of_New_York_(state)", "title": "Economy of New York (state) - Wikipedia", "content": "Jump to content\nMain menu\nSearch\nDonate\nCreate account\nLog in\nPersonal tools\nToggle the table of contents\nEconomy of New York (state)\n1 language\nArticle\nTalk\nRead\nEdit\nView history\nTools\nFrom Wikipedia, the free encyclopedia\nThis article is about the overall economy of New York State. For the economy of New York City, see Economy of New York City.\nEconomy of New York\nNew York City, the economic capital of New York (state)\nStatistics\nGDP $2.3 trillion (2024)[1]\nGDP per capita $117,332 (2024)[2] [...] The economy of the State of New York is reflected in its gross state product in 2024 of $2.284 trillion, ranking third in size behind the larger states of California and Texas. If New York State were an independent nation, it would rank as the 10th largest economy in the world by nominal GDP. However, in 2019, the multi-state, New York City-centered metropolitan statistical area produced a gross metropolitan product (GMP) of $US2.0 trillion, ranking first nationally by a wide margin and would [...] Population below poverty line 13.6%[3]\nGini coefficient 0.5157 ± 0.0029 (2023)[4]\nLabour force 9,645,984 (2023)[5]\nUnemployment 4.4% (August 2024)[6]\nPublic finances\nRevenues $63.5 billion[7]\nExpenses $54.6 billion[8]", "score": 0.9530353, "raw_content": null}, {"url": "https://en.wikipedia.org/wiki/List_of_U.S._states_and_territories_by_GDP", "title": "List of U.S. states and territories by GDP - Wikipedia", "content": "GDP per capita also varied widely throughout the United States in 2024, with New York ($117,332), Massachusetts ($110,561), and Washington (state) ($108,468) recording the three highest GDP per capita figures in the U.S., while Mississippi ($53,061), Arkansas ($60,276), and West Virginia ($60,783) recorded the three lowest GDP per capita figures in the U.S. The District of Columbia, though, recorded a GDP per capita figure far higher than any U.S. state in 2024 at $263,220. [...] Overall, in the calendar year 2024, the United States' Nominal GDP at Current Prices totaled at $29.017 trillion, as compared to $25.744 trillion in 2022.\nThe three U.S. states with the highest GDPs were California ($4.080 trillion), Texas ($2.695 trillion), and New York ($2.284 trillion). The three U.S. states with the lowest GDPs were Vermont ($45.4 billion), Wyoming ($53.0 billion), and Alaska ($69.8 billion).", "score": 0.89997756, "raw_content": null}, {"url": "https://edc.nyc/sites/default/files/2025-01/NYCEDC-State-of-the-NYC-Economy-2024-v3.pdf", "title": "[PDF] State of the New York City Economy - NYCEDC", "content": "for talent faced a limited supply. STATE OF THE NEW YORK CITY ECONOMY 2024 / 21 STATE OF THE NEW YORK CITY ECONOMY 2024 / 22 After losing nearly a million jobs during the COVID-19 pandemic, New York City is now at record-high levels of private and total employment. The city’s gross city product (GCP) stands at $1.18 trillion as of 2023.24 While legacy sectors such as Finance and Insurance have continued to play a vital part in the city’s economic successes, emerging sectors like Tech, the Green [...] STATE OF THE NEW YORK CITY ECONOMY 2024 / 11 New York City’s economy is the largest in the nation, with $2 trillion in gross metropolitan product (GMP) for the metro area, representing 9% of the total US economy. As such, the city’s economy is closely intertwined with the broader national economic landscape, and US macroeconomic conditions play a significant role in shaping the economic backdrop for the city. National interest rates, inflation, gross domestic product (GDP), and employment [...] 1 Macro Trends STATE OF THE NEW YORK CITY ECONOMY 2024 / 12 Output Has Grown Faster in NYC than Nationally Since 2022 Actual and Projected US Real GDP and NYC Real GCP Growth Rates 2019 2020 2021 2022 2023 2024* 2025* NYC 2.6% -4.2% 5.5% 2.6% 2.8% 3.6% 1.9% US 2.5% -2.2% 5.8% 1.9% 2.5% 2.5% 1.4% Source: NYC OMB and US Bureau of Economic Analysis. Projections for 2024 and and 2025 from NYC OMB.", "score": 0.8616433, "raw_content": null}], "response_time": 2.35} Update from subgraph research_agent: Update from node agent: ================================== Ai Message ================================== Name: research_agent - US GDP in 2024 was $29.017 trillion (nominal, current prices) ([source](https://en.wikipedia.org/wiki/List_of_U.S._states_and_territories_by_GDP)). - New York State GDP in 2024 was $2.284 trillion ([source](https://en.wikipedia.org/wiki/Economy_of_New_York_(state))). Percentage of US GDP attributed to New York State in 2024: approximately 7.9%. Update from subgraph supervisor: Update from node agent: ================================== Ai Message ================================== Name: supervisor Tool Calls: transfer_to_math_agent (call_oYbIXhQQeTWlj2zvZSoStUgO) Call ID: call_oYbIXhQQeTWlj2zvZSoStUgO Args: Update from subgraph math_agent: Update from node agent: ================================== Ai Message ================================== Name: math_agent Tool Calls: divide (call_K5QxmkQYFfCZw5Vzkbz43VIG) Call ID: call_K5QxmkQYFfCZw5Vzkbz43VIG Args: a: 2.284 b: 29.017 Update from subgraph math_agent: Update from node tools: ================================= Tool Message ================================= Name: divide 0.07871247889168417 Update from subgraph math_agent: Update from node agent: ================================== Ai Message ================================== Name: math_agent New York state's GDP was approximately 7.87% of US GDP in 2024. Update from subgraph supervisor: Update from node agent: ================================== Ai Message ================================== Name: supervisor Here are the findings: - US GDP in 2024: $29.017 trillion - New York State GDP in 2024: $2.284 trillion - New York State accounted for approximately 7.87% of US GDP in 2024. 試試看台灣的經濟股市相關問題!! import os import getpass from langchain_tavily import TavilySearch from langgraph.prebuilt import create_react_agent from langchain.chat_models import init_chat_model from typing import Annotated from langchain_core.tools import tool, InjectedToolCallId from langgraph.prebuilt import InjectedState from langgraph.graph import StateGraph, START, END, MessagesState from langgraph.types import Command from langchain_core.messages import HumanMessage # 設置 API 金鑰 if not os.environ.get("OPENAI_API_KEY"): os.environ["OPENAI_API_KEY"] = getpass.getpass("請輸入 OpenAI API Key: ") if not os.environ.get("TAVILY_API_KEY"): os.environ["TAVILY_API_KEY"] = getpass.getpass("請輸入 Tavily API Key: ") # 創建研究工具 web_search = TavilySearch(max_results=3) # 創建數學工具 - 使用英文函數名但中文說明 @tool def add(a: float, b: float) -> float: """Add two numbers (將兩個數字相加)""" return a + b @tool def multiply(a: float, b: float) -> float: """Multiply two numbers (將兩個數字相乘)""" return a * b @tool def divide(a: float, b: float) -> float: """Divide two numbers (將兩個數字相除)""" if b == 0: return "錯誤:除數不能為零" return a / b @tool def calculate_growth_rate(initial: float, final: float) -> float: """Calculate growth rate (計算成長率)""" if initial == 0: return "錯誤:初始值不能為零" return ((final - initial) / initial) * 100 @tool def calculate_percentage(part: float, whole: float) -> float: """Calculate percentage (計算百分比)""" if whole == 0: return "錯誤:總數不能為零" return (part / whole) * 100 # 創建研究代理 research_agent = create_react_agent( model="gpt-4", tools=[web_search], prompt=( "你是一個經濟研究助理。\n\n" "任務說明:\n" "- 只協助處理經濟數據查詢、市場調查等研究任務\n" "- 不要進行任何數學計算,計算任務請交給數學代理\n" "- 完成任務後,直接向主管回報研究結果\n" "- 只回報研究發現,不要加入其他無關文字" ), name="research_agent", ) # 創建數學代理 math_agent = create_react_agent( model="gpt-4", tools=[add, multiply, divide, calculate_growth_rate, calculate_percentage], prompt=( "你是一個數學計算專家。\n\n" "任務說明:\n" "- 只處理數學計算、統計分析等任務\n" "- 專注於財經數據的計算(如成長率、百分比、比例等)\n" "- 完成計算後,直接向主管回報結果\n" "- 只回報計算結果,不要加入其他解釋" ), name="math_agent", ) # 創建任務交接工具 - 保持英文名稱 def create_handoff_tool(*, agent_name: str, description: str | None = None): name = f"transfer_to_{agent_name}" description = description or f"Transfer task to {agent_name}" @tool(name, description=description) def handoff_tool( state: Annotated[MessagesState, InjectedState], tool_call_id: Annotated[str, InjectedToolCallId], ) -> Command: tool_message = { "role": "tool", "content": f"任務已成功轉交給 {agent_name}", "name": name, "tool_call_id": tool_call_id, } return Command( goto=agent_name, update={**state, "messages": state["messages"] + [tool_message]}, graph=Command.PARENT, ) return handoff_tool # 創建交接工具 assign_to_research_agent = create_handoff_tool( agent_name="research_agent", description="Assign economic research tasks to research assistant (將經濟研究任務分配給研究助理)", ) assign_to_math_agent = create_handoff_tool( agent_name="math_agent", description="Assign calculation tasks to math expert (將數學計算任務分配給計算專家)", ) # 創建監督代理(主管) supervisor_agent = create_react_agent( model="gpt-4", tools=[assign_to_research_agent, assign_to_math_agent], prompt=( "你是一位專案經理,管理兩個專業團隊:\n" "- 研究助理:負責查詢經濟數據、市場資訊等研究任務\n" "- 計算專家:負責數學計算、財務分析等計算任務\n\n" "工作規則:\n" "- 一次只分配任務給一個團隊,不要同時派工\n" "- 你只負責分配任務,不要親自執行工作\n" "- 根據任務性質選擇適合的團隊" ), name="supervisor", ) # 創建多代理系統圖 supervisor = ( StateGraph(MessagesState) .add_node(supervisor_agent) .add_node(research_agent) .add_node(math_agent) .add_edge(START, "supervisor") .add_edge("research_agent", "supervisor") .add_edge("math_agent", "supervisor") .compile() ) # 執行台灣相關的經濟問題查詢 print("=== 開始執行多代理系統 ===") # 測試簡單問題 simple_query = "台灣2023年GDP是多少?如果成長3%,2024年會是多少?" print(f"\n測試問題: {simple_query}") print("-" * 50) try: # 使用 invoke 而不是 stream 來避免錯誤 result = supervisor.invoke( {"messages": [HumanMessage(content=simple_query)]} ) # 顯示結果 print("\n執行結果:") for i, msg in enumerate(result["messages"]): msg_type = type(msg).__name__ content = getattr(msg, 'content', str(msg)) if content and not content.startswith("任務已成功轉交"): print(f"{i+1}. {msg_type}: {content[:200]}...") except Exception as e: print(f"執行錯誤: {type(e).__name__}: {e}") # 更多台灣經濟問題範例 taiwan_economic_queries = [ "查詢台積電最新股價,如果我有100萬元,可以買幾張?", "台北市平均房價每坪80萬,新北市每坪50萬,價差是多少百分比?", "如果美元兌台幣是31.5,歐元兌台幣是34.2,1萬歐元可以換多少美元?", "台灣基本工資月薪27,470元,如果明年調漲5%,會是多少?", ] # 執行一個範例 print(f"\n\n執行範例查詢: {taiwan_economic_queries[0]}") print("-" * 50) try: result2 = supervisor.invoke( {"messages": [HumanMessage(content=taiwan_economic_queries[0])]} ) # 只顯示重要訊息 for msg in result2["messages"]: if hasattr(msg, 'content') and msg.content: content = msg.content if any(keyword in content for keyword in ["研究結果", "計算結果", "股價", "張"]): print(f"→ {content}") except Exception as e: print(f"執行錯誤: {type(e).__name__}: {e}") # 簡化的直接測試 print("\n\n=== 簡化測試 ===") try: # 直接測試數學代理 math_result = math_agent.invoke({ "messages": [HumanMessage(content="計算 100萬除以600 (假設台積電股價600元)")] }) print("數學代理測試成功") # 直接測試研究代理 research_result = research_agent.invoke({ "messages": [HumanMessage(content="查詢台積電最新股價")] }) print("研究代理測試成功") except Exception as e: print(f"測試錯誤: {type(e).__name__}: {e}")
-
建立一個SQL代理
🤖 使用 LangGraph 建構 SQL Agent:從零到生產的完整指南 在現代數據驅動的世界中,讓機器理解自然語言並自動生成 SQL 查詢已經成為一個重要需求。本文將帶您從頭開始建構一個強大的 SQL Agent,它能夠接受自然語言問題,自動分析資料庫結構,生成並執行 SQL 查詢,最後提供易懂的答案。 🎯 什麼是 SQL Agent? SQL Agent 是一個智能代理程序,能夠: 接受自然語言查詢 分析資料庫結構 自動生成 SQL 語句 執行查詢並處理錯誤 提供人類可讀的回應 讓我們開始實作吧! ⚙️ 環境設置 首先,我們需要安裝必要的套件: # 安裝必要的套件 !pip install -q langgraph langchain_community langchain-openai requests sqlite3 # 匯入必要的模組 import os import requests import sqlite3 from langchain_community.utilities import SQLDatabase from langchain_community.agent_toolkits import SQLDatabaseToolkit from langchain_openai import ChatOpenAI from langgraph.prebuilt import create_react_agent from langchain_core.messages import HumanMessage # 設置 OpenAI API Key os.environ["OPENAI_API_KEY"] = "your-openai-api-key-here" # 請替換為您的 API Key print("✅ 套件安裝完成!") 🗄️ 準備示例資料庫 我們將使用 Chinook 資料庫,這是一個代表數位媒體商店的示例資料庫: # 下載 Chinook 示例資料庫 def download_chinook_db(): url = "https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db" print("📥 正在下載 Chinook 資料庫...") response = requests.get(url) if response.status_code == 200: with open("Chinook.db", "wb") as file: file.write(response.content) print("✅ 資料庫下載成功!") return True else: print(f"❌ 下載失敗,狀態碼:{response.status_code}") return False # 執行下載 if download_chinook_db(): # 建立資料庫連接 db = SQLDatabase.from_uri("sqlite:///Chinook.db") print(f"📊 資料庫方言:{db.dialect}") print(f"📋 可用表格:{db.get_usable_table_names()}") # 查看示例資料 sample_query = "SELECT * FROM Artist LIMIT 5;" result = db.run(sample_query) print(f"🎵 藝術家表格示例:\n{result}") 🔧 初始化 LLM 和工具 接下來,我們初始化語言模型和資料庫互動工具: # 初始化語言模型 def initialize_llm(): """初始化 ChatOpenAI 模型""" try: llm = ChatOpenAI( model="gpt-4", temperature=0, # 降低隨機性以獲得更一致的結果 ) # 測試連接 test_response = llm.invoke([HumanMessage(content="Hello!")]) print("✅ LLM 初始化成功!") return llm except Exception as e: print(f"❌ LLM 初始化失敗:{e}") return None # 建立資料庫工具包 def create_database_toolkit(db, llm): """建立 SQL 資料庫工具包""" toolkit = SQLDatabaseToolkit(db=db, llm=llm) tools = toolkit.get_tools() print("🛠️ 可用工具:") for i, tool in enumerate(tools, 1): print(f"{i}. {tool.name}: {tool.description[:100]}...") return tools # 執行初始化 llm = initialize_llm() if llm: tools = create_database_toolkit(db, llm) 🚀 建構預建 SQL Agent 讓我們首先建立一個預建的 SQL Agent 來快速開始: # 定義系統提示 def create_system_prompt(): """建立系統提示""" return f""" 你是一個專門與 SQL 資料庫互動的智能代理。 給定一個輸入問題,請建立語法正確的 {db.dialect} 查詢來執行, 然後查看查詢結果並返回答案。 重要指示: 1. 除非用戶指定特定數量,否則請將查詢限制在最多 5 個結果 2. 可以按相關欄位排序以返回最有趣的示例 3. 永遠不要查詢表格的所有欄位,只查詢與問題相關的欄位 4. 執行查詢前務必檢查查詢的正確性 5. 如果執行查詢時出錯,請重寫查詢並重試 6. 不要執行任何 DML 語句(INSERT、UPDATE、DELETE、DROP 等) 7. 開始時應該總是查看資料庫中的表格以了解可以查詢什麼 8. 然後查詢最相關表格的 schema 記住:安全第一,準確第二! """ # 建立預建 Agent def create_prebuilt_agent(llm, tools): """建立預建的 React Agent""" system_prompt = create_system_prompt() agent = create_react_agent( llm, tools, prompt=system_prompt, ) print("🤖 預建 SQL Agent 建立成功!") return agent # 執行 Agent 建立 if llm and tools: prebuilt_agent = create_prebuilt_agent(llm, tools) 🧪 測試預建 Agent 現在讓我們測試我們的預建 Agent: # 定義測試函數 def test_sql_agent(agent, question): """測試 SQL Agent 的功能""" print(f"❓ 問題:{question}") print("=" * 50) try: # 執行查詢 messages = [{"role": "user", "content": question}] # 串流執行並顯示過程 step_count = 0 for step in agent.stream({"messages": messages}, stream_mode="values"): step_count += 1 last_message = step["messages"][-1] print(f"\n📍 步驟 {step_count}:") if hasattr(last_message, 'content'): if last_message.content: print(f"💬 回應:{last_message.content[:200]}...") if hasattr(last_message, 'tool_calls') and last_message.tool_calls: for tool_call in last_message.tool_calls: print(f"🔧 工具呼叫:{tool_call.get('name', 'Unknown')}") print("\n" + "=" * 50) print("✅ 查詢完成!") except Exception as e: print(f"❌ 執行錯誤:{e}") # 測試不同類型的問題 test_questions = [ "哪個銷售代理在 2009 年的銷售額最高?", "列出前 3 名最受歡迎的音樂類型", "哪個國家的客戶最多?" ] # 執行測試 if 'prebuilt_agent' in locals(): for question in test_questions: test_sql_agent(prebuilt_agent, question) print("\n" + "🔄" * 20 + "\n") 🎨 建構自定義 SQL Agent 雖然預建 Agent 很方便,但自定義 Agent 能提供更精確的控制。讓我們建構一個: from typing import Literal from langchain_core.messages import AIMessage from langchain_core.runnables import RunnableConfig from langgraph.graph import END, START, MessagesState, StateGraph from langgraph.prebuilt import ToolNode # 取得特定工具 def setup_custom_tools(tools): """設置自定義工具""" tool_dict = {tool.name: tool for tool in tools} get_schema_tool = tool_dict["sql_db_schema"] get_schema_node = ToolNode([get_schema_tool], name="get_schema") run_query_tool = tool_dict["sql_db_query"] run_query_node = ToolNode([run_query_tool], name="run_query") list_tables_tool = tool_dict["sql_db_list_tables"] query_checker_tool = tool_dict["sql_db_query_checker"] return { 'get_schema_tool': get_schema_tool, 'get_schema_node': get_schema_node, 'run_query_tool': run_query_tool, 'run_query_node': run_query_node, 'list_tables_tool': list_tables_tool, 'query_checker_tool': query_checker_tool } # 建立專用節點函數 def create_custom_nodes(llm, custom_tools): """建立自定義節點""" # 1. 列出表格節點 def list_tables(state: MessagesState): tool_call = { "name": "sql_db_list_tables", "args": {}, "id": "list_tables_call", "type": "tool_call", } tool_call_message = AIMessage(content="", tool_calls=[tool_call]) tool_message = custom_tools['list_tables_tool'].invoke(tool_call) response = AIMessage(f"📋 可用表格:{tool_message.content}") return {"messages": [tool_call_message, tool_message, response]} # 2. 呼叫 schema 節點 def call_get_schema(state: MessagesState): llm_with_tools = llm.bind_tools([custom_tools['get_schema_tool']], tool_choice="any") response = llm_with_tools.invoke(state["messages"]) return {"messages": [response]} # 3. 生成查詢節點 def generate_query(state: MessagesState): system_message = { "role": "system", "content": f""" 你是專門設計來與 SQL 資料庫互動的代理。 給定輸入問題,建立語法正確的 {db.dialect} 查詢來執行。 限制查詢結果最多 5 個。 只查詢與問題相關的欄位。 不要執行任何 DML 語句。 """ } llm_with_tools = llm.bind_tools([custom_tools['run_query_tool']]) response = llm_with_tools.invoke([system_message] + state["messages"]) return {"messages": [response]} # 4. 檢查查詢節點 def check_query(state: MessagesState): system_message = { "role": "system", "content": f""" 你是一個細心的 SQL 專家。 檢查 {db.dialect} 查詢的常見錯誤,包括: - NULL 值的 NOT IN 使用 - 應該使用 UNION ALL 而非 UNION - 排他性範圍的 BETWEEN 使用 - 謂詞中的資料類型不匹配 - 標識符的正確引用 - 函數參數數量正確 - 正確的資料類型轉換 - 正確的 JOIN 欄位 如果有錯誤,重寫查詢。如果沒有錯誤,重現原始查詢。 """ } tool_call = state["messages"][-1].tool_calls[0] user_message = {"role": "user", "content": tool_call["args"]["query"]} llm_with_tools = llm.bind_tools([custom_tools['run_query_tool']], tool_choice="any") response = llm_with_tools.invoke([system_message, user_message]) response.id = state["messages"][-1].id return {"messages": [response]} return list_tables, call_get_schema, generate_query, check_query # 設置自定義工具和節點 if 'tools' in locals(): custom_tools = setup_custom_tools(tools) list_tables, call_get_schema, generate_query, check_query = create_custom_nodes(llm, custom_tools) print("✅ 自定義節點建立完成!") 🤖 SQL Agent 自定義工作流程詳解 📊 節點功能說明 1. 🚀 START → list_tables 觸發: 用戶輸入自然語言問題 功能: 自動列出資料庫中所有可用的表格 輸出: 表格列表 (如: Album, Artist, Customer, Employee...) 2. 📋 list_tables → call_get_schema 功能: 強制 LLM 呼叫 schema 工具 策略: 使用 tool_choice="any" 確保工具被呼叫 目的: 為下一步獲取詳細的表格結構做準備 3. 🔧 call_get_schema → get_schema 功能: 實際執行 schema 查詢工具 輸入: 相關表格名稱列表 輸出: 詳細的表格結構、欄位類型、樣本資料 4. 📊 get_schema → generate_query 功能: 基於問題和 schema 資訊生成 SQL 查詢 考量因素: 用戶的自然語言問題 表格結構和關聯性 資料類型匹配 查詢優化 5. ⚡ generate_query → should_continue (條件分支) 判斷邏輯:if not last_message.tool_calls: return END # 沒有生成 SQL,直接結束 else: return "check_query" # 有 SQL 查詢,進入檢查階段 6. 🤔 should_continue 分支點 路徑 A: END (結束) 條件: LLM 沒有生成工具呼叫 情況: 問題太模糊無法生成查詢 LLM 直接提供答案而無需查詢資料庫 發生錯誤導致無法繼續 路徑 B: check_query (檢查查詢) 條件: LLM 生成了 SQL 查詢工具呼叫 進入: 查詢驗證和優化流程 7. ✅ check_query → run_query 功能: SQL 查詢安全性和正確性檢查 檢查項目: 語法錯誤 安全性問題 (防止 SQL 注入) 效能考量 (避免過於複雜的查詢) 邏輯正確性 輸出: 修正後的 SQL 查詢 8. 🚀 run_query → generate_query (回環) 功能: 執行 SQL 查詢並處理結果 可能結果: ✅ 成功: 返回查詢結果,流程結束 ❌ 失敗: 回到 generate_query 重新生成查詢 🔄 工作流程特點 🎯 確定性執行順序 與預建 Agent 不同,自定義 Agent 強制執行特定的步驟順序,確保: 必定先查看資料庫結構 必定獲取相關表格的 schema 必定檢查生成的查詢 具備錯誤重試機制 🛡️ 安全性保障 強制查詢檢查: 每個 SQL 查詢都必須通過安全檢查 結構化驗證: 按步驟驗證每個階段的輸出 錯誤隔離: 錯誤發生在特定節點,便於除錯 ⚡ 效能優化 智能分支: 只在需要時執行查詢檢查 快速結束: 無需查詢時直接結束,節省資源 重試機制: 查詢失敗時自動重新生成 🔧 可擴展性 每個節點都是獨立的功能單元,可以輕鬆: 添加新的處理步驟 修改特定節點的邏輯 插入額外的驗證或處理步驟 📈 與預建 Agent 的比較 特性 預建 Agent 自定義 Agent 控制精度 依賴提示 強制執行順序 錯誤處理 通用處理 節點特定處理 可維護性 較低 高 (模組化) 除錯能力 困難 容易 (節點級別) 客製化 有限 完全可控 開發複雜度 低 中等 🎨 自定義建議 您可以根據需求增加額外節點: # 範例:添加結果格式化節點 builder.add_node("format_results", format_results) builder.add_edge("run_query", "format_results") builder.add_edge("format_results", END) # 範例:添加查詢快取節點 builder.add_node("check_cache", check_cache) builder.add_edge("generate_query", "check_cache") builder.add_conditional_edges("check_cache", cache_decision) 這種結構化的方法讓您對 SQL Agent 的每個步驟都有完全的控制權! 🔗 組裝自定義 Agent 圖結構 現在讓我們組裝完整的圖結構: # 建立條件邊函數 def should_continue(state: MessagesState) -> Literal[END, "check_query"]: """決定是否繼續到查詢檢查階段""" messages = state["messages"] last_message = messages[-1] # 如果沒有工具呼叫,結束流程 if not last_message.tool_calls: return END else: return "check_query" # 建構狀態圖 def build_custom_agent(): """建構自定義 SQL Agent""" builder = StateGraph(MessagesState) # 新增節點 builder.add_node("list_tables", list_tables) builder.add_node("call_get_schema", call_get_schema) builder.add_node("get_schema", custom_tools['get_schema_node']) builder.add_node("generate_query", generate_query) builder.add_node("check_query", check_query) builder.add_node("run_query", custom_tools['run_query_node']) # 定義邊的連接 builder.add_edge(START, "list_tables") builder.add_edge("list_tables", "call_get_schema") builder.add_edge("call_get_schema", "get_schema") builder.add_edge("get_schema", "generate_query") builder.add_conditional_edges("generate_query", should_continue) builder.add_edge("check_query", "run_query") builder.add_edge("run_query", "generate_query") # 編譯圖 agent = builder.compile() print("🎯 自定義 SQL Agent 建構完成!") return agent # 建立自定義 Agent if 'custom_tools' in locals(): custom_agent = build_custom_agent() 🧪 比較兩種 Agent 的效能 讓我們比較預建和自定義 Agent 的表現: import time def compare_agents(question): """比較兩種 Agent 的效能""" print(f"🔬 比較測試:{question}") print("=" * 60) agents = { "預建 Agent": prebuilt_agent, "自定義 Agent": custom_agent } results = {} for agent_name, agent in agents.items(): print(f"\n🤖 測試 {agent_name}:") print("-" * 30) start_time = time.time() try: messages = [{"role": "user", "content": question}] # 只取最後一個回應 final_response = None step_count = 0 for step in agent.stream({"messages": messages}, stream_mode="values"): step_count += 1 final_response = step["messages"][-1] end_time = time.time() duration = end_time - start_time results[agent_name] = { 'response': final_response.content if hasattr(final_response, 'content') else "無回應", 'duration': duration, 'steps': step_count, 'success': True } print(f"⏱️ 執行時間:{duration:.2f} 秒") print(f"📊 執行步驟:{step_count} 步") print(f"💬 最終回應:{results[agent_name]['response'][:100]}...") except Exception as e: end_time = time.time() duration = end_time - start_time results[agent_name] = { 'response': f"錯誤:{str(e)}", 'duration': duration, 'steps': 0, 'success': False } print(f"❌ 執行失敗:{e}") # 顯示比較結果 print(f"\n📈 比較結果:") print("-" * 30) for agent_name, result in results.items(): status = "✅" if result['success'] else "❌" print(f"{status} {agent_name}: {result['duration']:.2f}秒, {result['steps']}步驟") return results # 執行比較測試 if 'prebuilt_agent' in locals() and 'custom_agent' in locals(): comparison_results = compare_agents("列出銷售額前 3 名的藝術家") 📊 進階功能:錯誤處理和監控 讓我們加入錯誤處理和監控功能: import logging from functools import wraps # 設置日誌 def setup_logging(): """設置 SQL Agent 日誌""" logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) logger = logging.getLogger('sql_agent') return logger # 效能監控裝飾器 def monitor_performance(func): """監控 Agent 效能的裝飾器""" @wraps(func) def wrapper(*args, **kwargs): start_time = time.time() try: result = func(*args, **kwargs) success = True error = None except Exception as e: result = None success = False error = str(e) logger.error(f"Function {func.__name__} failed: {error}") end_time = time.time() duration = end_time - start_time # 記錄效能指標 logger.info(f"Function: {func.__name__}, Duration: {duration:.2f}s, Success: {success}") if not success: raise Exception(error) return result return wrapper # 建立強化版 Agent 測試函數 @monitor_performance def enhanced_agent_test(agent, question): """強化版 Agent 測試""" messages = [{"role": "user", "content": question}] responses = [] for step in agent.stream({"messages": messages}, stream_mode="values"): responses.append(step["messages"][-1]) return responses[-1] if responses else None # 初始化日誌 logger = setup_logging() # 測試強化版功能 if 'custom_agent' in locals(): print("🔧 測試強化版功能:") test_questions = [ "哪個員工負責最多客戶?", "2009年每月的銷售趨勢如何?", "最受歡迎的音樂專輯是什麼?" ] for question in test_questions: try: print(f"\n❓ 問題:{question}") result = enhanced_agent_test(custom_agent, question) print(f"✅ 回應:{result.content[:100] if hasattr(result, 'content') else 'No content'}...") except Exception as e: print(f"❌ 錯誤:{e}") 🚀 部署就緒的 Agent 類別 最後,讓我們建立一個生產就緒的 Agent 類別: class ProductionSQLAgent: """生產就緒的 SQL Agent 類別""" def __init__(self, database_uri, openai_api_key, model="gpt-4"): """初始化 SQL Agent""" self.db = SQLDatabase.from_uri(database_uri) self.llm = ChatOpenAI( api_key=openai_api_key, model=model, temperature=0 ) # 建立工具包 toolkit = SQLDatabaseToolkit(db=self.db, llm=self.llm) self.tools = toolkit.get_tools() # 建立 Agent self.agent = self._build_agent() # 設置監控 self.logger = logging.getLogger(f'sql_agent_{id(self)}') self.query_count = 0 self.total_time = 0 def _build_agent(self): """建構 Agent""" system_prompt = f""" 你是一個專業的 SQL 資料庫分析師。 資料庫類型:{self.db.dialect} 可用表格:{', '.join(self.db.get_usable_table_names())} 職責: 1. 理解自然語言問題 2. 分析資料庫結構 3. 生成安全的 SQL 查詢 4. 提供清晰的答案 安全原則: - 永遠不執行 DML 語句 - 限制結果數量 - 檢查查詢安全性 """ return create_react_agent( self.llm, self.tools, prompt=system_prompt ) @monitor_performance def query(self, question: str, max_results: int = 5): """執行查詢""" self.query_count += 1 start_time = time.time() try: # 加入結果限制到問題中 enhanced_question = f"{question} (限制結果數量為 {max_results})" messages = [{"role": "user", "content": enhanced_question}] # 執行查詢 final_response = None for step in self.agent.stream({"messages": messages}, stream_mode="values"): final_response = step["messages"][-1] end_time = time.time() duration = end_time - start_time self.total_time += duration response_content = final_response.content if hasattr(final_response, 'content') else "無法取得回應" self.logger.info(f"Query successful: {question[:50]}... | Duration: {duration:.2f}s") return { 'success': True, 'response': response_content, 'duration': duration, 'query_number': self.query_count } except Exception as e: self.logger.error(f"Query failed: {question[:50]}... | Error: {str(e)}") return { 'success': False, 'error': str(e), 'duration': time.time() - start_time, 'query_number': self.query_count } def get_stats(self): """取得統計資訊""" avg_time = self.total_time / self.query_count if self.query_count > 0 else 0 return { 'total_queries': self.query_count, 'total_time': self.total_time, 'average_time': avg_time, 'available_tables': self.db.get_usable_table_names() } # 建立生產級 Agent def create_production_agent(): """建立生產級 SQL Agent""" try: production_agent = ProductionSQLAgent( database_uri="sqlite:///Chinook.db", openai_api_key=os.environ.get("OPENAI_API_KEY", "your-api-key-here") ) print("🏭 生產級 SQL Agent 建立成功!") return production_agent except Exception as e: print(f"❌ 建立失敗:{e}") return None # 測試生產級 Agent if os.environ.get("OPENAI_API_KEY"): prod_agent = create_production_agent() if prod_agent: # 執行測試查詢 test_queries = [ "顯示銷售額最高的 3 個國家", "哪些藝術家有超過 10 張專輯?", "2008 年和 2009 年的銷售額比較" ] print("\n🧪 生產級 Agent 測試:") print("=" * 50) for query in test_queries: print(f"\n❓ 查詢:{query}") result = prod_agent.query(query) if result['success']: print(f"✅ 成功 ({result['duration']:.2f}s)") print(f"📝 回應:{result['response'][:150]}...") else: print(f"❌ 失敗:{result['error']}") # 顯示統計 stats = prod_agent.get_stats() print(f"\n📊 統計資訊:") print(f"總查詢數:{stats['total_queries']}") print(f"平均時間:{stats['average_time']:.2f}秒") print(f"可用表格:{len(stats['available_tables'])} 個") 🎯 總結和最佳實踐 通過本教學,我們從零開始建構了一個完整的 SQL Agent 系統。以下是關鍵要點: # 最佳實踐檢查清單 def best_practices_checklist(): """SQL Agent 最佳實踐檢查清單""" practices = { "安全性": [ "✅ 限制資料庫權限", "✅ 禁止 DML 操作", "✅ 輸入驗證和清理", "✅ 查詢複雜度限制", "✅ 錯誤訊息過濾" ], "效能": [ "✅ 結果數量限制", "✅ 查詢超時設定", "✅ 連接池管理", "✅ 快取策略", "✅ 監控和日誌" ], "可靠性": [ "✅ 錯誤處理機制", "✅ 重試邏輯", "✅ 查詢驗證", "✅ 結果格式化", "✅ 健康檢查" ], "可維護性": [ "✅ 模組化設計", "✅ 配置管理", "✅ 版本控制", "✅ 文件完整", "✅ 測試覆蓋" ] } print("📋 SQL Agent 最佳實踐檢查清單:") print("=" * 40) for category, items in practices.items(): print(f"\n🏷️ {category}:") for item in items: print(f" {item}") return practices # 顯示檢查清單 best_practices_checklist() print(""" 🎉 恭喜!您已經成功建構了一個完整的 SQL Agent 系統! 🔜 下一步建議: 1. 部署到雲端平台(AWS、GCP、Azure) 2. 整合到現有應用程式中 3. 添加更多安全性措施 4. 實施 A/B 測試 5. 收集用戶反饋並持續改進 記住:一個好的 AI 系統需要持續的監控、測試和優化。 祝您的 SQL Agent 專案成功!🚀 """) 🔗 額外資源 # 有用的資源連結 resources = { "官方文件": [ "https://python.langchain.com/docs/tutorials/sql_qa", "https://langchain-ai.github.io/langgraph/", "https://docs.anthropic.com/claude/docs" ], "範例專案": [ "https://github.com/langchain-ai/langgraph/tree/main/examples", "https://github.com/langchain-ai/langchain/tree/main/templates" ], "學習資源": [ "LangChain SQL Tutorial", "LangGraph Documentation", "SQL Database Best Practices" ] } print("📚 更多學習資源:") for category, links in resources.items(): print(f"\n🔗 {category}:") for link in links: print(f" • {link}") 🛠️ 疑難排解指南 在使用過程中可能遇到的常見問題和解決方案: class TroubleshootingGuide: """SQL Agent 疑難排解指南""" @staticmethod def common_issues(): """常見問題和解決方案""" issues = { "連接問題": { "症狀": "無法連接到資料庫", "可能原因": ["API Key 錯誤", "網路問題", "資料庫路徑錯誤"], "解決方案": [ "檢查 OpenAI API Key 是否正確設定", "確認資料庫檔案存在", "檢查網路連接", "驗證資料庫 URI 格式" ] }, "查詢錯誤": { "症狀": "SQL 查詢執行失敗", "可能原因": ["語法錯誤", "欄位名錯誤", "表格不存在"], "解決方案": [ "檢查資料庫 schema", "驗證表格名稱", "測試簡單查詢", "檢查資料類型匹配" ] }, "效能問題": { "症狀": "回應時間過長", "可能原因": ["複雜查詢", "大量資料", "模型回應慢"], "解決方案": [ "限制結果數量", "優化查詢邏輯", "使用索引", "考慮快取策略" ] }, "記憶體問題": { "症狀": "記憶體不足錯誤", "可能原因": ["結果集太大", "模型佔用過多記憶體"], "解決方案": [ "增加 LIMIT 限制", "分批處理資料", "清理不需要的變數", "考慮使用串流處理" ] } } print("🔧 SQL Agent 疑難排解指南:") print("=" * 50) for issue_type, details in issues.items(): print(f"\n🚨 {issue_type}:") print(f" 症狀:{details['症狀']}") print(f" 可能原因:{', '.join(details['可能原因'])}") print(f" 解決方案:") for solution in details['解決方案']: print(f" • {solution}") return issues @staticmethod def debug_agent(agent, question): """除錯 Agent 執行過程""" print(f"🔍 除錯模式:{question}") print("-" * 40) try: messages = [{"role": "user", "content": question}] step_count = 0 for step in agent.stream({"messages": messages}, stream_mode="values"): step_count += 1 last_message = step["messages"][-1] print(f"\n📍 步驟 {step_count}:") print(f" 類型:{type(last_message).__name__}") if hasattr(last_message, 'content') and last_message.content: print(f" 內容:{last_message.content[:100]}...") if hasattr(last_message, 'tool_calls') and last_message.tool_calls: print(f" 工具呼叫:") for tool_call in last_message.tool_calls: print(f" • {tool_call.get('name', 'Unknown')}") if 'args' in tool_call: print(f" 參數:{str(tool_call['args'])[:100]}...") # 檢查錯誤 if hasattr(last_message, 'content') and 'error' in last_message.content.lower(): print(f" ⚠️ 可能的錯誤:{last_message.content}") print(f"\n✅ 除錯完成,總共 {step_count} 個步驟") except Exception as e: print(f"❌ 除錯過程中發生錯誤:{e}") import traceback traceback.print_exc() # 使用疑難排解指南 troubleshooter = TroubleshootingGuide() troubleshooter.common_issues() # 如果需要除錯特定問題 if 'custom_agent' in locals(): print("\n" + "="*60) troubleshooter.debug_agent(custom_agent, "測試查詢:列出所有藝術家") 🚀 進階擴展功能 讓我們實作一些進階功能來增強我們的 SQL Agent: class AdvancedSQLAgent(ProductionSQLAgent): """進階 SQL Agent 擴展功能""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.query_history = [] self.performance_metrics = { 'successful_queries': 0, 'failed_queries': 0, 'total_execution_time': 0, 'average_response_time': 0 } def explain_query(self, question: str): """解釋查詢過程""" print(f"🔍 查詢解釋:{question}") print("-" * 40) # 模擬查詢規劃 print("📋 查詢規劃階段:") print(" 1. 分析自然語言問題") print(" 2. 識別相關資料表") print(" 3. 確定所需欄位") print(" 4. 規劃 JOIN 策略") print(" 5. 設定過濾條件") # 執行查詢並記錄過程 result = self.query(question) if result['success']: print(f"\n✅ 查詢成功完成") print(f"⏱️ 執行時間:{result['duration']:.2f} 秒") else: print(f"\n❌ 查詢失敗:{result.get('error', 'Unknown error')}") return result def batch_query(self, questions: list): """批次處理多個查詢""" print(f"📦 批次查詢:{len(questions)} 個問題") print("=" * 50) results = [] total_start_time = time.time() for i, question in enumerate(questions, 1): print(f"\n🔄 處理查詢 {i}/{len(questions)}:{question[:50]}...") result = self.query(question) results.append({ 'question': question, 'result': result }) status = "✅" if result['success'] else "❌" print(f"{status} 完成 ({result['duration']:.2f}s)") total_time = time.time() - total_start_time success_count = sum(1 for r in results if r['result']['success']) print(f"\n📊 批次處理結果:") print(f" 總查詢數:{len(questions)}") print(f" 成功數:{success_count}") print(f" 成功率:{success_count/len(questions)*100:.1f}%") print(f" 總時間:{total_time:.2f} 秒") print(f" 平均時間:{total_time/len(questions):.2f} 秒/查詢") return results def suggest_optimizations(self): """建議查詢優化""" print("💡 查詢優化建議:") print("-" * 30) suggestions = [ "使用適當的索引來加速查詢", "避免 SELECT * ,只選擇需要的欄位", "使用 LIMIT 限制結果數量", "考慮使用 EXISTS 而非 IN 子查詢", "避免在 WHERE 子句中使用函數", "使用 JOIN 而非多個子查詢", "考慮資料類型的匹配性" ] for i, suggestion in enumerate(suggestions, 1): print(f" {i}. {suggestion}") return suggestions def generate_report(self): """生成效能報告""" stats = self.get_stats() report = f""" 📊 SQL Agent 效能報告 {'='*40} 基本統計: • 總查詢數:{stats['total_queries']} • 總執行時間:{stats['total_time']:.2f} 秒 • 平均回應時間:{stats['average_time']:.2f} 秒 資料庫資訊: • 可用表格數:{len(stats['available_tables'])} • 資料庫類型:{self.db.dialect} 效能指標: • 成功查詢:{self.performance_metrics['successful_queries']} • 失敗查詢:{self.performance_metrics['failed_queries']} • 成功率:{self.performance_metrics['successful_queries']/(self.performance_metrics['successful_queries']+self.performance_metrics['failed_queries'])*100 if (self.performance_metrics['successful_queries']+self.performance_metrics['failed_queries']) > 0 else 0:.1f}% 建議: • 監控長時間執行的查詢 • 定期檢查資料庫效能 • 考慮實施查詢快取 • 優化常用查詢路徑 """ print(report) return report # 建立進階 SQL Agent def create_advanced_agent(): """建立進階 SQL Agent""" if os.environ.get("OPENAI_API_KEY"): try: advanced_agent = AdvancedSQLAgent( database_uri="sqlite:///Chinook.db", openai_api_key=os.environ.get("OPENAI_API_KEY") ) print("🚀 進階 SQL Agent 建立成功!") return advanced_agent except Exception as e: print(f"❌ 建立失敗:{e}") return None else: print("⚠️ 請設定 OPENAI_API_KEY 環境變數") return None # 測試進階功能 advanced_agent = create_advanced_agent() if advanced_agent: print("\n🧪 進階功能測試:") # 測試查詢解釋 advanced_agent.explain_query("哪個客戶購買最多?") # 測試批次查詢 batch_questions = [ "列出前 5 名暢銷專輯", "哪個城市的客戶最多?", "2009 年最受歡迎的音樂類型" ] print("\n" + "="*60) batch_results = advanced_agent.batch_query(batch_questions) # 顯示優化建議 print("\n" + "="*60) advanced_agent.suggest_optimizations() # 生成報告 print("\n" + "="*60) advanced_agent.generate_report() 🔒 安全性增強 實作額外的安全性措施: ```pythonimport reimport hashlibfrom datetime import datetime, timedelta class SecureSQLAgent(AdvancedSQLAgent): """安全增強版 SQL Agent""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.blocked_patterns = [ r'\bDROP\b', r'\bDELETE\b', r'\bINSERT\b', r'\bUPDATE\b', r'\bALTER\b', r'\bCREATE\b', r'\bTRUNCATE\b', r'\bGRANT\b', r'\bREVOKE\b', r'--', r'/\*', r'\*/', r'\bEXEC\b' ] self.query_cache = {} self.rate_limit = {} self.max_queries_per_minute = 10 def _is_query_safe(self, query: str) -> tuple[bool, str]: """檢查查詢是否安全""" query_upper = query.upper() # 檢查危險模式 for pattern in self.blocked_patterns: if re.search(pattern, query_upper, re.IGNORECASE): return False, f"查詢包含被禁止的模式: {pattern}" # 檢查查詢長度 if len(query) > 1000: return False, "查詢長度超過限制" # 檢查是否只有 SELECT 語句 if not query_upper.strip().startswith('SELECT'): return False, "只允許 SELECT 查詢" return True, "查詢安全" def _check_rate_limit(self, user_id: str = "default") -> bool: """檢查速率限制""" now = datetime.now() minute_ago = now - timedelta(minutes=1) if user_id not in self.rate_limit: self.rate_limit[user_id] = [] # 清除一分鐘前的記錄 self.rate_limit[user_id] = [ timestamp for timestamp in self.rate_limit[user_id] if timestamp > minute_ago ] # 檢查是否超過限制 if len(self.rate_limit[user_id]) >= self.max_queries_per_minute: return False # 記錄當前查詢 self.rate_limit[user_id].append(now) return True def _get_query_hash(self, question: str) -> str: """生成查詢雜湊值""" return hashlib.md5(question.encode()).hexdigest() def secure_query(self, question: str, user_id: str = "default"): """安全查詢執行""" # 檢查速率限制 if not self._check_rate_limit(user_id): return { 'success': False, 'error': '查詢頻率超過限制,請稍後再試', 'duration': 0 } # 檢查快取 query_hash = self._get_query_hash(question) if query_hash in self.query_cache: cached_result = self.query_cache[query_hash] print("📦 使用快取結果") return { **cached_result, 'from_cache': True } # 執行查詢 result = self.query(question) # 如果成功,加入快取 if result['success']: self.query_cache[query_hash] = { 'response': result['response'], 'timestamp': datetime.now(), 'success': True } return result def audit_log(self, action: str, details: dict): """記錄審計日誌""" log_entry = { 'timestamp': datetime.now().isoformat(), 'action': action, 'details': details } print(f"📋 審計日誌:{log_entry}") # 在實際應用中,這裡應該寫入到安全的日誌系統 return log_entry def security_report(self): """生成安全報告""" report = f""" 🔒 安全報告{'='*30} 快取統計: • 快取條目數:{len(self.query_cache)} • 快取命中可節省的查詢數 速率限制: • 每分鐘最大查詢數:{self.max_queries_per_minute} • 目前活躍用戶:{len(self.rate_limit)} 安全檢查: • 被禁止的 SQL 模式:{len(self.blocked_patterns)} 個 • 查詢長度限制:1000 字元 • 只允許 SELECT 語句 建議: • 定期審查查詢日誌 • 監控異常查詢模式 • 更新安全規則 """ print(report) return report 建立安全版 Agent def create_secure_agent(): """建立安全版 SQL Agent""" if os.environ.get("OPENAI_API_KEY"): try: secure_agent = SecureSQLAgent( database_uri="sqlite:///Chinook.db", openai_api_key=os.environ.get("OPENAI_API_KEY") ) print("🔒 安全版 SQL Agent 建立成功!") return secure_agent except Exception as e: print(f"❌ 建立失敗:{e}") return None else: print("⚠️ 請設定 OPENAI_API_KEY 環境變數") return None 測試安全功能 secure_agent = create_secure_agent() if secure_agent: print("\n🔒 安全功能測試:") # 測試正常查詢 print("\n1. 測試正常查詢:") result1 = secure_agent.secure_query("列出前 3 名客戶") print(f"結果:{'✅ 成功' if result1['success'] else '❌ 失敗'}") # 測試快取 print("\n2. 測試查詢快取:") result2 = secure_agent.secure_query("列出前 3 名客戶") # 相同查詢 if result2.get('from_cache'): print("✅ 成功使用快取") # 測試速率限制(模擬多次查詢) print("\n3. 測試速率限制:") for i in range(12): # 超過限制的查詢 result = secure_agent.secure_query(f"測試查詢 {i}") if not result['success'] and '頻率超過限制' in result['error']: print(f"✅ 速率限制在第 {i+1} 次查詢時觸發") break # 生成安全報告 print("\n" + "="*60) secure_agent.security_report() print(""" 🎉 教學完成! 您已經學會了:✅ 建構基本的 SQL Agent✅ 實作自定義工作流程✅ 添加錯誤處理和監控✅ 實施安全性措施✅ 效能優化和快取✅ 批次處理和報告生成 🚀 現在您可以: 部署到生產環境 整合到您的應用程式 根據需求進行客製化 擴展更多功能 記住:持續監控、測試和改進是成功的關鍵! Happy coding! 🎯""")
-