dfejza.com
制作物一覧
AIインフラ

MrHuang

コード· ベータ

所有するハードウェア上で完全に動作するセルフホスト型AIコンパニオン。Python/FastAPIバックエンドと自前のエージェントループが、vLLMで提供するローカルのオープンウェイトモデル(Qwen3、約35B/アクティブ約3BのMoE)を呼び出すため、推論は100%ローカルで完結し、クラウドLLMもトークン単価の課金も一切なし。全会話をセルフホストのインフラにローカル保存し、商用チャットツールでは残らない永続的でプライベートなナレッジベースを構築。WhatsApp(Meta Cloud API webhook)とWeb UIからアクセスでき、Deepgramによる音声メモの文字起こしと、差し替え可能なローカル経路として展開したセルフホストのWhisperサイドカーに対応。Cloudflare Tunnel + Tailscaleによるゼロトラストネットワーキング、全アクションに適用されるフェイルクローズドな権限上限、そして本番環境を常に検証済みのビルドに保つヘルスチェック連動のデプロイパイプラインを備える。

PythonFastAPIvLLMQwen3 35B MoE (open-weight)OpenAI Python SDKSQLiteCloudflare TunnelTailscale

AI生成: この技術解説はプロジェクトのソースコードからAIが作成したもので、不正確な記述を含む場合があります。

MrHuangは、所有するハードウェア上で完全に動作するセルフホスト型のAIコンパニオンです。頭脳はセルフホストGPU上のローカルなオープンウェイトモデルで、クラウドLLMもフォールバックも一切使いません。会話がこの筐体の外へ出ることはなく、トークン単位の課金も発生しません。すべてのメッセージはセルフホストのインフラにローカル保存されるため、日々の対話がそのまま永続的でプライベートなナレッジベースになります。セッションをまたぐと何も覚えていないステートレスなチャット画面とは対極の設計です。

最初からこうだったわけではありません。MrHuangは、サードパーティ製のAgent SDKを介してホスト型モデルに対してエージェントループを実行する、TypeScript/Express製のサービスとして始まりました。現在のビルドは、バックエンドをPythonへ全面的に書き直し、セルフホスト推論への ブレイン・トランスプラント(脳移植) を施した結果です。どちらもすでに出荷済みで、稼働しており、以下で詳しく扱います。

アーキテクチャ

メッセージはWhatsApp(Meta Cloud API経由)またはWeb UIから到着し、認証とレート制限を行うゲートウェイを通過したのち、トークン予算内のプロンプトに組み立てられ、自社製のエージェントループでローカルのオープンウェイトモデルに対して実行されます。レスポンスが返り、やり取り全体がローカルのSQLiteストアに永続化されます。

頭脳はセルフホスト型のオープンウェイトMixture-of-Expertsモデル(約3Bアクティブパラメータを持つ約35Bモデル、FP8重み、大きなネイティブコンテキスト)で、vLLM 上でOpenAI互換APIを介して配信されます。推論は100%ローカルで、クラウドLLMもフォールバック経路もありません。Chain-of-thought(思考の連鎖)推論は毎ターン既定で有効です。エージェントループは自社製でターンごとにステートレスであり、ローカルモデルへ直接向けたOpenAI Python SDK上に構築されています。ツールは別個のトランスポート越しではなくインプロセスで動作します(自律的なツールループ自体はゲートされたまま — セキュリティ を参照)。

flowchart TD
    WA["WhatsApp<br/>Meta Cloud API"] -->|webhook| GW
    UI["Web UI"] --> GW
    GW["ゲートウェイ<br/>署名検証 · 許可リスト · レート制限"]
    GW --> CA["ContextAssembler<br/>Markdown KB → トークン予算内のプロンプト<br/>ホットリロード対応のペルソナ"]
    CA -.->|セマンティック検索| EMB["Embeddings サイドカー<br/>ローカル · vLLM"]
    CA --> AL["エージェントループ<br/>自社製 · インプロセスツール<br/>並行制御 · タイムアウト · バックオフ · ストリーミング"]
    AL -->|OpenAI互換| LLM["オープンウェイト MoE<br/>セルフホストGPU · vLLM"]
    AL --> DB[("SQLite<br/>ローカル履歴 + ナレッジベース")]
    AL -.->|応答| WA
    AL -.->|応答| UI

エージェントループ — ローカルモデルとOpenAI互換APIで対話する、自社製でターンごとにステートレスなステップループです。モデルサーバーが呼び出し側に委ねている本番運用の関心事を、このループが担います。並行数は上限を設け、ソフト/ハードのタイムアウトでレイテンシを抑え、バックオフで一時的な障害を吸収し、レスポンスは完了をブロックせずストリーミングします。ツールはインプロセスで動作します(メールとカレンダーのツールは、別個のツールサーバー用トランスポート越しではなくインプロセスのレジストリに存在します — MCPは廃止しました)。自律的なツールループ自体は、サンドボックスが出荷されるまでゲートされたままです(セキュリティ を参照)。

ContextAssembler は、Markdownのナレッジベースから各システムプロンプトを構築し、最も関連性の高いコンテキストを固定のトークン予算に収めます。検索はセルフホスト型の埋め込みモデルに支えられた単一ベクトルのセマンティック検索です。同じvLLM筐体上で動く小さなエンベッダーであり、ユーザーのコンテキストに関するものが検索のためにハードウェアの外へ出ることはありません。パーソナリティ仕様はホットリロード対応で、再デプロイなしに振る舞いを調整できます。

永続的なジョブ基盤 — MrHuangのプロアクティブかつスケジュール実行される側面は、自社で構築したクラッシュ耐性のあるジョブ基盤の上で動作します。スケジューラ、ワーカー、そしてリースとリトライを備えた冪等な配信台帳から成り、クラッシュや再デプロイを乗り越えて存続します。これは旧来のcronベースのジョブを置き換え、いまやすべてのスケジュールタスクが乗る土台となっています。同じ基盤が、あらゆるアクションに対してフェイルクローズの権限上限を強制します(セキュリティ を参照)。

メッセージ1件の処理ライフサイクル全体は次のとおりです。

sequenceDiagram
    actor U as ユーザー · WhatsApp / Web
    participant G as ゲートウェイ
    participant A as ContextAssembler
    participant E as エージェントループ
    participant M as オープンウェイトモデル · vLLM
    participant D as SQLite
    U->>G: メッセージ + Webhook署名
    G->>G: 検証 · 許可リスト · レート制限
    G->>A: 認証済みメッセージ
    A->>A: トークン予算内のプロンプトを構築
    A->>E: プロンプト
    E->>M: ターンを実行(OpenAI互換 · ローカル)
    M-->>E: 推論 + 応答
    E-->>U: 応答
    E->>D: やり取りを永続化

ここに至るまで

当初の設計は、サードパーティ製のAgent SDKを介してホスト型モデルを呼び出すエージェントループを持つ、TypeScript/Expressのバックエンドで動いていました。変わったことが2つあり、どちらもいまや稼働中のシステムです。

ブレイン・トランスプラント(脳移植) — ホスト型モデルからセルフホスト推論への移行 — は完了しました。vLLM上のローカルなオープンウェイトMoEが唯一の頭脳であり、クラウドへの依存も、認証も、課金も、緊急脱出用のフォールバックもありません。2つ目のvLLMインスタンスが、同じGPU上で検索用の埋め込みを配信します。セルフホスト型の音声テキスト変換(speech-to-text)サイドカーも同じGPU上で動作しており、現在はデスクトップのディクテーションを担っています。MrHuang自身のアプリ内ボイスメモは、いまもクラウドサービスを介して文字起こしされており、ローカルへの差し替えは順番待ちです(今後の展望 を参照)。頭脳そのもの — 本ページの旧版が「今後の展望」に分類していた部分 — はいまや本番稼働しています。

バックエンドの書き直し — TypeScript/Expressから、自社製エージェントループを備えた Python 3.13 / FastAPI へ — は、パリティを検証済みの一方向カットオーバーでした。サードパーティ製のAgent SDKと別個のツールトランスポートはなくなり、ツールはインプロセスで動作します。このカットオーバーは、その目玉となるパリティの主張に対して独立したピアレビューを受け(8.1/10)、main は本番と同期を保っています。旧来のTypeScriptツリーは退役しました。

セキュリティ

公開インターネットから到達可能な個人用アシスタントは格好の標的になるため、信頼境界は敵対的なものとして扱っています。

  • Webhook認証 — 受信メッセージは、生のリクエストボディに対して暗号署名で検証し、いかなる処理に入る前にも信頼済み送信者の許可リストと照合します。
  • プロンプトインジェクション防御 — 信頼できないメッセージ内容が、システム命令を上書きしたり、アクションに許可された権限を昇格させたりすることは決して許しません。
  • 段階的な権限上限 — あらゆる副作用は3段階のエグレス階層(自動 / ゲート&ログ / 人間承認)を通過し、基盤はジョブを生成したロールとチャネルによって範囲が定まるフェイルクローズの上限を強制します。発生元が付与する以上の権限を必要とするアクションは、黙って格下げされるのではなく拒否されます。
  • ツール境界の牢獄(設計済み・未出荷) — 自律的なツールループは、現状コード上で完全に拒否されています。それがゲートされている先の防御は、ツール境界におけるOSレベルの牢獄です。realpathとルート封じ込め、シンボリックリンクによる脱出の拒否、ネットワーク制限付きの実行コンテナから成り、ジェイルブレイクされたプロンプトがシステムパスへの書き込みを発しても、1バイトもディスクに到達する前に拒否されます。これは設計・仕様化済みで、出荷されるまでループは拒否されたままです。
  • ゼロトラストネットワーキング — 開放された受信ポートは存在しません。受信はCloudflare Tunnel経由で、筐体自体はTailscaleメッシュ越しにのみ到達可能であり、運用アクセスも同じ経路を通ります。

これらの制御は組み合わさり、デフォルトで敵対的な単一の経路を形成します。エッジ、ゲートウェイ、そして権限上限をクリアするまで、何も動き出しません。

flowchart LR
    A["受信メッセージ<br/>信頼できない"]
    A -->|"Cloudflare Tunnel<br/>受信ポートの開放なし"| GW
    subgraph trust["信頼境界(敵対的として扱う)"]
        direction TB
        GW["ゲートウェイ<br/>署名検証 · 許可リスト · レート制限"]
        AG["エージェント<br/>プロンプトインジェクション防御"]
        CEIL["権限上限<br/>3段階階層 · フェイルクローズ"]
        JAIL["ツールの牢獄<br/>設計済み · 出荷までループは拒否"]
        GW --> AG --> CEIL --> JAIL
    end
    OP["オペレーター"] -.->|Tailscaleメッシュ| GW

デプロイ

MrHuangはセルフホストのインフラ上でDockerコンテナとして出荷され、日々本番稼働しています。更新は自動かつヘルスゲート付きのデプロイパイプラインで適用され、手動介入なしに本番を既知の良好なビルドのまま最新に保ちます。ロールバックはリバート&リビルドで行い、カットオーバー前のデータスナップショットを安全網とします。

完全にセルフホストのオブザーバビリティスタックがアプリと並走し、デプロイジョブとは別系統でデプロイされています。メトリクス収集、コードとしてプロビジョニングされたダッシュボード、プッシュ通知付きのアラート、稼働監視、そして推論用筐体向けのGPUメトリクスエクスポーターから成ります。これらすべてを所有するハードウェア上に置くこと自体が目的です。データ、モデル、稼働時間が直接コントロール下に置かれるからです。テストカバレッジは広範でグリーンを保っています。数千件のバックエンドテストに加え、旧スタックから引き継いだリグレッションスイートも維持しています。

今後の展望

ブレイン・トランスプラント(脳移植)は完了したので、フロンティアは前進します。推論が完全にセルフホスト化され、永続的な基盤が荷重を担ういま、次のマイルストーンはMrHuangをテキストアシスタントから、常時利用可能な家庭用音声アシスタントへ、そしていずれは安全な自律的アクターへと変えていきます。以下はすべて方向性です。仕様化され、一部はデリスク済みですが、まだ出荷されていません。

正直なところ、難所はもはや配線ではありません。セルフホスト型の音声テキスト変換(speech-to-text)はすでに筐体上で動作しています(現在はデスクトップのディクテーションを担っており、いまもクラウドサービスを介して文字起こしされているアシスタントのアプリ内ボイスメモへ組み込むのが近い将来の差し替えです)。難しいのは、音声による対話をコマンドラインではなく「存在」として感じられるようにすること、そしてジェイルブレイクされたプロンプトに鍵を渡すことなく自律性の拒否を解くことです。

音声のラウンドトリップEpic P1 ~L

音声の入力と出力を、所有するハードウェア上で完結させてラウンドトリップさせます。ストーリー: GPU上にセルフホスト型のテキスト音声変換(text-to-speech)サイドカーを立ち上げる ; 音声入力経路と独立したマイクデーモンを追加し、ウェイクワードとローカルな文字起こしがエッジで行われるようにする ; アプリ内ボイスメモをセルフホストのSTTサイドカーへ差し替えきり、音声がクラウドサービスに届かないようにする。MrHuang Listener のハードウェアマイクは、デスクトップのマイクデーモンに対応する組み込み版としてここに接続します。どちらも同じセルフホスト型の音声テキスト変換レイヤーに供給します。

プロアクティブな起動Epic P1 ~M

MrHuangが応答するだけでなく、促されずに自ら働きかけられるようにします。ストーリー: 永続的な基盤と同じテキスト音声変換エフェクターに乗る、スケジュール済みの朝の声かけ ; プロアクティブなメッセージが悪い瞬間をさらに悪化させることが決してないようにするクライシスガード。基盤はすでにクラッシュと再デプロイを乗り越えて存続しており、それこそが無人のスケジュール済みアクションを信頼して任せられる根拠です。

サンドボックス化された自律性Epic P2 ~L · ツールの牢獄が前提条件

エージェントのツールループの拒否を解きます — ただし本物のサンドボックスの内側に限ります。ストーリー: OSレベルの牢獄を出荷する(パスとルートの封じ込め、シンボリックリンクによる脱出の拒否、ネットワーク制限付きの実行コンテナ); すでに出荷済みの段階的な権限上限をその前段に維持する ; WhatsAppと音声チャネルはサンドボックスの内側でのみ動作可能で、ホスト上では決して動作しないことを強制する。ツールループは、牢獄が本物になるまでコード上で完全に拒否されたままです。

これら3つすべてを貫く荷重制約は、ブレイン・トランスプラントを安全にしたものと同じです。すなわち、副作用は — それが音声によるリマインダーであれ、促されない朝のメッセージであれ、ファイル書き込みであれ — それを要求したチャネルの権限を超えることが決してあってはならない、ということです。