ローカルLLM(手元のサーバーやデバイス上で動かす大規模言語モデル)を使った継続的な対話システムを設計しているエンジニアに向けて、状態管理の設計判断を整理します。クラウドAPIに頼らず、プロンプト設計とコードで「忘れない仕組み」をどう作るかという話です。
題材は、Raspberry Pi 5(小型のシングルボードコンピュータ)上で動く家庭用ロボット「Mirae」の毎晩ストーリーテリング機能です。単発の応答で終わるAIチャットと違い、毎晩同じ物語の続きを語り、登場人物の生死や退場を正確に覚え続けるという要件を持っています。これは状態を持つAIアプリケーション全般に通じる設計課題です。
何が課題で、なぜ注目に値するか
一般的なチャットボットは1回のやり取りで完結します。会話が終われば文脈も消えます。
Miraeの要件はそれとは違います。30日分のエピソードを書き続けても、1日目に登場した船長が30日目も船長のままでなければなりません。2日目に加わった見知らぬ人物は9日目でも乗船している必要があります。死んだキャラクターは生き返ってはいけません。
この「長期の一貫性」をどう保証するかは、LLMを使ったプロダクト設計で頻繁にぶつかる非機能要件です。単純に全履歴をプロンプトに詰め込む方法では、コンテキストウィンドウ(モデルが一度に読める入力の上限)を圧迫し、応答速度も劣化します。かといって要約だけ渡すと固有名詞や設定が崩れていきます。
技術的な仕組み: 状態をコードで持たせる設計
Miraeの実装は、Gemma 4(12Bパラメータのオープンウェイトモデル)をOllama(ローカルでLLMを動かすためのランタイム)経由で呼び出しています。OpenAI互換のエンドポイントであれば、ロボット本体でもホームサーバーでもデスクトップでも動くという構成です。
設計の核心は「モデルに全部を覚えさせない」という判断にあります。毎晩のプロンプトには、次の3種類の情報だけを渡します。
- Reference: ジャンルと前提設定、そしてこれまでの短い要約
- Cast: 登場人物とその役割、退場者や死亡者とそのタイミング
- Situation: 前回のエピソードがどこで終わったか
これは「モデルの記憶」をアプリケーション側の「状態ストア」に移す設計です。モデル自体はステートレス(状態を持たない)に保ち、継続性の担保はコード側の責務にする、という役割分担になっています。
ジャンルはプロンプト内でハードルール(絶対に守らせる制約)として固定されており、SFの物語が4日目に突然おとぎ話に迷い込むことを防いでいます。これはLLMの出力が確率的である以上、重要な制約ほどプロンプトの外側、つまり検証ロジックで縛る必要があるという設計原則の具体例です。
もう一段深い仕組みとして、1回のエピソード生成で2回モデルを呼び出しています。1回目は物語本文をプレーンテキストで書かせ、2回目はその本文を読み返させて、誰が加わったか・退場したか・死亡したかをJSON形式の「継続性メモ」として返させます。
そしてこのJSONをそのまま信用せず、決定的(毎回同じ入力なら同じ結果になる)なPythonコードで検証してから状態に反映します。たとえば主人公の削除が提案されても、コード側が「主人公は物語から退場させない」というルールで却下する、という処理が入っています。
背景・関連技術との比較: なぜこの設計が妥当か
この構成は、LLMアプリケーション設計における「RAG(検索拡張生成)」や「エージェントのメモリ機構」と似た発想です。RAGが外部知識ベースを検索して都度プロンプトに注入するのに対し、Miraeのアプローチは物語という狭いドメインに特化した軽量な状態管理と言えます。
汎用のベクトルデータベースやLangChainのメモリモジュールのような重量級のフレームワークを使わず、JSON構造と素のPythonロジックで要件を満たしている点は注目に値します。小規模なエッジデバイス(Raspberry Pi 5のような計算資源が限られた端末)で動かす前提なら、依存関係を増やさない選択は合理的です。
また「モデルが提案し、コードが決定する」という役割分担は、LLMの出力をそのままシステムの真実(source of truth)として扱わない設計原則の実例でもあります。金融システムや医療システムでLLMの判断をそのまま実行させないガードレール設計と同じ考え方が、趣味のロボットプロジェクトにも応用されています。
性能面では、約350語のエピソード生成に約1分かかるとされています。1日1回の生成であれば許容範囲ですが、同じアーキテクチャをリアルタイム対話に転用する場合は、2段階呼び出し(本文生成+継続性抽出)のレイテンシが積み重なる点に注意が必要です。
読者への影響と、今日確認できること
自分のプロジェクトでローカルLLMによる長期状態管理を検討しているなら、次の観点で確認してみてください。
- 使用中のモデルがOllamaなどでOpenAI互換APIを公開できるか。できればクラウドAPIとの切り替えが容易になります
- コンテキストウィンドウのサイズと、毎回渡す「要約+キャスト情報+直前状況」の合計トークン数を見積もっているか
- LLMの出力(JSON等の構造化データ)を無条件に状態へ反映していないか。検証・却下ロジックの有無を確認する
- 「変更してはいけない項目」(主人公の退場禁止、ジャンルの固定など)をプロンプトだけに頼らず、コード側のルールとしても持っているか
- 1回の更新サイクルにかかる時間が、プロダクトの利用シーン(毎晩1回か、リアルタイム対話か)に見合っているか
もう一つ確認しておきたいのは、音声認識とストーリー生成の結合部分です。Miraeではfaster-whisper(ローカルで動く音声認識エンジン)がすでに動いていますが、「続きを聞かせて」という音声コマンドとストーリー機能の接続はまだ実装されていません。機能ごとに独立してリリースし、結合は後回しにするという段階的な設計判断も、個人プロジェクトやMVP開発では参考になる進め方です。
まとめ
Miraeのストーリー機能は、ローカルLLMを使った長期状態管理の具体的な設計パターンを示しています。
- モデルをステートレスに保ち、継続性はアプリケーション側の状態(Reference/Cast/Situation)で管理する
- 生成と検証を2段階に分け、LLMの提案を決定的なコードで検証してから反映する
- 絶対に守るべき制約(ジャンル固定、主人公の不滅など)はプロンプトだけでなくコードのルールとしても持つ
自分のLLMアプリケーションに長期の一貫性が必要になったら、まずは「何をモデルに覚えさせ、何をコードに外出しするか」の線引きから見直してみると設計の見通しが立てやすくなります。