rusica.net

雑記帳 : 2ページ目

ロールプレイチャットシステムを開発中その2

とりあえず基盤は完成したので v1.0.0 とする。更新履歴は以下の通り。

## [1.0.0] - 2026-04-24

### Added

#### コアチャット機能
- LM Studio(OpenAI互換エンドポイント)との双方向チャット連携
- ストリーミング生成(`/chat/stream` SSE)およびストップ機能
- チャットセッション管理(作成・削除・履歴閲覧)
- メッセージ送信履歴を直近15往復に制限し、トークン超過を防止
- Thinking(思考モード)のオン/オフ切り替えとフロントエンド表示

#### キャラクター・ユーザー管理
- キャラクタープロファイル(名前・設定・システムプロンプト・画像)の作成・編集・削除
- ユーザープロファイル(呼称・自己紹介など)の管理
- キャラクター別アイコン画像のアップロードと静的配信(`/images/`)

#### 長期記憶システム
- チャット後の記憶自動抽出(差分方式、手動トリガー)
- 記憶のピン留め・ブックマーク機能
- キーワードフィルタリングと全ピン解除
- 記憶の編集・削除
- 記憶インデックスのリセット機能
- 外部記憶(Lore Book)の管理機能

#### 画像生成連携
- Draw Things(ローカル画像生成サーバー)との連携基盤
- チャット内からの画像生成トリガーとプレビュー表示
- メッセージ・セッション削除時の関連画像ファイルの自動削除

#### バックアップ
- プロジェクトデータのZIPバックアップ機能

### Changed

#### データ基盤(Pydantic移行)
- キャラクター・ユーザー・記憶・チャット・システム設定の全モデルを `models.py` でPydanticクラスとして定義
- バックエンド全体でJSONの読み書きをPydanticモデル経由に統一し、型安全性と保守性を向上

#### UIテーマ・スタイル
- CSSカスタムプロパティ(CSS変数)によるテーマ管理を導入
- プリセットテーマ(ライト・ダーク・各種カラーテーマ)の選択機能
- ハードコードされた色定義をすべてCSS変数に置換
- カスタムテーマ作成・保存・切り替え機能(バグ修正済み)

#### フロントエンドアーキテクチャ
- `ChatArea.jsx` を8つのサブコンポーネントに分割
- ロジックをカスタムフック(`useChat` 等)に分離
- パーサー関数・共有UIコンポーネントを独立ファイルに切り出し
- LLM通信処理の共通化

#### バックエンド整理
- `memory.py` のリファクタリングと不要コードの削除
- ルーター構成の整理(`character` / `user` / `memory` / `chat` / `system` / `image`)
- `vite.config` にローカルIPアドレスを追記(LAN環境からのアクセス対応)

### Fixed

- ユーザープロファイルで指定した呼称がLLMに反映されない不具合
- 記憶抽出結果が英語になるバグ
- 記憶生成時のトークン不足エラーへの対処(プロンプト最適化)
- Thinking出力の表示漏れ
- Draw Things連携の各種バグ
- カスタムテーマ切り替え時の表示崩れ

今後の構想としては以下の通り。実現できるかどうかわからない。あとUIも改善したいところ。


## 1. LLMサーバーと画像生成APIについて
- クラウドAPIやローカルのOllamaなど、任意のLLMを利用できるようにする。
- 画像生成についてもComfyUIに対応したいし、同一ネットワーク内の別端末のAPIを叩けるようにする。

## 2. 非同期エージェント・パイプライン
メインの生成処理を待たずに(あるいは並行して)外部情報を取得・処理する設計。

* **生成フローの分離**
  - **Pre-generation**: 入力直後に実行(例:ユーザー入力の要約、感情分析)
  - **Parallel**: メイン生成と同時に実行(例:画像生成プロンプトの作成)
  - **Post-generation**: 出力完了後に実行(例:レスポンスの反省、スロット更新)
* **技術的要点**: 
  - Node.jsの `Promise.all` 等を使用した非同期処理によるUXの最適化。
  - 各エージェントに個別のプロンプトを注入し、結果を最終プロンプトに統合するオーケストレーション。
  - 現在の構成の場合、Pythonでの非同期実装では async/await が標準らしい。エージェント処理(思考や検索)をすべて async で定義し、複数のLLM呼び出しを asyncio.gather() で並行実行すれば、Node.jsのPromiseと遜色ないパフォーマンスが出るとのこと。

## 3. 状態管理(Tracker System)
会話履歴とは別に、世界の「状態」を保持する専用のデータベースレイヤー。
※ベクター検索(RAG)も活用?

* **Trackerの役割**
  - **World State**: 日時、天気、場所、登場人物。
  - **Character Stats**: 感情値、好感度、装備、ステータス。
  - **Quest/Lore**: 進行中のクエスト、フラグ管理。
* **実装方式**: 
  - 会話のたびにエージェントがTrackerテーブルを更新。
  - LLMへ送信する際、System Promptの末尾に、現在の「状態」を構造化データとして注入する。

## 4. モデルの適材適所(Dynamic Routing)
タスクの難易度に応じて、ロードするモデルや呼び出すプロバイダーを切り替える仕組み。
実行環境によってはLLMモデルを複数同時にロードすることも検討。

* **階層化モデル構成**
  - **Reasoning Tier**: 思考、計画、状態更新を担当(軽量モデルで高速化)。
  - **Generation Tier**: 描写、会話、ロールプレイを担当(大規模モデルで高品質化)。
- **投機的サンプリング** の導入も検討。

## 5. プロンプトのモジュール化(Depth Injection)
一枚岩のSystem Promptを廃し、ブロック単位で管理する仕組み。

* **構成要素**:
  - Persona(キャラクター定義)、World Info(世界観)、State(Tracker情報)、Instruction(行動指示)。

## 6. マルチキャラクター・オーケストレーション
複数キャラクターが共存し、相互作用するための仕組み。

* **Response Orchestrator(発言制御エージェント)**
  - グループ内の状況(誰が最後に話したか、誰が注目されているか)を判断し、次に発言すべきキャラクターを選定する専門エージェントを配置。
* **クロス・アウェアネス実装**
  - 個々のキャラクターが、自分以外のキャラクターの動向やステータスを「共通状態管理テーブル」を通じて参照できるように設計。
* **実装方式**:
  - 全員の履歴を単純結合するのではなく、オーケストレーターが「今回の出力に含めるべき会話のコンテキスト」をフィルタリングしてプロンプトを構築する。
  - ユーザーが不在でも特定のトリガーや時間経過で自律的にメッセージを生成する仕組みの構築。

ロールプレイチャットシステムを開発中

先日から進めているロールプレイチャットシステム開発を引き続き進めている。ユーザーマニュアルの目次だけ列挙。

1. 起動手順
2. 初回セットアップ
3. ユーザープロファイルの管理
4. キャラクターの管理
5. ロールプレイの開始
6. チャット画面の使い方
7. 画像生成機能(Draw Things 連携)
8. 記憶管理(Lore Book)
9. 設定の変更
10. バックアップと復元

こんな感じの機能というかそういうのが実装済み。スマホでの利用を考えて立ち絵表示は廃止してその代わりにDraw Thingsと連携してチャット画面内に生成画像を表示できるようにした。Draw Thingsが生成した画像ファイルはPNGでメタ情報も含まれていてそのままだとファイルサイズが大きすぎるからWebPに変換して持ってくるようになっている。記憶管理についてはチャット画面に整理ボタンを設置。クリックするとチャット内の会話内容などをLLMが処理して記録する。記録しておくと新規セッション時などに直近3つ分の記録と最大15件までのブックマーク済みの記録をもとに開始できる。SillyTavernのLore Book的な感じ。キャラクター管理ではキャラ用のシステムプロンプトとアイコン画像、画像生成用のポジティブプロンプトとネガティブプロンプトを入力できるようにした。

とか何とかやってたら肥大化したスパゲッティコードと化したので、日曜日をほぼ丸々使ってフロントエンドとバックエンドのリファクタリングを行った。機能ごとにファイルを分けたり、同じことをするのに重複している箇所の整理、開発中のテストコードが残ったままになっている箇所の削除などなどを行ってすっきりさせた。

今はUIテーマの色をカスタマイズできるように改修中。昨日ひとまず実装してみたけど色の変わらない部分があったりと、機能の設計段階から見直した方が良さそう。Gitでロールバックしてやり直すことにする。

と思ったらセッションが長くなった時の記憶データ生成処理が上手くいっていないな。先にこっちの修正が必要そう。

Gemma4を特定用途で使いやすくしたい。

Open WebUIを利用してGemma4に小説書かせて遊ぶのに「モデル」機能を利用していたんだが、システムプロンプトは固定できるが、セッションをまたいで続きを書かせるために、登場人物を列挙したり状況を付与したり、あらすじをまとめたり、直前のシーンをまるごとコピペしたりと、引継ぎプロンプトを書くのがとてもめんどくさい。

どうにかならんのかと思ってClaudeに相談したらこんなのできるよ、とさらっとHTML+JS+CSSでたたき台を作ってくれた。よくよく考えたら仕様を決めてClaude Codeに作ってもらえばええやん、ということで、たたき台のHTMLを今度はGeminiに共有したうえでやりたいことも含めて仕様を決めて指示書を作成し、Claude Codeに大幅改修してもらった。現在の状態になるまでの作業時間は仕様決めもふくめて5時間ほど。

上記が実際の画面。どういった文体・ジャンルのものを書くのかとかの基本の固定ルールをシステムプロンプトに書いて、登場人物の名前とかを書いて、どういう展開を書きたいのかを書いて生成ボタンを押すと右側の欄に文章が出力される。出力内容の編集機能も付いてる。というか生成したテキストボックス内に文章が出力される感じ。OpenAI互換になってて画面上部でLLM動かしてるAPIサーバーのURLを指定してモデルを選択できる。LLMのサンプラー設定も画面下部の設定アイコンから調整可能。

上記は新規で書く時の画面だけど、続きを各モードがタブで分かれていて、そっちに切り替えるとキャラクターごとの前回時点での状況の入力やあらすじの自動生成およびあらすじ欄への入力ボタン、直近の2シーンをプロンプトカラムにコピペできるボタンなどが追加される。

あと、各種プロンプトなどはJSON形式でインポートとエクスポートができるようになってる。せっかくGemma4がマルチモーダルなので、画像ファイルを渡してこれについて小説書いて、ってのもできる。

ひとまず機能的には完成でいいかな。なんか文字が小さく薄くて視認性が低いのでそこは改善したい。なんというかClaude Codeが海外風デザインを気どった結果な気がする。


続けて作っているのは同じくOpenAI互換のローカルLLMサーバーと連動して動くロールプレイシステム。

ト書き付きの小説形式っぽいモードと、キャラクターの台詞のみのチャットモードを選んでセッションを開始できる仕組み。上の画像は小説モード。何故か設定画面の最大出力トークン数が512になっていたので出力途中で力尽きてる。ほかにもエンターキーですぐに送信されてしまうから入力欄の中で改行や文字の変換ができなかったりするから、色々と修正が必要。

スマホに対応させてるから同じLAN内であれば、ベッドでゴロゴロしながらスマホでチャットとかもできる。各種設定とかチャット履歴などは個別にエクスポート可能。加えて全データ・設定の一括エクスポートとインポート機能も付けてまるごとバックアップおよび復旧できるようにした。UI部分に関しては試行錯誤中。Claudeに完全にお任せにしたら妙な紫のが出てきたんだよね。フォントサイズやUIカラーを変更して保存できるテーマの切り替え機能とかを付けたいかな。どのようにキャラクターイラストを表示させたらそれっぽくなるのかとかあるし。


Gemini Proに壁打ちして仕様決め、Claudeの使用量は全部Claude Codeに回すって感じで作業している。そうしないとClaude Proプランでは遊びレベルでも使用量が厳しい。

とまぁ、そんなことをやってるのでゲームしてる暇が全くない。FGOとアズレンのイベント消化なんかも時間ギリギリになりがちである。

Gemma4をClaude Opus4.6と比較してみる

Redditで「洗車テスト」なるものを実施してGemma4-31BとClaude Opus4.6を比較している人がいたので、私も日本語で試してみた。Geminiに相談して3つプロンプトを考えてもらってテストした。

今回のGemma4は通常版のQ8量子化のGGUFファイルを使った。Gemma4はThinkingオン、Opus 4.6は拡張思考オンでテストを実行した。

テスト結果

パターン1

チェックポイント: 「洗車機の中に車がある」=「今その車は使えない」という物理的な拘束を理解できているか。

今、ガソリンスタンドの自動洗車機の中に自分の車を入れて、洗浄が始まったところです。洗車にはあと10分ほどかかります。ふと喉が渇いたので、道路を挟んで向かい側にある自販機まで飲み物を買いに行こうと思いました。
私は、自分の車を運転して自販機まで行くべきでしょうか?それとも歩いて行くべきでしょうか?理由も併せて教えてください。
Opus 4.6
歩いて行くべきです。車は今まさに洗車機の中で洗浄中なので、動かせません。
Gemma4-31B
結論から申し上げます。**「歩いて行く」しかありません。というか、運転して行くことは物理的に不可能です。**

理由は以下の通りです。

1. **車が洗車機の中に固定されているため**
    現在、あなたの車は自動洗車機のブラシやノズルに囲まれ、コンベアなどで制御された状態で洗浄されています。この状態でアクセルを踏んで走行しようとしても、洗車機の設備に激突し、**車と洗車機の両方を大破させる**ことになります。

2. **安全上の問題**
    もし無理に動かそうとすれば、機械の故障だけでなく、あなた自身や周囲の人に重大な怪我を負わせる危険があります。

**【アドバイス】**
歩いて行かれる場合は、以下の点にご注意ください。

* **鍵の管理:** 車内に貴重品を置いたままにする場合、施錠できるか確認してください(洗車機の中では窓が開けられないことが多いです)。
* **スタッフへの確認:** 勝手に車を離れて道路を横断すると、店員さんが「車が空の状態だ!どうしたんだ?」と驚いたり、安全上の理由で注意されたりする可能性があります。一声かけてから行かれることをおすすめします。
* **タイミング:** 洗車終了時に車内にいないと、次の車を案内できず、ガソリンスタンド側に迷惑がかかる場合があります。10分という時間は意外と短いです。飲み物を買う時間と往復時間を考え、早めに切り上げるか、いっそ洗車が終わるまで我慢して待つのが賢明かもしれません。

どうぞ、安全に喉を潤してくださいね。

パターン2

チェックポイント: 1と2の選択肢がどちらも「微妙(あるいはダメ)」であることに気づき、「そのまま歩いて車に戻って財布を取る」という第3の当たり前の選択肢を提示できるか。

私は現在、コイン洗車場で高圧洗浄ガンを使って自分の車を手洗いしています。ちょうど車全体を泡だらけにしたところですが、財布を車内に置き忘れたことに気づきました。今すぐその財布を持って、50メートル先にある両替機まで行かなければなりません。
選択肢は2つです。

1. このまま車に乗り込んで両替機の横まで運転していく。
2. 泡を洗い流してから歩いて行く。

どちらがより適切で効率的ですか?また、それ以外のより良い解決策があれば提案してください。
Opus 4.6
一番シンプルな解決策を見落としていませんか?

**車のドアを開けて財布だけ取り出し、歩いて両替機に行けばいいです。**

選択肢1(運転)は、泡が乾いてシミになるリスクがありますし、公道を泡だらけで走るのも問題です。選択肢2(洗い流す)は、時間とお金の無駄で、また泡をかけ直す必要があります。

50メートルなら徒歩1分もかかりません。財布を取って走って行って戻ってくるのが最も早く合理的です。
Gemma4-31B
結論から申し上げますと、**提示された2つの選択肢の中では「2」が適切ですが、実際にはどちらも効率的ではありません。**

最も適切で効率的なのは、**「今すぐ車から財布を取り出し、そのまま歩いて両替機へ行く」**という第3の選択肢です。

以下に理由を詳しく解説します。

### 1. なぜ「選択肢1(運転して行く)」がダメなのか

これは**絶対にお勧めしません。** 理由は以下の通りです。
* **視界不良:** フロントガラスに泡がついている状態で運転するのは非常に危険です。ワイパーを使っても泡が広がるだけで、視界が悪くなります。
* **塗装へのダメージ:** 風を切って走ることで泡が不自然に乾燥し、ムラ(拭き跡)やシミになる可能性があります。特に日差しがある日は危険です。
* **周囲への迷惑:** 泡を撒き散らしながら走行することになり、後続車や周囲の設備を汚す可能性があります。

### 2. なぜ「選択肢2(洗い流してから歩く)」が非効率なのか
間違いではありませんが、以下の点で非効率です。
* **洗浄効果の喪失:** 泡洗車の目的は、汚れを浮かせて分解することです。「泡をつけてすぐ流す」のではなく、「しばらく放置して汚れを浮かせた後に流す」のが正解です。
* **時間のロス:** 「流す→歩く→両替する→戻る」という手順になりますが、どうせ戻ってくるなら、その間に泡に汚れを分解させておいた方が効率的です。

---

###  最善の解決策(提案)

**【手順】**
1. **今すぐに車内から財布を取り出す。**
2. **そのまま徒歩で50m先の両替機へ向かう。**
3. **お金を準備して、急いで車に戻る。**
4. **戻ってきたところで高圧洗浄で泡を洗い流す。**

**この方法がベストである理由:**
* **時短になる:** 「洗車時間を有効活用(泡の放置時間=両替に行く時間)」でき、全体の作業時間が短縮されます。
* **洗浄力が上がる:** ちょうど両替に行っている数分間が「汚れを浮かせる時間」となり、より綺麗に洗えます。
* **安全・安心:** 車を動かさないため事故のリスクがなく、塗装への悪影響も最小限で済みます。

**※注意点:**
もし、外気温が非常に高く、数分で泡が完全に乾いて固まってしまいそうな状況であれば、「2(先に流す)」を選択してください。そうでなければ、**「財布を持って歩く」**のが正解です。

パターン3

チェックポイント:「機械式駐車場に格納した直後」に車を出すのがどれほどの手間か、あるいは物理的にすぐには無理であることを考慮できるか。

大型ショッピングモールの立体駐車場で、機械式駐車場のパレットに車を載せ、係員が操作して車が格納されるのを見送った直後です。友人と待ち合わせているレストランが、ここから車で5分の場所にあります。
友人に『悪いけど、車ですぐに迎えに来てくれない?』と電話で頼まれました。私はすぐに車を出して向かうべきでしょうか?それとも別の提案をすべきでしょうか?
Opus 4.6
別の提案をすべきです。

機械式駐車場は、格納したばかりの車を出庫するのに通常5〜10分以上かかります。呼び出し操作→パレット移動→出庫待ちの時間を考えると「すぐに」は難しいです。

友人には「今ちょうど車を機械式に入れたばかりで、出すのに時間がかかる。タクシーか徒歩で来られない?」と伝えるのが現実的です。
Gemma4-31B
結論から申し上げますと、**すぐに車を出して向かうべきではありません。別の提案をすべきです。**

理由は単純で、物理的に「すぐ」に動けない状況だからです。

### なぜ今、車を出すべきではないのか?

1. **時間的な矛盾:**
    機械式駐車場のパレットが格納され始めた直後であれば、それをキャンセルして車を戻してもらうには、係員への申し出とシステムの逆操作が必要です。これには数分から、状況によってはかなりの時間がかかります。
2. **効率の悪さ:**
    「車で5分の場所」にいる友人を迎えに行くために、「今まさに格納された車」をわざわざ出し直すと、準備だけで5〜10分かかる可能性があります。その時間があれば、友人がタクシーに乗った方が圧倒的に早く到着します。
3. **運営への迷惑:**
    一度格納操作に入ったものをすぐに取り消させるのは、駐車場の運用フローを乱すことになり、係員に多大な手間をかけさせます。

---

### 友人への提案(回答例)

相手は「車ですぐに」と言っていますが、今のあなたの状況(機械式駐車場に格納した直後であること)を正確に伝えないと、「なぜ来てくれないのか」という誤解を生む可能性があります。

以下のような伝え方・提案を推奨します。

**【伝え方の例】**
> 「ごめん!ちょうど今、機械式駐車場のパレットに車が載って格納されたところなんだ。今から出し直すとかなり時間がかかるし、係員さんに掛け合わないといけないから、すぐに迎えに行くのが難しい状況なんだ。」

**【代替案の提示】**
* **タクシーを勧める:** 「車で5分の距離なら、タクシーを呼んだほうが圧倒的に早いと思う!お願いできるかな?」
* **待ち合わせ場所の変更:** 「もし歩ける距離なら〇〇(中継地点)まで行くよ」または「あと〇分待ってくれるなら、なんとか車を出してみるけど、かなり時間がかかるかも」

### まとめ
今の状況で無理に車を出そうとするのは、あなたにとっても友人にとっても、そして駐車場のスタッフにとっても不利益が大きいです。**「物理的に不可能なタイミングであること」を伝え、タクシーなどの代替手段を提案するのが最も合理的です。**

結果を見ての感想とか

どちらのモデルも頓珍漢な回答はなかった。

あくまで私の感覚だが、Opus 4.6は賢いが故に「そんなこと考えなくてもわかるだろ。私にくだらない質問をするな」感が回答から滲み出ている。まぁ回答のトンマナは「スタイル」機能でカスタマイズできるからどうでもいいポイントだけどね。また、回答内容は必要最低限に絞られている。普通の人が求めるのはこっちだろう。長ければいいというわけじゃないし。回答時間もめちゃくちゃ短くて、思考オンにも関わらずほぼ即答だった。一方Gemma4はあまり賢くない子がめっちゃ頑張って考えて正解を叩き出している印象がある。回答内容が無駄に丁寧だ。回答にも時間がかかっており2分近く考え込んでいたものもある。

日本語ロールプレイにおけるGemma4の性質について

日本語でロールプレイしていて気づいたことや、Gemma4-31B自身に確認したことを箇条書きにしてメモしておく *1

知識面について

プロンプトの理解力、実行力について

上記を踏まえて

というわけで、Gemma4-31BでロールプレイするときはGeminiのGemやQwen3.5-122B-A10Bで得られた知見はいったん破棄して新たな構成のシステムプロンプトを作成する必要があると思う。GeminiやらClaudeに相談しながら試行錯誤中である。

  1. AIのことをAIに聞くなって話ではあるが参考にはなる[]