学习

使用 Redis 和 LangChain 构建由 AI 驱动的视频问答应用

Will Johnston
作者
Will Johnston, Redis 开发者增长经理
Prasan Kumar
作者
Prasan Kumar, Redis 技术解决方案开发者

您将在此教程中学到什么#

本教程重点介绍如何为视频内容构建问答引擎。它将涵盖以下主题

  1. 1.如何使用 OpenAI、 Google Gemini 和 LangChain 总结视频内容并生成向量嵌入
  2. 2.如何使用 Redis 存储和搜索向量嵌入
  3. 3.如何使用 Redis 作为语义向量搜索缓存
GITHUB 代码

下面是用于克隆本教程中使用的应用源代码的命令

git clone https://github.com/redis-developer/video-qa-semantic-vector-caching

简介#

在深入了解本教程的细节之前,让我们先回顾一下构建生成式 AI 应用时需要理解的几个重要概念。

  1. 1.生成式 AI 是一个快速发展的领域,专注于创建内容,无论是文本、图像,还是视频。它利用深度学习技术根据学习到的模式和数据生成新的、独特的输出。
  2. 2.检索增强生成 (RAG) 将生成模型与外部知识源结合,以提供更准确和明智的响应。这项技术在上下文化信息至关重要的应用中特别有用。
  3. 3.LangChain 是一个强大的库,可促进涉及语言模型的应用开发。它简化了诸如摘要、问答以及与 ChatGPT 或 Google Gemini 等生成模型的交互等任务。
  4. 4.Google Gemini 和 OpenAI/ChatGPT 是可用于根据给定提示生成文本的生成模型。它们对于需要大量文本生成的应用很有用,例如摘要或问答。
  5. 5.语义向量搜索 是一种使用向量嵌入在数据库中查找相似项的技术。它通常与 RAG 结合使用,以提供对用户查询更准确的响应。
  6. 6.Redis 是一种内存数据库,可用于存储和搜索向量嵌入。它对于需要快速、实时响应的应用特别有用。

我们的应用利用这些技术创建一个基于视频内容的独特问答平台。用户可以上传 YouTube 视频 URL 或 ID,应用利用生成式 AI 总结这些视频,形成潜在问题,并创建一个可搜索的数据库。然后可以查询此数据库,直接从视频内容中查找用户提交问题的答案。

使用 Redis 构建的 AI 视频问答应用的高级概览#

我们的应用如何使用 AI 和语义向量搜索根据视频内容回答用户问题

  1. 1.上传视频:用户可以通过链接(例如 https://www.youtube.com/watch?v=LaiQFZ5bXaM)或视频 ID(例如 LaiQFZ5bXaM)上传 YouTube 视频。应用处理这些输入以检索必要的视频信息。出于本教程的目的,应用已预先填充了来自 Redis YouTube 频道 的视频集合。但是,当您运行应用时,您可以调整它以涵盖您自己的视频集。

2. 视频处理与 AI 交互:应用使用 Youtube Data API 获取视频的标题、描述和缩略图。它还使用 SearchAPI.io 获取视频文字稿。然后将这些文字稿传递给大型语言模型 (LLM)——无论是 Google Gemini 还是 OpenAI 的 ChatGPT——进行摘要和示例问题生成。LLM 还为这些摘要生成向量嵌入。

下面是 LLM 生成的示例摘要和示例问题

https://www.youtube.com/watch?v=LaiQFZ5bXaM
Summary:
The video provides a walkthrough of building a real-time stock tracking application
using Redis Stack, demonstrating its capability to handle multiple data models and
act as a message broker in a single integrated database. The application maintains
a watch list of stock symbols, along with real-time trading information and a chart
updated with live data from the Alpaca API. The presenter uses Redis Stack features
such as sets, JSON documents, time series, Pub/Sub, and Top-K filter to store and
manage different types of data. An architecture diagram is provided, explaining the
interconnection between the front end, API service, and streaming service within
the application. Code snippets highlight key aspects of the API and streaming
service written in Python, highlighting the use of Redis Bloom, Redis JSON, Redis
Time Series, and Redis Search for managing data. The video concludes with a
demonstration of how data structures are visualized and managed in RedisInsight,
emphasizing how Redis Stack can simplify the building of a complex real-time
application by replacing multiple traditional technologies with one solution.

Example Questions and Answers:

Q1: What is Redis Stack and what role does it play in the application?
Q2: How is the stock watch list stored and managed within the application?
Q3: What type of data does the application store using time series capabilities of
Redis Stack?
Q4: Can you explain the use of the Top-K filter in the application?
Q5: What methods are used to update the front end with real-time information in
the application?
Q6: How does the application sync the watch list with the streaming service?
Q7: What frontend technologies are mentioned for building the UI of the application?
Q8: How does Redis Insight help in managing the application data?

3. 使用 Redis 存储数据:所有生成的数据,包括视频摘要、潜在问题和向量嵌入,都存储在 Redis 中。应用利用 Redis 的多种数据类型进行高效的数据处理、缓存和快速检索。

4. 搜索和答案检索:使用 Next.js 构建的前端允许用户提问。然后,应用使用语义向量相似性搜索 Redis 数据库以查找相关的视频内容。它进一步使用 LLM 生成答案,优先考虑视频文字稿中的信息。

5. 结果呈现:应用显示最相关的视频以及 AI 生成的答案,提供全面且交互式的用户体验。它还使用语义向量缓存显示先前查询的缓存结果,以加快响应时间。

环境设置#

要开始使用我们的 AI 驱动的视频问答应用,您首先需要设置开发环境。我们将遵循项目 README.md 文件中概述的说明。

要求#

  • Node.js
  • Docker
  • SearchAPI.io API Key
  • 这用于检索视频文字稿,最多可免费请求 100 次。应用将缓存结果以帮助避免超出免费层级。
  • Google API Key
  • 您必须启用以下 API:
  • YouTube Data API v3
  • Generative Language API
  • 这用于检索视频信息并提示 Google Gemini 模型。这不是免费的。
  • OpenAI API Key
  • 这用于提示 OpenAI ChatGPT 模型。这不是免费的。

设置 Redis#

Redis 用作我们的数据库,用于高效存储和检索数据。您可以快速开始使用云托管的 Redis 实例,只需在 redis.com/try-free 注册即可。这对于开发和测试目的都是理想的选择。您可以在 Redis 免费层级的限制内轻松存储此应用的数据。

克隆仓库#

首先,克隆包含我们项目的仓库

git clone https://github.com/redis-developer/video-qa-semantic-vector-caching

安装依赖项#

设置好 Node.js 环境后,您需要安装必要的包。导航到项目根目录并运行以下命令

npm install

此命令将安装 package.json 文件中列出的所有依赖项,确保您拥有运行应用所需的一切。

配置#

运行应用之前,请务必配置环境变量。有一个脚本可以为您自动生成 .env 文件。运行以下命令

npm run setup

这将生成以下文件

  1. 1.app/.env - 此文件包含 Next.js 应用的环境变量。
  2. 2.app/.env.docker - 此文件包含在 Docker 中运行时环境变量的覆盖项。
  3. 3.services/video-search/.env - 此文件包含视频搜索服务的环境变量。
  4. 4.services/video-search/.env.docker - 此文件包含在 Docker 中运行时环境变量的覆盖项。

默认情况下,您无需更改 app 中的环境文件。但是,您需要配置 services/video-search 目录中的环境文件。

services/video-search/.env 文件内容如下:

USE=<HF|OPENAI>

REDIS_URL=<redis[s]://[[username][:password]@][host][:port][/db-number]>
SEARCHAPI_API_KEY=<https://www.searchapi.io/>
YOUTUBE_TRANSCRIPT_PREFIX=<redis-transcript-prefix>
YOUTUBE_VIDEO_INFO_PREFIX=<redis-video-info-prefix>

GOOGLE_API_KEY=<https://console.cloud.google.com/apis/credentials>
GOOGLE_EMBEDDING_MODEL=<https://ai.google.dev/models/gemini#model_variations>
GOOGLE_SUMMARY_MODEL=<https://ai.google.dev/models/gemini#model_variations>

OPENAI_API_KEY=<https://platform.openai.com/api-keys>
OPENAI_ORGANIZATION=<https://platform.openai.com/account/organization>
OPENAI_EMBEDDING_MODEL=<https://platform.openai.com/account/limits>
OPENAI_SUMMARY_MODEL=<https://platform.openai.com/account/limits>

对于 Gemini 模型,如果您不确定如何操作,可以使用以下内容

GOOGLE_EMBEDDING_MODEL=embedding-001
GOOGLE_SUMMARY_MODEL=gemini-pro

对于 OpenAI 模型,如果您不确定如何操作,可以使用以下内容

OPENAI_EMBEDDING_MODEL=text-embedding-ada-002
OPENAI_SUMMARY_MODEL=gpt-4-1106-preview

注意:根据您的 OpenAI 层级,您可能需要使用不同的摘要模型。 gpt-3.5 模型应该没问题。

_PREFIX 环境变量用于为 Redis 中的键添加前缀。如果您想在多个应用中使用同一个 Redis 实例,这将非常有用。它们的默认值如下

YOUTUBE_TRANSCRIPT_PREFIX=transcripts:
YOUTUBE_VIDEO_INFO_PREFIX=yt-videos:

如果您对默认值感到满意,可以从 .env 文件中删除这些值。

最后, services/video-search/.env.docker 文件包含在 Docker 中使用时 Redis URL 的覆盖项。默认情况下,此应用在 Docker 中设置本地 Redis 实例。如果您使用云实例,只需将 URL 添加到 .env 文件中,并删除 .env.docker 文件中的覆盖项。

运行应用#

安装并配置应用后,运行以下命令构建 Docker 镜像并运行容器

npm run dev

此命令构建应用和视频服务,并将它们部署到 Docker。它已设置好热重载,因此如果您更改代码,服务器将自动重启。

容器启动并运行后,您可以通过 Web 浏览器访问应用

此设置允许您通过浏览器与客户端应用交互,并向托管在独立端口上的视频搜索服务发出请求。

视频搜索服务不发布客户端应用。相反,它公开了一个 REST API,可用于与该服务交互。您可以通过检查 Docker 或访问以下 URL 来验证它是否正在运行

您现在应该已经启动并运行了!本教程的其余部分重点介绍应用的工作原理以及如何使用它,并提供代码示例。

如何使用 Redis 和 LangChain 构建视频问答应用#

视频上传和处理#

处理视频上传并检索视频文字稿和元数据#

后端设置为处理 YouTube 视频链接或 ID。项目中的相关代码片段演示了如何处理这些输入。

services/video-search/src/transcripts/load.ts
export type VideoDocument = Document<{
  id: string;
  link: string;
  title: string;
  description: string;
  thumbnail: string;
}>;

export async function load(videos: string[] = config.youtube.VIDEOS) {
  // Parse the video URLs to get a list of video IDs
  const videosToLoad: string[] = videos.map(parseVideoUrl).filter((video) => {
    return typeof video === 'string';
  }) as string[];

  // Get video title, description, and thumbnail from YouTube API v3
  const videoInfo = await getVideoInfo(videosToLoad);

  // Get video transcripts from SearchAPI.io, join the video info
  const transcripts = await mapAsyncInOrder(videosToLoad, async (video) => {
    return await getTranscript(video, videoInfo[video]);
  });

  // Return the videos as documents with metadata, and pageContent being the transcript
  return transcripts.filter(
    (transcript) => typeof transcript !== 'undefined',
  ) as VideoDocument[];
}

在同一个文件中,您将看到两个缓存

services/video-search/src/transcripts/load.ts
const cache = cacheAside(config.youtube.TRANSCRIPT_PREFIX);
const videoCache = jsonCacheAside<VideoInfo>(config.youtube.VIDEO_INFO_PREFIX);

这些缓存用于在 Redis 中存储文字稿(作为 string)和视频元数据(作为 JSON)。 cache 函数是使用 Redis 存储和检索数据的辅助函数。它们看起来像这样

services/video-search/src/db.ts
export function cacheAside(prefix: string) {
  return {
    get: async (key: string) => {
      return await client.get(`${prefix}${key}`);
    },
    set: async (key: string, value: string) => {
      return await client.set(`${prefix}${key}`, value);
    },
  };
}

export function jsonCacheAside<T>(prefix: string) {
  return {
    get: async (key: string): Promise<T | undefined> => {
      return client.json.get(`${prefix}${key}`) as T;
    },
    set: async (key: string, value: RedisJSON) => {
      return await client.json.set(`${prefix}${key}`, '$', value);
    },
  };
}

您将在应用的其它地方看到这些函数被使用。它们用于防止不必要的 API 调用,在本例中是对 SearchAPI.io 和 YouTube API 的调用。

使用 LangChain、Redis、Google Gemini 和 OpenAI ChatGPT 总结视频内容#

获取视频文字稿和元数据后,使用 LangChain 和 LLM(包括 Gemini 和 ChatGPT)对文字稿进行总结。这里有几个值得理解的代码片段

  1. 1.用于指示 LLM 总结视频文字稿并生成示例问题的 prompt
  2. 2.用于获取视频总结和示例问题的 refinement chain
  3. 3.使用 LLM 生成文本嵌入并将其存储在 Redis 中的 vector embedding chain

LLM  summary prompt 被分成两部分。这样做是为了分析文字稿长度大于 LLM 可接受上下文的视频。

services/video-search/src/api/templates/video.ts
import { PromptTemplate } from 'langchain/prompts';

const summaryTemplate = `
You are an expert in summarizing YouTube videos.
Your goal is to create a summary of a video.
Below you find the transcript of a video:
--------
{text}
--------

The transcript of the video will also be used as the basis for a question and answer bot.
Provide some examples questions and answers that could be asked about the video. Make these questions very specific.

Total output will be a summary of the video and a list of example questions the user could ask of the video.

SUMMARY AND QUESTIONS:
`;

export const SUMMARY_PROMPT = PromptTemplate.fromTemplate(summaryTemplate);

const summaryRefineTemplate = `
You are an expert in summarizing YouTube videos.
Your goal is to create a summary of a video.
We have provided an existing summary up to a certain point: {existing_answer}

Below you find the transcript of a video:
--------
{text}
--------

Given the new context, refine the summary and example questions.
The transcript of the video will also be used as the basis for a question and answer bot.
Provide some examples questions and answers that could be asked about the video. Make
these questions very specific.
If the context isn't useful, return the original summary and questions.
Total output will be a summary of the video and a list of example questions the user could ask of the video.

SUMMARY AND QUESTIONS:
`;

export const SUMMARY_REFINE_PROMPT = PromptTemplate.fromTemplate(
  summaryRefineTemplate,
);

summary prompts 用于使用 LangChain 创建一个 refinement chain。LangChain 将自动处理分割视频文字稿文档并相应地调用 LLM。

services/video-search/src/api/prompt.ts
const videoSummarizeChain = loadSummarizationChain(llm, {
  type: 'refine',
  questionPrompt: SUMMARY_PROMPT,
  refinePrompt: SUMMARY_REFINE_PROMPT,
});

const summaryCache = cacheAside(`${prefix}-${config.redis.SUMMARY_PREFIX}`);

async function summarizeVideos(videos: VideoDocument[]) {
  const summarizedDocs: VideoDocument[] = [];

  for (const video of videos) {
    log.debug(`Summarizing ${video.metadata.link}`, {
      ...video.metadata,
      location: `${prefix}.summarize.docs`,
    });
    const existingSummary = await summaryCache.get(video.metadata.id);

    if (typeof existingSummary === 'string') {
      summarizedDocs.push(
        new Document({
          metadata: video.metadata,
          pageContent: existingSummary,
        }),
      );

      continue;
    }

    const splitter = new TokenTextSplitter({
      chunkSize: 10000,
      chunkOverlap: 250,
    });
    const docsSummary = await splitter.splitDocuments([video]);
    const summary = await videoSummarizeChain.run(docsSummary);

    log.debug(`Summarized ${video.metadata.link}:\n ${summary}`, {
      summary,
      location: `${prefix}.summarize.docs`,
    });
    await summaryCache.set(video.metadata.id, summary);

    summarizedDocs.push(
      new Document({
        metadata: video.metadata,
        pageContent: summary,
      }),
    );
  }

  return summarizedDocs;
}

注意, summaryCache 用于首先询问 Redis 视频是否已总结。如果已总结,它将返回总结并跳过 LLM。这是 Redis 如何用于缓存数据和避免不必要的 API 调用的绝佳示例。下面是一个带有问题的视频总结示例。

https://www.youtube.com/watch?v=LaiQFZ5bXaM
Summary:
The video provides a walkthrough of building a real-time stock tracking application
using Redis Stack, demonstrating its capability to handle multiple data models and
act as a message broker in a single integrated database. The application maintains
a watch list of stock symbols, along with real-time trading information and a chart
updated with live data from the Alpaca API. The presenter uses Redis Stack features
such as sets, JSON documents, time series, Pub/Sub, and Top-K filter to store and
manage different types of data. An architecture diagram is provided, explaining the
interconnection between the front end, API service, and streaming service within
the application. Code snippets highlight key aspects of the API and streaming
service written in Python, highlighting the use of Redis Bloom, Redis JSON, Redis
Time Series, and Redis Search for managing data. The video concludes with a
demonstration of how data structures are visualized and managed in RedisInsight,
emphasizing how Redis Stack can simplify the building of a complex real-time
application by replacing multiple traditional technologies with one solution.

Example Questions and Answers:

Q1: What is Redis Stack and what role does it play in the application?
Q2: How is the stock watch list stored and managed within the application?
Q3: What type of data does the application store using time series capabilities of
Redis Stack?
Q4: Can you explain the use of the Top-K filter in the application?
Q5: What methods are used to update the front end with real-time information in
the application?
Q6: How does the application sync the watch list with the streaming service?
Q7: What frontend technologies are mentioned for building the UI of the application?
Q8: How does Redis Insight help in managing the application data?

vector embedding chain 用于生成视频总结的向量嵌入。这是通过要求 LLM 为总结生成文本嵌入来实现的。 vector embedding chain 定义如下

services/video-search/src/api/store.ts
const vectorStore = new RedisVectorStore(embeddings, {
  redisClient: client,
  indexName: `${prefix}-${config.redis.VIDEO_INDEX_NAME}`,
  keyPrefix: `${prefix}-${config.redis.VIDEO_PREFIX}`,
  indexOptions: {
    ALGORITHM: VectorAlgorithms.HNSW,
    DISTANCE_METRIC: 'IP',
  },
});

矢量存储使用 LangChain 中的 RedisVectorStore 类。此类是 Redis 的封装器,允许您存储和搜索矢量嵌入。我们使用 HNSW 算法和 IP 距离指标。有关支持的算法和距离指标的更多信息,请参阅 Redis 矢量存储文档。我们将 embeddings 对象传递给 RedisVectorStore 构造函数。此对象的定义如下

services/video-search/src/api/llms/google.ts
new GoogleGenerativeAIEmbeddings({
  apiKey: config.google.API_KEY,
  modelName: modelName ?? config.google.EMBEDDING_MODEL,
  taskType: TaskType.SEMANTIC_SIMILARITY,
});

或对于 OpenAI

services/video-search/src/api/llms/openai.ts
new OpenAIEmbeddings({
  openAIApiKey: config.openai.API_KEY,
  modelName: modelName ?? config.openai.EMBEDDING_MODEL,
  configuration: {
    organization: config.openai.ORGANIZATION,
  },
});

使用 embeddings 对象为视频摘要生成矢量嵌入。然后使用 vectorStore 将这些嵌入存储在 Redis 中。

services/video-search/src/api/store.ts

async function storeVideoVectors(documents: VideoDocument[]) {
  log.debug('Storing documents...', {
    location: `${prefix}.store.store`,
  });
  const newDocuments: VideoDocument[] = [];

  await Promise.all(
    documents.map(async (doc) => {
      const exists = await client.sIsMember(
        `${prefix}-${config.redis.VECTOR_SET}`,
        doc.metadata.id,
      );

      if (!exists) {
        newDocuments.push(doc);
      }
    }),
  );

  log.debug(`Found ${newDocuments.length} new documents`, {
    location: `${prefix}.store.store`,
  });

  if (newDocuments.length === 0) {
    return;
  }

  await vectorStore.addDocuments(newDocuments);

  await Promise.all(
    newDocuments.map(async (doc) => {
      await client.sAdd(
        `${prefix}-${config.redis.VECTOR_SET}`,
        doc.metadata.id,
      );
    }),
  );
}

请注意,我们首先检查是否已使用 Redis Set VECTOR_SET 生成了矢量。如果已生成,则跳过 LLM 并使用现有矢量。这避免了不必要的 API 调用,可以加快速度。

Redis 矢量搜索功能和用于视频问答的 AI 集成#

我们应用程序的关键功能之一是能够使用 AI 生成的查询搜索视频内容。本节将介绍后端如何处理搜索请求以及与 AI 模型交互。

将问题转换为矢量#

当用户通过前端提交问题时,后端执行以下步骤以获取问题的答案和支持视频

  1. 1.我们生成与所问问题语义相似的问题。这有助于找到最相关的视频。
  2. 2.然后我们使用 vectorStore 根据语义问题搜索最相关的视频。
  3. 3.如果我们找不到任何相关的视频,我们使用原始问题进行搜索。
  4. 4.找到视频后,我们调用 LLM 来回答问题。
  5. 5.最后,我们将答案和支持视频返回给用户。

为了回答问题,我们首先生成与所问问题语义相似的问题。这使用下面定义的 QUESTION_PROMPT 完成

services/video-search/src/api/templates/questions.ts
import { PromptTemplate } from 'langchain/prompts';

const questionTemplate = `
You are an expert in summarizing questions.
Your goal is to reduce a question down to its simplest form while still retaining the semantic meaning.
Below you find the question:
--------
{question}
--------

Total output will be a semantically similar question that will be used to search an existing dataset.

SEMANTIC QUESTION:
`;

export const QUESTION_PROMPT = PromptTemplate.fromTemplate(questionTemplate);

使用此提示,我们生成 semantic question 并用它来搜索视频。如果我们使用 semantic question 找不到任何视频,我们可能还需要使用原始 question 进行搜索。这使用下面定义的 ORIGINAL_QUESTION_PROMPT 完成

services/video-search/src/api/search.ts
async function getVideos(question: string) {
  log.debug(
    `Performing similarity search for videos that answer: ${question}`,
    {
      question,
      location: `${prefix}.search.search`,
    },
  );

  const KNN = config.searches.KNN;
  /* Simple standalone search in the vector DB */
  return await (vectorStore.similaritySearch(question, KNN) as Promise<
    VideoDocument[]
  >);
}

async function searchVideos(question: string) {
  log.debug(`Original question: ${question}`, {
    location: `${prefix}.search.search`,
  });

  const semanticQuestion = await prompt.getSemanticQuestion(question);

  log.debug(`Semantic question: ${semanticQuestion}`, {
    location: `${prefix}.search.search`,
  });
  let videos = await getVideos(semanticQuestion);

  if (videos.length === 0) {
    log.debug(
      'No videos found for semantic question, trying with original question',
      {
        location: `${prefix}.search.search`,
      },
    );

    videos = await getVideos(question);
  }

  log.debug(`Found ${videos.length} videos`, {
    location: `${prefix}.search.search`,
  });

  const answerDocument = await prompt.answerQuestion(question, videos);

  return [
    {
      ...answerDocument.metadata,
      question: answerDocument.pageContent,
      isOriginal: true,
    },
  ];
}

上面的代码展示了从 LLM 获取答案并将其返回给用户的整个过程。识别相关视频后,后端使用 Google Gemini 或 OpenAI 的 ChatGPT 生成答案。这些答案根据存储在 Redis 中的视频转录稿 формулировать,确保它们与用户的查询具有上下文相关性。用于向 LLM 提问以获取答案的 ANSWER_PROMPT 如下所示

services/video-search/src/api/templates/answers.ts
import { PromptTemplate } from 'langchain/prompts';

const answerTemplate = `
You are an expert in answering questions about Redis and Redis Stack.
Your goal is to take a question and some relevant information extracted from videos and return the answer to the question.

- Try to mostly use the provided video info, but if you can't find the answer there you can use other resources.
- Make sure your answer is related to Redis. All questions are about Redis. For example, if a question is asking about strings, it is asking about Redis strings.
- The answer should be formatted as a reference document using markdown. Make all headings and links bold, and add new paragraphs around any code blocks.
- Your answer should include as much detail as possible and be no shorter than 500 words.

Here is some extracted video information relevant to the question: {data}

Below you find the question:
--------
{question}
--------

Total output will be the answer to the question.

ANSWER:
`;

export const ANSWER_PROMPT = PromptTemplate.fromTemplate(answerTemplate);

就是这样!后端现在将答案和支持视频返回给用户。

进一步探索语义答案缓存#

我们在本教程中构建的应用程序是探索 AI 驱动的视频问答可能性的绝佳起点。但是,有许多方法可以改进应用程序并使其更高效。其中一个改进是使用 Redis 作为语义矢量缓存。

请注意,在上一节中,我们讨论了调用 LLM 来回答每个问题。在此步骤中存在性能瓶颈,因为 LLM 响应时间各不相同,但可能需要几秒钟。如果我们有一种方法可以阻止不必要的 LLM 调用呢?这就是 semantic vector caching 的用武之地。

什么是语义矢量缓存?#

语义矢量缓存发生在您获取 LLM 调用结果并将其与提示的矢量嵌入一起缓存时。在我们的应用程序中,我们可以为问题生成矢量嵌入,并将其与 LLM 的答案一起存储在 Redis 中。这将使我们能够避免对已回答的类似问题进行 LLM 调用。

您可能会问为什么将问题存储为矢量?为什么不只将问题存储为字符串?答案是将问题存储为矢量允许我们执行语义矢量相似性搜索。因此,我们可以确定可接受的相似性分数并返回类似问题的答案,而不是依赖于有人提出完全相同的问题

如何在 Redis 中实现语义矢量缓存#

如果您已经熟悉在 Redis 中存储矢量(我们在本教程中已经介绍过),那么语义矢量缓存是它的扩展,其操作方式基本相同。唯一的区别是我们将问题存储为矢量,而不是视频摘要。我们还使用 cache aside 模式。过程如下

  1. 1.当用户提问时,我们对问题的现有答案执行矢量相似性搜索。
  2. 2.如果找到答案,则将其返回给用户。从而避免了对 LLM 的调用。
  3. 3.如果找不到答案,则调用 LLM 生成答案。
  4. 4.然后我们将问题作为矢量存储在 Redis 中,以及 LLM 的答案。

为了存储问题矢量,我们需要创建一个新的矢量存储。这将专门为问答矢量创建一个索引。代码如下所示

services/video-search/src/api/store.ts
const answerVectorStore = new RedisVectorStore(embeddings, {
  redisClient: client,
  indexName: `${prefix}-${config.redis.ANSWER_INDEX_NAME}`,
  keyPrefix: `${prefix}-${config.redis.ANSWER_PREFIX}`,
  indexOptions: {
    ALGORITHM: VectorAlgorithms.FLAT,
    DISTANCE_METRIC: 'L2',
  },
});

answerVectorStore 看起来与我们之前定义的 vectorStore 几乎相同,但它使用了不同的 算法和距离指标。此算法更适合于我们问题的相似性搜索。

以下代码演示了如何使用 answerVectorStore 检查类似问题是否已回答。

services/video-search/src/api/search.ts
async function checkAnswerCache(question: string) {
  const haveAnswers = await answerVectorStore.checkIndexExists();

  if (!(haveAnswers && config.searches.answerCache)) {
    return;
  }

  log.debug(`Searching for closest answer to question: ${question}`, {
    location: `${prefix}.search.getAnswer`,
    question,
  });

  /**
   * Scores will be between 0 and 1, where 0 is most accurate and 1 is least accurate
   */
  let results = (await answerVectorStore.similaritySearchWithScore(
    question,
    config.searches.KNN,
  )) as Array<[AnswerDocument, number]>;

  if (Array.isArray(results) && results.length > 0) {
    // Filter out results with too high similarity score
    results = results.filter(
      (result) => result[1] <= config.searches.maxSimilarityScore,
    );

    const inaccurateResults = results.filter(
      (result) => result[1] > config.searches.maxSimilarityScore,
    );

    if (Array.isArray(inaccurateResults) && inaccurateResults.length > 0) {
      log.debug(
        `Rejected ${inaccurateResults.length} similar answers that have a score > ${config.searches.maxSimilarityScore}`,
        {
          location: `${prefix}.search.getAnswer`,
          scores: inaccurateResults.map((result) => result[1]),
        },
      );
    }
  }

  if (Array.isArray(results) && results.length > 0) {
    log.debug(
      `Accepted ${results.length} similar answers that have a score <= ${config.searches.maxSimilarityScore}`,
      {
        location: `${prefix}.search.getAnswer`,
        scores: results.map((result) => result[1]),
      },
    );

    return results.map((result) => {
      return {
        ...result[0].metadata,
        question: result[0].pageContent,
        isOriginal: false,
      };
    });
  }
}

similaritySearchWithScore 将找到与所问问题相似的问题。它将其从 01 排名,其中 0 最相似或“最接近”。然后,我们过滤掉任何过于相似的结果,如 maxSimilarityScore 环境变量所定义。如果找到任何结果,则将其返回给用户。此处使用最大分数至关重要,因为我们不想返回不准确的结果。

要完成此过程,我们需要应用 cache aside 模式并将问题作为矢量存储在 Redis 中。方法如下

services/video-search/src/api/search.ts
async function searchVideos(
    question: string,
    { useCache = config.searches.answerCache }: VideoSearchOptions = {},
) {
    log.debug(`Original question: ${question}`, {
        location: `${prefix}.search.search`,
    });

    if (useCache) {
        const existingAnswer = await checkAnswerCache(question);

        if (typeof existingAnswer !== 'undefined') {
            return existingAnswer;
        }
    }

    const semanticQuestion = await prompt.getSemanticQuestion(question);

    log.debug(`Semantic question: ${semanticQuestion}`, {
        location: `${prefix}.search.search`,
    });

    if (useCache) {
        const existingAnswer = await checkAnswerCache(semanticQuestion);

        if (typeof existingAnswer !== 'undefined') {
            return existingAnswer;
        }
    }

    let videos = await getVideos(semanticQuestion);

    if (videos.length === 0) {
        log.debug(
            'No videos found for semantic question, trying with original question',
            {
                location: `${prefix}.search.search`,
            },
        );

        videos = await getVideos(question);
    }

    log.debug(`Found ${videos.length} videos`, {
        location: `${prefix}.search.search`,
    });

    const answerDocument = await prompt.answerQuestion(question, videos);

    if (config.searches.answerCache) {
        await answerVectorStore.addDocuments([answerDocument]);
    }

    return [
        {
            ...answerDocument.metadata,
            question: answerDocument.pageContent,
            isOriginal: true,
        },
    ];
}

提问时,我们首先检查答案缓存。我们同时检查问题和生成的语义问题。如果找到答案,则将其返回给用户。如果找不到答案,则调用 LLM 生成答案。然后我们将问题作为矢量存储在 Redis 中,以及 LLM 的答案。看起来我们在这里比没有缓存时做了更多工作,但请记住 LLM 是瓶颈。通过这样做,我们避免了不必要的 LLM 调用。

以下是应用程序的几个截图,可查看找到现有问题答案时的样子

结论#

在本教程中,我们探讨了如何使用 Redis、LangChain 和各种其他技术构建 AI 驱动的视频问答应用程序。我们涵盖了设置环境、处理视频上传以及实现搜索功能。您还了解了如何使用 Redis 作为 vector storesemantic vector cache

注意:本教程未包含对前端 Next.js 应用的概述。但是,您可以在 GitHub 仓库app 目录中找到代码。

主要收获#

  • 可以利用生成式 AI 创建功能强大的应用程序,而无需编写大量代码。
  • Redis 在处理 AI 生成的数据和矢量方面高度通用且高效。
  • LangChain 可以轻松地将 AI 模型与矢量存储集成。

请记住,Redis 提供云托管实例,让您可以轻松开始,您可以在 redis.com/try-free 注册。这使得使用 AI 和 Redis 进行实验比以往任何时候都更方便。

我们希望本教程能激发您探索将 AI 与 Redis 等强大数据库相结合创建创新应用程序的激动人心的可能性。

进一步阅读#