
从文本到功能代码:使用自适应提示工程为 Rag 增效--权威指南!
从结构化表中提取相关数据
从结构化表中提取相关数据需要的不仅仅是标准的 RAG 方法。我们通过索引术语建议、上下文行检索和动态 few-shot 示例增强了提示工程,以生成可靠的 Pandas 查询,使我们的系统既准确又高效。
共同作者:Michael Leshchinsky
Clalit 是以色列最大的健康维护组织——它既是超过 450 万名成员的保险公司,也是医疗服务提供者。正如您所料,这样一个庞大的组织拥有大量有用的信息,应该对所有客户和员工可用——医疗提供者名单、患者的资格、医疗测试和程序的信息等等。不幸的是,这些信息分散在多个来源和系统中,使最终用户很难获取他们所寻找的确切信息。
为了解决这个问题,我们决定构建一个多代理 RAG 系统,能够理解需要查询的知识领域,从一个或多个来源获取相关上下文,并根据这些上下文为用户提供正确和完整的答案。
每个代理专注于特定领域,并且本身就是一个小型 RAG,因此它可以检索上下文并回答其领域的问题。协调代理理解用户的问题,并决定应该向哪个代理发送请求。然后,它聚合所有相关代理的答案,并为用户编制一个答案。
解决方案的一般架构。作者提供的图像。
至少,这就是最初的想法——我们很快发现并非所有数据源都是相同的,有些代理应该与人们所称的经典 RAG 完全不同。
在本文中,我们将重点关注一个这样的用例——医疗提供者名单,也称为 服务手册。服务手册是一个大约有 23K 行的表格,每一行代表一个医疗提供者。每个提供者的信息包括其地址和联系信息、提供的职业和服务(包括员工姓名)、营业时间以及关于诊所可达性和评论的一些额外自由文本评论。
以下是表中的一些伪示例(由于列数较多,垂直显示)。
我们最初的方法是将每一行转换为文本文档,进行索引,然后使用简单的 RAG 进行提取。然而,我们很快发现这种方法有几个局限性:
-
用户可能期望得到包含多行的答案。例如,考虑这个问题:“特拉维夫有哪些药店?” 我们的 RAG 应该检索多少个文档?如果用户明确规定期望多少行呢?
-
对于检索器来说,区分不同字段可能极其困难——某个城市的诊所可能以另一个城市的名字命名(例如,耶路撒冷诊所位于特拉维夫的耶路撒冷路上)。
-
作为人类,我们可能不会“文本扫描”一个表格来提取信息。相反,我们更愿意根据规则过滤表格。我们的应用程序没有理由表现得不同。
相反,我们决定朝另一个方向发展——要求 LLM 将用户的问题转换为提取相关行的计算机代码。
这种方法受到 llama-index 的 Pandas 查询引擎 的启发。简而言之,LLM 的提示由用户的查询、_df.head()
(用于教导 LLM 表格的结构)和一些一般指令构成。
query = """
## Your job is to convert user's questions to a single line of Pandas code that will filter `df` and answer the user's query.
Here are the top five rows from df:
## {df}
- Your output will be a single code line with no additional text.
- The output must include: the clinic/center name, type, address, phone number(s), additional remarks, website, and all columns including the answer or that were filtered.
## - Think carefully about each search term!
USER'S QUESTION: {user_query}
PANDAS CODE:
"""
response = llm.complete(query.format(df=df.head(),
user_query=user_query)
)
try:
result_df = eval(response.text)
except:
result_df = pd.DataFrame()
听起来很简单,对吧?实际上,我们遇到了残酷的现实,在我们生成的大多数代码中,Pandas 出现了一个或另一个原因的错误,因此主要工作才刚刚开始。
我们能够识别生成代码失败的三个主要原因,并使用几种动态提示工程技术来解决它们:
-
通过**添加“thesaurus”**来修复与 不精确术语 相关的问题,这些术语可以用来过滤每一列。
-
向 LLM 提供来自 df 的相关行,而不是由
_df.head()
提取的任意行。 -
使用定制的代码示例进行动态 few-shotting,以帮助 LLM 生成正确的 Pandas 代码。
将同义词添加到提示中
在许多情况下,用户的问题可能与表格“期望”的内容不完全一致。例如,用户可能会询问在 特拉维夫 的药店,但表格中的术语是 特拉维夫-雅法。在另一个情况下,用户可能在寻找 眼科医生,而不是 眼科,或者在寻找 心脏病专家 而不是 心脏病学。对于 LLM 来说,编写能够涵盖所有这些情况的代码将是困难的。相反,检索正确的术语并将其作为建议包含在提示中可能更有益。
正如您所想,服务手册中每一列的术语数量是有限的——诊所类型可能是医院诊所、私人诊所、初级保健诊所,等等。城市名称、医疗职业和服务以及医疗人员的数量也是有限的。我们使用的解决方案是创建所有术语的列表(在每个字段下),将每个术语作为一个文档,然后将其作为向量进行索引。
然后,使用仅检索的引擎,我们为每个搜索术语提取大约 3 个项目,并将这些项目包含在提示中。例如,如果用户的问题是“特拉维夫有哪些药店可用?”,则可能检索到以下术语:
- 诊所类型:药店;初级保健诊所;医院诊所
- 城市:特拉维夫-雅法,特尔谢瓦,法拉迪斯
- 职业和服务:药店,肛肠学,儿科
- …
检索到的术语包括我们正在寻找的真实术语(药店,特拉维夫-雅法),以及一些可能听起来相似的无关术语(特尔谢瓦,肛肠学)。所有这些术语将作为建议包含在提示中,我们期望 LLM 能够筛选出可能有用的术语。
from llama_index.core import VectorStoreIndex, Document
unique_cities = df['city'].unique()
cities_documents = [Document(text=city) for city in unique_cities]
cities_index = VectorStoreIndex.from_documents(documents=cities_documents)
cities_retriever = cities_index.as_retriever()
suggest_cities = ", ".join([doc.text for doc in cities_retriever.retrieve(user_query)])
query = """您的任务是将用户的问题转换为一行 Pandas 代码,该代码将筛选 df
并回答用户的查询。
以下是 df 的前五行:
{df}
这是您可能正在寻找的城市: {suggest_cities}
- 您的输出将是一行代码,没有额外的文本。
- 输出必须包括:诊所/中心名称、类型、地址、电话号码、附加备注、网站,以及所有包含答案或被筛选的列。
- 仔细考虑每个搜索词!
用户的问题: {user_query}
PANDAS 代码:
"""
response = llm.complete(query.format(df=df.head(),
suggest_cities=suggest_cities,
user_query=user_query)
)
try:
result_df = eval(response.text)
except:
result_df = pd.DataFrame()
选择相关行示例以包含在提示中
默认情况下,PandasQueryEngine通过将_df.head()_嵌入到提示中来包含_df_的前几行,以便让LLM学习表的结构。然而,这五行很可能与用户的问题无关。想象一下,我们可以明智地选择哪些行包含在提示中,以便LLM不仅能学习表的结构,还能看到与当前任务相关的示例。
为了实现这个想法,我们使用了上述描述的初始方法:
- 我们将每一行转换为文本,并将其作为单独的Document进行索引。
- 然后,我们使用检索器根据用户的query提取五个最相关的行,并将它们包含在提示中的_df_示例中。
- 我们在这个过程中学到的一个重要教训是包含一些随机的、不相关的示例,以便LLM也能看到负面示例,并知道必须区分它们。
这里有一些代码示例:
rows = df.fillna('').apply(lambda x: ", ".join(x), axis=1).to_dict()
rows_documents = [Document(text=v, metadata={'index_number': k}) for k, v in rows.items()]
rows_index = VectorStoreIndex.from_documents(documents=rows_documents)
rows_retriever = rows_index.as_retriever(top_k_similarity=5)
retrieved_indices = rows_retriever.retrieve(user_query)
relevant_indices = [i.metadata['index_number'] for i in retrieved_indices]
query = """您的任务是将用户的问题转换为一行 Pandas 代码,该代码将过滤 df
并回答用户的查询。
以下是 df 中的前五行:
{df}
这些是您最可能寻找的城市:{suggest_cities}
- 您的输出将是一个没有额外文本的单行代码。
- 输出必须包括:诊所/中心名称、类型、地址、电话号码、附加备注、网站,以及所有包含答案或已过滤的列。
- 仔细考虑每个搜索词!
示例:
{relevant_example}
用户的问题:{user_query} PANDAS 代码: """
response = llm.complete(query.format(df=pd.concat([df.head(), df.loc[relevant_indices]]), suggest_cities=suggest_cities, user_query=user_query) ) try: result_df = eval(response.text) except: result_df = pd.DataFrame()
添加量身定制的代码示例到提示中(动态少量示例)
考虑以下用户的问题:H&C 诊所允许服务动物吗?
生成的代码是:
df[
(df['clinic_name'].str.contains('H&C')) &
(df['accessibility'].str.contains('service animals'))
][['clinic_name', 'address', 'phone number', 'accessibility']]
乍一看,这段代码看起来是正确的。但是……用户并不想在 accessibility 列上进行 过滤 — 而是想检查其内容!
在这个过程中,我们很早就意识到,采用少量示例的方法,将示例问题和代码答案包含在提示中,可能会解决这个问题。然而,我们意识到我们可以想到的不同示例实在是太多了,每个示例都强调不同的概念。
我们的解决方案是创建一个不同示例的列表,并使用检索器来包含与当前用户问题最相似的示例。每个示例都是一个字典,其中的键是:
- QUESTION:潜在用户的问题
- CODE:请求的输出代码,正如我们所写的那样
- EXPLANATION:文本解释,强调我们希望 LLM 在生成代码时考虑的概念。
例如:
{
'QUESTION': 'H&C 诊所允许服务动物吗?',
'CODE': "df[df['clinic_name'].str.contains('H&C')][['clinic_name', 'address', 'phone number', 'accessibility']]",
'EXPLANATION': "当询问某个诊所是否存在服务时 - 你不应在相关列上进行过滤。相反,你应该返回给用户以供检查!"
}
现在,每当我们遇到一个新的概念时,我们可以扩展这个 示例库,让我们的系统知道如何处理:
examples_documents = [
Document(text=ex['QUESTION'],
metadata={k: v for k, v in ex.items()})
for ex in examples_book
]
examples_index = VectorStoreIndex.from_documents(documents=examples_documents)
examples_retriever = examples_index.as_retriever(top_k_similarity=1)
relevant_example = examples_retriever.retrieve(user_query)
relevant_example = f"""问题: {relevant_example.text}
代码: {relevant_example.metadata['CODE']}
解释: {relevant_example.metadata['EXPLANATION']}
"""
query = """您的任务是将用户的问题转换为一行 Pandas 代码,该代码将过滤 df
并回答用户的查询。
以下是 df 的前五行:
{df}
这些是您可能正在寻找的城市:{suggest_cities}
- 您的输出将是没有额外文本的单行代码。
- 输出必须包括:诊所/中心名称、类型、地址、电话号码、附加备注、网站,以及所有包括答案或被过滤的列。
- 仔细考虑每个搜索词!
示例:
{relevant_example}
用户的问题:{user_query} PANDAS 代码: """
response = llm.complete(query.format(df=pd.concat([df.head(), df.loc[relevant_indices]]), suggest_cities=suggest_cities, relevant_example=relevant_example, user_query=user_query) ) try: result_df = eval(response.text) except: result_df = pd.DataFrame()
摘要
在本文中,我们尝试描述我们用来创建更具体、动态提示以从我们的表中提取数据的几种启发式方法。通过使用预先索引的数据和检索器,我们可以丰富我们的提示,并使其根据用户当前的问题进行定制。值得一提的是,尽管这使得代理变得更加复杂,但运行时间仍然相对较低,因为检索器通常很快(至少与文本生成器相比)。这是完整流程的示例:
查询引擎的完整图形描述。作者提供的图片。