文為筆者學習LangChain時對官方文檔以及一系列資料進行一些總結~覆蓋對Langchain的核心六大模塊的理解與核心使用方法,全文篇幅較長,共計50000+字,可先碼住輔助用于學習Langchain。
如今各類AI模型層出不窮,百花齊放,大佬們開發的速度永遠遙遙領先于學習者的學習速度。。為了解放生產力,不讓應用層開發人員受限于各語言模型的生產部署中..LangChain橫空出世界。
Langchain可以說是現階段十分值得學習的一個AI架構,那么究竟它有什么魔法才會配享如此高的地位呢?會不會學習成本很高?不要擔心!Langchain雖然功能強大,但其實它就是一個為了提升構建LLM相關應用效率的一個工具,我們也可以將它理解成一個“說明書",是的,只是一個“說明書”!它標準的定義了我們在構建一個LLM應用開發時可能會用到的東西。比如說在之前寫過的AI文章中介紹的prompt,就可以通過Langchain中的PromptTemplate進行格式化:
prompt = """Translate the text \
that is delimited by triple backticks \
into a style that is {style}. \
text: ```{text}```
"""
當我們調用ChatPromptTemplate進行標準化時
from langchain.prompts import ChatPromptTemplate
prompt_template=ChatPromptTemplate.from_template(prompt)
print(prompt_template,'ChatPromptTemplate')
該prompt就會被格式化成:
從上述例子,可以直觀的看到ChatPromptTemplate可以將prompt中聲明的輸入變量style和text準確提取出來,使prompt更清晰。當然,Langchain對于prompt的優化不止這一種方式,它還提供了各類其他接口將prompt進一步優化,這里只是舉例一個較為基礎且直觀的方法,讓大家感受一下。
Langchain其實就是在定義多個通用類的規范,去優化開發AI應用過程中可能用到的各類技術,將它們抽象成多個小元素,當我們構建應用時,直接將這些元素堆積起來,而無需在重復的去研究各"元素"實現的細枝末節。
毋庸置疑,想要學習Langchain最簡單直接的方法就是閱讀官方文檔,先貼一個鏈接Langchain官方文檔
通過文檔目錄我們可以看到,Langchain由6個module組成,分別是Model IO、Retrieval、Chains、Memory、Agents和Callbacks。
Model IO:AI應用的核心部分,其中包括輸入、Model和輸出。
Retrieval:“檢索“——該功能與向量數據密切庫相關,是在向量數據庫中搜索與問題相關的文檔內容。
Memory:為對話形式的模型存儲歷史對話記錄,在長對話過程中隨時將這些歷史對話記錄重新加載,以保證對話的準確度。
Chains:雖然通過Model IO、Retrieval和Memory這三大模塊可以初步完成應用搭建,但是若想實現一個強大且復雜的應用,還是需要將各模塊組合起來,這時就可以利用Chains將其連接起來,從而豐富功能。
Agents:它可以通過用戶的輸入,理解用戶的意圖,返回一個特定的動作類型和參數,從而自主調用相關的工具來滿足用戶的需求,將應用更加智能化。
Callbacks: 回調機制可以調用鏈路追蹤,記錄日志,幫助開發者更好的調試LLM模型。
六個module具體的關系如下圖所示(圖片來源于網絡):
好了,說到這我們只要一個一個module去攻破,最后將他們融會貫通,也就成為一名及格的Langchain學習者了。
這一部分可以說是Langchain的核心部分,引用一下之前介紹AI時用過的圖,介紹了Model IO內部的一些具體實現原理)
由上圖可以看出:我們在利用Model IO的時候主要關注的就是輸入、處理、輸出這三個步驟。Langchain也是根據這一點去實現Model IO這一模塊的,在這一模塊中,Langchain針對此模塊主要的實現手段為:Prompt(輸入)、Language model(處理)、Output Pasers(輸出),Langchain通過一系列的技術手法優化這三步,使得其更加的標準化,我們也無需再關注每一步驟中的具體實現,可以直接通過Langchain提供的API,堆積木式的完善我們應用構建(貼張官方文檔的圖,可以更清晰的了解)。
既然我們無需再關注每一步驟的具體實現,所以使用Langchain的Model IO應用時,主要關注的就是prompt的構建了。下文將主要介紹Langchain中常用的一些prompt構建方法。
Langchain對于prompt的優化:主要是致力于將其優化成為可移植性高的Prompt,以便更好的支持各類LLM,無需在切換Model時修改Prompt。 通過官方文檔可以看到,Prompt在Langchain被分成了兩大類,一類是Prompt template,另一類則是Selectors。
Propmpt template:這個其實很好理解就是利用Langchain接口將prompt按照template進行一定格式化,針對Prompt進行變量處理以及提示詞的組合。
Selectors: 則是指可以根據不同的條件去選擇不同的提示詞,或者在不同的情況下通過Selector,選擇不同的example去進一步提高Prompt支持能力。
在prompt中有兩種類型的模版格式,一是f-string,這是十分常見的一類prompt,二是jinja2。
f-string 是 Python 3.6 以后版本中引入的一種特性,用于在字符串中插入表達式的值。語法簡潔,直接利用{}花括號包裹變量或者表達式,即可執行簡單的運算,性能較好,但是只限用在py中。
#使用 Python f 字符串模板:
from langchain.prompts import PromptTemplate
fstring_template = """Tell me a {adjective} joke about {content}"""
prompt = PromptTemplate.from_template(fstring_template)
print(prompt.format(adjective="funny", content="chickens"))
# Output: Tell me a funny joke about chickens.
jinja2常被應用于網頁開發,與 Flask 和 Django 等框架結合使用。它不僅支持變量替換,還支持其他的控制結構(例如循環和條件語句)以及自定義過濾器和宏等高級功能。此外,它的可用性范圍更廣,可在多種語境下使用。但與 f-string 不同,使用 jinja2 需要安裝相應的庫。
#使用 jinja2 模板:
from langchain.prompts import PromptTemplate
jinja2_template = "Tell me a {{ adjective }} joke about {{ content }}"
prompt = PromptTemplate.from_template(jinja2_template, template_format="jinja2")
print(prompt.format(adjective="funny", content="chickens"))
# Output: Tell me a funny joke about chickens.
總結一下:如果只需要基本的字符串插值和格式化,首選f-string ,因為它的語法簡潔且無需額外依賴。但如果需要更復雜的模板功能(例如循環、條件、自定義過濾器等),jinja2 更合適。
3.1.1.2Propmpt Template:
在prompt template這一部分中需要掌握的幾個概念:
1??基本提示模版:
大多是字符串或者是由對話組成的數組對象。 對于創建字符串類型的prompt要了解兩個概念,一是input_variables 屬性,它表示的是prompt所需要輸入的變量。二是format,即通過input_variables將prompt格式化。比如利用PromptTemplate進行格式化。
from langchain.prompts import PromptTemplate #用于 PromptTemplate 為字符串提示創建模板。
#默認情況下, PromptTemplate 使用 Python 的 str.format 語法進行模板化;但是可以使用其他模板語法(例如, jinja2 )
prompt_template = PromptTemplate.from_template("Tell me a {adjective} joke about {content}.")
print(prompt_template.format(adjective="funny", content="chickens"))
Output如下(該例子就是將兩個input_variables分別設置為funny和chickens,然后利用format分別進行賦值。若在template中聲明了input_variables,利用format進行格式化時就一定要賦值,否則會報錯,當在template中未設置input_variables,則會自動忽略。)
Tell me a funny joke about chickens.
當對對話類型的prompt進行格式化的時候,可以利用ChatPromptTemplate進行:
#ChatPromptTemplate.from_messages 接受各種消息表示形式。
template = ChatPromptTemplate.from_messages([
("system", "You are a helpful AI bot. Your name is {name}."),
("human", "Hello, how are you doing?"),
("ai", "I'm doing well, thanks!"),
("human", "{user_input}"),
])
messages = template.format_messages(
name="Bob",
user_input="What is your name?"
)
print(messages)
Output如下(可以看到,ChatPromptTemplate會根據role,對每一句進行標準格式化。除了此類方法,也可以直接指定身份模塊如SystemMessage, HumanMessagePromptTemplate進行格式化,這里不再贅述。)
解釋
[('system', 'You are a helpful AI bot. Your name is Bob.'),
('human', 'Hello, how are you doing?'),
('ai', "I'm doing well, thanks!"),
('human', 'What is your name?')]
2??部分提示詞模版:
在生成prompt前就已經提前初始化部分的提示詞,實際進一步導入模版的時候只導入除已初始化的變量即可。通常部分提示詞模版會被用在全局設置上,如下示例,在正式format前設定foo值為foo,這樣在生成最終prompt的時候只需要指定bar的值即可。有兩種方法去指定部分提示詞:
解釋
from langchain.prompts import PromptTemplate
prompt = PromptTemplate(template="{foo}{bar}", input_variables=["foo", "bar"])
# 可以使用 PromptTemplate.partial() 方法創建部分提示模板。
partial_prompt = prompt.partial(foo="foo")
print(partial_prompt.format(bar="baz"))
#也可以只使用分部變量初始化提示。
prompt = PromptTemplate(template="{foo}{bar}", input_variables=["bar"], partial_variables={"foo": "foo"})
print(prompt.format(bar="baz"))
Output如下:
foobaz
foobaz
此外,我們也可以將函數的最終值作為prompt的一部分進行返回,如下例子,如果想在prompt中實時展示當下時間,我們可以直接聲明一個函數用來返回當下時間,并最終將該函數拼接到prompt中去:
解釋
from datetime import datetime
def _get_datetime():
now = datetime.now()
return now.strftime("%m/%d/%Y, %H:%M:%S")
prompt = PromptTemplate(
template="Tell me a {adjective} joke about the day {date}",
input_variables=["adjective", "date"]
)
partial_prompt = prompt.partial(date=_get_datetime)
print(partial_prompt.format(adjective="funny"))
# 除上述方法,部分函數聲明和普通的prompt一樣,也可以直接用partial_variables去聲明
prompt = PromptTemplate(
template="Tell me a {adjective} joke about the day {date}",
input_variables=["adjective"],
partial_variables={"date": _get_datetime})
Output如下:
Tell me a funny joke about the day 12/08/2022, 16:25:30
3??組成提示詞模版:
可以通過PromptTemplate.compose()方法將多個提示詞組合到一起。如下示例,生成了full_prompt和introduction_prompt進行近一步組合。
解釋
from langchain.prompts.pipeline import PipelinePromptTemplate
from langchain.prompts.prompt import PromptTemplate
full_template = """{introduction}
{example}
"""
full_prompt = PromptTemplate.from_template(full_template)
introduction_template = """You are impersonating Elon Musk."""
introduction_prompt = PromptTemplate.from_template(introduction_template)
example_template = """Here's an example of an interaction """
example_prompt = PromptTemplate.from_template(example_template)
input_prompts = [("introduction", introduction_prompt),
("example", example_prompt),]
pipeline_prompt = PipelinePromptTemplate(final_prompt=full_prompt, pipeline_prompts=input_prompts)
4??自定義提示模版:
在創建prompt時,我們也可以按照自己的需求去創建自定義的提示模版。官方文檔舉了一個生成給定名稱的函數的英語解釋例子,在這個例子中函數名稱作為輸入,并設置提示格式以提供函數的源代碼:
解釋
import inspect
# 該函數將返回給定其名稱的函數的源代碼。 inspect作用就是獲取源代碼
defget_source_code(function_name):
# Get the source code of the function
return inspect.getsource(function_name)
# 測試函數
deftest():
return1 + 1
from langchain.prompts import StringPromptTemplate
from pydantic import BaseModel, validator
# 初始化字符串prompt
PROMPT = """\
提供一個函數名和源代碼并給出函數的相應解釋
函數名: {function_name}
源代碼:
{source_code}
解釋:
"""
classFunctionExplainerPromptTemplate(StringPromptTemplate, BaseModel):
"""一個自定義提示模板,以函數名作為輸入,并格式化提示模板以提供函數的源代碼。 """
@validator("input_variables")
defvalidate_input_variables(cls, v):
"""驗證輸入變量是否正確。"""
if len(v) != 1or"function_name"notin v:
raise ValueError("函數名必須是唯一的輸入變量。")
return v
defformat(self, **kwargs) -> str:
# 獲取源代碼
source_code = get_source_code(kwargs["function_name"])
# 源代碼+名字提供給prompt
prompt = PROMPT.format(
function_name=kwargs["function_name"].__name__, source_code=source_code)
return prompt
def_prompt_type(self):
return"function-explainer"
FunctionExplainerPromptTemplate接收兩個變量一個是prompt,另一個則是傳入需要用到的model,該class下面的validate_input_variables用來驗證輸入量,format函數用來輸出格式化后的prompt.
解釋
#初始化prompt實例
fn_explainer = FunctionExplainerPromptTemplate(input_variables=["function_name"])
# 定義函數 test_add
def test_add():
return 1 + 1
# Generate a prompt for the function "test_add"
prompt_1 = fn_explainer.format(function_name=test_add)
print(prompt_1)
Output如下:
5??少量提示模版:
在構建prompt時,可以通過構建一個少量示例列表去進一步格式化prompt,每一個示例表都的結構都為字典,其中鍵是輸入變量,值是輸入變量的值。該過程通常先利用PromptTemplate將示例格式化成為字符串,然后創建一個FewShotPromptTemplate對象,用來接收few-shot的示例。官方文檔中舉例:
解釋
from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
from langchain.prompts import FewShotPromptTemplate, PromptTemplate
examples = [
{"question": "Who lived longer, Muhammad Ali or Alan Turing?",
"answer":
"""
Are follow up questions needed here: Yes.
Follow up: How old was Muhammad Ali when he died?
Intermediate answer: Muhammad Ali was 74 years old when he died.
Follow up: How old was Alan Turing when he died?
Intermediate answer: Alan Turing was 41 years old when he died.
So the final answer is: Muhammad Ali
"""},
{"question": "When was the founder of craigslist born?",
"answer":
"""
Are follow up questions needed here: Yes.
Follow up: Who was the founder of craigslist?
Intermediate answer: Craigslist was founded by Craig Newmark.
Follow up: When was Craig Newmark born?
Intermediate answer: Craig Newmark was born on December 6, 1952.
So the final answer is: December 6, 1952
"""},
{"question": "Who was the maternal grandfather of George Washington?",
"answer":
"""
Are follow up questions needed here: Yes.
Follow up: Who was the mother of George Washington?
Intermediate answer: The mother of George Washington was Mary Ball Washington.
Follow up: Who was the father of Mary Ball Washington?
Intermediate answer: The father of Mary Ball Washington was Joseph Ball.
So the final answer is: Joseph Ball
"""},
{"question": "Are both the directors of Jaws and Casino Royale from the same country?",
"answer":
"""
Are follow up questions needed here: Yes.
Follow up: Who is the director of Jaws?
Intermediate Answer: The director of Jaws is Steven Spielberg.
Follow up: Where is Steven Spielberg from?
Intermediate Answer: The United States.
Follow up: Who is the director of Casino Royale?
Intermediate Answer: The director of Casino Royale is Martin Campbell.
Follow up: Where is Martin Campbell from?
Intermediate Answer: New Zealand.
So the final answer is: No
"""}
]
# 配置一個格式化程序,該格式化程序將prompt格式化為字符串。此格式化程序應該是一個 PromptTemplate 對象。
example_prompt = PromptTemplate(input_variables=["question", "answer"], template="Question: {question}\n{answer}")
print(example_prompt.format(**examples[0]))
# 創建一個選擇器來選擇最相似的例子
example_selector = SemanticSimilarityExampleSelector(
examples=examples,
vector_store=Chroma(),
embeddings_model=OpenAIEmbeddings(),
example_prompt=example_prompt
)
# 最后用FewShotPromptTemplate 來創建一個提示詞模板,該模板將輸入變量作為輸入,并將其格式化為包含示例的提示詞。
prompt = FewShotPromptTemplate(
example_selector=example_selector,
example_prompt=example_prompt,
suffix="Question: {input}",
input_variables=["input"]
)
print(prompt)
除了上述普通的字符串模版,聊天模版中也可以采用此類方式構建一個帶例子的聊天提示詞模版:
解釋
from langchain.prompts import ChatPromptTemplate, FewShotChatMessagePromptTemplate
# 這是一個聊天提示詞模板,它將輸入變量作為輸入,并將其格式化為包含示例的提示詞。
examples = [{"input": "2+2", "output": "4"}, {"input": "2+3", "output": "5"},]
# 提示詞模板,用于格式化每個單獨的示例。
example_prompt = ChatPromptTemplate.from_messages(
[("human", "{input}"),
("ai", "{output}"),])
few_shot_prompt = FewShotChatMessagePromptTemplate(
example_prompt=example_prompt,
examples=examples)
print(few_shot_prompt.format())
6??獨立化prompt:
為了便于共享、存儲和加強對prompt的版本控制,可以將想要設定prompt所支持的格式保存為JSON或者YAML格式文件。也可以直接將待格式化的prompt單獨存儲于一個文件中,通過格式化文件指定相應路徑,以更方便用戶加載任何類型的提示信息。
創建json文件:
解釋
{
"_type": "prompt",
"input_variables": ["adjective", "content"],
"template": "Tell me a {adjective} joke about {content}."
}
主文件代碼:
from langchain.prompts import load_prompt
prompt = load_prompt("./simple_prompt.json")
print(prompt.format(adjective="funny", content="chickens"))
Output如下:
Tell me a funny joke about chickens.
這里是直接在json文件中指定template語句,除此之外也可以將template單獨抽離出來,然后在json文件中指定template語句所在的文件路徑,以實現更好的區域化,方便管理prompt。
創建json文件:
解釋
{
"_type": "prompt",
"input_variables": ["adjective", "content"],
"template_path": "./simple_template.txt"
}
simple_template.txt:
Tell me a {adjective} joke about {content}.
其余部分代碼同第一部分介紹,最后的輸出結果也是一致的。
3.1.1.3Selector:
在few shot模塊,當我們列舉一系列示例值,但不進一步指定返回值,就會返回所有的prompt示例,在實際開發中我們可以使用自定義選擇器來選擇例子。例如,想要返回一個和新輸入的內容最為近似的prompt,這時候就可以去選擇與輸入最為相似的例子。這里的底層邏輯是利用了SemanticSimilarityExampleSelector這個例子選擇器和向量相似度的計算(openAIEmbeddings)以及利用chroma進行數據存儲,代碼如下:
解釋
from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings
example_selector = SemanticSimilarityExampleSelector.from_examples(
# 可選的示例列表。
examples,
# 用于生成嵌入的嵌入類,這些嵌入用于測量語義相似性。
OpenAIEmbeddings(),
# 用于存儲嵌入并進行相似性搜索的 VectorStore 類。
Chroma,
# 要生成的示例數。
k=1)
然后我們去輸入一條想要構建的prompt,遍歷整個示例列表,找到最為合適的example。
解釋
# 選擇與輸入最相似的示例。
question = "Who was the father of Mary Ball Washington?"
selected_examples = example_selector.select_examples({"question": question})
print(f"Examples most similar to the input: {question}")
for example in selected_examples:
print("\n")
for k, v in example.items():
print(f"{k}: {v}")
此時就可以返回一個最相似的例子。接下來我們可以重新重復few shot的步驟,利用FewShotPromptTemplate去創建一個提示詞模版。
對于聊天類型的few shot的prompt我們也可以采用例子選擇器進行格式化:
解釋
examples = [
{"input": "2+2", "output": "4"},
{"input": "2+3", "output": "5"},
{"input": "2+4", "output": "6"},
{"input": "What did the cow say to the moon?", "output": "nothing at all"},
{
"input": "Write me a poem about the moon",
"output": "One for the moon, and one for me, who are we to talk about the moon?",
},
]
# 由于我們使用向量存儲來根據語義相似性選擇示例,因此我們需要首先填充存儲。
to_vectorize = [" ".join(example.values()) for example in examples]
# 這里就單純理解為將value對應的值提取出來進行格式化即可。
# 創建向量庫后,可以創建 example_selector 以表示返回的相似向量的個數
# 注意:您需要先創建一個向量存儲庫(例如:vectorstore = ...)并填充它,然后將其傳遞給 SemanticSimilarityExampleSelector。
example_selector = SemanticSimilarityExampleSelector(vectorstore=vectorstore, k=2)
# 提示詞模板將通過將輸入傳遞給 `select_examples` 方法來加載示例
example_selector.select_examples({"input": "horse"})
此時就可以返回兩個個最相似的例子。接下來我們可以重復few shot的步驟 利用FewShotChatPromptTemplate去創建一個提示詞模版。
上文中介紹了在利用Langchain進行應用開發時所常用的構建prompt方式,無論哪種方式其最終目的都是為了更方便的去構建prompt,并盡可能的增加其復用性。Langchain提供的prompt相關工具遠不止上文這些,在了解了基礎能力后可以進一步查閱官方文檔找到最適合項目特點的工具,進行prompt格式化。
上除了上文中的prompt,LLM作為langchain中的核心內容,也是我們需要花心思去了解學習的,不過還是那句話,應用層的開發實際上無需到模型底層原理了解的十分透徹,我們更應該關注的是llm的調用形式,Langchain作為一個“工具”它并沒有提供自己的LLM,而是提供了一個接口,用于與許多不同類型的LLM進行交互,比如耳熟能詳的openai、huggingface或者是cohere等,都可以通過langchain快速調用。
1.單個調用:直接調用Model對象,傳入一串字符串然后直接返回輸出值,以openAI為例:
from langchain.llms import OpenAI
llm = OpenAI()
print(llm('你是誰'))
2.批量調用:通過generate可以對字符串列表,進行批量應用Model,使輸出更加豐富且完整。
llm_result = llm.generate(["給我背誦一首古詩", "給我講個100字小故事"]*10)
這時的llm_result會生成一個鍵為generations的數組,這個數組長度為20項,第一項為古詩、第二項為故事、第三項又為古詩,以此規則排列..
3.異步接口:asyncio庫為LLM提供異步支持,目前支持的LLM為OpenAI、PromptLayerOpenAI、ChatOpenAI 、Anthropic 和 Cohere 受支持。 可以使用agenerate 異步調用 OpenAI LLM。 在代碼編寫中,如果用了科學上網/魔法,以openAI為例,在異步調用之前,則需要預先將openai的proxy設置成為本地代理(這步很重要,若不設置后續會有報錯)
解釋
import os
import openai
import asyncio
from langchain.llms import OpenAI
# 設置代理
openai.proxy = os.getenv('https_proxy')
# 定義一個同步方式生成文本的函數
def generate_serially():
llm = OpenAI(temperature=0.9) # 創建OpenAI對象,并設置temperature參數為0.9
for _ in range(10): # 循環10次
resp = llm.generate(["Hello, how are you?"]) # 調用generate方法生成文本
print(resp.generations[0][0].text) # 打印生成的文本
# 定義一個異步生成文本的函數
async def async_generate(llm):
resp = await llm.agenerate(["Hello, how are you?"]) # 異步調用agenerate方法生成文本
print(resp.generations[0][0].text) # 打印生成的文本
# 定義一個并發(異步)方式生成文本的函數
async def generate_concurrently():
llm = OpenAI(temperature=0.9) # 創建OpenAI對象,并設置temperature參數為0.9
tasks = [async_generate(llm) for _ in range(10)] # 創建10個異步任務
await asyncio.gather(*tasks) # 使用asyncio.gather等待所有異步任務完成
可以用time庫去檢查運行時間,利用同步調用耗時大概為12s,異步耗時僅有2s。通過這種方式可以大大提速任務執行。
4.自定義大語言模型:在開發過程中如果遇到需要調用不同的LLM時,可以通過自定義LLM實現效率的提高。自定義LLM時,必須要實現的是_call方法,通過這個方法接受一個字符串、一些可選的索引字,并最終返回一個字符串。除了該方法之外,還可以選擇性生成一些方法用于以字典的模式返回該自定義LLM類的各屬性。
解釋
from langchain.callbacks.manager import CallbackManagerForLLMRun
from langchain.llms.base import LLM
from typing import Optional, List, Any, Mapping
class CustomLLM(LLM): # 這個類 CustomLLM 繼承了 LLM 類,并增加了一個新的類變量 n。
n: int # 類變量,表示一個整數
@property
def _llm_type(self) -> str:
return"custom"
def _call(
self,
prompt: str, # 輸入的提示字符串
stop: Optional[List[str]] = None, # 可選的停止字符串列表,默認為 None
run_manager: Optional[CallbackManagerForLLMRun] = None, # 可選的回調管理器,默認為 None
**kwargs: Any,
) -> str:
# 如果 stop 參數不為 None,則拋出 ValueError 異常
if stop is not None:
raise ValueError("stop kwargs are not permitted.")
return prompt[: self.n] # 返回 prompt 字符串的前 n 個字符
@property # 一個屬性裝飾器,用于獲取 _identifying_params 的值
def _identifying_params(self) -> Mapping[str, Any]:
"""Get the identifying parameters."""# 這個方法的文檔字符串,說明這個方法的功能是獲取標識參數
return {"n": self.n} # 返回一個字典,包含 n 的值
5.測試大語言模型:為了節省我們的成本,當寫好一串代碼進行測試的時候,通常情況下我們是不希望去真正調用LLM,因為這會消耗token(打工人表示傷不起),貼心的Langchain則提供給我們一個“假的”大語言模型,以方便我們進行測試。
解釋
# 從langchain.llms.fake模塊導入FakeListLLM類,此類可能用于模擬或偽造某種行為
from langchain.llms.fake import FakeListLLM
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
# 調用load_tools函數,加載"python_repl"的工具
tools = load_tools(["python_repl"])
# 定義一個響應列表,這些響應可能是模擬LLM的預期響應
responses = ["Action: Python REPL\nAction Input: print(2 + 2)", "Final Answer: 4"]
# 使用上面定義的responses初始化一個FakeListLLM對象
llm = FakeListLLM(responses=responses)
# 調用initialize_agent函數,使用上面的tools和llm,以及指定的代理類型和verbose參數來初始化一個代理
agent = initialize_agent(
tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)
# 調用代理的run方法,傳遞字符串"whats 2 + 2"作為輸入,詢問代理2加2的結果
agent.run("whats 2 + 2")
與模擬llm同理,langchain也提供了一個偽類去模擬人類回復,該功能依賴于wikipedia,所以模擬前需要install一下這個庫,并且需要設置proxy。這里同fakellm需要依賴agent的三個類,此外它還依賴下面的庫:
解釋
# 從langchain.llms.human模塊導入HumanInputLLM類,此類可能允許人類輸入或交互來模擬LLM的行為
from langchain.llms.human import HumanInputLLM
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
# 調用load_tools函數,加載名為"wikipedia"的工具
tools = load_tools(["wikipedia"])
# 初始化一個HumanInputLLM對象,其中prompt_func是一個函數,用于打印提示信息
llm = HumanInputLLM(
prompt_func=lambda prompt: print(f"\n===PROMPT====\n{prompt}\n=====END OF PROMPT======"))
# 調用initialize_agent函數,使用上面的tools和llm,以及指定的代理類型和verbose參數來初始化一個代理
agent = initialize_agent(tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True)
# 調用代理的run方法,傳遞字符串"What is 'Bocchi the Rock!'?"作為輸入,詢問代理關于'Bocchi the Rock!'的信息
agent.run("What is 'Bocchi the Rock!'?")
6.緩存大語言模型:和測試大語言模型具有一樣效果的是緩存大語言模型,通過緩存層可以盡可能的減少API的調用次數,從而節省費用。在Langchain中設置緩存分為兩種情況:一是在內存中設置緩存,二是在數據中設置緩存。存儲在內存中加載速度較快,但是占用資源并且在關機之后將不再被緩存,在內存中設置緩存示例如下:
解釋
from langchain.cache import SQLiteCache
import langchain
from langchain.llms import OpenAI
import time
langchain.llm_cache = SQLiteCache(database_path=".langchain.db")
llm = OpenAI(model_name="text-davinci-002", n=2, best_of=2)
start_time = time.time() # 記錄開始時間
print(llm.predict("用中文講個笑話"))
end_time = time.time() # 記錄結束時間
elapsed_time = end_time - start_time # 計算總時間
print(f"Predict method took {elapsed_time:.4f} seconds to execute.")
這里的時間大概花費1s+ ,因為被問題放在了內存里,所以在下次調用時幾乎不會再耗費時間。
除了存儲在內存中進行緩存,也可以存儲在數據庫中進行緩存,當開發企業級應用的時候通常都會選擇存儲在數據庫中,不過這種方式的加載速度相較于將緩存存儲在內存中更慢一些,不過好處是不占電腦資源,并且存儲記錄并不會隨著關機消失。
解釋
from langchain.cache import SQLiteCache
import langchain
from langchain.llms import OpenAI
import time
langchain.llm_cache = SQLiteCache(database_path=".langchain.db")
llm = OpenAI(model_name="text-davinci-002", n=2, best_of=2)
start_time = time.time() # 記錄開始時間
print(llm.predict("用中文講個笑話"))
end_time = time.time() # 記錄結束時間
elapsed_time = end_time - start_time # 計算總時間
print(f"Predict method took {elapsed_time:.4f} seconds to execute.")
7.跟蹤token使用情況(僅限model為openAI):
解釋
from langchain.llms import OpenAI
from langchain.callbacks import get_openai_callback
llm = OpenAI(model_name="text-davinci-002", n=2, best_of=2, cache=None)
with get_openai_callback() as cb:
result = llm("講個笑話")
print(cb)
上述代碼直接利用get_openai_callback即可完成對于單條的提問時token的記錄,此外對于有多個步驟的鏈或者agent,langchain也可以追蹤到各步驟所耗費的token。
解釋
from langchain.agents import load_tools
from langchain.agents import initialize_agent
from langchain.agents import AgentType
from langchain.llms import OpenAI
from langchain.callbacks import get_openai_callback
llm = OpenAI(temperature=0)
tools = load_tools(["llm-math"], llm=llm)
agent = initialize_agent(
tools, llm, agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION, verbose=True
)
with get_openai_callback() as cb:
response = agent.run("王菲現在的年齡是多少?")
print(f"Total Tokens: {cb.total_tokens}")
print(f"Prompt Tokens: {cb.prompt_tokens}")
print(f"Completion Tokens: {cb.completion_tokens}")
print(f"Total Cost (USD): ${cb.total_cost}")
8.序列化配置大語言模型:Langchain也提供一種能力用來保存LLM在訓練時使用的各類系數,比如template、 model_name等。這類系數通常會被保存在json或者yaml文件中,以json文件為例,配置如下系數,然后利用load_llm方法即可導入:
解釋
from langchain.llms.loading import load_llm
llm = load_llm("llm.json")
{
"model_name": "text-davinci-003",
"temperature": 0.7,
"max_tokens": 256,
"top_p": 1.0,
"frequency_penalty": 0.0,
"presence_penalty": 0.0,
"n": 1,
"best_of": 1,
"request_timeout": None,
"_type": "openai"
}
亦或者在配置好大模型參數之后,直接利用save方法即可直接保存配置到指定文件中。
llm.save("llmsave.json")
9.流式處理大語言模型的響應:流式處理意味著,在接收到第一個數據塊后就立即開始處理,而不需要等待整個數據包傳輸完畢。這種概念應用在LLM中則可達到生成響應時就立刻向用戶展示此下的響應,或者在生成響應時處理響應,也就是我們現在看到的和ai對話時逐字輸出的效果:可以看到實現還是較為方便的只需要直接調用StreamingStdOutCallbackHandler作為callback即可。
解釋
from langchain.llms import OpenAI
from langchain.callbacks.streaming_stdout import StreamingStdOutCallbackHandler
llm = OpenAI(streaming=True, callbacks=[StreamingStdOutCallbackHandler()], temperature=0)
resp = llm("Write me a song about sparkling water.")
可以看到實現還是較為方便的只需要直接調用StreamingStdOutCallbackHandler作為callback即可。
Model返回的內容通常都是字符串的模式,但在實際開發過程中,往往希望model可以返回更直觀的內容,Langchain提供的輸出解析器則將派上用場。在實現一個輸出解析器的過程中,需要實現兩種方法:1??獲取格式指令:返回一個字符串的方法,其中包含有關如何格式化語言模型輸出的說明。2??Parse:一種接收字符串(假設是來自語言模型的響應)并將其解析為某種結構的方法。
1.列表解析器:利用此解析器可以輸出一個用逗號分割的列表。
解釋
from langchain.output_parsers import CommaSeparatedListOutputParser
from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate
from langchain.llms import OpenAI
from langchain.chat_models import ChatOpenAI
output_parser = CommaSeparatedListOutputParser()
format_instructions = output_parser.get_format_instructions()
prompt = PromptTemplate(
template="List five {subject}.\n{format_instructions}",
input_variables=["subject"],
partial_variables={"format_instructions": format_instructions}
)
model = OpenAI(temperature=0)
_input = prompt.format(subject="冰淇淋口味")
output = model(_input)
output_parser.parse(output)
2.日期解析器:利用此解析器可以直接將LLM輸出解析為日期時間格式。
解釋
from langchain.prompts import PromptTemplate
from langchain.output_parsers import DatetimeOutputParser
from langchain.chains import LLMChain
from langchain.llms import OpenAI
output_parser = DatetimeOutputParser()
template = """回答用戶的問題:
{question}
{format_instructions}"""
prompt = PromptTemplate.from_template(
template,
partial_variables={"format_instructions": output_parser.get_format_instructions()},
)
chain = LLMChain(prompt=prompt, llm=OpenAI())
output = chain.run("bitcoin是什么時候成立的?用英文格式輸出時間")
3.枚舉解析器
解釋
from langchain.output_parsers.enum import EnumOutputParser
from enum import Enum
class Colors(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
parser = EnumOutputParser(enum=Colors)
4.自動修復解析器:這類解析器是一種嵌套的形式,如果第一個輸出解析器出現錯誤,就會直接調用另一個一修復錯誤
解釋
# 導入所需的庫和模塊
from langchain.prompts import PromptTemplate, ChatPromptTemplate, HumanMessagePromptTemplate
from langchain.llms import OpenAI
from langchain.chat_models import ChatOpenAI
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field, validator
from typing import List
# 定義一個表示演員的數據結構,包括他們的名字和他們出演的電影列表
class Actor(BaseModel):
name: str = Field(description="name of an actor") # 演員的名字
film_names: List[str] = Field(description="list of names of films they starred in") # 他們出演的電影列表
# 定義一個查詢,用于提示生成隨機演員的電影作品列表
actor_query = "Generate the filmography for a random actor."
# 使用`Actor`模型初始化解析器
parser = PydanticOutputParser(pydantic_object=Actor)
# 定義一個格式錯誤的字符串數據
misformatted = "{'name': 'Tom Hanks', 'film_names': ['Forrest Gump']}"
# 使用解析器嘗試解析上述數據
try:
parsed_data = parser.parse(misformatted)
except Exception as e:
print(f"Error: {e}")
parser.parse(misformatted)
格式錯誤的原因是因為json文件需要雙引號進行標記,但是這里用了單引號,此時利用該解析器進行解析就會出現報錯,但是此時可以利用RetryWithErrorOutputParser進行修復錯誤,則會正常輸出不報錯。
解釋
from langchain.output_parsers import RetryWithErrorOutputParser
from langchain.llms import OpenAI
retry_parser = RetryWithErrorOutputParser.from_llm(
parser=parser, llm=OpenAI(temperature=0))
retry_parser.parse_with_prompt(bad_response, prompt_value)
這里的“Parse_with_prompt”:一種方法,它接受一個字符串(假設是來自語言模型的響應)和一個提示(假設是生成此類響應的提示)并將其解析為某種結構。提示主要在 OutputParser 想要以某種方式重試或修復輸出時提供,并且需要來自提示的信息才能執行此操作。
Retrieval直接漢譯過來即”檢索“。該功能經常被應用于構建一個“私人的知識庫”,構建過程更多的是將外部數據存儲到知識庫中。細化這一模塊的主要職能有四部分,其包括數據的獲取、整理、存儲和查詢。如下圖:
首先,在該過程中可以從本地/網站/文件等資源庫去獲取數據,當數據量較小時,我們可以直接進行存儲,但當數據量較大的時候,則需要對其進行一定的切片,切分時可以按照數據類型進行切片處理,比如針對文本類數據,可以直接按照字符、段落進行切片;代碼類數據則需要進一步細分以保證代碼的功能性;此外,除了按照數據類型進行切片處理,也可以直接根據token進行切片。而后利用Vector Stores進行向量存儲,其中Embedding完成的就是數據的向量化,雖然這一能力往往被嵌套至大模型中,但是我們也要清楚并不是所有的模型都能直接支持文本向量化這一能力。除此之外的memory、self-hosted以及baas則是指向量存儲的三種載體形式,可以選擇直接存儲于內存中,也可以選擇存儲上云。最后則利用這些向量化數據進行檢索,檢索形式可以是直接按照向量相似度去匹配相似內容,也可以直接網絡,或者借用其他服務實現檢索以及數據的返回。
從上文中我們可以發現,對于retrievers來說,向量數據庫發揮著很大的作用,它不僅實現向量的存儲也可以通過相似度實現向量的檢索,但是向量數據庫到底是什么呢?它和普通的數據庫有著怎樣的區別呢?相信還是有很多同學和我一樣有一點點疑惑,所以在介紹langchain在此module方面的能力前,先介紹一下向量數據庫,以及它在LLM中所發揮的作用。
我們在對一個事物進行描述的時候,通常會根據事物的各方面特征進行表述。設想這樣一個場景,假設你是一名攝影師,拍了大量的照片。為了方便管理和查找,你決定將這些照片存儲到一個數據庫中。傳統的關系型數據庫(如 MySQL、PostgreSQL 等)可以幫助你存儲照片的元數據,比如拍攝時間、地點、相機型號等。但是,當你想要根據照片的內容(如顏色、紋理、物體等)進行搜索時,傳統數據庫可能無法滿足你的需求,因為它們通常以數據表的形式存儲數據,并使用查詢語句進行精確搜索。但向量包含了大量信息,使用查詢語句很難精確地找到唯一的向量。
那么此時,向量數據庫就可以派上用場。我們可以構建一個多維的空間使得每張照片特征都存在于這個空間內,并用已有的維度進行表示,比如時間、地點、相機型號、顏色....此照片的信息將作為一個點,存儲于其中。以此類推,即可在該空間中構建出無數的點,而后我們將這些點與空間坐標軸的原點相連接,就成為了一條條向量,當這些點變為向量之后,即可利用向量的計算進一步獲取更多的信息。當要進行照片的檢索時,也會變得更容易更快捷。但在向量數據庫中進行檢索時,檢索并不是唯一的而是查詢和目標向量最為相似的一些向量,具有模糊性。
那么我們可以延伸思考一下,只要對圖片、視頻、商品等素材進行向量化,就可以實現以圖搜圖、視頻相關推薦、相似寶貝推薦等功能,那應用在LLM中,小則可直接實現相關問題提示,大則我們完全可以利用此特性去歷史對話記錄中找到一些最類似的對話,然后重新喂給大模型,這將極大的提高大模型的輸出結果的準確性。 為更好的了解向量數據庫,接下來將繼續介紹向量的幾種檢索方式,以對向量數據庫有一個更深度的了解。
因為每一個向量所記錄的信息量都是比較多的,所以自然而然其所占內存也是很大的,舉個例子,如果我們的一個向量維度是256維的,那么該向量所占用的內存大小就是:256*32/8=1024字節,若數據庫中共計一千萬個向量,則所占內存為10240000000字節,也就是9.54GB,已經是一個很龐大的數目了,而在實際開發中這個規模往往更大,因此解決向量數據庫的內存占用問題是重中之重的。我們往往會對每個向量進行壓縮,從而縮小其內存占用。常常利用乘積量化方法
乘積量化:該思想將高維向量分解為多個子向量。例如,將一個D維向量分解為m個子向量,每個子向量的維度為D/m。然后對每個子向量進行量化。對于每個子向量空間,使用聚類算法將子向量分為K個簇,并將簇中心作為量化值。然后,用子向量在簇中的索引來表示原始子向量。這樣,每個子向量可以用一個整數(量化索引)來表示。最后將量化索引組合起來表示原始高維向量。對于一個D維向量,可以用m個整數來表示,其中每個整數對應一個子向量的量化索引。此外這類方法不僅可以用于優化存儲向量也可以用于優化檢索。
通過上段文字的描述,我們不難發現,向量檢索過程可以抽象化為“最近鄰問題“,對應的算法就是最近鄰搜索算法,具體有如下幾種:
1.暴力搜索:依次比較向量數據庫中所有的的向量與目標向量的相似度,然后找出相似度最高一個或一些向量,這樣得到的結果質量是極高的,但這對于數據量龐大的數據庫來說無疑是十分耗時的。
2.聚類搜索:這類算法首先初始化K個聚類中心,將數據對象分組成若干個類別或簇(cluster)。其主要目的是根據數據的相似性或距離度量來對數據進行分組,然后根據所選的聚類算法,通過迭代計算來更新聚類結果。例如,在K-means算法中,需要不斷更新簇中心并將數據對象分配給最近的簇中心;在DBSCAN算法中,需要根據密度可達性來擴展簇并合并相鄰的簇。最后設置一個收斂條件,用于判斷聚類過程是否結束。收斂條件可以是迭代次數、簇中心變化幅度等。當滿足收斂條件時,聚類過程結束。這樣的搜索效率大大提高,但是不可避免會出現遺漏的情況。
3.位置敏感哈希:此算法首先選擇一組位置敏感哈希函數,該函數需要滿足一個特性:對于相似的數據點,它們的哈希值發生沖突的概率較高;對于不相似的數據點,它們的哈希值發生沖突的概率較低。而后利用該函數對數據集中的每個數據點進行哈希。將具有相同哈希值的數據點存儲在相同的哈希桶中。在檢索過程中,對于給定的查詢點,首先使用LSH函數計算其哈希值,然后在相應的哈希桶中搜索相似的數據點。最后根據需要,可以在搜索到的候選數據點中進一步計算相似度,以找到最近鄰。
4.分層級的導航小世界算法:這是一種基于圖的近似最近鄰搜索方法,適用于大規模高維數據集。其核心思想是將數據點組織成一個分層結構的圖,使得在高層次上可以快速地找到距離查詢點較近的候選點,然后在低層次逐步細化搜索范圍,從而加速最近鄰搜索過程。
該算法首先創建一個空的多層圖結構。每一層都是一個圖,其中節點表示數據點,邊表示節點之間的連接關系。最底層包含所有數據點,而上層圖只包含部分數據點。每個數據點被分配一個隨機的層數,表示該點在哪些層次的圖中出現。然后插入數據點:對于每個新插入的數據點,首先確定其層數,然后從最高層開始,將該點插入到相應的圖中。插入過程中,需要找到該點在每層的最近鄰,并將它們連接起來。同時,還需要更新已有節點的連接關系,以保持圖的導航性能。其檢索過程是首先在最高層的圖中找到一個起始點,然后逐層向下搜索,直到達到底層。在每一層,從當前點出發,沿著邊進行搜索,直到找到一個局部最近鄰。然后將局部最近鄰作為下一層的起始點,繼續搜索。最后,在底層找到的結果則為最終結果。
前文中大概介紹了向量數據庫是什么以及向量數據庫所依賴的一些實現技術,接下來我們來談論一下向量數據庫與大模型之間的關系。為什么說想要用好大模型往往離不開向量數據庫呢?對于大模型來講,處理的數據格式一般都是非結構化數據,如音頻、文本、圖像..我們以大語言模型為例,在喂一份數據給大模型的時候,數據首先會被轉為向量,在上述內容中我們知道如果向量較近那么就表示這兩個向量含有的信息更為相似,當大量數據不斷被喂到大模型中的時候,語言模型就會逐漸發現詞匯間的語義和語法。當用戶進行問答的時候,問題輸入Model后會基于Transformer架構從每個詞出發去找到它與其他詞的關系權重,找到權重最重的一組搭配,這一組就為此次問答的答案了。最后再將這組向量返回回來,也就完成了一次問答。當我們把向量數據庫接入到AI中,我們就可以通過更新向量數據庫的數據,使得大模型能夠不斷獲取并學習到業界最新的知識,而不是將能力局限于預訓練的數據中。這種方式要比微調/重新訓練大模型的方式節約更多成本。
為了更好的理解retrieval的功能,在上文中先介紹了一下它所依賴的核心概念——向量數據庫,接下來讓我們看一下Langhcain中的retrieval是如何發揮作用的。我們已經知道,一般在用戶開發(LLM)應用程序,往往會需要使用不在模型訓練集中的特定數據去進一步增強大語言模型的能力,這種方法被稱為檢索增強生成(RAG)。LangChain 提供了一整套工具來實現 RAG 應用程序,首先第一步就是進行文檔的相應加載即DocumentLoader:
LangChain提供了多種文檔加載器,支持從各種不同的來源加載文檔(例如,私有的存儲桶或公共網站),支持的文檔類型也十分豐富:如 HTML、PDF 、MarkDown文件等...
1.加載 md文件:
解釋
from langchain.document_loaders import TextLoader
# 創建一個TextLoader實例,指定要加載的Markdown文件路徑
loader = TextLoader("./index.md")
# 使用load方法加載文件內容并打印
print(loader.load())
2.加載csv文件:
解釋
# 導入CSVLoader類
from langchain.document_loaders.csv_loader import CSVLoader
# 創建CSVLoader實例,指定要加載的CSV文件路徑
loader = CSVLoader(file_path='./index.csv')
# 使用load方法加載數據并將其存儲在數據變量中
data = loader.load()
3.自定義 csv 解析和加載 指定csv文件的字段名fieldname即可
解釋
from langchain.document_loaders.csv_loader import CSVLoader
# 創建CSVLoader實例,指定要加載的CSV文件路徑和CSV參數
loader = CSVLoader(file_path='./index.csv', csv_args={
'delimiter': ',',
'quotechar': '"',
'fieldnames': ['title', 'content']
})
# 使用load方法加載數據并將其存儲在數據變量中
data = loader.load()
4.可以使用該 source_column 參數指定文件加載的列。
解釋
from langchain.document_loaders.csv_loader import CSVLoader
# 創建CSVLoader實例,指定要加載的CSV文件路徑和源列名
loader = CSVLoader(file_path='./index.csv', source_column="context")
# 使用load方法加載數據并將其存儲在數據變量中
data = loader.load()
除了上述的單個文件加載,我們也可以批量加載一個文件夾內的所有文件,該加載依賴unstructured,所以開始前需要pip一下。如加載md文件就:pip install "unstructured[md]"
解釋
# 導入DirectoryLoader類
from langchain.document_loaders import DirectoryLoader
# 創建DirectoryLoader實例,指定要加載的文件夾路徑、要加載的文件類型和是否使用多線程
loader = DirectoryLoader('/Users/kyoku/Desktop/LLM/documentstore', glob='**/*.md', use_multithreading=True)
# 使用load方法加載所有文檔并將其存儲在docs變量中
docs = loader.load()
# 打印加載的文檔數量
print(len(docs))
# 導入UnstructuredHTMLLoader類
from langchain.document_loaders import UnstructuredHTMLLoader
# 創建UnstructuredHTMLLoader實例,指定要加載的HTML文件路徑
loader = UnstructuredHTMLLoader("./index.html")
# 使用load方法加載HTML文件內容并將其存儲在data變量中
data = loader.load()
# 導入BSHTMLLoader類
from langchain.document_loaders import BSHTMLLoader
# 創建BSHTMLLoader實例,指定要加載的HTML文件路徑
loader = BSHTMLLoader("./index.html")
# 使用load方法加載HTML文件內容并將其存儲在data變量中
data = loader.load()
當文件內容成功加載之后,通常會對數據集進行一系列處理,以便更好地適應你的應用。比如說,可能想把長文檔分成小塊,這樣就能更好地放入模型。LangChain 提供了很多現成的文檔轉換器,可以輕松地拆分、組合、過濾文檔,還能進行其他操作。
雖然上述步驟聽起來較為簡單,但實際上有很多潛在的復雜性。最好的情況是,把相關的文本片段放在一起。這種“相關性”可能因文本的類型而有所不同。
Langchain提供了工具RecursiveCharacterTextSplitter用來進行文本的拆分,其運行原理為:首先嘗試用第一個字符進行拆分,創建小塊。如果有些塊太大,它就會嘗試下一個字符,以此類推。默認情況下,它會按照 ["\n\n", "\n", " ", ""] 的順序嘗試拆分字符。以下為示例代碼:
解釋
# 打開一個文本文件并讀取內容
with open('./test.txt') as f:
state_of_the_union = f.read()
# 導入RecursiveCharacterTextSplitter類
from langchain.text_splitter import RecursiveCharacterTextSplitter
# 創建RecursiveCharacterTextSplitter實例,設置塊大小、塊重疊、長度函數和是否添加開始索引
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=100,
chunk_overlap=20,
length_function=len,
add_start_index=True,
)
# 使用create_documents方法創建文檔并將其存儲在texts變量中
texts = text_splitter.create_documents([state_of_the_union])
從輸出結果可以看到其是被拆分成了一個數組的形式。
除了上述的文本拆分,代碼拆分也經常被應用于llm應用的構建中:
解釋
# 導入所需的類和枚舉
from langchain.text_splitter import RecursiveCharacterTextSplitter, Language
# 定義一個包含Python代碼的字符串
PYTHON_CODE = """
def hello_world():
print("Hello, World!")
# Call the function
hello_world()
"""
# 使用from_language方法創建一個針對Python語言的RecursiveCharacterTextSplitter實例
python_splitter = RecursiveCharacterTextSplitter.from_language(
language=Language.PYTHON, chunk_size=50, chunk_overlap=0
)
# 使用create_documents方法創建文檔并將其存儲在python_docs變量中
python_docs = python_splitter.create_documents([PYTHON_CODE])
調用特定的拆分器可以保證拆分后的代碼邏輯,這里我們只要指定不同的Language就可以對不同的語言進行拆分。
在實際開發中我們可以將數據向量化細分為兩步:一是將數據向量化(向量化工具:openai的embeding、huggingface的n3d...),二是將向量化后的數據存儲到向量數據庫中,常見比較好用的免費向量數據庫有Meta的faiss、chrome的chromad以及lance。
1.高性能:利用 CPU 和 GPU 的并行計算能力,實現了高效的向量索引和查詢操作。 2.可擴展性:支持大規模數據集,可以處理數十億個高維向量的相似性搜索和聚類任務。 3.靈活性:提供了多種索引和搜索算法,可以根據具體需求選擇合適的算法。 4.開源:是一個開源項目,可以在 GitHub 上找到其源代碼和詳細文檔。
安裝相關庫: pip install faiss-cpu (顯卡好的同學也可以install gpu版本)
準備一個數據集,這個數據集包含一段關于信用卡年費收取和提高信用卡額度的咨詢對話。客戶向客服提出了關于信用卡年費和額度的問題,客服則詳細解答了客戶的疑問:
解釋
text = """客戶:您好,我想咨詢一下信用卡的問題。\n客服:您好,歡迎咨詢建行信用卡,我是客服小李,請問有什么問題我可以幫您解答嗎?\n客戶:我想了解一下信用卡的年費如何收取?\n客服:關于信用卡年費的收取,我們會在每年的固定日期為您的信用卡收取年費。當然,如果您在一年內的消費達到一定金額,年費會自動免除。具體的免年費標準,請您查看信用卡合同條款或登錄我們的網站查詢。\n客戶:好的,謝謝。那我還想問一下,如何提高信用卡的額度?\n客服:關于提高信用卡額度,您可以通過以下途徑操作:1. 登錄建行信用卡官方網站或手機APP,提交在線提額申請;2. 撥打我們的客服熱線,按語音提示進行提額申請;3. 您還可以前往附近的建行網點,提交提額申請。在您提交申請后,我們會根據您的信用狀況進行審核,審核通過后,您的信用卡額度將會相應提高。\n客戶:明白了,非常感謝您的解答。\n客服:您太客氣了,很高興能夠幫到您。如果您還有其他問題,請隨時聯系我們。祝您生活愉快!"""
list_text = text.split('\n')
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import FAISS
db = FAISS.from_texts(list_text, OpenAIEmbeddings())
query = "信用卡的額度可以提高嗎"
docs = db.similarity_search(query)
print(docs[0].page_content)
embedding_vector = OpenAIEmbeddings().embed_query(query)
print(f'embedding_vector:{embedding_vector}')
docs = db.similarity_search_by_vector(embedding_vector)
print(docs[0].page_content)
除了上述直接輸出效果最好的結果,也可以按照相似度分數進行輸出,不過這里的規則是分數越低,相似度越高。
解釋
# 使用帶分數的相似性搜索
docs_and_scores = db.similarity_search_with_score(query)
# 打印文檔及其相似性分數
for doc, score in docs_and_scores:
print(f"Document: {doc.page_content}\nScore: {score}\n")
如果每次都要調用embedding無疑太浪費,所以最后我們也可以直接將數據庫保存起來,避免重復調用。
解釋
# 保存
db.save_local("faiss_index")
# 加載
new_db = FAISS.load_local("faiss_index", OpenAIEmbeddings())
在官網中還介紹了另外兩種向量數據庫的使用方法,這里不再贅述。
Memory——存儲歷史對話信息。該功能主要會執行兩步:1.輸入時,從記憶組件中查詢相關歷史信息,拼接歷史信息和用戶的輸入到提示詞中傳給LLM。2.自動把LLM返回的內容存儲到記憶組件,用于下次查詢。
Memory——存儲歷史對話信息。該功能主要會執行兩步:
1.輸入時,從記憶組件中查詢相關歷史信息,拼接歷史信息和用戶的輸入到提示詞中傳給LLM。
2.自動把LLM返回的內容存儲到記憶組件,用于下次查詢。
不過,GPT目前就有這個功能了,它已經可以進行多輪對話了,為何我們還要把這個功能拿出來細說呢?在之前介紹prompt的文章中介紹過:在進行多輪對話時,我們會把歷史對話內容不斷的push到prompt數組中,通俗來講就是將所有的聊天記錄都作為prompt了,以存儲的形式實現了大語言模型的“記憶”功能,而大語言模型本身是無狀態的,這種方式無疑會較為浪費token,所以開發者不得不將注意力聚焦于如何在保證大語言模型功能的基礎上盡可能的減少token的使用,Memory這個組件也就隨之誕生。po一張Memory官網的圖:
從上圖可以看到Memory實現思路還是蠻簡單的,就是存儲查詢,存儲的過程我們無需過度思考,無非就是存到內存/數據庫,但是讀取的過程還是值得我們探討一番,為什么這么說呢?在上文中已經知道memory的目的其實就是要在保證大語言模型能力的前提下盡可能的減少token消耗,所以我們不能把所有的數據一起丟給大語言模型,這就失去了memory的意義了,不是嗎?目前memory常利用以下幾種查詢策略:
1.將會話直接作為prompt喂回給大模型背景,可以稱之為buffer。
2.將所有歷史消息丟給模型生成一份摘要,再將摘要作為prompt背景,可以稱之為summary。
3.利用之前提及的向量數據庫,查詢相似歷史信息,作為prompt背景,可以稱之為vector。
Memory這一功能的使用方式還是較為簡單的,本節將會按照memory的三大分類,依次介紹memory中會被高頻使用到的一些工具函數。
1??ConversationBufferMemory
先舉例一個最簡單的使用方法——直接將內容存儲到buffer,無論是單次或是多次存儲,其對話內容都會被存儲到一個memory:
memory = ConversationBufferMemory() memory.save_context({"input": "你好,我是人類"}, {"output": "你好,我是AI助手"})memory.save_context({"input": "很開心認識你"}, {"output": "我也是"})
存儲后可直接輸出存儲內容:
print(memory.load_memory_variables({}))
# {'history': 'Human: 你好,我是人類\nAI: 你好,我是AI助手\nHuman: 很開心認識你\nAI: 我也是'}
ConversationBufferMemory無疑是很簡單方便的,但是可以試想一下,當我們與大語言模型進行多次對話時,直接利用buffer存儲的話,所占內存量是十分大的,并且消耗的token是十分多的,這時通過ConversationBufferWindowMemory進行窗口緩存的方式就可以解決上述問題。其核心思想:就是保留一個窗口大小的對話,其內容只是最近的N次對話。在這個工具函數中,可以利用k參數來聲明保留的對話記憶,比如k=1時,上述對話內容輸出結果就會發生相應的改變:
memory = ConversationBufferWindowMemory(k=1)
memory.save_context({"input": "你好,我是人類"}, {"output": "你好,我是AI助手"})
memory.save_context({"input": "很開心認識你"}, {"output": "我也是"})
只保存了最近的k條記錄:
print(memory.load_memory_variables({}))
# {'history': 'Human: 很開心認識你\nAI: 我也是'}
通過內置在Langchain中的緩存窗口(BufferWindow)可以將meomory"記憶"下來。
除了通過設置對話數量控制memory,也可以通過設置token來限制。如果字符數量超出指定數目,它會切掉這個對話的早期部分 以保留與最近的交流相對應的字符數量
解釋
from langchain.chat_models import ChatOpenAI
from langchain.memory import ConversationTokenBufferMemory
llm = ChatOpenAI(temperature=0.0)
memory = ConversationTokenBufferMemory(llm=llm,)
memory.save_context({"input": "春眠不覺曉"}, {"output": "處處聞啼鳥"})
memory.save_context({"input": "夜來風雨聲"}, {"output": "花落知多少"})
print(memory.load_memory_variables({}))
#{'history': 'AI: 花落知多少。'}
對于buffer方式我們不難發現,如果全部保存下來太過浪費,截斷時無論是按照對話條數還是token都是無法保證即節省內存或token又保證對話質量的,所以我們可以對其進行summary:
在進行總結時最基礎的就是ConversationSummaryBufferMemory這個工具函數,利用該函數時通過設置token從而在清除歷史對話時生成一份對話記錄:
解釋
memory = ConversationSummaryBufferMemory(llm=llm, max_token_limit=40, return_messages=True)
memory.save_context({"input": "嗨"}, {"output": "你好嗎"})
memory.save_context({"input": "沒什么特別的,你呢"}, {"output": "我也是"})
messages = memory.chat_memory.messages
previous_summary = ""
print(memory.predict_new_summary(messages, previous_summary))
# 人類和AI都表示沒有做什么特別的事
該API通過 predict_new_summary成功的將對話進行了摘要總結。
最后來介紹一下vector在memory中的用法,通過VectorStoreRetrieverMemory可以將memory存儲到Vector數據庫中,每次調用時,就會查找與該記憶關聯最高的k個文檔,并且不會跟蹤交互順序。不過要注意的是,在利用VectorStoreRetrieverMemory前,我們需要先初始化一個VectorStore,免費向量數據庫有Meta的faiss、chrome的chromad以及lance,以faiss為例:
解釋
import faiss
from langchain.docstore import InMemoryDocstore
from langchain.vectorstores import FAISS
embedding_size = 1536 # Dimensions of the OpenAIEmbeddings
index = faiss.IndexFlatL2(embedding_size)
embedding_fn = OpenAIEmbeddings().embed_query
vectorstore = FAISS(embedding_fn, index, InMemoryDocstore({}), {})
初始化好一個數據庫之后,我們就可以根據該數據庫實例化出一個memory:
解釋
# 在實際使用中,可以將`k` 設為更高的值,這里使用 k=1 來展示
# 向量查找仍然返回語義相關的信息
retriever = vectorstore.as_retriever(search_kwargs=dict(k=1))
memory = VectorStoreRetrieverMemory(retriever=retriever)
# 當添加到一個代理時,內存對象可以保存來自對話或使用的工具的相關信息
memory.save_context({"input": "我最喜歡的食物是披薩"}, {"output": "好的,我知道了"})
memory.save_context({"input": "我最喜歡的運動是足球"}, {"output": "..."})
memory.save_context({"input": "我不喜歡凱爾特人隊"}, {"output": "好的"})
print(memory.load_memory_variables({"prompt": "我應該看什么運動?"})["history"])
這時便會根據向量數據庫檢索后輸出memory結果
{'history': [{'input': '我最喜歡的運動是足球','output': '...'}]}
這表示在與用戶的對話歷史中,語義上與 "我應該看什么運動?" 最相關的是 "我最喜歡的運動是足球" 這個對話。更復雜一點可以通過conversationchain進行多輪對話:
解釋
llm = OpenAI(temperature=0) # 可以是任何有效的LLM
_DEFAULT_TEMPLATE = """以下是一個人類與AI之間的友好對話。AI非常健談,并從其上下文中提供大量具體細節。如果AI不知道問題的答案,它會誠實地說不知道。
之前對話的相關部分:
{history}
(如果不相關,您不需要使用這些信息)
當前對話:
人類:{input}
AI:"""
PROMPT = PromptTemplate(
input_variables=["history", "input"], template=_DEFAULT_TEMPLATE
)
conversation_with_summary = ConversationChain(
llm=llm,
prompt=PROMPT,
# 我們為測試目的設置了一個非常低的max_token_limit。
memory=memory,
verbose=True
)
conversation_with_summary.predict(input="嗨,我叫Perry,你好嗎?")
# 輸出:"> Entering new ConversationChain chain...
# Prompt after formatting:
# ...
# > Finished chain.
# " 嗨,Perry,我很好。你呢?"
# 這里,與籃球相關的內容被提及
conversation_with_summary.predict(input="我最喜歡的運動是什么?")
# 輸出:"> Entering new ConversationChain chain...
# ...
# > Finished chain.
# ' 你之前告訴我你最喜歡的運動是足球。'"
# 盡管語言模型是無狀態的,但由于獲取到了相關的記憶,它可以“推理”出時間。
# 為記憶和數據加上時間戳通常是有用的,以便讓代理確定時間相關性
conversation_with_summary.predict(input="我的最喜歡的食物是什么?")
# 輸出:"> Entering new ConversationChain chain...
# ...
# > Finished chain.
# ' 你說你最喜歡的食物是披薩。'"
# 對話中的記憶被自動存儲,
# 由于這個查詢與上面的介紹聊天最匹配,
# 代理能夠“記住”用戶的名字。
conversation_with_summary.predict(input="我的名字是什么?")
# 輸出:"> Entering new ConversationChain chain...
# ...
# > Finished chain.
# ' 你的名字是Perry。'"
conversation_with_summary這個實例使用了一個內存對象(memory)來存儲與用戶的對話歷史。這使得AI可以在后續的對話中引用先前的上下文,從而提供更準確和相關的回答。
在Langchain中memory屬于較為簡單的一模塊,小型開發中常常使用summary類型,對于大一點的開發來說,最常見的就是利用向量數據庫進行數據的存儲,并在ai模型給出輸出時到該數據庫中檢索出相似性最高的內容。
如果把用Langchain構建AI應用的過程比作“積木模型”的搭建與拼接,那么Chain可以說是該模型搭建過程中的骨骼部分,通過它將各模塊快速組合在一起就可以快速搭建一個應用。Chain的使用方式也是通過接口的直接調用,在本文中將Chain分為三種類型,從簡單到復雜依次介紹按照首先以一個簡單的示例,來直觀的感受Chain的作用:
這種類型的Chain應用起來很簡單也可以說是后續要介紹的Chain的基礎,但其功能是足夠強大的。通過LLMChain可以直接將數據、prompt、以及想要應用的Model串到一起,以一個簡單的例子來感受LLMChain。
解釋
from langchain import PromptTemplate, OpenAI, LLMChain
prompt_template = "What is a good name for a company that makes {product}?"
llm = OpenAI(temperature=0)
chain = LLMChain(
llm=llm,
prompt=PromptTemplate.from_template(prompt_template)
)
print(chain("colorful socks"))
# 輸出結果'Socktastic!'
在這個示例中,我們首先初始化了一個prompt的字符串模版,并初始化大語言模型,然后利用Chain將模型運行起來。在「Chain將模型運行起來」這個過程中:Chain將會格式化提示詞,然后將它傳遞給LLM。回憶一下,在之前的ai入門篇中,對于每個model的使用,我們需要針對這個model去進行一系列初始化、實例化等操作。而用了chain之后,我們無需再關注model本身。
不同于基本的LLMChain,Sequential chain(序列鏈)是由一系列的鏈組合而成的,序列鏈有兩種類型,一種是單個輸入輸出/另一個則是多個輸入輸出。先來看第一種單個輸入輸出的示例代碼:
在這個示例中,創建了兩條chain,并且讓第一條chain接收一個虛構劇本的標題,輸出該劇本的概要,作為第二條chain的輸入,然后生成一個虛構評論。通過sequential chains可以簡單的實現這一需求。
第一條chain:
解釋
# This is an LLMChain to write a synopsis given a title of a play.
from langchain import PromptTemplate, OpenAI, LLMChain
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:
解釋
# This is an LLMChain to write a review of a play given a synopsis.
from langchain import PromptTemplate, OpenAI, 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.
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)
最后利用SimpleSequentialChain即可將兩個chain直接串聯起來:
from langchain.chains import SimpleSequentialChain
overall_chain = SimpleSequentialChain(chains=[synopsis_chain, review_chain], verbose=True)
print(review = overall_chain.run("Tragedy at sunset on the beach"))
可以看到對于單個輸入輸出的順序鏈,就是將兩個chain作為參數傳給simplesequentialchain即可,無需復雜的聲明。
除了單個輸入輸出的模式,順序鏈還支持更為復雜的多個輸入輸出,對于多輸入輸出模式來說,最應該需要關注的就是輸入關鍵字和輸出關鍵字,它們需要十分的精準,才能夠保證chain的識別與應用,依舊以一個demo為例:
解釋
from langchain import PromptTemplate, OpenAI, 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: {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")
#第一條chain
解釋
from langchain import PromptTemplate, OpenAI, 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.
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")
#第二條chain
解釋
from langchain.chains import SequentialChain
overall_chain = SequentialChain(
chains=[synopsis_chain, review_chain],
input_variables=["era", "title"],
# Here we return multiple variables
output_variables=["synopsis", "review"],
verbose=True)
#第三條chain
overall_chain({"title": "Tragedy at sunset on the beach", "era": "Victorian England"})
對于每一個chain在定義的時候,都需要關注其output_key 、和input_variables,按照順序將其指定清楚。最終在運行chain時我們只需要指定第一個chain中需要聲明的變量。
最后介紹一個經常會用到的場景,比如我們目前有三類chain,分別對應三種學科的問題解答。我們的輸入內容也是與這三種學科對應,但是隨機的,比如第一次輸入數學問題、第二次有可能是歷史問題... 這時候期待的效果是:可以根據輸入的內容是什么,自動將其應用到對應的子鏈中。Router Chain就為我們提供了這樣一種能力,它會首先決定將要傳遞下去的子鏈,然后把輸入傳遞給那個鏈。并且在設置的時候需要注意為其設置默認chain,以兼容輸入內容不滿足任意一項時的情況。
解釋
physics_template = """You are a very smart physics professor. \
You are great at answering questions about physics in a concise and easy to understand manner. \
When you don't know the answer to a question you admit that you don't know.
Here is a question:
{input}"""
math_template = """You are a very good mathematician. You are great at answering math questions. \
You are so good because you are able to break down hard problems into their component parts, \
answer the component parts, and then put them together to answer the broader question.
Here is a question:
{input}"""
如上有一個物理學和數學的prompt:
解釋
prompt_infos = [
{
"name": "physics",
"description": "Good for answering questions about physics",
"prompt_template": physics_template
},
{
"name": "math",
"description": "Good for answering math questions",
"prompt_template": math_template
}
]
然后,需要聲明這兩個prompt的基本信息。
解釋
from langchain import ConversationChain, LLMChain, PromptTemplate, OpenAI
llm = OpenAI()
destination_chains = {}
for p_info in prompt_infos:
name = p_info["name"]
prompt_template = p_info["prompt_template"]
prompt = PromptTemplate(template=prompt_template, input_variables=["input"])
chain = LLMChain(llm=llm, prompt=prompt)
destination_chains[name] = chain
default_chain = ConversationChain(llm=llm, output_key="text")
最后將其運行到routerchain中即可,我們此時在輸入的時候chain就會根據input的內容進行相應的選擇最為合適的prompt。
解釋
from langchain.chains.router.llm_router import LLMRouterChain, RouterOutputParser
from langchain.chains.router.multi_prompt_prompt import MULTI_PROMPT_ROUTER_TEMPLATE
# Create a list of destinations
destinations = [f"{p['name']}: {p['description']}"for p in prompt_infos]
destinations_str = "\n".join(destinations)
# Create a router template
router_template = MULTI_PROMPT_ROUTER_TEMPLATE.format(destinations=destinations_str)
router_prompt = PromptTemplate(
template=router_template,
input_variables=["input"],
output_parser=RouterOutputParser(),
)
router_chain = LLMRouterChain.from_llm(llm, router_prompt)
chain = MultiPromptChain(
router_chain=router_chain,
destination_chains=destination_chains,
default_chain=default_chain,
verbose=True,
)
print(chain.run('什么是黑體輻射'))
Agents這一模塊在langchain的使用過程中也是十分重要的,官方文檔是這樣定義它的“The core idea of agents is to use a language model to choose a sequence of actions to take. In chains, a sequence of actions is hardcoded (in code). In agents, a language model is used as a reasoning engine to determine which actions to take and in which order.”也就是說,在使用Agents時,其行為以及行為的順序是由LLM的推理機制決定的,并不是像傳統的程序一樣,由核心代碼預定義好去運行的。
舉一個例子來對比一下,對于傳統的程序,我們可以想象這樣一個場景:一個王子需要經歷3個關卡,才可以救到公主,那么王子就必須按部就班的走一條確定的路線,一步步去完成這三關,才可以救到公主,他不可以跳過或者修改關卡本身。但對于Agents來說,我們可以將其想象成一個剛出生的原始人類,隨著大腦的日漸成熟和身體的不斷發育,該人類將會逐步擁有決策能力和記憶能力,這時想象該人類處于一種饑餓狀態,那么他就需要吃飯。此時,他剛好走到小河邊,通過“記憶”模塊,認知到河里的“魚”是可以作為食物的,那么他此時就會巧妙的利用自己身邊的工具——魚鉤,進行釣魚,然后再利用火,將魚烤熟。第二天,他又餓了,這時他在叢林里散步,遇到了一頭野豬,通過“記憶”模塊,認知到“野豬”也是可以作為食物的,由于野豬的體型較大,于是他選取了更具殺傷力的長矛進行狩獵。從他這兩次狩獵的經歷,我們可以發現,他并不是按照預先設定好的流程,使用固定的工具去捕固定的獵物,而是根據環境的變化選擇合適的獵物,又根據獵物的種類,去決策使用的狩獵工具。這一過程完美的利用了自己的決策、記憶系統,并輔助利用工具,從而做出一系列反應去解決問題。以一個數學公式來表示,可以說Agents=LLM(決策)+Memory(記憶)+Tools(執行)。
通過上述的例子,相信你已經清楚的認知到Agents與傳統程序比起來,其更加靈活,通過不同的搭配,往往會達到令人意想不到的效果,現在就用代碼來實操感受一下Agents的實際應用方式,下文的示例代碼主要實現的功能是——給予Agent一個題目,讓Agent生成一篇論文。
在該示例中,我們肯定是要示例化Agents,示例化一個Agents時需要關注上文中所描述的它的三要素:LLM、Memory和tools,其代碼如下:
解釋
# 初始化 agent
agent = initialize_agent(
tools, # 配置工具集
llm, # 配置大語言模型 負責決策
agent=AgentType.OPENAI_FUNCTIONS, # 設置 agent 類型
agent_kwargs=agent_kwargs, # 設定 agent 角色
verbose=True,
memory=memory, # 配置記憶模式 )
首先是配置工具集tools,如下列代碼,可以看到這是一個二元數組,也就意味著本示例中的Agents依賴兩個工具。
解釋
from langchain.agents import initialize_agent, Tool
tools = [
Tool(
name="Search",
func=search,
description="useful for when you need to answer questions about current events, data. You should ask targeted questions"
),
ScrapeWebsiteTool(),
]
先看第一個工具:在配置工具時,需要聲明工具依賴的函數,由于該示例實現的功能為依賴網絡收集相應的信息,然后匯總成一篇論文,所以創建了一個search函數,這個函數用于調用Google搜索。它接受一個查詢參數,然后將查詢發送給Serper API。API的響應會被打印出來并返回。
解釋
# 調用 Google search by Serper
def search(query):
serper_google_url = os.getenv("SERPER_GOOGLE_URL")
payload = json.dumps({
"q": query
})
headers = {
'X-API-KEY': serper_api_key,
'Content-Type': 'application/json'
}
response = requests.request("POST", serper_google_url, headers=headers, data=payload)
print(f'Google 搜索結果: \n {response.text}')
return response.text
再來看一下所依賴的第二個工具函數,這里用了另一種聲明工具的方式Class聲明—— ScrapeWebsiteTool(),它有以下幾個屬性和方法:
解釋
class ScrapeWebsiteTool(BaseTool):
name = "scrape_website"
description = "useful when you need to get data from a website url, passing both url and objective to the function; DO NOT make up any url, the url should only be from the search results"
args_schema: Type[BaseModel] = ScrapeWebsiteInput
def _run(self, target: str, url: str):
return scrape_website(target, url)
def _arun(self, url: str):
raise NotImplementedError("error here")
1.name:工具的名稱,這里是 "scrape_website"。 2.description:工具的描述。 args_schema:工具的參數模式,這里是 ScrapeWebsiteInput 類,表示這個工具需要的輸入參數,聲明代碼如下,這是一個基于Pydantic的模型類,用于定義 scrape_website 函數的輸入參數。它有兩個字段:target 和 url,分別表示用戶給agent的目標和任務以及需要被爬取的網站的URL。
解釋
class ScrapeWebsiteInput(BaseModel):
"""Inputs for scrape_website"""
target: str = Field(
description="The objective & task that users give to the agent")
url: str = Field(description="The url of the website to be scraped")
_run 方法:這是工具的主要執行函數,它接收一個目標和一個URL作為參數,然后調用 scrape_website 函數來爬取網站并返回結果。scrape_website 函數根據給定的目標和URL爬取網頁內容。首先,它發送一個HTTP請求來獲取網頁的內容。如果請求成功,它會使用BeautifulSoup庫來解析HTML內容并提取文本。如果文本長度超過5000個字符,它會調用 summary 函數來對內容進行摘要。否則,它將直接返回提取到的文本。其代碼如下:
解釋
# 根據 url 爬取網頁內容,給出最終解答
# target :分配給 agent 的初始任務
# url : Agent 在完成以上目標時所需要的URL,完全由Agent自主決定并且選取,其內容或是中間步驟需要,或是最終解答需要
def scrape_website(target: str, url: str):
print(f"開始爬取: {url}...")
headers = {
'Cache-Control': 'no-cache',
'Content-Type': 'application/json',
}
payload = json.dumps({
"url": url
})
post_url = f"https://chrome.browserless.io/content?token={browserless_api_key}"
response = requests.post(post_url, headers=headers, data=payload)
# 如果返回成功
if response.status_code == 200:
soup = BeautifulSoup(response.content, "html.parser")
text = soup.get_text()
print("爬取的具體內容:", text)
# 控制返回內容長度,如果內容太長就需要切片分別總結處理
if len(text) > 5000:
# 總結爬取的返回內容
output = summary(target, text)
return output
else:
return text
else:
print(f"HTTP請求錯誤,錯誤碼為{response.status_code}")
從上述代碼中我們可以看到其還依賴一個summary 函數,用此函數解決內容過長的問題,這個函數使用Map-Reduce方法對長文本進行摘要。它首先初始化了一個大語言模型(llm),然后定義了一個大文本切割器(text_splitter)。接下來,它創建了一個摘要鏈(summary_chain),并使用這個鏈對輸入文檔進行摘要。
解釋
# 如果需要處理的內容過長,先切片分別處理,再綜合總結
# 使用 Map-Reduce 方式
def summary(target, content):
# model list : https://platform.openai.com/docs/models
# gpt-4-32k gpt-3.5-turbo-16k-0613
llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo-16k-0613")
# 定義大文本切割器
# chunk_overlap 是一個在使用 OpenAI 的 GPT-3 或 GPT-4 API 時可能會遇到的參數,特別是需要處理長文本時。
# 該參數用于控制文本塊(chunks)之間的重疊量。
# 上下文維護:重疊確保模型在處理后續塊時有足夠的上下文信息。
# 連貫性:它有助于生成更連貫和一致的輸出,因為模型可以“記住”前一個塊的部分內容。
text_splitter = RecursiveCharacterTextSplitter(
separators=["\n\n", "\n"], chunk_size=5000, chunk_overlap=200)
docs = text_splitter.create_documents([content])
map_prompt = """
Write a summary of the following text for {target}:
"{text}"
SUMMARY:
"""
map_prompt_template = PromptTemplate(
template=map_prompt, input_variables=["text", "target"])
summary_chain
_arun 方法:這是一個異步版本的 _run 方法,這里沒有實現,如果調用會拋出一個 NotImplementedError 異常。
# 初始化大語言模型,負責決策
llm = ChatOpenAI(temperature=0, model="gpt-3.5-turbo-16k-0613")
這段代碼初始化了一個名為 llm 的大語言模型對象,它是 ChatOpenAI 類的實例。ChatOpenAI 類用于與大語言模型(如GPT-3)進行交互,以生成決策和回答。在初始化 ChatOpenAI 對象時,提供了以下參數:
1.temperature:一個浮點數,表示生成文本時的溫度。溫度值越高,生成的文本將越隨機和多樣;溫度值越低,生成的文本將越確定和一致。在這里設置為 0,因為本demo的目的為生成一個論文,所以我們并不希望大模型有較多的可變性,而是希望生成非常確定和一致的回答。 2.model:一個字符串,表示要使用的大語言模型的名稱。在這里,我們設置為 "gpt-3.5-turbo-16k-0613",表示使用 GPT-3.5 Turbo 模型。
首先來看一下AgentType這個變量的初始化,這里是用來設置agent類型的一個參數,具體可以參考官網:AgentType
可以看到官網里列舉了7中agent類型,可以根據自己的需求進行選擇,在本示例中選用的是第一種類型OpenAi functions。此外,還要設定agent角色以及記憶模式:
解釋
# 初始化agents的詳細描述
system_message = SystemMessage(
content="""您是一位世界級的研究員,可以對任何主題進行詳細研究并產生基于事實的結果;
您不會憑空捏造事實,您會盡最大努力收集事實和數據來支持研究。
請確保按照以下規則完成上述目標:
1/ 您應該進行足夠的研究,盡可能收集關于目標的盡可能多的信息
2/ 如果有相關鏈接和文章的網址,您將抓取它以收集更多信息
3/ 在抓取和搜索之后,您應該思考“根據我收集到的數據,是否有新的東西需要我搜索和抓取以提高研究質量?”如果答案是肯定的,繼續;但不要進行超過5次迭代
4/ 您不應該捏造事實,您只應該編寫您收集到的事實和數據
5/ 在最終輸出中,您應該包括所有參考數據和鏈接以支持您的研究;您應該包括所有參考數據和鏈接以支持您的研究
6/ 在最終輸出中,您應該包括所有參考數據和鏈接以支持您的研究;您應該包括所有參考數據和鏈接以支持您的研究"""
)
# 初始化 agent 角色模板
agent_kwargs = {
"extra_prompt_messages": [MessagesPlaceholder(variable_name="memory")],
"system_message": system_message,
}
# 初始化記憶類型
memory = ConversationSummaryBufferMemory(
memory_key="memory", return_messages=True, llm=llm, max_token_limit=300)
1??在設置agent_kwargs時:"extra_prompt_messages":這個鍵對應的值是一個包含 MessagesPlaceholder 對象的列表。這個對象的 variable_name 屬性設置為 "memory",表示我們希望在構建 agent 的提示時,將 memory 變量的內容插入到提示中。"system_message":這個鍵對應的值是一個 SystemMessage 對象,它包含了 agent 的角色描述和任務要求。
# 初始化記憶類型
memory = ConversationSummaryBufferMemory(
memory_key="memory", return_messages=True, llm=llm, max_token_limit=300)
在設置 memory 的記憶類型對象時:利用了 ConversationSummaryBufferMemory 類的實例。該類用于在與AI助手的對話中緩存和管理信息。在初始化這個對象時,提供了以下參數:1.memory_key:一個字符串,表示這個記憶對象的鍵。在這里設置為 "memory"。2.return_messages:一個布爾值,表示是否在返回的消息中包含記憶內容。在這里設置為 True,表示希望在返回的消息中包含記憶內容。3.llm:對應的大語言模型對象,這里是之前初始化的 llm 對象。這個參數用于指定在處理記憶內容時使用的大語言模型。4。max_token_limit:一個整數,表示記憶緩存的最大令牌限制。在這里設置為 300,表示希望緩存的記憶內容最多包含 300 個token。
這里導入所需庫:這段代碼導入了一系列所需的庫,包括os、dotenv、langchain相關庫、requests、BeautifulSoup、json和streamlit。
解釋
import os
from dotenv import load_dotenv
from langchain import PromptTemplate
from langchain.agents import initialize_agent, Tool
from langchain.agents import AgentType
from langchain.chat_models import ChatOpenAI
from langchain.prompts import MessagesPlaceholder
from langchain.memory import ConversationSummaryBufferMemory
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains.summarize import load_summarize_chain
from langchain.tools import BaseTool
from pydantic import BaseModel, Field
from langchain.schema import SystemMessage
from typing import Type
from bs4 import BeautifulSoup
import requests
import json
import streamlit as st
# 加載必要的參數
load_dotenv()
serper_api_key=os.getenv("SERPER_API_KEY")
browserless_api_key=os.getenv("BROWSERLESS_API_KEY")
openai_api_key=os.getenv("OPENAI_API_KEY")
main 函數:這是streamlit應用的主函數。它首先設置了頁面的標題和圖標,然后創建了一些header,并提供一個文本輸入框讓用戶輸入查詢。當用戶輸入查詢后,它會調用agent來處理這個查詢,并將結果顯示在頁面上。
解釋
def main():
st.set_page_config(page_title="AI Assistant Agent", page_icon=":dolphin:")
st.header("LangChain 實例講解 3 -- Agent", divider='rainbow')
st.header("AI Agent :blue[助理] :dolphin:")
query = st.text_input("請提問題和需求:")
if query:
st.write(f"開始收集和總結資料 【 {query}】 請稍等")
result = agent({"input": query})
st.info(result['output'])
至此Agent的使用示例代碼就描述完畢了,我們可以看到,其實Agents的功能就是其會自主的去選擇并利用最合適的工具,從而解決問題,我們提供的Tools越豐富,則其功能越強大。
Callbacks對于程序員們應該都不陌生,就是一個回調函數,這個函數允許我們在LLM的各個階段使用各種各樣的“鉤子”,從而達實現日志的記錄、監控以及流式傳輸等功能。在Langchain中,該回掉函數是通過繼承 BaseCallbackHandler 來實現的,該接口對于每一個訂閱事件都聲明了一個回掉函數。它的子類也就可以通過繼承它實現事件的處理。如官網所示:
解釋
class BaseCallbackHandler:
"""Base callback handler that can be used to handle callbacks from langchain."""
def on_llm_start(
self, serialized: Dict[str, Any], prompts: List[str], **kwargs: Any
) -> Any:
"""Run when LLM starts running."""
def on_chat_model_start(
self, serialized: Dict[str, Any], messages: List[List[BaseMessage]], **kwargs: Any
) -> Any:
"""Run when Chat Model starts running."""
def on_llm_new_token(self, token: str, **kwargs: Any) -> Any:
"""Run on new LLM token. Only available when streaming is enabled."""
def on_llm_end(self, response: LLMResult, **kwargs: Any) -> Any:
"""Run when LLM ends running."""
def on_llm_error(
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
) -> Any:
"""Run when LLM errors."""
def on_chain_start(
self, serialized: Dict[str, Any], inputs: Dict[str, Any], **kwargs: Any
) -> Any:
"""Run when chain starts running."""
def on_chain_end(self, outputs: Dict[str, Any], **kwargs: Any) -> Any:
"""Run when chain ends running."""
def on_chain_error(
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
) -> Any:
"""Run when chain errors."""
def on_tool_start(
self, serialized: Dict[str, Any], input_str: str, **kwargs: Any
) -> Any:
"""Run when tool starts running."""
def on_tool_end(self, output: str, **kwargs: Any) -> Any:
"""Run when tool ends running."""
def on_tool_error(
self, error: Union[Exception, KeyboardInterrupt], **kwargs: Any
) -> Any:
"""Run when tool errors."""
def on_text(self, text: str, **kwargs: Any) -> Any:
"""Run on arbitrary text."""
def on_agent_action(self, action: AgentAction, **kwargs: Any) -> Any:
"""Run on agent action."""
def on_agent_finish(self, finish: AgentFinish, **kwargs: Any) -> Any:
"""Run on agent end."""
這個類包含了一系列方法,這些方法在 langchain 的不同階段被調用,以便在處理過程中執行自定義操作。參考源碼BaseCallbackHandler:
on_llm_start: 當大語言模型(LLM)開始運行時調用。 on_chat_model_start: 當聊天模型開始運行時調用。 on_llm_new_token: 當有新的LLM令牌時調用。僅在啟用流式處理時可用。 on_llm_end: 當LLM運行結束時調用。 on_llm_error: 當LLM出現錯誤時調用。 on_chain_start: 當鏈開始運行時調用。 on_chain_end: 當鏈運行結束時調用。 on_chain_error: 當鏈出現錯誤時調用。 on_tool_start: 當工具開始運行時調用。 on_tool_end: 當工具運行結束時調用。 on_tool_error: 當工具出現錯誤時調用。 on_text: 當處理任意文本時調用。 on_agent_action: 當代理執行操作時調用。 on_agent_finish: 當代理結束時調用。
StdOutCallbackHandler 是 LangChain 支持的最基本的處理器,它繼承自 BaseCallbackHandler。這個處理器將所有回調信息打印到標準輸出,對于調試非常有用。以下是如何使用 StdOutCallbackHandler 的示例:
解釋
from langchain.callbacks import StdOutCallbackHandler
from langchain.chains import LLMChain
from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
handler = StdOutCallbackHandler()
llm = OpenAI()
prompt = PromptTemplate.from_template("Who is {name}?")
chain = LLMChain(llm=llm, prompt=prompt, callbacks=[handler])
chain.run(name="Super Mario")
在這個示例中,我們首先從 langchain.callbacks 模塊導入了 StdOutCallbackHandler 類。然后,創建了一個 StdOutCallbackHandler 實例,并將其賦值給變量 handler。接下來,導入了 LLMChain、OpenAI 和 PromptTemplate 類,并創建了相應的實例。在創建 LLMChain 實例時,將 callbacks 參數設置為一個包含 handler 的列表。這樣,當鏈運行時,所有的回調信息都會被打印到標準輸出。最后,使用 chain.run() 方法運行鏈,并傳入參數 name="Super Mario"。在鏈運行過程中,所有的回調信息將被 StdOutCallbackHandler 處理并打印到標準輸出。
解釋
from langchain.callbacks.base import BaseCallbackHandler
import time
class TimerHandler(BaseCallbackHandler):
def __init__(self) -> None:
super().__init__()
self.previous_ms = None
self.durations = []
def current_ms(self):
return int(time.time() * 1000 + time.perf_counter() % 1 * 1000)
def on_chain_start(self, serialized, inputs, **kwargs) -> None:
self.previous_ms = self.current_ms()
def on_chain_end(self, outputs, **kwargs) -> None:
if self.previous_ms:
duration = self.current_ms() - self.previous_ms
self.durations.append(duration)
def on_llm_start(self, serialized, prompts, **kwargs) -> None:
self.previous_ms = self.current_ms()
def on_llm_end(self, response, **kwargs) -> None:
if self.previous_ms:
duration = self.current_ms() - self.previous_ms
self.durations.append(duration)
llm = OpenAI()
timerHandler = TimerHandler()
prompt = PromptTemplate.from_template("What is the HEX code of color {color_name}?")
chain = LLMChain(llm=llm, prompt=prompt, callbacks=[timerHandler])
response = chain.run(color_name="blue")
print(response)
response = chain.run(color_name="purple")
print(response)
這個示例展示了如何通過繼承 BaseCallbackHandler 來實現自定義的回調處理器。在這個例子中,創建了一個名為 TimerHandler 的自定義處理器,它用于跟蹤 Chain 或 LLM 交互的起止時間,并統計每次交互的處理耗時。從 langchain.callbacks.base 模塊導入 BaseCallbackHandler 類。導入 time 模塊,用于處理時間相關操作。
定義 TimerHandler 類,繼承自 BaseCallbackHandler。在 TimerHandler 類的 init 方法中,初始化 previous_ms 和 durations 屬性。定義 current_ms 方法,用于返回當前時間的毫秒值。重寫 on_chain_start、on_chain_end、on_llm_start 和 on_llm_end 方法,在這些方法中記錄開始和結束時間,并計算處理耗時。接下來,我們創建了一個 OpenAI 實例、一個 TimerHandler 實例以及一個 PromptTemplate 實例。然后,我們創建了一個使用 timerHandler 作為回調處理器的 LLMChain 實例。最后,我們運行了兩次Chain,分別查詢藍色和紫色的十六進制代碼。在鏈運行過程中,TimerHandler 將記錄每次交互的處理耗時,并將其添加到 durations 列表中。
輸出如下:
1??通過構造函數參數 callbacks 設置。這種方式可以在創建對象時就設置好回調處理器。例如,在創建 LLMChain 或 OpenAI 對象時,可以通過 callbacks 參數設置回調處理器。
timerHandler = TimerHandler()
llm = OpenAI(callbacks=[timerHandler])
response = llm.predict("What is the HEX code of color BLACK?") print(response)
在這里構建llm的時候我們就直接指定了構造函數。
2??通過運行時的函數調用。這種方式可以在運行時動態設置回調處理器,如在Langchain的各module如Model,Agent,Tool,以及 Chain的請求執行函數設置回調處理器。例如,在調用 LLMChain 的 run 方法或 OpenAI 的 predict 方法時,可以通過 callbacks 參數設置回調處理器。以OpenAI 的 predict 方法為例:
解釋
timerHandler = TimerHandler()
llm = OpenAI()
response = llm.predict("What is the HEX code of color BLACK?", callbacks=[timerHandler])
print(response)
這段代碼首先創建一個 TimerHandler 實例并將其賦值給變量 timerHandler。然后創建一個 OpenAI 實例并將其賦值給變量 llm。調用 llm.predict() 方法,傳入問題 "What is the HEX code of color BLACK?",并通過 callbacks 參數設置回調處理器 timerHandler。
兩種方法的主要區別在于何時和如何設置回調處理器。
構造函數參數 callbacks 設置:在創建對象(如 OpenAI 或 LLMChain)時,就通過構造函數的 callbacks 參數設置回調處理器。這種方式的優點是你可以在對象創建時就確定回調處理器,后續在使用該對象時,無需再次設置。但如果在后續的使用過程中需要改變回調處理器,可能需要重新創建對象。
通過運行時的函數調用:在調用對象的某個方法(如 OpenAI 的 predict 方法或 LLMChain 的 run 方法)時,通過該方法的 callbacks 參數設置回調處理器。這種方式的優點是你可以在每次調用方法時動態地設置回調處理器,更加靈活。但每次調用方法時都需要設置,如果忘記設置可能會導致回調處理器不生效。
在實際使用中,可以根據需要選擇合適的方式。如果回調處理器在對象的整個生命周期中都不會變,可以選擇在構造函數中設置;如果回調處理器需要動態變化,可以選擇在運行時的函數調用中設置。
至此,Langchain的各個模塊使用方法就已經介紹完畢啦,相信你已經感受到Langchain的能力了~不難發現,LangChain 是一個功能十分強大的AI語言處理框架,它將Model IO、Retrieval、Memory、Chains、Agents和Callbacks這六個模塊組合在一起。Model IO負責處理AI模型的輸入和輸出,Retrieval模塊實現了與向量數據庫相關的檢索功能,Memory模塊則負責在對話過程中存儲和重新加載歷史對話記錄。Chains模塊充當了一個連接器的角色,將前面提到的模塊連接起來以實現更豐富的功能。Agents模塊通過理解用戶輸入來自主調用相關工具,使得應用更加智能化。而Callbacks模塊則提供了回調機制,方便開發者追蹤調用鏈路和記錄日志,以便更好地調試LLM模型。總之,LangChain是一個功能豐富、易于使用的AI語言處理框架,它可以幫助開發者快速搭建和優化AI應用。本文只是列舉了各模塊的核心使用方法和一些示例demo,建議結合本文認真閱讀一遍官方文檔會更加有所受益~
土耳其陸軍特種部隊簡稱“OKK”,當中的成員全部都是從陸軍挑選出來的精英,其戰斗力與西方著名特種部隊不相上下,同時所使用的武器裝備也不差。
那本期就來為大家盤點一下,土耳其OKK陸軍特種部隊的10種常用武器。
由土耳其科尼亞軍工,為土耳其軍隊研發的一種40毫米下掛式步槍榴彈發射器,可發射40×46毫米低速榴彈,從側面打開放入榴彈,合并扣動扳機就可發射,操作性簡單靈活,射程能達到300米左右,可通過導軌安裝在MPT-76突擊步槍上配合使用,一般土耳其OKK特種部隊也會裝備這種榴彈發射器。
M72 LAW是一種美制單兵火箭筒,其構造簡單,威力大,口徑為66毫米,可發射各類不同規格的彈藥,可對裝甲車、坦克、建筑和防御工事等進行破壞,本質上重量也很輕,還可以收起,方便士兵攜帶,土耳其OKK特種部隊在進行大規模戰斗時,通常也會裝備M72 LAW單兵火箭筒。
全名為:麥克米蘭TAC50重型反器材狙擊步槍,由美國在上世紀80年代期間研制,21世紀初期投入伊拉克和阿富汗戰爭當中使用,主要裝備美軍特種部隊的狙擊手,該槍的精準度高,射程遠,可發射12.7×99毫米彈藥,彈匣容量5發,有效射程可達到2000米,機匣頂部還可安裝16倍瞄準鏡,美軍特種部隊通常用這種狙擊步槍執行遠程狙擊任務,土耳其的OKK特種部隊作為精英,同樣也裝備了TAC50狙擊步槍。
由土耳其MKEK軍工在2004年到2008年期間,為軍方研制的一種手動式高精度狙擊步槍,也叫做Bora-12狙擊步槍,在2014年開始裝備,該槍采用模塊化設計,槍身附帶皮卡汀尼導軌,可以匹配各種不同規格的瞄準鏡,同時槍托部分還能折疊,槍管、雙腳支架等都可以拆卸,便于士兵攜帶。
該槍發射7.62×51毫米北約全威力彈藥,使用10發彈匣供彈,采用旋轉后拉式槍機,有效射程1200米,這種狙擊步槍一般主要裝備給土耳其的特種部隊使用,其中就包括陸軍的OKK特種部隊。
也可以叫做“M110半自動狙擊系統”,由美國奈特軍工開發,該槍采用氣吹式半自動發射原理,射速快,精度高,發射7.62×51毫米北約全威力彈藥,可以使用10發或20發彈匣供彈,槍口前端還能配備消音器,機匣上面也可根據不同的任務情況,配備不同類型的瞄準鏡,但默認情況下安裝的是Mark4-3.5×10可調倍率瞄準鏡,M110精確射手步槍通常作為美軍特種兵的專屬,但土耳其的OKK特種部隊也裝備了一部分。
M60E4也叫做MK43,是基于原本M60通用機槍改進出來的一個短管版車載武器,與此同時減輕了重量,士兵也可以直接攜帶作戰,M60E4仍然發射7.62×51毫米全威力彈藥,使用100發彈鏈供彈,每分鐘理論射速650發左右,整體性能要比之前的老款M60更好一些。
但在西方經過現代化戰術改進的M60E4,已經成為了特種部隊最常用的機槍,如今的土耳其OKK陸軍特種部隊,也裝備了M60E4的戰術改進版。
MPT-76是土耳其在2008年左右自行設計的一種突擊步槍,主要參考了德國HK416和HK417型,并分為兩個版本,比如5.56和7.62毫米,MPT-76型發射的就是7.62×51毫米彈藥,正常情況下會使用20發短彈匣供彈,可以全連發射擊,也可以單發點射,并且槍身自帶皮卡汀尼導軌,具備模塊化設計,既能作為常規的自動步槍,又能作為精確射手步槍使用。
在該槍的前面,還有一款5.56毫米版本的MPT-55型,但這個型號并不常見,土耳其OKK特種部隊正常采用的突擊步槍,就是7.62毫米版本的MPT-76突擊步槍,同時還會配合雙腳支架、握把或者40毫米榴彈發射器一起使用。
UTS-15是土耳其UTAS旗下為軍方所開發的一種無托式結構,手動戰斗霰彈槍,其外形非常科幻,并且也具備模塊化設計,機匣頂部自帶皮卡汀尼導軌,可用于擴展瞄準鏡和其他戰術組件。
槍身均采用工程塑料結構,發射12號口徑霰彈,彈藥當中也包括鉛彈、鹿彈和鳥彈等,目前該型霰彈槍主要被土耳其特種部隊所采用,其中就包括OKK陸軍特種部隊。
由土耳其薩爾希馬茲軍工所研發的一種新型沖鋒槍,采用AR式槍械結構,并發射9×19毫米帕拉貝魯姆手槍彈,能使用20或30發彈匣供彈,每分鐘理論射速900發,有效射程100米,槍身還安裝了皮卡汀尼導軌,可用于快速為其配備瞄準鏡和其他戰術組件,全槍長度705毫米,空槍重量3.32千克。
目前SAR-09T型沖鋒槍,也已經被土耳其的陸軍OKK特種部隊所少量裝備。
是由土耳其AKDAL軍工所研制的一種半自動手槍,主要模仿的就是奧地利格洛克G17型手槍,其內部的構造與前者格洛克G17基本相同,可發射9×19毫米帕拉貝魯姆手槍彈,使用17發彈匣供彈,有效射程50米。
同樣,TR-01型的套筒采用金屬結構,套筒底座則采用工程塑料結構,而且土耳其所生產的TR-01型手槍,在扳機的旁邊進一步結合了人體工程學設計,握持時手感要更好,套筒底座前部也帶有一小段導軌,可用于安裝紅外線激光指示器或者戰術手電筒,目前TR-01型也作為土耳其陸軍OKK特種部隊的常用手槍之一。
本期土耳其OKK陸軍特種部隊的10種常用武器,就為大家盤點到這里,想要了解更多,請記得關注,下期繼續為大家更新。