天天看点

基于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/

继续阅读