LangChain Expression Language (LCEL)でのRAGに入門してみた

# はじめに
普段、RAGを使ったシステムをよく作っているのですがLangChainでやったことがなかったので何番煎じかわかりませんがやってみた記録として残します。

この記事はLCELの何となくの雰囲気を知りたい人、ちょこっとRAGを作ってみたい人向けです。


# 手順
手順は、LangChainのドキュメントを参考に進めます。

https://python.langchain.com/docs/expression_language/get_started/

以下の組織が提供するモデルを使えますが、今回はOpenAIのものを使います。また、APIキーは取得済みの前提で進めます。

- OpenAI
- Anthropic
- Google
- Cohere
- FireworksAI
- MistralAI
- TogetherAI


## インストール
```
pip install --upgrade --quiet  langchain-core langchain-community langchain-openai
```

## とりあえず動かす

RAG云々の前にLCELの記法で書かれたプログラムを動かしてみます。
サンプルコードが載っていたのでそのまま動かしてみました。

```python:sample.py
import getpass
import os
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

os.environ["OPENAI_API_KEY"] = getpass.getpass()

prompt = ChatPromptTemplate.from_template("何かジョークを言って。お題: {topic}")
model = ChatOpenAI(model="gpt-3.5-turbo")
output_parser = StrOutputParser()

chain = prompt | model | output_parser

result = chain.invoke({"topic": "アイスクリーム"})
print(result)
```

```shell:結果
アイスクリームが好きな理由は、何度食べても「冷たい！」と言っても怒られないからです！
```

無事ジョークを言ってくれました。
ポイントは`chain = prompt | model | output_parser`の部分ですね。

`|`で繋ぐことで左から右に処理が進んでいきます。

`prompt`、 `model`、`output_parser`でそれぞれ何が起こっているか見た方が理解が進みそうなので、`|`で一括で処理するのではなく要素ごとに出力するようにしてみました。

```python:sample-details.py
import getpass
import os
import json
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

os.environ["OPENAI_API_KEY"] = getpass.getpass()

# Prompt
prompt = ChatPromptTemplate.from_template("何かジョークを言って。お題: {topic}")
prompt_value = prompt.invoke({"topic": "アイスクリーム"})
print("Prompt:")
print(prompt_value)

# Model
model = ChatOpenAI(model="gpt-3.5-turbo")
message = model.invoke(prompt_value)
print("Model:")
print("- Message content")
print(message.content)
print("- Message metadata")
print(json.dumps(message.response_metadata, indent=4))

# Output parser
output_parser = StrOutputParser()
result = output_parser.invoke(message)
print("Output parser:")
print(result)
```

```shell:結果
Prompt:
messages=[HumanMessage(content='何かジョークを言って。お題: アイスクリーム')]
Model:
- Message content
アイスクリームって、氷のように冷たいけど、人気は熱いよね！
- Message metadata
{
    "token_usage": {
        "completion_tokens": 33,
        "prompt_tokens": 28,
        "total_tokens": 61
    },
    "model_name": "gpt-3.5-turbo",
    "system_fingerprint": null,
    "finish_reason": "stop",
    "logprobs": null
}
- Entire message
content='アイスクリームって、氷のように冷たいけど、人気は熱いよね！' response_metadata={'token_usage': {'completion_tokens': 33, 'prompt_tokens': 28, 'total_tokens': 61}, 'model_name': 'gpt-3.5-turbo', 'system_fingerprint': None, 'finish_reason': 'stop', 'logprobs': None} id='run-32b5af3f-2481-447e-a356-765305a40ac6-0'
Output parser:
アイスクリームって、氷のように冷たいけど、人気は熱いよね！
```

想像通りというか特に違和感のない結果かと思います。

1. プロンプトのテンプレートの変数を辞書型で渡してプロンプトを作る
1. そのプロンプトをモデルに渡すとメッセージをが返る
1. パースして文字列部分を返す
というだけですね。

## RAGしてみる
とりあえず、動いてざっくりわかったところでRAGも試してみます。

まずは必要なライブラリを入れるところからです。
```
pip install langchain docarray
```

`docarray`でVectorStoreをメモリに簡易的に用意して、RAGしてみます。

サンプルコードと実行結果です。その後に要素ごとに説明を書いてみます。
```python:lcel-rag-trial.py
from langchain_community.vectorstores import DocArrayInMemorySearch
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnableParallel, RunnablePassthrough
from langchain_openai import OpenAIEmbeddings
from langchain_openai import ChatOpenAI

vectorstore = DocArrayInMemorySearch.from_texts(
    # Vector Storeに保持する文章群
    [
        "Vantiqにはクラウドバージョンとオンプレバージョンがあります", 
        "Vantiqのクラウドバージョンはパブリッククラウドとプライベートクラウドがあります"
    ],
    embedding=OpenAIEmbeddings()
)
retriever = vectorstore.as_retriever()

template = """次の参考情報のみに基づいて質問に答えてください
参考情報: {reference_info}

質問: {question}
"""
prompt = ChatPromptTemplate.from_template(template)
model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
output_parser = StrOutputParser()

setup_and_retrieval = RunnableParallel(
    {"reference_info": retriever, "question": RunnablePassthrough()}
)
chain = setup_and_retrieval | prompt | model | output_parser

user_question = "Vantiqのバージョンにはどんなものがありますか。"
result = chain.invoke(user_question)

print("結果:")
print(result)
```
```shell:結果
結果:
Vantiqのバージョンにはクラウドバージョンとオンプレバージョンがあります。クラウドバージョンにはパブリッククラウドとプライベートクラウドがあります。
```
無事、回答できています。


まずVectorStoreに登録している内容です。`Vantiqにはクラウドバージョンとオンプレバージョンがあります`と`Vantiqのクラウドバージョンはパブリッククラウドとプライベートクラウドがあります`という２つの情報を今回は登録しています。
```python:登録ドキュメント
vectorstore = DocArrayInMemorySearch.from_texts(
    # VectorStoreに保持する文章群
    [
        "Vantiqにはクラウドバージョンとオンプレバージョンがあります", 
        "Vantiqのクラウドバージョンはパブリッククラウドとプライベートクラウドがあります"
    ],
    embedding=OpenAIEmbeddings()
)
```
次に、プロンプトについてです。`{reference_info}`にはVectorStoreに登録された内容うち、質問された内容と類似度が高いものが入ります。`{question}`にはユーザーからの質問が入ります。
```python:プロンプト
template = """次の参考情報のみに基づいて質問に答えてください
参考情報: {reference_info}

質問: {question}
"""
```
モデルは最初とほぼ同じですが`temperature`の設定を変更したかったのでその設定を行なっています。
```python:モデル
model = ChatOpenAI(model="gpt-3.5-turbo", temperature=0)
```

# 最後に
今回実施した内容はかなり基礎的な内容で、実際には類似性検索周りのチューニングやリランキング、そもそも入力内容に対してRAGの実行が必要かどうかの判断などなどやることは多いかと思います。

その辺りも触っていこうと思います。