LangChainとChainlitによるデータ連携型AIチャットボットの実装からデプロイまで | 第1回


# 第1回 | LangChainの導入
- LangChainをセットアップする
- 実際に質問してみて返事が来ることに感動する
- チェーンを理解する

## はじめに
本シリーズの目標は次の3つです！
1. [LangChain](https://python.langchain.com/docs/get_started/introduction)によるデータ連携型[^1]AIチャットボットの実装
2. [Chainlit](https://docs.chainlit.io/get-started/overview)によるUIの作成
3. デプロイして他端末からアクセスできるようにする

LangChainはLLMに基づくアプリを開発するためのフレームワークで、PythonとJavaScriptの2言語で提供されています。本シリーズではPythonを使います。
ChainlitはAIチャットボット向けの対話型UIが簡単に作れるインターフェースです。

この界隈は技術の新陳代謝がヒジョーーに早いので、ご使用のバージョンをご確認ください！2024年4月現在、私は次のバージョンを使っています。
- LanghChain: 0.1.14
- Chainlit: 1.0.502

### シリーズ一覧
| シリーズ | 内容                                                                                       |
| -------- | ------------------------------------------------------------------------------------------ |
| 第1回    | LangChainの導入 ← イマココ                                                                 |
| 第2回    | [LangServeでシンプルUIを作ってみる](https://qiita.com/Sego-don/items/1208a001a2d795225ada) |
| 第3回    | [LangSmithによるプロセス追跡](https://qiita.com/Sego-don/items/7d95864fd0d33192e3e5) |

[^1]: 「データ連携型」は事前に与えたPDFやウェブページなどのコンテンツに基づいて回答させるという意味で用いています。正確には[RAG](https://python.langchain.com/docs/use_cases/question_answering/)と呼ばれる仕組みを用います。

## セットアップ
LangChainの[Quickstart](https://python.langchain.com/docs/get_started/quickstart/)ページに則ってインストールと導入を進めていきます。

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

### モデルの選択
LangChainは色んなLLMを統一的に扱うことを目的としたフレームワークですので、LangChain自体がLLMを提供しているわけではありません。そこで外部のモデルを導入する必要があります。

私が使っているのはOpenAIです。ドキュメントも充実していますし、ネットでも色んな情報を見つけることができるのでおすすめです。色んなフレームワークを触っていると必ず最初に説明されてて、デファクトスタンダード感がありますね。ただ、お金がかかるのが難点なんですよね… でも今回のシリーズ全体の開発は1ドルくらいでできました。実際に色んな人に使ってもらうってなるとまた話は別ですが。

[LangChainのHP](https://python.langchain.com/docs/get_started/quickstart/#llm-chain)ではOpenAI以外のモデルの選択肢が提示されているので、OpenAI以外を使い方はご一読ください。Ollamaっていうのを使うとオープンソースなモデルを使えるみたいですね。以降ではOpenAIベースで進めていきますが、HPを参考に適宜読み替えたり実装を少し変更していただければと思います。

### インストール
次のコマンドで必要なパッケージをインストールします。OpenAIのモデルを使わないことにした方は```langchain-openai```は不要です。適宜必要なインストールを行ってください。```python-dotenv```は環境変数を管理する```.env```ファイルを使うためのパッケージです。
```bash
pip install langchain langchain-openai python-dotenv
```

### APIキーの設定
OpenAIのモデルを使うのにはAPIキーの設定が必要です。[こちら](https://platform.openai.com/account/api-keys)からキーを取得して、```OPENAI_API_KEY```という環境変数として登録しておきます。```export```コマンドなどを使って毎回登録しても問題はありませんが、面倒なので```.env```ファイルに記録しておくのがおすすめです。
```.env
OPENAI_API_KEY=先ほど取得したAPIキー
```
コード内で次のようにすれば```.env```ファイルから環境変数が読み込まれます。
```sample.py
from dotenv import load_dotenv
load_dotenv()
```
:::note warn
コードを外部に公開する際には```.env```ファイルを含まないようご注意ください。
:::

何らかの問題があってうまく環境変数が登録できなかった場合、pythonコードの先頭で
```sample.py
import os
os.environ["OPENAI_API_KEY"] = "先ほど取得したAPIキー"
```
とするとそのコード内でのみ使える環境変数が登録できますので、とりあえずはこれで次に進めるはずです。ただしコードにAPIキーをべた書きするのはセキュリティ上非常によろしくないので、このように環境変数を設定した場合にはコードのこの部分を他人に共有しないよう細心の注意を払ってください。

## 使ってみる
では早速LangChainを使ってみましょう！以下ではJupyter notebookを使っている前提でコーディングしていきます[^2]。
```python
from langchain_openai import ChatOpenAI
llm = ChatOpenAI()
llm.invoke("Qiitaとはなんですか")
```
> AIMessage(
> 　content='Qiitaは、プログラマーやエンジニア向けの技術情報共有プラットフォームです。ユーザーは自身の知識や経験を記事として投稿し、他のユーザーと共有することができます。主にプログラミング言語やフレームワーク、開発ツールなどの技術に関する情報が多く共有されています。Qiitaは日本発のサービスで、国内外の技術者の間で広く利用されています。', 
> 　response_metadata={
> 　　'token_usage': {'completion_tokens': 159, 'prompt_tokens': 14, 'total_tokens': 173}, 
> 　　'model_name': 'gpt-3.5-turbo', 
> 　　'system_fingerprint': 'fp_b28b39ffa8', 
> 　　'finish_reason': 'stop', 
> 　　'logprobs': None
> 　}, 
> 　id='run-d4c34ee2-2095-468d-ab8c-1f5ad8580204-0'
> )

（出力は一行になっていてで分かりづらかったので私が整形しました）
出力結果を見てみると、content、resposnse_metadata、idという要素を持ったAIMessageという型が返されているようです。contentにはChatGPTからの返事が、response_metadataにはモデル名などの情報が含まれていますね。

本来はOpenAIにはOpenAIの、他のモデルには他のモデルの質問の流儀と返答の流儀があるのでモデルに応じてプログラムを書き換えないといけないんですが、LangChainを使えばどのモデルを使っても同じ方法でやり取りできるのが素晴らしいですね！

[^2]: pythonファイルを実行して頂いても問題ありませんが、その場合は出力を適宜```print()```などしてください。

## チェーンの導入
LangChainにはその名の由来となったチェーンというキーコンセプトがあります。ここでは最も簡単なチェーンを導入してみましょう。

### プロンプト
チェーンを導入するにあたってまずはプロンプトと呼ばれるものを作成します。
```python
# promptの生成
from langchain_core.prompts import ChatPromptTemplate
prompt = ChatPromptTemplate.from_messages([
    ("system", "あなたはQiitaの専門家です。Qiitaに関すること以外には分からないと返答してください。"),
    ("user", "{input}")
])
prompt.invoke({"input": "Qiitaとは何ですか？"})
```
> ChatPromptValue(
> 　messages=[
> 　　SystemMessage(content='あなたはQiitaの専門家です。Qiitaに関すること以外には分からないと返答してください。'), 
> 　　HumanMessage(content='Qiitaとは何ですか？')
> 　]
> )

このように、最終行の```invoke()```でプロンプトを呼び出すと、システムのメッセージと人間のメッセージからなる一種の"会話履歴"が返されます。プロンプト内では変数```{input}```を用いているので、呼び出す際には```{"input": "..."}```を渡します。

ユーザーからの入力を直接llmに渡すのではなく、いったんプロンプトで生成した会話履歴をllmに渡すことで、ユーザーからの質問に"文脈"を与え、どのように返事をすべきか指示を与えられるわけです。

プロンプトとllmを連続して使うには、次のようにしたらいいですね。
```python
# この質問にはちゃんと答えてくれるが、
response_from_prompt = prompt.invoke({"input": "Qiitaとは何ですか?"})
llm.invoke(response_from_prompt)

# こちらには答えてくれない
response_from_prompt = prompt.invoke({"input": "明日の天気は何ですか?"})
llm.invoke(response_from_prompt)
```

ちなみに、今回は会話履歴を生成するプロンプトを利用していますが、他にも色んなものがあります。単に文字列を返すだけの```PromptTemplate```もよく使われます。（[参考](https://python.langchain.com/docs/modules/model_io/prompts/quick_start/)）

### チェーン
このようにインプット → プロンプト → llm → アウトプットという「つながり」がありますが、これを楽に利用できるのが「チェーン」です。今回のチェーンは```chain = prompt | llm```とすることで実装できます。次のようにするとプロンプトとllmのチェーンが作成され、先ほど自分で書いていたプロンプトからllmへの情報伝達を自動でやってくれます。「チェーン」っぽいでしょ？
```python
chain = prompt | llm 
chain.invoke({"input": "Qiitaとは何ですか？"}) # これだけでOK
```

```|```はパイプ演算子のようなものです。パイプ演算子というのはLinux等のシェル環境でよく使われる演算子で、あるコマンドの出力を次のコマンドの入力として渡すという意味があります。例えば、```ls | grep "txt"```は```ls```コマンドの出力を```grep```コマンドに渡すことで、ファイル名に"txt"が含まれるファイルのみを表示します。

### アウトプットパーサー
先ほど見たようにllmからの返答は次のようになっていて、本当に欲しい回答以外にもいろいろ情報があってややこしいです。
> AIMessage(
> 　content='Qiitaは、プログラマーやエンジニア向けの技術情報共有プラットフォームです。ユーザーは自身の知識や経験を記事として投稿し、他のユーザーと共有することができます。主にプログラミング言語やフレームワーク、開発ツールなどの技術に関する情報が多く共有されています。Qiitaは日本発のサービスで、国内外の技術者の間で広く利用されています。', 
> 　response_metadata={
> 　　'token_usage': {'completion_tokens': 159, 'prompt_tokens': 14, 'total_tokens': 173}, 
> 　　'model_name': 'gpt-3.5-turbo', 
> 　　'system_fingerprint': 'fp_b28b39ffa8', 
> 　　'finish_reason': 'stop', 
> 　　'logprobs': None
> 　}, 
> 　id='run-d4c34ee2-2095-468d-ab8c-1f5ad8580204-0'
> )

そこで、回答のみを表示して出力結果を見やすくするためのOutputParserなるものが用意されています。
```python
from langchain_core.output_parsers import StrOutputParser
chain = prompt | llm | StrOutputParser()
chain.invoke({"input": "Qiitaとは何ですか"})
```
> Qiitaは、プログラマーやエンジニア向けの技術情報共有プラットフォームです。ユーザーは自身の知識や経験を記事として投稿し、他のユーザーと共有することができます。主にプログラミング言語やフレームワーク、開発ツールなどの技術に関する情報が多く共有されています。Qiitaは日本発のサービスで、国内外の技術者の間で広く利用されています。

こうすることでAIMessage型のcontent要素のみを取り出してくれるわけですね。まあ、わざわざパーサーを使わなくても次のように同じことは実現できるわけですが…
```python
chain = prompt | llm
response = chain.invoke({"input": "Qiitaとは何ですか"})
response.content
```
チェーンに組み込めたほうが楽っていうのと、あとは他に用意されているパーサーを使えばもっと複雑な出力処理ができます（[参考](https://python.langchain.com/docs/modules/model_io/output_parsers/)）。

## おわりに
今回はLangChainの導入から簡単なチェーンの実装までを行いました。僕は初めて自分の環境からLLMを触ったとき返答が来てめちゃくちゃ感動しましたが、皆さんにも同じ感動を共有できれば嬉しいです！
次回はLangServeでシンプルなUIを実装します！

https://qiita.com/Sego-don/items/1208a001a2d795225ada