构建 PubMed 数据集
构建关于心血管疾病研究的 PubMed 收录文献数据集的逐步说明
https://dianarozenshteyn.medium.com/?source=post_page---byline--b1267408417c--------------------------------https://towardsdatascience.com/?source=post_page---byline--b1267408417c-------------------------------- Diana Rozenshteyn
·发表于 Towards Data Science ·6 分钟阅读·2024 年 10 月 31 日
–
图片由作者提供
挑战
当我开始撰写我的硕士论文《与 NIH 资助的心脏病研究中具有影响力的科学出版物相关的因素》时,第一项任务是构建一个原始数据集来进行研究。为了实现这一目标,我转向了 PubMed,这是由美国国立医学图书馆(NLM)提供的一个免费的生物医学文献研究数据库。
该数据集需要满足多个特定标准,包括:
-
跨越尽可能长的时间段。
-
包含美国国立卫生研究院(NIH)资助的研究。
-
仅专注于心血管疾病研究的文献。
-
提供第一作者的详细信息,例如全名、性别、所在机构以及研究机构所在国家。
-
包含每篇文章的引用次数、NIH 百分位排名、文章中的参考文献总数以及其他与引用相关的数据。
-
包括期刊的科学排名信息。
在本文中,我将解释如何根据这些标准创建一个 PubMed 收录文献的数据集。
两个限制因素——第一作者的完整姓名的可用性和引用发生所需的年数——被用来选择数据收集的时间段。PubMed 记录从 2002 年开始包含完整的作者姓名(Full Author, FAU)[1]。此外,三年是进行引用和出版影响分析的最低推荐年数[2]。为了最大化数据集的大小,采用了至少两年的时间框架来积累引用,因为数据集是在 2022 年构建的。此外,2020 年是当时用于数据分析的科学期刊排名(SJR)信息可用的最后一年[3]。因此,我在 PubMed 上搜索了 2002 年到 2020 年的记录,共创建了 18 个数据集——每年一个。限制因素的概述如下图所示。
图片由作者提供
我使用 PubMed 的高级搜索工具[4]构建了关于心血管疾病的出版物数据集。PubMed 数据元素(字段)描述[1]被用来构建查询。NIH 资助通过国立心脏、肺、血液研究所(NHLBI)资助来表示。我在查询中使用了 NHLBI 资助([GR])、出版日期([DP])等关键词,以及基于心血管疾病相关病症的关键词组合。这些关键词包括 cardiovascular、ischemic 和 heart。
图片由作者提供
2020 年 PubMed 查询示例:“cardiovascular OR ischemic OR heart AND NHLBI[GR] AND 2020[DP]”。
图片由作者提供
为了获取期刊名称、文章第一作者所属机构及其国家信息以便进一步解析,我通过选择显示选项菜单中的摘要格式选项和保存引用到文件菜单中的 PubMed 格式选项,保存了 PubMed 高级搜索查询。
图片由作者提供
为了获取每个出版物的 PMID(PubMed 唯一标识符)列表,以便进一步获取引用信息,我通过选择显示选项菜单中的摘要格式选项和保存引用到文件菜单中的 PMID 格式选项,保存了通过高级搜索 PubMed 查询收集的数据。
图片由作者提供
以下流程图概述了从 PubMed 网站下载 PubMed 和 PMID 文件后的步骤。更详细的解释见后文。
图片由作者提供
为了获取引用相关信息和(如有)完整未缩写的作者名字,我将每年的 PMID 数据集上传到 ICite Web 工具[5]。我将结果数据分析保存为 csv 文件。ICite 由 NIH 的投资组合分析办公室(OPA)运行。OPA 是 NIH 的一个部门,负责基于数据的研究评估,帮助 NIH 决定哪些当前或新领域的研究将对科学和人类健康产生更大的益处。ICite 提供了有关作者的完整名字、总引用次数、每年引用次数、一个经过领域和时间调整的科学影响力的引用衡量标准——相对引用比率(RCR),以及 NIH 百分位数的可用信息。
图片来自作者
PubMed 格式数据集不能以 CSV 格式保存,因此必须解析以提取期刊标题(JT)、第一作者所属机构(AD)和国家。我为此编写了一个 Python 3.10.1 解析脚本。本文不讨论该脚本的详细内容,但我计划在未来的出版物中涉及。第一作者所属机构是通过向研究组织登记处(ROR)API [5]发出应用程序编程接口(API)请求来确定的。由于数据元素字段提供了研究机构不一致的名称以及如地址和部门名称等不必要的信息,因此需要进行 ROR 匹配。ROR 匹配可以帮助查找在 PubMed 格式数据集中提到的研究机构,这些机构随后通过 API 调用提供。API 调用的结果以 JSON 格式返回。我使用 PubMed 数据元素(字段)描述从 PubMed 格式数据集中解析期刊标题和国家。我分别处理了每年的数据集。以下是 PubMed 格式文件条目的示例。
图片来自作者
我在 JupyterLab 中处理了每年查询的解析过的 PubMed 格式数据集,并通过 ICite 引用数据集在 PMID 上合并它们。然后,我根据期刊名称将每年的合并数据集与 SJR 数据集进行合并。我从 SCImago Journal & Country Rank 网站[3]下载了最后可用年份(2020 年)的 SJR 数据集。SCImago Journal & Country Rank 数据库排名基于 SJR 指标。SJR 指标是通过 Scopus®数据库中的信息开发的,是衡量期刊科学影响力的一个标准。
图片来自作者
随后的步骤包括使用 Gender-API Web 服务估算第一作者的性别,并进行数据清理。这些步骤将在本刊物中不做详细讨论。
总结来说,自己构建数据集是有多方面益处的:
-
定制化:你可以根据特定的研究需求定制数据集。
-
数据理解:构建自己的数据集帮助你深入理解数据本身,包括其局限性和偏差。
-
技能发展:它加强了如数据收集、清理、组织等关键研究技能。
-
质量控制:通过自行构建数据集,你可以控制数据质量。
-
灵活性:你可以在出现新问题时修改或扩展数据集。
-
问题解决:这一过程促进了创造性问题解决,因为你需要开发收集、筛选和结构化数据以进行分析的方法。
这些好处提升了你的研究能力,并有助于产生更有影响力和更精确的结果。
本文使用的 Jupyter Notebook 可以在GitHub找到。
这里引用的完整硕士论文也可以在GitHub找到。
感谢阅读,
Diana
参考文献
-
美国国立医学图书馆,“MEDLINE/PubMed 数据元素(字段)描述,”nlm.nih.gov。可用:
www.nlm.nih.gov/bsd/mms/medlineelements.html#fau -
M. Thelwall,“1996–2014 年 27 个领域和 6 个英语国家的引用影响性别差异,”《定量科学研究》,第 1 卷,第 2 期,页码 599–617,2020 年 6 月。
-
SCImago,(无日期),“SJR — SCImago 期刊与国家排名[门户]”,2020 年,scimagojr.com。可用:
www.scimagojr.com -
美国国立医学图书馆,“高级搜索结果 — pubmed,”可用:
pubmed.ncbi.nlm.nih.gov/advanced/ -
研究组织注册,“ROR,”ror.org。可用:
ror.org/
使用 Amazon Bedrock 和 LangChain 构建 QA 研究聊天机器人
使用 Python 的概述和实现
https://medium.com/@aashishnair?source=post_page---byline--677fbd19e3c1--------------------------------https://towardsdatascience.com/?source=post_page---byline--677fbd19e3c1-------------------------------- Aashish Nair
·发布于 Towards Data Science ·9 分钟阅读·2024 年 3 月 16 日
–
目录
∘ 介绍
∘ 目标
∘ 聊天机器人架构
∘ 技术栈
∘ 过程
∘ 步骤 1 — 加载 PDF 文档
∘ 步骤 2 — 构建向量存储
∘ 步骤 3 — 加载 LLM
∘ 步骤 4 — 创建检索链
∘ 步骤 5 — 构建用户界面
∘ 步骤 6 — 运行聊天机器人应用程序
∘ 步骤 7 — 容器化应用程序
∘ 未来步骤
∘ 结论
∘ 参考文献
介绍
不久前,我尝试了构建一个完全在我的 CPU 上运行的简单自定义聊天机器人。
结果令人震惊,应用程序频繁崩溃。尽管如此,这并不令人惊讶。事实证明,将一个 13B 参数的模型放在一台 600 美元的电脑上,相当于让一个蹒跚学步的孩子爬山。
使用 LangChain 表达式语言(LCEL)构建 RAG 链
学习 LCEL 的构建模块,以开发越来越复杂的 RAG 链
https://medium.com/@RSK2327?source=post_page---byline--3688260cad05--------------------------------https://towardsdatascience.com/?source=post_page---byline--3688260cad05-------------------------------- Roshan Santhosh
·发布在 Towards Data Science ·7 分钟阅读·2024 年 4 月 11 日
–
在这篇文章中,我将介绍使用 LangChain 表达式语言(LCEL)实现自我评估 RAG 管道的问答功能。本文的重点是使用 LCEL 来构建管道,而不是实际的 RAG 和自我评估原理,这些原理已简化,以便于理解。
我将涵盖以下主题:
-
基本初始化步骤
-
使用 LCEL 开发不同复杂度的 RAG 管道变体
-
从 LCEL 脚本化管道中提取中间变量的方法
-
使用 LCEL 的原因
设置
在我们开始开发 RAG 链之前,需要执行一些基本的设置步骤以初始化此设置。这些步骤包括:
数据摄取
数据摄取包括两个关键步骤:
-
从 PDF 中读取文本
-
将 PDF 文本拆分成多个块以输入向量数据库
提示模板
我们将为问答任务和自我评估任务使用不同的提示。我们将有 3 个不同的提示模板:
-
qa_prompt : 问答任务的基本提示
-
qa_eval_prompt : 评估模型的提示,输入为问答对
-
qa_eval_prompt_with_context : 与上述提示类似,但额外包含上下文以进行评估
数据库初始化
我们使用 FAISS 和 Open AI 嵌入初始化一个简单的向量数据库。对于检索,我们将 k 设置为 3(返回给定查询的前 3 个块)
RAG 开发
简单的 QA RAG
我们从一个基本的 RAG 链示例开始,执行以下步骤:
-
根据用户的问题,从向量数据库中检索相关的文本块(PDF 文本的分割),并将它们合并为一个单一字符串
-
将检索到的上下文文本和问题一起传递给提示模板以生成提示
-
将生成的输入提示传递给 LLM,以生成最终答案
使用 LangChain 表达式语言(LCEL),该 RAG 的实现方式如下:
rag_chain = (
RunnableParallel(context = retriever | format_docs, question = RunnablePassthrough() ) |
qa_prompt |
llm
)
上述代码主要遵循管道架构,其中前一个元素的输出作为下一个元素的输入。下图展示了数据流。从用户的输入开始,首先通过 RunnableParallel 块,然后通过 qa_prompt 生成提示。这个提示随后被发送到 LLM,以生成最终输出。
基本的 LCEL 输入/输出流程
这个管道中有两个 LangChain 独有的关键添加项:
-
RunnableParallel:顾名思义,这个类提供了并行运行多个进程的功能。因此,RunnableParallel 的输出是一个字典,键是初始化时提供的参数。在这种情况下,输出将包含两个键:context和question。
那么,为什么我们在当前情况下需要这个呢?这是因为 qa_prompt 模板需要两个输入值:上下文和问题。因此,我们需要分别计算这些值,然后将它们一起传递给 qa_prompt 模板。
-
RunnablePassthrough:当你想将输入传递给下一个阶段而不做任何修改时,这是一个有用的类。本质上,这充当了一个恒等函数,返回传入的任何内容作为其输入。
上述 RAG 的流程图如下:
QA RAG 与自我评估 I
在之前的 RAG 链的基础上,我们现在将新的元素引入链中,以实现自我评估组件。
自我评估组件的实现相对直接。我们获取第一个 LLM 提供的答案,并将其与问题一起传递给评估器 LLM,并要求它提供一个二进制响应(正确/错误)。
rag_chain = (
RunnableParallel(context = retriever | format_docs, question = RunnablePassthrough() ) |
RunnableParallel(answer= qa_prompt | llm | retrieve_answer, question = itemgetter("question") ) |
qa_eval_prompt |
llm_selfeval |
json_parser
)
第一个关键区别是添加了一个额外的 RunnableParallel 组件。这是必需的,因为,与 QA 的初始提示类似,自我评估提示也需要两个输入:基础 LLM 的答案以及用户的问题。
因此,第一个 RunnableParallel 的输出是上下文文本和问题,而第二个 RunnableParallel 的输出是 LLM 的答案和问题。
注意: 对于第二个 RunnableParallel,我们使用 itemgetter 方法仅保留前一个输入中的问题值并将其向前传递。这是为了避免使用 RunnablePassthrough,因为它会传递完整的输入(带有两个键的字典),而我们现在只关心传递问题而不是上下文。此外,还有格式化的问题,因为 qa_eval_prompt 期望的是一个 str -> str 映射,而使用 RunnablePassthrough 会导致 str -> dict 映射。
这个 RAG 实现的流程图如下所示:
QA 自评 RAG II
对于这个变体,我们对评估过程进行了更改。除了问答对外,我们还将检索到的上下文传递给评估器 LLM。
为了实现这一点,我们在第二个 RunnableParallel 中添加了一个额外的 itemgetter 函数,以收集上下文字符串并将其传递给新的 qa_eval_prompt_with_context 提示模板。
rag_chain = (
RunnableParallel(context = retriever | format_docs, question = RunnablePassthrough() ) |
RunnableParallel(answer= qa_prompt | llm | retrieve_answer, question = itemgetter("question"), context = itemgetter("context") ) |
qa_eval_prompt_with_context |
llm_selfeval |
json_parser
)
实现流程图:
检索中间变量
使用像 LCEL 这样的链式实现时,一个常见的痛点是难以访问中间变量,而访问这些变量对于调试管道非常重要。我们查看了几个选项,通过操作 LCEL 来访问我们感兴趣的任何中间变量。
使用 RunnableParallel 传递中间输出
如前所述,RunnableParallel 允许我们将多个参数传递到链中的下一步。因此,我们利用 RunnableParallel 的这个能力,直到最后一步都将所需的中间值传递下去。
在下面的示例中,我们修改了原始的自评 RAG 链,以便输出检索到的上下文文本以及最终的自评输出。主要的变化是,我们在每个步骤中都添加了一个 RunnableParallel 对象,以将上下文变量传递下去。
此外,我们还使用了 itemgetter 函数来明确指定后续步骤的输入。例如,对于最后两个 RunnableParallel 对象,我们使用*itemgetter(‘input’)*来确保仅将前一步的输入参数传递给 LLM/Json 解析器对象。
rag_chain = (
RunnableParallel(context = retriever | format_docs, question = RunnablePassthrough() ) |
RunnableParallel(answer= qa_prompt | llm | retrieve_answer, question = itemgetter("question"), context = itemgetter("context") ) |
RunnableParallel(input = qa_eval_prompt, context = itemgetter("context")) |
RunnableParallel(input = itemgetter("input") | llm_selfeval , context = itemgetter("context") ) |
RunnableParallel(input = itemgetter("input") | json_parser, context = itemgetter("context") )
)
该链的输出如下所示:
更简洁的变体:
rag_chain = (
RunnableParallel(context = retriever | format_docs, question = RunnablePassthrough() ) |
RunnableParallel(answer= qa_prompt | llm | retrieve_answer, question = itemgetter("question"), context = itemgetter("context") ) |
RunnableParallel(input = qa_eval_prompt | llm_selfeval | json_parser, context = itemgetter("context"))
)
使用全局变量保存中间步骤
这种方法本质上使用了日志记录器的原理。我们引入了一个新函数,将其输入保存到全局变量中,从而允许我们通过全局变量访问中间变量。
global context
def save_context(x):
global context
context = x
return x
rag_chain = (
RunnableParallel(context = retriever | format_docs | save_context, question = RunnablePassthrough() ) |
RunnableParallel(answer= qa_prompt | llm | retrieve_answer, question = itemgetter("question") ) |
qa_eval_prompt |
llm_selfeval |
json_parser
)
在这里,我们定义了一个全局变量context和一个名为save_context的函数,该函数在返回相同的输入之前将其输入值保存到全局context变量中。在链中,我们将save_context函数添加为获取上下文步骤的最后一步。
此选项允许您在不进行重大更改的情况下访问任何中间步骤。
使用全局变量访问中间变量
使用回调
将回调附加到链上是另一种常用于记录中间变量值的方法。在 LangChain 中,回调有很多内容需要探讨,因此我将在另一篇文章中详细讨论。
为什么使用 LCEL?
使用 LCEL 的原因最好由 LangChain 的作者在其官方文档中解释。
在文档中提到的要点中,以下是我认为特别有用的一些:
鉴于以上原因,作为个人偏好,我认为使用 LCEL 有助于提高代码的可读性,并允许更清晰的实现。
资源
图像 :所有图像均由作者创建
除了 Medium,我还在 Linkedin上分享我的想法、创意和其他更新。
使用 MongoDB 构建 RAG 流水线:个性化推荐的向量搜索
https://medium.com/@pablomerchanrivera?source=post_page---byline--46a58a2aaac9--------------------------------https://towardsdatascience.com/?source=post_page---byline--46a58a2aaac9-------------------------------- Pablo Merchán-Rivera, Ph.D.
·发表于 Towards Data Science ·7 分钟阅读·2024 年 8 月 1 日
–
本文探讨了使用检索增强生成(RAG)流水线构建电影推荐系统的过程。目标是学习如何利用 MongoDB 的向量搜索功能,将数据描述转化为可搜索的数字指纹,并创建一个能够理解你偏好和沟通细节的系统。换句话说,我们的目标是构建一个不仅智能,而且高效的推荐系统。
在本文结束时,你将能够构建一个功能完整的电影推荐系统。该系统能够接受用户的查询,如 “我想看一部探讨人工智能的科幻电影” 或 “什么是适合成人也能欣赏的好动画电影?为什么你的建议适合?” 并返回相关的电影建议及选择理由。
图片由 Alexandr Popadin 提供,来源于 Unsplash
什么是 RAG 流水线?
RAG 管道指的是数据通过一系列处理步骤的顺序流动,结合了大型语言模型(LLM)与结构化数据检索的优势。它的工作原理是首先从知识库中检索相关信息,然后利用这些信息增强大型语言模型的输入,从而生成最终的输出。此类管道的主要目标是生成更准确、更具上下文相关性且更具个性化的响应,以回答用户针对庞大数据库提出的查询。
为什么选择 MongoDB?
MongoDB 是一个开源的 NoSQL 数据库,它以灵活的、类似 JSON 的文档形式存储数据,允许轻松扩展,并能处理多种数据类型和结构。MongoDB 在这个项目中扮演了重要角色。它的文档模型与我们的电影数据非常契合,而它的向量搜索功能可以对我们的嵌入(即电影内容的数字表示)进行相似度搜索。我们还可以利用索引和查询优化功能,以保持即使数据集扩展时也能快速检索数据。
我们的项目
我们的管道流程如下所示:
-
设置环境并从 Hugging Face 加载电影数据
-
使用 Pydantic 对数据进行建模
-
为电影信息生成嵌入
-
将数据导入 MongoDB 数据库
-
在 MongoDB Atlas 中创建向量搜索索引
-
执行向量搜索操作,找到相关电影
-
使用 LLM 模型处理用户查询
-
使用 RAG 管道获取电影推荐
第一步:设置环境并加载数据集
首先,我们需要导入必要的库并设置我们的环境。这还包括设置 API 密钥和应用程序用于连接 MongoDB 数据库的连接字符串:
import warnings
warnings.filterwarnings('ignore')
import os
from dotenv import load_dotenv, find_dotenv
from datasets import load_dataset
import pandas as pd
from typing import List, Optional
from pydantic import BaseModel
from datetime import datetime
from pymongo.mongo_client import MongoClient
import openai
import time
_ = load_dotenv(find_dotenv())
MONGO_URI = os.environ.get("MONGO_URI")
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY")
openai.api_key = OPENAI_API_KEY
接下来,我们加载我们的电影数据集:
dataset = load_dataset("Pablinho/movies-dataset", streaming=True, split="train")
dataset = dataset.take(200) # 200 movies for the sake of simplicity
dataset_df = pd.DataFrame(dataset)
数据集包含超过 9000 条记录。然而,在这个练习中,我们将数据集限制为 200 部电影,使用dataset.take(200)。在实际应用中,您可能会使用更大的数据集。
第二步:使用 Pydantic 对数据建模
数据建模对于确保我们应用程序的一致性和类型安全至关重要。因此,我们使用 Pydantic 来实现这一目的:
class Movie(BaseModel):
Release_Date: Optional[str]
Title: str
Overview: str
Popularity: float
Vote_Count: int
Vote_Average: float
Original_Language: str
Genre: List[str]
Poster_Url: str
text_embeddings: List[float]
使用 Pydantic 提供了多个好处,例如自动数据验证、类型检查和简便的序列化/反序列化。注意,我们还创建了一个text_embeddings字段,用于存储我们生成的嵌入,作为浮动点数列表。
第三步:嵌入生成
现在,我们可以使用 OpenAI API,并编写一个生成嵌入的函数,如下所示:
def get_embedding(text):
if not text or not isinstance(text, str):
return None
try:
embedding = openai.embeddings.create(
input=text,
model="text-embedding-3-small", dimensions=1536).data[0].embedding
return embedding
except Exception as e:
print(f"Error in get_embedding: {e}")
return None
在之前的代码行中,我们首先检查输入是否有效(非空字符串)。然后,我们使用 OpenAI 的 embeddings.create 方法生成嵌入,采用“text-embedding-3-small”模型,该模型生成 1536 维的嵌入。
现在,我们可以处理每条记录并使用之前的函数生成嵌入。我们还添加了一些代码来处理 'Genre' 字段,将其从字符串(如果存在)转换为一组类型列表。
def process_and_embed_record(record):
for key, value in record.items():
if pd.isnull(value):
record[key] = None
if record['Genre']:
record['Genre'] = record['Genre'].split(', ')
else:
record['Genre'] = []
text_to_embed = f"{record['Title']} {record['Overview']}"
embedding = get_embedding(text_to_embed)
record['text_embeddings'] = embedding
return record
records = [process_and_embed_record(record) for record in dataset_df.to_dict(orient='records')]
这些嵌入将使我们能够进行语义搜索,找到与给定查询在概念上相似的电影。请注意,这一过程可能需要一些时间,尤其是在数据集较大的情况下,因为我们为每部电影都要进行一次 API 调用。
第 4 步:将数据导入 MongoDB
我们建立与 MongoDB 数据库的连接:
def get_mongo_client(mongo_uri):
client = MongoClient(mongo_uri, appname="pmr.movie.python")
print("Connection to MongoDB successful")
return client
mongo_client = get_mongo_client(MONGO_URI)
database_name = "movies_dataset"
collection_name = "movies"
db = mongo_client.get_database(database_name)
collection = db.get_collection(collection_name)
collection.delete_many({})
我们将处理并嵌入的数据插入 MongoDB,这使得我们能够高效地存储和查询我们的电影数据,包括高维嵌入:
movies = [Movie(**record).dict() for record in records]
collection.insert_many(movies)
第 5 步:在 MongoDB Atlas 中创建向量搜索索引
在执行向量搜索操作之前,我们需要创建一个向量搜索索引。此步骤可以直接在 MongoDB Atlas 平台上完成:
-
登录到您的 MongoDB Atlas 帐户
-
导航到您的集群
-
转到“搜索与向量搜索”标签
-
点击“创建搜索索引”
-
在“Atlas 向量搜索”部分选择“JSON 编辑器”,并使用以下配置:
{
"fields": [
{
"numDimensions": 1536,
"path": "text_embeddings",
"similarity": "cosine",
"type": "vector"
}
]
}
目标是创建一个名为 "vector_index_text" 的向量搜索索引,索引的字段为 "text_embeddings"。我们使用余弦相似度,因为它有助于通过比较嵌入向量的方向来找到主题或内容相似的电影,忽略长度或细节的差异,这对于将用户的查询与电影描述进行匹配非常有效。
第 6 步:实现向量搜索
现在,我们实现向量搜索功能。以下函数用于在我们的 MongoDB 集合中执行向量搜索。它首先为用户的查询生成嵌入。然后,利用 $vectorSearch 运算符构建 MongoDB 聚合管道。搜索会在 150 个候选项中查找 20 个最近的邻居。
def vector_search(user_query, db, collection, vector_index="vector_index_text", max_retries=3):
query_embedding = get_embedding(user_query)
if query_embedding is None:
return "Invalid query or embedding generation failed."
vector_search_stage = {
"$vectorSearch": {
"index": vector_index,
"queryVector": query_embedding,
"path": "text_embeddings",
"numCandidates": 150,
"limit": 20
}
}
pipeline = [vector_search_stage]
for attempt in range(max_retries):
try:
results = list(collection.aggregate(pipeline))
if results:
explain_query_execution = db.command(
'explain', {
'aggregate': collection.name,
'pipeline': pipeline,
'cursor': {}
},
verbosity='executionStats')
vector_search_explain = explain_query_execution['stages'][0]['$vectorSearch']
millis_elapsed = vector_search_explain['explain']['collectStats']['millisElapsed']
print(f"Total time for the execution to complete on the database server: {millis_elapsed} milliseconds")
return results
else:
print(f"No results found on attempt {attempt + 1}. Retrying...")
time.sleep(2)
except Exception as e:
print(f"Error on attempt {attempt + 1}: {str(e)}")
time.sleep(2)
return "Failed to retrieve results after multiple attempts."
我们实现了一个重试机制(最多 3 次尝试),以处理可能的临时问题。该函数还执行 explain 命令,提供有关查询执行的详细信息。
第 7 步:使用 LLM 处理用户查询
最后,我们可以处理用户查询。首先,我们定义一个 SearchResultItem 类来构建搜索结果。然后,handle_user_query 函数将所有内容结合在一起:它根据用户的查询执行向量搜索,将搜索结果格式化为 pandas DataFrame,并使用 OpenAI 的 GPT 模型(即 gpt-3.5-turbo)根据搜索结果和用户的查询生成回应,并显示结果及生成的回应:
class SearchResultItem(BaseModel):
Title: str
Overview: str
Genre: List[str]
Vote_Average: float
Popularity: float
def handle_user_query(query, db, collection):
get_knowledge = vector_search(query, db, collection)
if isinstance(get_knowledge, str):
return get_knowledge, "No source information available."
search_results_models = [SearchResultItem(**result) for result in get_knowledge]
search_results_df = pd.DataFrame([item.dict() for item in search_results_models])
completion = openai.chat.completions.create(
model="gpt-3.5-turbo",
messages=[
{"role": "system", "content": "You are a movie recommendation system."},
{"role": "user", "content": f"Answer this user query: {query} with the following context:\n{search_results_df}"}
]
)
system_response = completion.choices[0].message.content
print(f"- User Question:\n{query}\n")
print(f"- System Response:\n{system_response}\n")
return system_response
这个功能实际上展示了 RAG 的核心价值:通过从我们的数据库中检索相关信息,生成一个情境适当的回应。
8. 使用 RAG 管道
要使用此 RAG 管道,现在可以进行如下查询:
query = """
I'm in the mood for a highly-rated action movie. Can you recommend something popular?
Include a reason for your recommendation.
"""
handle_user_query(query, db, collection)
系统将返回类似以下的响应:
I recommend "Spider-Man: No Way Home" as a popular and highly-rated action
movie for you to watch. With a vote average of 8.3 and a popularity score
of 5083.954, this film has garnered a lot of attention and positive
reviews from audiences.
"Spider-Man: No Way Home" is a thrilling action-packed movie that brings
together multiple iterations of Spider-Man in an epic crossover event. It
offers a blend of intense action sequences, emotional depth, and nostalgic
moments that fans of the superhero genre will surely enjoy. So, if you're
in the mood for an exciting action movie with a compelling storyline and
fantastic visual effects, "Spider-Man: No Way Home" is an excellent choice
for your movie night.
结论
构建一个 RAG 管道涉及多个步骤,从数据加载和建模到嵌入生成和向量搜索。这个示例展示了如何通过将我们数据库中的特定电影数据与语言模型的自然语言理解和生成能力结合,来提供信息丰富、上下文感知的回答。在此基础上,我们使用 MongoDB,因为它具有原生向量搜索功能、灵活的文档模型和可扩展性,非常适合这种工作流程。
你可以通过添加更多数据、微调你的嵌入,或实现更复杂的推荐算法来扩展这个系统。
有关完整代码和更多资源,请查看 GitHub 仓库。此项目使用的数据集来源于 Kaggle ,并已获得原作者授予的 CC0 1.0 通用公共领域授权(CC0 1.0)。你可以在 这里 找到数据集和更多信息。
手动在 Python 中构建随机森林
对一种强大且流行的算法的深入探讨
https://mgsosna.medium.com/?source=post_page---byline--187ac0620875--------------------------------https://towardsdatascience.com/?source=post_page---byline--187ac0620875-------------------------------- Matt Sosna
·发布于 Towards Data Science ·17 分钟阅读·2024 年 1 月 30 日
–
从 药物发现 到 物种分类,从 信用评分 到 网络安全 等,随机森林是一个流行且强大的算法,用于建模我们复杂的世界。它的多功能性和预测能力似乎需要最前沿的复杂技术,但如果我们深入了解随机森林的实际构成,我们会发现它实际上只是一个出乎意料简单的重复步骤集。
我发现,学习某个东西最好的方式就是动手实践。因此,为了直观理解随机森林的工作原理,我们将手动在 Python 中构建一个,从决策树开始,逐步扩展到完整的森林。我们将亲自体验这个算法在分类和回归中的灵活性和可解释性。虽然这个项目听起来可能很复杂,但我们实际上只需要学习几个核心概念:1)如何迭代地划分数据,以及 2)如何量化数据划分的效果。
背景
决策树推理
决策树是一种监督学习算法,它识别一组将特征映射到标签的二元规则分支。与像逻辑回归这样的算法不同,后者的输出是一个方程,决策树算法是非参数的…
构建可靠的文本分类管道:使用 LLMs 的分步指南
克服 LLM 文本分类中的常见挑战
https://medium.com/@CVxTz?source=post_page---byline--87dc73213605--------------------------------https://towardsdatascience.com/?source=post_page---byline--87dc73213605-------------------------------- Youness Mansar
·发表于Towards Data Science ·阅读时长 11 分钟·2024 年 11 月 12 日
–
注意:编辑于 2024 年 11 月 15 日,修复了代码中的一个 bug。现在结果更好!
在本教程中,我们将一步一步地讲解如何使用大语言模型(LLMs)构建一个准确且可靠的文本分类管道。LLMs 是强大的通用模型,在各种自然语言处理任务中展现出了卓越的能力,且它们正逐渐取代许多 AI 应用中的专业模型。然而,如果使用不当,LLMs 在分类任务中可能会遇到挑战。
在应用大语言模型(LLMs)进行分类时,常见的问题是模型可能没有按照预期的输出或格式进行响应,从而导致需要额外的后处理,这些后处理可能复杂且耗时。在本文中,我们将介绍一些实用的技巧和方法来解决这些问题。这些策略都很简单易行,但能够显著提高 LLMs 作为文本分类器的准确性和可用性。让我们一起来深入了解如何让你的 LLM 文本分类系统既高效又可靠。
主要内容
构建一个能够写入 Google Docs 的研究代理(第一部分)
Dalle-3 对“一个古怪的 AI 助手在辛勤工作中检查文档”的诠释。图片由作者生成。
可能帮助你完成作业的工具
https://medium.com/@rmartinshort?source=post_page---byline--4b49ea05a292--------------------------------https://towardsdatascience.com/?source=post_page---byline--4b49ea05a292-------------------------------- Robert Martin-Short
·发表于面向数据科学 ·阅读时间 15 分钟·2024 年 11 月 20 日
–
***本文是两部分系列的第一篇,我们将使用 LangGraph 和 Tavily 构建一个简单的研究代理,能够编写并优化简短的文章。为了跟踪代理生成的计划、文章和评论,我们添加了通过编程方式创建和编辑 Google Docs 的功能。在本文中,我们将重点介绍代理部分,将 Google Docs 连接的部分留到第二篇文章中。你可以在这里找到所有相关代码。
大型语言模型(LLMs)正在快速应用于与分析师和研究人员相关的各种场景,特别是在文本信息的提取、组织和总结方面。无论是商业界还是开源社区,都在不断简化构建和扩展所谓“代理型”应用程序的过程,在这些应用程序中,LLM 充当(希望是)熟练的分析师角色,并做出半自主决策。例如,在一个聊天机器人应用程序中,如果用户提出一个复杂或多步骤的问题,LLM 可能需要设计一个行动计划,正确查询多个外部工具——可能是计算器、网页搜索工具、向量数据库等——汇总结果并生成答案。
这种系统通常被认为使用了ReAct 框架的提示工程方法,其中“ReAct”代表“Reasoning-Action”(推理-行动)。基本上,提示的结构和顺序迫使 LLM 以非常有条理的方式回答问题,首先通过表达一个思想(通常是攻击计划),然后执行一个行动,再观察结果。在代理系统中,这个过程可以不断迭代,直到 LLM 认为已经得出了一个可接受的答案。
在这一系列文章中,我们将使用LangGraph库和Tavily搜索工具,构建一个简单的研究助手,展示一些这些概念,并且可能对我们那些希望快速生成关于任何主题的简洁、写得好的报告的人有帮助。我们的代理将受到同行评审研究中的计划 -> 研究 -> 写作 -> 提交 -> 审阅 -> 修订周期的启发,你可以在这里查看这些不同部分的提示。
为了让系统感觉更完整,我们还将添加将生成的材料自动添加到 Google 文档的功能,详细内容请参见第二部分。这应该被视为一个附加功能,而不是代理的集成组件,但它本身也很有趣,因此也可以作为一篇独立的文章阅读。
1. 我们的研究助手应该做些什么?
在看看我们如何构建这个助手以及它的“代理性”意味着什么之前,我们应该简要思考一下我们希望它做些什么。目标是构建一个可以计划并撰写关于给定主题的简短、信息丰富的文章,然后通过审阅和修订来改进自己的工作的系统。
为什么?主要这只是一次技术探索,但将 LLM 用作半自主研究员是一个活跃的研究领域,并且产生了一些有趣的项目,如GPT-researcher。它们有潜力加速分析师、学生、作家和研究人员的工作——尽管当然,如果目标是人类学习,仔细阅读、做笔记和讨论是不可替代的,AI 无法取而代之。
像 GPT4、Anthropic Claude Sonnet、Meta Llama 3、Google Gemini Pro 等大型语言模型(LLM)已经能够通过一个简单的提示写出很棒的文章。然而,这些 LLM 存在知识截止问题,因此需要访问额外的工具来获取最新信息,比如关于时事新闻的内容。有许多服务——特别是像 Perplexity、ChatGPT(现在可以通过 chat.com 访问)和 Google 的 AI 概览这些工具,它们已经具备了这种能力,但它们更倾向于提供快速总结,而不是精心编写的研究报告。
在这里,我们假设多次的审阅和修改将提升 LLM 生成文章的质量。这当然是人类写作中的工作方式。我们的助手将有以下几个组件,每个组件都有自己的指令提示。
-
规划者。 将一个定义不清的任务转化为结构化的文章计划
-
研究员。 接受计划并在互联网上搜索相关内容。
-
写作者。 利用计划、检索到的内容和自身知识来撰写报告
-
审阅者。 阅读报告并提供建设性的批评
-
编辑。 阅读报告和审阅者的批评,决定报告是否需要修改。如果需要,报告将被送回研究员和写作者阶段。
在我们的实现中,这些组件将调用相同的 LLM,即 GPT4o-mini,但在实际应用中,它们完全可以使用不同的、更专业的模型。
输出将是一份写得很好的、信息丰富的报告——最好附带参考文献——我们可以程序化地将其放入 Google 文档中保存。通过调整提示,可以轻松修改我们研究员的“个性”。编辑特别重要,因为它是整个过程的把关人。如果我们让编辑非常严格,系统可能需要经过多次修改才能被接受。严格的编辑在多大程度上能提高结果质量?这是一个非常有趣的问题,正如他们所说,这超出了当前工作的范围!
2. 代理的结构
我们的研究助手在很大程度上基于这门关于 LangGraph 的优秀短期课程。LangGraph 是一个 LLM 编排库,旨在让我们更容易设计和构建可靠的代理。关于 LangGraph 与 LangChain 的深入对比,我推荐这篇优秀的文章。
代理究竟是什么?似乎社区尚未达成统一定义,但至少广义上来说,我们可以说代理是一个多步骤的系统,在这个系统中,LLM 被允许对结果做出有意义的决策。这使得它比链条更复杂(并且可能更不可预测),链条只是预先定义的一组 LLM 调用按顺序执行。
在代理框架中,LLM 对于如何解决其所给定的问题有一定的自主性,可能通过选择合适的工具来调用,或者决定在解决方案足够好时何时停止改进。从这个意义上讲,LLM 更像是系统的大脑,更像一个人类分析师,而不仅仅是一个 API 调用。这里的一个有趣挑战是,尽管代理可以自由做出决策,但它们通常嵌入在或与传统的软件系统交互,这些系统需要结构化的输入和输出。因此,迫使代理以这些其他系统能够理解的方式返回答案非常重要,无论它做出了什么决策。
对于在 LangGraph 上下文中讨论代理的更深入内容,这份文档非常有帮助。我们的研究代理将是一个相当简单的代理(部分原因是我也在学习这些材料!),但希望它能成为通向更复杂系统的一块垫脚石。
在 LangGraph 中,我们将系统的逻辑定义为一个图,其中包含节点和边。节点是进行 LLM 调用的地方,边则将信息从一个节点传递到下一个节点。边可以是有条件的,意味着它们可以根据做出的决策将信息指向不同的节点。信息以由状态定义的结构化格式在节点之间传递。
我们的研究助手只有一个阶段,叫做AgentState,看起来像这样:
class AgentState(TypedDict):
"""
A dictionary representing the state of the research agent.
Attributes:
task (str): The description of the task to be performed.
plan (str): The research plan generated for the task.
draft (str): The current draft of the research report.
critique (str): The critique received for the draft.
content (List[str]): A list of content gathered during research.
revision_number (int): The current revision number of the draft.
max_revisions (int): The maximum number of revisions allowed.
finalized_state (bool): Indicates whether the report is finalized.
"""
task: str
plan: str
draft: str
critique: str
content: List[str]
editor_comment: str
revision_number: int
max_revisions: int
finalized_state: bool
这是存储与我们问题相关的所有信息的地方,并且可以通过 LLM 在图的某个节点内部进行更新。
现在我们可以定义一些节点。在代码中,所有节点都保存在AgentNodes类中,这只是我发现的一个有用的方式来对它们进行分组。例如,规划节点看起来像这样:
def plan_node(self, state: AgentState) -> Dict[str, str]:
"""
Generate a research plan based on the current state.
Args:
state (AgentState): The current state of the research agent.
Returns:
Dict[str, str]: A dictionary containing the generated research plan.
"""
messages = [
SystemMessage(content=ResearchPlanPrompt.system_template),
HumanMessage(content=state["task"]),
]
response = self.model.invoke(messages)
return {"plan": response.content}
注意它如何接收一个AgentState并返回对其某个组件的修改,即研究计划的文本。当这个节点被运行时,计划会被更新。
节点函数中的代码使用标准的 LangChain 语法。self.model是ChatOpenAI的一个实例,像这样:
model = ChatOpenAI(
model="gpt-4o-mini", temperature=0, api_key=secrets["OPENAI_API_KEY"]
)
这个提示由来自ResearchPlanPrompt数据类的系统消息与 AgentState 的“task”元素拼接而成,后者是用户提供的研究课题。计划提示看起来像这样。
@dataclass
class ResearchPlanPrompt:
system_template: str = """
You are an expert writer tasked with creating a high-level outline for a research report.
Write such an outline for the user-provided topic. Include relevant notes or instructions for each section.
The style of the research report should be geared towards the educated public. It should be detailed enough to provide
a good level of understanding of the topic, but not unnecessarily dense. Think of it more like a whitepaper to be consumed
by a business leader rather than an academic journal article.
"""
需要为以下任务创建类似的节点:
-
进行研究。在这里,我们使用 LLM 将研究任务转换为一系列查询,然后使用 Tavily 搜索工具在线查找答案并将其保存在 AgentStage 的“content”下。此过程将在第二部分中详细讨论。
-
撰写报告。在这里,我们利用任务名称、研究计划、研究内容以及任何先前的审稿人评论来实际撰写研究报告。这些内容会保存在 AgentState 的“draft”下。每次运行时,
revision_number指示器都会更新。 -
审查报告。 调用 LLM 来批评研究报告,并将审查保存到“critique”下。
-
根据反馈进行更多的研究。这将处理原始草稿和审查意见,并为 Tavily 生成更多的查询,帮助系统解决审稿人评论。再一次,这些信息会保存在“content”下。
-
做出决策,判断报告是否满足审稿人的评论。LLM 会根据编辑提示的指导做出是/否决策,并解释其推理过程。
-
虚拟节点,用于拒绝或接受研究。一旦我们到达这两个节点中的任何一个,我们就可以结束流程。最终的研究报告可以从 AgentState 中提取。
我们需要在图中的编辑节点处创建一个条件边:如果编辑器选择是,我们进入已接受节点。如果选择否,我们返回审查节点。
为了定义此逻辑,我们需要创建一个函数在条件边内运行。我选择将其放入一个 AgentEdges 类中,但这不是必须的。
def should_continue(state: AgentState) -> str:
"""
Determine whether the research process should continue based on the current state.
Args:
state (AgentState): The current state of the research agent.
Returns:
str: The next state to transition to ("to_review", "accepted", or "rejected").
"""
# always send to review if editor hasn't made comments yet
current_editor_comments = state.get("editor_comment", [])
if not current_editor_comments:
return "to_review"
final_state = state.get("finalized_state", False)
if final_state:
return "accepted"
elif state["revision_number"] > state["max_revisions"]:
logger.info("Revision number > max allowed revisions")
return "rejected"
else:
return "to_review"
在代码中,整个图的设置如下所示
from research_assist.researcher.AgentComponents import (
AgentNodes,
AgentState,
AgentEdges,
)
# this is the predefined end node
from langgraph.graph import END
agent = StateGraph(AgentState)
nodes = AgentNodes(model, searcher)
edges = AgentEdges()
## Nodes
agent.add_node("initial_plan", nodes.plan_node)
agent.add_node("write", nodes.generation_node)
agent.add_node("review", nodes.review_node)
agent.add_node("do_research", nodes.research_plan_node)
agent.add_node("research_revise", nodes.research_critique_node)
agent.add_node("reject", nodes.reject_node)
agent.add_node("accept", nodes.accept_node)
agent.add_node("editor", nodes.editor_node)
## Edges
agent.set_entry_point("initial_plan")
agent.add_edge("initial_plan", "do_research")
agent.add_edge("do_research", "write")
agent.add_edge("write", "editor")
## Conditional edges
agent.add_conditional_edges(
"editor",
edges.should_continue,
{"accepted": "accept", "to_review": "review", "rejected": "reject"},
)
agent.add_edge("review", "research_revise")
agent.add_edge("research_revise", "write")
agent.add_edge("reject", END)
agent.add_edge("accept", END)
在数据可以流经图之前,图必须被编译。从文档中的理解来看,它只是对图的结构做一些简单检查,并返回一个CompiledGraph对象,该对象具有像stream和invoke这样的函数。这些方法允许你将输入传递给起始节点,起始节点通过上面的代码中的set_entry_point来定义。
在构建这些图时,将所有节点和边可视化在笔记本中非常有帮助。这可以通过以下命令实现。
from IPython.display import Image
Image(agent.compile().get_graph().draw_png())
LangGraph 提供了几种不同的绘制图形的方式,具体取决于你安装的可视化包。我使用的是 pygraphviz,可以通过以下命令在 M 系列 Mac 上安装。
brew install graphviz
pip install -U --no-cache-dir \
--config-settings="--global-option=build_ext" \
--config-settings="--global-option=-I$(brew --prefix graphviz)/include/" \
--config-settings="--global-option=-L$(brew --prefix graphviz)/lib/" \
pygraphviz
我们代理的控制流可视化。节点是 LLM 调用发生的地方,而边表示信息流动。图像由作者生成。
我们如何测试代理?最简单的方法是使用一些 AgentState 组件的初始值(例如任务、最大修订次数和修订号)调用invoke,这些值将进入图的入口节点。
graph = agent.compile()
res = graph.invoke(
{
"task": "What are the key trends in LLM research and application that you see in 2024",
"max_revisions": 1,
"revision_number": 0,
}
)
经过一段时间(如果max_revisions设置为较大值,可能需要几分钟),这将返回一个包含所有组件的代理状态字典。我正在使用 gpt4o-mini 进行此操作,结果非常令人印象深刻,尽管关于“审查”和“编辑器”组件是否能真正提高文章质量的问题仍有争议,我们将在第三节中讨论这个问题。
如果我们希望更深入了解图中每个阶段节点的输入和输出怎么办?这是调试和解释性非常重要的,特别是在图形变得越来越复杂或我们希望将其部署到生产环境时。幸运的是,LangGraph 提供了一些很棒的工具,这些工具在其文档的持久性和流式传输部分中有介绍。一个最小的实现可能类似于下面的样子,在这里我们使用内存存储来跟踪图的每个阶段的更新。
from langgraph.store.memory import InMemoryStore
from langgraph.checkpoint.memory import MemorySaver
import uuid
checkpointer = MemorySaver()
in_memory_store = InMemoryStore()
graph = agent.compile(checkpointer=checkpointer, store=self.in_memory_store)
# Invoke the graph
user_id = "1"
config = {"configurable": {"thread_id": "1", "user_id": user_id}}
namespace = (user_id, "memories")
for i, update in enumerate(graph.stream(
{
"task": task_description,
"max_revisions": max_revisions,
"revision_number": 0,
}, config, stream_mode="updates"
)):
# print the data that just got generated
print(update)
memory_id = str(uuid.uuid4())
# store the data that just got generated in memory
self.in_memory_store.put(namespace, memory_id, {"memory": update})
results.append(update)
更复杂的应用程序将从节点内部访问存储,允许聊天机器人回忆与某个用户的先前对话。例如,在这里我们只是使用内存来保存每个节点的输出,这些输出可以用于调试目的查看。我们将在最后一节中进一步探讨这个问题。
3. “do_research”节点中有什么?Tavily 搜索的强大功能
也许上述控制流中最有趣的部分是do_research和research_revise节点。在这两个节点中,我们使用 LLM 生成与任务相关的一些网页搜索查询,然后我们使用Tavily API 实际进行搜索。Tavily 是一个相对较新的服务,提供针对 AI 代理优化的搜索引擎。实际上,这意味着该服务返回来自网站的相关文本块,而不是像典型的搜索引擎 API 那样仅返回网址列表(这些网址需要被抓取和解析)。
在背后,Tavily 很可能使用网页抓取工具和 LLM 来提取与用户搜索相关的内容,但所有这些都被抽象化了。你可以在这里注册 Tavily 的免费“研究员”计划,获得 1000 次免费的 API 调用。不幸的是,超过此次数后,你需要支付月费才能继续使用,可能只有在商业用例中才值得这样做。
让我们来看一个使用与AgentNodes.research_plan_node中非常相似的代码的例子。
from langchain_core.messages import (
SystemMessage,
HumanMessage,
)
from research_assist.researcher.prompts import (
ResearchPlanPrompt,
)
from langchain_openai import ChatOpenAI
from tavily import TavilyClient
class Queries(BaseModel):
"""
A model representing a list of search queries.
Attributes:
queries (List[str]): A list of search queries to be executed.
"""
queries: List[str]
# set up task
task = """
What are the key trends in LLM reseach and application that you see in 2024
"""
# set up LLM and Tavily
model = ChatOpenAI(
model="gpt-4o-mini", temperature=0, api_key=secrets["OPENAI_API_KEY"]
)
tavily = TavilyClient(api_key=secrets["TAVILY_API_KEY"])
# generate some queries relevant to the task
queries = agent.nodes.model.with_structured_output(Queries).invoke(
[
SystemMessage(content=ResearchPlanPrompt.system_template),
HumanMessage(content=task),
]
)
这会生成 5 个与我们定义的任务相关的搜索查询,结果如下所示:
['key trends in LLM research 2024',
'LLM applications 2024',
'latest developments in LLM technology 2024',
'future of LLMs 2024',
'LLM research advancements 2024']
接下来,我们可以对这些查询中的每一个调用 Tavily 搜索。
response = tavily.search(query=queries[0], max_results=2)
这将提供一个格式良好的结果,包含网址、标题和文本块。
来自 Tavily 搜索的示例结果。图片由作者生成。
这是一个非常强大且易于使用的搜索工具,可以让 LLM 应用程序访问网络,而无需额外的工作!
在我们的研究员代理中,我们目前只使用内容字段,并将其提取并附加到一个列表中,该列表被传递到 AgentState 中。然后,这些信息会被注入到用于写作节点的提示中,从而允许 LLM 在生成报告时访问这些信息。
Tavily 搜索可以做的事情还很多,但要注意,实验使用它会迅速消耗你的免费 API 调用。事实上,对于我们的报告写作任务,有很多应用场景 Tavily 调用可能不是必须的(即 LLM 已经有足够的知识来写报告),所以我建议添加一个额外的条件边,使系统在判断不需要进行网络搜索时跳过 do_research 和 research_revise 节点。我可能很快会在仓库中更新这个修改。
4. 演示一个例子
为了巩固我们刚刚学到的内容,让我们通过一个实际的例子来演示研究人员的工作,使用与上面相同的任务。
首先,我们导入库并设置我们的 LLM 和搜索模型
from research_assist.researcher.Agent import ResearchAgent, load_secrets
from langchain_openai import ChatOpenAI
from tavily import TavilyClient
secrets = load_secrets()
model = ChatOpenAI(
model="gpt-4o-mini", temperature=0, api_key=secrets["OPENAI_API_KEY"]
)
tavily = TavilyClient(api_key=secrets["TAVILY_API_KEY"])
agent = ResearchAgent(model, tavily)
现在我们可以在任务上运行代理,并给它一个最大的修订次数。
task = """
What are the key trends in LLM reseach and application that you see in 2024
"""
result = agent.run_task(task_description=task,max_revisions=3)
现在代理将运行它的任务,这可能需要大约一分钟。已经添加了日志记录以显示它正在做什么,重要的是,结果正在保存到 in_memory_store 中,我们在第二部分末尾看到了它。
最终报告有几种方式可以访问。它存储在结果列表中,可以像这样在笔记本中可视化。
Markdown(result[-3]['write']['draft'])
它也存储在代理的记忆中,和所有其他输出一起。我们可以按照以下方式访问它。
agent.in_memory_store.search(("1", "memories"))[-3].dict()
报告本身大约有 1300 字——有点多,无法在这里复制——但我已经将其粘贴到了仓库的这里。我们也可以看看编辑器在经过一轮修订后的看法。
editor_comments = agent.in_memory_store.search(("1", "memories"))[-2].dict()
{'value': {'memory': {'editor': {'editor_comment':
'The report has addressed the critiques by enhancing depth in key sections,
adding clarity, and improving structure with subheadings.
It provides specific examples and discusses ethical considerations,
making it a valuable resource. The revisions are sufficient for publication.',
'finalized_state': True}}},
'key': '9005ad06-c8eb-4c6f-bb94-e77f2bc867bc',
'namespace': ['1', 'memories'],
'created_at': '2024-11-11T06:09:46.170263+00:00',
'updated_at': '2024-11-11T06:09:46.170267+00:00'}
看起来编辑器很满意!
为了调试,我们可能需要阅读其他所有输出。不过,在笔记本中做这件事可能会很痛苦,所以在下一篇文章中,我们将讨论如何将这些输出程序化地插入到 Google Docs 中。感谢你坚持看到最后,我们将在第二部分继续!
作者与本文讨论的任何工具都没有任何关联。
构建一个能够写入 Google Docs 的研究助手(第二部分)
Dalle-3 对“一个 AI 助手将文件随风抛向清澈蓝海”的解读。图像由作者生成。
可能对你做作业有所帮助的工具
https://medium.com/@rmartinshort?source=post_page---byline--ac9dcacff4ff--------------------------------https://towardsdatascience.com/?source=post_page---byline--ac9dcacff4ff-------------------------------- Robert Martin-Short
·发表于Towards Data Science ·12 分钟阅读·2024 年 11 月 20 日
–
本文是两部分系列中的第二部分,我们使用 LangGraph 和 Tavily 构建了一个简单的研究助手,该助手能够撰写和修改短篇文章。为了跟踪它生成的计划、文章和评论,我们为其增加了编程创建和编辑 Google Docs 文档的功能。在第一篇文章中,我们构建了该助手。现在,我们将构建文档连接功能。你可以在这里找到所有相关代码。
在本系列的第一部分中,我们讨论了代理,并使用 LangGraph 和 Tavily 的工具构建了一个最小化的代理,能够进行研究、写作、审阅和修订短篇文章。这对于展示很有用,但如果我们实际上希望在笔记本外阅读这些文章怎么办?或者,更雄心勃勃的目标是,如何将这个代理做成一个工具,可能对某个学习新主题的人有实际帮助?这有潜力成为一个完整的项目,但在这里,我将专注于其中一个有趣的元素——赋予我们的系统上传文章到 Google Docs 的能力。回想一下,我们还保存了代理在得出最终答案过程中所采取的中间步骤——或许也值得对这些步骤进行记录。
1. 最小可行产品
针对一个问题或话题提示,我们的代理会生成一长串输出。至少,我们希望将其导入到一个 Google 文档中,并附上标题和时间戳。我们还希望能够控制该文档写入 Google Drive 的位置,并最好能够创建和命名文件夹,以便将我们的文章进行有逻辑地存储。我们这里不会过多关注格式化——尽管使用 Google Docs API 完全可以实现——我们更关心的是将文本放入一个人们实际会阅读的地方。格式化可以稍后进行,或者直接留给读者的个人偏好。
一旦我们设置了文档连接,接下来有很多更高级的操作可以做——比如使用 LLM 来重新格式化它们以用于演示,并将其上传到 Google Slides 演示文稿中?或者抓取某个参考数据源并将其上传到 Google Sheets?我们可以将这些功能作为工具添加到代理的控制流程中,让它来决定执行什么。显然这里有很多选择,但最好还是从简单的开始。
2. 连接到 Google Drive
让我们首先写一些代码,以基本方式与 Google Docs 进行交互。首先需要进行一些设置:您需要一个 Google Cloud 账户和一个新的项目。然后,您需要启用 Google Drive 和 Google Docs API。为了为此项目创建一些凭证,我们将使用服务账户,可以按照这里的说明进行设置。此过程将生成一个 .json 文件格式的私钥,您将其存储在本地计算机上。接下来,最好在您的 Google Drive 中为此项目创建一个“主文件夹”。完成后,您可以将您的服务账户添加到该文件夹并授予其写入权限。现在,您的服务账户就具备了以编程方式与该文件夹内容进行交互的权限。
from google.oauth2 import service_account
from abc import ABC, abstractmethod
from googleapiclient.discovery import build
# path to your .json credentials file
from research_assist.gsuite.base.config import CREDENTIALS
from typing import Any
class GSuiteService(ABC):
"""
An abstract base class for G Suite services.
This class defines the structure for any G Suite service implementation,
requiring subclasses to specify the scopes and service creation logic.
Attributes:
credential_path (str): The path to the credentials file.
SCOPES (list): The scopes required for the service.
"""
def __init__(self) -> None:
"""
Initializes the GSuiteService with the credential path and scopes.
"""
# The name of the file containing your credentials
self.credential_path = CREDENTIALS
self.SCOPES = self.get_scopes()
@abstractmethod
def get_scopes(self) -> list[str]:
"""
Retrieves the scopes required for the G Suite service.
Returns:
list[str]: A list of scopes required for the service.
"""
raise NotImplementedError("Subclasses must implement this method.")
@abstractmethod
def get_service(self, credentials: Any) -> Any:
"""
Creates and returns the service object for the G Suite service.
Args:
credentials (Any): The credentials to use for the service.
Returns:
Any: The service object for the G Suite service.
"""
raise NotImplementedError("Subclasses must implement this method.")
def build(self) -> Any:
"""
Builds the G Suite service using the provided credentials.
Returns:
Any: The constructed service object.
"""
# Get credentials into the desired format
creds = service_account.Credentials.from_service_account_file(
self.credential_path, scopes=self.SCOPES
)
service = self.get_service(creds)
return service
class GoogleDriveService(GSuiteService):
"""
A service class for interacting with Google Drive API.
Inherits from GSuiteService and implements the methods to retrieve
the required scopes and create the Google Drive service.
Methods:
get_scopes: Returns the scopes required for Google Drive API.
get_service: Creates and returns the Google Drive service object.
"""
def get_scopes(self) -> list[str]:
"""
Retrieves the scopes required for the Google Drive service.
Returns:
list[str]: A list containing the required scopes for Google Drive API.
"""
SCOPES = ["https://www.googleapis.com/auth/drive"]
return SCOPES
def get_service(self, creds: Any) -> Any:
"""
Creates and returns the Google Drive service object.
Args:
creds (Any): The credentials to use for the Google Drive service.
Returns:
Any: The Google Drive service object.
"""
return build("drive", "v3", credentials=creds, cache_discovery=False)
代码是这样设置的,因为未来我们可能需要使用许多 GSuite API(如 drive、docs、sheets、slides 等)。它们都会继承自 GSuiteService,并将它们的 get_service 和 get_scopes 方法重写为该 API 的特定细节。
一旦这一切设置完成,您就可以开始与 Google Drive 进行交互了。这是一篇很棒的文章,展示了几种主要的操作方法。
在我们的实现中,我们与 Google Drive 的交互方式是通过 GoogleDriveHelper 的方法,该方法在初始化时创建一个 GoogleDriveService 实例。我们首先为它提供我们的主文件夹的名称。
from research_assist.gsuite.drive.GoogleDriveHelper import GoogleDriveHelper
master_folder_name = ai_assistant_research_projects
drive_helper = GoogleDriveHelper(f"{master_folder_name}")
假设我们想创建一个关于“旅行者”系列太空探测器的项目。我们可以通过在主文件夹中设置一个文件夹来进行组织:
project_folder_id = drive_helper.create_new_folder("voyager")
这会创建一个文件夹并返回其 ID,我们可以利用这个 ID 在文件夹中创建文档。这个项目可能有多个版本,所以我们也可以创建相关的子文件夹。
version_folder_id = drive_helper.create_new_folder(
"v1",
parent_folder_id=project_folder_id
)
现在我们准备创建一个空白文档,这也可以通过驱动服务来完成。
final_report_id = drive_helper.create_basic_document(
"final report", parent_folder_id=version_folder_id
)
在后台,驱动助手运行以下代码,它传递了一些元数据,指示我们要通过googleapiclient.discovery.build的创建方法来创建文档(即运行GoogleDriveService().build()时返回的内容)。
document_metadata = {
"name": document_name,
"mimeType": "application/vnd.google-apps.document",
"parents": [parent_folder_id],
}
# make the document
doc = (
self.drive_service.files()
.create(body=document_metadata, fields="id")
execute()
)
doc_id = doc.get("id")
正如你可能想象的那样,Google Drive API 有很多不同的功能和选项,我们在这里并没有涉及。到目前为止,我找到的最全面的 Python 封装库是这个,如果你想进一步探索,这是一个很好的起点。
3. 写入 Google Docs
现在我们已经创建了一个空白文档,接下来让我们用最终的文章填充它!这时GoogleDocsService和GoogleDocsHelper就派上用场了。GoogleDocsService与GoogleDriveService非常相似,也继承自我们在第二节中讨论过的GSuiteService。GoogleDocsHelper包含一些工具,可以将文本和图像写入 Google Docs。它们现在非常基础,但对于这个项目来说已经足够了。
我们可以首先使用我们在第一部分中构建的代理来写一篇关于“旅行者”的文章。
from research_assist.researcher.Agent import ResearchAgent, load_secrets
from langchain_openai import ChatOpenAI
from tavily import TavilyClient
secrets = load_secrets()
model = ChatOpenAI(
model="gpt-4o-mini", temperature=0, api_key=secrets["OPENAI_API_KEY"]
)
tavily = TavilyClient(api_key=secrets["TAVILY_API_KEY"])
agent = ResearchAgent(llm, tavily)
agent.run_task(
task_description="The Voyager missions: What did we learn?",
max_revisions=3
)
记住,代理的各种输出会存储在它的内存中,可以通过以下方式进行探索。在代码中,你可以看到我们在这里使用“user_id = 1”作为占位符,但在有多个用户的应用程序中,这个 ID 将允许模型访问正确的内存存储。
memories = agent.in_memory_store.search(("1", "memories"))
最终报告的文本可以在这里找到,关键名称与我们在第一部分中讨论过的 AgentState 相对应。它位于索引-3 的位置,因为它后面跟着一个调用编辑器节点(它返回了“是”)和接受节点,当前接受节点只是返回“True”。接受节点可以很容易地扩展,实际上将这个报告自动写入文档。
final_essay = agent.in_memory_store.search(("1", "memories"))[-3].dict()["value"][
"memory"
]["write"]["draft"]
让我们看看如何将这些文本放入 Google 文档中。回想一下,在第二节中我们用doc_id创建了一个空白文档。GoogleDocsHelper有两个基本方法可以做到这一点。第一个方法用于提供标题和基本元数据,基本上就是文档编写的日期和时间。第二个方法则是将文本粘贴到文档中。
代码展示了如何控制文本的位置和格式,可能会有些混淆。我们定义了一个请求列表,包含像insertText这样的指令。当我们插入文本时,我们需要提供开始插入的索引,它对应于文档中的某个位置。
def create_doc_template_header(self, document_title: str, doc_id: str) -> int:
"""
Creates a header template for the document,
including the title and the current date.
Args:
document_title (str): The title of the document.
doc_id (str): The ID of the document to update.
Returns:
int: The index after the inserted header.
"""
# add template header
title = f"""
{document_title}
"""
template = f"""
Written on {datetime.date.today()} at {datetime.datetime.now().strftime("%H:%M:%S")}
"""
requests: List[Dict[str, Any]] = [
{
"insertText": {
"location": {
"index": 1,
},
"text": template,
}
},
{
"insertText": {
"location": {
"index": 1,
},
"text": title,
}
},
{
"updateParagraphStyle": {
"range": {
"startIndex": 1,
"endIndex": len(title),
},
"paragraphStyle": {
"namedStyleType": "TITLE",
"spaceAbove": {"magnitude": 1.0, "unit": "PT"},
"spaceBelow": {"magnitude": 1.0, "unit": "PT"},
},
"fields": "namedStyleType,spaceAbove,spaceBelow",
}
},
{
"updateParagraphStyle": {
"range": {
"startIndex": len(title) + 1,
"endIndex": len(title) + len(template),
},
"paragraphStyle": {
"namedStyleType": "SUBTITLE",
"spaceAbove": {"magnitude": 1.0, "unit": "PT"},
"spaceBelow": {"magnitude": 1.0, "unit": "PT"},
},
"fields": "namedStyleType,spaceAbove,spaceBelow",
}
},
]
result = (
self.docs_service.documents()
.batchUpdate(documentId=doc_id, body={"requests": requests})
.execute()
)
end_index = len(title) + len(template) + 1
return end_index
def write_text_to_doc(self, start_index: int, text: str, doc_id: str) -> int:
"""
Writes text to the document at the specified index.
Args:
start_index (int): The index at which to insert the text.
text (str): The text to insert.
doc_id (str): The ID of the document to update.
Returns:
int: The index after the inserted text.
"""
end_index = start_index + len(text) + 1
requests: List[Dict[str, Any]] = [
{
"insertText": {
"location": {
"index": start_index,
},
"text": text,
}
},
{
"updateParagraphStyle": {
"range": {
"startIndex": start_index,
"endIndex": start_index + len(text),
},
"paragraphStyle": {
"namedStyleType": "NORMAL_TEXT",
"spaceAbove": {"magnitude": 1.0, "unit": "PT"},
"spaceBelow": {"magnitude": 1.0, "unit": "PT"},
},
"fields": "namedStyleType,spaceAbove,spaceBelow",
}
},
]
result = (
self.docs_service.documents()
.batchUpdate(documentId=doc_id, body={"requests": requests})
.execute()
)
return end_index
你可以在这里了解有关索引如何定义的更多信息。当有多个 insertText 调用时,似乎先写最后一段文本更容易——例如在下面的代码中,template(即应出现在标题下方的元数据)首先出现在索引 1 的列表中。然后我们在索引 1 写入 title。这导致 title 在文档中首先出现,而 template 出现在其下方。注意,我们还需要指定 paragraphStyle 块的 startIndex 和 endIndex,才能改变文本的格式。
上面代码中的两种方法都会返回当前文本块的结束索引,以便它可以用作后续要附加文本块的起始索引。如果你打算在文档的样式和格式上更加富有创意,这份指南可能会有所帮助。
现在我们已经看到了底层代码,我们可以调用它来将最终报告写入文档。
from research_assist.gsuite.docs.GoogleDocsHelper import GoogleDocsHelper
docs_helper = GoogleDocsHelper()
# add the document title
title_end_index = docs_helper.create_doc_template_header(
"voyager final report", doc_id
)
# add the text
doc_end_index = docs_helper.write_text_to_doc(
start_index=title_end_index, text=final_essay, doc_id=doc_id
)
太好了!现在我们有了所有文档工具,可以用来编辑、格式化并分享我们的代理生成的报告。有趣的是,代理将文本格式化为 markdown,而 Google Docs 支持这种格式,但我没能找到一种方法来让文档自动识别并将 markdown 转换为漂亮的标题和副标题。毫无疑问,一定有办法做到这一点,这样报告看起来会更漂亮。
在运行上面的代码后,文档应该会呈现如下所示的内容。
包含代理生成的最终报告的文档截图。图像由作者生成
4. 那么其他代理输出呢?
我们应该能够将代理内存中存储的所有信息写入文档,这样我们就可以轻松浏览每个阶段的结果。一种稍微黑客一点的方式如下:
memories = agent.in_memory_store.search(("1", "memories"))
# this is needed because we may call some nodes several times
# and we want to keep track of this so that we can make new documents
# for each call
seen_keys = set()
iterations = defaultdict(int)
# folder id where we want to write the documents
folder_id = f"{folder_id}"
for m in memories:
data = m.dict()["value"]["memory"]
available_keys = data.keys()
node_key = list(available_keys)[0]
unique_node_key = node_key + "_00"
if unique_node_key in seen_keys:
iterations[node_key] += 1
unique_node_key = unique_node_key.replace("_00", "") + "_{:02d}".format(
iterations[node_key]
)
print("-" * 20)
print("Creating doc {}".format(unique_node_key))
# get the text
text = data[node_key][list(data[node_key].keys())[0]]
# the tavily research output is a list, so convert it to a string
if isinstance(text, List):
text = "\n\n".join(text)
# if anything else is not a string (e.g. the output of the accept node)
# convert it to a string
if not isinstance(text, str):
text = str(text)
# create document
report_id = drive_service.create_basic_document(
unique_node_key, parent_folder_id=folder_id
)
# create header
end_index = docs_helper.create_doc_template_header(unique_node_key, report_id)
# fill document
end_index = docs_helper.write_text_to_doc(
start_index=end_index, text=text, doc_id=report_id
)
seen_keys.add(unique_node_key)
这将生成 7 个文档,下面我们将查看一些示例截图。
运行上述代码的输出。图像由作者生成
初步计划概述了报告的结构。有趣的是,模型似乎更倾向于使用大量简短的章节,我认为这很合适,因为提示要求将报告做得简洁且易于普通读者消化。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/3a7eb62fe4a38648c0866643ef27dede.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/9b48c7b1ec4952ca764da720e9929c35.png
上述代码片段编写的初步计划和研究文档的部分截图。图像由作者生成。
在研究阶段,调用了 Tavily 搜索,并返回了与使用的查询相关的格式良好的小块文本。其中一些小块被截断,这使得文档不太易读,但它很好地展示了从研究节点到写作节点传递的信息类型。
在审阅阶段,我们得到了对文章第一版本的精彩批评。通常这些审阅的结构与初始计划类似,提出许多非常笼统的建议,如“考虑使用更具描述性的标题”或“这一部分可以扩展,增加更多例子”。如果我们比较审阅前后的实际报告,通常会看到结构上的微小变化以及每个部分中增加的一些细节。这是否真正提高了文本的质量值得商榷,但通过在几个例子中进行尝试,我相信它确实有所帮助。
https://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/a1dd4e1dc0e37f1c84a80d1535b14765.pnghttps://github.com/OpenDocCN/towardsdatascience-blog-zh-2024/raw/master/docs/img/0184229a9da34e4de535a5b1c7c25d0b.png
这是通过上述代码片段生成的部分审阅和编辑反馈文档的截图。图片由作者生成。
最后,我们得到了编辑对审阅后草稿的判断。我目前使用的提示使得编辑相当宽容,因此它通常会说一些类似这里所示的内容。通过调整一些提示,我们可以鼓励它在需要时将更多的报告发送回审阅。
这就是本文及本小系列的全部内容。感谢阅读,希望你能从中找到一些对你自己项目有用的内容。在让研究代理更加健壮、对其输出进行适当评估以及更好地与 Docs(或其他 GSuite API)集成方面,这里有很多潜在的扩展。如果你有任何其他酷炫的想法,请告诉我!
作者与本文讨论的任何工具没有关联。
构建一个稳健的数据可观察性框架以确保数据质量和完整性
我们如何通过开源工具提高可观察性?
https://medium.com/@jurgitamotus?source=post_page---byline--07ff6cffdf69--------------------------------https://towardsdatascience.com/?source=post_page---byline--07ff6cffdf69-------------------------------- Jurgita Motus | Coresignal
·发布于Towards Data Science ·7 分钟阅读·2024 年 8 月 27 日
–
传统的监控方式已经无法满足复杂数据组织的需求。数据工程师不再依赖反应式系统来识别已知问题,而是必须创建互动式可观察性框架,帮助他们快速发现任何类型的异常。
虽然可观察性涵盖了许多不同的实践,但在本文中,我将分享一个高层次的概述以及我们在组织中使用开源工具构建可观察性框架的实际经验和技巧。
那么,如何构建一个能够有效显示数据健康状况并确保数据质量的基础设施呢?
什么是数据可观察性?
总的来说,可观察性定义了你从外部输出中能够了解多少有关内部系统的信息。这个术语最早由匈牙利裔美国工程师Rudolf E. Kálmán在 1960 年提出,他在讨论数学控制系统中的可观察性时首次定义了这一术语。
多年来,这一概念已经被应用于多个领域,包括数据工程。在这里,它解决了数据质量的问题,并能够追踪数据是如何收集的以及如何进行转化的。
数据可观察性意味着确保所有管道和系统中的数据都是完整且高质量的。这是通过实时监控和管理数据来解决质量问题。可观察性确保了清晰度,使得在问题蔓延之前能够采取行动。
什么是数据可观察性框架?
数据可观察性框架是一个监控和验证机构内数据完整性和质量的过程。它有助于主动确保数据的质量和完整性。
该框架必须基于五个强制性方面,这些方面由IBM定义:
-
数据新鲜度。必须找到并移除任何过时的数据。
-
分布。必须记录预期数据值,以帮助识别异常值和不可靠数据。
-
数据量。必须跟踪预期数据值的数量,以确保数据完整。
-
数据模式。必须监控数据表和组织的变化,以帮助找到破损的数据。
-
血缘追踪。收集元数据并映射数据源是帮助故障排除的必要步骤。
这五个原则确保数据可观察性框架有助于维护和提高数据质量。通过实施以下数据可观察性方法,您可以实现这些目标。
如何将可观察性实践添加到数据管道中
只有从可靠来源收集的高质量数据才能提供准确的见解。正如那句老话所说:垃圾进,垃圾出。您不能指望从组织混乱的数据集中提取任何实际的知识。
作为公共数据提供商 Coresignal 的高级数据分析师,我不断寻求改善数据质量的新方法。尽管在动态的技术环境中实现这一目标相当复杂,但许多路径可以通向它。良好的数据可观察性在这里发挥着重要作用。
那么,我们如何确保数据的质量呢?归根结底,就是在每个数据管道阶段中添加更好的可观察性方法——从数据摄取、转换到存储和分析。这些方法中的一些将在整个管道中起作用,而另一些只会在某个特定阶段相关。让我们来看看:
跨越数据管道不同阶段的数据可观察性。来源:Jurgita Motus
首先,我们需要考虑涵盖整个管道的五个项目:
-
端到端数据血缘追踪。追踪血缘关系可以让您快速访问数据库历史,并从原始数据源跟踪数据直到最终输出。通过理解结构及其关系,您将更容易发现不一致之处,从而避免它们成为问题。
-
端到端测试。验证过程会检查数据管道各个阶段的完整性和质量,帮助工程师确定管道是否正常运行,并发现任何不典型的行为。
-
根本原因分析。如果管道的任何阶段出现问题,工程师必须能够准确定位源头,并快速找到解决方案。
-
实时警报。可观察性的一个重要目标是迅速发现新出现的问题。在标记异常行为时,时间至关重要,因此任何数据可观察性框架都必须能够实时发送警报。这对数据接收以及存储和分析阶段尤其重要。
-
异常检测。数据管道中可能会出现丢失数据或性能低下等问题。异常检测是一种先进的可观察性方法,通常会在流程的后期实现。在大多数情况下,需要机器学习算法来检测数据和日志中的异常模式。
接下来,我们有五个其他项目,在不同的数据管道阶段中更为相关:
-
服务水平协议(SLAs)。SLA 有助于为客户和供应商设定标准,并定义数据的质量、完整性和一般责任。SLA 的阈值还可以在设置警报系统时提供帮助,通常,它们会在数据接收阶段之前或期间签署。
-
数据合同。这些协议定义了数据进入其他系统之前的结构方式。它们充当一套规则,明确你可以期望的数据的新鲜度和质量水平,通常会在数据接收阶段之前谈判确定。
-
架构验证。它保证数据结构的一致性,并确保与下游系统的兼容性。工程师通常在数据接收或处理阶段验证架构。
-
日志、指标和追踪。虽然这些对于监控性能至关重要,但收集和轻松访问这些关键信息将成为危机中的有力工具——它帮助你更快地找到问题的根本原因。
-
数据质量仪表板。仪表板帮助监控数据管道的整体健康状况,并提供可能出现问题的高级视图。它们确保通过其他可观察性方法收集到的数据以清晰、实时的方式呈现。
最后,数据可观察性无法实现,如果框架中没有自我评估,因此,持续审计和检查系统对于任何组织来说都是必须的。
接下来,让我们讨论一些可能有助于你简化工作流程的工具。
数据可观察性平台及其功能
那么,如果你开始在组织中构建数据可观察性框架,应该考虑哪些工具呢?虽然市场上有许多选择,但根据我的经验,你最好的选择是从以下工具开始。
在构建我们的数据基础设施时,我们专注于最大化利用开源平台。下面列出的工具在处理大量数据时,能够确保透明度和可扩展性。虽然它们中的大多数工具并非专门用于数据可观察性,但它们结合使用时能提供确保数据管道可见性的好方法。
下面是我推荐查看的五个必要平台的列表:
-
Prometheus 和 Grafana 平台互为补充,帮助工程师实时收集和可视化大量数据。Prometheus 是一款开源监控系统,非常适合数据存储和观察,而可观察性平台 Grafana 则通过易于导航的可视化仪表板帮助追踪新趋势。
-
Apache Iceberg 表格格式提供了数据库元数据的概览,包括跟踪表列的统计信息。跟踪元数据有助于更好地理解整个数据库,而无需不必要地处理数据。它不完全是一个可观察性平台,但其功能允许工程师更好地了解他们的数据。
-
Apache Superset 是另一款开源数据探索和可视化工具,可以帮助展示大量数据、构建仪表板并生成警报。
-
Great Expectations 是一个帮助测试和验证数据的 Python 包。例如,它可以使用预定义规则扫描样本数据集,并创建数据质量条件,稍后可用于整个数据集。我们的团队使用 Great Expectations 对新数据集进行质量测试。
-
Dagster 数据管道编排工具可以帮助确保数据血缘关系并进行资产检查。尽管它不是作为数据可观察性平台而创建的,但它通过现有的数据工程工具和表格格式提供可视化。该工具有助于找出数据异常的根本原因。该平台的付费版本还包含由 AI 生成的洞察。这款应用程序提供自助式可观察性,并带有内置的资产目录,用于跟踪数据资产。
请记住,这些只是众多可选工具中的一部分。务必进行研究,找到适合你组织的工具。
如果忽视数据可观察性原则会发生什么?
一旦出现问题,组织通常依赖工程师的直觉来找出问题的根本原因。正如软件工程师 Charity Majors 在她的回忆录中生动地解释的那样,在 MBaaS 平台 Parse 工作时,大多数传统的监控系统都是由在公司工作时间最长的工程师推动的,他们能够迅速猜测系统的问题。这使得资深工程师变得不可替代,并且带来了其他问题,比如较高的职业倦怠率。
使用数据可观察性工具可以消除故障排除中的猜测,减少停机时间,并增强信任。如果没有数据可观察性工具,你可能会遇到较长的停机时间、数据质量问题以及对新出现问题反应迟缓等问题。因此,这些问题可能会迅速导致收入损失、客户流失,甚至损害品牌声誉。
数据可观察性对处理大量信息并必须保证数据质量和完整性不中断的大型企业至关重要。
数据可观察性未来会如何发展?
数据可观察性是每个组织必须具备的,尤其是那些从事数据收集和存储的公司。一旦所有工具就位,就可以开始使用先进的方法来优化这一过程。
机器学习,特别是大型语言模型(LLMs),是这里显而易见的解决方案。它们可以帮助快速扫描数据库,标记异常,并通过识别重复项或添加新的丰富字段来提高整体数据质量。同时,这些算法还能帮助跟踪模式和日志的变化,改善数据一致性并提升数据血缘关系。
然而,选择合适的时机来实施你的人工智能计划至关重要。提升你的可观察性能力需要资源、时间和投资。在开始使用自定义的 LLM 之前,你应该仔细考虑这是否真的能为你的组织带来好处。有时,坚持使用上述已经有效完成工作的标准开源数据可观察性工具,可能会更高效。
构建安全且可扩展的数据与 AI 平台
通过数据驱动的决策赋能业务
https://medium.com/@rizviadil?source=post_page---byline--074e191b291f--------------------------------https://towardsdatascience.com/?source=post_page---byline--074e191b291f-------------------------------- Adil Rizvi
·发表于Towards Data Science ·7 分钟阅读·2024 年 2 月 22 日
–
图片由Igor Omilaev提供,来自Unsplash
在过去的四年里,我有幸领导了全球规模的大数据和 AI 平台的战略、设计和实施,涉及的不仅是一个而是两个公共云平台——AWS 和 GCP。此外,我的团队使 70 多个数据科学/机器学习(DSML)用例和 10 个数字应用得以投入运营,为公司贡献了约 1 亿美元的收入增长。
这段旅程充满了令人兴奋的挑战和一些陡峭的学习曲线,但最终的结果非常有影响力。通过这篇文章,我想分享我的学习和经验,这将帮助其他技术创新者思考他们的规划过程,并使他们的实施能够跨越式发展。
本文将主要集中在基础构建上,以提供整体生产生态系统的全貌。在之后的文章中,我将讨论技术选择,并分享更详细的规范性建议。
让我先给你展示一下数据和 AI 平台的构建模块。
数据和 AI 平台的端到端区块级架构
思考从端到端架构是一个极好的主意,因为这样你可以避免快速而粗糙完成工作的常见陷阱。毕竟,你的 ML 模型输出的质量取决于你输入的数据。而且,你不想在数据安全性和完整性上做出妥协。
1. 数据采集与摄取
创建一个良好的数据操作(DataOps)框架对于整体数据导入过程至关重要。很多因素取决于生成数据的来源(结构化与非结构化数据)以及接收数据的方式(批量、复制、近实时、实时)。
在获取数据时,有多种方式可以将其导入 -
-
提取 → 加载(无需转换)
-
提取 → 加载 → 转换(主要用于批量上传)
-
提取 → 转换 → 加载(适用于流式数据)
特征工程师必须进一步结合数据,创建用于机器学习用例的特征(特征工程)。
2. 数据存储
选择最佳的数据存储至关重要,像 S3、GCS 或 Blob Storage 这样的对象存储桶是导入原始数据的最佳选择,尤其适用于非结构化数据。
对于纯粹的分析用例,此外,如果你要导入 SQL 结构化数据,也可以将数据直接导入云数据仓库(如 Big Query 等)。许多工程团队也更倾向于使用数据仓库存储(与对象存储不同)。你的选择将取决于使用场景和涉及的成本。请谨慎选择!
通常,你可以直接从内部和外部(第一方和第三方)源导入数据,而无需任何中间步骤。
然而,在一些情况下,数据提供方可能需要访问你的环境进行数据交易。计划在 DMZ 设置中为第三方创建一个着陆区,以防止将整个数据系统暴露给供应商。
此外,对于合规相关的数据,如 PCI、PII 和受监管的数据(如 GDPR、MLPS、AAPI、CCPA 等),应创建结构化存储区,从一开始就妥善处理这些数据。
记得根据你的机器学习模型和分析报告的时间旅行或历史上下文要求,规划数据保留和备份策略。虽然存储便宜,但随着时间的推移,积累的数据会成倍增加成本。
3. 数据治理
虽然大多数组织擅长导入和存储数据,但大多数工程团队在使数据对最终用户可消费方面存在困难。
导致采纳不良的主要因素有 —
-
组织内的数据素养不足
-
缺乏明确定义的数据目录和数据字典(元数据)
-
无法访问查询接口
数据团队必须与法律、隐私和安全团队合作,了解国家和地区的数据法规以及合规要求,以确保数据治理的合规性。
实施数据治理的几种方法包括:
-
数据掩码和匿名化
-
基于属性的访问控制
-
数据本地化
如果未能妥善保护存储和数据访问,可能会使组织面临法律问题及相关处罚。
4. 数据消费模式
随着数据被转化并丰富为业务关键绩效指标(KPIs),数据的呈现和消费有不同的方面。
对于纯粹的可视化和仪表盘,简单的存储数据访问和查询接口就足够了。
随着需求变得更加复杂,比如向机器学习模型提供数据,你必须实施和增强功能存储。这个领域需要成熟,且大多数云原生解决方案仍处于生产级就绪的早期阶段。
同时,寻找一个水平数据层,通过 API 向其他应用程序提供数据消费。GraphQL 是一个很好的解决方案,能够帮助创建微服务层,从而显著提升访问的便捷性(数据即服务)。
随着这一领域的成熟,考虑将数据结构化为数据产品域,并在业务单元中找到数据管理员,作为该域的管理者。
5. 机器学习
在数据后处理后,机器学习采用两步走的方式——模型开发和模型部署与治理。
操作化 AI 平台
在模型开发阶段,机器学习工程师与数据科学家密切合作,直到模型被打包并准备好进行部署。选择机器学习框架和功能,并与数据科学家合作进行超参数调优和模型训练,都是开发生命周期的一部分。
创建部署流水线并选择技术栈来使模型能够投入生产并提供服务,这些都属于 MLOps 范畴。MLOps 工程师还提供机器学习模型管理,包括监控、评分、漂移检测和启动再训练。
自动化机器学习模型生命周期中的所有这些步骤有助于实现规模化。
不要忘记将所有训练过的模型存储在机器学习模型注册表中,并促进重用,以提高操作效率。
6. 生产操作
提供模型输出需要与其他功能领域的持续合作。提前规划和开放的沟通渠道对于确保发布日程的协调至关重要。请务必做到这一点,以避免错过截止日期、技术选择冲突以及集成层的问题。
根据消费层和部署目标,你将通过 API 发布模型输出(模型端点),或者让应用程序直接从存储中获取推断结果。结合 API 网关使用 GraphQL 是实现这一目标的高效方式。
7. 安全层
分离管理平面并创建共享服务层,这将成为你的云账户的主要进出口点。它也将是你组织内外部公有/私有云的会面室。
共享服务 — 谷歌云平台
共享服务 — 亚马逊 Web 服务
你的服务控制策略(AWS)或组织政策限制(GCP)应集中管理,并保护资源,防止在没有适当访问控制的情况下被创建或托管。
8. 用户管理界面 / 消费层
明智的做法是提前选择云账户的结构。您可以根据业务线(LOB)、产品领域或两者的混合来构建账户结构。同时,设计并分隔您的开发、测试和生产环境。
最好将您的 DevOps 工具链集中化。我更倾向于使用一个与云平台无关的工具集,以支持在混合多云生态系统之间的无缝集成和过渡。
对于开发者 IDE,可能会有个人 IDE 和共享 IDE 的混合。确保开发者经常将代码提交到代码库,否则他们可能会丢失工作进度。
使用云无关的 DevSecOps 工具链进行 GCP 设置
端到端数据科学过程
在组织动态中进行导航,并将利益相关者聚集在一个共同的对齐目标上,对于成功的生产部署和持续运营至关重要。
我正在分享使这个复杂系统顺畅运行的跨职能工作流和流程。
从头到尾的数据科学模型部署过程
结论
希望这篇文章能激发您的思考,激发新的想法,并帮助您勾画出您的工作全貌。这是一个复杂的任务,但通过深思熟虑的设计、精心规划的执行和大量的跨职能合作,您将能够轻松应对。
最后的建议:不要仅仅因为某项技术解决方案看起来很酷就去创建它。首先要理解业务问题,并评估潜在的投资回报。最终目标是创造业务价值,并为公司的收入增长做出贡献。
祝你在构建或完善数据和 AI 平台的过程中好运。
-
一路顺风!
- Adil { LinkedIn}
<< 除非另有说明,所有图片均来自作者 >>
构建语义图书搜索:使用 Apache Spark 和 AWS EMR Serverless 扩展嵌入管道
图片来源:Unsplash
使用 OpenAI 的 Clip 模型支持对 70,000 本书封面的自然语言搜索
https://medium.com/@erevear?source=post_page---byline--1d074ee9cb55--------------------------------https://towardsdatascience.com/?source=post_page---byline--1d074ee9cb55-------------------------------- Eva Revear
·发布在Towards Data Science ·阅读时长 8 分钟·2024 年 1 月 31 日
–
在上一篇文章中,我做了一个小的 PoC,看看能否使用 OpenAI 的 Clip 模型来构建语义图书搜索。依我看,效果出乎意料地好,但我不禁想,是否有更多数据会更好。之前的版本仅使用了大约 3.5 千本书,但在Openlibrary 数据集中有数百万本书,我觉得尝试添加更多的搜索选项是值得的。
然而,完整的数据集大约有 40GB,试图在我的小笔记本电脑上,甚至在 Colab 笔记本中处理这么多数据有点困难,所以我不得不想办法构建一个管道来处理过滤和嵌入更大的数据集。
TLDR; 它改善了搜索吗?我认为是的!我们将数据量增加了 15 倍,这为搜索提供了更多可用数据。它还不完美,但我觉得结果相当有趣;虽然我没有做正式的准确度评测。
这是一个无论如何表述都无法在上一版本中运行的例子,但在使用更多数据的版本中效果相当不错。
作者图片
如果你感兴趣,可以在Colab上试试看!
总体来说,这是一次有趣的技术旅程,过程中遇到了许多障碍和学习机会。技术栈仍然包括 OpenAI 的 Clip 模型,但这次我使用了 Apache Spark 和 AWS EMR 来运行嵌入管道。
技术栈
图片由作者提供
这似乎是一个使用 Spark 的好机会,因为它允许我们将嵌入计算并行化。
我决定在 EMR Serverless 中运行管道,这是 AWS 提供的一项相对较新的服务,提供了一个无服务器环境来运行 EMR 并自动管理资源的扩展。我认为这对于这个用例很合适——而不是在 EC2 集群上启动 EMR——因为这是一个相对临时的项目,我对集群成本很敏感,而且最初我不确定这个作业需要什么资源。EMR Serverless 使得实验作业参数变得非常简单。
以下是我完成一切并使其运行的完整过程。我想可能有更好的方法来管理某些步骤,这只是对我有效的方式,如果你有想法或意见,请一定分享!
使用 Spark 构建嵌入管道作业
初步步骤是编写 Spark 作业。整个管道分为两个阶段,第一个阶段获取初始数据集并筛选出近十年内的小说(过去 10 年)。这导致了大约 25 万本书籍,其中约 7 万本有封面图片可以下载并嵌入到第二阶段中。
首先,我们从原始数据文件中提取相关列。
然后对数据类型进行一些通用的数据转换,并筛选出除英文小说(超过 100 页)以外的所有数据。
第二阶段获取第一阶段的输出数据集,并通过从 Hugging Face 下载的 Clip 模型处理图片。这里的关键步骤是将我们需要应用于数据的各种函数转化为 Spark UDF。最关键的函数是 get_image_embedding,它接受图片并返回嵌入信息。
我们将其注册为 UDF:
然后在数据集上调用 UDF:
设置向量数据库
作为代码中的最后一个可选步骤,我们可以设置一个向量数据库,在这个案例中是 Milvus,用于加载和查询。请注意,我没有在此项目的云作业中执行此操作,因为我将我的嵌入数据序列化存储,以便不需要保持集群长时间运行。然而,设置 Milvus 并将 Spark DataFrame 加载到集合中是相当简单的。
首先,创建一个集合,并在图像嵌入列上创建索引,数据库可以用来进行搜索。
然后我们可以在 Spark 脚本中访问该集合,并从最终的 DataFrame 中将嵌入加载到其中。
最后,我们可以使用与上述 UDF 相同的方法将搜索文本嵌入,并使用嵌入信息查询数据库。数据库负责进行繁重的匹配工作,找出最佳匹配。
在 AWS 中设置管道
前提条件
现在有一些设置需要完成,以便在 EMR Serverless 上运行这些作业。
我们需要以下先决条件:
-
一个 S3 桶,用于存放作业脚本、输入和输出以及作业所需的其他工件
-
一个具有 S3 读取、列出和写入权限的 IAM 角色,以及 Glue 的读取和写入权限。
-
一个信任策略,允许 EMR 作业访问其他 AWS 服务。
AWS 文档中有关于角色和权限策略的详细描述,以及如何开始使用 EMR Serverless 的概述:开始使用 Amazon EMR Serverless
接下来,我们需要设置一个 EMR Studio:创建 EMR Studio
通过互联网网关访问网页
另一个特定于此作业的设置是,我们必须允许该作业访问互联网,而默认情况下 EMR 应用程序是无法做到这一点的。正如我们在脚本中看到的,作业需要访问要嵌入的图像,以及访问 Hugging Face 以下载模型配置和权重。
注意:可能有更高效的方式来处理模型,而不是将其下载到每个工作节点(例如广播、将其存储在系统中的某个地方等),但在这种情况下,对于一次数据运行,这就足够了。
无论如何,允许 Spark 作业运行的机器访问互联网需要配置具有 NAT 网关的私有子网的 VPC。所有这些设置都从访问 AWS VPC 界面开始 -> 创建 VPC -> 选择 VPC 和更多选项 -> 选择至少一个 NAT 网关选项 -> 点击创建 VPC。
图像由作者提供
VPC 设置需要几分钟。一旦完成,我们还需要在安全组界面中创建一个安全组,并将刚才创建的 VPC 附加到它。
创建 EMR Serverless 应用程序
现在,来看看将提交作业的 EMR Serverless 应用程序!创建并启动一个 EMR Studio 应该会打开一个界面,提供几个选项,包括创建应用程序。在创建应用程序的 UI 中,选择使用自定义设置 -> 网络设置。在这里,VPC、两个私有子网和安全组将发挥作用。
图像由作者提供
构建虚拟环境
最后,环境中没有太多库,因此为了添加额外的 Python 依赖项,我们可以使用本地 Python 或创建并打包一个虚拟环境:在 EMR Serverless 中使用 Python 库。
我选择了第二种方式,最简单的方法是使用 Docker,因为它允许我们在运行 EMR 作业的 Amazon Linux 发行版中构建虚拟环境(在其他发行版或操作系统中做这件事可能会变得非常混乱)。
另一个警告:小心选择与您使用的 Python 版本对应的 EMR 版本,并相应地选择合适的包版本。
Docker 过程将打包好的虚拟环境输出为 pyspark_dependencies.tar.gz,然后将其与作业脚本一起上传到 S3 存储桶。
然后,我们可以将这个打包好的环境和其他 Spark 作业配置一起发送出去。
太好了!我们有了作业脚本、环境依赖项、网关和一个 EMR 应用程序,我们可以提交作业了!不过,别着急!接下来才是真正有趣的部分:Spark 调优。
如前所述,EMR Serverless 会自动扩展以处理我们的工作负载,这通常是很好的,但我发现(事后看来是显而易见的)它对于这个特定的用例并没有什么帮助。
几万条记录根本不算是“大数据”;Spark 需要处理的是 TB 级别的数据,而我发送的基本上只是几千个图片 URL(甚至连图片本身都没有)。如果让 EMR Serverless 自行决定,它会把任务发送到一个节点,在单线程中处理,完全违背了并行化的初衷。
此外,虽然嵌入作业处理的数据量相对较小,但它们会显著扩大数据量,因为嵌入体积相当大(在 Clip 的情况下是 512)。即使你让那个节点不停地运行几天,它也会在完成所有数据处理之前就耗尽内存。
为了让它运行,我尝试了几个 Spark 属性,目的是能够在集群中使用大型机器,但将数据拆分成非常小的分区,以便每个核心只需要处理少量数据并输出:
-
spark.executor.memory:每个执行器进程使用的内存量。
-
spark.sql.files.maxPartitionBytes:读取文件时单个分区中要打包的最大字节数。
-
spark.executor.cores:每个执行器使用的核心数。
你需要根据数据的具体性质调整这些设置,嵌入仍然不是一个快速的过程,但它能够处理我的数据。
结论
与我的上一篇文章一样,结果当然不是完美的,绝对不能替代其他人提供的真实书籍推荐!但话说回来,对于我搜索的一些问题,确实有一些准确的答案,我觉得还挺酷的。
如果你想自己玩一下这个应用,它在Colab上,整个管道的代码在Github上!
在 LangChain 中使用工具和工具包构建简单代理
熟悉 LangChain 中代理的构建模块
https://medium.com/@ssmaameri?source=post_page---byline--77e0f9bd1fa5--------------------------------https://towardsdatascience.com/?source=post_page---byline--77e0f9bd1fa5-------------------------------- Sami Maameri
·发布于Towards Data Science ·13 分钟阅读·2024 年 4 月 10 日
–
图片由Dan LeFebvre提供,来源于Unsplash
让我们在 LangChain 中构建一个简单的代理,帮助我们理解一些基础概念以及代理如何工作的构建模块。
通过保持简单,我们可以更好地理解这些代理背后的基础理念,从而在未来构建更复杂的代理。
**Contents**
What are Agents?
Building the Agent
- The Tools
- The Toolkit
- The LLM
- The Prompt
The Agent
Testing our Agent
Observations
The Future
Conclusion
想要获得我未来文章的通知吗?在此订阅
什么是代理
LangChain 文档实际上有一个相当好的页面介绍了关于代理的高层次概念。它简短易懂,在开始之前浏览一下肯定值得。
如果你查找人工智能代理的定义,你会看到类似于“一个能够感知其环境、对环境采取行动、做出智能决策以达到目标,并且具有边走边学习能力的实体”的描述。
我认为这完全符合 LangChain 代理的定义。让这一切在软件中成为可能的是大型语言模型(LLM)的推理能力。LangChain 代理的大脑是 LLM。正是 LLM 被用来推理执行用户请求的最佳方法。
为了执行任务并操作事物以及获取信息,代理在 LangChain 中有一些叫做工具(Tool)的东西可供使用。正是通过这些工具,它能够与环境进行交互。
这些工具基本上就是代理可以访问的方法/类,它们可以做一些事情,比如通过 API 与股票市场指数交互、更新 Google 日历事件或对数据库执行查询。我们可以根据需要构建工具,这取决于我们试图通过代理完成的任务的性质。
在 LangChain 中,工具的集合称为 Toolkit。在实现上,这实际上只是一个包含可供代理使用的工具的数组。因此,LangChain 中代理的高级概述大致如下所示
图片由作者提供
所以,从基本的角度来看,一个代理需要
-
一个 LLM 作为它的大脑,并赋予它推理能力
-
工具,以便它能够与周围的环境进行交互,并实现其目标
构建代理
为了让这些概念更加具体,我们来构建一个简单的代理。
我们将创建一个数学代理,它可以执行一些简单的数学运算。
环境设置
首先,让我们设置我们的环境和脚本
mkdir simple-math-agent && cd simple-math-agent
touch math-agent.py
python3 -m venv .venv
. .venv/bin/activate
pip install langchain langchain_openai
另外,你也可以克隆这里使用的代码从 GitHub
git clone git@github.com:smaameri/simple-math-agent.git
或者也可以查看Google Colab中的代码。
工具
最简单的开始方式是首先为我们的数学代理定义工具。
让我们为它提供“add”、“multiply”和“square”工具,以便它能够对我们传递给它的问题执行这些操作。通过保持工具简单,我们可以专注于核心概念,自己构建工具,而不是依赖像 WikipediaTool 这样更复杂的现成工具,它作为 Wikipedia API 的包装器,需要我们从 LangChain 库中导入。
再次强调,我们并不打算做什么复杂的事情,只是保持简单,将代理的主要构建模块拼凑在一起,以便我们能够理解它们是如何工作的,并让我们的第一个代理启动并运行。
我们从“add”工具开始。在 LangChain 中创建工具的自底向上的方式是扩展BaseTool类,设置类中的name和description字段,并实现**_run**方法。代码如下所示
from langchain_core.tools import BaseTool
class AddTool(BaseTool):
name = "add"
description = "Adds two numbers together"
args_schema: Type[BaseModel] = AddInput
return_direct: bool = True
def _run(
self, a: int, b: int, run_manager: Optional[CallbackManagerForToolRun] = None
) -> str:
return a + b
注意,我们需要实现**_run**方法,以显示我们的工具如何处理传递给它的参数。
还要注意它需要一个 pydantic 模型作为args_schema。我们将在这里定义它。
AddInput
a: int = Field(description="first number")
b: int = Field(description="second number")
现在,LangChain 确实为我们提供了一种更简便的方式来定义工具,而不需要每次都扩展BaseTool类。我们可以借助**@tool**装饰器来做到这一点。使用@tool 装饰器在 LangChain 中定义“add”工具的代码如下所示
from langchain.tools import tool
@tool
def add(a: int, b: int) -> int:
“””Adds two numbers together””” # this docstring gets used as the description
return a + b # the actions our tool performs
更简单对吧。幕后,装饰器神奇地使用了提供的方法来扩展 BaseTool 类,就像我们之前做的那样。有一点需要注意:
-
方法名称也成为工具名称
-
方法参数定义了工具的输入参数
-
文档字符串被转换为工具的描述
你也可以在工具上访问这些属性
print(add.name) # add
print(add.description) # Adds two numbers together.
print(add.args) # {'a': {'title': 'A', 'type': 'integer'}, 'b': {'title': 'B', 'type': 'integer'}}
请注意,工具的描述非常重要,因为 LLM 会根据这个描述来决定是否选择该工具。如果描述不准确,可能导致工具在该使用时没有被使用,或者在错误的时机被使用。
完成 add 工具后,让我们继续定义 multiply 和 square 工具。
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b
@tool
def square(a) -> int:
"""Calculates the square of a number."""
a = int(a)
return a * a
就是这样,简单如斯。
所以我们定义了自己的三个 自定义工具。一个更常见的用例可能是使用 LangChain 中已提供和存在的一些工具,你可以在 这里 查看。不过,在源代码层面,它们都会使用类似上述描述的方法来构建和定义。
到这里为止,我们的工具就算完成了。现在是时候将工具组合成一个工具包了。
工具包
工具包听起来很复杂,但其实它们非常简单。它们实际上就是一组工具的列表。我们可以像这样定义我们的工具包,作为一个工具数组:
toolkit = [add, multiply, square]
就这样。非常简单,没有什么好混淆的。
通常,工具包是一些可以一起使用的工具组,对于那些试图执行特定任务的智能体来说非常有帮助。例如,SQLToolkit 可能包含用于生成 SQL 查询、验证 SQL 查询和执行 SQL 查询的工具。
LangChain 文档中的 集成工具包 页面列出了由社区开发的许多工具包,可能对你有用。
LLM
如上所述,LLM 是智能体的大脑。它根据传入的问题决定调用哪些工具,根据工具的描述决定下一步应该采取哪些最佳步骤。它还决定何时达到最终答案,并准备将其返回给用户。
在这里设置 LLM
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-3.5-turbo-1106", temperature=0)
提示词
最后,我们需要一个提示词传递给我们的智能体,这样它就能大致了解它是什么类型的智能体,以及应该解决哪些任务。
我们的智能体需要一个 ChatPromptTemplate 来工作(稍后会详细介绍)。这是一个基本的 ChatPromptTemplate,主要关注部分是系统提示,其他则是我们需要传递的默认设置。
在我们的提示词中,我们包含了一个示例答案,向智能体展示我们希望它只返回答案,而不附带任何描述性文字。
prompt = ChatPromptTemplate.from_messages(
[
("system", """
You are a mathematical assistant. Use your tools to answer questions.
If you do not have a tool to answer the question, say so.
Return only the answers. e.g
Human: What is 1 + 1?
AI: 2
"""),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
]
)
就这样。我们已经设置好了代理所需的工具和工具包,代理需要这些作为设置的一部分,以便了解它可以执行哪些类型的操作和具有哪些能力。我们也已经设置好了 LLM 和系统提示。
现在是有趣的部分了。设置我们的代理!
代理
LangChain 有多种不同的代理类型可以创建,这些代理具有不同的推理能力和特性。我们将使用目前最强大和最有能力的代理——OpenAI 工具代理。根据 OpenAI 工具代理的文档,它也使用了更新的 OpenAI 模型。
更新后的 OpenAI 模型已经经过微调,以便能够检测何时应该调用一个或多个函数,并响应应传递给函数的输入。在 API 调用中,你可以描述函数,并让模型智能地选择输出一个包含调用这些函数所需参数的 JSON 对象。OpenAI 工具 API 的目标是比使用通用的文本完成或聊天 API 更可靠地返回有效且有用的函数调用。
换句话说,这个代理擅长生成正确的结构来调用函数,并能够理解我们的任务是否需要多个函数(工具)。这个代理还能够调用具有多个输入参数的函数(工具),就像我们的代理一样。有些代理只能处理具有单一输入参数的函数。
如果你熟悉OpenAI 的函数调用功能,在该功能中,我们可以使用 OpenAI 的 LLM 来生成正确的参数,以便调用函数,那么我们在这里使用的 OpenAI 工具代理也在利用其中的一部分功能,能够以正确的参数调用正确的工具。
为了在 LangChain 中设置代理,我们需要使用提供的工厂方法来创建我们选择的代理。
创建 OpenAI 工具代理的工厂方法是create_openai_tools_agent()。它需要传入我们上面设置的 LLM、工具和提示。因此,让我们初始化我们的代理。
agent = create_openai_tools_agent(llm, toolkit, prompt)
最后,为了在 LangChain 中运行代理,我们不能直接调用“run”类型的方法。它们需要通过 AgentExecutor 来运行。
我之所以在最后提到 Agent Executor,是因为我认为它不是理解代理工作原理的关键概念,放在开始时与其他内容一起讲解只会让整个过程看起来比实际需要的更复杂,而且还可能分散对一些更基本概念的理解。
所以,现在我们介绍一下,AgentExecutor 作为 LangChain 中代理的运行时,允许代理一直运行,直到准备好返回最终的响应给用户。在伪代码中,AgentExecutor 的工作原理大致如下(直接摘自LangChain 文档)
next_action = agent.get_action(...)
while next_action != AgentFinish:
observation = run(next_action)
next_action = agent.get_action(..., next_action, observation)
return next_action
所以它们基本上是一个 while 循环,持续调用代理的下一个操作方法,直到代理返回最终响应。
所以,让我们将代理设置在代理执行器内。我们传递给它代理,并且还必须传递工具包。我们将“verbose”设置为 True,以便在代理处理我们的请求时了解它在做什么。
agent_executor = AgentExecutor(agent=agent, tools=toolkit, verbose=True)
就这些了。我们现在准备向代理传递命令。
result = agent_executor.invoke({"input": "what is 1 + 1"})
让我们运行我们的脚本,看看代理的输出。
python3 math-agent.py
图片来自作者
由于我们在 AgentExecutor 上设置了verbose=True,我们可以看到代理所采取的每个操作步骤。它已经识别出我们应该调用“加法”工具,并使用所需的参数调用了“加法”工具,最终返回了结果。
这就是完整的源代码
import os
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI
from langchain.tools import BaseTool, StructuredTool, tool
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
os.environ["OPENAI_API_KEY"] = "sk-"
# setup the tools
@tool
def add(a: int, b: int) -> int:
"""Add two numbers."""
return a + b
@tool
def multiply(a: int, b: int) -> int:
"""Multiply two numbers."""
return a * b
@tool
def square(a) -> int:
"""Calculates the square of a number."""
a = int(a)
return a * a
prompt = ChatPromptTemplate.from_messages(
[
("system", """You are a mathematical assistant.
Use your tools to answer questions. If you do not have a tool to
answer the question, say so.
Return only the answers. e.g
Human: What is 1 + 1?
AI: 2
"""),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
]
)
# Choose the LLM that will drive the agent
llm = ChatOpenAI(model="gpt-3.5-turbo-1106", temperature=0)
# setup the toolkit
toolkit = [add, multiply, square]
# Construct the OpenAI Tools agent
agent = create_openai_tools_agent(llm, toolkit, prompt)
# Create an agent executor by passing in the agent and tools
agent_executor = AgentExecutor(agent=agent, tools=toolkit, verbose=True)
result = agent_executor.invoke({"input": "what is 1 + 1?"})
print(result['output'])
测试我们的代理
让我们向代理提几个问题,看看它的表现如何。
5 的平方是多少?
再次获得正确的结果,看到它确实使用了我们的平方工具
图片来自作者
5 的 6 次方是多少?
它采取了一个有趣的行动步骤。它首先使用平方工具。然后,使用该结果,尝试使用乘法工具多次来得到最终答案。坦率地说,最终答案 3125 是错误的,还需要再乘以 5 才能得到正确的答案。但有趣的是,看到代理尝试使用不同的工具,并通过多个步骤来尝试得到最终答案。
图片来自作者
1 减去 3 是多少?
我们没有减法工具。但它足够聪明,使用我们的加法工具,并将第二个值设置为-3。有时候它们真是太聪明和富有创意了,挺有趣的,甚至令人惊讶。
图片来自作者
64 的平方根是多少
作为最后的测试,如果我们要求它执行一个不在我们工具集中的数学运算会怎样?由于我们没有平方根工具,它不会尝试调用工具,而是直接使用 LLM 计算该值。
图片来自作者
我们的系统提示确实告诉它,如果没有正确的工具,它应该回答“不知道”,并且在测试过程中确实有时会这样做。一个改进的初始系统提示可能会有所帮助,至少在一定程度上能解决这个问题。
观察
基于对代理的使用,我注意到以下几点
-
当直接询问它具备工具可以回答的问题时,它在使用正确工具并返回正确答案方面相当一致。所以,从这个角度来看,它是相当可靠的。
-
如果问题稍微复杂一点,例如我们提到的“5 的 6 次方”问题,它并不总是返回正确的结果。
-
它有时可以仅仅使用 LLM 的强大能力来回答我们的问题,而无需调用我们的工具。
未来
代理和能够自主推理的程序是编程中的一种新范式,我认为它们将成为构建许多事物的主流方式。显然,LLM 的非确定性(即不是完全可预测的)意味着代理的结果也会受到影响,这使我们质疑在需要确保答案准确无误的任务中,我们能在多大程度上依赖它们。
也许随着技术的成熟,它们的结果将变得更加可预测,我们可能会为此开发一些解决方法。
我还看到代理类型的库和软件包开始成为一个趋势。类似于我们如何将第三方库和软件包安装到软件中,例如通过 Python 的 pip 包管理器或 Docker 镜像的 Docker Hub,我想我们可能会开始看到一个代理的库和包管理器开始被开发出来,代理在某一特定任务上变得非常擅长,然后我们也可以将它们作为软件包安装到我们的应用程序中。
的确,LangChain 的工具包库列在其集成页面上,这些工具包是社区构建的工具集,供大家使用,这可能是社区构建的代理类型库的一个早期示例。
结论
希望这篇文章能为你提供一些有用的入门指导,帮助你开始在 LangChain 中构建代理。
记住,代理基本上就是一个大脑(即 LLM)和一堆工具,它们可以利用这些工具来完成我们周围世界中的任务。
快乐编程!
如果你喜欢这篇文章,并希望随时了解我发布的关于使用 LangChain 和 AI 工具构建项目的未来文章,欢迎 在这里订阅 ,这样当新文章发布时,你会通过电子邮件收到通知
1611

被折叠的 条评论
为什么被折叠?



