ReAct智能体开发实战

哆啦A梦是很多人都熟悉的角色,包括我自己。 在成长过程中,我常常对他口袋里的许多小玩意感到惊讶,而且他知道何时使用它们。 随着大型语言模型 (LLM) 的发展趋势,你也可以构建一个具有相同行为方式的模型!

我们将构建一个智能代理,其含义在这里得到了很好的定义。 我们将重点关注一种名为 ReAct 的常见代理类型,它使用 LLM 作为其主要引擎来分解任务、推理并适当地使用特定的工具集。 你可以在本文中阅读有关 ReAct 框架的更多信息。

上一篇文章中,我提到过构建一个涉及食品评论的简单检索增强生成(RAG)工具。 现在,是时候更进一步,构建一个食品评论代理,它不仅能够检索相关的食品评论数据,还可以在必要时处理地理位置过滤。

0、实施计划

在定义要作为工具合并的功能和方法之前,我们将首先定义将用作我们代理的核心引擎的LLM。 此后,我们将它们添加到我们将初始化的代理中。 在此过程中将进行一些定制,以使结果更好。 该代理中包含的工具有:

  • 数据库检索器
  • 地理定位检索器
  • 具有地理位置过滤功能的数据库检索器

这篇精彩文章中的一些技术,例如元数据过滤和提示工程,将一路应用来改进检索过程的结果。 让我们开始!

1、定义我们的LLM

为了让事情变得简单、无忧,我们将使用 OpenAI GPT-3.5 Turbo 模型。 我将通过 Azure OpenAI 来利用它。 你还可以直接使用 OpenAI 甚至开源 LLM 的 API 服务。

import os
from langchain.chat_models import AzureChatOpenAI

# define env variables for AzureOpenAI model
os.environ["OPENAI_API_TYPE"] = "azure"
os.environ["OPENAI_API_BASE"] = "YOUR_ENDPOINT_HERE"
os.environ["OPENAI_API_KEY"] = "YOUR_API_KEY_HERE"
os.environ["OPENAI_API_VERSION"] = "2023-03-15-preview"

# Instatiate LLM
llm = AzureChatOpenAI(
    deployment_name='YOUR_DEPLOYMENT_NAME_HERE',
    temperature=0
)

2、创建工具

我们将首先创建一个包含食品评论及其嵌入的矢量存储。 嵌入对于代理检索搜索查询的相关结果非常重要。 由于 Deeplake 向量存储易于实现,因此将用于本实验。

使用与我上一篇文章中使用的类似的 CSV 数据源,我们可以创建矢量存储。 所需的一项定制是将美食场所位置的坐标添加到文档的元数据中。 这对于稍后使用地理位置过滤的工具很有用。

import pandas as pd
from langchain.vectorstores import DeepLake
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.document_loaders.csv_loader import CSVLoader

实例化 OpenAIEmbeddings 以用于从文档上下文创建嵌入:

# instantiate OpenAIEmbeddings to use to create embeddings from document context
embeddings = OpenAIEmbeddings(deployment="EMBEDDING_DEPLOYMENT_NAME", chunk_size=16)

实例化 CSV 加载器并加载食品评论,并将评论链接作为源:

# instantiate CSV loader and load food reviews with review link as source
loader = CSVLoader(file_path='final_data.csv', csv_args={
        "delimiter": ",",
}, encoding='utf-8', source_column='review_link')
data = loader.load()

使用纬度和经度值扩充数据的元数据,这是我们稍后的工具之一所需要的:

# augment data's metadata with lat and long values; this is needed for one of our tools later on
df = pd.read_csv("final_data.csv", index_col=False)

df_lat = df.lat.values.tolist()
df_long = df.long.values.tolist()

for lat, long, item in zip(df_lat, df_long, data):
    item.metadata["lat"] = lat
    item.metadata["long"] = long

创建deeplake数据库:

# create deeplake db
db = DeepLake(
    dataset_path="./my_deeplake/", embedding=embeddings, overwrite=True
)
db.add_documents(data)

2.1 数据库检索器

我们现在将创建我们的第一个工具:食品评论数据库检索器。 我将定义一个自定义检索器,以便过滤掉低于特定相似性阈值的文档,而不是简单地使用向量存储对象的 as_retriever() 方法。 Langchain 库中有一个 similarity_score_threshold 方法可用,但我已经尝试过,在撰写本文时它似乎有错误。

# defining a normal retriever
def search(query):
    search_results = db.similarity_search_with_score(k = 5,
                                            query = query,
                                            distance_metric = 'cos'
                                        )
    
    filtered_res = list(filter(lambda x: x[1] >= 0.8, search_results))
    filtered_res = [t[0] for t in filtered_res]

    return filtered_res

如果用户想根据文本查询检索相关的食品评论,这个简单的搜索功能现在可以充当我们的工具。

2.2 地理定位检索器

我们现在将继续创建我们的地理位置检索工具。 它需要接受一个地点的名称并返回其纬度和经度坐标。 由于 Foursquare API 是免费的 API 服务,我将使用它来搜索并返回坐标。 你可以通过上面的链接轻松免费获取 API 密钥。

import requests

API 调用的标头,其中包括 FQ API 密钥:

# headers for API call which includes FQ API key
headers = {
    "Accept": "application/json",
    "Authorization": "YOUR_API_KEY"
}

根据 foursquares 平台上最相关的搜索检索坐标的函数:

# function to retrieve coordinates based on most relevant search on foursquares platform
def get_coordinates(address):
    url = f'https://api.foursquare.com/v3/places/search?query={address.lower().strip().replace(" ", "%20")}&near=singapore'

    req = requests.request("GET", url, headers=headers)
    results_dict = eval(req.text)

    result_name = results_dict['results'][0]['name']
    result_geo = results_dict['results'][0]['geocodes']['main']
    
    return (result_geo['latitude'], result_geo['longitude']), result_name

2.3 具有地理位置过滤功能的数据库检索器

最后,我们可以创建具有地理位置过滤功能的数据库检索器。 用户有时希望根据某个位置找到相关的美食地点。 这意味着需要对数据库进行地理位置过滤。 根据该位置的坐标与数据库中所有地点的坐标之间的半正矢距离的计算,我们可以创建一个过滤器来删除那些超出设定距离的坐标。

from haversine import haversine as hs
import deeplake

创建一个过滤函数供deeplake使用。具有多个参数的过滤函数需要 deeplake.compute 装饰器:

# create a filter function for deeplake to use
# deeplake.compute decorator is needed for filter function with multiple arguments
@deeplake.compute
def filter_fn(x, coor):
    venue_lat = float(x['metadata'].data()['value']['lat'])
    venue_long = float(x['metadata'].data()['value']['long'])

    dist = hs(coor, (venue_lat, venue_long))
    return dist <= 0.8 # listings that are >800m away from target location is filtered out

创建一个函数来使用 deeplake.search 进行过滤的相似性搜索:

# create a function to do similarity search with filtering using deeplake.search
def search_with_filter(prompt_coor_string):

    # seperate search query and coordinates string input and create coordinate float tuple
    params_ls = prompt_coor_string.split(" | ")
    prompt = params_ls[0].replace('"', '')
    coor_ls = params_ls[1].split(", ")
    coor = (float(coor_ls[0]), float(coor_ls[1]))
    
    search_results = db.similarity_search_with_score(k = 5,
                                                query = prompt,
                                                filter = filter_fn(coor),
                                                distance_metric = 'cos'
                                            )
    
    # only keep reviews that have a similarity score of 0.8 and above
    filtered_res = list(filter(lambda x: x[1] >= 0.8, search_results))
    filtered_res = [t[0] for t in filtered_res]
    
    return filtered_res

根据下面的示例,你可以看到仅返回了Tanjong Pagar地铁站附近的相关 omakase 餐厅。

# an example on how the function will be used
# coordinates for Tanjong Pagar MRT Station is used
search_with_filter('"omakase courses" | 1.276525, 103.845725')
[Document(page_content="place_title: Kei Hachi\nplace_url: https://www.burpple.com/kei-hachi?bp_ref=%2Ff%2FWr8X_PCG\nfood_desc_title: Best Meal I've Had So Far\nfood_desc_body: Just some random photos of dishes served during their Kei Hachi Lunch Omakase ($128) because its too hard to choose specific favourites from their beautiful course meal. All of their food in the course are right on point and so darn delicious. Each food item is presented like a form of art and paired with the beautiful ambience, this is one hell of a treat. You get a huge variety of different food preparation styles and definitely a filling meal by the end of the course. Loved the hospitality of the chefs and the servers. Highly recommended if you love Japanese food and would like a good treat! Indeed the best meal I have ever had so far 😍😍\nreview_link: https://www.burpple.com/f/R6qr9qKk\npos_prob: 0.9908563\nnum_stars: 5\nvenue_price: ~$130/pax\nvenue_tag: Date Night\nFine Dining\nJapanese\nvenue_loc: https://www.google.com/maps/search/?api=1&query=1.279512,103.8415799\nlat: 1.279512\nlong: 103.8415799\nnearest_stations: Maxwell, Outram Park, Chinatown, Tanjong Pagar", metadata={'source': 'https://www.burpple.com/f/R6qr9qKk', 'row': 401, 'lat': 1.279512, 'long': 103.8415799}),
 Document(page_content="place_title: KYUU By Shunsui\nplace_url: https://www.burpple.com/kyuu-by-shunsui?bp_ref=%2Ff%2F9TUCRyhw\nfood_desc_title: Do come here if you love your ikura!\nfood_desc_body: Have heard about their unique omakase where you get enormous quantities of ikura and we were pretty impressed!\nThe standard omakase ($128) comprises of 9 courses and there is indeed a huge variety. From sashimi to tempera to grilled dishes, they have them all. These are just some of the dishes we had during the course and I must say I was very surprised by the beauty of the plating of their dishes!\nAs for the food quality, all dishes were of decent quality definitely. The sashimi pieces were decently fresh and the Wagyu Beef was pretty tender but not melt in the mouth though. Though I must admit I was not expecting much when I was served corn, but that is probably the sweetest, juiciest and most delicious corn I've had.\nIf you love your ikura, this is definitely a place to check out. To make it less painful on your wallet, you can get some discounts via @chopesg vouchers! Definitely a place to bring your loved ones for a celebration!\nreview_link: https://www.burpple.com/f/bAd77B8k\npos_prob: 0.8500501\nnum_stars: 4\nvenue_price: ~$130/pax\nvenue_tag: Late Night\nJapanese\nDinner With Drinks\nvenue_loc: https://www.google.com/maps/search/?api=1&query=1.2799799,103.841516\nlat: 1.2799799\nlong: 103.841516\nnearest_stations: Maxwell, Outram Park, Chinatown, Tanjong Pagar", metadata={'source': 'https://www.burpple.com/f/bAd77B8k', 'row': 207, 'lat': 1.2799799, 'long': 103.841516}),
 Document(page_content="place_title: Teppei Japanese Restaurant (Orchid Hotel)\nplace_url: https://www.burpple.com/teppei-japanese-restaurant?bp_ref=%2Ff%2FSjq7Uauy\nfood_desc_title: Very Good Omakase, Worth The Price\nfood_desc_body: One will find a gastronomical experience here definitely, as you will experience so many different flavour profiles from their dinner omakase ($100). We realised that we really cannot pick a favourite among the dishes served as most were so darn good. All seafood ingredients served are really fresh, and you really don't need the soya sauce because they are all so flavourful! There is a total of about 15-17 courses, and although most were small bites, they were more than enough to make us full. In fact many in the restaurant were saying that they were already filled towards the end of the course! Really felt like it's worth the price 😊\nreview_link: https://www.burpple.com/f/PWJbmM5Z\npos_prob: 0.95687157\nnum_stars: 5\nvenue_price: ~$100/pax\nvenue_tag: Sushi\nChirashi\nSeafood\nDate Night\nJapanese\nvenue_loc: https://www.google.com/maps/search/?api=1&query=1.276993,103.843891\nlat: 1.276993\nlong: 103.843891\nnearest_stations: Tanjong Pagar, Maxwell, Shenton Way, Outram Park, Chinatown", metadata={'source': 'https://www.burpple.com/f/PWJbmM5Z', 'row': 413, 'lat': 1.276993, 'long': 103.843891})]

3、创建代理

现在我们已经定义了用作代理中工具的函数和方法,我们可以将它们放在一起。 为了初始化代理可以使用的工具集,需要进一步为每个工具定义名称和描述。 两者对于 ReAct 代理都极其重要,因为 LLM 严重依赖它们来了解每个工具的用途以及何时使用。 正确地构建它是一个迭代过程,需要使用不同的测试用例来改进它们。

请注意,我为需要超过 1 个参数的函数定义了输入格式。 如果我定义 STRUCTURED_CHAT_ZERO_SHOT_REACT_DESCRIPTION 代理类型,则没有必要这样做,因为它允许多个输入,但在撰写本文时似乎存在错误。 如果它适合你,请随意尝试!

# initialise the 3 tools to be added to the agent.
# Note: that the name and description of the tool are very important for a reAct agent type, as it relies on them heavily to decide on when to use the tools
tools = [
    Tool(
        name="Database Search",
        func=search,
        description="Useful for querying food review data. Input should be the user text query to find relevant food review articles."
    ),
    Tool(
        name="Database Search with Distance Filter",
        func=search_with_filter,
        description="Useful for searching food places near specific locations. This function is to be used after Geolocation Search. The input string should be a text search query and a set of latitude and longitude coordinates seperated by |."  
    ),
    Tool(
        name="Geolocation Search",
        func=get_coordinates,
        description="Useful to retrieve latitude and longitude coordinates and name of place from a defined location. This is used before Database Search with Distance Filter. Input should be the place location given by the user. Output should be a tuple containing latitude and longitude coordinates and name of place."
    )
]

我意识到默认代理存在某些问题,它有时可能会使用自己的知识来回答一些问题,并且需要将输出格式标准化为我上一篇文章中讨论的格式。 这些提示可以通过编辑代理的提示模板来解决。 你可以通过编辑其提示符前缀来完成此操作。

# Define prefix of LLM agent
# Note: this is where you can prompt engineer the template for the agent, so that the agent understands clearly on what tasks it aims to do and how it should format its answers.
PREFIX = """
Answer the following questions as best you can. You have access to the following tools below that can help to find food reviews. Only answer questions that are related to the use of the tools given.
If the question is unrelated, reject the question. If answer is not found, just say that you do not have any relevant results. Return the final answer if you think you are ready. In your final answer, you MUST list the reviews in the following format:

Here are my recommendations:
🏠 [Name of place]
<i>[venue tags]</i>
✨ Avg Rating: [Rating of venue]
💸 Price: [Estimated price of dining at venue] (this is optional. If not found or not clear, use a dash instead.)
📍 <a href=[Location of venue] ></a>
📝 Reviews:
[list of review_link, seperated by linespace] (Use this format: 1. <a href=[review_link] >[food_desc_title text]</a>)

If you cannot find any reviews to respond to the user, just say that you don't know.
"""

现在剩下要做的就是使用正确的参数初始化代理并对其进行测试:

# Construct the agent
# Note: sometimes the agent will hallucinate and not format its answer properly. Setting handle_parsing_error to True will allow the agent to check its own formatting.
agent = initialize_agent(
    tools,
    llm,
    agent=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
    verbose=True,
    handle_parsing_errors=True,
    agent_kwargs={
        'prefix': PREFIX
    }
)
示例:你知道Tanjong Pagar地铁站附近有哪些提供 omakase 课程的餐厅吗?

使用上面的具体示例详细运行代理,你可以看到 LLM 尝试找出所提供的任务所需的步骤,并尝试识别每个步骤所需的工具。

在得出最终答案之前LLM的思考过程。

输出正如预期的那样,丹戎巴葛地铁站附近的餐厅提供我以前去过的 omakase 课程。 输出格式在我在前缀模板中指定的 HTML 格式中也是正确的。

Here are my recommendations:
🏠 Kei Hachi
<i>Date Night, Fine Dining, Japanese</i>
✨ Avg Rating: 5
💸 Price: ~$130/pax
📍 <a href=https://www.google.com/maps/search/?api=1&query=1.279512,103.8415799 ></a>
📝 Reviews:
1. <a href=https://www.burpple.com/f/R6qr9qKk >Best Meal I've Had So Far</a>

🏠 KYUU By Shunsui
<i>Late Night, Japanese, Dinner With Drinks</i>
✨ Avg Rating: 4
💸 Price: ~$130/pax
📍 <a href=https://www.google.com/maps/search/?api=1&query=1.2799799,103.841516 ></a>
📝 Reviews:
1. <a href=https://www.burpple.com/f/bAd77B8k >Do come here if you love your ikura!</a>

🏠 Teppei Japanese Restaurant (Orchid Hotel)
<i>Sushi, Chirashi, Seafood, Date Night, Japanese</i>
✨ Avg Rating: 5
💸 Price: ~$100/pax
📍 <a href=https://www.google.com/maps/search/?api=1&query=1.276993,103.843891 ></a>
📝 Reviews:
1. <a href=https://www.burpple.com/f/PWJbmM5Z >Very Good Omakase, Worth The Price</a>

4、结束语

从上面的演练中,你可以看到LLM如何分解任务并适当地使用其武器库中的工具,就像《哆啦A梦》在动画系列中所做的那样。 我相信代理可以在很多方面提供帮助,例如将多工具和多模式平台压缩为对话机器人,用户可以简单地上传数据,代理可以根据数据类型和数据来决定使用哪些工具。 它对用户提供的任务的理解。

ReAct 类型的代理只是我们可以创建和探索的多种代理类型之一。 我见过能够在游戏中学习并茁壮成长的代理,以及具有不同角色的多个代理在模拟公司中一起工作。 有很多东西值得探索,我希望我能启发你。 快乐编码!


原文链接:Build your own digital Doraemon using ReAct LLM Agents

BimAnt翻译整理,转载请标明出处