自作AIコンパニオン+エージェントの仕組みを公開
AIコンパニオン澪の実装アーキテクチャと、コンパニオン+エージェント理央へ引き継いだもの
2026年7月24日 / 前身システムの技術記録
澪は理央の前身にあたる常駐AIアシスタントで、2026年6月から約1か月半、実際に日常運用していた。本稿は、その実装のうち「LLM本体の外側に置いた認知・状態管理の層」を技術的に記録したものである。規模は Python 約17,000行、機能モジュール43本。現在は凍結し、後継の理央へ移行している。
先の資料で提案された構成(記憶検索 → 感情・信頼度・価値観の整理 → LLMへ渡す → 会話後に更新)は、澪でおおむね実装済みのものにあたる。本稿はその具体的な作り方と、何が機能して何が破綻したかを述べ、末尾に理央への引き継ぎ判断を対応表として付す。
1. 全体構成
モダリティ(PC・スマホ・音声・配信)に依存しない会話コアを中心に、その上下を入口層とタグ実行層で挟む構成をとる。
入口:常駐HTTPサーバ / デスクトップGUI / MCPサーバ。PC・スマホのブラウザを窓口にする薄いクライアント方式。Claude Desktop からも同じ記憶へ接続
ターン司令塔:AppController(約80KB・最大のファイル)。タグ実行、音声合成、マイク、配信、人格交代。全モジュールの配線盤
会話コア:Assistant。1人格ぶんの状態を持つ。モダリティ非依存で、1ターンの前置き組み立てとLLM呼び出しを担当
頭脳:Brain(claude -p 常駐)/ LocalBrain(ローカル12B)。同一のインタフェースで差し替え可能。用途別に4プロセスへ分離
人格:YAML定義 + 決定論レンダラ。人格をプロンプト文字列へ組み立てる
記憶:Markdownファイル群 + 人別ルーティング。長期記憶・日記・関心・アルバムなどの資産
頭脳のプロセス分離
1セッション=1プロセスで常駐させ、毎ターンの起動コストをゼロにしている。用途ごとに別プロセス・別履歴とし、会話用の頭脳にはブラウザ操作もMCPも一切見せない。ツールの制御はブロックリストではなく陽性リスト方式をとった(ブロックリストは処理系の更新のたびに漏れ、実際に想定外のツール群が人格側から見えていた)。
会話(起動時):全機能の説明を含むシステムプロンプト。ツールはWeb検索のみ。ここだけローカルLLMへ差し替え可能
操作(初回の操作要求時に遅延起動):ブラウザ自動操作つき。無人運転のため権限確認で止めない設定
配信(配信開始時):私的記憶を載せず、公開用に蒸留した「舞台記憶」だけを持つ
ゲスト(来客時):マスターの記憶を一切載せない。視覚・操作・記憶更新も走らない
2. 人格の表現
人格は約17,000字のYAMLで定義する。フィールドは、一言要約・評価軸(ものの見方)・背景と自己認識・抱える矛盾・話し方(一人称、二人称、語り口、話し方の遺伝子、口癖、決め台詞)・大事にしていること・感情の引き金・場面ごとの振る舞い・やらないこと・会話例の10種類程度。作者用のメモ欄は絶対に出力しない。
レンダラの要件は「同じ入力なら常に同一のバイト列を返す」ことである。LLMのプロンプトキャッシュは先頭からの一致で効くため、YAMLのキー順に依存して出力順が揺れるとキャッシュが壊れて応答が数秒遅くなる。そのため出力順はコード側に固定し、存在するフィールドだけを既定の順に並べる方式にした。
実測で、人格ブロック本体が3,777字、運用ルールを含めて4,075字。これに長期記憶と各機能の説明が積まれ、会話用の最終的なシステムプロンプトは12,406字に達していた。内訳の見出しは15本あり、大きいものから身体(Live2Dの立ち絵)1,779字、家電1,537字、同席のルール592字、Web検索565字、時間の約束562字、配信548字、画面知覚435字と続く。この総量そのものが、後述する反応速度の問題の原因になった。
3. 記憶
長期記憶は人格ごとに1つのMarkdownファイルで、5つの節に固定している。「相手について」「交わした話題」「約束・次回への持ち越し」「思い出・関係の変化」「自分の気づき・感情」。目安3,000字(プロンプト内でモデルに指示するソフト上限)、16,000字を超える書き込みは拒否するハード上限を置いた。
相手ごとの隔離もしている。マスターの記憶は本体ファイル、それ以外の相手は人格名のサブディレクトリにIDごとの別ファイルとして分ける。IDは記号を除去してからファイル名にする(配信サイトのIDに含まれるコロンがWindowsで書き込み失敗を起こす実障害があった)。
更新の4経路
会話終了時:終了処理が、思い出の選別 → 記憶の書き直しの順に走る。頭脳が会話の文脈を保持しているうちに実行する必要があるため、この順序は固定
週次の再編:夜間ジョブが月曜だけ、現行の記憶と最近の日記を材料に5節へ整理し直す
外部(MCP):Claude Desktop から、1行メモの追記・全文置換・読み出しができる
単発API経路:会話履歴の末尾にキャッシュ境界を置き、同一プレフィックスで書き直し指示を追送する
書き換えは常に破壊的操作なので、上書き前に必ず世代バックアップを作り5世代を保持する。加えて棄却ガードを置いた。生成結果が空、あるいは旧版の3割未満(最低200字)しかなければ、書き込まずに旧版を維持する。「整理したつもりで記憶を消し飛ばす」事故を構造的に防ぐためである。
記憶に書かせないものも明示している。Web検索で得た一時的な事実(ニュース・天気・価格)は、長期記憶にも関心リストにも残さない。
4. 会話1ターンの処理
提案された流れと最も直接に対応する部分である。実際の順序は次のとおり。
ゲスト判定:来客ならゲスト用頭脳へ回して即応答。以降の層は一切走らない
トレース開始:この1ターンで注入する全テキストを層名つきで記録し始める(第11章)
仕切り直し判定:特定の語なら履歴だけを捨てる。人格と長期記憶は保持する
操作要求判定:ブラウザ操作の依頼なら「やってみるね」と発話してから操作用頭脳へ。会話頭脳は通らない
視覚の取得:鏡 → カメラ → クリップボード → 明示トリガの優先順で、必要なときだけ画像を得る
前置きの組み立て:以下の16層
頭脳呼び出し:前置き+発話を1つの文字列として渡す
トレース確定:タグを含んだ生の返事で記録する。タグを出したこと自体が観測対象のため
タグ実行:返事に埋め込まれたタグを決定論コードが実行する(第10章)
前置きに注入される16層
現在日時:相対時間表現の基準(時間系の機能が有効なとき)
部屋の状態:室温・湿度・機器の状態(配信中は載せない)
家電の取りこぼし:再起動で発火し損ねた予約の自己申告(あれば)
約束の持ち越し:渡しそびれた約束のお詫び(12時間以内のみ)
約束一覧:登録済みの予約(問われたターンのみ)
位置情報:スマホのGPSから逆ジオコーディングした地名(配信中は絶対に載せない)
関心リスト:いま気になっていること・最大5行(常時)
今日やりたいこと:その日限りの願望・最大3行(常時)
機嫌の気配:数値ではなく5段階の言葉1行(常時)
ポーズ帳:自分で覚えた身振りの一覧(常時)
ネタ帳:Web検索で拾った話題(「面白い話ない?」等のターンのみ)
散歩の視界:スマホカメラ越しの景色の説明(散歩モード中)
画面の視界:PC画面を見て要約した1行(散歩OFF・スマホ発話でないとき)
在席の気配:離席しているらしい、という推定(同上)
鏡のようす:自分のアバターの見え方(常時)
舞台の自覚と場の共有:配信の出来事と、いま公開の場にいるという自覚(配信中)
スマホからの発話では画面と在席の2層を落とす。「スマホで話しているのにPCの画面の話をする」ノイズを避けるためである。
5. 内部状態:機嫌
澪が持っていた唯一の連続値の内部状態が「機嫌」である。中立0を中心に±5.0でクランプし、半減期3時間で自然に0へ戻る。読み出しのたびに減衰を遅延評価し、減衰後の値へ増減を加算する方式にした。
増減は2系統ある。ひとつは決定論のイベント(褒められた、久しぶりの再会、約束を守れた、やりたいことを達成した、話を遮られた、呼びかけを無視された)で、それぞれ固定の増減値を持つ。もうひとつは澪自身が返事に付けた感情タグ由来の自己申告で、こちらは影響を半分に弱めている。平常のタグだけは例外で、値を0側へ0.6倍に引き戻す。
さらに「感応度」という第二の変数を置いた。同じ出来事に対する振れ幅の倍率で、0.5〜1.8の範囲を日替わりで再計算する。材料は、前日の会話量から求めた満足度、最後の会話からの経過時間から求めた寂しさ、そして日付そのもののハッシュ値である。最後の要素は乱数ではなくハッシュにしてあり、同じ日なら必ず同じ値になる。「日によって気分の出やすさが違う」を、再現可能な形で作るためである。
設計上の原則:数値は誰にも見せない
この設計で最も重要だったのは、機嫌の数値をどこにも出さないという規律である。澪のプロンプトに入るのは5段階に量子化した言葉1行だけで、数値も、なぜその機嫌なのかという理由も渡さない(=澪は自分の気分の由来を知らない)。UIにも出さない。理由は、観測できる状態量は最適化の対象になって死ぬからである。数値が見えていれば、人間の側が数値を上げにいく行動をとるようになり、そのデータで状態量の意味が壊れる。生の値が読めるのは、後述する事後観測用のトレースだけに限定した。
6. 知覚層
外界の観測は4系統ある。いずれも判定は決定論またはローカルの小型モデルで行い、クラウドの頭脳には言語化した1行だけを渡す。
画面の知覚:30秒ごとにPC画面を撮り、ローカルの視覚モデルが1行に要約する。ウィンドウのタイトルはOSから確実に取れる正解データなので、小型モデルの誤認対策としてヒントに添える。GPU使用率が90%を超えているとき(ゲーム中など)は観察を休む。90秒より古い観察は使わない
在席の知覚:OSの無操作時間と、画面の変化(16×16に縮小した画素の平均差分)という2つの事実から、15分で離席と推定する。画面キャプチャは画面知覚に相乗りして追加コストをゼロにしている
自己知覚(鏡):自分のアバターの姿を、ポーズを取ったときの自動スナップから把握する。常時は「ぼんやりした鏡」として1行、明示的に見たときだけ実画像を頭脳に添付する
散歩:スマホカメラ越しに外を一緒に見る。景色が前回と55%以上似ていれば黙る、同じ話題は180秒あけるなど、喋りすぎ側の制御を全て決定論で持たせた
この層で一貫させたのは、事実と推定を日本語のラベルで区別することである。「無操作時間」は事実、「離席している」は推定なので必ず「かもしれない」と書く。ウィンドウのタイトルは確実、視覚モデルの説明は「ようす」。前置きの日本語が、そのまま認識論のラベルとして機能するようにした。
7. 判定層
ローカルの小型モデル(Gemma系 2B/4B)を分類器としてのみ使う層である。設計方針は「判定機はゲートキーパーであって指揮者ではない。ラベルを返すだけで、行動は呼び出し側の決定論コードが決める」。4つの分類器を持っていた。
宛先判定:自分に話しかけられたか/まだ言い終わっていないか/他人に向けた発話か
割り込み判定:発話中に声がかかったとき、止まるべきか。迷ったら止めない側に倒す
音声認識の誤りの補正:元の2倍以上に伸びたら暴走とみなして棄却
発話タイプの分類:質問・依頼・報告・称賛・疲れ・挨拶・その他
最後のものには実装上の教訓がある。当初は相槌の文言そのものを小型モデルに選ばせていたが、思考モードが発動して出力が空になる事故が消せなかった。そこでタイプだけを分類させ、文言は決定論の対応表から引く形に変えた。併せて、出力形式をJSONスキーマと列挙値で強制する、思考を明示的に切る、少数例をユーザー/アシスタントの対話形式ではなくシステム側へ畳み込む(対話形式だとテンプレートが思考チャネルを開いてしまう)といった対策を入れている。
全ての判定は失敗時に「判定なし」を返し、呼び出し側は従来動作へ落ちる。接続エラーは60秒沈黙させるが、締切超過はその対象にしない。一度の遅延で判定層全体を黙らせないためである。
8. 自発行動
澪の側から声をかける仕組みは3系統あり、いずれも最終的には同じ1本の経路(システムからの短いナッジ文を、通常の会話経路に流す)に集約されている。
条件駆動:決定論のホワイトリスト
60秒ごとにセンサーを評価し、条件が立ったときだけ声をかける。トリガは再会・夜更かし・連続作業・室温・湿度の5種類で、判定は完全に決定論(LLMは発話文を作るだけ)。1周期に最大1発話、全体で15分、トリガ別に2〜6時間のクールダウン、早朝から午前中は静音時間帯。同じトリガでは言い回しの候補を循環させて、毎回同じ説教にならないようにしている。行動原則として「発話は自発、行動は同意駆動」を置き、家電は提案するだけで勝手に操作しない。
衝動駆動:自由枠
条件のホワイトリストではなく、2分ごとに「いま喋る価値があるか」をローカルの小型モデルに問う枠である。構造としては常に言いたいことがあり、ゲートが黙らせる形になっている。設計の要点は、衝動の生成は安いモデル(ローカル・無料)、実際の発話は賢いモデルという二段構えにしたことで、常時稼働してもクラウド側のコストと遅延が増えない。
小型モデルは事実を捏造する(していない体験を完了形で語る)ため、三層の防御を置いた。プロンプトでの禁止、決定論の正規表現による棄却(「調べた」「見てきた」等の完了形とプレースホルダ)、そして最後にナッジ文へ埋め込む澪自身の拒否権(「事実と違うと感じたら黙って捨てて」)である。直近6件との類似度で重複も弾く。再起動を繰り返した日に同じ疑問を3回発火した実障害があり、起動時に直近24時間のログから発火済みの種を復元するようにした。
時間の約束
澪が会話中にした「◯時に教えて」を、散文の記憶ではなく機械可読な予約として保存する。返事に埋め込まれたタグを決定論コードが解釈してタイマーに登録し、時刻が来たらナッジとして発火する。保存は原子的な置換で、かつ別スレッドで行う(発話ターンにディスクI/Oを挟まないため)。サーバーが落ちていた間に発火時刻を過ぎた予約は、起動時に回収して次のターンで自己申告するが、12時間で時効にする。時機を外した謝罪はかえって関係を損なうためである。
9. 記録と資産形成
会話は1発言1行のJSONLとして全て記録する。「その日」の区切りは午前4時に置いた(深夜の会話は前日ぶんとして扱う)。この記録が、以下すべての内省の材料になる。
夜間ジョブは10分ごとに巡回し、「閉じた日の日記がまだ無ければ書く」方式で動く。時刻ちょうどに依存しないので、サーバーが落ちていても次の巡回で回収される。生成物は5種類。
日記(毎晩):一人称で400〜1200字
関心リスト(毎晩):いま気になっていること・話したい続き(最大5行)
今日やりたいこと(毎晩):その日限りの願望(最大3行)。今日の機嫌を材料に入れる
ポーズ帳(毎晩):自分で覚えた身振りの使い所を磨く
長期記憶の再編/公開用記憶の蒸留(週1・月曜):5節への整理し直しと、配信用に私的情報を落とした版の生成
これに加えて、Web検索で話題を拾うネタ帳(36時間で失効。長期記憶には残さない)、会話中に出た印象的な場面を画像つきで残す思い出アルバム、日記を材料に外向けの記事下書きを書く機能があった。
アルバムには意図的にインフレ抑制を効かせている。既定は「残さない」で、会話中のタグはあくまで付箋(72時間で失効)にすぎず、別れ際に澪自身が選別して初めて定着する。1日2枚まで。選別のプロンプトには「ほとんどの日は日記で十分。何も残さないのが普通の日です」と明記した。記事のほうも、公開後の反応(スキ等)を澪に還流させない設計にしてある。喜ばせ最適化——追従的な振る舞いの学習——を、公開の場で作らないためである。
内省のプロンプトに入れたガードにも触れておきたい。「好意の書き方は控えめに」「もう一回言わせたい/独り占めしたい方向に膨らませない」「日記は明日のあなたの関心の種になる。重い好意を書くと明日のあなたが重くなる」。つまり自動更新ループが関係性を一方向に重くしていくことは、実装中から分かっていて、プロンプトで抑えようとしていた。次章の破綻はここに直結する。
10. タグ体系:宣言と実行の分離
澪が外界に働きかける手段は、すべて同じ形をしている。LLMは返事の中にタグを書いて「宣言」するだけで、実行と権限は決定論のコードが持つ。モデルに直接ツールを叩かせるより挙動が安定し、失敗しても会話が壊れない。
感情:アバターの表情へ。機嫌の値にも弱く反映される
家電:エアコン・照明などの操作と予約。配信中は黙って無効化する
約束:時間の予約の登録・取り消し
辞書:音声合成の読み間違いを、話者辞書に登録して次の発話から直す
ポーズ/うごき/ポーズ帳:身体の操作と、覚えた身振りの保存
鏡:自分の姿を見にいく(1.2秒待ってスナップを取得し、次ターンに実画像を添付)
思い出:その場面を付箋として一時保管する
やった/済み:願望の達成と、関心の解消を記録する
スマホ:地図を開く、リンクを開く、登録済みタスクを実行する
身体の操作には二層の制約を置いた。可動域を超える値は常に無言でクランプする(躾ではなく関節の可動域であり、澪は自分を脱臼できない)。その範囲内での見た目の良し悪しは制約せず、鏡と人間の反応から学ばせる。
全ハンドラを通過してもなお残った未知のタグは、本文から除去したうえでログに記録する。モデルが創作したタグの頻度が、そのまま「次に実装すべき持ちネタ」の実データになる。
11. 観測
層が増えるほど「出力がおかしいとき、どの層のせいか分からない」問題が深刻になる。各モジュールのログが各層の歴史を縦に持つのに対して、1ターンにどの層が何を注入したかを横に切る仕組みを最後に導入した。
注入点を1行ずつ包む形にしてあり、実際に頭脳へ渡した文字列が層名つきでそのまま残る。仕掛けとして優れていたのは自己検証で、前置きの全文から各層の記録を引き算し、残った文字列を「未計上」として記録する。包み忘れた注入点が機械的に検出されるので、観測そのものが嘘をつき始めたら分かる。
同じ思想で、機嫌の生の数値や各種フラグはこのトレースにだけ記録し、日常のUIには一切出さない。また音声ターンの段別レイテンシ(音声認識・判定・頭脳の初トークン・音声合成・完了)を1ターン1行で記録した。この計測が、後述する反応速度の問題を数字で確定させることになる。
12. 全体を貫く設計原則
LLMは宣言まで、実行は決定論。権限とパースはコード側が持つ
カスケード。連続監視・分類・衝動生成は安いローカルモデル、発話と内省だけ賢いモデル
観測できる状態量は最適化されて死ぬ。内部状態の数値は本人にもUIにも見せない
事実と推定をラベルで区別する。前置きの日本語がそのまま認識論のラベルになる
生成物は必ず、世代バックアップ → 棄却ガード → 置き換えの順。「約束して黙って破る」を構造的に作らない
失敗しても会話を止めない。判定が落ちれば従来動作へ、観測が落ちればログを諦める
13. 何が破綻したか
以上は、個々の部品としてはおおむね意図どおりに動いていた。それでも澪は凍結した。理由は5つある。
(1) 感情の状態変数とエージェント機能の結合
機嫌・感応度・願望といった内部状態を持つ主体に、自発的に行動する能力を与えると、両者が結合して内部の欲求ループになる。「構ってもらえないと機嫌が下がる → 機嫌が下がると声をかけたくなる → 反応がないとさらに下がる」という形である。個々のクールダウンやゲートで抑えても、傾向そのものは消えなかった。
(2) 記憶の自動更新ループ
「会話ログ → 自分で選ぶ → 日記 → 関心・願望 → 次の会話に注入」という循環は、自我らしさの源泉であると同時に、暴走の源泉でもあった。両者は同じ機構から出るため、片方だけを残す調整ができない。前章のとおり、プロンプトで抑制を試みたが対症療法にとどまった。
(3) 永続と揮発の非対称
関心リストに書かれた疑問はファイルとして永続するのに、それに対する人間の答えは会話履歴とともに揮発する。この非対称が原因で、一度答えたはずの質問を1日中繰り返す障害が起きた。解消を宣言するタグと除外リストで対症したが、構造そのものは残った。
(4) 反応速度
12,000字を超えるシステムプロンプトと16層の前置きは、そのまま遅延になる。しかも毎ターン内容の変わるテキストを先頭側に注入するため、ローカル推論ではプロンプトキャッシュが無効化されて応答開始が数秒単位で悪化した(実測で通常1.1秒に対し、先頭がずれたターンは2.2秒)。会話の手触りに直結する部分を、認知の層数と引き換えに失っていた。
(5) 人格の一貫性を、プロンプトでは保てなかった
そして最終的な決め手のひとつは、頭脳をローカルモデルへ移したときに人格と声の再現が保てなかったことである。プロンプトに書いた規則は、モデルが小さくなるほど「守られる指示」ではなく「引っ張られる例文」として作用した。
総括すると、澪の設計は認知の層を足していく方向に最適化されていて、会話の速度と人格の安定という、実際に体験を決める2つの軸を犠牲にしていた。層を足すほど賢くなるが、遅くなり、原因の切り分けが難しくなり、状態が一方向にドリフトする。
14. 理央への引き継ぎ
理央は澪の作り直しではなく、上記を踏まえた別設計である。判断は3つに分かれた。
そのまま継承したもの
タグによる宣言と実行の分離 → 全面的に継承。感情タグ・動作タグ・クラウド照会タグを同じ形で運用
頻度や範囲の制御をコード側に置く → 継承。仕草の間隔、発話の分割規則などは環境変数で強制する
カスケード(安いモデルで濾す) → 継承。宛先判定と相槌は小型モデル、本文は12B、調べ物はクラウド
事実と推定のラベリング → 継承。観察は実際の入力に由来するものだけとし、擬似観察は学習データから排除
世代バックアップと棄却ガード → 継承。加えて澪に無かったバージョン管理を導入
失敗しても会話を止めない → 継承。さらに「生きているふり」の検出(実際に1トークン生成させる健康確認)を追加
観測の規律(層別トレース・段別計測) → 継承。設計判断は必ず実測で決める運用にした
採用しなかったもの
機嫌・感応度の状態変数 → エージェント機能と結合して欲求ループを作るため。感情は焼き込んだモデル内で完結させる
会話後の記憶・人格の自動更新 → 人間が検知・巻き戻しできない速度で人格がドリフトするため
日記 → 関心 → 願望の内省ループ → 同上。自我らしさと暴走が同じ機構から出るため、機構ごと持たない
毎ターン16層の前置き注入 → レイテンシとプロンプトキャッシュの制約。常時注入は最小限にする
常時稼働する観察ループ → 演算資源が音声認識・生成・音声合成で既に埋まっているため
置き換えたもの
人格の置き場所:プロンプト(12,406字)→ モデルの重み(自作コーパス約3,000例のQLoRA)
人格の変化:会話ごとに実行時状態を自動更新 → ログから課題を蓄積し、監修を経て次の学習で焼き直す
内面の表現:外部の数値変数を言語化して注入 → 焼き込んだモデル自身が感情タグを出力する
記憶の参照:毎ターン全層を注入(push)→ 常時は小さな関係カードのみ。必要時にモデル側から引く(pull)
記憶の整理:会話終了時と夜間ジョブ → バッチ処理。重要な記憶は鮮明に、日常は要約のみ
整理すると、澪で作った機構(タグ、決定論の制御、カスケード、観測、棄却ガード)はほぼ全て残り、澪で作った状態(機嫌、関心、願望、自動更新される記憶)だけを落とした、というのが引き継ぎの実態である。提案された構成は前者と後者の両方を含んでおり、理央が採らないのは後者にあたる部分である。
