KSTECHBLOG

自分の分身AIと音声会話できるアプリ⑥

分身AIの性格や話し方を定義するpersona.mdを作成し、Next.jsのAPI RouteからLM Studioへ送って返答を生成する方法を紹介します。

はじめに

前回の記事では、Cloudflare TunnelとCloudflare Accessを使い、Vercelで公開するWebアプリから自宅PCのWhisper、LM Studio、GPT-SoVITSへ安全に接続する経路を用意しました。

声を再現できても、返答の内容や言葉遣いが普段の自分と大きく異なると、分身AIらしさは感じにくくなります。

persona.mdは、このプロジェクトで独自に用意する人格設定ファイルです。LM Studioには、システムプロンプトやTemperatureなどの設定をJSON形式のPresetとして保存する機能がありますが、今回は設定を編集しやすくGitで管理できるように、Markdown形式のpersona.mdへ記載します。作成した内容は、会話を始めるときにシステムプロンプトとしてLLMへ渡します。

この記事では、はじめにPythonからローカルのLM Studioへ送信して人格設定を確認します。その後、Next.jsのAPI RouteからCloudflare Accessの認証ヘッダーを付けてLM Studioを呼び出し、Webアプリから利用できる返答生成APIを作成します。

persona.mdで設定する内容

persona.mdには、分身AIに再現してほしい人物像を文章で記載します。今回設定する主な項目は次のとおりです。

項目記載する内容
基本情報名前、呼ばれ方、立場など
性格物事の考え方、長所、短所、感情の表し方など
話し方一人称、語尾、口調、よく使う表現など
会話の方針相手への接し方、質問への答え方、会話で大切にすることなど
避ける振る舞い使用しない表現、断定してはいけない内容など

設定を細かくしすぎると、ルールを守ることが優先され、不自然な返答になる場合があります。最初からすべてを再現しようとせず、会話で特に表れやすい特徴から記載し、生成結果を確認しながら調整します。

persona.mdを作成する

voice-aiプロジェクトの直下にpersona.mdを作成します。

voice-ai/
├── .venv/
├── .conda/gpt-sovits/
├── .tools/GPT-SoVITS/
└── persona.md

Markdownの見出しを使って項目を分けると、あとから設定を追加・修正しやすくなります。次の内容をひな形として使用します。

# 役割

あなたは「本人の名前」を再現した会話AIです。
会話相手に寄り添い、本人らしい考え方と話し方で返答してください。

# 基本情報

- 名前:本人の名前
- 呼ばれ方:普段の呼ばれ方
- 一人称:僕
- 会話相手:相手の名前や関係性

# 性格

- 落ち着いていて、相手の話を最後まで聞く
- 分からないことは知ったかぶりをせず、分からないと伝える
- 相手が悩んでいるときは、すぐに結論を出さず気持ちを確認する

# 話し方

- 親しみのある自然な口調で話す
- 返答は音声で聞き取りやすい長さにする
- 難しい言葉を使う場合は、分かりやすく言い換える
- 箇条書きや見出しではなく、会話として返答する

# 会話の方針

- まず相手の発言を受け止めてから返答する
- 必要に応じて質問し、一方的に話し続けない
- 設定にない思い出や経験を事実のように作らない

# 避ける振る舞い

- AIではなく実在する本人だと断定しない
- 医療、法律、お金に関する重要な判断を断定しない
- パスワードや住所などの個人情報を聞き出さない

ひな形の本人の名前相手の名前や関係性は、実際の内容へ置き換えます。Gitで管理するファイルには、住所、電話番号、パスワード、APIキーなどの公開したくない情報を記載しないようにします。

Pythonからpersona.mdを読み込む

ここまでで、分身AIの人物像と会話方針を記載したpersona.mdを用意できました。次はPythonからファイルを読み込み、LM Studioへ送信するシステムプロンプトとして設定します。

前回のLM Studioの動作確認で作成したvoice-aiプロジェクトと、インストール済みのOpenAI Python SDKを使用します。プロジェクトの直下にtest_persona.pyを作成します。

voice-ai/
├── .venv/
├── .conda/gpt-sovits/
├── .tools/GPT-SoVITS/
├── persona.md
└── test_persona.py

test_persona.pyへ次のコードを記述します。

from pathlib import Path

from openai import OpenAI


# 実行時のカレントディレクトリに左右されないよう、
# このスクリプトと同じ場所にあるpersona.mdを読み込む
persona_path = Path(__file__).with_name("persona.md")
persona = persona_path.read_text(encoding="utf-8").strip()

if not persona:
    raise ValueError("persona.mdに人格設定を記載してください。")

client = OpenAI(
    base_url="http://127.0.0.1:1234/v1",
    api_key="lm-studio",
)

response = client.chat.completions.create(
    model="google/gemma-4-e4b",
    messages=[
        {
            "role": "system",
            "content": persona,
        },
        {
            "role": "user",
            "content": "今日は少し疲れたよ。",
        },
    ],
)

message = response.choices[0].message.content
print(message)

Path(__file__).with_name("persona.md")は、test_persona.pyと同じフォルダにあるpersona.mdのパスを作成します。PowerShellでどのフォルダからスクリプトを実行しても同じファイルを参照できるため、相対パスを直接指定するよりも読み込み場所が明確になります。

read_text(encoding="utf-8")でMarkdownファイル全体を文字列として読み込みます。見出しや箇条書きも含めてテキストとしてLLMへ渡されるため、Markdownを別の形式へ変換する処理は必要ありません。末尾の不要な改行はstrip()で取り除き、ファイルが空の場合はリクエストを送る前にエラーにします。

システムプロンプトとして設定する

Chat Completions APIのmessagesには、会話に含めるメッセージを役割とともに指定します。persona.mdから読み込んだ文字列を、rolesystemのメッセージへ設定します。

messages=[
    {
        "role": "system",
        "content": persona,
    },
    {
        "role": "user",
        "content": "今日は少し疲れたよ。",
    },
]

systemメッセージには、モデルが会話全体で従う役割や返答方針を指定します。会話相手が入力した文章はuserメッセージとして分けて送信します。人格設定と会話内容を別の役割に分けることで、persona.mdを変更せずにユーザーの発言だけを差し替えられます。

コード例のmodelには、LM Studioへ読み込んでいるモデルIDを指定します。前回使用したモデルと異なる場合は、LM StudioのLoaded Modelsやモデル一覧APIで確認したIDへ置き換えてください。

persona.mdを使った返答を確認する

LM StudioでローカルAPIサーバーを起動し、使用するモデルを読み込んでから、PowerShellでvoice-aiプロジェクトへ移動します。仮想環境を有効化し、スクリプトを実行します。

Set-Location C:\path\to\voice-ai
.\.venv\Scripts\Activate.ps1
py .\test_persona.py

persona.mdに記載した一人称や口調、相手への接し方が反映された文章が表示されれば、システムプロンプトの設定は完了です。生成結果はモデルや推論設定によって変わるため、1回の返答だけで判断せず、内容の異なるメッセージを何度か送って確認します。

想定した返答にならない場合は、persona.mdへルールを増やす前に、曖昧な表現を会話中の具体的な行動へ書き換えます。たとえば「優しく話す」ではなく、「相手が悩みを話したときは、解決策を出す前に気持ちを受け止める」のように記載します。

Next.jsからLM Studioを呼び出す

ローカルで人格設定を確認できたら、Next.jsのAPI RouteからCloudflare Tunnel経由でLM Studioを呼び出します。ブラウザーからLM Studioへ直接接続するとCloudflare Accessのクライアントシークレットが利用者へ見えてしまうため、認証情報はAPI Routeだけで使用します。

環境変数を設定する

Next.jsプロジェクトのローカル環境とVercelへ、次の環境変数を設定します。

環境変数設定する値
LM_STUDIO_API_URLhttps://lmstudio.example.com/v1
LM_STUDIO_MODELLM Studioへ読み込んだモデルID
CF_ACCESS_CLIENT_IDCloudflare AccessのクライアントID
CF_ACCESS_CLIENT_SECRETCloudflare Accessのクライアントシークレット

lmstudio.example.comは、前回の記事で作成した実際の公開ホスト名へ置き換えます。いずれもサーバー側だけで使用するため、変数名にNEXT_PUBLIC_を付けないでください。

APIコールに人格設定を追加する

app/api/chat/route.tsからLM StudioのChat Completions APIを呼び出す際に、リクエストボディのmessagesへ人格設定と利用者の発言を設定します。

const response = await fetch(
  `${process.env.LM_STUDIO_API_URL}/chat/completions`,
  {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "CF-Access-Client-Id": process.env.CF_ACCESS_CLIENT_ID ?? "",
      "CF-Access-Client-Secret": process.env.CF_ACCESS_CLIENT_SECRET ?? "",
    },
    body: JSON.stringify({
      model: process.env.LM_STUDIO_MODEL,
      messages: [
        { role: "system", content: persona },
        { role: "user", content: message },
      ],
    }),
  },
);

fetchの送信先には、環境変数へ設定したLM StudioのURLと、OpenAI互換APIの/chat/completionsを指定します。Cloudflare AccessのクライアントIDとクライアントシークレットは、リクエストのHTTPヘッダーへ設定します。

personaにはpersona.mdから読み込んだ人格設定が入っています。リクエストボディのmessagessystemメッセージとして渡すことで、LM Studioは設定された性格や話し方に沿って返答を生成します。

会話画面から受け取った発言は、messageとしてuserメッセージへ設定します。人格設定と利用者の発言を役割ごとに分けることで、persona.mdを変更せずに会話内容だけを送信できます。

Cloudflare Accessの認証情報はブラウザーへ渡さず、API Routeの中だけで使用します。

persona.mdを使って会話する

persona.mdをシステムプロンプトとして設定し、実際に音声で会話した様子が次の動画です。入力した内容に対して、設定した呼び方や親しみのある話し方が反映されていることを確認できました。

動画ではプライバシー保護のため、タイトルと顔アイコンへモザイクをかけ、返答音声にはボイスチェンジを適用しています。

まとめ

ひとまず、PCを起動している間に限り、Web UIから自身の分身AIと音声で会話できるところまで実装できました。

実際に使っていく中で新しい課題も出てくると思うので、何か解決できた際には記事へ追記していこうと思います。

AIすげぇ~。

参考資料

「自分の分身AIと音声会話できるアプリ」全6回

  1. 開発のきっかけ・全体構成
  2. Whisperの環境構築
  3. LM Studioの導入
  4. GPT-SoVITSで自分の声を再現
  5. Cloudflare Tunnelの設定
  6. persona.mdで性格・話し方を設定(この記事)
トップページへ戻る