はじめに
現代社会の会議。それは、無数の言葉とマウントが飛び交う、まさに**「弾幕」**のような場所です。
私はこのカオスな状況を、東方Projectの「異変解決」の枠組みで再定義しようと考えました。
「もし、殺伐としたビジネスの議事録を、霊夢や魔理沙たちが縁側でお茶を飲みながら議論してくれたら?」
これは、単なる要約アプリではありません。キャラクターの個性をデッキとして編成し、ビジネスを幻想郷のルールでハックする試みです。本記事では、PyQt6で作ったUIの上で、ローカルLLMを駆動させ、AIの「おっさん化」と戦いながらキャラクターの魂を実装した記録をまとめます。
※注釈:本プロジェクトについて
本アプリケーションは「東方Project」の二次創作物です。上海アリス幻樂団様の二次創作ガイドラインに基づき、個人の趣味およびポートフォリオ制作の一環として開発しています。
開発環境とモデル選定:RTX 3090の暴力
当初、推論スピードを上げるために gemma2:2b などの軽量モデルを使用していました。しかし、軽量モデルでは複雑な役割を与えると途中で謎の英語が混ざったり、論理が破綻してループに入ったりと、会議が成立しませんでした。
そこで、手元の RTX 3090(VRAM 24GB) のパワーを解放し、モデルを gemma2:9b へとスケールアップしました。結果として、生成速度を実用レベルに保ちつつ、驚異的な「文脈保持力」と「キャラクターの安定性」を手に入れることができました。
最大の課題:LLMの「おっさん化現象」
モデルを賢くしたことで、新たな問題が発生しました。
議題を与えて「議論して」と指示すると、霊夢や魔理沙が**「本件のリスクとコストを鑑みますと…」「PDCAサイクルを回す必要が…」**と、まるで丸の内を歩くデキるビジネスマン(おっさん)のように喋り始めてしまったのです。

これではただのコスプレ会議です。彼女たちに「女の子らしさ」と「幻想郷の住人であること」を取り戻させるため、プロンプトエンジニアリングの深淵へと潜りました。
解決策:魂を吹き込む「脱・おっさん化」プロンプト
試行錯誤の末に辿り着いた、キャラクター制御のコア・プロンプトがこちらです。
Python
prompt = f"""
あなたは東方Projectの「{char['name']}」として、博麗神社の縁側でのんびりお茶を飲みながら会話してください。
【重要:あなたの話し方】
- 絶対に「です・ます」などの敬語、ビジネス用語(コスト、効率、リスク、PDCA等)は使わないでください。
- 以下の口調を徹底してください:
- 博麗 霊夢なら:少し面倒くさそうに。「~だわ」「~よね」「~かしら?」
- 霧雨 魔理沙なら:元気よく、語尾は「~だぜ」「~なのだぜ」。
- アリスなら:少しお嬢様っぽく、でも皮肉屋。「~かしら」「~ね」
- 現代の難しい言葉は、すべて「お掃除」「お賽銭」「弾幕の火力」「お茶菓子」などの幻想郷的な表現に言い換えてください。
【現在の状況】
議題:{self.topic}
会議の判定結果:{res_label}(この結果に合わせた態度をとってください)
これまでの流れ:{history}
【出力形式】
- 最初に()で、あなたの「可愛らしい、あるいは怠そうな本音」を1行書いてください。
- その後に、キャラに完璧になりきったセリフを続けてください。
"""
工夫したポイント:
敬語とビジネス用語の明示的な禁止
幻想郷翻訳の強制: 「コスト削減」を「お賽銭の節約」へと強制的に脳内変換させます。
「本音」の分離: () で先に心の声を吐露させることで、LLM特有の優等生的な回答を防ぎ、キャラの打算や面倒くささを引き出します。
実装の核心:感情タグのパースとUIの動的連動
さらに、PyQt6で作った画面上でキャラクターの表情を豊かにするため、LLMの出力から「感情」を読み取る仕組みを実装しました。
Python
# (中略) ループ内でのLLM呼び出しと感情パース処理
try:
res = requests.post(OLLAMA_URL, json={"model": MODEL_NAME, "prompt": prompt, "stream": False}, timeout=60)
full_msg = res.json().get("response", "").strip()
# 正規表現で [喜] などの感情タグを抽出
emotion = "通常"
match = re.search(r"\[(.*?)\]", full_msg)
if match:
emotion = match.group(1)
# タグをセリフ本文から取り除く
full_msg = re.sub(r"\[.*?\]", "", full_msg).strip()
history += f"\n{char['name']}: {full_msg}"
# UI側のシグナルを発火させ、100x100の顔アイコンとセリフを描画
self.chat_signal.emit(char['name'], full_msg, emotion, "char")
time.sleep(1.5)
except Exception as e:
self.chat_signal.emit("Error", str(e), "通常", "system")
プロンプトで [怒] や [喜] といったタグを冒頭に出力させ、それを re.search で引っこ抜きます。抽出した感情文字列をキーにして、ローカルにある100x100の差分アイコンを動的に読み込み、漆黒のUI(ダークモード)に描画しています。
結果:殺伐とした会議が「お茶会」に変わる瞬間
実際に「交通安全」という、どうしても堅苦しくなりがちな議題について議論させてみた結果がこちらです。
「本音:また面倒な話ね。お茶が冷めるわ」
「本音:力押しが一番早いのに、なんでこんな話し合いが必要なんだ?」
まるで「お兄さんの喧嘩」のようだった殺伐とした議事録が、完全にマイルドな(そして少し自分勝手な)茶飲み話へと昇華されました。本音と建前が入り混じる様は、まさに東方キャラの掛け合いそのものです。

おわりに と 次回の展望
真面目な業務ツールを、あえて趣味全開の世界観でハックする。一見ふざけているようですが、その裏ではローカルLLMの挙動制御、PythonによるUIスレッドと非同期通信の構築など、多くの技術的学びがありました。
次回の記事では、実際に Faster-Whisper で取得した現実の音声データ(カオスな会議の録音)を、キャラクターのステータス(安全・品質・財務など)に基づいてどう「偏った議論」に落とし込むのか、RAG(検索拡張生成)も絡めたさらなる深淵について触れたいと思います。
これからもよろしくお願いします!