ChatGPT APIの運用で必須のツール: LangChainの使い方まとめ (1)

こんにちは！逆瀬川( https://twitter.com/gyakuse )です！
今日はLangChainの使い方について書いていこうと思います。

## ChatGPT API の欠点について

LangChainについて書く前に、ChatGPT APIの使いづらい部分をまとめていきたいと思います。
これを考えておくと、なぜLangChainが必要であるかということがわかり、さらに今後どのような機能が搭載されうるか/されるべきかということがわかります。
ChatGPT APIを使う際の難しい部分は一般的に以下のようにまとめられます。

- プロンプトの共通化や管理が面倒くさい
- 最近の事実をベースとした質問-応答が難しい
- 最大の入出力合計が4096トークン(約3000字)であるため、長い情報を持たせることがしづらい
- ExcelやCSV、PDF等を直接読み込ませることができない
- 出力の処理のチェーンの実装がたいへん

これらの課題は、以下のようなサービスを作る際に問題となります。

- 人間らしいふるまいをするChatbotの開発
- 任意の書類を処理するシステムの開発
- Make/IFTTTのような言語モデルが途中に入った使ったツール連携の開発

LangChainはそれらの課題を克服するためのツールです。
実際に、わたしが実装した[議事録書き出し・サマリ作成](https://qiita.com/sakasegawa/items/3855472a8566ea302a99)や[CSVデータからのレポート自動作成](https://qiita.com/sakasegawa/items/7bde39583a1c076ba526)など、いくつかのシステムはLangChainで置き換えることが可能です。

## LangChainが実装するもの

上記を踏まえて、以下のようなものが必要であるということがわかります。

- プロンプト管理機構
- 任意の形式のファイル入力への対応
- ローカルの文章検索
- 会話記憶
- 情報要約
- botとしてのインターフェイス

こうしたてんこもり系ツールは設計が難しいため、今後破壊的な変更などもある可能性がありますが、
少なくとも利便性を加味して変更を追っておく必要はあるでしょう。

## LangChainを使わないという選択

ちなみにわたしはLangChainは中身は追っていますが、ほとんど使っていません。
理由はいくつかありますが、単純に破壊的変更があった場合に動かない/古いコードとなってしまうことと、
機序を理解して実装しておきたいというモチベーションがあるためです。
また、シンプルな実装であればほとんど同じ機能を作ることはコストがあまりかかりません。

それと、個人的には、データの入力部分と記憶の部分は独立していると嬉しい気持ちがあります。
そうした意味で[LlamaIndex](https://github.com/jerryjliu/gpt_index)も便利なのですが、これもデカくなっているので注意が必要と考えます。

とはいえサクッと実装できるうれしみが強いので、とりあえず実装して公開するというときには積極的に使っていくべきでしょう。ちなみにmicrosoftから出た画像対話(画像に関するQAや編集等)を行えるシステムの論文でもLangChainが内部的に使われています。

https://github.com/microsoft/visual-chatgpt

## LangChainの各モジュール

それでは、LangChainに実際に用意されている各モジュールを見ていきます。
なお、副読用としてcolabを用意しました。動かしながら遊びましょう。

https://colab.research.google.com/drive/14UVuBpTtuz5cwlHzMi22EsV9TNAKTakM?usp=sharing


### Prompt Templates: プロンプトテンプレート

https://langchain.readthedocs.io/en/latest/modules/prompts.html

これはプロンプトの管理を行うための機構です。
プロンプトテンプレートでは独自のプロンプトを管理したり、LangChainHubから[プロンプトテンプレート](https://github.com/hwchase17/langchain-hub/tree/master/prompts)を呼び出すなどができます。

独自のプロンプトを作成する場合は以下のような実装になります。
PromptTemplateクラスを用いて作成し、そのインスタンスに対してformat関数を実行することで作成されたプロンプトを使うことができます。
ここでは以前書いた[プレゼン作成自動化](https://qiita.com/sakasegawa/items/abd0e5efac2969898878#%E5%85%A5%E5%8A%9B%E5%86%85%E5%AE%B9%E3%81%8B%E3%82%89%E3%83%97%E3%83%AC%E3%82%BC%E3%83%B3%E3%81%AE%E9%AA%A8%E5%AD%90%E3%82%92%E4%BD%9C%E3%82%8B)の任意の概念についての説明出力用プロンプトを使用します。

```py
from langchain import PromptTemplate

template = """{item}とは何ですか？
説明してください。説明においては以下のルールに従ってください。

・リスト形式で出力する (先頭は ・ を使う)
・わかりやすい文章で丁寧に説明する

A:"""

prompt = PromptTemplate(
    input_variables=["item"],
    template=template,
)
```

作るうえでは、以下に注意してください。

- templateに f-strings 形式と同様に変数を`{}`で囲って定義する
- templateを f-strings 形式にしない (`f"""~~~"""` と書かない)
- input_valiablesにリスト形式で入れる

これをプロンプトとして出力させるときは以下のように使います。

```py
prompt.format(item="りんご")
```

これはprintすると、以下のように出力されます。

```
りんごとは何ですか？
説明してください。説明においては以下のルールに従ってください。

・リスト形式で出力する (先頭は ・ を使う)
・わかりやすい文章で丁寧に説明する

A:
```

変数がないときは `prompt.format()` で大丈夫です。

### LLMs: 言語モデルラッパー

https://langchain.readthedocs.io/en/latest/modules/llms.html

言語モデルのラッパーです。OpenAI GPT-3 (text-davinci-003) などを使う場合は以下のように呼び出します。

```py
from langchain.llms import OpenAI
gpt = OpenAI(model_name="text-davinci-003", openai_api_key=openai_key)
gpt("織田信長について教えて下さい")
```

max tokens(出力可能なトークン数) やtemperatureを変えるときは以下のようにします。

```py
from langchain.llms import OpenAI
gpt = OpenAI(
    model_name="text-davinci-003",
    max_tokens=512,
    temperature=1,
    frequency_penalty=0.02,
    openai_api_key=openai_key
)
gpt("織田信長について教えて下さい")
```

こちらについてはnpakaさんの記事に詳しいです。

https://note.com/npaka/n/n716dfd26094d

### Document Loaders: ドキュメントの読み取り

https://langchain.readthedocs.io/en/latest/modules/document_loaders.html

任意のドキュメント(PDF, csv, 画像など)を読み取る機構です。
Images(画像), PDF, CSV, Markdown, Email, Word, Notion, Evernote, YouTube, Google Driveなどに対応しています。
多くの部分がunstructuredに依存しています。

https://note.com/npaka/n/n7c9847e262a2

たとえばPDFの読み取りは以下のクラスが使えます。

- OnlinePDFLoader
    - 内部的にUnstructuredPDFLoaderが呼び出されています
- UnstructuredPDFLoader
    - [unstructured](https://github.com/Unstructured-IO/unstructured)を使ったローダーです
- PDFMinerLoader
    - [pdfminer.six](https://github.com/pdfminer/pdfminer.six)を使ったローダーです
- PyMuPDFLoader
    - [PyMuPDF](https://pymupdf.readthedocs.io/en/latest/)を使ったローダーです
- PagedPDFSplitter
    - ページ単位で取得できるローダーです
    - [pypdf](https://github.com/py-pdf/pypdf)を使っています

なお、PyMuPDFはAGPL-3.0ライセンスであるため、これを使ったソフトウェアを提供する場合AGPLが伝播する気がします。

たとえば PagedPDFSplitter を使うと以下のように実装できます。

```py
from langchain.document_loaders import PagedPDFSplitter

loader = PagedPDFSplitter(target_pdf_file_path)
pages = loader.load_and_split()

print(pages[0].page_content[:200])
```

### Utils

https://langchain.readthedocs.io/en/latest/modules/utils.html

検索APIのラッパーなどが入っています。とりあえず突っ込んだ感があるのであとで整理されることでしょう。

### Indexes: インデックス

https://langchain.readthedocs.io/en/latest/modules/indexes.html

大規模文書等からQAするというモチベーションを解決するために必要な機構です。

テキストデータを任意文字列等で分割したり、埋め込みにしたり、
Vector Storeに入れたり、近傍探索する処理を統括しています。
このあたりの機構がなぜ必要なのかや埋め込みがどう便利なのか等は以下にまとめています。

https://qiita.com/sakasegawa/items/16714fa132e874cab069

#### テキストの分割

テキストの分割は以下のように実装できます。

```py
from langchain.text_splitter import RecursiveCharacterTextSplitter
text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 100,
    chunk_overlap  = 20,
    length_function = len,
)
texts = text_splitter.create_documents([sample_text])
```

単純にn文字ずつチャンクにするのとは異なり、overlapで重複部分を作ることができるため、文章が突然切れて言語モデルが困ることがなさそうです。

#### 埋め込み作成

今回はOpenAIのEmbeddings APIを用います。
Embeddings　APIは非英語は推奨されないと以前は説明されていたのですが、最近なくなったので何らかの対応がされたのだと思われます。
参考: https://twitter.com/OfficialLoganK/status/1621942366914461699

```py
from langchain.embeddings import OpenAIEmbeddings
embeddings = OpenAIEmbeddings()
query_result = embeddings.embed_query(text)
```

一括して行う場合は以下を実行します

```py
doc_result = embeddings.embed_documents([text])
```

これにより、1536次元の埋め込み表現が取得できます

#### Vector Storeへの保存

ここではFaissを使って保存します。

```py
# Vector Store
from langchain.vectorstores import FAISS

text_splitter = RecursiveCharacterTextSplitter(
    chunk_size = 500,
    chunk_overlap  = 20,
    length_function = len,
)
docs = text_splitter.create_documents([sample_text])
embeddings = OpenAIEmbeddings(openai_api_key=openai_key)
db = FAISS.from_documents(docs, embeddings)
```

探索は以下のように行います。

```py
query = "埋め込みのつらみ"
results = db.similarity_search(query)
print(results[0].page_content)
```

## References

https://note.com/npaka/n/n716dfd26094d

https://note.com/npaka/n/n7c9847e262a2