LangChainを活用してAIアシスタントDiscord Botを作ってみた件

お疲れ様です。[エンジニア学生団体IDEA Advent Calendar 2023](https://qiita.com/advent-calendar/2023/idea)7日目の投稿となります。
もう2023年も終わりですね...最近ほんとうに時間の流れが早い。

# はじめに
この記事では、LangChainを活用したDiscord Botの開発について紹介しようと思います。
昨今のAIの流れ的にも、今後はLLMを活用するケースが多くなってくると思うので、ぜひ今回の記事を通して、LLMを活用した開発フレームワークのLangChainを触ってみるきっかけとなれば本望です！

:::note info
前提知識：PythonやDiscord Botの開発手順をおおまかに理解している
到達レベル：LangChainを用いたBot開発の概要がなんとなく分かる
:::


今回紹介するBotに関しては、リポジトリを公開しているので詳細なソースコードは以下をご覧ください。

https://github.com/YoshiYoshiPro/IDEA?tab=readme-ov-file

> ちなみに、IDEAという名前は私が所属している別団体の名前に由来しています。IDEAさんには許可取りしてないです...

### IDEAくんの動作
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787207/1ddd6b86-7ed1-acea-97b7-b209c9f3a0ce.png)




# LangChainとは
LangChainとは、大規模言語モデル（LLM）を活用したアプリケーションの開発を容易にするためのフレームワークです。主にPythonで書かれており、OpenAIのGPTなどのLLMを統合し、それを使った様々な機能を簡単に実装することができます。LangChainは、会話型AI、チャットボット、自動文書生成など、多くの用途に適用可能なので可能性は無限大だと感じてます。

### Discord BotとLangChainの組み合わせについて
> OpenAIから発行されているGPTモデルのAPIを叩けば別にLangChain必要なくね？

と思ったそこのあなた！！！
私も最初はそう思いました。ただ、Discord Bot開発にLangChainを利用することで、高度な対話型機能や柔軟な自然言語処理の機能をBotに組み込むことが可能です。例えば、ユーザーの質問に対してGoogleカスタム検索APIを活用して最新情報を加味して回答したり、既存のLLMを数学や計算に特化させたり、特定のテーマに沿った会話を進行させたりすることができます。

### LangChainを使ったDiscord Bot開発のメリット
上記を踏まえた上で、LangChainを使ったDiscord Bot開発のメリットを考えると、以下のものがあるかなと思います。

1. **柔軟性**:　
LangChainは、さまざまなLLMやAPIと組み合わせることができ、開発者のニーズに合わせてカスタマイズが可能です。
1. **高度な機能の統合**:
Googleカスタム検索APIのような外部APIを組み込むことで、Botが提供できる情報の幅が広がります。
1. **特化した能力**:
特定の分野に特化した機能をBotに実装することができ、より専門的な対応が可能になります。

### LangChain実行ログ
下のログのように、Google検索と計算特化のToolをAgentが自ら必要だと判断し、実行しているのが分かると思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787207/143e23d2-587c-a3f9-1f04-76c1bc9bcd55.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787207/720ec9f1-f79a-acc0-662d-3958abacdcbf.png)


### LangChainを使ったDiscord Bot開発のデメリット
LangChainを活用することで多くのメリットがありますが、一方でいくつかのデメリットも考慮する必要があります。

1. **処理時間の増加**:
LangChainを介して外部APIを叩く場合、AgentやChainという機能により、処理の内容が動的に変わります。そのため、リクエストに応じてシステム全体の処理時間やレスポンス時間が長くなることや、サービスの可用性に左右されることがあります。
1. **不覚なコストの発生**:
LangChainには、Agentという機能があり、LLMの判断によってシステムの動作が動的に変動します。特に大量のデータを扱う場合や高頻度でAPIを叩くことを要求されるケースは、不覚にも実はAPI使用料金が発生している場合ももしかしたらあるかもしれません。
1. **不安定性**:
LangChainは比較的新しいフレームワークではあるため、多様なAPIやLLM、モジュールを統合する場合に互換性の問題やバグが生じやすくなる可能性があります。私自身もこの開発において、LangChin関連のモジュールがバージョンの違いによりうまく読み込んでくれないなどの問題が多々発生しました。

上記に登場したAgentやChainの概念はこの記事では説明を割愛します。
詳細は公式ドキュメントや参考記事を参照してください。

https://www.langchain.com/

https://zenn.dev/umi_mori/books/prompt-engineer/viewer/intro

https://zenn.dev/zerebom/scraps/689dafd0f42a4a

# 実際のプログラムについて
今回は、discord.pyを使用してLangChainを組み込んだDiscord Botを作成しました。
全体のソースコード量も少なかったため、雑に`main.py`に記述しています。
（後に拡張する時に分割する予定）

```python: main.py
import discord
from langchain.llms import OpenAI
import env
from discord.ext import commands
import traceback
import openai
import requests
from langchain.agents import initialize_agent, Tool
from langchain.utilities.google_search import GoogleSearchAPIWrapper
from langchain import LLMMathChain
import discord.app_commands


intents = discord.Intents.all()
intents.message_content = True  # コマンド拡張機能
bot = commands.Bot(command_prefix="/", intents=intents, activity=discord.Game("/jpi"))

openai.api_key = env.OPENAI_API_KEY
client = openai

# LLMの指定
llm = OpenAI(client=client, temperature=0)

google_search = GoogleSearchAPIWrapper(
    search_engine="chrome",
    google_api_key=env.GOOGLE_API_KEY,
    google_cse_id=env.GOOGLE_CSE_ID,
)
llm_math_chain = LLMMathChain(llm=llm, verbose=True)

# LangChainのツール
tools = [
    Tool(
        name="Google Search",
        func=google_search.run,
        description="最新の情報や話題について答える場合に利用することができます。また、今日の日付や今日の気温、天気、為替レートなど現在の状況についても確認することができます。入力は検索内容です。",
    ),
    Tool(
        name="Calculator", func=llm_math_chain.run, description="計算をする場合に利用することができます。"
    ),
]

# LangChainのエージェントを初期化
agent = initialize_agent(
    tools,
    llm,
    agent="zero-shot-react-description",
    verbose=True,
)


# 画像検索
def search_google_images(api_key, cse_id, query, num=1):
    url = "https://www.googleapis.com/customsearch/v1"
    params = {
        "key": api_key,
        "cx": cse_id,
        "q": query,
        "searchType": "image",
        "num": num,
    }

    response = requests.get(url, params=params)
    response.raise_for_status()
    search_results = response.json()

    return [item["link"] for item in search_results["items"]]


@bot.event
async def on_ready():
    await bot.tree.sync()  # グローバルコマンドの登録
    print(f"{bot.user}がログインしました")  # 起動したらターミナルにログイン通知


@bot.event
async def on_command_error(ctx, error):
    orig_error = getattr(error, "original", error)
    error_msg = "".join(
        traceback.TracebackException.from_exception(orig_error).format()
    )
    await ctx.send(error_msg)


# リスナーとして処理することでスラッシュコマンドを併用
@bot.listen("on_message")
async def reply(message):
    if message.author == bot.user:  # 検知対象がボットの場合
        return
    if bot.user.id in [member.id for member in message.mentions]:
        query = message.content.split(">")[1].lstrip()
        search_result = agent.run(query)

        if not search_result:
            response_msg = "申し訳ございませんが、結果を取得できませんでした。"
        elif "error" in search_result:
            response_msg = f"エラーが発生しました: {search_result['error']}"
        else:
            response_msg = search_result

        await message.channel.send(response_msg)


# スラッシュコマンド処理
@bot.command(name="jpi", description="入力されたキーワードの画像を送信します")  # type: ignore
async def image_search(ctx: commands.Context, keyword: str):
    try:
        image_links = search_google_images(
            env.GOOGLE_API_KEY, env.GOOGLE_CSE_ID, keyword
        )
        if image_links:
            await ctx.send(image_links[0])  # 最初の画像のリンクを送信
        else:
            await ctx.send("該当する画像が見つかりませんでした。")
    except Exception as e:
        await ctx.send(f"エラーが発生しました: {e}")


bot.run(env.BOT_TOKEN)
```

ここでは、discord.pyを用いた基本的な設定や実装の説明は割愛します。
詳細は以下を参照してください。

https://github.com/Rapptz/discord.py

https://discordpy.readthedocs.io/ja/latest/

### 各種設定
```python: LangChainの設定
llm = OpenAI(client=client, temperature=0)
llm_math_chain = LLMMathChain(llm=llm, verbose=True)
```
```python: OpenAIとGoogleカスタム検索APIの設定
openai.api_key = env.OPENAI_API_KEY
client = openai

google_search = GoogleSearchAPIWrapper(
    search_engine="chrome",
    google_api_key=env.GOOGLE_API_KEY,
    google_cse_id=env.GOOGLE_CSE_ID,
)
```
今回のDiscord Botでは、LLMにOpenAIのGPTモデルを指定しています。ドキュメントの数もかなり充実しているので、特に特別な理由がなければGPTモデルで問題ないかと思います。
上記のソースコードでは、
`llm`は、OpenAIのクライアントを使用してLangChainでLLMを設定します。
`LLMMathChain`は、数学関連のクエリに特化したLangChainの設定です。

今回のDiscord Botでは、学生のコミュニティに導入しているので、数学や計算の質問にも対応させたいなと感じ、数学関連のクエリに特化させました。

### Tool, Agentの設定
```python: Tool, Agentの設定
tools = [
    Tool(
        name="Google Search",
        func=google_search.run,
        description="最新の情報や話題について答える場合に利用することができます。また、今日の日付や今日の気温、天気、為替レートなど現在の状況についても確認することができます。入力は検索内容です。"
    ),
    Tool(
        name="Calculator", func=llm_math_chain.run, description="計算をする場合に利用することができます。"
    ),
]

agent = initialize_agent(
    tools,
    llm,
    agent="zero-shot-react-description",
    verbose=True,
)

```
ここでは、LangChainのTool, Agentを設定しています。Googleカスタム検索Toolと計算Toolが含まれており、それぞれGoogleカスタム検索APIと数学関連のクエリ処理に使用されます。`initialize_agent`はこれらのToolとLLMを統合して、LangChainエージェントを初期化しています。

:::note info
`description`を日本語で記述すると、LangChainの実行ログも日本語になります。
:::

### キーワード画像検索（脱線）
今回の主旨とは脱線しますが、せっかくLangChainにてGoogleカスタム検索APIを使用しているので、他の使用用途として、当APIを活用してキーワード画像検索の機能も実装してみました。
```python: キーワード画像検索
def search_google_images(api_key, cse_id, query, num=1):
    url = "https://www.googleapis.com/customsearch/v1"
    params = {
        "key": api_key,
        "cx": cse_id,
        "q": query,
        "searchType": "image",
        "num": num,
    }
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2787207/8c1923b0-4cb4-f2cd-d332-eeb575bf4301.png)
[画像引用元](https://www.sophia-sw.jp/wp-content/uploads/2023/08/note%E3%80%80%E3%83%8E%E3%83%BC%E3%83%88%E3%80%80%E8%A8%98%E4%BA%8B%E8%A6%8B%E5%87%BA%E3%81%97%E7%94%BB%E5%83%8F%E3%80%80%E3%82%A2%E3%82%A4%E3%82%AD%E3%83%A3%E3%83%83%E3%83%81.jpg)

# まとめ
LangChainは、多機能で柔軟なDiscord Botを開発するための強力なツールなので、うまくLangChainを活用して柔軟なBotを開発することが可能です。ぜひ、対話型Bot開発の際には検討してみてください。来年もエンジニアリングライフを楽しんでいきましょ〜〜！！