Histórico do chat e perguntas de acompanhamento

O processo descrito acima funciona bem quando os usuários podem fazer apenas uma única pergunta. Mas este aplicativo também permite perguntas de acompanhamento, o que introduz algumas complicações adicionais. Por exemplo, é necessário armazenar todas as perguntas e respostas anteriores para que possam ser incluídas como contexto adicional ao enviar a nova pergunta ao LLM.

O histórico de bate-papo neste aplicativo é gerenciado por meio da classe ElasticsearchChatMessageHistory , outra classe que faz parte da integração do Elasticsearch com o Langchain. Cada grupo de perguntas e respostas relacionadas é gravado em um índice do Elasticsearch com uma referência ao ID da sessão utilizada.

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
)

Você deve ter notado na seção anterior que, embora a resposta do LLM seja transmitida ao cliente em partes, uma variável answer é gerada com a resposta completa. Isso permite que a resposta, juntamente com a pergunta, seja adicionada ao histórico após cada interação:

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

Se o cliente enviar um argumento session_id na string de consulta do URL da solicitação, presume-se que a pergunta seja feita no contexto de quaisquer perguntas anteriores na mesma sessão.

A abordagem adotada por este aplicativo para perguntas de acompanhamento é usar o LLM para criar uma pergunta concisa que resuma toda a conversa, a ser usada na fase de recuperação. O objetivo disso é evitar a execução de uma busca vetorial em um histórico potencialmente extenso de perguntas e respostas. Segue a lógica que executa essa tarefa:

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)

Isso tem muitas semelhanças com a forma como as perguntas principais são tratadas, mas neste caso não há necessidade de usar a interface de streaming do LLM, então o método invoke() é usado em vez disso.

Para condensar a pergunta, utiliza-se um prompt diferente, armazenado no arquivo **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:

Este prompt exibe todas as perguntas e respostas da sessão, além de uma nova pergunta de acompanhamento no final. O profissional com mestrado em Direito (LLM) deve elaborar uma pergunta simplificada que resuma todas as informações.

Para permitir que o LLM tenha o máximo de contexto possível na fase de geração, o histórico completo da conversa é adicionado ao prompt principal, juntamente com os documentos recuperados e a pergunta de acompanhamento. Segue a versão final do prompt, conforme utilizado no aplicativo de exemplo:

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:

Note que a forma como a pergunta condensada é utilizada pode ser adaptada às suas necessidades. Você pode descobrir que, para algumas aplicações, enviar a pergunta condensada também na fase de geração funciona melhor, reduzindo também a quantidade de tokens. Ou talvez não usar uma pergunta resumida e enviar sempre todo o histórico da conversa traga melhores resultados. Esperamos que agora você tenha uma boa compreensão de como este aplicativo funciona e possa experimentar diferentes opções para descobrir o que funciona melhor para o seu caso de uso.

Pronto para criar buscas de última geração?

Uma pesquisa suficientemente avançada não se consegue apenas com o esforço de uma só pessoa. O Elasticsearch é impulsionado por cientistas de dados, especialistas em operações de aprendizado de máquina, engenheiros e muitos outros que são tão apaixonados por buscas quanto você. Vamos nos conectar e trabalhar juntos para construir a experiência de busca mágica que lhe trará os resultados desejados.

Experimente você mesmo(a)