Historial de chats y preguntas de seguimiento

El proceso descrito anteriormente funciona bien cuando los usuarios solo pueden hacer una sola pregunta. Pero esta aplicación también permite preguntas de seguimiento, lo que introduce algunas complicaciones adicionales. Por ejemplo, es necesario almacenar todas las preguntas frecuentes anteriores para que puedan incluir como contexto adicional al enviar la nueva pregunta al LLM.

El historial de chat en esta aplicación se gestiona a través de la clase ElasticsearchChatMessageHistory , otra clase que forma parte de la integración de Elasticsearch con Langchain. Cada grupo de preguntas frecuentes relacionadas se escribe en un índice de Elasticsearch con referencia al ID de sesión empleado.

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
)

Quizá notaste en la sección anterior que, aunque la respuesta del LLM se transmite al cliente en fragmentos, se genera una variable answer con la respuesta completa. Esto es para que la respuesta, junto con su pregunta, pueda agregar al historial tras cada interacción:

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

Si el cliente envía un argumento session_id en la cadena de consulta de la URL de la solicitud, entonces se asume que la pregunta se hizo en el contexto de cualquier pregunta previa en esa misma sesión.

El enfoque que adopta esta aplicación para las preguntas de seguimiento es emplear el LLM para crear una pregunta condensada que resume toda la conversación, para usar en la fase de recuperación. El propósito de esto es evitar realizar una búsqueda vectorial sobre un historial potencialmente extenso de preguntas frecuentes. Esta es la lógica que realiza esta tarea:

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)

Esto tiene muchas similitudes con cómo se manejan las preguntas principales, pero en este caso no es necesario usar la interfaz de streaming del LLM, así que se emplea el método invoke() en su lugar.

Para resumir la pregunta, se emplea un prompt diferente, almacenado en el archivo **api/plantillas/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:

Este prompt muestra todas las preguntas frecuentes de la sesión, además de la nueva pregunta de seguimiento al final. El LLM recibe instrucciones para proporcionar una pregunta simplificada que resuma toda la información.

Para que el LLM tenga la mayor cantidad posible de contexto en la fase de generación, se agrega la historia completa de la conversación al prompt principal, junto con los documentos recuperados y la pregunta de seguimiento. Aquí está la versión final del prompt tal como se usa en la aplicación de ejemplo:

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:

Debes tener en cuenta que la forma en que se emplea la pregunta condensada puede adaptar a tus necesidades. Puede que para algunas aplicaciones envíes la pregunta condensada también en la fase de generación funcione mejor, reduciendo además el número de tokens. O quizá no usar una pregunta condensada y enviar siempre todo el historial del chat te da mejores resultados. Espero que ahora entiendas bien cómo funciona esta aplicación y puedas experimentar con diferentes prompts para encontrar lo que mejor se adapte a tu caso.

¿Estás listo para crear experiencias de búsqueda de última generación?

No se logra una búsqueda suficientemente avanzada con los esfuerzos de uno. Elasticsearch está impulsado por científicos de datos, operaciones de ML, ingenieros y muchos más que son tan apasionados por la búsqueda como tú. Conectemos y trabajemos juntos para crear la experiencia mágica de búsqueda que te dará los resultados que deseas.

Pruébalo tú mismo