【LangChain】LLMアプリで使用していたモデルをGPT-4からClaude 3に乗り換えてみる

# 前書き
`Claude 3`の性能が`GPT-4`を超えたというレビューが多く、Xが賑わいを見せていますね。
気になったので自分のWebアプリにも導入して遊んでみました。

自分のアプリはLangChainを用いて実装していましたが、今までOpenAI製のモデルしか使ったことがなく、他社製のモデルに切り替える手順、工数感も把握しておきたかったというのもありました。

# 切り替え手順
LangChainを使って実装していればモデルの切り替えは一瞬でできました。
以下の手順を実行して完了です。

| 項番 | 内容 | 作業概要 |
|:----:|:-------------|:----|
| 1 | [Anthropicのパッケージをインストール](#python環境にanthropicのパッケージをインストール) | `pip install langchain-anthropic` |
| 2 | [Anthropic APIの取得](#anthropic-apiの取得) | `Webブラウザ上での操作 ` |
| 3 | [コードの変更](#コードの変更) | `モデル指定箇所の更新` |

### python環境にAnthropicのパッケージをインストール
Anthropicのパッケージをインストールするには、ターミナルやコマンドプロンプトで以下のコマンドを実行します：

```
pip install langchain-anthropic
```

パッケージの詳細については、[PyPIのlangchain-anthropicページ](https://pypi.org/project/langchain-anthropic/#history)を参照してください。
依存関係の管理を楽にするため、Poetryを使用することもお勧めします。Poetryを使用すると、プロジェクトの依存関係を簡単に管理し、より安定した開発環境を維持できます。

### Anthropic APIの取得

https://www.anthropic.com/claude

AnthropicのAPIキーを取得するには、Anthropicの公式ウェブサイトにアクセスしてユーザー登録を行い、APIキーを生成してください。このキーは、プログラムからAnthropicのLLMにアクセスする際に必要です。

### コードの変更
使用しているLLMモデルを`GPT-4`から`Claude 3`に変更するためには、コード内でモデルを指定している部分を更新する必要があります。

現在（2024-03-17）のモデル対応表は以下です。Langchainから使用する場合は右側の文字列を指定してあげればOKです。

- `Claude 3 Opus` -> `claude-3-opus-20240229`
- `Claude 3 Sonnet` -> `claude-3-sonnet-20240229`
- `Claude 3 Haiku` -> `claude-3-haiku-20240307`

https://docs.anthropic.com/claude/docs/models-overview

# 実際の切り替えコード

```patch
diff --git a/interpreter/convert.py b/interpreter/convert.py
index ee17ab0..0671a8f 100644
--- a/interpreter/convert.py
+++ b/interpreter/convert.py
@@ -1,8 +1,10 @@
 from langchain_openai import ChatOpenAI
+from langchain_anthropic import ChatAnthropic
 from langchain_core.output_parsers import StrOutputParser
 from langchain.callbacks.tracers import ConsoleCallbackHandler
 
 # Tools
+import os
 from dataclasses import dataclass
 
 # MyLibrary
@@ -32,7 +34,7 @@ def _print_articles(articles: List[SearchArticle]) -> None:
         print(f"Search_Word: {article.search_word} \nTitle: {article.title} \nPrint_Contents: {article.html_content} \n")
 
 
-def _summarize_each_html_contents(llm: ChatOpenAI, articles: List[SearchArticle], summary_word_count: int) -> List[SearchArticle]:
+def _summarize_each_html_contents(llm: ChatOpenAI|ChatAnthropic, articles: List[SearchArticle], summary_word_count: int) -> List[SearchArticle]:
     prompt = summarize_pmt()
     output_parser = StrOutputParser()
     chain = prompt | llm | output_parser
@@ -56,7 +58,7 @@ def _integrate_search_articles(articles: List[SearchArticle]) -> IntegratedSearc
     return summarized_article
 
 
-def _convert_integrated_search_article_into_blog_posting(llm: ChatOpenAI, integrated_search_article: IntegratedSearchArticle, comment_num: int) -> BlogPosting:
+def _convert_integrated_search_article_into_blog_posting(llm: ChatOpenAI|ChatAnthropic, integrated_search_article: IntegratedSearchArticle, comment_num: int) -> BlogPosting:
     prompt_class = MakeConversationPrompt()
     prompt = prompt_class.pmt_tmpl()
     chain_input = prompt_class.variables(integrated_search_article.search_word, integrated_search_article.contents, "ordinary", comment_num)
@@ -68,7 +70,7 @@ def _convert_integrated_search_article_into_blog_posting(llm: ChatOpenAI, integr
     return BlogPosting(integrated_search_article.search_word, resp_title, resp_contents)
 
 
-def _make_title_from_contents(llm: ChatOpenAI, contents: str) -> str:
+def _make_title_from_contents(llm: ChatOpenAI|ChatAnthropic, contents: str) -> str:
     prompt_class = MakeTitlePrompt()
     prompt = prompt_class.pmt_tmpl()
     chain_input = prompt_class.variables(article_contents=contents)
@@ -78,11 +80,22 @@ def _make_title_from_contents(llm: ChatOpenAI, contents: str) -> str:
     return llm_resp
 
 
+def _create_llm_model() -> ChatOpenAI | ChatAnthropic:
+    use_model = os.environ["LLM_MODEL_NAME"]
+    if use_model == "GPT":
+        # 環境変数OPENAI_API_KEYを参照します。
+        llm = ChatOpenAI(model_name="gpt-4-0125-preview", temperature=0.7, request_timeout=180)
+    elif use_model == "CLAUDE":
+        # 環境変数ANTHROPIC_API_KEYを参照します。
+        llm = ChatAnthropic(model_name="claude-3-haiku-20240307")
+    return llm
+
+
 def convert_search_articles_into_blog_posting(articles: List[SearchArticle], summary_word_count: int = 1000, comment_num: int = 25, need_summary: bool = False) -> BlogPosting:
     """
     summary_word_count: 記事要約の文字数。この文字数*要約記事数（だいたい3くらい）がLLMに入力される。
     """
-    llm = ChatOpenAI(model_name="gpt-4-0125-preview", temperature=0.7, request_timeout=180)
+    llm = _create_llm_model()
     each_articles = _summarize_each_html_contents(llm, articles, summary_word_count) if need_summary else articles
     _print_articles(each_articles)
     integrated = _integrate_search_articles(each_articles)
 ```
コレ↑だけで動くようになりました。今後さらに切り替え候補モデルが増えることを考慮してモデル切り替え用関数`_create_llm_model()`を作っていますが、もっと簡単に切り替えたい場合は
```
llm = ChatOpenAI(model_name="gpt-4-0125-preview", temperature=0.7, request_timeout=180)
```
を
```
llm = ChatAnthropic(model_name="claude-3-haiku-20240307")
```
に変えるだけでも大丈夫です。

※ [上記変更をしたコミットログ](https://github.com/Ry-Kurihara/ArticleGPT/commit/923e5df9cbdd84593c1d51777e0dd5ad53afbbf7)

# `Claude 3 Haiku`で実行してみた感想
```sh
python main.py 桜 開花予想 2024 東京
```
このコマンドは、2024年の東京における桜の開花予想についての情報をWebから取得し、その情報を基に`Claude 3 Haiku`がブログ記事を生成します。

このあたりの動作の詳細は自分のWebアプリ紹介記事を参照していただけるとより理解しやすいかと思います。

https://qiita.com/Ryku/items/939287c5f500c60dbac9

結果としては以下のようなファイルが出来上がりました。

![スクリーンショット 2024-03-17 19.13.59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/965436/4ad9b3f1-b73c-567e-7e07-fb19addc6a6a.png)


結論として、上記のように、`Claude 3`の`MaxOutput=4092トークン`制限に引っかかってしまい、最後まで書いてくれませんでした。

![スクリーンショット 2024-03-17 21.11.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/965436/ffa3557a-c705-739f-e3c1-1d8a25b6603e.png)

- [`claude/docs/models-overview`より](https://docs.anthropic.com/claude/docs/models-overview)

この制限が存在するため、後続の処理がある場合、エラーになる可能性があります。
記事が完全には生成されないことがあるため、より短い内容に調整するか、複数回に分けて生成する必要があることがわかりました。

### 生成クオリティ
`Claude3（claude-3-haiku-20240307）`が書いてくれた内容を見てみると、内容として破綻している感じはありませんでした。

:::note alert
生成されたタイトル: 東京の桜の開花予想をチェック!2024年の最新情報
:::

:::note info 
:laughing: 東京の桜の開花はいつ頃かな?毎年楽しみにしているけど、なかなかタイミングが合わないんだよね。
:::

:::note info 
:joy: >>1<br/>東京は平年よりも少し早めに開花するみたいだよ。3月下旬頃が見頃になるって予想されているみたいだよ。
:::

:::note info 
:smiley: 去年はめちゃくちゃ早く咲いたからね。今年はどうなるか楽しみだわ。
:::

:::note info 
:smiley: 都内の主要な桜スポットの開花と満開の予想を見てみると、だいたん3月下旬から4月上旬にかけて見頃を迎えそうだね。
:::

:::note info 
:laughing: >>2<br/>そうなんだ、それはうれしいニュースだわ。外出の機会も増えそうだし、都心部の桜を散策するのが楽しみだ。
:::

:::note info 
:smiley: >>5<br/>そうだね。定番のスポットはもちろん、意外な場所の桜も見つかるかもしれないよ。今年は例年よりゆっくり桜を楽しめそうだ。
:::

:::note info 
:smiley: 都内の桜の開花予想見てきた。やっぱり私が行きたい上野公園は3月下旬に開花するみたい。お弁当も用意して、のんびり花見に行きたいな。
:::

:::note info 
:angry: >>7<br/>上野公園は毎年人気ありますからね。でも、人混みが嫌だったら、もしかしたらあまり知られていない隠れた桜スポットを見つけられるかもしれないよ。
:::

用途にもよりますが、例えばブログ記事のベースとなる下書きを頼む程度であれば、破綻してなければ良いかなと思いますので、そういった用途には`Claude3 Haiku`の時点で十分な性能を持っています。

ちなみに`GPT-4 Turbo（gpt-4-0125-preview）`で実行すると↓のような感じです。

:::note alert
生成されたタイトル: 2024年東京の桜開花予想とお花見計画に対する期待と懸念
:::

:::note info 
:laughing: 今年の桜の開花予想、もう出てる？<br/>東京でお花見したいんだけど、計画立てたいなあ。
:::

:::note info 
:joy: >>1<br/>実はもう出てるよ！東京は3月18日に開花するって予想されてるみたい。全国的にも平年より早い開花が予想されてるんだって。
:::

:::note info 
:smiley: あれ、でも他のところでは3月22日って見た気がする…どっちが正しいんだろう？
:::

:::note info 
:angry: >>3 東京の開花予想日はいくつかの情報源で差があるみたい。ウェザーニュースは3月18日、NHKでは3月22日って予想してる。
:::

:::note info 
:scream: そういえば、去年は記録的な早さで開花したって話だけど、今年はどうなんだろう？
:::

:::note info 
:joy: >>5<br/>今年は全国的に平年より早い開花が予想されてるけど、東京は特に早いみたい。でも、開花直前の天候によっては予想が変わることもあるから注意が必要だね。
:::

:::note info 
:laughing: 開花日って結構気温に左右されるよね。今年は暖冬だったから、その影響で早まってるのかな？
:::

:::note info 
:neckbeard: 満開の桜を見るタイミングって計画するの難しいよね。開花予想からどれくらいで満開になるんだろう？
:::

:::note info 
:joy: >>7<br/>そうそう、この冬は全国的に平年より高い気温だったから、桜の生長も早まってるみたい。ただ、冬の寒波で気温の変動があったから、地域によっては開花のタイミングが違うかも。
:::

:::note info 
:angry: >>8<br/>だいたい開花から5日〜2週間ほどで満開を迎えることが多いみたい。でも、今年みたいに平年より早い開花だと、そのペースも変わるかもしれないね。
:::

なんとなくこっちの方が詳しく書いてくれてますかね？
GPT-4の出力はより詳細な情報を提供してくれる傾向にあるように感じました。

### 実行時間
![スクリーンショット 2024-03-17 19.57.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/965436/31b5571e-e13d-94b3-743e-485627b23643.png)

青線が`GPT-4 Turbo`の実行ログ。緑線が`Claude 3 Haiku`の実行ログです。
①は記事タイトル生成タスク、②は記事内容生成タスクです。

長い記事の生成や、複雑な質問への回答など、処理に時間がかかるタスクでは、`Claude 3 Haiku`が`GPT-4 Turbo`よりも大幅に高速に動作することが観察されました。②では`Claude 3 Haiku`が`GPT-4 Turbo`の10倍近く早くなってますね。

※ `LangSmith`を使用して計測しました。神ツールなので使っていない方がいればぜひ使ってみてください。

https://www.langchain.com/langsmith

### 実行料金
LangSmithに`Claude 3`のコスト計算機能が実装されていないようで、上記実行ログ画像にコストが表示されませんでした。参考として、現在（2024-03-17）の規定料金を載せておきます。


- [OpenAI Pricing](https://openai.com/pricing)
- [claude models](https://docs.anthropic.com/claude/docs/models-overview)

| Model | Input | Output |
|:----:|:-------------|:----|
| GPT-3 Turbo | $0.50 / 1M tokens | $1.50 / 1M tokens |
| GPT-4 Turbo | $10.00 / 1M tokens | $30.00 / 1M tokens |
| Claude 3 Haiku | $0.25 / 1M tokens | $1.25 / 1M tokens |
| Claude 3 Opus | $15.00 / 1M tokens | $75.00 / 1M tokens |

# まとめ

途中まで生成されている出力のクオリティ的には`Haiku`の時点で`GPT-4 Turbo`に劣らないレベルで生成できており、これからに期待が持てますね。
※ `Opus`は高そうだったので検証してませんm()m （値段を考えると`GPT-4 Turbo`とは`Opus`で比較するべきだったかも...）

ただ、トークンの出力制限が`4096`なのは結構厳しめの印象で、現状だとまだここがネックで切り替えづらいこともあるかなと感じました。

この制限が緩和された場合は、`Claude 3`が`GPT-4`を差し置いて第一候補のモデルになる人も多くいるかと思いました。