채팅 기록 및 후속 질문

위에서 설명한 프로세스는 사용자가 한 가지 질문만 할 수 있을 때 잘 작동합니다. 그러나 이 애플리케이션은 후속 질문도 허용하므로 몇 가지 추가적인 문제가 발생합니다. 예를 들어 이전 질문과 답변을 모두 저장하여 새 질문을 LLM에 보낼 때 추가 컨텍스트로 포함할 수 있도록 해야 합니다.

이 애플리케이션의 채팅 기록은 Langchain과 Elasticsearch 통합의 일부인 또 다른 클래스인 ElasticsearchChatMessageHistory 클래스를 통해 관리됩니다. 관련 질문과 답변의 각 그룹은 사용된 세션 ID에 대한 참조와 함께 Elasticsearch 인덱스에 기록됩니다.

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
)

이전 섹션에서 LLM의 응답이 청크로 클라이언트에 스트리밍되지만 전체 응답이 포함된 answer 변수가 생성되는 것을 보셨을 것입니다. 이는 각 상호작용 후에 질문과 함께 응답이 기록에 추가될 수 있도록 하기 위한 것입니다:

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

클라이언트가 요청 URL의 쿼리 문자열에 session_id 인수를 보내면 해당 질문은 동일한 세션의 이전 질문의 맥락에서 이루어진 것으로 간주됩니다.

이 애플리케이션에서 후속 질문에 대해 취하는 접근 방식은 LLM을 사용하여 전체 대화를 요약하는 압축 질문을 만들어 검색 단계에 사용하는 것입니다. 이는 잠재적으로 방대한 질문 및 답변 기록에 대해 벡터 검색을 실행하지 않기 위한 것입니다. 이 작업을 수행하는 로직은 다음과 같습니다:

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)

이는 주요 질문이 처리되는 방식과 많은 유사점이 있지만 이 경우에는 LLM의 스트리밍 인터페이스를 사용할 필요가 없으므로 대신 invoke() 메서드가 사용됩니다.

질문을 압축하기 위해 다른 프롬프트가 사용되며, 이는 **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:

이 프롬프트는 세션의 모든 질문과 답변, 그리고 마지막에 새로운 후속 질문을 렌더링합니다. LLM은 모든 정보를 요약한 간소화된 질문을 제공하도록 지시받습니다.

LLM이 생성 단계에서 가능한 한 많은 맥락을 파악할 수 있도록 대화의 전체 기록이 검색된 문서 및 후속 질문과 함께 기본 프롬프트에 추가됩니다. 다음은 예제 애플리케이션에서 사용된 프롬프트의 최종 버전입니다:

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:

압축된 질문의 사용 방식은 필요에 따라 조정할 수 있다는 점에 유의하세요. 일부 애플리케이션의 경우 생성 단계에서도 압축된 질문을 전송하는 것이 더 효과적이며 토큰 수 또한 줄일 수 있습니다. 또는 아예 압축된 질문을 사용하지 않고 항상 전체 채팅 내역을 보내는 것이 더 나은 결과를 얻을 수도 있습니다. 이제 이 애플리케이션의 작동 방식을 잘 이해하셨기를 바라며, 다양한 프롬프트를 실험하여 사용 사례에 가장 적합한 것을 찾을 수 있기를 바랍니다.

최첨단 검색 환경을 구축할 준비가 되셨나요?

충분히 고급화된 검색은 한 사람의 노력만으로는 달성할 수 없습니다. Elasticsearch는 여러분과 마찬가지로 검색에 대한 열정을 가진 데이터 과학자, ML 운영팀, 엔지니어 등 많은 사람들이 지원합니다. 서로 연결하고 협력하여 원하는 결과를 얻을 수 있는 마법 같은 검색 환경을 구축해 보세요.

직접 사용해 보세요