RAG增强指南:通过自适应提示工程从文本到代码

2025年02月11日 由 alex 发表 1398 0

Clalit是以色列最大的健康管理组织,它同时为以色列超过450万名会员提供保险和医疗服务。正如你所预料的,像这样一个庞大的组织拥有大量对其所有客户和员工都有用的信息——包括医疗服务提供者名单、患者资格信息、医疗检查和程序的相关信息等等。不幸的是,这些信息分散在多个来源和系统中,使得终端用户很难准确找到他们所需的信息。


为了解决这个问题,我们决定构建一个多智能体检索增强生成(RAG)系统,该系统能够理解需要查询的知识领域,从一个或多个来源获取相关上下文,并基于这些上下文为用户提供正确且完整的答案。


每个智能体专注于一个特定的领域,并且本身就是一个小型的RAG系统,因此它能够检索上下文并回答其领域内的问题。一个协调智能体负责理解用户的问题,并决定应该向哪个(或哪些)智能体发出请求。然后,它汇总所有相关智能体的答案,并为用户编译出一个答案。


2


至少,这是最初的想法——但很快我们就发现,并非所有数据源都是等同的,有些智能体应该与人们所称的经典检索增强生成(RAG)系统截然不同。


在本文中,我们将重点讨论一个这样的用例——医疗服务提供者列表,也称为服务手册。服务手册是一个包含约2.3万行的表格,每一行代表一个医疗服务提供者。每个提供者的信息包括其地址和联系方式、提供的职业和服务(包括员工姓名)、营业时间,以及一些关于诊所无障碍设施和评论的额外自由文本备注。


以下是表格中的一些伪示例(由于列数较多,因此以纵向方式显示)。


3


我们最初的方法是将每一行转换为一个文本文档,对其进行索引,然后使用一个简单的检索增强生成(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 因为各种原因抛出了错误,所以真正的工作才刚刚开始。


我们确定了生成代码失败的三个主要原因,并使用了几种动态的提示工程技术来解决它们:


  1. 通过添加可用于过滤每列的术语“同义词库”,解决术语不精确的问题。
  2. 为大型语言模型(LLM)提供相关行(来自df),而不是df.head()提取的任意行。
  3. 使用定制的代码示例进行动态少样本学习,帮助LLM生成正确的pandas代码。


在提示中添加同义词库

在许多情况下,用户的问题可能并不是表格“期望”的精确表述。例如,用户可能询问特拉维夫的药店,但表格中的术语是特拉维夫-雅法。在另一种情况下,用户可能寻找的是眼科医生(oftalmologist),而表格中的术语是眼科专家(ophthalmologist),或者用户寻找的是心脏病专家(cardiologist),而表格中的术语是心脏病学(cardiology)。对于LLM来说,编写能够覆盖所有这些情况的代码将非常困难。相反,可以检索正确的术语,并将其作为建议包含在提示中。


正如你可能想象的那样,服务手册中每列的术语数量是有限的——诊所类型可能是医院诊所、私人诊所、初级保健诊所等。城市名称、医疗职业和服务以及医务人员的数量都是有限的。我们采用的解决方案是创建所有术语的列表(在每个字段下),将每个术语作为文档保存,然后将其索引为向量。


然后,使用仅检索引擎,我们为每个搜索术语提取约3个相关项,并将其包含在提示中。例如,如果用户的问题是“特拉维夫有哪些药店?”,可能会检索到以下术语:


  • 诊所类型:药店;初级保健诊所;医院诊所
  • 城市:特拉维夫-雅法,特尔-舍瓦,法拉迪斯
  • 职业和服务:药店,肛肠科,儿科
  • ……


检索到的术语包括我们正在寻找的真实术语(药店,特拉维夫-雅法),以及一些听起来相似的无关术语(特尔-舍瓦,肛肠科)。所有这些术语都将作为建议包含在提示中,我们期望LLM能够筛选出可能有用的术语。


from llama_index.core import VectorStoreIndex, Document
# Indexing all city names
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()
# Retrieving suggested cities with the user's query
suggest_cities = ", ".join([doc.text for doc in cities_retriever.retrieve(user_query)])
# Revised query
# Note how it now includes suggestions for relevant cities. 
# In a similar manner, we can add suggestions for clinic types, medical professions, etc.
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}
---
This are the most likely cities you're looking for: {suggest_cities}
---
- 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:
"""
# Re-filtering the table using the new query
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不仅将学习表格的结构,还将看到与当前任务相关的示例。


为了实现这一想法,我们使用了上面描述的初步方法:

  • 我们将每一行转换为文本,并将其索引为单独的文档。
  • 然后,我们使用检索器提取与用户查询最相关的五行,并将它们包含在提示中的 df 示例里。
  • 在这个过程中,我们学到的一个重要教训是,要包含一些随机的、不相关的示例,这样LLM也能看到负面示例,并知道它必须对这些示例进行区分。


以下是一些代码示例:


# Indexing and retrieving suggested city names and other fields, as shown above
...
# We convert each row to a document. 
# Note how we keep the index of each row - we will use it later.
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()]
# Index all examples
rows_index = VectorStoreIndex.from_documents(documents=rows_documents)
rows_retriever = rows_index.as_retriever(top_k_similarity=5)
# Generate example df to include in prompt
retrieved_indices = rows_retriever.retrieve(user_query)
relevant_indices = [i.metadata['index_number'] for i in retrieved_indices]
# Revised query
# This time we also add an example to the prompt
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}
---
This are the most likely cities you're looking for: {suggest_cities}
---
- 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!
---
Example:
{relevant_example}
---
USER'S QUESTION: {user_query}
PANDAS CODE:
"""
# Re-filtering the table using the new query
# Note how we include both df.head() (as random rows) and the top five relevant rows extracted 
# from the retriever
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', 'accessability']]


乍一看,代码似乎没问题。但是……用户并不想根据“accessibility”(无障碍设施)列进行过滤,而是想检查其内容!


在这个过程中,我们很早就发现,在提示中包含一个示例问题和代码答案的少样本方法可能解决这个问题。然而,我们意识到可以想到的示例实在太多了,每个示例都强调了一个不同的概念。


我们的解决方案是创建一个不同示例的列表,并使用检索器包含与当前用户问题最相似的示例。每个示例都是一个字典,其中的键是:


  • QUESTION(问题):一个潜在用户的问题
  • CODE(代码):我们期望生成的请求输出代码
  • EXPLANATION(解释):文本解释,强调我们希望LLM在生成代码时考虑的概念。


例如:


{
'QUESTION': 'Does H&C clinic allows service animals?',
'CODE': "df[df['clinic_name'].str.contains('H&C')][['clinic_name', 'address', 'phone number', 'accessability']]""",
'EXPLANATION': "When asked about whether a service exists or not in a certain clinic - you're not expected to filter upon the related column. Rather, you should return it for the user to inspect!"
}


现在,每当我们遇到一个新概念,希望系统能够学会如何处理时,我们都可以扩展这个示例库。


# Indexing and retrieving suggested city names, as seen before
...
# Indexing and retrieveing top five relvant rows
...
# Index all examples
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=cities_documents)
examples_retriever = examples_index.as_retriever(top_k_similarity=1)
# Retrieve relevant example
relevant_example = examples_retriever.retrieve(user_query)
relevant_example = f"""Question: {relevant_example.text}
Code: {relevant_example.metadata['CODE']}
Explanation: {relevant_example.metadata['EXPLANATION']}
"""
# Revised query
# This time we also add an example to the prompt
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}
---
This are the most likely cities you're looking for: {suggest_cities}
---
- 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!
---
Example:
{relevant_example}
---
USER'S QUESTION: {user_query}
PANDAS CODE:
"""
# Re-filtering the table using the new query
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()


总结

在本文中,我们尝试描述了几种启发式方法,用于创建更具体、更动态的提示,以便从表格中提取数据。通过使用预先索引的数据和检索器,我们可以丰富提示内容,并使其根据用户当前的问题进行定制。值得一提的是,尽管这增加了代理的复杂性,但运行时间仍然相对较低,因为检索器通常速度很快(至少与文本生成器相比是这样)。以下是完整流程的示意图:


4


文章来源:https://medium.com/towards-data-science/how-we-implemented-rag-for-tabular-data-c7fc87baf783
欢迎关注ATYUN官方公众号
商务合作及内容投稿请联系邮箱:bd@atyun.com
评论 登录
热门职位
Maluuba
20000~40000/月
Cisco
25000~30000/月 深圳市
PilotAILabs
30000~60000/年 深圳市
写评论取消
回复取消