基于Prefect、Weave和RAGAS的RAG应用开发实战 原创
本文介绍了RAG应用程序开发中的一些增强技术,例如加入线下/线上评估、借助Prefect自动提取数据等。
几个月前,我发布了一款电影搜索应用程序“Film Search”,这是一个检索增强生成(RAG)应用程序,旨在根据用户查询实现电影推荐。例如,用户可能会发出下面的英文提问:
“Find me drama movies in English that are less than 2 hours long and feature dogs.”
中文意思:“给我找一部长度不到2小时、以狗为主角的英文剧情片。”
之后,用户会收到类似下面这样的英文推荐:
Title of Film: Hachi: A Dog’s Tale
Runtime: 93 minutes
Release Year: 2009
Streaming: Not available for streaming
This film tells the poignant true story of Hachiko, an Akita dog known for his remarkable loyalty to his owner. The emotional depth and the themes of friendship and loyalty resonate strongly, making it a touching drama that showcases the profound bond between humans and dogs. It’s perfect for anyone looking for a heartfelt story that highlights the importance of companionship.
对应的中文回答意思是:
电影名称:忠犬八公的故事
片长:93分钟
发行年份:2009
流媒体:不可用于流媒体
这部电影告诉了秋田犬Hachiko的辛酸真实故事,这种犬以其对主人的非凡忠诚而闻名。情感深度以及友谊和忠诚的主题引起了强烈的共鸣,使其成为一部感人的电影,展示了人与狗之间的深厚友谊。它非常适合想寻找一个发自内心的故事来强调友谊重要性的人。
这个软件不仅仅是一个简单的RAG应用程序。该程序中使用了所谓的自查询检索。这意味着,机器人接受用户的查询,并通过添加元数据过滤器对其进行转换。这样就确保了拉入聊天模型上下文的任何文档都遵守用户查询设置的约束。有关更多信息,我建议查看我文后提供的我早些时候发表的文章的链接。
但遗憾的是,该应用程序尚存在如下一些问题:
- 除了通过“肉眼测试”外,并没有进行离线评估。这种测试是必要的,但还不够。
- 没有提供可观察性支持。如果查询进展不顺利,你必须手动调出项目并运行一些特别的脚本,以便找出问题所在。
- 必须手动拉入Pinecone向量数据库。这意味着,如果一部电影从流媒体服务中撤下,这些文件很快就会过时。
在本文中,我将简要介绍对以前开发的那款电影搜索应用程序“Film Search”所做的一些改进,具体的改进内容将包括:
- 使用RAGAS和Weave进行离线评估
- 在线评估和可观察性
- 使用Prefect自动提取数据
在我们正式开始之前,还有一个细节需要说明:我发现“Film Search”这个名字有点笼统,所以我把这个应用程序重新命名为“Rosebud”,如下图所示。不用多作解释,我想任何一位真正的电影迷都会明白这个意思的(【译者注】影片Citizen Kane(公民凯恩)的故事是由报业巨子凯恩临死前说的一个字“玫瑰花蕾”(Rosebud)引出的)。
程序名字“Film Search”更改为“Rosebud”,此图来自Unsplash网站
线下评估
能够判断对LLM应用程序所做的更改是提高还是降低了程序性能,这一点是非常重要的。不幸的是,LLM应用程序的评估是一个困难而新颖的领域。对于什么是好的评估,根本没有达成太多的共识。
在新的程序Rosebud中,我决定解决所谓的“RAG Triad(RAG三元组):https://www.trulens.org/trulens_eval/getting_started/core_concepts/rag_triad/”问题。这种方法由TruLens推出,TruLens是一个评估和跟踪LLM应用程序的平台。
RAG Triad(RAG三元组)
概括来看,三元组涵盖了RAG应用程序的三个方面:
- 上下文相关性:当用户进行查询时,文档会填充聊天模型的上下文。检索到的上下文真的有用吗?如果没有用,你可能需要调整文档嵌入、分块或元数据过滤等操作。
- 可信度:模型的响应是否真的基于检索到的文档而生成?你不希望模型编造事实;RAG的全部目的是通过使用检索到的文档来帮助减少幻觉。
- 答案相关性:模型的响应是否真正回答了用户的查询?如果用户询问“20世纪90年代制作的喜剧电影有哪些?”,该模型的答案最好只包含20世纪90时代制作的喜剧影片。
目前,已经存在几种方法可以尝试评估RAG应用程序的这三个功能。一种方法是借助人类专家评估员。不幸的是,这种方法十分昂贵,而且无法扩展。在新的程序Rosebud中,我决定使用大型数据模型进行评估。这意味着,使用聊天模型来查看上述三个标准中的每一个,并为每个标准分配0到1的分数值。这种方法具有成本低、可扩展性好的优点。为了实现这一点,我使用了RAGAS(https://github.com/explodinggradients/ragas),这是一个流行的框架,可以帮助你评估RAG应用程序。RAGAS框架包括上述三个指标,可以很容易地使用它们来评估你的应用程序。下面是一个代码片段,演示了我是如何使用开源的RAGAS框架进行离线评估的:
from ragas import evaluate
from ragas.metrics import AnswerRelevancy, ContextRelevancy, Faithfulness
import weave
@weave.op()
def evaluate_with_ragas(query, model_output):
#将数据放入一个数据集对象中
data = {
"question": [query],
"contexts": [[model_output['context']]],
"answer": [model_output['answer']]
}
dataset = Dataset.from_dict(data)
# 定义要判断的指标
metrics = [
AnswerRelevancy(),
ContextRelevancy(),
Faithfulness(),
]
judge_model = ChatOpenAI(model=config['JUDGE_MODEL_NAME'])
embeddings_model = OpenAIEmbeddings(model=config['EMBEDDING_MODEL_NAME'])
evaluation = evaluate(dataset=dataset, metrics=metrics, llm=judge_model, embeddings=embeddings_model)
return {
"answer_relevancy": float(evaluation['answer_relevancy']),
"context_relevancy": float(evaluation['context_relevancy']),
"faithfulness": float(evaluation['faithfulness']),
}
def run_evaluation():
#初始化聊天模型
model = rosebud_chat_model()
# 定义评估问题
questions = [
{"query": "Suggest a good movie based on a book."}, # Adaptations
{"query": "Suggest a film for a cozy night in."}, # Mood-Based
{"query": "What are some must-watch horror movies?"}, # Genre-Specific
...
# 共20个问题
]
#创建Weave评估对象
evaluation = weave.Evaluation(dataset=questions, scorers=[evaluate_with_ragas])
#运行评估
asyncio.run(evaluation.evaluate(model))
if __name__ == "__main__":
weave.init('film-search')
run_evaluation()
在上述代码中,有几点注意事项:
- 有20个问题和3个评判标准,你会看到60次LLM调用仅需要一次评估!然而,接下来的情况变得更糟了:通过调用函数rosebud_chat_model,每个查询都需要两次调用。其中,一个用于构造元数据过滤器,另一个用于提供答案;所以,实际上这是对单个模型计算的120次调用!我评估的所有模型都是使用新的gpt-4o-mini,我也强烈推荐使用这种模型。根据我的经验,每次评估的调用费用为0.05美元。
- 请注意,我们使用了异步的asyncio.run运行模型计算。这种情况下,使用异步调用是比较合适的,因为你不想一个接一个地以顺序方式评估每个问题。相反,借助于asyncio框架,我们可以在等待之前的I/O操作完成时开始评估其他的问题。
- 一次评估共有20个问题。这些涵盖了用户可能会提问的各种典型的电影查询。这些大多是我自己想出的,但在实践中,最好使用生产中用户实际提出的查询。
- 请注意正在使用的weap.init和@weap.op装饰器。它们是Weights & Biases(W&B) AI开发者平台提供的新的Weave库的一部分。其中,Weave库是对传统W&B库的补充,专注于LLM应用程序。它允许你通过使用简单的@weap.op装饰器来捕获LLM的输入和输出。它还允许你使用weave.Evaluation(…)评估结果。通过集成RAGAS来执行评估,并集成Weave框架来捕获和记录它们,我们便有了一个强大的组合,可以帮助GenAI开发人员以迭代方式不断改进他们的应用程序。此外,你还可以记录下模型延迟、所需成本等其他信息。
集成Weave+RAGAS的示例程序
从理论上讲,现在我们可以调整一个超参数(如温度),重新运行评估,看看调整是否有积极或消极的影响了。但遗憾的是,在实践中,我发现大型语言评判者的评判很挑剔,而且我也不是唯一一个发现这一点的人(https://x.com/aparnadhinak/status/1748368364395721128)。
大型语言模型评估似乎不太擅长使用浮点数来评估这些指标。相反,它们似乎在分类方面做得更好些,例如回答“同意/不同意”这样的问题。当前,RAGAS尚不支持使用LLM评判者进行分类。直接手写有关代码似乎也并不难,也许在未来的更新中,我可能会自己尝试一下。
在线评估
离线评估有助于了解调整超参数如何影响性能,在我看来,在线评估要有用得多。在新的程序Rosebud中,我现在已经使用“同意/不同意”的方案——使用每个响应底部的两个相应按钮来提供反馈。
在线反馈示例
当用户点击上图中底部任一按钮时,就会被告知他们的反馈已被记录。以下给出在Streamlit应用程序界面中如何实现这一点的代码片段:
def start_log_feedback(feedback):
print("Logging feedback.")
st.session_state.feedback_given = True
st.session_state.sentiment = feedback
thread = threading.Thread(target=log_feedback, args=(st.session_state.sentiment,
st.session_state.query,
st.session_state.query_constructor,
st.session_state.context,
st.session_state.response))
thread.start()
def log_feedback(sentiment, query, query_constructor, context, response):
ct = datetime.datetime.now()
wandb.init(project="film-search",
name=f"query: {ct}")
table = wandb.Table(columns=["sentiment", "query", "query_constructor", "context", "response"])
table.add_data(sentiment,
query,
query_constructor,
context,
response
)
wandb.log({"Query Log": table})
wandb.finish()
请注意,向W&B发送反馈的过程是在单独的线程上运行的,而不是在主线程上运行。这是为了防止用户在等待日志记录完成时被卡住几秒钟。
我们使用了一个W&B表格用于存储反馈。表中记录了五个数值:
- 情绪(Sentiment):用户是否点击了拇指图标(同意/不同意)。
- 查询(Query):用户的查询,例如,查找长度不到2小时的英文戏剧电影和故事狗。
- Query_Constructor:查询构造函数的结果,它重写用户的查询,并在必要时包含元数据过滤,例如:
{
"query": "drama English dogs",
"filter": {
"operator": "and",
"arguments": [
{
"comparator": "eq", "attribute": "Genre", "value": "Drama"
},
{
"comparator": "eq", "attribute": "Language", "value": "English"
},
{
"comparator": "lt", "attribute": "Runtime (minutes)", "value": 120
}
]
},
}
- 上下文(Context):基于重建的查询检索到的上下文,例如标题“Title: Hachi: A Dog’s Tale. Overview: A drama based on the true story of a college professor’s…”。
- 回应(Response):模型的回应。
所有这些都可以方便地记录在与前面显示的Weave评估相同的项目中。现在,当查询“不同意”情况时,只需按下拇指向下的图标按钮即可查看到底发生了什么。这将有助于使推荐应用程序Rosebud的迭代和改进加快速度。
模型响应可观测性展示(请注意左侧的W&B和Weave之间的无缝过渡)
借助Prefect自动提取数据
为了使推荐程序Rosebud保持准确性,将数据提取和上传到Pinecone向量数据库的过程自动化非常重要。对于这个任务,我选择使用Prefect(https://www.prefect.io/)。Prefect是一个流行的工作流编排工具。我一直在寻找一些轻量级、易于学习和Python风格的程序。最后,我在Prefect中找到了这一切。
Prefect提供的用于提取和更新Pinecone向量存储的自动流程
Prefect支持提供多种方式来规划你的工作流程。我决定使用带有自动基础设施配置的推送工作池方式。我发现这种设置在简单性和可配置性之间取得了平衡。它允许用户委托Prefect自动配置在所选云提供商中运行流所需的所有基础设施。经过几番权衡后,我选择在Azure上部署,但是在GCP或AWS上部署的话只需要更改几行代码即可。有关更多详细信息,请参阅pinecone_flow.py文件。下面代码只是提供了一个简化的流程:
@task
def start():
"""
启动:检查一切工作或失败的速度快!
"""
#打印出一些调试信息
print("Starting flow!")
# 确保用户已经设置了适当的环境变量
assert os.environ['LANGCHAIN_API_KEY']
assert os.environ['OPENAI_API_KEY']
...
@task(retries=3, retry_delay_seconds=[1, 10, 100])
def pull_data_to_csv(config):
TMBD_API_KEY = os.getenv('TMBD_API_KEY')
YEARS = range(config["years"][0], config["years"][-1] + 1)
CSV_HEADER = ['Title', 'Runtime (minutes)', 'Language', 'Overview', ...]
for year in YEARS:
# 获取所有在{Year}中制作的电影的id列表
movie_list = list(set(get_id_list(TMBD_API_KEY, year)))
FILE_NAME = f'./data/{year}_movie_collection_data.csv'
#生成文件
with open(FILE_NAME, 'w') as f:
writer = csv.writer(f)
writer.writerow(CSV_HEADER)
...
print("Successfully pulled data from TMDB and created csv files in data/")
@task
def convert_csv_to_docs():
#从所有csv文件中加载数据
loader = DirectoryLoader(
...
show_progress=True)
docs = loader.load()
metadata_field_info = [
AttributeInfo(name="Title",
description="The title of the movie", type="string"),
AttributeInfo(name="Runtime (minutes)",
description="The runtime of the movie in minutes", type="integer"),
...
]
def convert_to_list(doc, field):
if field in doc.metadata and doc.metadata[field] is not None:
doc.metadata[field] = [item.strip()
for item in doc.metadata[field].split(',')]
...
fields_to_convert_list = ['Genre', 'Actors', 'Directors',
'Production Companies', 'Stream', 'Buy', 'Rent']
...
# 将'overview' 和'keywords' 设置为'page_content',其他字段设置为'metadata'
for doc in docs:
#将page_counte字符串解析为字典
page_content_dict = dict(line.split(": ", 1)
for line in doc.page_content.split("\n") if ": " in line)
doc.page_content = (
'Title: ' + page_content_dict.get('Title') +
'. Overview: ' + page_content_dict.get('Overview') +
...
)
...
print("Successfully took csv files and created docs")
return docs
@task
def upload_docs_to_pinecone(docs, config):
# 创建空索引
PINECONE_KEY, PINECONE_INDEX_NAME = os.getenv(
'PINECONE_API_KEY'), os.getenv('PINECONE_INDEX_NAME')
pc = Pinecone(api_key=PINECONE_KEY)
# 目标索引和检查状态
pc_index = pc.Index(PINECONE_INDEX_NAME)
print(pc_index.describe_index_stats())
embeddings = OpenAIEmbeddings(model=config['EMBEDDING_MODEL_NAME'])
namespace = "film_search_prod"
PineconeVectorStore.from_documents(
docs,
...
)
print("Successfully uploaded docs to Pinecone vector store")
@task
def publish_dataset_to_weave(docs):
#初始化Weave
weave.init('film-search')
rows = []
for doc in docs:
row = {
'Title': doc.metadata.get('Title'),
'Runtime (minutes)': doc.metadata.get('Runtime (minutes)'),
...
}
rows.append(row)
dataset = Dataset(name='Movie Collection', rows=rows)
weave.publish(dataset)
print("Successfully published dataset to Weave")
@flow(log_prints=True)
def pinecone_flow():
with open('./config.json') as f:
config = json.load(f)
start()
pull_data_to_csv(config)
docs = convert_csv_to_docs()
upload_docs_to_pinecone(docs, config)
publish_dataset_to_weave(docs)
if __name__ == "__main__":
pinecone_flow.deploy(
name="pinecone-flow-deployment",
work_pool_name="my-aci-pool",
cron="0 0 * * 0",
image=DeploymentImage(
name="prefect-flows:latest",
platform="linux/amd64",
)
)
请注意,将Python函数转换为Prefect流是非常简单的事情。你只需要在主函数上使用@task装饰器和@flow装饰器来设计一些子函数。还要注意,在将文档上传到Pinecone向量数据库后,我们流程的最后一步是将数据集发布到Weave。这对于再现性很重要。为了学习Prefect的基础知识,我建议你浏览一下他们官网上的教程(https://docs.prefect.io/latest/tutorial/)。
在上面脚本的最后,我们看到部署是如何在Prefect中完成的。
- 我们需要为部署提供一个名称,这个名称是自由决定的。
- 我们还需要指定一个work_pool_name。Prefect中的推送工作池会自动将任务发送到无服务器计算机,而不需要中介。此名称需要与用于创建池的名称相匹配,我们将在下面看到。
- 你还需要指定一个cron,它是计时器的缩写。这允许你指定重复工作流的频率。值“0 0**0”表示每周重复此工作流。
- 最后,你需要指定一个DeploymentImage。在这里,你可以指定名称和平台。名称是任意的,但平台不是。由于我想部署到Azure计算实例,而这些实例运行Linux操作系统,所以我在DeploymentImage中指定这一点很重要。
要使用命令行方式在Azure上部署此流,请运行以下命令:
prefect work-pool create --type azure-container-instance:push --provision-infra my-aci-pool
prefect deployment run 'get_repo_info/my-deployment'
这些命令将自动在Azure上提供所有必要的基础设施。这包括一个Azure容器注册表(ACR),它将保存一个Docker映像,其中包含目录中的所有文件以及requirements.txt中列出的任何必要的依赖库。它还将包括一个Azure容器实例(ACI)标识,该标识将具有部署具有上述Docker映像的容器所需的权限。最后,使用deployment run命令安排每周运行的代码。你可以通过Prefect控制面板来查看你的流是否运行:
Prefect中的流正在成功运行的情形
通过每周更新我的Pinecone向量库,我可以确保来自程序Rosebud的推荐结果准确。
总结
在本文中,我介绍了我的改进后的Rosebud应用程序的一些改进方案。这包括整合离线和在线评估的过程,以及自动更新我的Pinecone向量库等。
本文还有未提及的其他一些改进,包括:
- 在电影数据中包括电影数据库的评级。现在,你可以使用“好评电影(highly rated films)”,这种聊天模式能够过滤掉7/10以上的电影。
- 升级了聊天模式。现在查询和摘要模型使用的是模型gpt-4o-mini。请回想一下,LLM判断模型也是使用了模型gpt-4o-mini。
- 嵌入模型从text-Embedding-ada-002升级为text-embeading-3-small。
- 现在的年份跨越1950年至2023年,而不是从1920年开始。1920年至1950年的电影数据质量不高,只有糟糕的推荐。
- 用户界面更加清晰,所有关于项目的细节都放在侧边栏中。
- 程序在GitHub上的文档得到了进一步的改进。
- 修复了一些错误。
正如文章一开始提到的,该应用程序现在可以100%免费使用!在可预见的未来,我将为查询买单(因此选择gpt-4o-mini而不是更昂贵的gpt-4o)。我真的很想获得在生产环境中运行应用程序的经验,并让我的读者测试Rosebud,这是一个很好的方法。万一应用程序真的“火爆”了,我将不得不想出其他的融资模式。但这会是一个很大的问题。
下面,请尽情享受使用Rosebud程序搜索精彩电影的乐趣吧!
译者介绍
朱先忠,51CTO社区编辑,51CTO专家博客、讲师,潍坊一所高校计算机教师,自由编程界老兵一枚。
原文标题:Productionizing a RAG App with Prefect, Weave, and RAGAS,作者:Ed Izaguirre
链接:https://towardsdatascience.com/productionizing-a-rag-app-04c857e0966e。