跳到内容

RAG 不仅仅是嵌入搜索

随着大型语言模型 (LLM) 的出现,检索增强生成 (RAG) 已成为一个热门话题。然而,在过去一年中,我帮助创业公司将 LLM 集成到其技术栈时注意到,将用户查询嵌入并直接搜索向量存储的模式实际上只是一种演示用途。

什么是 RAG?

检索增强生成 (RAG) 是一种利用 LLM 生成响应的技术,但使用搜索后端来增强生成。在过去一年中,我看到最流行的被宣传的方法是使用文本嵌入和向量数据库。

RAG

嵌入用户查询并进行搜索的简单 RAG。

因此,让我们先来探讨我所谓的“愚蠢的”RAG 模型——这种基本设置比你想象的要普遍得多。

'愚蠢的' RAG 模型

当你提出一个问题,比如“法国的首都是什么?”时,RAG“愚蠢的”模型会嵌入该查询并在某个无偏见的搜索端点中进行搜索。它仅限于一个方法 API,例如 search(query: str) -> List[str]。对于简单查询来说这没问题,因为你会期望像“巴黎是法国的首都”这样的词出现在例如你的维基百科嵌入的顶部结果中。

为什么这是一个问题?

  • 查询与文档不匹配:这种模型假设查询嵌入和内容嵌入在嵌入空间中是相似的,但这并不总是正确的,取决于你试图搜索的文本。仅使用与内容语义相似的查询是一个巨大的限制!

  • 单一搜索后端:假设只有一个搜索后端,情况并非总是如此。你可能有多个搜索后端,每个都有自己的 API,而你希望将查询路由到向量存储、搜索客户端、SQL 数据库等等。

  • 文本搜索的局限性:将复杂查询限制为单个字符串({query: str}),牺牲了在使用关键词、过滤器和其他高级功能方面的表达能力。例如,询问上周我们解决了哪些问题无法通过简单的文本搜索来回答,因为包含问题、上周的文档每周都可能出现。

  • 计划能力有限:假设查询是搜索后端唯一的输入,但你可能想使用其他信息来改进搜索,例如用户的位置或一天中的时间,利用上下文重写查询。例如,如果你向语言模型提供更多上下文,它就能够规划一系列要执行的查询以返回最佳结果。

现在让我们深入探讨如何通过查询理解使其更智能。这才是真正有趣的地方。

通过查询理解改进 RAG 模型

致谢

这项工作的许多灵感来自 / 与我在 new.computerMetaphor SystemsNaro 的一些客户合作完成,去看看他们吧!

最终,你想要部署的是一个能够理解如何接收查询并重写它以提高精度和召回率的系统。

RAG

查询理解系统路由到多个搜索后端。

还没被说服?让我们从理论转向实践,看一个真实世界的例子。首先是 Metaphor Systems。

什么是 instructor?

Instructor 使用 Pydantic 通过函数调用 API 来简化程序员与语言模型之间的交互。

  • 广泛采用:Pydantic 是 Python 开发人员中流行的工具。
  • 简洁性:Pydantic 允许在 Python 中定义模型。
  • 框架兼容性:许多 Python 框架已经使用 Pydantic。

案例研究 1: Metaphor Systems

Metaphor Systems 为例,它将自然语言查询转化为其自定义的搜索优化查询。如果你查看其 Web UI,你会注意到他们有一个自动提示选项,该选项使用函数调用通过语言模型进一步优化你的查询,并将其转化为一个完全指定的 Metaphor Systems 查询。

Metaphor Systems

Metaphor Systems 用户界面

如果我们深入了解其内部,可以看到查询实际上是一个复杂的对象,包含一个日期范围和一个要搜索的域名列表。它实际上比这更复杂,但这只是一个好的开始。我们可以使用 instructor 库在 Pydantic 中建模这种结构化输出。

class DateRange(BaseModel):
    start: datetime.date
    end: datetime.date


class MetaphorQuery(BaseModel):
    rewritten_query: str
    published_daterange: DateRange
    domains_allow_list: List[str]

    async def execute():
        return await metaphor.search(...)

请注意我们如何建模一个重写的查询、发布日期范围以及一个要搜索的域名列表。这种强大的模式允许对用户查询进行重构以获得更好的性能,而无需用户了解搜索后端的工作细节。

import instructor
from openai import OpenAI

# Enables response_model in the openai client
client = instructor.from_openai(OpenAI())

query = client.chat.completions.create(
    model="gpt-4",
    response_model=MetaphorQuery,
    messages=[
        {
            "role": "system",
            "content": "You're a query understanding system for the Metafor Systems search engine. Here are some tips: ...",
        },
        {"role": "user", "content": "What are some recent developments in AI?"},
    ],
)

示例输出

{
  "rewritten_query": "novel developments advancements ai artificial intelligence machine learning",
  "published_daterange": {
    "start": "2023-09-17",
    "end": "2021-06-17"
  },
  "domains_allow_list": ["arxiv.org"]
}

这不仅仅是添加一些日期范围。这是关于细致入微、量身定制的搜索,它们与后端深度集成。Metaphor Systems 拥有一整套其他过滤器和选项,你可以用它们来构建强大的搜索查询。他们甚至可以使用一些思维链提示来改进他们对这些高级功能的使用方式。

class DateRange(BaseModel):
    start: datetime.date
    end: datetime.date
    chain_of_thought: str = Field(
        None,
        description="Think step by step to plan what is the best time range to search in",
    )

现在,让我们看看这种方法如何帮助建模一个像个人助理这样的代理。

案例研究 2: 个人助理

这种多重分派模式的另一个绝佳例子是个人助理。你可能会问:“我今天有什么安排?”,从一个模糊的查询中,你可能想要事件、电子邮件、提醒等等。这些数据可能存在于多个后端,但你想要的是一个统一的结果摘要。在这里,你不能假设这些文档的文本都嵌入在搜索后端中。可能有一个日历客户端、电子邮件客户端,跨越个人和工作账户。

class ClientSource(enum.Enum):
    GMAIL = "gmail"
    CALENDAR = "calendar"


class SearchClient(BaseModel):
    query: str
    keywords: List[str]
    email: str
    source: ClientSource
    start_date: datetime.date
    end_date: datetime.date

    async def execute(self) -> str:
        if self.source == ClientSource.GMAIL:
            ...
        elif self.source == ClientSource.CALENDAR:
            ...


class Retrieval(BaseModel):
    queries: List[SearchClient]

    async def execute(self) -> str:
        return await asyncio.gather(*[query.execute() for query in self.queries])

现在我们可以用一个简单的查询,比如“我今天有什么安排?”来调用它,它会尝试异步分派到正确的后端。仍然需要很好地提示语言模型,但这我们以后再说。

import instructor
from openai import OpenAI

# Enables response_model in the openai client
client = instructor.from_openai(OpenAI())

retrieval = client.chat.completions.create(
    model="gpt-4",
    response_model=Retrieval,
    messages=[
        {"role": "system", "content": "You are Jason's personal assistant."},
        {"role": "user", "content": "What do I have today?"},
    ],
)

示例输出

{
    "queries": [
        {
            "query": None,
            "keywords": None,
            "email": "jason@example.com",
            "source": "gmail",
            "start_date": "2023-09-17",
            "end_date": None
        },
        {
            "query": None,
            "keywords": ["meeting", "call", "zoom"]]],
            "email": "jason@example.com",
            "source": "calendar",
            "start_date": "2023-09-17",
            "end_date": None

        }
    ]
}

注意我们有一个路由到不同搜索后端(电子邮件和日历)的查询列表。我们甚至可以异步分派它们以尽可能提高性能。我们不仅分派到不同的后端(我们无法控制这些后端),而且很可能还会以不同的方式向用户呈现它们。也许你想将电子邮件摘要为文本,但你想将日历事件渲染为用户可以在移动应用上滚动的列表。

我可以使用框架 X 吗?

我经常收到这个问题,但这只是代码。在这些分派内部,你可以做任何你想做的事情。你可以使用 input() 来向用户询问更多信息,发起 POST 请求,调用 Langchain 代理或 LLamaindex 查询引擎来获取更多信息。可能性是无限的。

这两个例子都展示了搜索提供者和消费者如何使用 instructor 来建模他们的系统。这是一种强大的模式,允许你构建一个任何人都可以使用的系统,并且可以从头开始在任何任意后端之前构建一个 LLM 层。

结论

这与花哨的嵌入技巧无关,这只是简单的旧式信息检索和查询理解。instructor 的妙处在于它简化了复杂建模,让你可以在一个地方定义语言模型的输出、提示以及我们发送给后端的数据载荷。

下一步是什么?

在这里我想展示的是,instructor 不仅仅是关于数据提取。它是一个用于构建数据模型并将其与你的 LLM 集成的强大框架。结构化输出只是一个开始——尚未开发的金矿是熟练使用工具和 API。

如果你喜欢这些内容或想尝试 instructor,请查看 GitHub 并给我们一个星标!