# 对话式搜索 (RAG)

Typesense 具备响应自由形式问题的能力,可以提供对话式回答,并保持上下文以支持后续问答。

您可以将此功能视为类似 ChatGPT 的问答界面,但使用的是您已索引到 Typesense 中的数据。

Typesense 使用一种称为检索增强生成 (opens new window) (Retrieval Augmented Generation, RAG) 的技术来实现这种对话式搜索。

无需自行构建 RAG 管道,Typesense 内置了 RAG 功能,它利用向量存储进行语义搜索,并与 LLM 预集成以生成对话式响应。

# 创建对话历史集合

首先,我们创建一个名为 conversation_store 的 Typesense 集合,用于存储对话式搜索功能生成的对话历史。

集合可以任意命名,但必须遵循以下定义的模式,因为该集合会在对话发生时由 Typesense 自动填充。

curl "http://localhost:8108/collections" \
       -X POST \
       -H "Content-Type: application/json" \
       -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
       -d '{
        "name": "conversation_store",
        "fields": [
            {
                "name": "conversation_id",
                "type": "string"
            },
            {
                "name": "model_id",
                "type": "string"
            },
            {
                "name": "timestamp",
                "type": "int32"
            },
            {
                "name": "role",
                "type": "string",
                "index": false
            },
            {
                "name": "message",
                "type": "string",
                "index": false
            }
        ]
    }'

# 创建对话模型

在创建了上述对话历史集合后,我们可以使用选择的大语言模型(LLM)来创建一个对话模型资源。

Typesense 目前支持以下 LLM 平台:

以下是使用这些平台创建对话模型的方法。(使用下方代码片段中的标签在不同平台间切换)。

# 参数

参数 描述
model_name OpenAI、Cloudflare 或 vLLM 提供的 LLM 模型名称
api_key LLM 服务的 API 密钥
history_collection 存储历史对话记录的 Typesense 集合
account_id LLM 服务的账户 ID(仅适用于 Cloudflare)
system_prompt 包含给 LLM 的特殊指令的系统提示词
ttl 消息自动删除的时间间隔(秒)。默认值:86400(24 小时)
max_bytes 每次 API 调用发送给 LLM 的最大字节数。请参考 LLM 文档中关于上下文窗口支持的字节数限制。
vllm_url vLLM 服务的 URL

响应:

{
  "api_key": "sk-7K**********************************************",
  "id": "conv-model-1",
  "max_bytes": 16384,
  "model_name": "openai/gpt-3.5-turbo",
  "history_collection": "conversation_store",
  "system_prompt": "你是一个问答助手。你只能基于提供的上下文进行对话。如果无法严格根据提供的上下文形成回答,请礼貌地表示你对该主题不了解。"
}

:::提示 如果你没有为模型传递明确的 id,API 将返回一个带有自动生成的会话模型 id 的响应,我们可以在搜索查询中使用这个 ID: :::

# 开始对话

创建完对话模型后,我们可以使用搜索端点和以下 搜索 参数来启动对话:

  • conversation = true
  • conversation_model_id = X
  • q = <任意对话问题>
  • query_by = <自动嵌入字段>

其中 X 是上一步中 Typesense 返回的自动生成的对话模型 ID,query_by 是一个自动嵌入字段

以下是一个示例,我们在 q 参数中提出问题"你能推荐一部动作剧吗?",使用的是我们在 Typesense 中索引到名为 tv_shows 集合里的数据。

curl 'http://localhost:8108/multi_search?q=can+you+suggest+an+action+series&conversation=true&conversation_model_id=conv-model-1' \
        -X POST \
        -H "Content-Type: application/json" \
        -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
        -d '{
              "searches": [
                {
                  "collection": "tv_shows",
                  "query_by": "embedding",
                  "exclude_fields": "embedding"
                }
              ]
            }'

重要提示

在搜索请求中指定 "exclude_fields": "embedding" 非常重要,这样可以避免将原始的浮点数不必要地发送给 LLM,否则会占用大部分上下文窗口。

你还需要通过 exclude_fields 移除其他与生成对话响应无关的字段。

响应:

现在,Typesense 将在搜索 API 响应中返回一个名为 conversation 的新字段。

您可以将 conversation.answer 键的值展示给用户,作为对他们问题的回答。

{
  "conversation": {
    "answer": "我推荐《星际打捞队》,这是一部科幻动作剧集,背景设定在战后星系,讲述一群打捞队员在试图重建时面临的危险和道德困境。",
    "conversation_history": {
      "conversation": [
        {
          "user": "能推荐一部动作剧集吗"
        },
        {
          "assistant": "我推荐《星际打捞队》,这是一部科幻动作剧集,背景设定在战后星系,讲述一群打捞队员在试图重建时面临的危险和道德困境。"
        }
      ],
      "id": "771aa307-b445-4987-b100-090c00a13f1b",
      "last_updated": 1694962465,
      "ttl": 86400
    },
    "conversation_id": "771aa307-b445-4987-b100-090c00a13f1b",
    "query": "能推荐一部动作剧集吗"
  },
  "results": [
    {
      "facet_counts": [],
      "found": 10,
      "hits": [
        ...
      ],
      "out_of": 47,
      "page": 1,
      "request_params": {
        "collection_name": "tv_shows",
        "per_page": 10,
        "q": "能推荐一部动作剧集吗"
      },
      "search_cutoff": false,
      "search_time_ms": 3908
    }
  ]
}

排除对话历史

您可以通过设置搜索参数 exclude_fields: conversation_history 来从搜索 API 响应中排除对话历史记录。

多搜索

当使用 multi_search 端点与对话功能时,q 参数必须作为查询参数设置,而不是作为特定搜索中的 body 参数。

您可以在 multi_search 端点中搜索多个集合,Typesense 在与 LLM 通信时将使用每个集合中的顶部结果。 如果您的 multi_search 请求包含多个设置了 conversation=true 的搜索(通过公共查询参数或单个搜索内部),响应中只会返回一个对话对象。Typesense 将使用 multi_search 中所有搜索的结果,基于多个集合的数据生成一个回答。

自动嵌入模型

根据我们的经验,我们发现专为问答用例设计的模型(如 ts/all-MiniLM-L12-v2 S-BERT 模型)在对话场景中表现良好。您也可以使用 OpenAI 的文本嵌入模型。

# 后续问题

我们可以继续之前开始的对话,通过使用 Typesense 返回的 conversation_id 参数来提出后续问题。

继续上面的例子,让我们在 q 参数中提出后续问题 - "再来一个怎么样":

curl 'http://localhost:8108/multi_search?q=how+about+another+one&conversation=true&conversation_model_id=conv-model-1&conversation_id=771aa307-b445-4987-b100-090c00a13f1b' \
        -X POST \
        -H "Content-Type: application/json" \
        -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
        -d '{
              "searches": [
                {
                  "collection": "tv_shows",
                  "query_by": "embedding",
                  "exclude_fields": "embedding"
                }
              ]
            }'

请注意上面查询参数中新增的 conversation_id

这个参数会使得 Typesense 在与 LLM 通信时包含先前的上下文。

响应:

{
  "对话": {
    "回答": "当然!可以考虑《银河探险》如何?它可以讲述一群星际冒险者在不同星球间旅行时遇到各种挑战和谜题的故事。",
    "对话历史": {
      "对话记录": [
        {
          "用户": "能推荐一个动作系列吗"
        },
        {
          "助手": "我推荐《星际打捞》,这是一个科幻动作系列,背景设定在战后银河系,讲述一支打捞队面对危险和道德困境的同时试图重建家园的故事。"
        },
        {
          "用户": "再来一个推荐"
        },
        {
          "助手": "当然!可以考虑《银河探险》如何?它讲述一群星际冒险者在不同星球间旅行时遇到各种挑战和谜题的故事。"
        }
      ],
      "id": "771aa307-b445-4987-b100-090c00a13f1b",
      "最后更新时间": 1694963173,
      "生存时间": 86400
    },
    "对话ID": "771aa307-b445-4987-b100-090c00a13f1b",
    "查询": "再来一个推荐"
  },
  "搜索结果": [
    {
      "分面计数": [],
      "找到数量": 10,
      "命中结果": [
        ...
      ],
      "总数": 47,
      "页码": 1,
      "请求参数": {
        "集合名称": "tv_shows",
        "每页数量": 10,
        "查询词": "再来一个推荐"
      },
      "搜索截断": false,
      "搜索耗时(毫秒)": 3477
    }
  ]
}

在底层实现中,对于每个后续问题,Typesense 会向 LLM 发起 API 调用,使用以下提示模板生成一个包含对话历史所有相关上下文的独立问题:

基于人机对话历史,将后续问题重写为包含所有相关上下文的独立问题。

<对话历史>
{实际对话历史(不含系统提示)}

<问题>
{后续问题}

<独立问题>

生成的独立问题将用于在集合内进行语义/混合搜索,搜索结果随后会作为上下文转发给 LLM 来回答这个独立问题。

:::提示 上下文窗口限制 虽然我们在 Typesense 中保留了完整的对话历史,但由于上下文长度限制,只有最近 3000 个 token(约 1200 个字符)的对话历史会被发送用于生成独立问题。

同样地,只有前 3000 个 token 的顶部搜索结果会与独立问题一起发送。 :::

# 管理历史对话记录

Typesense 会将所有问答记录(对话历史)存储在由 history_collection 参数指定的 Typesense 集合中。每个对话都有一个关联的 conversation_id 参数,默认会保留 24 小时以支持后续追问。这个保留时长可以通过模型的 ttl 参数进行配置。

您可以使用集合 API 来管理这些消息。由于对话历史不与特定的搜索集合绑定,它们具有通用性,可以随时与不同的搜索集合兼容。

# 管理对话模型

# 获取所有模型

curl 'http://localhost:8108/conversations/models' \
  -X GET \
  -H 'Content-Type: application/json' \
  -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}"

# 获取单个模型

curl 'http://localhost:8108/conversations/models/conv-model-1' \
  -X GET \
  -H 'Content-Type: application/json' \
  -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}"

# 更新模型

您可以像这样更新模型参数:

curl 'http://localhost:8108/conversations/models/conv-model-1' \
  -X PUT \
  -H 'Content-Type: application/json' \
  -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}" \
  -d '{
        "id": "conv-model-1",
        "model_name": "openai/gpt-3.5-turbo",
        "history_collection": "conversation_store",
        "api_key": "OPENAI_API_KEY",
        "system_prompt": "嘿,你是一个用于问答的**智能**助手。你只能基于提供的上下文进行对话。如果无法严格根据提供的上下文形成回答,请礼貌地表示你对该主题不了解。",
        "max_bytes": 16384
      }'

# 删除模型

curl 'http://localhost:8108/conversations/models/conv-model-1' \
  -X DELETE \
  -H 'Content-Type: application/json' \
  -H "X-TYPESENSE-API-KEY: ${TYPESENSE_API_KEY}"