Historique du chat et questions complémentaires

Le processus décrit ci-dessus fonctionne bien lorsque les utilisateurs ne peuvent poser qu'une seule question. Mais cette application permet également de poser des questions de suivi, ce qui introduit quelques complications supplémentaires. Par exemple, il est nécessaire de stocker toutes les questions et réponses précédentes, afin qu'elles puissent être incluses comme contexte supplémentaire lors de l'envoi de la nouvelle question au mécanisme d'apprentissage tout au long de la vie.

L'historique du chat dans cette application est géré par la classe ElasticsearchChatMessageHistory, une autre classe qui fait partie de l'intégration d'Elasticsearch avec Langchain. Chaque groupe de questions et de réponses liées est écrit dans un index Elasticsearch avec une référence à l'identifiant de session utilisé.

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
)

Vous avez peut-être remarqué dans la section précédente que même si la réponse du LLM est transmise au client par morceaux, une variable answer est générée avec la réponse complète. Ainsi, la réponse, accompagnée de la question, peut être ajoutée à l'historique après chaque interaction :

chat_history.add_user_message(question)
chat_history.add_ai_message(answer)

Si le client envoie un argument session_id dans la chaîne de requête de l'URL de la demande, la question est supposée être posée dans le contexte de toutes les questions précédentes de la même session.

L'approche adoptée par cette application pour les questions de suivi consiste à utiliser le LLM pour créer une question condensée qui résume l'ensemble de la conversation, à utiliser pour la phase de recherche. L'objectif est d'éviter d'effectuer une recherche vectorielle sur un historique potentiellement important de questions et de réponses. Voici la logique qui permet d'effectuer cette tâche :

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)

Cela présente de nombreuses similitudes avec la manière dont les questions principales sont traitées, mais dans ce cas, il n'est pas nécessaire d'utiliser l'interface de streaming du LLM, et la méthode invoke() est donc utilisée à la place.

Pour condenser la question, une invite différente est utilisée, stockée dans le fichier **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:

Cette invite affiche toutes les questions et réponses de la session, ainsi que la nouvelle question de suivi à la fin. Le LLM est chargé de fournir une question simplifiée qui résume toutes les informations.

Pour permettre au LLM de disposer d'un contexte aussi large que possible lors de la phase de génération, l'historique complet de la conversation est ajouté à l'invite principale, ainsi que les documents récupérés et la question de suivi. Voici la version finale de l'invite telle qu'elle est utilisée dans l'exemple d'application :

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:

Notez que la façon dont la question condensée est utilisée peut être adaptée à vos besoins. Pour certaines applications, il peut s'avérer plus efficace d'envoyer la question condensée dès la phase de génération, ce qui permet également de réduire le nombre de jetons. Ou peut-être que le fait de ne pas utiliser de question condensée du tout et d'envoyer toujours l'historique complet de la discussion donne de meilleurs résultats. Nous espérons que vous avez maintenant une bonne compréhension du fonctionnement de cette application et que vous pouvez expérimenter différentes invites pour trouver ce qui fonctionne le mieux pour votre cas d'utilisation.

Prêt à créer des expériences de recherche d'exception ?

Une recherche suffisamment avancée ne se fait pas avec les efforts d'une seule personne. Elasticsearch est alimenté par des data scientists, des ML ops, des ingénieurs et bien d'autres qui sont tout aussi passionnés par la recherche que vous. Mettons-nous en relation et travaillons ensemble pour construire l'expérience de recherche magique qui vous permettra d'obtenir les résultats que vous souhaitez.

Jugez-en par vous-même