用LangChain、Google Maps API和Gradio构建智能行程建议器(第一部分)

使用LangChain、Google Maps API和Gradio打造智能行程建议器(第一部分)

学习如何构建一个可能激发你下一次公路旅行灵感的应用程序

本文是一个三部分系列的第一部分,我们将使用OpenAI和Google API构建一款旅行行程建议应用程序,并在使用gradio生成的简单UI中显示它。在本部分中,我们首先讨论了这个项目的提示工程。只想看代码吗?在这里找到它 here

1. 动机

自2022年底推出ChatGPT以来,对大型语言模型(LLMs)以及它们在聊天机器人和搜索引擎等面向消费者的产品中的应用引起了极大的兴趣。不到一年的时间,我们已经可以访问各种开源LLMs,这些模型来自模型中心如Hugging Face、模型托管服务如Lamini以及付费API如OpenAI和PaLM。看到这个领域的发展如此迅速,新的工具和开发范式似乎每几周就会出现,这既令人兴奋又有些让人不知所措。

在这里,我们将只抽样使用这个工具中的一小部分来构建一个有用的应用程序,可以帮助我们进行旅行计划。在计划度假时,往往很好从一些去过那儿的人那里得到建议,更好的是将这些建议呈现在地图上。如果没有这些建议,有时我会简单地在我想要访问的地区浏览谷歌地图,并随机选几个看起来有趣的地方。也许这个过程很有趣,但它是低效的并且可能会错过一些东西。如果有一个工具可以根据一些高级别的偏好为你提供一些建议,岂不是很好?

这正是我们要尝试构建的:一个可以根据一些高级别偏好提供旅行行程建议的系统,例如“我有3天时间探索旧金山,我喜欢艺术博物馆”。Google搜索的生成式AI功能和ChatGPT已经可以为这样的查询产生创造性结果,但我们想进一步提供一个实际的行程安排,并提供旅行时间和一个漂亮的地图来帮助用户定位。

我们将构建的是一个生成旅行建议并显示路线和LLM提供的中间点的基本地图的系统

目标更多是熟悉构建这样一个服务所需的工具,而不是实际部署应用程序,但在此过程中,我们将学到一些关于提示工程、LLM与LangChain的协调方法、使用Google地图API提取方向以及使用leafmap和gradio显示结果的知识。这些工具让你快速构建这样的系统的能力令人惊讶,但始终面临的真正挑战在于评估和处理极端情况。我们将构建的工具远非完美,如果有人有兴趣帮助我进一步开发它,那将是太棒了。

2. 提示策略

本项目将使用OpenAI和Google PaLM API。您可以通过在这里创建账号来获取API密钥:herehere。目前,Google API仅具有有限的普遍可用性,并有一个等待列表,但只需要几天就可以获得访问权限。

使用dotenv是一种避免将API密钥复制粘贴到开发环境中的简单方法。在创建一个包含以下行的.env文件后

OPENAI_API_KEY = {你的OpenAI密钥}GOOGLE_PALM_API_KEY = {你的Google PaLM API密钥}

我们可以使用这个函数来加载变量,为后续使用 LangChain 准备好。

from dotenv import load_dotenv
from pathlib import Path

def load_secets():
    load_dotenv()
    env_path = Path(".") / ".env"
    load_dotenv(dotenv_path=env_path)
    open_ai_key = os.getenv("OPENAI_API_KEY")
    google_palm_key = os.getenv("GOOGLE_PALM_API_KEY")
    return {
        "OPENAI_API_KEY": open_ai_key,
        "GOOGLE_PALM_API_KEY": google_palm_key,
    }

现在,我们应该如何为旅行代理服务设计提示呢?用户可以自由输入任何文本,所以我们首先希望能够确定他们的查询是否有效。我们肯定想要标记任何包含有害内容的查询,例如那些具有恶意意图的行程请求。

我们还想要过滤掉与旅行无关的问题——毫无疑问,LLM可能能够回答这样的问题,但它们超出了这个项目的范围。最后,我们还想要识别出不合理的请求,比如“我想飞到月球”或“我想从纽约到东京进行为期三天的公路旅行”。对于这样一个不合理的请求,如果模型能够解释为什么它是不合理的,并提出一个能够帮助改进的修改建议,那就太好了。

一旦请求经过验证,我们可以继续提供一个建议的行程,理想情况下应该包含每个途径点的具体地址,以便可以将它们发送到像谷歌地图这样的映射或导航 API。

行程应该是易于阅读的,包含足够的细节,使用户能够将其作为独立建议来使用。像 ChatGPT 这样的大型、针对指示的 LLM 似乎非常擅长提供这样的响应,但我们需要确保途径点地址以一致的方式提取。

所以这里有三个不同的阶段:

  1. 验证查询
  2. 生成行程
  3. 以谷歌地图 API 可理解的格式提取途径点

可能设计一个能够一次完成这三个步骤的提示,但为了方便调试,我们将它们分成三个 LLM 调用,每个部分一个。

幸运的是,LangChain 的 PydanticOutputParser 在这里确实很有帮助,它提供了一组预制的提示,鼓励 LLM 以符合输出模式的方式格式化它们的响应。

3. 验证提示

让我们看一下验证提示,我们可以将其包装在一个模板类中,以便更容易包含和迭代不同的版本。

from langchain.prompts.chat import (
    ChatPromptTemplate,
    SystemMessagePromptTemplate,
    HumanMessagePromptTemplate,
)
from langchain.output_parsers import PydanticOutputParser
from pydantic import BaseModel, Field

class Validation(BaseModel):
    plan_is_valid: str = Field(
        description="如果计划可行,则为“yes”,否则为“no”"
    )
    updated_request: str = Field(description="您对计划的更新")

class ValidationTemplate(object):
    def __init__(self):
        self.system_template = """    
        你是一位旅行代理,帮助用户制定激动人心的旅行计划。
        用户的请求将以四个井号表示。确定用户的请求是否在其设置的约束条件下是合理和可实现的。
        一个有效的请求应包含以下内容:
        - 一个起点和终点位置
        - 按照起点和终点位置合理的行程持续时间
        - 其他一些细节,如用户的兴趣和/或首选的交通方式
        任何包含潜在有害活动的请求都是无效的,无论其他细节如何。
        如果请求无效,请将 plan_is_valid = 0,并在保持修订的请求少于100个单词的情况下使用您的旅行专业知识来更新它。
        如果请求看起来合理,请将 plan_is_valid = 1,并且不要修订请求。
        {format_instructions}    
        """
        self.human_template = """    
        ####{query}####    
        """
        self.parser = PydanticOutputParser(pydantic_object=Validation)
        self.system_message_prompt = SystemMessagePromptTemplate.from_template(
            self.system_template,
            partial_variables={
                "format_instructions": self.parser.get_format_instructions()
            },
        )
        self.human_message_prompt = HumanMessagePromptTemplate.from_template(
            self.human_template, input_variables=["query"]
        )
        self.chat_prompt = ChatPromptTemplate.from_messages(
            [self.system_message_prompt, self.human_message_prompt]
        )

我们的 Validation 类包含查询的输出模式定义,它将是一个具有两个键 plan_is_validupdated_request 的 JSON 对象。在 ValidationTemplate 内部,我们使用 LangChain 的有用模板类来构建我们的提示,并创建一个带有 PydanicOutputParser 的解析器对象。这将把 Validation 中的 Pydantic 代码转换成一组指令,可与查询一起传递给 LLM。然后,我们可以在系统模板中引用这些格式指令。每次调用 API 时,我们都希望将 system_message_prompthuman_message_prompt 都发送到 LLM,这就是为什么我们将它们一起打包到 chat_prompt 中。

由于这不是一个真正的聊天机器人应用程序(尽管可以将其改造成一个!),我们可以将系统和人类模板放在同一个字符串中,然后得到相同的响应。

现在,我们可以创建一个使用 LangChain 调用 LLM API 的 Agent 类,其中使用了上述定义的模板。这里我们使用的是 ChatOpenAI,但您也可以用 GooglePalm 替换它,如果您更喜欢。

请注意,尽管我们只对 LLM 进行了单一调用,但在这里我们还是使用了来自 Langchain 的 LLMChainSequentialChain。这可能有些多余,但如果我们想在验证链运行之前添加另一个调用,比如对 OpenAI moderation API 的调用,它可能会有帮助。

import openaiimport loggingimport time# for Palmfrom langchain.llms import GooglePalm# for OpenAIfrom langchain.chat_models import ChatOpenAIfrom langchain.chains import LLMChain, SequentialChainlogging.basicConfig(level=logging.INFO)class Agent(object):    def __init__(        self,        open_ai_api_key,        model="gpt-3.5-turbo",        temperature=0,        debug=True,    ):        self.logger = logging.getLogger(__name__)        self.logger.setLevel(logging.INFO)        self._openai_key = open_ai_api_key        self.chat_model = ChatOpenAI(model=model, temperature=temperature, openai_api_key=self._openai_key)        self.validation_prompt = ValidationTemplate()        self.validation_chain = self._set_up_validation_chain(debug)    def _set_up_validation_chain(self, debug=True):              # make validation agent chain        validation_agent = LLMChain(            llm=self.chat_model,            prompt=self.validation_prompt.chat_prompt,            output_parser=self.validation_prompt.parser,            output_key="validation_output",            verbose=debug,        )                # add to sequential chain         overall_chain = SequentialChain(            chains=[validation_agent],            input_variables=["query", "format_instructions"],            output_variables=["validation_output"],            verbose=debug,        )        return overall_chain    def validate_travel(self, query):        self.logger.info("Validating query")        t1 = time.time()        self.logger.info(            "Calling validation (model is {}) on user input".format(                self.chat_model.model_name            )        )        validation_result = self.validation_chain(            {                "query": query,                "format_instructions": self.validation_prompt.parser.get_format_instructions(),            }        )        validation_test = validation_result["validation_output"].dict()        t2 = time.time()        self.logger.info("Time to validate request: {}".format(round(t2 - t1, 2)))        return validation_test

要运行一个示例,我们可以尝试以下代码。设置 debug=True 将激活 LangChain 的调试模式,它会打印查询文本在移动到 LLM 调用的过程中通过各种 LangChain 类的进展。

secrets = load_secets()travel_agent = Agent(open_ai_api_key=secrets[OPENAI_API_KEY],debug=True)query = """        I want to do a 5 day roadtrip from Cape Town to Pretoria in South Africa.        I want to visit remote locations with mountain views        """travel_agent.validate_travel(query)

这个查询似乎是合理的,所以我们得到了这样一个结果

INFO:__main__:正在验证查询INFO:__main__:在用户输入上调用验证(模型为gpt-3.5-turbo)INFO:__main__:验证请求所需的时间:1.08 {'plan_is_valid':'yes', 'updated_request':''} 

现在,我们通过将查询更改为某些较不合理的内容来进行测试,例如

query ="""          我想从开普敦步行到南非的比勒陀利亚。 我想参观山景的偏远地点          """

响应时间会更长,因为ChatGPT试图解释查询为什么无效,因此生成更多的标记。

INFO:__main__:正在验证查询INFO:__main__:在用户输入上调用验证(模型为gpt-3.5-turbo)INFO:__main__:验证请求所需的时间:4.12 {'plan_is_valid':'no', 'updated_request':'步行从开普敦到南非比勒陀利亚不合适...' a 

4.行程提示

如果查询有效,则可以转到下一阶段,即行程提示。在这里,我们希望模型返回详细的建议旅行计划,它应该采用带有途径地址和关于每个地方应该做什么的建议性项目列表的形式。这实际上是项目的主要“生成”部分,有许多设计查询的方法可以获得良好的结果。我们的ItineraryTemplate 看起来像这样

class ItineraryTemplate(object):    def __init__(self):        self.system_template ="""      您是一位旅行代理,帮助用户制定令人兴奋的旅行计划。      用户的请求将用四个井号表示。将      用户的请求转换为描述他们应该访问的地方和事物的详细行程。      尽量包括每个位置的具体地址。      请记住考虑用户的偏好和时间限制,      并为他们提供适合他们约束条件的有趣且可行的行程。      将行程返回为带有明确的起始和结束位置的项目列表。      要提到旅行的类型。      如果没有提供特定的起始和结束位置,则选择您认为合适的位置并提供具体地址。      您的输出必须是列表,其他的内容不算。    """        self.human_template ="""      ####{query}####    """        self.system_message_prompt = SystemMessagePromptTemplate.from_template(            self.system_template,        )        self.human_message_prompt = HumanMessagePromptTemplate.from_template(            self.human_template, input_variables = [“query”]        )        self.chat_prompt = ChatPromptTemplate.from_messages(            [self.system_message_prompt, self.human_message_prompt]        )

请注意,这里不需要 Pydantic 解析器,因为我们希望输出是一个字符串而不是JSON对象。

要使用此功能,可以向 Agent 类添加新的LLMChain,它如下所示

        travel_agent = LLMChain(llm=self.chat_model,prompt=self.itinerary_prompt.chat_prompt,verbose=debug,output_key =“agent_suggestion”)

我们没有在这里实例化 chat_model 时设置 max_tokens 参数,这允许模型决定其输出的长度。对于特别是GPT4,这可能会使响应时间变得相当长(在某些情况下为30秒以上)。有趣的是,PaLM的响应时间明显较短。

5.途径提取提示

使用行程提示可能会给我们一个很好的途径列表,也许像这样

-第1天:  -从加利福尼亚州伯克利开始  -驱车前往红木国家和州立公园,加利福尼亚州(Second St, Crescent City, CA 95531 1111)  -探索美丽的红杉森林,享受自然风光  -驱车前往加利福尼亚州尤里卡(Eureka,CA 531 Second St,Eureka,CA 95501)  -品尝当地美食并探索迷人的城市  -在加利福尼亚州尤里卡过夜-第2天:  -从加利福尼亚州尤里卡开始  -驱车前往奥勒冈州火山口湖国家公园(Crater Lake National Park,OR 97604)  -欣赏迷人的湖泊,并徒步旅行风景优美的小径  -驱车前往奥勒冈州本德(Bend,OR 97701)  -享受当地美食并探索充满活力的城市  -在奥勒冈州本德过夜-第3天:  -从奥勒冈州本德开始  -驱车前往华盛顿州雷尼尔山国家公园(Washington Ashford 55210 238th Ave E, WA 98304),享受令人惊叹的山景并徒步旅行  -驱车前往华盛顿州塔科马(Tacoma,WA 98402)  -品尝美味的食物选择并探索该市的景点  -在华盛顿州塔科马过夜-第4天:  -从华盛顿州塔科马开始  -驱车前往华盛顿州奥林匹克国家公园(Port Angeles, WA 3002 Mount Angeles Rd,WA 98362)  -探索公园多样化的生态系统并欣赏其自然美景  -驱车前往华盛顿州西雅图(Seattle, WA 98101)  -体验充满活力的美食场所并参观热门景点  -在华盛顿州西雅图过夜-第5天:  -从华盛顿州西雅图开始  -探索更多城市景点并享用当地美食  -在华盛顿州西雅图结束此行

现在我们需要提取途径点的地址,以便我们可以进行下一步,即在地图上绘制它们并调用Google Maps方向API以获取它们之间的路线。

为此,我们将进行另一个LLM调用,并再次使用 PydanicOutputParser 确保我们的输出格式正确。要了解这里的格式,简要考虑一下我们在本项目的下一个阶段要做的事情(第2部分涵盖)。我们将调用Google Maps Python API,代码如下:

import googlemaps 
gmaps = googlemaps.Client(key=google_maps_api_key)
directions_result = gmaps.directions(
    start,
    end,
    waypoints=waypoints,
    mode=transit_type,
    units="metric",
    optimize_waypoints=True,
    traffic_model="best_guess",
    departure_time=start_time,
)

其中,start和end是字符串形式的地址,waypoints是要在其间访问的地址列表。

我们请求的航路点提取模式如下:

class Trip(BaseModel):
    start: str = Field(description="旅行起始位置")
    end: str = Field(description="旅行终点位置")
    waypoints: List[str] = Field(description="途经点列表")
    transit: str = Field(description="交通方式")

这将使我们能够将LLM调用的输出插入到directions调用中。

对于这个提示,我发现添加一个单次性示例确实有助于模型符合期望的输出。通过使用ChatGPT/PaLM的结果,调优一个较小的开源LLM以提取途经点列表,可能会是一个有趣的衍生项目。

class MappingTemplate(object):
    def __init__(self):
        self.system_template = """您是一个将详细的旅行计划转化为简单的位置列表的代理。行程将由四个井号标识。将其转换为他们应该访问的地方的列表。尝试包括每个位置的具体地址。您的输出应始终包含旅行的起点和终点,也可以包括一个途经点列表。它还应包括交通方式。途经点的数量不能超过20个。如果您无法推断出交通方式,可以根据旅行地点进行最佳猜测。例如:
####
伦敦内2天的驾车行程:
- 第1天:
  - 从白金汉宫出发(The Mall, London SW1A 1AA)
  - 参观伦敦塔(Tower Hill, London EC3N 4AB)
  - 探索大英博物馆(Great Russell St, Bloomsbury, London WC1B 3DG)
  - 在牛津街上购物(Oxford St, London W1C 1JN)
  - 结束于科文特花园(Covent Garden, London WC2E 8RF)
- 第2天:
  - 从威斯敏斯特教堂出发(20 Deans Yd, Westminster, London SW1P 3PA)
  - 参观丘吉尔战争室(Clive Steps, King Charles St, London SW1A 2AQ)
  - 探索自然历史博物馆(Cromwell Rd, Kensington, London SW7 5BD)
  - 结束于伦敦塔桥(Tower Bridge Rd, London SE1 2UP)
#####
输出:
起点:白金汉宫,The Mall, London SW1A 1AA
终点:伦敦塔桥,Tower Bridge Rd, London SE1 2UP
途经点:["伦敦塔,Tower Hill, London EC3N 4AB", "大英博物馆,Great Russell St, Bloomsbury, London WC1B 3DG", "牛津街,London W1C 1JN", "科文特花园,London WC2E 8RF","威斯敏斯特,London SW1A 0AA", "圣詹姆斯公园,伦敦", "自然历史博物馆,Cromwell Rd, Kensington, London SW7 5BD"]
交通方式:驾车
交通方式必须是以下选项之一:"驾车"、"火车"、"公交"或"飞机"。
{format_instructions}"""
        self.human_template = """####{agent_suggestion}####"""
        self.parser = PydanticOutputParser(pydantic_object=Trip)
        self.system_message_prompt = SystemMessagePromptTemplate.from_template(
            self.system_template,
            partial_variables={
                "format_instructions": self.parser.get_format_instructions()
            },
        )
        self.human_message_prompt = HumanMessagePromptTemplate.from_template(
            self.human_template, input_variables=["agent_suggestion"]
        )
        self.chat_prompt = ChatPromptTemplate.from_messages(
            [self.system_message_prompt, self.human_message_prompt]
        )

现在,让我们向Agent类添加一个新方法,使用SequentialChain按顺序调用LLM,传递ItineraryTemplateMappingTemplate

def _set_up_agent_chain(self, debug=True):      # 设置LLMChain以将旅行计划作为字符串获取    travel_agent = LLMChain(            llm=self.chat_model,            prompt=self.itinerary_prompt.chat_prompt,            verbose=debug,            output_key="agent_suggestion",        )        # 设置LLMChain以将路线点作为JSON对象获取    parser = LLMChain(            llm=self.chat_model,            prompt=self.mapping_prompt.chat_prompt,            output_parser=self.mapping_prompt.parser,            verbose=debug,            output_key="mapping_list",        )         # 整体链允许我们按顺序调用travel_agent和parser    overall_chain = SequentialChain(            chains=[travel_agent, parser],            input_variables=["query", "format_instructions"],            output_variables=["agent_suggestion", "mapping_list"],            verbose=debug,        )    return overall_chain

要进行这些调用,我们可以使用以下代码

agent_chain = travel_agent._set_up_agent_chain()mapping_prompt = MappingTemplate()agent_result = agent_chain(                {                    "query": query,                    "format_instructions": mapping_prompt.parser.get_format_instructions(),                }            )trip_suggestion = agent_result["agent_suggestion"]waypoints_dict = agent_result["mapping_list"].dict()

waypoints_dict中的地址应该已经格式化得足够适用于Google Maps,但也可以进行地理编码以减少调用路线API时出错的可能性。路线点字典应该类似于以下内容。

{'start': '加利福尼亚州伯克利', 'end': '华盛顿州西雅图', 'waypoints': ['加利福尼亚州克雷森特城,第二大街1111号,加利福尼亚州95531号', '克雷特湖国家公园,克雷特湖国家公园,俄勒冈州97604号', '雷尼尔山国家公园,华盛顿州阿什福德,第238大道55210号,华盛顿州98304号', '奥林匹克国家公园,港口市,第3002号,华盛顿州港口市98362号'], 'transit': '驾车'}

6. 将所有内容整合在一起

现在我们拥有使用LLM验证旅行查询、生成详细行程,并提取路线点作为可以传递的JSON对象的能力。你会发现在代码中,几乎所有这些功能都由Agent类处理,该类在TravelMapperBase中实例化并按以下方式使用

travel_agent = Agent(   open_ai_api_key=openai_api_key,   google_palm_api_key=google_palm_api_key,   debug=verbose,)itinerary, list_of_places, validation = travel_agent.suggest_travel(query)

使用LangChain使更换正在使用的LLM非常容易。对于PALM,我们只需声明

from langchain.llms import GooglePalmAgent.chat_model = GooglePalm(   model_name="models/text-bison-001",   temperature=0,   google_api_key=google_palm_api_key,)

而对于OpenAI,我们可以根据上面的部分使用ChatOpenAIOpenAI

现在,我们准备进入下一阶段:如何将地点列表转换为一组方向,并在地图上绘制它们,供用户检查?这将在这个三部分系列的第2部分中介绍。

谢谢阅读!请随意探索完整的代码库:https://github.com/rmartinshort/travel_mapper。对于改进或扩展功能的任何建议都将不胜感激!