ConversationalRetrievalChainの使い方と解説

# 0. はじめに
VectorStoreを用いた質問応答モデルは、生成AIの分野で急速に進化しています。langchain ライブラリの ConversationalRetrievalChainはシンプルな質問応答モデルの実装を実現する方法の一つです。この記事では、その使い方と実装の詳細について解説します。

# 1. ConversationalRetrievalChainの概念
![conversationalRetrievalChain.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3565029/1f07eb1e-d346-2ca3-d697-53136c7a1edd.jpeg)
1. ConversationalRetrievalChainでは、まずLLMが質問と会話履歴を受け取って、質問の言い換え（生成質問）を行います。
2. 次に、言い換えられた質問をもとにVectorStoreに関連情報（チャンク群）を探しに行きます。
3. 生成質問にVectorStoreからの関連情報を合わせて、再度LLMに問い合わせ、最終的な回答を得ます。

このような一連の操作がこのモジュールの中で行われています。会話履歴をもとに質問の言い換えを行うため、質問者との会話のラリーがある程度スムースにできることになります。テクニカルにはトークン数の制限などで、ラリーの回数が制限されますが、実用上は２、3回のラリーに耐えられれば、まあそんなに違和感なく使えるのかな、というところです。

# 2. VectorStore Retrieverの用意
OpenAIEmbeddingsを使用して、VectorStore (今回はChroma DB)をロードします。
```py
loaded_db = Chroma(
    persist_directory='testdb',
    embedding_function=OpenAIEmbeddings()
)
```

# 3. ChatOpenAIモデルの設定
今回使うLLMも定義しておきます。
```py
# ChatOpenAIモデルの設定
LLM = ChatOpenAI(
    model_name='gpt-3.5-turbo',
    temperature=0
)
```

# 4. PromptTemplateの作成
質問応答のためのプロンプトテンプレートを作成します。上記の概念図から分かるように、Conversational RetrievalChain内では2回LLMにクエリを投げることになるので、プロンプトも２種類用意する必要があります。

最初のQuestion generatorの部分に関しては、デフォルトでは`CONDENSE_QUESTION_PROMPT`が使われますが、もちろん書き換えてもOKです。ちなみに`CONDENSE_QUESTION_PROMPT`を日本語で書くとこんな感じです。

```py
template_qg = """
次の会話に対しフォローアップの質問があるので、フォローアップの質問を独立した質問に言い換えなさい。
        
チャットの履歴:
{chat_history}
        
フォローアップの質問:
{question}
        
言い換えられた独立した質問:"""

prompt_qg = PromptTemplate(
        template=template_qg,
        input_variables=["chat_history", "question"],
        output_parser=None,
        partial_variables={},
        template_format='f-string',
        validate_template=True,
)
```

そして、最終のcombined document chainに使う方のプロンプトはこんな感じです。

```py
prompt_template_qa = """You are a helpful assistant. Please answer in Japanese! If the context is not relevant, please answer the question by using your own knowledge about the topic. 

{context}

Question: {question}
Answer in Japanese:"""

prompt_qa = PromptTemplate(
        template=prompt_template_qa, 
        input_variables=["context", "question"]
)
chain_type_kwargs = {"prompt": prompt_qa}
```

# 5. ConversationalRetrievalChainの初期化
プロンプトが完成したら、Question Generation, VectorStore Retrieval, Combined document Chainを全てConversational RetrievalChainに入れ込みます。chain_typeにstuffを使っていますが、他のchain_type (refine, map_reduceなど）でもOKです。stuffはトークン数はかさみますが、他のchain_typeよりも圧倒的に早いです。トークン数が増えれば増えるほど顕著です。

```py
retriever = loaded_db.as_retriever()
question_generator = LLMChain(llm=LLM, prompt=prompt_qg)
doc_chain = load_qa_chain(llm=LLM, chain_type="stuff", prompt=prompt_qa)

qa = ConversationalRetrievalChain(
        retriever=retriever,
        question_generator=question_generator,
        combine_docs_chain=doc_chain,
)
chat_history = []
```

# 6. 質問の処理
作成したConversationalRetrievalChainを使用して、質問のリストを処理します。

```py
questions = [
　'○○社にはどのようなDX支援サービスがありますか',
　...
]

for query in questions:
    print(f"Q: {query}")
    result = qa.run({"question": query, "chat_history": chat_history})
    print(f"A: {result}")
    chat_history.append((query, result))
```

# 7. まとめ
ConversationalRetrievalChainは、質問応答モデルの構築に非常に役立つツールです。このブログを通じて、その使い方と実装の詳細を理解できたことを願っています。

最後にインポートが必要なライブラリのリストです。
```py
import os
import openai
from langchain.embeddings.openai import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOpenAI
from langchain import PromptTemplate, LLMChain
from langchain.chains import ConversationalRetrievalChain
from langchain.chains.question_answering import load_qa_chain
from langchain.chains.conversational_retrieval.prompts import CONDENSE_QUESTION_PROMPT
```
＊本記事はcode snippetをもとにChatGPTに作らせています。