跳至主要内容

如何在问答链中添加聊天历史记录

先决条件

本指南假设您熟悉以下内容

在许多问答应用程序中,我们希望允许用户进行来回对话,这意味着应用程序需要某种形式的“记忆”,以记录过去的问题和答案,以及一些将这些内容纳入当前思考的逻辑。

在本指南中,我们将重点介绍 添加将历史消息纳入的逻辑,而不是聊天历史记录管理。 聊天历史记录管理在此处介绍

我们将从 Lilian Weng 的 LLM 驱动的自主代理 博客文章中构建的 Q&A 应用程序开始。我们需要更新现有应用程序的两个方面

  1. 提示:更新我们的提示以支持历史消息作为输入。
  2. 上下文化问题:添加一个子链,该子链将接收最新的用户问题,并在聊天历史记录的上下文中重新表述它。如果最新的问题引用了先前消息中的某些上下文,则需要此操作。例如,如果用户询问类似“你能详细说明第二个要点吗?”的后续问题,则在没有先前消息的上下文的情况下无法理解。因此,对于这样的问题,我们无法有效地执行检索。

设置

依赖项

在本演练中,我们将使用 OpenAI 聊天模型和嵌入以及内存向量存储,但此处显示的所有内容都适用于任何 ChatModelLLM嵌入 以及 VectorStore检索器

我们将使用以下包

npm install --save langchain @langchain/openai cheerio

我们需要设置环境变量 OPENAI_API_KEY

export OPENAI_API_KEY=YOUR_KEY

LangSmith

您使用 LangChain 构建的许多应用程序将包含多个步骤,以及对 LLM 调用的多次调用。随着这些应用程序变得越来越复杂,能够检查链或代理内部到底发生了什么变得至关重要。使用 LangSmith 是最好的方法。

请注意,LangSmith 不是必需的,但它很有帮助。如果您确实想使用 LangSmith,在您通过上面的链接注册后,请确保设置环境变量以开始记录跟踪

export LANGCHAIN_TRACING_V2=true
export LANGCHAIN_API_KEY=YOUR_KEY

# Reduce tracing latency if you are not in a serverless environment
# export LANGCHAIN_CALLBACKS_BACKGROUND=true

初始设置

import "cheerio";
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { OpenAIEmbeddings, ChatOpenAI } from "@langchain/openai";
import { pull } from "langchain/hub";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import {
RunnableSequence,
RunnablePassthrough,
} from "@langchain/core/runnables";
import { StringOutputParser } from "@langchain/core/output_parsers";

import { createStuffDocumentsChain } from "langchain/chains/combine_documents";

const loader = new CheerioWebBaseLoader(
"https://lilianweng.github.io/posts/2023-06-23-agent/"
);

const docs = await loader.load();

const textSplitter = new RecursiveCharacterTextSplitter({
chunkSize: 1000,
chunkOverlap: 200,
});
const splits = await textSplitter.splitDocuments(docs);
const vectorStore = await MemoryVectorStore.fromDocuments(
splits,
new OpenAIEmbeddings()
);

// Retrieve and generate using the relevant snippets of the blog.
const retriever = vectorStore.asRetriever();
// Tip - you can edit this!
const prompt = await pull<ChatPromptTemplate>("rlm/rag-prompt");
const llm = new ChatOpenAI({ model: "gpt-3.5-turbo", temperature: 0 });
const ragChain = await createStuffDocumentsChain({
llm,
prompt,
outputParser: new StringOutputParser(),
});

让我们看看这个提示实际上是什么样子的

console.log(prompt.promptMessages.map((msg) => msg.prompt.template).join("\n"));
You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.
Question: {question}
Context: {context}
Answer:
await ragChain.invoke({
context: await retriever.invoke("What is Task Decomposition?"),
question: "What is Task Decomposition?",
});
"Task Decomposition involves breaking down complex tasks into smaller and simpler steps to make them "... 243 more characters

上下文化问题

首先,我们需要定义一个子链,该子链将接收历史消息和最新的用户问题,并在问题引用历史信息中的任何信息时重新表述问题。

我们将使用一个提示,该提示在名为“chat_history”的名称下包含一个 MessagesPlaceholder 变量。这使我们能够使用“chat_history”输入键将消息列表传递到提示,这些消息将在系统消息之后以及包含最新问题的用户消息之前插入。

import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";

const contextualizeQSystemPrompt = `Given a chat history and the latest user question
which might reference context in the chat history, formulate a standalone question
which can be understood without the chat history. Do NOT answer the question,
just reformulate it if needed and otherwise return it as is.`;

const contextualizeQPrompt = ChatPromptTemplate.fromMessages([
["system", contextualizeQSystemPrompt],
new MessagesPlaceholder("chat_history"),
["human", "{question}"],
]);
const contextualizeQChain = contextualizeQPrompt
.pipe(llm)
.pipe(new StringOutputParser());

使用此链,我们可以提出引用先前消息的后续问题,并让它们重新表述为独立的问题

import { AIMessage, HumanMessage } from "@langchain/core/messages";

await contextualizeQChain.invoke({
chat_history: [
new HumanMessage("What does LLM stand for?"),
new AIMessage("Large language model"),
],
question: "What is meant by large",
});
'What is the definition of "large" in this context?'

带聊天历史记录的链

现在我们可以构建我们的完整 QA 链。

注意,我们添加了一些路由功能,以便仅在聊天历史记录不为空时运行“压缩问题链”。在这里,我们利用了这样一个事实,即如果 LCEL 链中的函数返回另一个链,那么该链本身将被调用。

import {
ChatPromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
import {
RunnablePassthrough,
RunnableSequence,
} from "@langchain/core/runnables";
import { formatDocumentsAsString } from "langchain/util/document";

const qaSystemPrompt = `You are an assistant for question-answering tasks.
Use the following pieces of retrieved context to answer the question.
If you don't know the answer, just say that you don't know.
Use three sentences maximum and keep the answer concise.

{context}`;

const qaPrompt = ChatPromptTemplate.fromMessages([
["system", qaSystemPrompt],
new MessagesPlaceholder("chat_history"),
["human", "{question}"],
]);

const contextualizedQuestion = (input: Record<string, unknown>) => {
if ("chat_history" in input) {
return contextualizeQChain;
}
return input.question;
};

const ragChain = RunnableSequence.from([
RunnablePassthrough.assign({
context: async (input: Record<string, unknown>) => {
if ("chat_history" in input) {
const chain = contextualizedQuestion(input);
return chain.pipe(retriever).pipe(formatDocumentsAsString);
}
return "";
},
}),
qaPrompt,
llm,
]);

const chat_history = [];

const question = "What is task decomposition?";
const aiMsg = await ragChain.invoke({ question, chat_history });

console.log(aiMsg);

chat_history.push(aiMsg);

const secondQuestion = "What are common ways of doing it?";
await ragChain.invoke({ question: secondQuestion, chat_history });
AIMessage {
lc_serializable: true,
lc_kwargs: {
content: "Task decomposition involves breaking down a complex task into smaller and simpler steps to make it m"... 358 more characters,
tool_calls: [],
invalid_tool_calls: [],
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: {}
},
lc_namespace: [ "langchain_core", "messages" ],
content: "Task decomposition involves breaking down a complex task into smaller and simpler steps to make it m"... 358 more characters,
name: undefined,
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: {
tokenUsage: { completionTokens: 83, promptTokens: 701, totalTokens: 784 },
finish_reason: "stop"
},
tool_calls: [],
invalid_tool_calls: []
}
AIMessage {
lc_serializable: true,
lc_kwargs: {
content: "Common ways of task decomposition include using simple prompting techniques like Chain of Thought (C"... 353 more characters,
tool_calls: [],
invalid_tool_calls: [],
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: {}
},
lc_namespace: [ "langchain_core", "messages" ],
content: "Common ways of task decomposition include using simple prompting techniques like Chain of Thought (C"... 353 more characters,
name: undefined,
additional_kwargs: { function_call: undefined, tool_calls: undefined },
response_metadata: {
tokenUsage: { completionTokens: 81, promptTokens: 779, totalTokens: 860 },
finish_reason: "stop"
},
tool_calls: [],
invalid_tool_calls: []
}

请查看第一个LangSmith 跟踪,以及第二个跟踪

我们已经介绍了如何添加应用程序逻辑来整合历史输出,但我们仍然手动更新聊天历史并将其插入到每个输入中。在实际的 Q&A 应用中,我们需要某种方法来持久化聊天历史,以及某种方法来自动插入和更新它。

为此,我们可以使用

有关如何将这些类组合在一起以创建有状态对话链的详细演练,请访问如何添加消息历史(内存) LCEL 页面。


此页面是否有帮助?


您也可以留下详细的反馈 在 GitHub 上.