一文入門最熱的LLM應用開發框架LangChain

在人工智能領域的不斷髮展中,語言模型扮演着重要的角色。特別是大型語言模型(LLM),如 ChatGPT,已經成爲科技領域的熱門話題,並受到廣泛認可。
在這個背景下,LangChain 作爲一個以 LLM 模型爲核心的開發框架出現,爲自然語言處理開啓了一個充滿可能性的世界。
藉助 LangChain,我們可以創建各種應用程序,包括聊天機器人和智能問答工具。

1. LangChain 簡介

1.1. LangChain 發展史

LangChain 的作者是 Harrison Chase,最初是於 2022 年 10 月開源的一個項目,在 GitHub 上獲得大量關注之後迅速轉變爲一家初創公司。2017 年 Harrison Chase 還在哈佛上大學,如今已是硅谷的一家熱門初創公司的 CEO,這對他來說是一次重大而迅速的躍遷。Insider 獨家報道,人工智能初創公司 LangChain 在種子輪一週後,再次獲得紅杉領投的 2000 萬至 2500 萬美元融資,估值達到 2 億美元。

圖片

1.2.LangChain 爲什麼這麼火

LangChain 目前是有兩個語言版本(python 和 nodejs),從下圖可以看出來,短短半年的時間該項目的 python 版本已經獲得了 54k+的 star。nodejs 版本也在短短 4 個月收貨了 7k+的 star,這無疑利好前端同學,不需要會 python 也能快速上手 LLM 應用開發。

圖片

筆者認爲 Langchain 作爲一個大語言模型應用開發框架,解決了現在開發人工智能應用的一些切實痛點。以 GPT 模型爲例:

1.數據滯後,現在訓練的數據是到 2021 年 9 月。

2.token 數量限制,如果讓它對一個 300 頁的 pdf 進行總結,直接使用則無能爲力。

3.不能進行聯網,獲取不到最新的內容。

4.不能與其他數據源鏈接。

另外作爲一個膠水層框架,極大地提高了開發效率,它的作用可以類比於 jquery 在前端開發中的角色,使得開發者可以更專注於創新和優化產品功能。

1.3. LLM 應用架構

LangChian 作爲一個大語言模型開發框架,是 LLM 應用架構的重要一環。那什麼是 LLM 應用架構呢?其實就是指基於語言模型的應用程序設計和開發的架構。

LangChian 可以將 LLM 模型、向量數據庫、交互層 Prompt、外部知識、外部工具整合到一起,進而可以自由構建 LLM 應用。

 

2. LangChain 組件

圖片

如上圖,LangChain 包含六部分組成,分別爲:Models、Prompts、Indexes、Memory、Chains、Agents。

2.1.Models(模型)

下面我們以具體示例分別闡述下 Chat Modals, Embeddings, LLMs。

2.1.1. 聊天模型

LangChain 爲使用聊天模型提供了一個標準接口。聊天模型是語言模型的一種變體。雖然聊天模型在內部使用語言模型,但它們所提供的接口略有不同。它們不是暴露一個 "輸入文本,輸出文本" 的 API,而是提供了一個以 "聊天消息" 作爲輸入和輸出的接口。

聊天模型的接口是基於消息而不是原始文本。LangChain 目前支持的消息類型有 AIMessage、HumanMessage、SystemMessage 和 ChatMessage,其中 ChatMessage 接受一個任意的角色參數。大多數情況下,您只需要處理 HumanMessage、AIMessage 和 SystemMessage。

# 導入OpenAI的聊天模型,及消息類型
from langchain.chat_models import ChatOpenAI
from langchain.schema import (
    AIMessage,
    HumanMessage,
    SystemMessage
)

# 初始化聊天對象
chat = ChatOpenAI(openai_api_key="...")

# 向聊天模型發問
chat([HumanMessage(content="Translate this sentence from English to French: I love programming.")])

OpenAI 聊天模式支持多個消息作爲輸入。這是一個系統和用戶消息聊天模式的例子:

messages = [
    SystemMessage(content="You are a helpful assistant that translates English to French."),
    HumanMessage(content="I love programming.")
]
chat(messages)

當然也可以進行批量處理,批量輸出。

batch_messages = [
    [
        SystemMessage(content="You are a helpful assistant that translates English to French."),
        HumanMessage(content="I love programming.")
    ],
    [
        SystemMessage(content="You are a helpful assistant that translates English to French."),
        HumanMessage(content="I love artificial intelligence.")
    ],
]
result = chat.generate(batch_messages)
result

上面介紹了聊天的角色處理以及如何進行批量處理消息。我們都知道向 openAI 調用接口都是要花錢的,如果用戶問同一個問題,對結果進行了緩存,這樣就可以減少接口的調用並且也能加快接口返回的速度。LangChain 也很貼心的提供了緩存的功能。並且提供了兩種緩存方案,內存緩存方案和數據庫緩存方案,當然支持的數據庫緩存方案有很多種。

# 導入聊天模型,SQLiteCache模塊
import os
os.environ["OPENAI_API_KEY"] = 'your apikey'
import langchain
from langchain.chat_models import ChatOpenAI
from langchain.cache import SQLiteCache

# 設置語言模型的緩存數據存儲的地址
langchain.llm_cache = SQLiteCache(database_path=".langchain.db")

# 加載 llm 模型
llm = ChatOpenAI()

# 第一次向模型提問
result = llm.predict('tell me a joke')
print(result)

# 第二次向模型提問同樣的問題
result2 = llm.predict('tell me a joke')
print(result2)

另外聊天模式也提供了一種流媒體迴應。這意味着,而不是等待整個響應返回,你就可以開始處理它儘快。

2.1.2. 嵌入

這個更多的是用於文檔、文本或者大量數據的總結、問答場景,一般是和向量庫一起使用,實現向量匹配。其實就是把文本等內容轉成多維數組,可以後續進行相似性的計算和檢索。他相比 fine-tuning 最大的優勢就是,不用進行訓練,並且可以實時添加新的內容,而不用加一次新的內容就訓練一次,並且各方面成本要比 fine-tuning 低很多。

下面以代碼展示下 embeddings 是什麼。

# 導入os, 設置環境變量,導入OpenAI的嵌入模型
import os
from langchain.embeddings.openai import OpenAIEmbeddings
os.environ["OPENAI_API_KEY"] = 'your apikey'

# 初始化嵌入模型
embeddings = OpenAIEmbeddings()

# 把文本通過嵌入模型向量化
res = embeddings.embed_query('hello world')
/*
[
   -0.004845875,   0.004899438,  -0.016358767,  -0.024475135, -0.017341806,
    0.012571548,  -0.019156644,   0.009036391,  -0.010227379, -0.026945334,
    0.022861943,   0.010321903,  -0.023479493, -0.0066544134,  0.007977734,
   0.0026371893,   0.025206111,  -0.012048521,   0.012943339,  0.013094575,
   -0.010580265,  -0.003509951,   0.004070787,   0.008639394, -0.020631202,
  -0.0019203906,   0.012161949,  -0.019194454,   0.030373365, -0.031028723,
   0.0036170771,  -0.007813894, -0.0060778237,  -0.017820721, 0.0048647798,
   -0.015640393,   0.001373733,  -0.015552171,   0.019534737, -0.016169721,
    0.007316074,   0.008273906,   0.011418369,   -0.01390117, -0.033347685,
    0.011248227,  0.0042503807,  -0.012792102, -0.0014595914,  0.028356876,
    0.025407761, 0.00076445413,  -0.016308354,   0.017455231, -0.016396577,
    0.008557475,   -0.03312083,   0.031104341,   0.032389853,  -0.02132437,
    0.003324056,  0.0055610985, -0.0078012915,   0.006090427, 0.0062038545,
  ... 1466 more items
]
*/

下圖是 LangChain 兩種語言包支持的 embeddings。

圖片
2.1.3. 大語言模型

LLMS 是 LangChain 的核心,從官網可以看到 LangChain 繼承了非常多的大語言模型。

圖片

2.2. Prompts(提示詞)

2.2.1. Prompt Templates

LangChain 提供了 PromptTemplates,允許你可以根據用戶輸入動態地更改提示,如果你有編程基礎,這應該對你來說很簡單。當用戶需要輸入多個類似的 prompt 時,生成一個 prompt 模板是一個很好的解決方案,可以節省用戶的時間和精力。下面是一個示例,將 LLM 作爲一個給新開商店命名的顧問,用戶只需告訴 LLM 商店的主要特點,它將返回 10 個新開商店的名字。

from langchain.llms import OpenAI

# 定義生成商店的方法
def generate_store_names(store_features):
    prompt_template = "我正在開一家新的商店,它的主要特點是{}。請幫我想出10個商店的名字。"
    prompt = prompt_template.format(store_features)

    llm = OpenAI()
    response = llm.generate(prompt, max_tokens=10, temperature=0.8)

    store_names = [gen[0].text.strip() for gen in response.generations]
    return store_names

store_features = "時尚、創意、獨特"

store_names = generate_store_names(store_features)
print(store_names)

這樣,用戶只需告訴 LLM 商店的主要特點,就可以獲得 10 個新開商店的名字,而無需重複輸入類似的 prompt 內容。另外LangChainHub包含了許多可以通過 LangChain 直接加載的 Prompt Templates。順便我們也可以通過學習他們的 Prompt 設計來給我們以啓發。

2.2.2. Few-shot example

Few-shot examples 是一組可用於幫助語言模型生成更好響應的示例。

要生成具有 few-shot examples 的 prompt,可以使用 FewShotPromptTemplate。該類接受一個 PromptTemplate 和一組 few-shot examples。然後,它使用這些 few-shot examples 格式化 prompt 模板。

我們再看一個例子,需求是根據用戶輸入,讓模型返回對應的反義詞,我們要通過示例來告訴模型什麼是反義詞, 這就是 few-shot examples(小樣本提示)。

import os
os.environ["OPENAI_API_KEY"] = 'your apikey'
from langchain import PromptTemplate, FewShotPromptTemplate
from langchain.llms import OpenAI

examples = [
    {"word": "黑", "antonym": "白"},
    {"word": "傷心", "antonym": "開心"},
]

example_template = """
單詞: {word}
反義詞: {antonym}\\n
"""

# 創建提示詞模版
example_prompt = PromptTemplate(
    input_variables=["word", "antonym"],
    template=example_template,
)

# 創建小樣本提示詞模版
few_shot_prompt = FewShotPromptTemplate(
    examples=examples,
    example_prompt=example_prompt,
    prefix="給出每個單詞的反義詞",
    suffix="單詞: {input}\\n反義詞:",
    input_variables=["input"],
    example_separator="\\n",
)

# 格式化小樣本提示詞
prompt_text = few_shot_prompt.format(input="粗")

# 調用OpenAI
llm = OpenAI(temperature=0.9)

print(llm(prompt_text))
2.2.3. Example Selector

如果你有大量的示例,則可以使用 ExampleSelector 來選擇最有信息量的一些示例,以幫助你生成更可能產生良好響應的提示。接下來,我們將使用 LengthBasedExampleSelector,根據輸入的長度選擇示例。當你擔心構造的提示將超過上下文窗口的長度時,此方法非常有用。對於較長的輸入,它會選擇包含較少示例的提示,而對於較短的輸入,它會選擇包含更多示例。

import os
os.environ["OPENAI_API_KEY"] = 'your apikey'
from langchain.prompts import PromptTemplate, FewShotPromptTemplate
from langchain.prompts.example_selector import LengthBasedExampleSelector
from langchain.prompts.example_selector import LengthBasedExampleSelector


# These are a lot of examples of a pretend task of creating antonyms.
examples = [
    {"word": "happy", "antonym": "sad"},
    {"word": "tall", "antonym": "short"},
    {"word": "energetic", "antonym": "lethargic"},
    {"word": "sunny", "antonym": "gloomy"},
    {"word": "windy", "antonym": "calm"},
]
# 例子格式化模版
example_formatter_template = """
Word: {word}
Antonym: {antonym}\n
"""
example_prompt = PromptTemplate(
    input_variables=["word", "antonym"],
    template=example_formatter_template,
)

# 使用 LengthBasedExampleSelector來選擇例子
example_selector = LengthBasedExampleSelector(
    examples=examples,
    example_prompt=example_prompt,
    # 最大長度
    max_length=25,
)

# 使用'example_selector'創建小樣本提示詞模版
dynamic_prompt = FewShotPromptTemplate(
    example_selector=example_selector,
    example_prompt=example_prompt,
    prefix="Give the antonym of every input",
    suffix="Word: {input}\nAntonym:",
    input_variables=["input"],
    example_separator="\n\n",
)

longString = "big and huge and massive and large and gigantic and tall and much much much much much bigger than everything else"

print(dynamic_prompt.format(input=longString))

另外官方也提供了根據最大邊際相關性、文法重疊、語義相似性來選擇示例。

2.3. Indexes(索引)

索引是指對文檔進行結構化的方法,以便 LLM 能夠更好的與之交互。該組件主要包括:Document Loaders(文檔加載器)、Text Splitters(文本拆分器)、VectorStores(向量存儲器)以及 Retrievers(檢索器)。

2.3.1. Document Loaders

指定源進行加載數據的。將特定格式的數據,轉換爲文本。如 CSV、File Directory、HTML、

JSON、Markdown、PDF。另外使用相關接口處理本地知識,或者在線知識。如 AirbyteJSON

Airtable、Alibaba Cloud MaxCompute、wikipedia、BiliBili、GitHub、GitBook 等等。

2.3.2. Text Splitters

由於模型對輸入的字符長度有限制,我們在碰到很長的文本時,需要把文本分割成多個小的文本片段。

文本分割最簡單的方式是按照字符長度進行分割,但是這會帶來很多問題,比如說如果文本是一段代碼,一個函數被分割到兩段之後就成了沒有意義的字符,所以整體的原則是把語義相關的文本片段放在一起。

LangChain 中最基本的文本分割器是 CharacterTextSplitter ,它按照指定的分隔符(默認“\n\n”)進行分割,並且考慮文本片段的最大長度。我們看個例子:

from langchain.text_splitter import CharacterTextSplitter

# 初始字符串
state_of_the_union = "..."

text_splitter = CharacterTextSplitter(
    separator = "\\n\\n",
    chunk_size = 1000,
    chunk_overlap  = 200,
    length_function = len,
)

texts = text_splitter.create_documents([state_of_the_union])

除了 CharacterTextSplitter 以外,LangChain 還支持多個高級文本分割器,如下:

圖片
2.3.3. VectorStores

存儲提取的文本向量,包括 Faiss、Milvus、Pinecone、Chroma 等。如下是 LangChain 集成的向量數據庫。

圖片
2.3.4. Retrievers

檢索器是一種便於模型查詢的存儲數據的方式,LangChain 約定檢索器組件至少有一個方法 get_relevant_texts,這個方法接收查詢字符串,返回一組文檔。下面是一個簡單的列子:

from langchain.chains import RetrievalQA
from langchain.llms import OpenAI
from langchain.document_loaders import TextLoader
from langchain.indexes import VectorstoreIndexCreator
loader = TextLoader('../state_of_the_union.txt', encoding='utf8')

# 對加載的內容進行索引
index = VectorstoreIndexCreator().from_loaders([loader])

query = "What did the president say about Ketanji Brown Jackson"

# 通過query的方式找到語義檢索的結果
index.query(query)

2.4. Chains(鏈)

鏈允許我們將多個組件組合在一起以創建一個單一的、連貫的任務。例如,我們可以創建一個鏈,它接受用戶輸入,使用 PromptTemplate 對其進行格式化,然後將格式化的響應傳遞給 LLM。另外我們也可以通過將多個鏈組合在一起,或者將鏈與其他組件組合來構建更復雜的鏈。

2.4.1. LLMChain

LLMChain 是一個簡單的鏈,它圍繞語言模型添加了一些功能。它在整個 LangChain 中廣泛使用,包括在其他鏈和代理中。它接受一個提示模板,將其與用戶輸入進行格式化,並返回 LLM 的響應。

from langchain import PromptTemplate, OpenAI, LLMChain

prompt_template = "What is a good name for a company that makes {product}?"

llm = OpenAI(temperature=0)
llm_chain = LLMChain(
    llm=llm,
    prompt=PromptTemplate.from_template(prompt_template)
)
llm_chain("colorful socks")

除了所有 Chain 對象共享的call和 run 方法外,LLMChain 還提供了一些調用得方法,如下是不同調用方法的說明.

● call方法返回輸入和輸出鍵值。

另外可以通過將 return_only_outputs 設置爲 True,可以將其配置爲只返回輸出鍵值。

llm_chain("corny", return_only_outputs=True)
    {'text': 'Why did the tomato turn red? Because it saw the salad dressing!'}

● run 方法返回的是字符串而不是字典。

llm_chain.run({"adjective": "corny"})
    'Why did the tomato turn red? Because it saw the salad dressing!'

● apply 方法允許你對一個輸入列表進行調用

input_list = [
    {"product": "socks"},
    {"product": "computer"},
    {"product": "shoes"}
]

llm_chain.apply(input_list)
    [{'text': '\n\nSocktastic!'},
     {'text': '\n\nTechCore Solutions.'},
     {'text': '\n\nFootwear Factory.'}]

● generate 方法類似於 apply 方法,但它返回的是 LLMResult 而不是字符串。LLMResult 通常包含有用的生成信息,例如令牌使用情況和完成原因。

llm_chain.generate(input_list)
    LLMResult(generations=[[Generation(text='\n\nSocktastic!', generation_info={'finish_reason': 'stop', 'logprobs': None})], [Generation(text='\n\nTechCore Solutions.', generation_info={'finish_reason': 'stop', 'logprobs': None})], [Generation(text='\n\nFootwear Factory.', generation_info={'finish_reason': 'stop', 'logprobs': None})]], llm_output={'token_usage': {'prompt_tokens': 36, 'total_tokens': 55, 'completion_tokens': 19}, 'model_name': 'text-davinci-003'})

● predict 方法類似於 run 方法,不同之處在於輸入鍵被指定爲關鍵字參數,而不是一個 Python 字典。

# Single input example
llm_chain.predict(product="colorful socks")
2.4.2. SimpleSequentialChain

順序鏈的最簡單形式,其中每個步驟都有一個單一的輸入/輸出,並且一個步驟的輸出是下一步的輸入。

圖片

如下就是將兩個 LLMChain 進行組合成順序鏈進行調用的案例。

from langchain.llms import OpenAI
from langchain.chains import LLMChain
from langchain.prompts import PromptTemplate
from langchain.chains import SimpleSequentialChain

# 定義第一個chain
llm = OpenAI(temperature=.7)
template = """You are a playwright. Given the title of play, it is your job to write a synopsis for that title.

Title: {title}
Playwright: This is a synopsis for the above play:"""
prompt_template = PromptTemplate(input_variables=["title"], template=template)
synopsis_chain = LLMChain(llm=llm, prompt=prompt_template)

# 定義第二個chain

llm = OpenAI(temperature=.7)
template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play.

Play Synopsis:
{synopsis}
Review from a New York Times play critic of the above play:"""
prompt_template = PromptTemplate(input_variables=["synopsis"], template=template)
review_chain = LLMChain(llm=llm, prompt=prompt_template)

# 通過簡單順序鏈組合兩個LLMChain
overall_chain = SimpleSequentialChain(chains=[synopsis_chain, review_chain], verbose=True)

# 執行順序鏈
review = overall_chain.run("Tragedy at sunset on the beach")
2.4.3. SequentialChain

相比 SimpleSequentialChain 只允許有單個輸入輸出,它是一種更通用的順序鍊形式,允許多個輸入/輸出。

特別重要的是: 我們如何命名輸入/輸出變量名稱。在上面的示例中,我們不必考慮這一點,因爲我們只是將一個鏈的輸出直接作爲輸入傳遞給下一個鏈,但在這裏我們確實需要擔心這一點,因爲我們有多個輸入。

第一個 LLMChain:

# 這是一個 LLMChain,根據戲劇的標題和設定的時代,生成一個簡介。
llm = OpenAI(temperature=.7)
template = """You are a playwright. Given the title of play and the era it is set in, it is your job to write a synopsis for that title.
# 這裏定義了兩個輸入變量title和era,並定義一個輸出變量:synopsis
Title: {title}
Era: {era}
Playwright: This is a synopsis for the above play:"""
prompt_template = PromptTemplate(input_variables=["title", "era"], template=template)
synopsis_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="synopsis")

第二個 LLMChain:

# 這是一個 LLMChain,根據劇情簡介撰寫一篇戲劇評論。
llm = OpenAI(temperature=.7)
template = """You are a play critic from the New York Times. Given the synopsis of play, it is your job to write a review for that play.
# 定義了一個輸入變量:synopsis,輸出變量:review
Play Synopsis:
{synopsis}
Review from a New York Times play critic of the above play:"""
prompt_template = PromptTemplate(input_variables=["synopsis"], template=template)
review_chain = LLMChain(llm=llm, prompt=prompt_template, output_key="review")

執行順序鏈:

overall_chain({"title":"Tragedy at sunset on the beach", "era": "Victorian England"})

執行結果,可以看到會把每一步的輸出都能打印出來。

    > Entering new SequentialChain chain...

    > Finished chain.

    {'title': 'Tragedy at sunset on the beach',
     'era': 'Victorian England',
     'synopsis': "xxxxxx",
     'review': "xxxxxxx"}
2.4.4. TransformChain

轉換鏈允許我們創建一個自定義的轉換函數來處理輸入,將處理後的結果用作下一個鏈的輸入。如下示例我們將創建一個轉換函數,它接受超長文本,將文本過濾爲僅前 3 段,然後將其傳遞到 LLMChain 中以總結這些內容。

from langchain.chains import TransformChain, LLMChain, SimpleSequentialChain
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate

# 模擬超長文本
with open("../../state_of_the_union.txt") as f:
    state_of_the_union = f.read()

# 定義轉換方法,入參和出參都是字典,取前三段
def transform_func(inputs: dict) -> dict:
    text = inputs["text"]
    shortened_text = "\n\n".join(text.split("\n\n")[:3])
    return {"output_text": shortened_text}

# 轉換鏈:輸入變量:text,輸出變量:output_text
transform_chain = TransformChain(
    input_variables=["text"], output_variables=["output_text"], transform=transform_func
)
# prompt模板描述
template = """Summarize this text:

{output_text}

Summary:"""
# prompt模板
prompt = PromptTemplate(input_variables=["output_text"], template=template)
# llm鏈
llm_chain = LLMChain(llm=OpenAI(), prompt=prompt)
# 使用順序鏈
sequential_chain = SimpleSequentialChain(chains=[transform_chain, llm_chain])
# 開始執行
sequential_chain.run(state_of_the_union)
# 結果
"""
    ' The speaker addresses the nation, noting that while last year they were kept apart due to COVID-19, this year they are together again.
    They are reminded that regardless of their political affiliations, they are all Americans.'

"""

2.5. Memory(記憶)

熟悉 openai 的都知道,openai 提供的聊天接口 api,本身是不具備“記憶的”能力。如果想要使聊天具有記憶功能,則需要我們自行維護聊天記錄,即每次把聊天記錄發給 gpt。具體過程如下

第一次發送:

import openai

openai.ChatCompletion.create(
  model="gpt-3.5-turbo",
  messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Hello"},
    ]
)

第二次發送就要帶上我們第一次的記錄:

import openai

openai.ChatCompletion.create(
  model="gpt-3.5-turbo",
  messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Hello"},
        {"role": "assistant", "content": "Hello, how can I help you?"},
        {"role": "user", "content": "who is more stylish Pikachu or Neo"},
    ]
)

那如果我們一直聊天下去,發送的內容也越來越多,那很可能就碰到 token 的限制。聰明的同學會發現,其實我們只保留最近幾次的聊天記錄就可以了。沒錯,其實 LangChain 也是這樣實現的,不過 LangChain 提供了更多的方法。

langchain 提供了不同的 Memory 組件完成內容記憶,如下是目前提供的組件。

2.5.1. ConversationBufferMemory

該組件類似我們上面的描述,只不過它會將聊天內容記錄在內存中,而不需要每次再手動拼接聊天記錄。

2.5.2. ConversationBufferWindowMemory

相比較第一個記憶組件,該組件增加了一個窗口參數,會保存最近看 k 論的聊天內容。

2.5.3. ConversationTokenBufferMemory

在內存中保留最近交互的緩衝區,並使用 token 長度而不是交互次數來確定何時刷新交互。

2.5.4. ConversationSummaryMemory

相比第一個記憶組件,該組件只會存儲一個用戶和機器人之間的聊天內容的摘要。

2.5.5. ConversationSummaryBufferMemory

結合了上面兩個思路,存儲一個用戶和機器人之間的聊天內容的摘要並使用 token 長度來確定何時刷新交互。

2.5.6. VectorStoreRetrieverMemory

它是將所有之前的對話通過向量的方式存儲到 VectorDB(向量數據庫)中,在每一輪新的對話中,會根據用戶的輸入信息,匹配向量數據庫中最相似的 K 組對話。

2.6. Agents(代理)

一些應用程序需要根據用戶輸入靈活地調用 LLM 和其他工具的鏈。代理接口爲這樣的應用程序提供了靈活性。代理可以訪問一套工具,並根據用戶輸入確定要使用哪些工具。我們可以簡單的理解爲他可以動態的幫我們選擇和調用 chain 或者已有的工具。代理主要有兩種類型 Action agents 和 Plan-and-execute agents。

2.6.1. Action agents

行爲代理: 在每個時間步,使用所有先前動作的輸出來決定下一個動作。下圖展示了行爲代理執行的流程。

圖片
2.6.2. Plan-and-execute agents

預先決定完整的操作順序,然後執行所有操作而不更新計劃,下面是其流程。

● 接收用戶輸入

● 計劃要採取的完整步驟順序

● 按順序執行步驟,將過去步驟的輸出作爲未來步驟的輸入傳遞

 

3. LangChain 實戰

3.1. 完成一次問答

LangChain 加載 OpenAI 的模型,並且完成一次問答。

先設置我們的 openai 的 key,然後,我們進行導入和執行。

# 導入os, 設置環境變量,導入OpenAI模型
import os
os.environ["OPENAI_API_KEY"] = '你的api key'
from langchain.llms import OpenAI

# 加載 OpenAI 模型,並指定模型名字
llm = OpenAI(model_name="text-davinci-003",max_tokens=1024)

# 向模型提問
result = llm("怎麼評價人工智能")

3.2. 通過谷歌搜索並返回答案

爲了實現我們的項目,我們需要使用 Serpapi 提供的 Google 搜索 API 接口。首先,我們需要在 Serpapi 官網上註冊一個用戶,並複製由 Serpapi 生成的 API 密鑰。接下來,我們需要將這個 API 密鑰設置爲環境變量,就像我們之前設置 OpenAI API 密鑰一樣。

# 導入os, 設置環境變量
import os
os.environ["OPENAI_API_KEY"] = '你的api key'
os.environ["SERPAPI_API_KEY"] = '你的api key'

然後,開始編寫我的代碼。

# 導入加載工具、初始化代理、代理類型及OpenAI模型
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.llms import OpenAI

# 加載 OpenAI 模型
llm = OpenAI(temperature=0)

# 加載 serpapi、語言模型的數學工具
tools = load_tools(["serpapi", "llm-math"], llm=llm)

# 工具加載後都需要初始化,verbose 參數爲 True,會打印全部的執行詳情
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)

# 執行代理
agent.run("今天是幾號?歷史上的今天發生了什麼事情")
圖片

可以看到,正確的返回了日期(有時差),並且返回了歷史上的今天。並且通過設置 verbose 這個參數爲 True,可以看到完整的 chain 執行過程。將我們的問題拆分成了幾個步驟,然後一步一步得到最終的答案。

3.3. 對超長文本進行總結

假如我們想要用 openai api 對一個段文本進行總結,我們通常的做法就是直接發給 api 讓他總結。但是如果文本超過了 api 最大的 token 限制就會報錯。這時,我們一般會進行對文章進行分段,比如通過 tiktoken 計算並分割,然後將各段發送給 api 進行總結,最後將各段的總結再進行一個全部的總結。

LangChain 很好的幫我們處理了這個過程,使得我們編寫代碼變的非常簡單。

# 導入os,設置環境變量。導入文本加載器、總結鏈、文本分割器及OpenAI模型
import os
os.environ["OPENAI_API_KEY"] = '你的api key'
from langchain.document_loaders import TextLoader
from langchain.chains.summarize import load_summarize_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain import OpenAI

# 獲取當前腳本所在的目錄
base_dir = os.path.dirname(os.path.abspath(__file__))

# 構建doc.txt文件的路徑
doc_path = os.path.join(base_dir, 'static', 'open.txt')

# 通過文本加載器加載文本
loader = TextLoader(doc_path)

# 將文本轉成 Document 對象
document = loader.load()

# 初始化文本分割器
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 800,
    chunk_overlap = 0
)

# 切分文本
split_documents = text_splitter.split_documents(document)

# 加載 llm 模型
llm = OpenAI(model_name="text-davinci-003", max_tokens=1500)

# 創建總結鏈
chain = load_summarize_chain(llm, chain_type="refine", verbose=True)

# 執行總結鏈
chain.run(split_documents)

這裏解釋下文本分割器的 chunk_overlap 參數和 chain 的 chain_type 參數。

chunk_overlap 是指切割後的每個 document 裏包含幾個上一個 document 結尾的內容,主要作用是爲了增加每個 document 的上下文關聯。比如,chunk_overlap=0 時, 第一個 document 爲 aaaaaa,第二個爲 bbbbbb;當 chunk_overlap=2 時,第一個 document 爲 aaaaaa,第二個爲 aabbbbbb。

chain_type 主要控制了將 document 傳遞給 llm 模型的方式,一共有 4 種方式:

stuff: 這種最簡單粗暴,會把所有的 document 一次全部傳給 llm 模型進行總結。如果 document 很多的話,勢必會報超出最大 token 限制的錯,所以總結文本的時候一般不會選中這個。

map_reduce: 這個方式會先將每個 document 進行總結,最後將所有 document 總結出的結果再進行一次總結。

圖片

refine: 這種方式會先總結第一個 document,然後在將第一個 document 總結出的內容和第二個 document 一起發給 llm 模型在進行總結,以此類推。這種方式的好處就是在總結後一個 document 的時候,會帶着前一個的 document 進行總結,給需要總結的 document 添加了上下文,增加了總結內容的連貫性。

圖片

map_rerank: 這種一般不會用在總結的 chain 上,而是會用在問答的 chain 上,他其實是一種搜索答案的匹配方式。首先你要給出一個問題,他會根據問題給每個 document 計算一個這個 document 能回答這個問題的概率分數,然後找到分數最高的那個 document ,在通過把這個 document 轉化爲問題的 prompt 的一部分(問題+document)發送給 llm 模型,最後 llm 模型返回具體答案。

3.4. 構建本地知識庫問答機器人

通過這個可以很方便的做一個可以介紹公司業務的機器人,或是介紹一個產品的機器人。這裏主要使用了 Embedding(相關性)的能力。

```
導入os,設置環境變量。導入OpenAI嵌入模型、Chroma向量數據庫、文本分割器、OpenAI模型、向量數據庫數據查詢模塊及文件夾文檔加載器
```

import os
os.environ["OPENAI_API_KEY"] = '你的api key'
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.text_splitter import CharacterTextSplitter
from langchain import OpenAI,VectorDBQA
from langchain.document_loaders import DirectoryLoader

# 獲取當前腳本所在的目錄
base_dir = os.path.dirname(os.path.abspath(__file__))

# 構建doc.txt文件的路徑
doc_Directory = os.path.join(base_dir, 'static')

# 加載文件夾中的所有txt類型的文件
loader = DirectoryLoader(doc_Directory, glob='**/*.txt')

# 將數據轉成 document 對象,每個文件會作爲一個 document
documents = loader.load()

# 初始化加載器
text_splitter = CharacterTextSplitter(chunk_size=100, chunk_overlap=0)

# 切割加載的 document
split_docs = text_splitter.split_documents(documents)

# 初始化 openai 的 embeddings 對象
embeddings = OpenAIEmbeddings()

# 將 document 通過 openai 的 embeddings 對象計算 embedding 向量信息並臨時存入 Chroma 向量數據庫,用於後續匹配查詢
docsearch = Chroma.from_documents(split_docs, embeddings)

# 創建問答對象
qa = VectorDBQA.from_chain_type(llm=OpenAI(), chain_type="stuff", vectorstore=docsearch,return_source_documents=True)

# 進行問答
result = qa({"query": "一年收入是多少?"})
圖片

上圖中成功的從我們的給到的數據中獲取了正確的答案。

3.5.構建向量索引數據庫

🏡 Home | Chroma

我們上個案例裏面有一步是將 document 信息轉換成向量信息和 embeddings 的信息並臨時存入 Chroma 數據庫。

因爲是臨時存入,所以當我們上面的代碼執行完成後,上面的向量化後的數據將會丟失。如果想下次使用,那麼就還需要再計算一次 embeddings,這肯定不是我們想要的。 LangChain 支持的數據庫有很多,這個案例介紹下通過 Chroma 個數據庫來講一下如何做向量數據持久化。

chroma 是個本地的向量數據庫,他提供的一個 persist_directory 來設置持久化目錄進行持久化。讀取時,只需要調取 from_document 方法加載即可。

from langchain.vectorstores import Chroma

# 持久化數據
docsearch = Chroma.from_documents(documents, embeddings, persist_directory="D:/vector_store")
docsearch.persist()

# 從已有文件中加載數據
docsearch = Chroma(persist_directory="D:/vector_store", embedding_function=embeddings)

3.6.基於 LangChain 構建的開源應用

基於 LangChain 的優秀項目資源庫

基於 LangChain 和 ChatGLM-6B 等系列 LLM 的針對本地知識庫的自動問答

 

4. 總結

隨着 LangChain 不斷迭代和優化,它的功能將變得越來越強大,支持的範圍也將更廣泛。無論是處理複雜的語言模型還是解決各種實際問題,LangChain 都將展現出更高的實力和靈活性。然而,我必須承認,我的理解能力和解釋能力是有限的,可能會出現錯誤或者解釋不夠清晰。因此,懇請讀者們諒解。

5、參考文獻

● LangChain | LangChain

● LangChain 中文入門教程 - LangChain 的中文入門教程

 

作者: 騰訊應用寶 MoonWebTeam 團隊  jansezhou

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章