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.