重要链接
简介
LLM 的一大缺点是它们只能回答关于其训练数据的问题。也就是说,除非你能将它们连接到外部知识或计算资源——这正是 LangChain 构建的目的。将 LLM 连接到的最受欢迎的知识来源之一是互联网——从 You.com 到 Perplexity 再到 ChatGPT Browsing。在这篇博文中,我们将展示如何构建一个由 Tavily 驱动的开源网络研究助手。
为了构建即使是最简单的这些应用程序,也需要做出许多小的但影响深远的工程决策。为了最好地说明这一点,我们将详细介绍构建此类应用程序的所有决策。然后,我们将尝试通过构建“对抗性”搜索查询来破坏我们构建的应用程序。我们这样做是为了展示我们所做的各种工程决策的权衡。我们将主要关注适用于所有 RAG 应用程序的通用工程决策,而只花少量时间在特定于网络的事项上。
我们希望这篇文章有几个好处。首先,我们希望展示导致此应用程序失败的具体查询示例,有助于展示工程决策如何在产品体验中体现出来,使您更好地了解 LLM 支持系统的局限性。其次,我们尝试推理我们做出某些工程决策的原因。我们将讨论这些决策的优缺点,以及我们最终选择的解决方案的原因。我们希望这能为构建 LLM 应用程序时要考虑的各种工程权衡提供见解。最后,我们分享所有源代码。我们希望这能让您轻松开始构建自己的 LLM 应用程序。
检索增强生成
在底层,这些网络研究工具使用一种称为“检索增强生成”(通常称为 RAG)的技术。有关该主题的深入探讨,请参阅这篇文章。RAG 的高级描述包括两个步骤
- 检索:检索一些信息
- 增强生成:使用检索到的信息生成对问题的响应
虽然这两个步骤看起来很简单,但实际上这两个步骤都包含相当多的复杂性。
检索
这些网络研究人员做的第一件事是从互联网上查找东西。虽然这可能看起来很简单,但实际上这里有很多有趣的决策需要做出。这些决策并非特定于互联网搜索应用程序——它们是所有 RAG 应用程序创建者都需要做出的决策(无论他们是否意识到)。
- 我们是否总是查找内容?
- 我们是查找原始用户搜索查询还是派生的查询?
- 对于后续问题,我们该怎么办?
- 我们是查找多个搜索词还是只查找一个?
- 我们可以多次查找内容吗?
还有一些更特定于一般网络研究的决策。我们将在这里花更少的时间,因为这些决策的通用性较差。
- 我们应该使用哪个搜索引擎?
- 我们如何从该搜索引擎获取信息?
我们是否总是查找内容?
您在 RAG 应用程序中需要做出的一个决定是,您是否总是想查找内容。为什么您可能不想总是查找内容?如果您的应用程序更倾向于通用聊天机器人,您可能不想总是查找内容。在这种情况下,如果用户与您的应用程序交互并说“Hi”,您无需进行任何检索,这样做只是浪费时间和令牌。您可以通过几种方式实现是否查找内容的逻辑。首先,您可以有一个简单的分类层来分类是否值得查找内容。另一种方法是允许 LLM 生成搜索查询,并且在不需要查找内容的情况下,只需允许它生成一个空搜索查询。不总是查找内容有几个缺点。首先,此逻辑可能比它本身更耗时/成本更高(例如,它可能需要额外的 LLM 调用)。其次,如果您强烈先验地认为用户将您用作搜索工具而不是通用聊天机器人,那么您将增加犯错的可能性,并在应该查找内容时不查找内容。
对于我们的应用程序,我们选择始终查找内容。我们选择这样做是因为我们试图重新创建一个网络研究员。这让我们强烈先验地认为我们的用户是为了研究而来的,因此期望的行为几乎总是查找内容。添加一些逻辑来决定是否这样做可能不值得付出成本(时间、金钱、出错的可能性)。
这确实有一些缺点——如果我们决定始终查找内容,那么当有人试图与它进行正常对话时,会有点奇怪。

我们是查找原始用户搜索查询还是派生的查询?
RAG 最直接的方法是采用用户的查询并查找该短语。这既快速又简单。但是,它可能有一些缺点。即,用户的输入可能不完全是他们打算查找的内容。
这方面的一个重要例子是漫无边际的问题。漫无边际的问题通常包含大量分散真正问题注意力的词语。让我们考虑下面的搜索查询
你好!我想知道一个问题的答案。可以吗?假设可以。我叫哈里森,是 langchain 的首席执行官。我喜欢 llm 和 openai。Maisie Peters 是谁?
我们想要回答的真正问题是“Maisie Peters 是谁”,但是那里有很多分散注意力的文本。处理此问题的一种方法是不使用原始问题,而是从用户问题中生成搜索查询。这样做的好处是可以生成显式搜索查询。缺点是会增加额外的 LLM 调用。
对于我们的应用程序,我们假设大多数初始用户问题都非常直接,因此我们将只查找原始查询。这样做会在遇到上述问题时严重失败

如您所见,我们未能获取任何相关来源,并且响应纯粹基于 LLM 的知识,而没有结合任何外部数据。
对于后续问题,我们该怎么办?
对于基于聊天的 RAG 应用程序,要考虑的一个非常重要的情况是在出现后续问题时该怎么办。之所以如此重要,是因为后续问题会带来许多有趣的边缘情况
- 如果后续问题间接引用了之前的对话怎么办?
- 如果后续问题完全不相关怎么办?
处理后续问题通常有两种常见方法
- 直接搜索后续问题。这对于完全不相关的问题很有用,但在后续问题引用之前的对话时会失效。
- 使用 LLM 生成新的搜索查询(或多个查询)。这通常效果很好,但确实会增加一些额外的延迟。
对于后续问题,它们不太可能成为好的独立搜索查询的可能性要高得多。出于这个原因,额外查询生成搜索查询的额外成本和延迟是值得的。让我们看看这如何使我们能够处理后续问题。
首先,让我们跟进“她的一些歌曲是什么?”。生成搜索查询使我们能够获得糟糕的相关搜索结果。

这样做的一个附带好处是,我们现在可以处理漫无边际的问题。如果我们重新提出与之前相同的漫无边际的问题,我们现在会得到更好的结果。

这一次,它得到了很好的结果。
您可以在这里看到我们用于改写搜索查询的提示。
我们是查找多个搜索词还是只查找一个?
好的,我们将使用 LLM 生成搜索词。接下来要弄清楚的是——它总是只有一个搜索词吗?或者它可以是多个搜索词?如果是多个搜索词,有时可以是零个搜索词吗?
允许可变数量的搜索词的好处是更灵活。缺点是更复杂。这种复杂性值得吗?
允许零个搜索词的复杂性可能不值得。与我们必须做出的关于是否总是查找内容的决定类似,我们假设人们使用我们的网络研究应用程序是因为他们想查找内容。因此,始终生成查询是有意义的。
生成多个查询可能不会那么糟糕,但会增加更长的查找时间。为了保持简单,在此应用程序中,我们将只生成一个搜索查询。
但是,这也有其缺点。让我们考虑下面的问题
谁赢得了 2023 年的第一场 NFL 比赛?谁赢得了 2023 年女子世界杯?
在下面的结果中,我们可以看到所有检索到的来源都只关于其中一件事(它们都与谁赢得了 2023 年女子世界杯有关)。因此,它感到困惑,无法回答问题的第一个部分(实际上,根本无法回答问题)。

我们可以多次查找内容吗?
大多数 RAG 应用程序只执行单个查找步骤。但是,允许它执行多个查找步骤可能是有益的。
请注意,这与生成多个搜索查询不同。当生成多个搜索查询时,可以并行搜索这些查询。允许执行多个查找步骤背后的动机是,最终答案可能取决于先前查找步骤的结果,并且这些查找需要按顺序完成。这在 RAG 应用程序中不太常见,因为它会增加更多成本和延迟。
这个决定代表了一个相当大的岔路口,也是 ChatGPT Browsing(它可以多次查找内容)与 Perplexity(它不这样做)之间最大的区别之一。这也代表了两种非常不同类型的应用程序之间的重大决策。
可以多次查找内容的应用程序(如 ChatGPT Browsing)开始变得像代理。这有利有弊。从好的方面来说,它允许这些应用程序回答更复杂的问题的长尾。但是,这通常以延迟(这些应用程序速度较慢)和可靠性(它们有时会失控)为代价。
不能多次查找内容的应用程序(如 Perplexity)则相反——它们通常更快、更可靠,但处理长尾复杂问题的能力较差。
作为示例,让我们考虑一个类似这样的问题
谁赢得了 2023 年女子世界杯?该国的 GDP 是多少?
鉴于 Perplexity 不会多次查找内容,我们预计它无法很好地处理这种情况。让我们试一下

它答对了第一部分(西班牙确实赢得了世界杯),但由于它不能查找两次内容,因此没有答对第二个问题。
现在让我们用 ChatGPT Browsing 试一下。由于它可以执行多个操作,因此有可能正确回答这个问题。但是,总是有失控的可能。会是哪一种呢?

在这种情况下,它完美地处理了它!但是,这远非必然。作为失控的示例,让我们看看它如何处理之前真正包含两个单独子问题的问题

这不算太糟糕,但也不完美。还值得注意的是,这两个问题获得答案的时间都比 Perplexity 长得多。
我们构建这些类型应用程序的经验是,在某些时候,您必须在速度更快的应用程序(功能稍有限)和速度较慢的应用程序(可以涵盖更广泛的任务)之间做出选择。这种速度较慢的应用程序通常更容易失控,并且通常更难做好。对于我们的网络研究助手,我们选择了快速聊天体验。这意味着它不能很好地处理多部分问题

我们应该使用哪个搜索引擎?
我们如何从该搜索引擎获取信息?
这些问题更特定于网络研究助手,但仍然值得考虑。实际上有两种方法可以考虑
- 使用一些搜索引擎来获取热门结果以及该页面上的相应摘要
- 使用一些搜索引擎来获取热门结果,然后分别调用每个页面并加载那里的全文
方法 1 的优点是速度快。方法 2 的优点是它将获得更完整的信息。
对于我们的应用程序,我们使用 Tavily 来进行实际的网络抓取。Tavily 是一个搜索 API,专门为 AI 代理设计,并为 RAG 目的量身定制。通过 Tavily Search API,AI 开发人员可以轻松地将他们的应用程序与实时在线信息集成。Tavily 的主要目标是提供来自可信来源的真实可靠的信息,从而提高 AI 生成内容的准确性和可靠性。
我们特别喜欢 Tavily 的地方
- 它速度很快
- 它为每个页面返回良好的摘要,因此我们不必加载每个页面
- 它还返回图像(有趣!)
检索总结
总而言之,我们的网络研究助手使用的检索算法是
- 对于第一个问题,直接将其传递给搜索引擎
- 对于后续问题,生成一个基于对话历史记录的单个搜索查询,以传递给搜索引擎
- 使用 Tavily 获取搜索结果,包括摘要
当我们实现这一点时,它最终看起来像下面这样
if not use_chat_history:
# If no chat history, we just pass the question to the retriever
initial_chain = itemgetter("question") | retriever
return initial_chain
else:
# If there is chat history, we first generate a standalone question
condense_question_chain = (
{
"question": itemgetter("question"),
"chat_history": itemgetter("chat_history"),
}
| CONDENSE_QUESTION_PROMPT
| llm
| StrOutputParser()
)
# We then pass that standalone question to the retriever
conversation_chain = condense_question_chain | retriever
return conversation_chain
我们可以看到,如果没有 chat_history
,那么我们只需将问题直接传递给搜索引擎。如果存在 chat_history
,我们使用 LLM 将聊天历史记录凝练成一个查询发送给检索器。
增强生成
执行检索步骤只是产品的一半。另一半是现在使用这些检索到的结果以自然语言做出响应。这里有几个问题
- 我们应该使用哪个 LLM?
- 我们应该使用什么提示?
- 我们应该只给出答案吗?还是应该提供额外信息?
我们应该使用哪个 LLM?
对于这一点,我们选择了 GPT-3.5-Turbo,因为它成本低且响应速度快。随着时间的推移,我们将使其更可配置,以允许使用 Anthropic、Vertex 和 Llama 模型。
我们应该使用什么提示?
这可以说是人们在应用程序上花费最多时间的地方。如果我们是真正构建它,我们可能会为此投入数小时。为了快速启动我们的流程,我们从之前泄露的 Perplexity 提示开始。经过一些修改,我们最终得到的最终提示可以在这里找到。

我们应该只给出答案吗?还是应该提供额外信息?
一个非常常见的做法是不仅提供答案,还提供答案所依据的来源的引文。这在研究应用程序中很重要,原因有几个。首先,它使验证 LLM 在其响应中提出的任何声明变得容易(因为您可以导航到引用的来源并自行检查)。其次,它使深入研究特定事实或声明变得容易。
由于泄露的 Perplexity 提示使用了特定的约定来引用其来源,因此我们继续使用相同的约定。该特定约定涉及要求 LLM 以以下符号生成来源:[N]
。然后,我们在客户端解析出来并将其呈现为超链接。
生成总结
将所有内容放在一起,在代码中它最终看起来像
_context = RunnableMap(
{
# Use the above retrieval logic to get documents and then format them
"context": retriever_chain | format_docs,
"question": itemgetter("question"),
"chat_history": itemgetter("chat_history"),
}
)
response_synthesizer = prompt | llm | StrOutputParser()
chain = _context | response_synthesizer
这是一个相对简单的链,我们首先获取相关上下文,然后将其传递给单个 LLM 调用。
结论
我们希望这有助于您以几种方式准备构建自己的 RAG 应用程序。首先,我们希望这有助于探索(比您可能想要的更详细地)构成 RAG 应用程序的所有小型工程决策。了解这些决策以及所涉及的权衡对于构建您自己的应用程序至关重要。其次,我们希望开源存储库(包括功能齐全的 Web 应用程序和与 LangSmith 的连接)是一个有用的起点。
底层应用程序逻辑与我们上周发布的 ChatLangChain 应用程序非常相似(基本上相同)。这不是偶然的。我们认为这种应用程序逻辑非常通用,可以扩展到各种应用程序——因此我们希望您尝试这样做!虽然我们希望这可以作为一个简单的入门点,但我们期待下周的改进,这将使自定义变得更加容易。