对话式检索问答
对话式检索问答链在检索问答链的基础上提供了一个聊天历史组件。
首先,它将聊天历史(可以是显式传入的,也可以是从提供的存储器中检索到的)和问题合并成一个独立的问题,然后从检索器中查找相关的文档,最后将这些文档和问题传递给问答链以返回一个响应。
要创建一个对话式检索问答链,你需要一个检索器。在下面的示例中,我们将从一个向量存储器创建一个检索器,这个存储器可以从嵌入中创建。
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。- 如果您使用此选项并传递一个内存实例,请将内存实例上的
inputKey
和outputKey
设置为与链输入和最终对话链输出相同的值。默认值分别为"question"
和"text"
,指定内存应存储的值。
- 如果您使用此选项并传递一个内存实例,请将内存实例上的
内置记忆
这是一个使用更快的低级语言模型(LLM)生成问题和一个更慢、更全面的LLM生成最终答案的定制示例。它使用一个内置的记忆对象,并返回引用的源文件。因为我们设置了 returnSourceDocuments
并且从链中返回多个值,所以我们必须在内存实例上设置 inputKey
和 outputKey
,让它知道要存储哪些值。
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分心,无法获得其他相关的检索信息。