Chatverlauf und Folgefragen
Das oben beschriebene Verfahren funktioniert gut, wenn die Benutzer nur eine einzige Frage stellen können. Diese Anwendung ermöglicht aber auch Nachfragen, was einige zusätzliche Komplikationen mit sich bringt. Beispielsweise ist es notwendig, alle vorherigen Fragen und Antworten zu speichern, damit diese als zusätzlicher Kontext bei der Übermittlung der neuen Frage an den LLM einbezogen werden können.
Der Chatverlauf in dieser Anwendung wird über die Klasse ElasticsearchChatMessageHistory verwaltet, eine weitere Klasse, die Teil der Elasticsearch-Integration mit Langchain ist. Jede Gruppe zusammengehöriger Fragen und Antworten wird in einem Elasticsearch-Index unter Angabe der verwendeten Sitzungs-ID gespeichert.
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
)Möglicherweise ist Ihnen im vorherigen Abschnitt aufgefallen, dass, obwohl die Antwort des LLM in Blöcken an den Client gestreamt wird, eine Variable answer mit der vollständigen Antwort generiert wird. Dies dient dazu, die Antwort zusammen mit der dazugehörigen Frage nach jeder Interaktion der Historie hinzuzufügen:
chat_history.add_user_message(question)
chat_history.add_ai_message(answer)Sendet der Client ein session_id -Argument in der Abfragezeichenfolge der Anfrage-URL, so wird davon ausgegangen, dass die Frage im Kontext aller vorherigen Fragen innerhalb derselben Sitzung gestellt wird.
Der Ansatz dieser Anwendung für Folgefragen besteht darin, mithilfe des LLM eine komprimierte Frage zu erstellen, die das gesamte Gespräch zusammenfasst und für die Abrufphase verwendet wird. Der Zweck dieser Vorgehensweise besteht darin, eine Vektorsuche in einer potenziell großen Historie von Fragen und Antworten zu vermeiden. Hier ist die Logik, die diese Aufgabe ausführt:
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)Dies weist viele Ähnlichkeiten mit der Art und Weise auf, wie die Hauptfragen behandelt werden, aber in diesem Fall besteht keine Notwendigkeit, die Streaming-Schnittstelle des LLM zu verwenden, daher wird stattdessen die invoke() -Methode verwendet.
Um die Frage zu verkürzen, wird eine andere Aufforderung verwendet, die in der Datei **api/templates/condense_question_prompt.txt` gespeichert ist:
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:Diese Eingabeaufforderung zeigt alle Fragen und Antworten aus der Sitzung sowie die neue Folgefrage am Ende an. Der LLM wird angewiesen, eine vereinfachte Frage zu formulieren, die alle Informationen zusammenfasst.
Um dem LLM in der Generierungsphase so viel Kontext wie möglich zu bieten, wird der Hauptaufforderung die vollständige Gesprächshistorie zusammen mit den abgerufenen Dokumenten und der Folgefrage hinzugefügt. Hier ist die endgültige Version der Eingabeaufforderung, wie sie in der Beispielanwendung verwendet wird:
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:Beachten Sie bitte, dass die Verwendung der Kurzform der Frage an Ihre Bedürfnisse angepasst werden kann. Möglicherweise stellen Sie fest, dass es bei einigen Anwendungen besser funktioniert, die komprimierte Frage bereits in der Generierungsphase zu senden, wodurch sich auch die Anzahl der Tokens reduziert. Oder vielleicht erzielt man bessere Ergebnisse, wenn man gar keine Kurzfrage stellt, sondern immer den gesamten Chatverlauf sendet. Ich hoffe, Sie haben nun ein gutes Verständnis davon, wie diese Anwendung funktioniert, und können mit verschiedenen Eingabeaufforderungen experimentieren, um herauszufinden, was für Ihren Anwendungsfall am besten geeignet ist.