NSDT工具推荐: Three.js AI纹理开发包 - YOLO合成数据生成器 - GLTF/GLB在线编辑 - 3D模型格式在线转换 - 可编程3D场景编辑器 - REVIT导出3D模型插件 - 3D模型语义搜索引擎 - AI模型在线查看 - Three.js虚拟轴心开发包 - 3D模型在线减面 - STL模型在线切割 - 3D道路快速建模
记得一个月前,Eugene Yan 在 Linkedin 上发了一个投票:
你是否因为不从事 LLM/生成式 AI 而感到 FOMO?
大多数人回答“是”。 鉴于 chatGPT 引起的广泛关注以及现在 gpt-4 的发布,原因很容易理解。 人们形容大型语言模型 (LLM) 的兴起感觉就像是 iPhone 时刻。 但我认为真的没有必要去感受 FOMO。 考虑一下:错失开发 iPhone 的机会并不排除创造创新 iPhone 应用程序的巨大潜力。 LLM也是如此。 我们刚刚进入一个新时代的黎明,现在是利用集成 LLM 的魔力来构建强大应用程序的最佳时机。
在这篇文章中,我将介绍以下主题:
- 什么是 OPL 栈?
- 如何使用 OPL 技术栈构建具有领域知识的 chatGPT? (带代码演练的基本组件)
- 生产注意事项
- 常见的误解
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。
原文链接:Building LLMs-Powered Apps with OPL Stack
BimAnt翻译整理,转载请标明出处