一架梯子,一头程序猿,仰望星空!
LangChain教程(JS版本) > 内容正文

基于对话的问答任务


对话式检索问答

对话式检索问答链在检索问答链的基础上提供了一个聊天历史组件。

首先,它将聊天历史(可以是显式传入的,也可以是从提供的存储器中检索到的)和问题合并成一个独立的问题,然后从检索器中查找相关的文档,最后将这些文档和问题传递给问答链以返回一个响应。

要创建一个对话式检索问答链,你需要一个检索器。在下面的示例中,我们将从一个向量存储器创建一个检索器,这个存储器可以从嵌入中创建。

import { ChatOpenAI } from "langchain/chat_models/openai";
import { ConversationalRetrievalQAChain } from "langchain/chains";
import { HNSWLib } from "langchain/vectorstores/hnswlib";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { BufferMemory } from "langchain/memory";
import * as fs from "fs";

export const run = async () => {
  /* 初始化用于回答问题的长文本模型(LLM) */
  const model = new ChatOpenAI({});
  /* 加载要进行问答的文件 */
  const text = fs.readFileSync("state_of_the_union.txt", "utf8");
  /* 将文本分割成块 */
  const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
  const docs = await textSplitter.createDocuments([text]);
  /* 创建向量存储器 */
  const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
  /* 创建链条 */
  const chain = ConversationalRetrievalQAChain.fromLLM(
    model,
    vectorStore.asRetriever(),
    {
      memory: new BufferMemory({
        memoryKey: "chat_history", // 必须设置为 "chat_history"
      }),
    }
  );
  /* 提问 */
  const question = "总统对布雷耶尔大法官有什么说法?";
  const res = await chain.call({ question });
  console.log(res);
  /* 进一步提问 */
  const followUpRes = await chain.call({
    question: "那是好的吗?",
  });
  console.log(followUpRes);
};

在上面的代码片段中,ConversationalRetrievalQAChain类的fromLLM方法具有以下签名:

static fromLLM(
  llm: BaseLanguageModel,
  retriever: BaseRetriever,
  options?: {
    questionGeneratorChainOptions?: {
      llm?: BaseLanguageModel;
      template?: string;
    };
    qaChainOptions?: QAChainParams;
    returnSourceDocuments?: boolean;
  }
): ConversationalRetrievalQAChain

以下是options对象的每个属性的说明:

  • questionGeneratorChainOptions:允许您传递自定义模板和LLM(语言模型)到基础问题生成链的对象。
    • 如果提供了模板,则ConversationalRetrievalQAChain将使用此模板从对话上下文中生成问题,而不是使用问题参数中提供的问题。
    • 在此处传递单独的LLM(llm)可以让您在使用更强大的模型进行最终回答时,使用便宜/更快的模型来创建简化的问题,从而减少不必要的延迟。
  • qaChainOptions:允许您自定义最终步骤中使用的特定QA链的选项。默认值为StuffDocumentsChain,但是您可以通过传递type参数来自定义使用哪个链。在此处传递特定选项是完全可选的,但如果您希望自定义向最终用户展示响应的方式,或者如果默认的StuffDocumentsChain对于太多文档来说不够用,这将非常有用。您可以在此处查看可用字段的API参考。如果您想要将chat\_history提供给最终的答案qaChain,该qaChain最终回答用户的问题,您必须传递带有chat\_history作为输入的自定义qaTemplate,因为默认模板中没有该输入,该输入只传递了context文档和生成的问题。
  • returnSourceDocuments:布尔值,指示ConversationalRetrievalQAChain是否应返回用于检索答案的源文档。如果设置为true,则文档将包含在调用方法返回的结果中。如果您希望允许用户查看用于生成答案的源文档,这将非常有用。如果未设置,默认值为false。
    • 如果您使用此选项并传递一个内存实例,请将内存实例上的inputKeyoutputKey设置为与链输入和最终对话链输出相同的值。默认值分别为"question""text",指定内存应存储的值。

内置记忆

这是一个使用更快的低级语言模型(LLM)生成问题和一个更慢、更全面的LLM生成最终答案的定制示例。它使用一个内置的记忆对象,并返回引用的源文件。因为我们设置了 returnSourceDocuments 并且从链中返回多个值,所以我们必须在内存实例上设置 inputKeyoutputKey ,让它知道要存储哪些值。

import { ChatOpenAI } from "langchain/chat_models/openai";
import { ConversationalRetrievalQAChain } from "langchain/chains";
import { HNSWLib } from "langchain/vectorstores/hnswlib";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { BufferMemory } from "langchain/memory";

import * as fs from "fs";

export const run = async () => {
  const text = fs.readFileSync("state_of_the_union.txt", "utf8");
  const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
  const docs = await textSplitter.createDocuments([text]);
  const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
  const fasterModel = new ChatOpenAI({
    modelName: "gpt-3.5-turbo",
  });
  const slowerModel = new ChatOpenAI({
    modelName: "gpt-4",
  });
  const chain = ConversationalRetrievalQAChain.fromLLM(
    slowerModel,
    vectorStore.asRetriever(),
    {
      returnSourceDocuments: true,
      memory: new BufferMemory({
        memoryKey: "chat_history",
        inputKey: "question", // 问题的输入键
        outputKey: "text", // 链的最终对话输出的键
        returnMessages: true, // 如果与聊天模型(如 gpt-3.5 或 gpt-4)一起使用
      }),
      questionGeneratorChainOptions: {
        llm: fasterModel,
      },
    }
  );
  /* 提问一个问题 */
  const question = "总统对布雷耶有什么评论?";
  const res = await chain.call({ question });
  console.log(res);

  const followUpRes = await chain.call({ question: "那是友好的吗?" });
  console.log(followUpRes);
};

流式传输

您还可以使用上述的概念,使用两个不同的LLM来流式传输链条的最终响应,而不输出中间独立的问题生成步骤的输出。以下是一个示例代码:

import { ChatOpenAI } from "langchain/chat_models/openai";
import { ConversationalRetrievalQAChain } from "langchain/chains";
import { HNSWLib } from "langchain/vectorstores/hnswlib";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { BufferMemory } from "langchain/memory";

import * as fs from "fs";

export const run = async () => {
  const text = fs.readFileSync("state_of_the_union.txt", "utf8");
  const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
  const docs = await textSplitter.createDocuments([text]);
  const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
  let streamedResponse = "";
  const streamingModel = new ChatOpenAI({
    streaming: true,
    callbacks: [
      {
        handleLLMNewToken(token) {
          streamedResponse += token;
        },
      },
    ],
  });
  const nonStreamingModel = new ChatOpenAI({});
  const chain = ConversationalRetrievalQAChain.fromLLM(
    streamingModel,
    vectorStore.asRetriever(),
    {
      returnSourceDocuments: true,
      memory: new BufferMemory({
        memoryKey: "chat_history",
        inputKey: "question", // 对应链条输入的键
        outputKey: "text", // 对应链条最终输出的键
        returnMessages: true, // 如果是用于聊天模型
      }),
      questionGeneratorChainOptions: {
        llm: nonStreamingModel,
      },
    }
  );
  /* 问一个问题 */
  const question = "总统对布雷耶尔大法官有何评价?";
  const res = await chain.call({ question });
  console.log({ streamedResponse });
};

外部管理内存

对于这个链条,如果你希望以自定义的方式格式化聊天记录(或者为了方便直接传入聊天消息),你也可以在 chain.call 方法中显式地传入聊天记录,而不使用 memory 选项,只需提供一个 chat_history 字符串或者 HumanMessages 和 AIMessages 的数组:

import { OpenAI } from "langchain/llms/openai";
import { ConversationalRetrievalQAChain } from "langchain/chains";
import { HNSWLib } from "langchain/vectorstores/hnswlib";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import * as fs from "fs";

/* 初始化要用来回答问题的 LLM */
const model = new OpenAI({});
/* 加载要进行问题回答的文件 */
const text = fs.readFileSync("state_of_the_union.txt", "utf8");
/* 将文本分割成块 */
const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
const docs = await textSplitter.createDocuments([text]);
/* 创建向量存储 */
const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
/* 创建链条 */
const chain = ConversationalRetrievalQAChain.fromLLM(
  model,
  vectorStore.asRetriever()
);
/* 问一个问题 */
const question = "总统对布雷耶法官说了什么?";
/* 可以是字符串或者聊天消息的数组 */
const res = await chain.call({ question, chat_history: "" });
console.log(res);
/* 问一个后续问题 */
const chatHistory = `${question}\n${res.text}`;
const followUpRes = await chain.call({
  question: "那好吗?",
  chat_history: chatHistory,
});
console.log(followUpRes);

自定义提示

如果你想进一步改变链式模型的行为,可以更改底层的问题生成链和问答链的提示。

有一种情况是,你可能想要改进链式模型对于聊天历史中的元问题的回答能力。默认情况下,问答链的唯一输入是从问题生成链生成的独立问题。当询问关于聊天历史中的信息的元问题时,这可能会带来挑战。

例如,如果你介绍了一个名叫Bob的朋友,并提到他的年龄为28岁,链式模型在问“Bob多大年纪了?”这样的问题时不能提供他的年龄。这种限制是因为机器人在向量存储中搜索Bob,而不是考虑消息历史。

你可以为问题生成链传递一个替代的提示,该提示还返回与答案相关的聊天历史的部分,从而允许问答链通过额外的上下文回答元问题:

import { ChatOpenAI } from "langchain/chat_models/openai";
import { ConversationalRetrievalQAChain } from "langchain/chains";
import { HNSWLib } from "langchain/vectorstores/hnswlib";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { BufferMemory } from "langchain/memory";

const CUSTOM_QUESTION_GENERATOR_CHAIN_PROMPT = \`给定以下对话和随后的问题,请返回包含问题相关上下文的对话历史摘录(如果存在),并将随后的问题重新表述为独立问题。
对话历史:
{chat_history}
随后的输入: {question}
你的回答应遵循以下格式:
\`\`\`
使用以下上下文片段来回答用户的问题。
如果你不知道答案,只需说你不知道,不要试图编造一个答案。
----------------

独立问题: 
\`\`\`
你的回答:\`;

const model = new ChatOpenAI({
  modelName: "gpt-3.5-turbo",
  temperature: 0,
});

const vectorStore = await HNSWLib.fromTexts(
  [
    "线粒体是细胞的动力源",
    "Foo是红色的",
    "Bar是红色的",
    "建筑物是由砖块建成的",
    "线粒体是由脂质构成的",
  ],
  [{ id: 2 }, { id: 1 }, { id: 3 }, { id: 4 }, { id: 5 }],
  new OpenAIEmbeddings()
);

const chain = ConversationalRetrievalQAChain.fromLLM(
  model,
  vectorStore.asRetriever(),
  {
    memory: new BufferMemory({
      memoryKey: "chat_history",
      returnMessages: true,
    }),
    questionGeneratorChainOptions: {
      template: CUSTOM_QUESTION_GENERATOR_CHAIN_PROMPT,
    },
  }
);

const res = await chain.call({
  question:
    "我有一个朋友叫Bob。他今年28岁。他想知道细胞的动力源是什么?",
});

console.log(res);
/*
  {
    text: "细胞的动力源是线粒体。"
  }
*/

const res2 = await chain.call({
  question: "Bob多大年纪了?",
});

console.log(res2); // Bob 今年28岁。

/*
  {
    text: "Bob 今年28岁。"
  }
*/

请注意,以这种方式在提示中添加更多上下文可能会使L
LM分心,无法获得其他相关的检索信息。



关联主题