記得一個月前,Eugene Yan 在 Linkedin 上發了一個投票:
你是否因為不從事 LLM/生成式 AI 而感到 FOMO?
大多數人回答“是”。 鑒于 chatGPT 引起的廣泛關注以及現在 gpt-4 的釋出,原因很容易了解。 人們形容大型語言模型 (LLM) 的興起感覺就像是 iPhone 時刻。 但我認為真的沒有必要去感受 FOMO。 考慮一下:錯失開發 iPhone 的機會并不排除創造創新 iPhone 應用程式的巨大潛力。 LLM也是如此。 我們剛剛進入一個新時代的黎明,現在是利用內建 LLM 的魔力來建構強大應用程式的最佳時機。
在這篇文章中,我将介紹以下主題:
- 什麼是 OPL 棧?
- 如何使用 OPL 技術棧建構具有領域知識的 chatGPT? (帶代碼演練的基本元件)
- 生産注意事項
- 常見的誤解
推薦:用 NSDT設計器 快速搭建可程式設計3D場景。
1、什麼是OPL棧
OPL代表OpenAI、Pinecone和Langchain,越來越成為行業解決LLMs兩大局限的解決方案:
- LLM 的幻覺:chatGPT 有時會過于自信地提供錯誤的答案。 其中一個根本原因是,這些語言模型經過訓練可以非常有效地預測下一個單詞,或者準确地說是下一個标記。 給定一個輸入文本,chatGPT 将以高機率傳回單詞,這并不意味着 chatGPT 具有推理能力。
- 最新知識較少:chatGPT 的訓練資料僅限于 2021 年 9 月之前的網際網路資料。是以,如果你的問題是關于最近的趨勢或主題,它會産生不太理想的答案。
常見的解決方案是在 LLM 之上添加知識庫,并使用 Langchain 作為建構管道的架構。 每種技術的基本組成部分可以總結如下:
OpenAI:
- 提供對功能強大的 LLM(例如 chatGPT 和 gpt-4)的 API 通路
- 提供嵌入模型以将文本轉換為嵌入。
Pinecone:
- 提供嵌入向量存儲、語義相似度比較、快速檢索等功能。
Langchain:包含 6 個子產品(模型、提示、索引、記憶體、鍊和代理)。
- 模型在嵌入模型、聊天模型和 LLM 方面提供了靈活性,包括但不限于 OpenAI 的産品。 你還可以使用 Hugging Face 的其他模型,如 BLOOM 和 FLAN-T5。
- 記憶:有多種方法可以讓聊天機器人記住過去的對話記憶。 根據我的經驗,實體記憶體運作良好且高效。
- Chains:如果你是 Langchain 的新手,Chains 是一個很好的起點。 它遵循類似管道的結構來處理使用者輸入,選擇 LLM 模型,應用 Prompt 模闆,并從知識庫中搜尋相關上下文。
接下來,我将介紹我使用 OPL 堆棧建構的應用程式。
2、使用OPL建構領域chatGPT
我建構的應用程式稱為 chatOutside ,它有兩個主要部分:
- chatGPT:讓你直接與 chatGPT 聊天,格式類似于問答應用程式,一個輸入一個輸出。
- chatOutside:允許你使用具有戶外活動和趨勢專業知識的 chatGPT 版本聊天。 該格式更像是聊天機器人風格,其中所有消息都随着對話的進行而被記錄下來。 我還包括了一個提供源連結的部分,這可以增強使用者的信心并且總是有用的。
如你所見,如果你問同樣的問題:“2023 年最好的跑鞋是什麼? 我的預算大約是 200 美元”。 chatGPT 會說“作為一種 AI 語言模型,我無法通路未來的資訊。” 而 chatOutside 将為你提供更多最新答案以及源連結。
開發過程涉及三個主要步驟:
- 第 1 步:在 Pinecone 中建立外部知識庫
- 第2步:使用Langchain進行問答服務
- 第 3 步:在 Streamlit 中建構我們的應用程式
下面讨論每個步驟的實施細節。
3、第 1 步 -在 Pinecone 中建立外部知識庫
步驟 1.1:我連接配接到我們的外部目錄資料庫并選擇了 2022 年 1 月 1 日至 2023 年 3 月 29 日期間發表的文章。這為我們提供了大約 20,000 條記錄。
接下來,我們需要執行兩個資料轉換。
Step 1.2: 将上面的dataframe轉換為字典清單,保證資料可以正确的upserted到Pinecone中。
# Convert dataframe to a list of dict for Pinecone data upsert
data = df_item.to_dict('records')
Step 1.3:使用 Langchain 的 RecursiveCharacterTextSplitter 将内容拆分成更小的塊。 将文檔分解成更小的塊的好處是雙重的:
- 一篇典型的文章可能會超過 1000 個字元,這很長。 想象一下,我們想要檢索前 3 篇文章作為上下文來提示 chatGPT,我們很容易達到 4000 個令牌的限制。
- 較小的塊提供更多相關資訊,進而産生更好的上下文來提示 chatGPT。
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
chunk_size=400,
chunk_overlap=20,
length_function=tiktoken_len,
separators=["\n\n", "\n", " ", ""]
)
拆分後,每條記錄的内容被分解成多個塊,每個塊少于 400 個令牌。
值得注意的是,使用的文本拆分器稱為 RecursiveCharacterTextSplitter ,這是 Langchain 的建立者 Harrison Chase 推薦使用的。 基本思想是先按段落拆分,然後按句子拆分,重疊(20 個标記)。 這有助于保留周圍句子中有意義的資訊和上下文。
Step 1.4:将資料更新到 Pinecone。 以下代碼改編自詹姆斯布裡格斯的精彩教程。
import pinecone
from langchain.embeddings.openai import OpenAIEmbeddings
# 0. Initialize Pinecone Client
with open('./credentials.yml', 'r') as file:
cre = yaml.safe_load(file)
# pinecone API
pinecone_api_key = cre['pinecone']['apikey']
pinecone.init(api_key=pinecone_api_key, environment="us-west1-gcp")
# 1. Create a new index
index_name = 'outside-chatgpt'
# 2. Use OpenAI's ada-002 as embedding model
model_name = 'text-embedding-ada-002'
embed = OpenAIEmbeddings(
document_model_name=model_name,
query_model_name=model_name,
openai_api_key=OPENAI_API_KEY
)
embed_dimension = 1536
# 3. check if index already exists (it shouldn't if this is first time)
if index_name not in pinecone.list_indexes():
# if does not exist, create index
pinecone.create_index(
name=index_name,
metric='cosine',
dimension=embed_dimension
)
# 3. Connect to index
index = pinecone.Index(index_name)
我們批量上傳并嵌入所有文章。 插入 20k 條記錄大約需要 20 分鐘。 請務必根據你的環境相應地調整 tqdmimport(你不需要同時導入!)
# If using terminal
from tqdm.auto import tqdm
# If using in Jupyter notebook
from tqdm.autonotebook import tqdm
from uuid import uuid4
batch_limit = 100
texts = []
metadatas = []
for i, record in enumerate(tqdm(data)):
# 1. Get metadata fields for this record
metadata = {
'item_uuid': str(record['id']),
'source': record['url'],
'title': record['title']
}
# 2. Create chunks from the record text
record_texts = text_splitter.split_text(record['content'])
# 3. Create individual metadata dicts for each chunk
record_metadatas = [{
"chunk": j, "text": text, **metadata
} for j, text in enumerate(record_texts)]
# 4. Append these to current batches
texts.extend(record_texts)
metadatas.extend(record_metadatas)
# 5. Special case: if we have reached the batch_limit we can add texts
if len(texts) >= batch_limit:
ids = [str(uuid4()) for _ in range(len(texts))]
embeds = embed.embed_documents(texts)
index.upsert(vectors=zip(ids, embeds, metadatas))
texts = []
metadatas = []
更新外部文章資料後,我們可以使用 index.describe_index_stats() 檢查我們的Pinecone索引。 要注意的統計資料之一是 index_fullness,在我們的例子中為 0.2。 這意味着 Pinecone pod 已滿 20%,表明單個 p1 pod 可以存儲大約 10 萬篇文章。
4、第2步- 使用LangChain進行問答服務
注意:Langchain最近更新太快了,下面代碼使用的版本是0.0.118。
上面的草圖說明了資料在推理階段的流動方式:
- 使用者提出一個問題:“2023 年最好的跑鞋是什麼?”。
- 使用 ada-002 模型将問題轉換為嵌入。
- 使用 similarity_search 函數将使用者問題嵌入與存儲在 Pinecone 中的所有向量進行比較,該函數檢索最有可能回答問題的前 3 個文本塊。
- Langchain 然後将前 3 個文本塊作為 context 以及使用者問題傳遞給 gpt-3.5 (ChatCompletion) 以生成答案。
所有這些都可以用不到 30 行代碼實作:
from langchain.vectorstores import Pinecone
from langchain.chains import VectorDBQAWithSourcesChain
from langchain.embeddings.openai import OpenAIEmbeddings
# 1. Specify Pinecone as Vectorstore
# =======================================
# 1.1 get pinecone index name
index = pinecone.Index(index_name) #'outside-chatgpt'
# 1.2 specify embedding model
model_name = 'text-embedding-ada-002'
embed = OpenAIEmbeddings(
document_model_name=model_name,
query_model_name=model_name,
openai_api_key=OPENAI_API_KEY
)
# 1.3 provides text_field
text_field = "text"
vectorstore = Pinecone(
index, embed.embed_query, text_field
)
# 2. Wrap the chain as a function
qa_with_sources = VectorDBQAWithSourcesChain.from_chain_type(
llm=llm,
chain_type="stuff",
vectorstore=vectorstore
)
現在我們可以通過問一個與徒步相關的問題來測試:“你能推薦一些加州灣區有水景的進階徒步路線嗎?
5、第 3 步 - 在 Streamlit 中建構應用程式
在驗證邏輯在 Jupyter notebook 中正常工作後,我們可以将所有内容組裝在一起并使用 streamlit 建構前端。 在我們的 streamlit 應用程式中,有兩個 python 檔案:
- app.py:前端的主要 python 檔案并為應用程式供電
- utils.py : 将由 app.py 調用的支援函數
這是我的 utils.py 的樣子:
import pinecone
import streamlit as st
from langchain.chains import VectorDBQAWithSourcesChain
from langchain.chat_models import ChatOpenAI
from langchain.vectorstores import Pinecone
from langchain.embeddings.openai import OpenAIEmbeddings
# ------OpenAI: LLM---------------
OPENAI_API_KEY = st.secrets["OPENAI_KEY"]
llm = ChatOpenAI(
openai_api_key=OPENAI_API_KEY,
model_name='gpt-3.5-turbo',
temperature=0.0
)
# ------OpenAI: Embed model-------------
model_name = 'text-embedding-ada-002'
embed = OpenAIEmbeddings(
document_model_name=model_name,
query_model_name=model_name,
openai_api_key=OPENAI_API_KEY
)
# --- Pinecone ------
pinecone_api_key = st.secrets["PINECONE_API_KEY"]
pinecone.init(api_key=pinecone_api_key, environment="us-west1-gcp")
index_name = "outside-chatgpt"
index = pinecone.Index(index_name)
text_field = "text"
vectorstore = Pinecone(index, embed.embed_query, text_field)
# ======= Langchain ChatDBQA with source chain =======
def qa_with_sources(query):
qa = VectorDBQAWithSourcesChain.from_chain_type(
llm=llm,
chain_type="stuff",
vectorstore=vectorstore
)
response = qa(query)
return response
最後,這是我的 app.py 的樣子:
import os
import openai
from PIL import Image
from streamlit_chat import message
from utils import *
openai.api_key = st.secrets["OPENAI_KEY"]
# For Langchain
os.environ["OPENAI_API_KEY"] = openai.api_key
# ==== Section 1: Streamlit Settings ======
with st.sidebar:
st.markdown("# Welcome to chatOutside ")
st.markdown(
"**chatOutside** allows you to talk to version of **chatGPT** \n"
"that has access to latest Outside content! \n"
)
st.markdown(
"Unlike chatGPT, chatOutside can't make stuff up\n"
"and will answer from Outside knowledge base. \n"
)
st.markdown(" Developer: Wen Yang")
st.markdown("---")
st.markdown("# Under The Hood ")
st.markdown("How to Prevent Large Language Model (LLM) hallucination?")
st.markdown("- **Pinecone**: vector database for Outside knowledge")
st.markdown("- **Langchain**: to remember the context of the conversation")
# Homepage title
st.title("chatOutside: Outside + ChatGPT")
# Hero Image
image = Image.open('VideoBkg_08.jpg')
st.image(image, caption='Get Outside!')
st.header("chatGPT ")
# ====== Section 2: ChatGPT only ======
def chatgpt(prompt):
res = openai.ChatCompletion.create(
model='gpt-3.5-turbo',
messages=[
{"role": "system",
"content": "You are a friendly and helpful assistant. "
"Answer the question as truthfully as possible. "
"If unsure, say you don't know."},
{"role": "user", "content": prompt},
],
temperature=0,
)["choices"][0]["message"]["content"]
return res
input_gpt = st.text_input(label='Chat here! ')
output_gpt = st.text_area(label="Answered by chatGPT:",
value=chatgpt(input_gpt), height=200)
# ========= End of Section 2 ===========
# ========== Section 3: chatOutside ============================
st.header("chatOutside ️")
def chatoutside(query):
# start chat with chatOutside
try:
response = qa_with_sources(query)
answer = response['answer']
source = response['sources']
except Exception as e:
print("I'm afraid your question failed! This is the error: ")
print(e)
return None
if len(answer) > 0:
return answer, source
else:
return None
# ============================================================
# ========== Section 4. Display ChatOutside in chatbot style ===========
if 'generated' not in st.session_state:
st.session_state['generated'] = []
if 'past' not in st.session_state:
st.session_state['past'] = []
if 'source' not in st.session_state:
st.session_state['source'] = []
def clear_text():
st.session_state["input"] = ""
# We will get the user's input by calling the get_text function
def get_text():
input_text = st.text_input('Chat here! ', key="input")
return input_text
user_input = get_text()
if user_input:
# source contain urls from Outside
output, source = chatoutside(user_input)
# store the output
st.session_state.past.append(user_input)
st.session_state.generated.append(output)
st.session_state.source.append(source)
# Display source urls
st.write(source)
if st.session_state['generated']:
for i in range(len(st.session_state['generated'])-1, -1, -1):
message(st.session_state["generated"][i], key=str(i))
message(st.session_state['past'][i], is_user=True,
avatar_style="big-ears", key=str(i) + '_user')
6、OPL生産注意事項
好吧,足夠的編碼!
該應用程式實際上已經很不錯了。 但是如果我們想轉向生産,還有一些額外的事情需要考慮:
- 在 Pinecone 中攝取新資料和更新資料:我們對文章資料進行了一次批量更新插入。 實際上,每天都有新文章添加到我們的網站,并且某些字段可能會根據已攝取到 Pinecone 中的資料進行更新。 這不是機器學習問題,但它一直存在于媒體公司:如何在每項服務中保持資料更新。 潛在的解決方案是設定一個 cron 作業來運作 upsert 并定期更新作業。 有一個關于如何并行發送更新插入的說明,如果我們可以使用 Django 和 Celery 的異步任務,這可能會非常有用。
- Pinecone pod 存儲的限制:該應用程式目前使用 p1 pod,它可以存儲多達 100 萬個 768 維向量,如果我們使用 OpenAI 的 ada-002embedding 模型(次元為 1536),則大約可以存儲 500k 個向量。
- 用于更快響應時間的流功能:為了減少使用者感覺的延遲,向應用程式添加流功能可能會有所幫助。 這将通過逐個令牌傳回生成的輸出令牌來模仿 chatGPT,而不是一次顯示整個響應。 雖然此功能适用于使用 LangChain 函數的 REST API,但它對我們提出了獨特的挑戰,因為我們使用 GraphQL 而不是 REST。
7、OPL常見的誤解和問題
- chatGPT 會記住 2021 年 9 月之前的網際網路資料。它會根據記憶檢索答案。
這不是它的工作原理。 訓練後,chatGPT 從記憶體中删除資料并使用其 1750 億個參數(權重)來預測最可能的标記(文本)。 它不會根據記憶檢索答案。 這就是為什麼如果你隻是複制 chatGPT 生成的答案,你不太可能從網際網路上找到任何來源。
- 我們可以訓練/微調/提示工程聊天 GPT。
訓練和微調大型語言模型實際上意味着改變模型參數。 你需要有權通路實際模型檔案并針對你的特定用例指導模型。 在大多數情況下,我們不會訓練或微調 chatGPT。 我們隻需要及時的工程:為 chatGPT 提供額外的上下文并允許它根據上下文進行回答。
- token 和 word 有什麼差別?
Token 是一個詞塊。 100 個标記大約等于 75 個單詞。 例如,“Unbelievable”是一個詞,但有 3 個标記(un、belie、able)。
- 4000 個令牌的限制是什麼意思?
OpenAI gpt-3.5 的令牌限制為 4096,用于組合使用者輸入、上下文和響應。 使用Langchain的記憶體時,(使用者問題+上下文+記憶體+chatGPT響應)使用的總詞數需要小于3000詞(4000個代币)。
gpt-4 有更高的令牌限制,但它也貴 20 倍! (gpt-3.5:0.002 美元/1K 代币;gpt-4:0.045 美元/1K 代币,假設 500 用于提示,500 用于完成)。
- 我必須使用像 Pinecone 一樣的 Vector Store 嗎?
不。Pinecode不是矢量存儲的唯一選擇。 其他矢量存儲選項包括 Chroma、FAISS、Redis 等。 此外,你并不總是需要向量存儲。 例如,如果你想為特定網站建構一個問答,你可以抓取網頁并遵循這個 openai-cookbook-recipe。
原文連結:http://www.bimant.com/blog/llm-app-dev-with-opl-stack/