記憶與知識壓縮技術

隨著對話累積和知識庫擴大,處理大量資訊會面臨挑戰。記憶和知識壓縮技術可以有效解決這個問題。

1. 對話摘要與記憶壓縮

對於長時間對話,可以使用摘要技術壓縮記憶,以保持對話的連貫性,同時避免上下文過長。

from langchain.memory import ConversationSummaryBufferMemory
from langchain.chat_models import ChatOpenAI

# 建立摘要緩衝記憶體
# llm 參數指定用於生成摘要的大語言模型
summary_buffer_memory = ConversationSummaryBufferMemory(
    llm=ChatOpenAI(),
    max_token_limit=1000  # 設定記憶體中 token 的最大限制
)

# 模擬一段長對話並儲存上下文
for i in range(10):
    summary_buffer_memory.save_context(
        {"input": f"這是使用者的第{i+1}個問題"},
        {"output": f"這是系統的第{i+1}個回答,提供了一些資訊"}
    )

# 檢視緩衝區中最近的對話內容
print(summary_buffer_memory.buffer)
# 檢視被壓縮的移動摘要
print("\n摘要:")
print(summary_buffer_memory.moving_summary_buffer)

記憶壓縮序列圖

圖表描述 (Alt Text): 此序列圖展示了 ConversationSummaryBufferMemory 的工作流程。當儲存新的對話上下文時,如果超出 max_token_limit,系統會自動呼叫 LLM 將較早的對話壓縮成摘要,以維持記憶體大小。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 14
title ConversationSummaryBufferMemory 運作流程

actor User as U
participant "App" as App
participant "ConversationSummaryBufferMemory" as Memory
participant "LLM" as LLM

U -> App: 輸入新對話
App -> Memory: save_context(input, output)
Memory -> Memory: 檢查 token 是否超出 max_token_limit
alt 超出限制
    Memory -> LLM: 請求壓縮舊對話
    LLM --> Memory: 回傳摘要
    Memory -> Memory: 更新 moving_summary_buffer
end
Memory --> App: 完成儲存
@enduml

2. 知識壓縮與重要性排序

對於大型知識庫,可以使用 LLM 進行知識壓縮和重要性排序,過濾掉不相關的資訊。

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMChainFilter
from langchain.chat_models import ChatOpenAI

# 假設 vectorstore 已被初始化
# 建立一個基礎的向量檢索器,先找出10個最相似的文件
base_retriever = vectorstore.as_retriever(search_kwargs={"k": 10})

# 建立一個 LLM 過濾器,用於判斷文件是否與查詢相關
llm = ChatOpenAI(temperature=0)
filter_prompt = """判斷以下文字片段是否與問題相關:
問題: {query}
文字: {text}
如果相關,回答"相關";如果不相關,回答"不相關"。
"""
llm_filter = LLMChainFilter.from_llm(
    llm=llm,
    prompt_template=filter_prompt
)

# 建立一個上下文壓縮檢索器
# 它會先使用 base_retriever 檢索文件,然後用 llm_filter 進行過濾
compression_retriever = ContextualCompressionRetriever(
    base_compressor=llm_filter,
    base_retriever=base_retriever
)

# 執行檢索與過濾
filtered_docs = compression_retriever.get_relevant_documents("什麼是向量資料庫的應用場景?")

知識壓縮流程活動圖

圖表描述 (Alt Text): 此活動圖展示了 ContextualCompressionRetriever 的知識壓縮流程。從接收使用者查詢開始,首先由基礎檢索器找出候選文件,再由 LLM 過濾器逐一判斷其相關性,最終回傳經過壓縮和排序的相關文件。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 14
title 知識壓縮與重要性排序流程

start
:接收使用者查詢;
:基礎檢索器 (Base Retriever)\n檢索 N 個候選文件;
:LLM 過濾器 (LLMChainFilter)\n逐一判斷文件相關性;
:回傳相關的文件;
stop
@enduml

RAG 系統的進階最佳化策略

以下進階策略能顯著提升 RAG 系統的效能:

1. 查詢重寫與擴充

透過 LLM 重寫使用者查詢,可以提高檢索效果。

from langchain.retrievers import ContextualCompressionRetriever
from langchain.retrievers.document_compressors import LLMQueryTransformer
from langchain.chat_models import ChatOpenAI

# 建立查詢轉換器,用於將原始問題改寫為更有效的搜尋查詢
query_transformer_prompt = """給定以下使用者問題,請重新表述為一個更有效的搜尋查詢,以從向量資料庫中檢索相關文件。
原始問題: {query}
重寫的搜尋查詢:"""

query_transformer = LLMQueryTransformer(
    llm=ChatOpenAI(temperature=0),
    prompt_template=query_transformer_prompt
)

# 建立一個使用查詢轉換的增強檢索器
retriever_with_query_transformation = ContextualCompressionRetriever(
    base_compressor=query_transformer,
    base_retriever=vectorstore.as_retriever()
)

# 使用轉換後的查詢來檢索文件
transformed_docs = retriever_with_query_transformation.get_relevant_documents(
    "AI會取代人類工作嗎?"
)

查詢重寫流程活動圖

圖表描述 (Alt Text): 此活動圖展示了利用 LLM 進行查詢重寫以提升檢索效果的流程。使用者輸入原始問題後,由 LLMQueryTransformer 將其轉換為更有效的搜尋查詢,再用此查詢從向量資料庫中檢索文件。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 14
title 查詢重寫與擴充流程

start
:接收使用者原始查詢;
:LLM 查詢轉換器 (LLMQueryTransformer)\n重寫並擴充查詢;
:使用增強後的查詢\n從向量資料庫檢索文件;
:回傳相關文件;
stop
@enduml

2. 多模型協同策略

結合不同特點的模型可以提升整體效能,例如使用一個較快的模型進行檢索,再用一個較強的模型生成最終回應。

from langchain.chat_models import ChatOpenAI, ChatAnthropic
from langchain.schema.runnable import RunnablePassthrough, RunnableLambda
from langchain.prompts import ChatPromptTemplate
from operator import itemgetter

# 假設 vectorstore 已被初始化
# 1. 定義檢索函式
def retrieve_documents(query):
    return vectorstore.similarity_search(query, k=5)

# 2. 定義提示範本
prompt_template = ChatPromptTemplate.from_template("""
根據以下資訊回答使用者問題。如果無法從提供的資訊中找到答案,請說明你不知道。

資訊:
{context}

使用者問題: {question}
""")

# 3. 建立不同的 LLM
# retrieval_llm = ChatOpenAI(model_name="gpt-3.5-turbo") # 可選:用於檢索或改寫查詢
generation_llm = ChatAnthropic(model_name="claude-2")   # 用於生成最終答案

# 4. 構建文件格式化函式
def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)

# 5. 使用 LangChain Expression Language (LCEL) 組裝 RAG 鏈
rag_chain = (
    # 將問題傳遞下去,同時執行檢索並格式化上下文
    {"question": RunnablePassthrough(), "context": RunnableLambda(retrieve_documents) | format_docs}
    # 將結果傳入提示範本
    | prompt_template
    # 將提示傳入生成模型
    | generation_llm
)

# 6. 執行 RAG 鏈
response = rag_chain.invoke("區塊鏈技術有哪些實際應用?")
print(response)

多模型協同 RAG 元件圖

圖表描述 (Alt Text): 此元件圖展示了一個多模型協同的RAG系統架構。使用者問題首先觸發文件檢索,檢索到的上下文與原始問題一起被格式化並傳入提示範本,最終由一個專門的生成模型(如Claude-2)產生最終答案。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 14
title 多模型協同 RAG 架構

actor User

package "RAG Chain" {
  [RunnablePassthrough] as Pass
  [RunnableLambda\n(retrieve_documents)] as Retrieve
  [format_docs] as Format
  [ChatPromptTemplate] as Prompt
  [Generation LLM\n(Claude-2)] as GenLLM
}

User --> Pass : question
Pass --> Retrieve : question
Retrieve --> Format : docs
Format --> Prompt : context
Pass --> Prompt : question
Prompt --> GenLLM : formatted_prompt
GenLLM --> User : final_response

@enduml

3. 語義快取機制

對於頻繁查詢的系統,實作語義快取(Semantic Cache)可以顯著提高效率,避免對語義相似的查詢重複執行昂貴的 RAG 流程。

import numpy as np
from langchain.embeddings import OpenAIEmbeddings

class SemanticCache:
    """一個簡單的語義快取實現"""
    def __init__(self, embedding_model, similarity_threshold=0.95):
        self.embedding_model = embedding_model
        self.cache = {}  # 儲存查詢與結果的對應
        self.query_embeddings = {}  # 儲存查詢的向量
        self.similarity_threshold = similarity_threshold
    
    def get(self, query: str):
        """嘗試從快取中獲取結果"""
        if not self.query_embeddings:
            return None
            
        query_embedding = self.embedding_model.embed_query(query)
        
        # 找出最相似的已快取查詢
        cached_embeddings = np.array(list(self.query_embeddings.values()))
        similarities = self._cosine_similarity_matrix([query_embedding], cached_embeddings)[0]
        
        most_similar_idx = np.argmax(similarities)
        max_similarity = similarities[most_similar_idx]
        
        if max_similarity > self.similarity_threshold:
            most_similar_query = list(self.query_embeddings.keys())[most_similar_idx]
            print(f"快取命中! 相似度: {max_similarity:.4f}, 原查詢: '{most_similar_query}'")
            return self.cache[most_similar_query]
        
        return None
    
    def set(self, query: str, result: str):
        """將新的查詢結果存入快取"""
        self.cache[query] = result
        self.query_embeddings[query] = self.embedding_model.embed_query(query)
    
    def _cosine_similarity_matrix(self, matrix_a, matrix_b):
        """計算兩個矩陣間的餘弦相似度"""
        return np.dot(matrix_a, matrix_b.T) / (np.linalg.norm(matrix_a, axis=1)[:, np.newaxis] * np.linalg.norm(matrix_b, axis=1))

# 模擬 RAG 查詢函式
def query_rag_system(query, cache):
    cached_result = cache.get(query)
    if cached_result:
        return cached_result
    
    print(f"執行完整 RAG 查詢: '{query}'")
    result = f"這是 '{query}' 的查詢結果"
    cache.set(query, result)
    return result

# 使用範例
embeddings = OpenAIEmbeddings()
semantic_cache = SemanticCache(embeddings)

print(query_rag_system("什麼是向量資料庫?", semantic_cache))
print(query_rag_system("向量資料庫是什麼?", semantic_cache))
print(query_rag_system("機器學習模型如何工作?", semantic_cache))

SemanticCache 類別圖

圖表描述 (Alt Text): 此類別圖展示了 SemanticCache 類別的結構,包含其屬性(如嵌入模型、快取字典、相似度閾值)和主要方法(如 getset),用於實現語義快取功能。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 14
title SemanticCache 類別設計

class SemanticCache {
  - embedding_model: OpenAIEmbeddings
  - cache: dict
  - query_embeddings: dict
  - similarity_threshold: float
  + __init__(embedding_model, similarity_threshold)
  + get(query: str): str
  + set(query: str, result: str): void
  - _cosine_similarity_matrix(a, b): np.array
}
@enduml

語義快取查詢序列圖

圖表描述 (Alt Text): 此序列圖展示了語義快取的查詢流程。當應用程式發起查詢時,SemanticCache 首先計算查詢的嵌入向量,並與已快取的向量進行比較。如果相似度超過閾值,則直接返回快取結果(快取命中);否則,應用程式需執行完整的RAG查詢,並將新結果存入快取。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 14
title 語義快取查詢流程

actor User
participant "Application" as App
participant "SemanticCache" as Cache
participant "RAG System" as RAG

User -> App: 發起查詢 (query)
App -> Cache: get(query)
Cache -> Cache: 計算 query_embedding
Cache -> Cache: 與 cached_embeddings 計算相似度

alt 相似度 > 閾值 (快取命中)
    Cache --> App: 回傳 cached_result
    App --> User: 顯示結果
else 相似度 <= 閾值 (快取未命中)
    Cache --> App: 回傳 None
    App -> RAG: 執行完整查詢(query)
    RAG --> App: 回傳 new_result
    App -> Cache: set(query, new_result)
    App --> User: 顯示結果
end
@enduml

實際應用中的RAG系統最佳實踐

經過多個RAG系統的開發與最佳化,玄貓總結了以下最佳實踐,幫助你構建高效的記憶與知識系統:

1. 分塊策略最佳化

文字分塊是RAG系統的關鍵環節,建議:

  • 根據語義分塊:使用段落、標題等自然分隔點,而非固定字元數
  • 動態塊大小:根據內容型別調整塊大小,例如程式碼需要較大塊以保持完整性
  • 合適的重疊區域:通常設定為塊大小的10-20%,確

人工智慧代理的記憶與知識:向量相似度搜尋的力量

在開發人工智慧代理系統時,我常發現記憶與知識管理是決定系統效能的關鍵因素。無論是客服機器人還是個人助理,若沒有高效的記憶檢索機制,它們就無法提供連貫與有價值的回應。今天,我將分享如何運用向量相似度搜尋來建構人工智慧代理的記憶系統,這是我在多個專案中反覆驗證的有效方法。

非結構化記憶與知識:檢索增強生成的基礎

非結構化記憶概念依賴於文字相似度搜尋機制,遵循檢索增強生成(RAG)模式。與預先載入檔案的知識函式庫不同,記憶系統會將對話或對話片段進行向量化,然後儲存到向量資料函式庫中。

人工智慧代理的記憶檢索流程通常包含以下幾個關鍵步驟:

  1. 檢索階段:將使用者查詢轉換為向量表示,然後在向量資料函式庫中進行相似度搜尋
  2. 增強階段:將檢索到的相關記憶內容加入到提示中
  3. 生成階段:大模型語言根據增強後的提示生成回應
  4. 記憶階段:將新的對話內容向量化並增加到向量資料函式庫中

這種檢索模式和檔案索引需要仔細考量才能成功實施。接下來,我將解析如何儲存和檢索資料,以建立高效的人工智慧代理記憶系統。

深入語意搜尋與檔案索引技術

檔案索引的核心目的是將檔案中的資訊轉換成更易於檢索的形式。索引的方式取決於查詢或搜尋的需求,無論是搜尋特定的片語還是比對語意相似的內容。

語意搜尋(Semantic Search)是一種能夠比對詞義而非僅比對關鍵字的強大搜尋方式。接下來,我將探討如何運用向量相似度搜尋來建立語意搜尋的框架。

向量相似度搜尋的實際應用

我們來看如何將檔案轉換為語意向量,並用於執行距離或相似度比對。雖然有多種方法可以將文字轉換為語意向量,這裡我選擇一個直觀的方法來說明。

首先,讓我們檢視一個使用詞頻-逆檔案頻率(TF-IDF)的範例。TF-IDF是一種數值統計,用於反映一個詞語對於一個檔案集合中的某一個檔案的重要程度。

import plotly.graph_objects as go
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity

# 檔案樣本
documents = [
    "The sky is blue and beautiful.",
    "Love this blue and beautiful sky!",
    "The quick brown fox jumps over the lazy dog.",
    "A king's breakfast has sausages, ham, bacon, eggs, toast, and beans",
    "I love green eggs, ham, sausages and bacon!",
    "The brown fox is quick and the blue dog is lazy!",
    "The sky is very blue and the sky is very beautiful today",
    "The dog is lazy but the brown fox is quick!"
]

# 使用TF-IDF進行向量化
vectorizer = TfidfVectorizer()
X = vectorizer.fit_transform(documents)

這段程式碼使用scikit-learn的TfidfVectorizer將8個文字樣本轉換為向量表示。TF-IDF是自然語言處理中常用的特徵提取方法,它能夠評估一個詞在特設定檔案中的重要性。向量化後的結果X是一個稀疏矩陣,其中每一行代表一個檔案,每一列代表詞彙表中的一個詞,而矩陣中的值則是TF-IDF分數。這種表示方式使我們能夠計算檔案間的相似度,為語意搜尋奠定基礎。

理解 TF-IDF 計算原理

讓我們深入分析 TF-IDF 的計算方式,以句子 “The sky is blue and beautiful” 中的 “blue” 一詞為例。

  • 詞頻 (Term Frequency, TF): 衡量一個詞在單一文件中的出現頻率。
    • TF("blue") = 1 / 6 ≈ 0.167
  • 逆文件頻率 (Inverse Document Frequency, IDF): 衡量一個詞在整個文件集合中的重要性。
    • IDF("blue") = log(總文件數 / 包含 "blue" 的文件數) = log(8 / 4) ≈ 0.301
  • TF-IDF 分數: 兩者相乘,分數越高,代表該詞對該文件的特徵表達越重要。
    • TF-IDF("blue") = 0.167 * 0.301 ≈ 0.050

在實作中,我選擇 TF-IDF 是因為它易於理解和應用。一旦我們將文件表示為向量,就可以使用餘弦相似度(Cosine Similarity)來測量文件間的相似性。

餘弦相似度的應用

餘弦相似度(Cosine Similarity)是衡量多維空間中兩個向量夾角餘弦值的度量,用於判斷它們的方向相似程度,值範圍從 -1(完全相反)到 1(完全相同)。在文字分析中,它可以有效地比較兩段文字的向量表示,判斷它們的語意相似度。

# 計算所有文件對之間的餘弦相似度
cosine_similarities = cosine_similarity(X)

# 建立一個互動迴圈,讓使用者查詢文件相似度
while True:
    try:
        # 提示使用者輸入
        user_input = input(f"請輸入文件編號 (0-{len(documents)-1}) 或輸入 'exit' 離開: ").strip()
        
        if user_input.lower() == 'exit':
            break
            
        selected_index = int(user_input)
        if not 0 <= selected_index < len(documents):
            raise ValueError("索引超出範圍")
            
        # 獲取並顯示相似度結果
        similarities = cosine_similarities[selected_index]
        print(f"\n文件 '{documents[selected_index]}' 與其他文件的相似度:")
        for i, score in enumerate(similarities):
            print(f"  - 與文件 {i}: {score:.4f}")
            
    except ValueError as e:
        print(f"輸入無效: {e}。請輸入有效的數字。")

語意搜尋流程活動圖

圖表描述 (Alt Text): 此活動圖展示了語意搜尋的完整流程,從輸入文件集開始,經過TF-IDF向量化,再到計算餘弦相似度矩陣,最後進入一個互動迴圈,讓使用者可以持續查詢不同文件間的語意相似度。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 14
title 語意搜尋流程

start
:輸入文件集;
:使用 TF-IDF 進行向量化;
:計算餘弦相似度矩陣;
repeat
  :等待使用者輸入文件索引;
  :計算並顯示該文件與其他文件的相似度;
repeat while (使用者未輸入 'exit')
stop
@enduml

向量相似度在人工智慧代理記憶中的實際應用

在建構人工智慧代理時,向量相似度搜尋技術可以顯著提升系統的語境理解能力。我曾在一個客服機器人專案中實作類別似機制,讓系統能夠記住與使用者的過往往互動,並在適當時機喚回相關記憶。

實作人工智慧代理記憶系統時,我注意到以下幾點特別重要:

記憶粒度的選擇

在設計記憶系統時,需要決定以什麼粒度儲存對話內容。我發現將對話分割為較小的語意單元(如單個問答對或相關的幾個回合)比儲存整個對話歷史更有效。這樣可以實作更精確的相似度比對,避免檢索到過多不相關的內容。

記憶衰減、優先級與整合

為了讓 AI 代理的記憶系統更接近人類,可以引入更複雜的機制。

  1. 記憶衰減機制: 模擬人類記憶隨時間衰減的特性,為較新的記憶賦予更高的權重。
    import math
    from datetime import datetime, timedelta
    
    def apply_time_decay(similarity_score: float, timestamp: datetime, decay_rate: float = 0.1) -> float:
        """應用時間衰減因子到相似度分數"""
        time_diff_days = (datetime.now() - timestamp).total_seconds() / 86400.0
        decay_factor = math.exp(-decay_rate * time_diff_days)
        return similarity_score * decay_factor
    
  2. 記憶優先級與重要性: 並非所有對話都同等重要。可以設計機制來識別並強化重要記憶的權重,例如包含使用者偏好、關鍵決策或高情緒強度的互動。
  3. 記憶整合與摘要: 隨著對話歷史增長,定期將相關的記憶片段合併並生成摘要,既保留關鍵訊息又減少儲存量,以維持檢索效率。
    def consolidate_memories(memory_chunks: list, similarity_threshold: float = 0.8) -> list:
        """整合相似的記憶片段並生成摘要"""
        # ... 函式實作如前所示 ...
        pass
    

智慧記憶管理系統類別圖

圖表描述 (Alt Text): 此類別圖展示了一個智慧記憶管理系統的設計,包含 MemoryManager 核心類別,以及其用於實現記憶衰減、優先級判斷和整合摘要等功能的方法。

@startuml
!theme _none_
skinparam dpi auto
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam minClassWidth 100
skinparam defaultFontSize 14
title 智慧記憶管理系統

class MemoryManager {
  - memories: List[MemoryChunk]
  + add_memory(chunk: MemoryChunk): void
  + retrieve_memory(query: str): List[MemoryChunk]
  + apply_time_decay(score, timestamp): float
  + determine_importance(chunk: MemoryChunk): float
  + consolidate_memories(): void
}

class MemoryChunk {
  + content: str
  + timestamp: datetime
  + importance: float
  + vector: np.array
}

MemoryManager o-- "many" MemoryChunk
@enduml