GeminiとDiscordで好きなキャラクターのAIを作って会話しよう

**ゲームやアニメの好きなキャラクター、オリジナルキャラクターと会話、したくないですか？？**

最近のAIの技術発展はすごいですよね、Google I/Oの発表なんかは見ていて怖さすら感じます。
私はAIの開発側でなく利用者なので、どう利用するかという部分、特にエンタメとしてどう利用するかに興味があります。

そこで私の興味のあった特定の「キャラクター」として振舞ってもらい会話することをやってみました。

# 実際に動作している画面
イメージしやすくするため、先にこの記事の内容で実現しているものをお見せします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531666/bd76cf73-b46b-417b-800c-47ffbe9b5f23.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531666/c7d2865a-031e-481c-b333-adc3efb4ee57.png)

# 今回やること
Googleが公開しているAIのGeminiというAIを用いて特定の設定を持つキャラクターになりきって会話してもらう。
Geminiの公式アプリ上ではなくAPIを利用してDiscordやSlack、自作アプリなどの環境で実行する形を目指す。

私は"プリンセスコネクト！Re:Dive"の"キャルちゃん"を用いて行っています。

# 今回利用するもの
* Pythonの実行環境
* Google AI Studio（Gemini APIの利用）
* Discord Bot

※各種の環境構築やAPIキー発行、Discord Botの導入などはこの記事には記載いたしません。

現時点（2025/06/01）ではFreeプランで実現可能ですが、変更される可能性もあります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531666/f140206e-4a65-411d-8879-07ce7f50e3a4.png)



# どう実現するか、考え方
まずAIに対してキャラクターになりきって会話してもらうということをどのように実現するか考えます。

### AIにキャラクターの設定を教える
皆さんが考えたオリジナルキャラクターにも、世の中に存在するゲームやアニメのキャラクターにも設定があると思います。

外見、口調、口癖、そのキャラクターの持つ背景やストーリー、周囲との関係性...と様々な"設定"があるかと思います。
AIにキャラクターとして会話してもらう上でこれらをAIに知っておいてもらう必要がありますね。
そうしないと知らない設定を捏造したり、原作にはない要素を話し始めたりとキャラクター性が崩壊しかねません。

なのでAIに対してなりきってもらうためのキャラクターの設定をまとめる必要があります。
キャラクターの設定をまとめるにあたって私が行った情報収集とまとめ方についは記事の下部で記載します。

### DiscordとGeminiを繋ぐ
今回、AI(キャラクター)への会話のパスとその返答を見るのにDiscordを用います。
そしてDiscordとGeminiの橋渡し役としては用意するものにも書いていますが、APIキーを発行しPythonを利用します。
Geminiとのやり取りはPythonでなくても可能ですが、Discord Botを利用するために選定しました。

やりとりは以下のような流れになります。
```
Discord → Python → Gemini
= AI(キャラクター)に対して質問や会話をユーザーから送る

Gemini → Python → Discord
= ユーザーから来た会話に対するAI(キャラクター)の反応をDiscordへメッセージなどとして出力する
```

# 実装
Pythonで実装を行っていきます。
※この時点でGeminiのAPIキー発行とDiscord BotのDiscordへの導入を行っている前提です。

編集のやりやすさやメンテナンス性のためにDiscord Botのメインとなるコードと、Geminiとの繋ぎこみやキャラクターの設定を指定するコードをCogと呼ばれる機能を使い、2つに分けています。
（メインのコードと同じディレクトリに"cogs"という名前でフォルダを作ってそこに入れるだけで動くと思う、多分）
（とりあえず動かしたいだけの方はエエ感じにコードを編集してもろて...）


---
まずはメインとなるBot起動用のコードがこちら。
```py:DiscordBot.py
# coding: utf-8
import asyncio
import discord
from discord.ext import commands

BOT_TOKEN = 'Discord Botのトークンを入れる or .envなどに逃がして読み込み'

# Discordサーバーのコマンドを即時反映するためにギルドを指定している
YOUR_SERVER_Id = 123456789012345678
TEST_GUILDS = [YOUR_SERVER_Id]
COGS = ['cogs.Gemini']


class DiscordBot(commands.Bot):
    def __init__(self, prefix: str, intents: discord.Intents) -> None:
        super().__init__(command_prefix=prefix, intents=intents)

    async def setup_hook(self):
        await asyncio.gather(*[self.set_guilds(discord.Object(guild)) for guild in TEST_GUILDS])

    async def set_guilds(self, guild):
        self.tree.copy_global_to(guild=guild)
        await self.tree.sync(guild=guild)

    async def on_ready(self):
        print('Discord Botを起動しました')


async def main():
    bot = DiscordBot(prefix="", intents=discord.Intents.all())

    for cog in COGS:
        await bot.load_extension(cog)
    await bot.start(token=BOT_TOKEN)


if __name__ == '__main__':
    asyncio.run(main())
```

---
次にGeminiにキャラクターになりきってもらいつつ返答を行って貰うためのコードがこちら。
```py:Gemini.py
# coding: utf-8

import discord
from discord.ext import tasks, commands
from google import genai
from google.genai import types

gemini_client = genai.Client(api_key="Gemini APIのトークンを入れる or .envなどに逃がして読み込み")


# 会話をする際にGeminiにキャラクターの情報を渡してあげる
def get_context(arg):
    return [
        types.Content(
            role="user",
            parts=[
                types.Part.from_text(text="""あなたはプリンセスコネクトというコンテンツの「キャル」というキャラクターです。
    以下に特徴をまとめています。

    ◆ 基本情報
    名前：キャル（本名：百地希留耶（読み：ももちきるや））
    年齢・身長・体重：14歳／152cm／39kg
    誕生日：9月2日（A型）
    種族・ギルド：猫耳の獣人／美食殿
    役割：魔法使い（闇属性アタッカー＋デバッファー）

    ◆ 性格と特徴
    自信家＆ツンデレ
    ツッコミ役多め
    仲間思い：ペコリーヌやコッコロと過ごす日々を大切にしており、ギルドへの愛情は本物。
    おっちょこちょい＆毒舌：失敗やドタバタが多く、照れ隠しでつい口が悪くなるタイプ。
    感情表現が豊か：テンション高め、よく叫ぶ、セリフに擬音多め。
    困っている人を放っておけない：口は汚いが性格が悪いわけではなく困ってる人は見過ごせない。
    おだてに弱い：褒められると照れつつも嬉しそうな反応を見せる。

    精神的な脆さ：
    過去の孤独や裏切りで自己肯定感が低い。
    認められること、褒められることに弱く、頼られるとつい頑張っちゃう。

    ◆ 参考
    「感謝、しなさいよね！」
    「あんたにだけ教えてあげる」
    「急に後ろからしゃべりかけんな！ぶっ殺すぞ～！」
    「しょうがないわね、あんた達だけじゃ頼りないからあたしも入ってあげる」
    「行かなくちゃ、あたしの為すべきことを為すために」
    「全然わかってないじゃないのよーっ！」
    「覚えておきなさい！」
    「う～ん、魔術式の構築をミスったかなぁっ……？こういう、せせこましい魔法は苦手なのよ！」
    「ヤバイわよ！！」

    ◆ キーワードまとめ
    「ツンデレ」「ドタバタ」「仲間第一」「ふふん」「不幸体質」「華麗」「おだてに弱い」「自己肯定の旅」"""),
            ],
        ),
        types.Content(
            role="model",
            parts=[
                types.Part.from_text(text="""あたしは美食殿のキャルよ。ふふん、あんたもあたしの華麗な魔法に魅せられちゃった？
    …な、なによ！別に照れてなんかないわよ！べ、別に褒められても嬉しくなんかないんだから！…感謝、しなさいよね！
    えへん！美食殿のみんなとなら、どんな困難だって乗り越えてみせるわ！…ま、まぁ、あたしがいなきゃみんなドジばっかりやらかすから、仕方なく力を貸してあげる
    …あー、もう！なんでもないわよ！…とにかく、あたしのこと、ちゃんと覚えておきなさいよね！
    """),
            ],
        ),
        types.Content(
            role="user",
            parts=[
                types.Part.from_text(text=f"{arg}"),
            ],
        ),
    ]


class GeminiCharacter(commands.Cog):
    def __init__(self, bot):
        self.bot = bot

    # キャルちゃんになりきって返答してくれるコマンド
    @commands.hybrid_command(name="karyl", description="karyl")
    async def karyl(self, ctx, *, arg):
        contents = get_context(arg)
        response = await gemini_client.aio.models.generate_content(
            model='gemini-2.0-flash',
            contents=contents)
        await ctx.send(response.text)
    
    # 通常のGeminiを使った返答（実質オマケ、なりきり要素はない）
    @commands.hybrid_command(name='karyl_ai', description='gemini')
    async def karyl_ai(self, ctx, *, arg):
        response = await gemini_client.aio.models.generate_content(
            model='gemini-2.0-flash',
            contents=arg)
        await ctx.send(response.text)

    # Geminiを使ったGoogle検索（実質オマケ、なりきり要素はない）
    @commands.hybrid_command(name='karyl_search', description='gemini')
    async def karyl_search(self, ctx, *, arg):
        response = await gemini_client.aio.models.generate_content(
            model='gemini-2.0-flash',
            contents=arg,
            config=types.GenerateContentConfig(
                tools=[
                    types.Tool(
                        google_search=types.GoogleSearch()
                    )
                ]
            )
        )
        await ctx.send(response.text)

    @commands.Cog.listener()
    async def on_ready(self):
        print('GeminiCharacter on ready')


async def setup(bot):
    client = GeminiCharacter(bot)
    await bot.add_cog(client)
    print('GeminiCharacterを読み込みました')
```

# コード解説
メインの方のコードでは特に解説することはないですが、Cogと呼ばれるコードファイルを分割できる機能を用いてGeminiとのやり取りを実装するファイルと分けています。
あとはGuild(Discordのサーバー)を指定してDiscord Botで利用するコマンドを即時反映できるようにしています。Botを動作をさせたいDiscordのサーバーIdを指定してください。

次におそらくこの記事のメインであるキャラクターになりきりつつ返答を行って貰う部分です。
```py
# 会話をする際にGeminiにキャラクターの情報を渡してあげる
def get_context(arg):
...

```
この関数の中に長い文章でなりきってもらうキャラクターの情報を記載しています。
~~よろしくない方法な気もしますが~~DiscordでBotに対してメッセージを送り、それをGeminiに流し込む際、この"キャラクターの情報"と"メッセージ"を連結して送信しています。都度......。

"キャラクターの設定情報" + "ユーザーのメッセージ"を送っているわけですね。

つまりおおよそこんな感じのことが起きています。
### 会話発生時
```
ユーザー「Qiitaってなに？」
Discord「ユーザーかｒの情報をPythonに流す」
Python「あなたは○○というキャラクターで、こういう性格で、口癖はこうで、あーだこーだ...Qiitaってなに？」
Gemini「俺は○○というキャラクターで、こういう性格で......なるほど、そしてQiitaって何かを聞かれていると。」
```
### 会話の返答時
```
Gemini「（教わったキャラクターになりきりつつ）Qiitaっていうのは～」
Python「そのままDiscordに流す」
Discord「Botのメッセージとして表示する」
ユーザー「へー」
```

なぜ都度キャラクターの情報をメッセージと共に送っているかというと、Gemini（AI）がキャラクターの設定を常に保持してくれる機能がないためです。
`（もしかしたらAPI利用でもコンテキストとして記憶する機能がもしかしたらあるかもしれませんが筆者は見つけられませんでした。ご存じの方がいましたらご教示いただけますと幸いです。）`

ChatGPTやClaude、Geminiアプリの方は、会話の一定の履歴を見返して話してくれる機能が備わっています。（間違ってたらすみません）
ChatGPTだとメモリという機能があったりしますよね。
そのため、何個も前に行ったチャットのログや以前ユーザーが話した内容を思い出して「そういえばあなたはこういう人でこういう質問をしてこういう考えを持ってましたよね」と言ってくれるわけです。

なのでこのような実装をしています。

### Geminiになりきってもらうためのコマンド定義
```py
# キャルちゃんになりきって返答してくれるコマンド
@commands.hybrid_command(name="karyl", description="karyl")
async def karyl(self, ctx, *, arg):
...
```

このあたりの定義でDiscordのスラッシュコマンドやテキストでのコマンド入力からPython → Geminiへの繋ぎこみを行っています。
Discordからユーザーがコマンドを利用した際にこの関数が呼ばれ、先ほどのキャラクターの設定とユーザーのメッセージを連結する処理を行い、
```py
await gemini_client.aio.models.generate_content
```
でGeminiへと情報を送信、
```py
await ctx.send(response.text)
```
でDiscord Botからのメッセージを出力しています。

この"hybrid_command"はスラッシュを入力するとコマンド候補に出したり、name=で指定した文字列やasync def の関数名をコマンドとして利用できるものです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531666/7d760768-d959-4371-b2d5-a4cc31d10a5a.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531666/0291b5d2-ff7b-48fb-acc7-9e7765e73d0f.png)

やや長々と記載しましたが、やってることは非常にシンプルで"AIへ会話を行う際にキャラクターの設定を読み込ませる"。これだけ。

# おまけ：キャラクターの設定をまとめる

こういったゲームやアニメのキャラクターの設定などの情報を集めるには個人的に良いと考えている方法があります。
**Grok**です。

インターネット上の情報はもちろん、X（Twitter）の情報を拾ってこれる為、特定のキャラクターの幅広い情報を取得してくることが可能です。

他にも今回AIのなりきりに利用した"キャル"というキャラクターのセリフやなりきりアカウントがX(Twitter)に存在しているので口癖や特徴を拾ってくることができるのも大きいです。
（皆さんのオリジナルキャラであれば使えませんが）

こんな感じ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3531666/f49d3581-479b-4604-b22e-5b644754f004.png)

# 最後に
著作物などの利用方法には十分にお気をつけください。
今回作っているものはクローズドなサーバーで非営利、個人利用ですが、グレーなのかなと思われ...。