聊天记录和后续问题
当用户只能提出一个问题时,上述流程就能很好地发挥作用。但该应用程序还允许提出后续问题,这就带来了一些额外的麻烦。例如,有必要存储以前的所有问题和答案,以便在向 LLM 发送新问题时将其作为额外的上下文。
该应用程序中的聊天记录通过ElasticsearchChatMessageHistory 类进行管理,该类是 Elasticsearch 与 Langchain 集成的另一个部分。每组相关的问题和答案都会被写入 Elasticsearch 索引,并引用所使用的会话 ID。
def get_elasticsearch_chat_message_history(index, session_id):
return ElasticsearchChatMessageHistory(
es_connection=elasticsearch_client, index=index, session_id=session_id
)
INDEX_CHAT_HISTORY = os.getenv(
"ES_INDEX_CHAT_HISTORY", "workplace-app-docs-chat-history"
)
chat_history = get_elasticsearch_chat_message_history(
INDEX_CHAT_HISTORY, session_id
)在上一节中,您可能已经注意到,尽管 LLM 的响应是分块流式传输给客户端的,但还是会生成一个包含完整响应的answer 变量。这样就可以在每次交互后将回复及其问题添加到历史记录中:
chat_history.add_user_message(question)
chat_history.add_ai_message(answer)如果客户端在请求 URL 的查询字符串中发送了session_id 参数,那么该问题就会被假定为在同一会话下的任何先前问题的上下文中提出的。
该应用程序对后续问题采取的方法是使用 LLM 创建一个浓缩问题,总结整个对话内容,用于检索阶段。这样做的目的是避免对可能存在的大量问题和答案历史记录进行矢量搜索。下面是执行这项任务的逻辑:
if len(chat_history.messages) > 0:
# create a condensed question
condense_question_prompt = render_template(
'condense_question_prompt.txt', question=question,
chat_history=chat_history.messages)
condensed_question = get_llm().invoke(condense_question_prompt).content
else:
condensed_question = question
docs = store.as_retriever().invoke(condensed_question)这与主问题的处理方法有很多相似之处,但在这种情况下,不需要使用 LLM 的流接口,而是使用invoke() 方法。
要压缩问题,会使用不同的提示,存储在文件 **api/templates/condense_question_prompt.txt`中:
Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question, in its original language.
Chat history:
{% for dialogue_turn in chat_history -%}
{% if dialogue_turn.type == 'human' %}Question: {{ dialogue_turn.content }}{% elif dialogue_turn.type == 'ai' %}Response: {{ dialogue_turn.content }}{% endif %}
{% endfor -%}
Follow Up Question: {{ question }}
Standalone question:该提示将显示会议中的所有问题和回答,以及会议结束时的新后续问题。已指示法律硕士提供一个简化的问题,概括所有信息。
为了让 LLM 在生成阶段尽可能多地了解上下文,对话的完整历史会与检索到的文件和后续问题一起添加到主提示中。下面是示例应用程序中使用的提示符的最终版本:
Use the following passages and chat history to answer the user's question.
Each passage has a NAME which is the title of the document. After your answer, leave a blank line and then give the source name of the passages you answered from. Put them in a comma separated list, prefixed with SOURCES:.
Example:
Question: What is the meaning of life?
Response:
The meaning of life is 42.
SOURCES: Hitchhiker's Guide to the Galaxy
If you don't know the answer, just say that you don't know, don't try to make up an answer.
----
{% for doc in docs -%}
---
NAME: {{ doc.metadata.name }}
PASSAGE:
{{ doc.page_content }}
---
{% endfor -%}
----
Chat history:
{% for dialogue_turn in chat_history -%}
{% if dialogue_turn.type == 'human' %}Question: {{ dialogue_turn.content }}{% elif dialogue_turn.type == 'ai' %}Response: {{ dialogue_turn.content }}{% endif %}
{% endfor -%}
Question: {{ question }}
Response:请注意,压缩问题的使用方式可以根据您的需要进行调整。您可能会发现,对于某些应用,在生成阶段也发送浓缩问题效果更好,还能减少令牌数量。或者根本不使用浓缩问题,而总是发送整个聊天历史记录,也许效果会更好。希望您现在已经对该应用程序的工作原理有了很好的了解,并能尝试使用不同的提示,找到最适合您使用情况的方法。