LangChain初探:為你的AI應用之旅導航

蛋先生DX發表於2024-03-01

先來個溫馨的小提醒:

這篇文章雖然較為全面地介紹了 LangChain,但都是點到為止,只是讓你瞭解一下它的皮毛而已,適合小白選手。

So,如果你是 LangChain 的小白,看完之後還是一頭霧水,那就請毫不留情地,狠狠地 .................................... 給我點贊吧!有了你的鼓勵,我會再接再厲的!(ง •\_•)ง

What?

丹尼爾:蛋兄,剛剛聽到別人在說 LangChain,你知道是啥玩意嗎?

蛋先生:哦,LangChain 啊,一個開發框架

丹尼爾:開發啥的框架?

蛋先生:一個用於開發語言模型驅動的應用的框架

丹尼爾:哦,開發這種應用,不就是寫寫 Prompt 提示語,調調語言模型 API 的事麼?

蛋先生:沒錯。但 LangChain 使得 Prompt 的編寫,API 的呼叫更加標準化

丹尼爾:就這樣嗎?

蛋先生:當然不止,它還有很多很酷的功能

丹尼爾:比如?

蛋先生:它可以連線外部資料來源,根據輸入檢索相關資料作為上下文給到語言模型,使得語言模型可以回答訓練資料之外的問題。這是由 LangChain 的 Retrieval 來實現的

丹尼爾:太酷了,我想到了一個場景,比如透過它來連線客服的回答話術庫,這樣就可以讓語言模型搖身一變,變成一個專業的客服了

蛋先生:恩,這是一個很好的場景

丹尼爾:還有其它更酷的功能嗎?

蛋先生:它可以讓語言模型來自行決定採取哪些行動

丹尼爾:這個就不是很明白了

蛋先生:接著你那個客服的例子繼續說。如果使用者問的問題是關於公司產品的,我們就想讓語言模型使用客服的話術庫來回答;如果是其它問題,就讓語言模型用它自己的知識來直接回答。如果是你,你會怎麼實現?

丹尼爾:我想我會先透過語言模型來判斷使用者的問題是否關於公司產品。如果是,就走連線話術庫的邏輯;如果不是,就走讓語言模型直接回答的邏輯

蛋先生:恩,你這種就是 hardcode 邏輯的方式。還有一種更加 amazing 的 方式,就是讓語言模型自行決定採取哪種行為。這個由 LangChain 的 Agent 來實現。

丹尼爾:聽上去太酷了,怎麼用呢?

蛋先生:莫急,待我慢慢道來

Why?

丹尼爾:蛋兄,你剛剛說到 LangChain 使得 Prompt 的編寫,API 的呼叫更加標準化,標準化了肯定是好的,但好處很大嗎?我用語言模型的 SDK 不也用得好好的嗎?

蛋先生:那你先給一個使用 SDK 與語言模型互動的例子唄

丹尼爾:這還不簡單,我就用這個吧:fireworks.ai (注:這個平臺提供免費的資源,訪問也方便)

from fireworks.client import Fireworks

client = Fireworks(api_key="<FIREWORKS_API_KEY>")
response = client.chat.completions.create(
  model="accounts/fireworks/models/llama-v2-7b-chat",
  messages=[{
    "role": "user",
    "content": "Who are you?",
  }],
)
print(response.choices[0].message.content)
輸出:
Hello! I'm just an AI assistant, here to help you in any way I can. My purpose is to provide helpful and respectful responses, always being safe and socially unbiased. I'm here to assist you in a positive and ethical manner, and I'm happy to help you with any questions or tasks you may have. Is there anything specific you would like me to help you with?

蛋先生:很好,再給另外一個語言模型的例子唄

丹尼爾:額,一樣的操作啊,你這是在消遣我嗎?好吧,那我就再給一個百度的文心一言的例子

import os
import qianfan

os.environ["QIANFAN_AK"] = "<QIANFAN_AK>"
os.environ["QIANFAN_SK"] = "<QIANFAN_SK>"

chat_comp = qianfan.ChatCompletion()

resp = chat_comp.do(messages=[{
    "role": "user",
    "content": "Who are you?"
}])

print(resp.body['result'])
輸出:
您好,我是百度研發的知識增強大語言模型,中文名是文心一言,英文名是ERNIE Bot。我能夠與人對話互動,回答問題,協助創作,高效便捷地幫助人們獲取資訊、知識和靈感。

蛋先生:Good,現在假設我一開始使用 fireworks 來開發應用,過程中發現效果不太理想,想換成文心一言呢?

丹尼爾:Oh\~,各個語言模型的 SDK 的介面定義是不一樣的,替換起來確實麻煩。來吧,是時候開始你的表演了

蛋先生:我們直接來看下透過 LangChain 使用 fireworks 和 文心一言 的程式碼示例吧,畢竟 No Code No BB 嘛

  • fireworks LangChain 示例
import os
from langchain_community.chat_models.fireworks import ChatFireworks

os.environ["FIREWORKS_API_KEY"] = '<FIREWORKS_API_KEY>'
model = ChatFireworks(model="accounts/fireworks/models/llama-v2-13b-chat")

res = model.invoke("Who are you?")
print(res.content)
  • 文心一言 LangChain 示例
import os
from langchain_community.chat_models import QianfanChatEndpoint

os.environ["QIANFAN_AK"] = "<QIANFAN_AK>"
os.environ["QIANFAN_SK"] = "<QIANFAN_SK>"
model = QianfanChatEndpoint(model="ERNIE-Bot-turbo")

res = model.invoke("Who are you?")
print(res.content)

丹尼爾:好像看出來了,標準化之後,要更換語言模型變得非常方便了,只需要更換下 model 的例項化就行了

蛋先生:是的,這只是個最簡單的例子,LangChain 還有很多種優雅的方式來切換不同的模型。從此以後我們就可以專注於 Prompt 的開發了。語言模型嘛,哪個合適換哪個

How?

丹尼爾:好了,我決定入坑 LangChain 了,那咱們進一步聊聊?

蛋先生:當然可以!我們從簡單到複雜,結合程式碼和流程圖來展示 LangChain 的一些用法。先來最簡單的代替 SDK 的用法,這個上邊已經有提到了

res = model.invoke("tell me a short joke about a cat")
print(res.content)

image

丹尼爾:恩,這個 so easy,一瞄就懂

蛋先生:OK,那接下來我們來使用 PromptTemplate,透過變數的方式來控制模板裡的部分內容

from langchain_core.prompts import ChatPromptTemplate

...

prompt = ChatPromptTemplate.from_template("tell me a short joke about {topic}")
chain = prompt | model
res = chain.invoke({"topic": "a cat"})
print(res.content)

image

丹尼爾:使用 PromptTemplate 的方式來寫 prompt,確實比字串的拼接要優雅不少

蛋先生:再加個簡單的輸出轉換吧

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser

...

prompt = ChatPromptTemplate.from_template("tell me a short joke about {topic}")
output_parser = StrOutputParser()
chain = prompt | model | output_parser
res = chain.invoke({"topic": "a cat"})
print(res)

image

丹尼爾:終於知道為啥叫 chain

蛋先生:繼續?

丹尼爾:繼續...

蛋先生:接下來這段程式碼可能有點長哦

from langchain_community.embeddings import QianfanEmbeddingsEndpoint
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.vectorstores import faiss
from langchain_community.chat_models import QianfanChatEndpoint

from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.chains.combine_documents import create_stuff_documents_chain

from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser

# 1
docs = WebBaseLoader("https://docs.smith.langchain.com").load()
embeddings = QianfanEmbeddingsEndpoint()
documents = RecursiveCharacterTextSplitter(chunk_size=900).split_documents(docs)
vector = faiss.FAISS.from_documents(documents, embeddings)
retriever = vector.as_retriever(search_kwargs={'k': 4})

# 2
setup_and_retrieval = RunnableParallel(
    {"context": retriever, "input": RunnablePassthrough()}
)
prompt = ChatPromptTemplate.from_template("""Answer the following question based only on the provided context:
<context>
{context}
</context>
Question: {input}""")
model = QianfanChatEndpoint(streaming=False, model="ERNIE-Bot-turbo")
output_parser = StrOutputParser()

# 3
retrieval_chain = setup_and_retrieval | prompt | model | output_parser

res = retrieval_chain.invoke("how can langsmith help with testing?")
print(res)

丹尼爾:請把“可能”去掉,謝謝

蛋先生:但邏輯其實並不複雜,主要分為三塊

1)載入網頁文件,透過 Embeddings 將文件內容轉成向量並儲存在向量資料庫 FAISS 中,retriever 就是一個可以根據輸入從向量資料庫獲取相關文件的檢索工具

2)宣告 chain 的各個步驟

3)將各個步驟按順序 chain 起來

丹尼爾:等等,看著有點腦殼疼。Embeddings?向量?向量資料庫?

蛋先生:咱們今天是“初探”,所以也只能簡單講講,不然很多同學就要昏昏欲睡了

丹尼爾:沒問題,有個大概印象也好

蛋先生:首先,為什麼要將文字轉成向量呢?因為透過計算兩個向量的距離,我們就可以量化地評估它們的相關性。距離越小,通常意味著文字之間的相關性越高。我們這裡是需要檢索與輸入相關的文件內容,將其作為會話上下文提供給語言模型。如果是整個文件都傳過去,是不是就太大了呢?

丹尼爾:哦,原來向量有這麼高階的功能啊

蛋先生:沒錯。然後要將文字轉成向量,就需要用到 Embeddings(詞嵌入)技術。Embeddings 在歷史上有過多種方法,如基於統計的計數方法,基於神經網路的推理方法等。 QianfanEmbeddingsEndpoint 正是一個利用深度學習訓練得到的 Embeddings 模型服務,輸入為文字,輸出為向量

丹尼爾:大概有點明白了

蛋先生:那我們接著看下流程圖

image

丹尼爾:能否為小弟我解釋一下上面這個流程圖的前半部分

蛋先生:當然!首先輸入是 "how can langsmith help with testing?";接著有個並行的邏輯,一個是透過 Retriever 根據輸入檢索相關的文件內容作為 context 的值,另一個則是直接 pass 將輸入作為 input 的值;然後就是將資料傳給 Prompt 模板,最終就可以得到傳給語言模型的 PromptValue 了

丹尼爾:Soga

蛋先生:注意,壓軸要登場了哦,現在讓我們來請出大名鼎鼎的 Agent 吧

from langchain import hub
from langchain.agents import AgentExecutor, create_json_chat_agent
from langchain.tools import tool
from langchain_community.chat_models.fireworks import ChatFireworks

@tool
def leng(word: str) -> str:
    """Please use this tool if you want to find the length of the word."""
    return len(word)
@tool
def lower(word: str) -> str:
    """Please use this tool if you need to change the word to lowercase."""
    return f'dx_{word.lower()}'


tools = [leng, lower]
model = ChatFireworks(model="accounts/fireworks/models/llama-v2-70b-chat")
prompt = hub.pull("hwchase17/react-chat-json")

agent = create_json_chat_agent(model, tools, prompt, stop_sequence=False)
agent_executor = AgentExecutor(
    agent=agent, tools=tools, verbose=True, handle_parsing_errors=True, max_iterations=5)
    
res = agent_executor.invoke({"input": "Make this word lowercase: 'Daniel'"})
print(res)
{'input': "Make this word lowercase: 'Daniel'", 'output': "The lowercase version of 'Daniel' is 'dx_daniel'"}

丹尼爾:好耶,快點講解一下吧

蛋先生:首先我們宣告瞭兩個工具:一個是 leng(用於求字串長度),一個是 lower(用於將字串變成小寫)。這裡為了證明結果是透過我們的工具來得到結果的,所以特意在 lower 的實現中加了個 dx\_ 字首

丹尼爾:等等,hub.pull("hwchase17/react-chat-json") 是什麼神秘程式碼?

蛋先生:這是 LangChain hub 社群上共享的用於實現 Agent 的眾多 Prompt 中的一個,你可以在這裡找到很多有用的 Prompt。畢竟,語言工程也是一種藝術,也是需要實踐積累的。

丹尼爾:明白,請繼續

蛋先生:透過 Agent,語言模型就可以根據輸入自行判斷應該使用哪個工具了

丹尼爾:哇,這太神奇了!我對它是怎麼自行判斷很感興趣

蛋先生:簡單來說,語言模型可以根據輸入,再根據各個工具的描述,來判斷哪個工具更適合,然後將結果輸出為可以讓 LangChain 理解的執行指令(比如 JSON)

丹尼爾:太棒了!現在我對 LangChain 有了一個大致的瞭解,希望以後還能跟你繼續深入探討

蛋先生:機會有滴是,咱們後會有期!ヾ( ̄▽ ̄)Bye~Bye~

相關文章