天天看點

基于OPL棧的LLM應用開發【OpenAI/Pinecone/Langchain】

作者:新缸中之腦

記得一個月前,Eugene Yan 在 Linkedin 上發了一個投票:

你是否因為不從事 LLM/生成式 AI 而感到 FOMO?

大多數人回答“是”。 鑒于 chatGPT 引起的廣泛關注以及現在 gpt-4 的釋出,原因很容易了解。 人們形容大型語言模型 (LLM) 的興起感覺就像是 iPhone 時刻。 但我認為真的沒有必要去感受 FOMO。 考慮一下:錯失開發 iPhone 的機會并不排除創造創新 iPhone 應用程式的巨大潛力。 LLM也是如此。 我們剛剛進入一個新時代的黎明,現在是利用內建 LLM 的魔力來建構強大應用程式的最佳時機。

在這篇文章中,我将介紹以下主題:

  • 什麼是 OPL 棧?
  • 如何使用 OPL 技術棧建構具有領域知識的 chatGPT? (帶代碼演練的基本元件)
  • 生産注意事項
  • 常見的誤解
基于OPL棧的LLM應用開發【OpenAI/Pinecone/Langchain】
推薦:用 NSDT設計器 快速搭建可程式設計3D場景。

1、什麼是OPL棧

基于OPL棧的LLM應用開發【OpenAI/Pinecone/Langchain】

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 将為你提供更多最新答案以及源連結。

基于OPL棧的LLM應用開發【OpenAI/Pinecone/Langchain】

開發過程涉及三個主要步驟:

  • 第 1 步:在 Pinecone 中建立外部知識庫
  • 第2步:使用Langchain進行問答服務
  • 第 3 步:在 Streamlit 中建構我們的應用程式

下面讨論每個步驟的實施細節。

3、第 1 步 -在 Pinecone 中建立外部知識庫

步驟 1.1:我連接配接到我們的外部目錄資料庫并選擇了 2022 年 1 月 1 日至 2023 年 3 月 29 日期間發表的文章。這為我們提供了大約 20,000 條記錄。

基于OPL棧的LLM應用開發【OpenAI/Pinecone/Langchain】

接下來,我們需要執行兩個資料轉換。

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 個令牌。

基于OPL棧的LLM應用開發【OpenAI/Pinecone/Langchain】

值得注意的是,使用的文本拆分器稱為 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 萬篇文章。

基于OPL棧的LLM應用開發【OpenAI/Pinecone/Langchain】

4、第2步- 使用LangChain進行問答服務

注意:Langchain最近更新太快了,下面代碼使用的版本是0.0.118。

基于OPL棧的LLM應用開發【OpenAI/Pinecone/Langchain】

上面的草圖說明了資料在推理階段的流動方式:

  • 使用者提出一個問題:“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
)           

現在我們可以通過問一個與徒步相關的問題來測試:“你能推薦一些加州灣區有水景的進階徒步路線嗎?

基于OPL棧的LLM應用開發【OpenAI/Pinecone/Langchain】

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/

繼續閱讀