大語言模型(LLM)的上下文視窗限制和上下文完整性是處理長文字時需要權衡的兩個關鍵因素。LangChain 提供的 CharacterTextSplitter
、TokenTextSplitter
和 RecursiveCharacterTextSplitter
等工具可以有效地應對這個挑戰。CharacterTextSplitter
允許根據字元數量進行分割,而 TokenTextSplitter
則可以更精細地控制分割粒度,並結合 tiktoken
計算 token 數量。RecursiveCharacterTextSplitter
則適用於處理包含多級結構的文字,可以根據換行符、空格等分隔符遞迴地分割文字。理解這些工具的特性和使用方法,可以幫助開發者更好地控制文字分割的粒度和上下文保留,從而提高 LLM 的處理效率和輸出品質。
文字分割器:平衡檔案長度與上下文保留
在處理大型文字時,適當的分割策略對於維持上下文的完整性和模型的處理效率至關重要。如果檔案過長,可能會超出大語言模型(LLM)的上下文視窗限制(即單次請求中LLM能夠處理的最大token數量)。相反,如果檔案被過度分割成小片段,則可能導致重要的上下文資訊丟失,這同樣是不可取的。
文字分割中的挑戰
在進行文字分割時,您可能會遇到特定的挑戰,例如:
- 特殊字元(如#、@符號或連結)可能無法按預期分割,影響分割後檔案的整體結構。
- 如果檔案包含複雜的格式,如表格、列表或多級標題,文字分割器可能會難以保留原始格式。
LangChain中的文字分割器
LangChain 提供了一系列文字分割器,以便您可以輕鬆地按照以下方式進行分割:
- Token數量
- 遞迴多字元分割
- 字元數量
- 程式碼
- Markdown標題
讓我們來探索三種流行的分割器:CharacterTextSplitter
、TokenTextSplitter
和RecursiveCharacterTextSplitter
。
按長度和Token大小進行文字分割
在第三章中,您學習瞭如何使用tiktoken
來計算GPT-4呼叫中的token數量。您也可以使用tiktoken
將字串分割成適當大小的塊和檔案。
程式碼示例:使用CharacterTextSplitter
按Token數量分割
from langchain_text_splitters import CharacterTextSplitter
text = """
Biology is a fascinating and diverse field of science that explores the
living world and its intricacies \n\n. It encompasses the study of life, its
origins, diversity, structure, function, and interactions at various levels
from molecules and cells to organisms and ecosystems \n\n. In this 1000-word
essay, we will delve into the core concepts of biology, its history, key
areas of study, and its significance in shaping our understanding of the
natural world. \n\n ...(truncated to save space)...
"""
# 無區塊重疊:
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
chunk_size=50, chunk_overlap=0, separator="\n",
)
texts = text_splitter.split_text(text)
print(f"Number of texts with no chunk overlap: {len(texts)}")
# 包含區塊重疊:
text_splitter = CharacterTextSplitter.from_tiktoken_encoder(
chunk_size=50, chunk_overlap=48, separator="\n",
)
texts = text_splitter.split_text(text)
print(f"Number of texts with chunk overlap: {len(texts)}")
輸出:
Number of texts with no chunk overlap: 3 Number of texts with chunk overlap: 6
使用TokenTextSplitter
進行精細控制
您可以透過建立一個TextSplitter
並將其附加到您的檔案載入管道來對每個檔案的大小進行更精細的控制:
from langchain.text_splitter import TokenTextSplitter
from langchain_community.document_loaders import PyPDFLoader
text_splitter = TokenTextSplitter(chunk_size=500, chunk_overlap=50)
loader = PyPDFLoader("data/principles_of_marketing_book.pdf")
pages = loader.load_and_split(text_splitter=text_splitter)
print(len(pages)) # 737
使用遞迴字元分割進行文字分割
處理大型文字塊時,遞迴字元分割是一種有效的策略。這種方法透過使用字元列表作為引數,並根據這些字元順序分割文字,將大型文字分成可管理的段落,使進一步的分析更加便捷。
程式碼示例:使用RecursiveCharacterTextSplitter
from langchain_text_splitters import RecursiveCharacterTextSplitter
text = """
Biology is a fascinating and diverse field of science that explores the
living world and its intricacies \n\n. It encompasses the study of life, its
origins, diversity, structure, function, and interactions at various levels
from molecules and cells to organisms and ecosystems \n\n. In this 1000-word
essay, we will delve into the core concepts of biology, its history, key
areas of study, and its significance in shaping our understanding of the
natural world. \n\n ...(truncated to save space)...
"""
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=20,
separators=["\n\n", "\n", " ", ""],
)
texts = text_splitter.split_text(text)
print(f"Number of texts: {len(texts)}")
內容解密:
在上述程式碼中,我們使用了RecursiveCharacterTextSplitter
來對文字進行分割。透過設定chunk_size
和chunk_overlap
引數,我們可以控制每個分割塊的大小以及相鄰塊之間的重疊程度。separators
引數定義了用於分割文字的字元列表,這裡我們使用了\n\n
、\n
、空格和空字串作為分隔符。這種方法可以有效地保留段落、句子和詞彙的語義上下文。
使用LangChain進行文字分割與任務分解的最佳實踐
在處理大型檔案或複雜任務時,LangChain提供了一系列強大的工具來幫助開發者更有效地管理和處理資料。本文將探討如何使用LangChain的文字分割器(TextSplitter)來最佳化檔案處理,以及如何透過任務分解(Task Decomposition)來提升大語言模型(LLM)的效能。
文字分割器(TextSplitter)的使用
文字分割器是LangChain中一個非常有用的工具,它允許開發者將大段文字分割成更小的區塊,以便於後續的處理和分析。以下是如何使用RecursiveCharacterTextSplitter
的一個範例:
from langchain_text_splitters import RecursiveCharacterTextSplitter
# 初始化文字分割器
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=20,
length_function=len,
)
# 將文字分割成小區塊
texts = text_splitter.split_text(text)
# 建立LangChain Document物件
metadatas = {"title": "Biology", "author": "John Doe"}
docs = text_splitter.create_documents(texts, metadatas=[metadatas] * len(texts))
內容解密:
- 初始化文字分割器:我們使用
RecursiveCharacterTextSplitter
來建立一個文字分割器例項,其中chunk_size
設定為100,chunk_overlap
設定為20。這意味著每個文字區塊的大小約為100個字元,並且相鄰區塊之間會有20個字元的重疊,以保持上下文的連貫性。 - 分割文字:透過呼叫
split_text
方法,我們將原始文字分割成多個小區塊,並將結果儲存在texts
列表中。 - 建立Document物件:使用
create_documents
方法,我們可以將分割後的文字區塊轉換成LangChain的Document物件,並為每個檔案新增元資料(如標題和作者)。
處理過長的Document物件
如果現有的Document物件過長,可以使用.split_documents
函式來進一步分割:
text_splitter = RecursiveCharacterTextSplitter(chunk_size=300)
splitted_docs = text_splitter.split_documents(docs)
內容解密:
- 重新設定chunk_size:這裡我們將
chunk_size
調整為300,以適應不同的處理需求。 - 分割Document物件:透過呼叫
split_documents
方法,我們將過長的Document物件分割成更小的部分,以便於後續的處理。
任務分解(Task Decomposition)
任務分解是一種將複雜問題拆解成多個可管理子任務的策略。這種方法不僅適用於軟體工程,也能顯著提升LLM在處理複雜任務時的效能。
任務分解的應用場景
- 複雜問題解決:將複雜問題拆解成多個子任務,每個子任務可以獨立由LLM處理,最終綜合結果。
- 內容生成:將生成長篇內容的任務分解為生成大綱、撰寫個別章節、編譯和潤色最終草稿等步驟。
- 大型檔案摘要:將長篇檔案的摘要任務分解為理解個別章節、獨立摘要、最後編譯最終摘要。
Prompt Chaining
當單一提示無法完成特定任務時,可以利用Prompt Chaining技術,將多個提示輸入/輸出結合起來,建立更複雜的工作流程。
範例:電影製作流程
假設一家電影公司希望部分自動化其電影製作流程,可以將任務分解為角色建立、情節生成、場景/世界構建等步驟,並透過Prompt Chaining來實作這些步驟的有序執行。
graph LR; A[角色建立] --> B[情節生成]; B --> C[場景/世界構建];
圖表翻譯: 此圖示展示了一個順序性的故事創作流程,首先進行角色建立,接著是情節生成,最後是場景和世界構建。這樣的流程有助於電影製作公司系統化地自動化其創作過程。
內容解密:
- 角色建立:利用LLM生成角色描述,包括性格特徵、背景故事等。
- 情節生成:根據角色描述和初始情節構想,生成完整的故事線。
- 場景/世界構建:根據情節,進一步細化場景設計和世界觀構建。
透過這種方式,電影製作公司可以更高效地利用LLM來輔助其創作過程。
使用LangChain進行文字生成的進階技巧:序列鏈(Sequential Chain)實作
在前面的章節中,我們已經瞭解了LangChain的基本概念和簡單應用。在本章中,我們將探討如何使用LangChain進行更複雜的文字生成任務,特別是透過序列鏈(Sequential Chain)的實作來建立一個完整的故事。
任務分解與連結
要建立一個完整的故事,我們需要將任務分解為多個子任務,並將這些子任務連結起來形成一個序列鏈。在這個例子中,我們將任務分解為三個子任務:
- 角色生成(character_generation_chain):根據給定的型別(genre)生成多個角色。
- 情節生成(plot_generation_chain):根據生成的角色和給定的型別建立故事的情節。
- 場景生成(scene_generation_chain):根據角色、型別和情節生成具體的場景。
1. 建立提示範本
首先,我們需要為每個子任務建立相應的提示範本(ChatPromptTemplate)。
from langchain_core.prompts.chat import ChatPromptTemplate
character_generation_prompt = ChatPromptTemplate.from_template(
"""我希望你能為我的短篇小說構思三到五個角色。小說的型別是{genre}。每個角色必須有名字和簡介。
你必須為每個角色提供名字和簡介,這非常重要!
---
示例回應:
名字:CharWiz,簡介:一位精通魔法的巫師。
名字:CharWar,簡介:一位精通劍術的戰士。
---
角色:"""
)
plot_generation_prompt = ChatPromptTemplate.from_template(
"""根據以下角色和型別,建立一個有效的短篇小說情節:
角色:
{characters}
---
型別:{genre}
---
情節:"""
)
scene_generation_plot_prompt = ChatPromptTemplate.from_template(
"""作為一名優秀的內容創作者,根據多個角色和情節,你的任務是生成每個幕的多個有效場景。
你必須將情節分解為多個有效的場景:
---
角色:
{characters}
---
型別:{genre}
---
情節:{plot}
---
示例回應:
場景:
場景1:這裡有一些文字。
場景2:這裡有一些文字。
場景3:這裡有一些文字。
----
場景:
"""
)
#### 內容解密:
此段程式碼定義了三個提示範本,分別用於角色生成、情節生成和場景生成。每個範本都包含特定的指示和預期輸出格式。這樣的設計使得每個子任務都能獲得明確的輸入和輸出指導,從而提高整個故事生成的品質和一致性。
連結子任務
接下來,我們需要使用LangChain的核心功能將這些子任務連結起來。首先,我們需要了解如何使用itemgetter
函式從前一步驟中提取字典中的鍵值。
from operator import itemgetter
from langchain_core.runnables import RunnablePassthrough
chain = RunnablePassthrough() | {
"genre": itemgetter("genre"),
}
chain.invoke({"genre": "fantasy"})
# {'genre': 'fantasy'}
#### 內容解密:
此段程式碼展示瞭如何使用RunnablePassthrough
和itemgetter
來提取輸入字典中的特定鍵值。在這個例子中,我們提取了"genre"
鍵的值,並將其傳遞給下一步。這種技術確保了下游任務能夠獲得所需的輸入資料。
使用Lambda函式進行資料操作
除了提取資料,我們還可以使用Lambda函式或RunnableLambda
來操作前一步驟的資料。
from langchain_core.runnables import RunnableLambda
chain = RunnablePassthrough() | {
"genre": itemgetter("genre"),
"upper_case_genre": lambda x: x["genre"].upper(),
"lower_case_genre": RunnableLambda(lambda x: x["genre"].lower()),
}
chain.invoke({"genre": "fantasy"})
# {'genre': 'fantasy', 'upper_case_genre': 'FANTASY', 'lower_case_genre': 'fantasy'}
#### 內容解密:
此段程式碼展示瞭如何使用Lambda函式和RunnableLambda
來操作輸入資料。在這個例子中,我們將"genre"
的值轉換為大寫和小寫,並將結果儲存在新的鍵中。這種靈活性使得我們能夠在連結中進行各種資料轉換和處理。
使用RunnableParallel進行平行處理
LangChain還提供了RunnableParallel
,允許我們平行處理多個任務。
from langchain_core.runnables import RunnableParallel
master_chain = RunnablePassthrough() | RunnableParallel(
genre=itemgetter("genre"),
upper_case_genre=lambda x: x["genre"].upper(),
lower_case_genre=RunnableLambda(lambda x: x["genre"].lower()),
)
master_chain.invoke({"genre": "Fantasy"})
# {'genre': 'Fantasy', 'upper_case_genre': 'FANTASY', 'lower_case_genre': 'fantasy'}
#### 內容解密:
此段程式碼展示瞭如何使用RunnableParallel
來平行處理多個任務。在這個例子中,我們同時提取了"genre"
的值,並將其轉換為大寫和小寫。這種平行處理方式提高了程式碼的可讀性和效率。
建立子鏈和主鏈
現在,我們可以建立子鏈和主鏈來生成完整的故事。
from langchain_openai.chat_models import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
model = ChatOpenAI()
character_generation_chain = (character_generation_prompt | model | StrOutputParser())
plot_generation_chain = (plot_generation_prompt | model | StrOutputParser())
scene_generation_plot_chain = (scene_generation_plot_prompt | model | StrOutputParser())
master_chain = (
{"characters": character_generation_chain, "genre": RunnablePassthrough()}
| RunnableParallel(
characters=itemgetter("characters"),
genre=itemgetter("genre"),
plot=plot_generation_chain,
)
| RunnableParallel(
characters=itemgetter("characters"),
genre=itemgetter("genre"),
plot=itemgetter("plot"),
scenes=scene_generation_plot_chain,
)
)
story_result = master_chain.invoke({"genre": "Fantasy"})
#### 內容解密:
此段程式碼展示瞭如何建立子鏈和主鏈來生成完整的故事。首先,我們定義了三個子鏈,分別用於角色生成、情節生成和場景生成。然後,我們使用RunnableParallel
將這些子鏈組合成一個主鏈。最後,我們呼叫主鏈並傳入型別引數,生成了一個完整的故事。