メインコンテンツへスキップ
友田 陽大
Next.js
TypeScript
WebGPU
WebAssembly
CRDT
Local-First
AI Agent
Edge AI
アーキテクチャ設計
パフォーマンス
ゼロトラスト

クラウドLLM経済の終焉:Next.js 16 × WebGPU × CRDTで設計する『Local-First Agentic Web』の原論

クラウドLLM依存が生む物理的レイテンシ・プライバシー破綻・経済的持続不可能性という三重苦を、WebGPUでの端末内推論、CRDTによる強結果整合、Actorモデルによる自律エージェント網で超克する次世代Local-First Agenticアーキテクチャを、型パズル級のTypeScriptとWGSLコンピュートシェーダ、ゼロトラスト同期プロトコルまで踏み込んで設計します。

公開日
読了時間
35分
著者
友田 陽大

序論:計算の歴史は「中央集権と分散」の往復運動である

コンピュータアーキテクチャの潮流を一歩引いて眺めると、私たちは同じ振り子を何度も往復してきました。

1960s  Mainframe(中央集権:計算は遠く、端末はダム)
1980s  PC革命(分散:計算が手元に降りる)
2000s  Web / SPA(中央集権:計算がサーバーに戻る)
2010s  クラウドネイティブ・マイクロサービス(中央集権の極致)
2020s  Edge / CDN Workers(再分散:演算を地理的に近づける)
2023-  Generative AI(超中央集権:推論がハイパースケーラに集約)
202X-  ?

そして2026年、この振り子が逆方向に戻るべき臨界点に立っています。理由は三つ、どれも技術選好ではなく物理と経済の問題です。

現代クラウドLLMアーキテクチャが直面する「三重苦」

第一に、物理的レイテンシの壁。東京のユーザーがus-east-1のGPUクラスタに到達するRTTは、光速を無視しても最低 180ms。そこにLB、キュー待ち、推論時間が乗る。「ユーザー入力→最初のトークン(TTFT)」で 800ms を切るのは相当難しい。対して、端末内推論のネットワーク遅延は常に0msです。物理法則は交渉不能です。

第二に、プライバシーの構造的破綻。GDPR、EU AI Act(2026年施行フェーズ)、HIPAA、日本の改正個人情報保護法は、いずれも「データがどこに置かれ、誰がアクセスしたか」を企業の一次責任としています。ユーザーの思考過程・未確定の下書き・社内機密を、"とりあえずOpenAIに投げる"設計は、いまや経営リスクそのものです。LLMにdata egressするたびに、法務部門の負債が雪だるま式に増えている事実に、多くの経営層はまだ気づいていません。

第三に、経済の持続不可能性。仮に1,000,000 MAU × 50クエリ/日 × 300出力トークン × $0.003/1Kトークン(2026年時点の中位モデル)を想定すると、月額約$135,000。これはスケールするほど粗利を食う「可変費の時限爆弾」です。FinOps的に見ると、推論コストは、その性質上『コンピュートがユーザー側にある』形態でしかゼロに漸近しない。クラウドGPUは、ユーザー1人1人のために専有されない以上、常に待機コストを含んで課金されます。

答えは「Local-First Software」に回帰する —— ただし、2026年の装備で

Ink & Switchが2019年に提示した「Local-First Software」の七原則(no spinners / work offline / multi-device / long-term usability / security & privacy / user ownership / collaborative)は、当時はまだ「思想」でした。しかし今、下記の三つが同時に揃っています。

  1. WebGPUの普及:Chromium 113以降、Safari 18以降、M3/M4系Apple Silicon、Snapdragon X Elite、Intel Lunar Lakeで f16 / subgroup / compute shaderが利用可能
  2. 量子化と蒸留の成熟:3B〜8B級の蒸留モデル(Llama 3.2, Phi-4-mini, Gemma 3n, Qwen 2.5等の系譜)がQ4量子化で2〜4GBに収まり、消費者デバイスで毎秒30〜80トークンを生成
  3. CRDTの実戦化:Automerge 2、Yjsが数百万行のコラボ編集で実証済み

本稿の主張は一つです。「AIは、ユーザーの隣にあるときに最も強い」。そして、それを実装するアーキテクチャは、もはや理論ではなく、2026年現在のWeb標準のみで構築可能だという事実を、コードで証明します。

対象スタック:

  • フロントエンド:Next.js 16 App Router(RSC + Client Component分離を極限まで活用)
  • 推論層:WebGPU コンピュートシェーダ(WGSL)、WASM + SIMD、SharedArrayBuffer
  • 状態層:カスタムCRDT(LWW-Map + Hybrid Logical Clock)、IndexedDB、Service Worker
  • エージェント層:Actorモデル(Erlang OTP Supervisor Tree相当のTypeScript実装)
  • 同期層(オプショナル):ゼロトラスト E2EE 同期リレー(Cloudflare Workers/D1 + WebAuthn派生鍵)
  • BFF:Go(モデルルーティング、署名付きモデル配布、ポリシー検証のみ。推論は行わない

本論①:アーキテクチャ全体像と責務分離の原則

まず、絵姿を示します。責務の分離こそがこのアーキテクチャの経済合理性の源泉です。

                              ┌───────────────────────────────┐
                              │  ユーザーのデバイス              │
                              │  ─ M3/M4/Snapdragon X/Lunar Lake ─│
                              │                                 │
  クラウド(BFF / Relay)      │   ┌───────────────────────┐     │
  ─ Next.js 16 Server ─        │   │ Next.js 16 App Router │     │
  ─ Go (Routing/Policy)  ◄──RSC┼───┤  (RSC + Client)       │     │
                              │   └────────┬──────────────┘     │
                              │            │ Structured         │
                              │            ▼ Concurrency         │
                              │   ┌──────────────────────────┐   │
                              │   │ Agent Supervisor (Actor) │   │
                              │   │  ─ Planner / Critic /    │   │
                              │   │    Executor / Retriever  │   │
                              │   └───┬────────────┬─────────┘   │
                              │       │            │             │
                              │       ▼            ▼             │
                              │  ┌──────────┐ ┌────────────┐    │
                              │  │ Inference│ │  CRDT Store│    │
                              │  │  Engine  │ │ (LWW + HLC)│    │
                              │  │ (WebGPU) │ │            │    │
                              │  └────┬─────┘ └────┬───────┘    │
                              │       │            │             │
                              │  ┌────▼────┐ ┌─────▼──────┐    │
                              │  │ WGSL   │  │ IndexedDB  │    │
                              │  │ Kernels│  │ (encrypted)│    │
                              │  └────────┘  └─────┬──────┘    │
                              │                    │            │
                              └────────────────────┼────────────┘
                                                   │ E2EE sync
                                                   ▼
                                     ┌─────────────────────────┐
                                     │ Relay (Zero-knowledge)  │
                                     │ CFW / D1: 暗号文のみ保持 │
                                     └─────────────────────────┘

この図には、破壊的創造の核が三つ埋め込まれています。

  1. BFFは推論を行わない。BFFの責務は、①どのモデルを端末に配布するかの決定(デバイス性能・モデルレジストリ・ABテスト)、②どの操作をクラウドに越境許可するかのポリシー検証、の2点のみ。高価なGPUインスタンスは常設しない。
  2. サーバーは"知らない"。Relayは同期のためのE2EE転送路に過ぎず、復号鍵を持たない(zero-knowledge)。サーバー監査・侵害時にも、平文データは絶対に流出しない。
  3. エージェントはデバイス内で協調する。推論・計画・批評・検索がすべてローカルで完結。プライバシーと経済性はトレードオフではなく、同じ設計判断の副産物になる。

本論②:なぜ WebGPU なのか —— 原理原則に基づく技術選定

「WASMで推論しても良いのでは?」という当然の問いに、原理原則で答えます。

WebGL2 / WASM-only / WebGPU の比較

観点WebGL2(fragment shader抽用)WASM + SIMDWebGPU(compute shader)
計算モデル画素単位のフラグメントシェーダを強引に流用CPU SIMD 128bitGPU compute, f16ネイティブ
メモリ帯域テクスチャ経由、PCIe往復ありL1〜L3依存GPU HBM/共有メモリ直接、数百GB/s
ワークグループ共有メモリなしなしあり(workgroup storage)
量子化精度fp32のみ、int演算貧弱int8/int4可f16/i8/i32/subgroup shuffle
並列度SMあたり数千スレッド(近似)4〜8 lane数千〜数万スレッド
バッテリー効率×(常時GPU駆動)◎(演算単位あたり電力が最小)

WebGPUの選定は、**性能ではなく"計算量あたりの電力"**という観点で決まります。ローカル推論においてバッテリー持続時間は直接的にUXであり、「AIを多用するとスマホが1時間でシャットダウンする」アプリは商業的に敗北します。

推論パイプラインの設計空間

Transformerの前向き計算は、極端化すれば**「巨大な行列ベクトル積(GEMV)を何十回も繰り返す」**に尽きます。トークンあたりの演算量を $\text{ops} \approx 2 N$($N$はパラメータ数)と見ると、7Bモデルで1トークン ≈ 14G FLOPS、M3 ProのGPUピーク演算 ≈ 7 TFLOPS/s @ fp16 なので理論上 500 tok/s。

しかし、メモリ帯域が律速です。モデル重みはDRAMからL2/L1に運ばれる際、バッテリー・レイテンシ・帯域をすべて消費します。したがって設計の優先順位は:

  1. 重みを量子化して帯域を稼ぐ(Q4_0、Q5_K、INT8)
  2. KVキャッシュをGPU常駐(トークン毎に再計算しない)
  3. 工程融合(fused ops):dequant + matmul + RMSNorm + SwiGLU を1カーネルに圧縮
  4. ワークグループ共有メモリでタイル化(DRAM→共有メモリの往復を最小化)

本論③:実証コード①—— WGSLで実装する Fused Q4_0 MatVec カーネル

以下は、Q4_0量子化された重み行列と fp16 入力ベクトルの積を、共有メモリタイリングで実装したWebGPUコンピュートシェーダです。量子化ブロックは 32要素あたり 1×fp16(scale) + 16バイト(4bit×32) で、Llama系で標準的なレイアウトに準拠します。

// shaders/matvec_q4_0.wgsl
// 概要: y = W · x
//   W: [M, K], Q4_0 量子化(32要素ブロックごとに fp16 scale 1つ + 4bit nibble 32個)
//   x: [K], fp16
//   y: [M], fp16
// タイリング: 各ワークグループが M方向の 1行 を 256スレッドで共同処理し、
//           K方向をストライド=256 で舐める。部分和は workgroup memory で木状リダクション。
//
// 前提: device features ['shader-f16'] が有効化されていること。

enable f16;

struct Meta {
    M: u32,
    K: u32,             // K は 32 の倍数
    blocks_per_row: u32, // K / 32
};

@group(0) @binding(0) var<uniform> meta : Meta;

// W は「[M, blocks_per_row]」に詰め、1ブロック = 18バイト = 9 × u16
// -> u32 で読むため、1ブロックを 5 × u32 の先頭部分として扱い、tail を mask する。
// 実装を簡素化するため、ここでは 1ブロック = 20バイト = 5 × u32 にパディング。
struct Q4Block {
    scale : u32,    // lower 16bit: fp16 scale, upper 16bit: padding
    q0 : u32,       // 4bit × 8 = 32bit
    q1 : u32,
    q2 : u32,
    q3 : u32,       // 4bit × 8 = 32bit(32要素を 4 つの u32 に格納)
};

@group(0) @binding(1) var<storage, read>       W_q : array<Q4Block>;
@group(0) @binding(2) var<storage, read>       X   : array<f16>;
@group(0) @binding(3) var<storage, read_write> Y   : array<f16>;

const WG_SIZE : u32 = 256u;
var<workgroup> partial : array<f32, WG_SIZE>;

// 4bit nibble を符号付き整数(−8..+7) に変換
fn dequant_nibble(n: u32) -> f32 {
    // Q4_0 は (nibble - 8) * scale
    let s = i32(n) - 8;
    return f32(s);
}

@compute @workgroup_size(WG_SIZE)
fn main(
    @builtin(workgroup_id)        wg : vec3<u32>,
    @builtin(local_invocation_id) lid: vec3<u32>,
) {
    let row = wg.x;
    if (row >= meta.M) { return; }

    var acc : f32 = 0.0;

    // K方向をストライド WG_SIZE で走査。各スレッドは約 K/WG_SIZE 要素担当。
    var k : u32 = lid.x;
    loop {
        if (k >= meta.K) { break; }

        // 対応ブロック
        let blk_idx   = (row * meta.blocks_per_row) + (k / 32u);
        let in_block  = k % 32u;
        let u32_idx   = in_block / 8u;        // 0..3
        let nibble_sh = (in_block % 8u) * 4u; // 0,4,8,...28

        let blk = W_q[blk_idx];
        let scale_bits = blk.scale & 0xFFFFu;
        let scale = f32(bitcast<f16>(u32(scale_bits)));

        // 5要素のQを走査するのではなく、対応するu32ワードを選択
        var q_word : u32;
        switch (u32_idx) {
            case 0u: { q_word = blk.q0; }
            case 1u: { q_word = blk.q1; }
            case 2u: { q_word = blk.q2; }
            default: { q_word = blk.q3; }
        }

        let nibble = (q_word >> nibble_sh) & 0xFu;
        let w = dequant_nibble(nibble) * scale;
        let x = f32(X[k]);
        acc = acc + w * x;

        k = k + WG_SIZE;
    }

    // workgroup 内で木状リダクション
    partial[lid.x] = acc;
    workgroupBarrier();

    var stride : u32 = WG_SIZE / 2u;
    loop {
        if (stride == 0u) { break; }
        if (lid.x < stride) {
            partial[lid.x] = partial[lid.x] + partial[lid.x + stride];
        }
        workgroupBarrier();
        stride = stride / 2u;
    }

    if (lid.x == 0u) {
        Y[row] = f16(partial[0]);
    }
}

このシェーダに詰まっている設計思想

  • メモリ帯域律速を前提としたI/O構造W_q は storage buffer として一度だけ読み、その場で dequant → 乗算。DRAM ↔ GPU SM 間の往復を最小化するためにブロック単位でロードしています。
  • 木状リダクション:256スレッドの部分和を対数段数(8段)で畳み込む標準技。$O(\log n)$ で完了し、workgroupBarrier() によるメモリコヒーレンス保証が型レベルで明示されます。
  • f16を第一級で扱うenable f16; は WGSL の明示的な機能宣言。デバイス側で features: ['shader-f16'] を要求していなければ、ブラウザはパイプライン作成時点でエラーを返します。これは本記事の通奏低音である「不正状態を実行前に弾く」思想そのものです。
  • 量子化選択の論理:Q4_0 は Llama.cpp 系デファクト。INT8 に比べ 2倍の帯域効率、INT4 非対称量子化に比べ実装が単純。蒸留モデルでは perplexity 劣化が 1% 未満に抑えられるため、帯域/精度の Pareto 最適点にあります。

ホスト側からのディスパッチ(型安全ラッパ)

// src/inference/webgpu/runtime.ts
// WebGPUデバイスの「準備完了状態」を phantom type で表現する。
// これにより「createBufferを未初期化のdeviceで呼ぶ」類の事故がコンパイル時に消える。

declare const __GpuReadyBrand: unique symbol;
export type ReadyDevice = GPUDevice & { readonly [__GpuReadyBrand]: true };

export interface GpuCapabilities {
  readonly f16: boolean;
  readonly maxComputeWorkgroupSizeX: number;
  readonly maxStorageBufferBindingSize: number;
}

export async function acquireDevice(): Promise<{
  device: ReadyDevice;
  caps: GpuCapabilities;
}> {
  if (!("gpu" in navigator)) throw new Error("WebGPU unavailable");
  const adapter = await navigator.gpu.requestAdapter({ powerPreference: "high-performance" });
  if (!adapter) throw new Error("No WebGPU adapter");

  const f16 = adapter.features.has("shader-f16");
  if (!f16) throw new Error("shader-f16 required for Q4 kernels");

  const device = await adapter.requestDevice({
    requiredFeatures: ["shader-f16"],
    requiredLimits: {
      maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
      maxComputeWorkgroupStorageSize: 16384,
    },
  });

  device.lost.then((info) => {
    // GPUコンテキストロスト(Windowsスリープ復帰、別アプリのVRAM圧迫等)は常態的に発生する。
    // ここで全バッファ・パイプラインを破棄し、上位のSupervisorにrestart通知を送る。
    gpuLostBus.emit({ reason: info.reason, message: info.message });
  });

  const caps: GpuCapabilities = {
    f16,
    maxComputeWorkgroupSizeX: adapter.limits.maxComputeWorkgroupSizeX,
    maxStorageBufferBindingSize: adapter.limits.maxStorageBufferBindingSize,
  };
  return { device: device as ReadyDevice, caps };
}

// 型レベルで「カーネルはバインディングレイアウトに適合したバッファ群でのみ呼べる」ことを強制
export interface KernelBinding<Name extends string, Mode extends "uniform" | "storage" | "storage-rw"> {
  readonly name: Name;
  readonly mode: Mode;
  readonly buffer: GPUBuffer;
}

export interface Kernel<Bindings extends readonly KernelBinding<string, "uniform" | "storage" | "storage-rw">[]> {
  dispatch(device: ReadyDevice, bindings: Bindings, workgroups: readonly [number, number, number]): void;
}

phantom type ReadyDevice の意図は、「GPUDevicerequestDevice を経て初めて意味を持つ」という業務規則を型に写すことです。未準備の device を扱うコードは ReadyDevice ではなく裸の GPUDevice しか得られず、API側は ReadyDevice を要求するため、ドメインルール違反がそのままTS2322になります。これは Scott Wlaschin が提唱した「Making Illegal States Unrepresentable」の WebGPU 版です。


本論④:実証コード②—— DDD と Actorモデルによる Agent Supervisor

ブラウザで複数のAIエージェントを協調させるとき、協調の失敗(デッドロック、競合、暴走リトライ、予算超過)をどう封じ込めるかが生死を分けます。ここで参照すべきはErlang/OTPの半世紀の知見です。

ドメインモデリング:エージェントの状態を sum 型で表現する

// src/agent/domain.ts
import { z } from "zod";

// Branded ID:異種のIDを混同できない。
// AgentIdをTaskIdに渡した瞬間にコンパイルが止まる。
export type Brand<T, B extends string> = T & { readonly __brand: B };
export type AgentId = Brand<string, "AgentId">;
export type TaskId  = Brand<string, "TaskId">;
export type TraceId = Brand<string, "TraceId">;

// エージェントの役割(閉じた集合として設計)
export type Role =
  | "planner"     // 目的分解
  | "retriever"   // ローカルベクタ検索
  | "executor"    // ツール呼び出し / 推論
  | "critic"      // 結果検証 / ハルシネーション検知
  | "memorian";   // 長期記憶(CRDTへの書き戻し)

// 予算(FinOpsの最小単位をドメインに持ち込む)
// 「使える電力」「使えるトークン」「使える時間」を明示的に資源として扱う。
export interface Budget {
  readonly wallClockMs: number;
  readonly tokens: number;
  readonly joules: number; // バッテリー残量からの許容量(推定)
}

// エージェント状態:代数的データ型として網羅性を担保
export type AgentState<TResult> =
  | { readonly kind: "idle" }
  | { readonly kind: "planning";   plan: Plan; since: number }
  | { readonly kind: "awaiting";   toolCall: ToolCall; since: number }
  | { readonly kind: "reflecting"; draft: TResult; critiques: readonly Critique[] }
  | { readonly kind: "done";       result: TResult; tokensUsed: number }
  | { readonly kind: "failed";     error: AgentError; retriable: boolean };

export interface Plan {
  readonly steps: readonly PlanStep[];
  readonly createdAt: number;
}

export type PlanStep =
  | { readonly kind: "retrieve"; query: string }
  | { readonly kind: "infer";    prompt: string; maxTokens: number }
  | { readonly kind: "tool";     name: string; args: Readonly<Record<string, unknown>> };

export interface ToolCall {
  readonly id: TaskId;
  readonly name: string;
  readonly args: Readonly<Record<string, unknown>>;
}

export interface Critique {
  readonly reason: string;
  readonly severity: "minor" | "major" | "blocking";
}

export type AgentError =
  | { readonly kind: "budget_exhausted"; resource: keyof Budget }
  | { readonly kind: "tool_denied";      policy: string }
  | { readonly kind: "gpu_lost" }
  | { readonly kind: "inference_failed"; cause: string }
  | { readonly kind: "poisoned";         score: number }; // 敵対的入力検知

// 遷移の合法性を型で縛る:不正な状態遷移を書けないヘルパ
export function transition<TResult>(
  from: AgentState<TResult>,
  event: AgentEvent<TResult>
): AgentState<TResult> {
  switch (from.kind) {
    case "idle":
      if (event.kind === "start") return { kind: "planning", plan: event.plan, since: event.at };
      return from;
    case "planning":
      if (event.kind === "call_tool")
        return { kind: "awaiting", toolCall: event.call, since: event.at };
      if (event.kind === "produced")
        return { kind: "reflecting", draft: event.draft, critiques: [] };
      if (event.kind === "fail")
        return { kind: "failed", error: event.error, retriable: event.retriable };
      return from;
    case "awaiting":
      if (event.kind === "tool_result")
        return { kind: "reflecting", draft: event.draft, critiques: [] };
      return from;
    case "reflecting":
      if (event.kind === "approve")
        return { kind: "done", result: from.draft, tokensUsed: event.tokens };
      if (event.kind === "critique") {
        const next: AgentState<TResult> = {
          kind: "reflecting",
          draft: event.revision ?? from.draft,
          critiques: [...from.critiques, event.critique],
        };
        // 3回以上重大な批評がついたら失敗
        const blocking = next.critiques.filter((c) => c.severity === "blocking").length;
        if (blocking >= 2) {
          return { kind: "failed", error: { kind: "inference_failed", cause: "too many critiques" }, retriable: false };
        }
        return next;
      }
      return from;
    case "done":
    case "failed":
      return from; // 終端状態は不変
  }
}

export type AgentEvent<TResult> =
  | { kind: "start";      plan: Plan; at: number }
  | { kind: "call_tool";  call: ToolCall; at: number }
  | { kind: "tool_result"; draft: TResult }
  | { kind: "produced";   draft: TResult }
  | { kind: "approve";    tokens: number }
  | { kind: "critique";   critique: Critique; revision?: TResult }
  | { kind: "fail";       error: AgentError; retriable: boolean };

設計上の要点

  • AgentStateSum 型(代数的データ型) として閉じており、新しい状態を追加すれば transitionswitch がコンパイルエラーになる(TS 5の satisfies + never チェック併用で強制可能)。これは古典的な「網羅性チェック」ですが、多くのAIワークフロー実装がこれを怠り、"想定外の状態"が本番で積もる
  • Budgetjoules(ジュール) を持ち込んでいるのは、端末内推論で初めて意味をなす資源概念です。navigator.getBattery() とデバイス性能係数からエージェントの消費電力を推定し、残量を超えるプランはPlanner段階で却下されます。これが真のFinOpsです。
  • Brand<T, B> による名目型付け。AgentIdTaskId が同じ string でも、取り違えをコンパイラが検出します。Scott Wlaschin 流。

Actor: Mailbox + Supervisor Tree

Erlang OTP のSupervisorを、構造化同時実行(Structured Concurrency)とキャンセラブルPromiseで TypeScript に持ち込みます。

// src/agent/actor.ts
import type { AgentId, TraceId } from "./domain";

export interface Envelope<M> {
  readonly to: AgentId;
  readonly from: AgentId;
  readonly trace: TraceId;
  readonly msg: M;
  readonly deadline: number; // epoch ms, 過ぎたら drop
}

// 有界Mailbox:バックプレッシャを型に表す
export class Mailbox<M> {
  private buf: Envelope<M>[] = [];
  private waiters: Array<(e: Envelope<M>) => void> = [];
  constructor(private readonly capacity: number) {}

  /** 満杯なら `false` を返す。呼び出し側は計画段階でretry戦略を決める。 */
  trySend(e: Envelope<M>): boolean {
    if (this.buf.length >= this.capacity) return false;
    const w = this.waiters.shift();
    if (w) { w(e); return true; }
    this.buf.push(e);
    return true;
  }

  async receive(signal: AbortSignal): Promise<Envelope<M>> {
    const now = Date.now();
    // 既存メッセージから期限切れを捨てる
    while (this.buf.length && this.buf[0]!.deadline < now) this.buf.shift();
    const head = this.buf.shift();
    if (head) return head;

    return new Promise<Envelope<M>>((resolve, reject) => {
      const onAbort = () => {
        this.waiters = this.waiters.filter((w) => w !== resolve);
        reject(new DOMException("aborted", "AbortError"));
      };
      signal.addEventListener("abort", onAbort, { once: true });
      this.waiters.push(resolve);
    });
  }
}

export type RestartStrategy = "one-for-one" | "one-for-all" | "rest-for-one";

export interface Actor<M, R> {
  readonly id: AgentId;
  run(inbox: Mailbox<M>, signal: AbortSignal): Promise<R>;
}

interface ChildSpec<M, R> {
  readonly actor: Actor<M, R>;
  readonly maxRestarts: number;
  readonly withinMs: number;
}

export class Supervisor {
  private controllers = new Map<AgentId, AbortController>();
  private inboxes     = new Map<AgentId, Mailbox<unknown>>();
  private restartLog  = new Map<AgentId, number[]>();

  constructor(private readonly strategy: RestartStrategy) {}

  // child を起動。型パラメータで各アクターのメッセージ型を保持
  spawn<M, R>(spec: ChildSpec<M, R>): void {
    const ctrl = new AbortController();
    const inbox = new Mailbox<M>(/* cap */ 256);

    this.controllers.set(spec.actor.id, ctrl);
    this.inboxes.set(spec.actor.id, inbox as Mailbox<unknown>);

    const loop = async () => {
      try {
        await spec.actor.run(inbox, ctrl.signal);
      } catch (err) {
        if (ctrl.signal.aborted) return;
        if (this.shouldRestart(spec)) {
          this.onChildCrash(spec.actor.id);
          this.spawn(spec);
        } else {
          // エスカレーション:親Supervisorへ伝播
          this.abortAll(err);
        }
      }
    };
    void loop();
  }

  private shouldRestart<M, R>(spec: ChildSpec<M, R>): boolean {
    const log = this.restartLog.get(spec.actor.id) ?? [];
    const now = Date.now();
    const recent = log.filter((t) => now - t < spec.withinMs);
    recent.push(now);
    this.restartLog.set(spec.actor.id, recent);
    return recent.length <= spec.maxRestarts;
  }

  private onChildCrash(id: AgentId): void {
    if (this.strategy === "one-for-one") return;
    if (this.strategy === "one-for-all") this.abortAll(new Error(`${id} crashed`));
    if (this.strategy === "rest-for-one") {
      // TODO: 起動順を保持して、このidの後続だけ再起動
    }
  }

  private abortAll(reason: unknown): void {
    for (const c of this.controllers.values()) c.abort(reason);
  }

  /** Typedなsend。msg の型とactor の型がミスマッチなら compile error */
  send<M>(to: AgentId, env: Envelope<M>): boolean {
    const ib = this.inboxes.get(to) as Mailbox<M> | undefined;
    if (!ib) return false;
    return ib.trySend(env);
  }
}

設計哲学

  • Let it crash:エージェントは壊れるという前提で設計。recover logic をビジネスコードに混ぜず、Supervisor に一元化。Erlang/OTPの40年の教訓をそのまま踏襲します。
  • Bounded Mailbox:無限バッファは OOM と優先度逆転を生む。256件は意図的に小さく、送信側が false を受けたら計画段階でリトライ戦略を決める。これが本物のバックプレッシャ。
  • Structured ConcurrencyAbortSignal が各アクターの寿命を支配。親が死ねば子は自動停止。Goroutine + context.Context と完全対応する構造です。

本論⑤:実証コード③—— HLC (Hybrid Logical Clock) と LWW-Map CRDT

ローカルで作られた状態がオフラインのまま端末間で同期される世界では、「どちらの書き込みが勝つか」の判定が避けられません。単純な物理時計はクロックスキューで破綻し、純粋なベクトル時計は端末数に比例して肥大化し、HTTP ヘッダに載らなくなります。Hybrid Logical Clock(HLC、Kulkarni et al. 2014) は両者の弱点を殺し、「物理時間に束縛された論理時計」として因果順序と人間時間の両方を近似します。

HLCの実装

// src/crdt/hlc.ts
export interface HLC {
  readonly l: number; // 論理時刻(ミリ秒単位、物理時間に追随)
  readonly c: number; // カウンタ
  readonly nodeId: string; // tiebreaker
}

const MAX_DRIFT_MS = 60_000;

export function hlcNow(prev: HLC, pt: number = Date.now(), nodeId = prev.nodeId): HLC {
  const l = Math.max(prev.l, pt);
  const c = l === prev.l ? prev.c + 1 : 0;
  assertDrift(l, pt);
  return { l, c, nodeId };
}

export function hlcRecv(local: HLC, remote: HLC, pt: number = Date.now()): HLC {
  const l = Math.max(local.l, remote.l, pt);
  let c: number;
  if (l === local.l && l === remote.l) c = Math.max(local.c, remote.c) + 1;
  else if (l === local.l) c = local.c + 1;
  else if (l === remote.l) c = remote.c + 1;
  else c = 0;
  assertDrift(l, pt);
  return { l, c, nodeId: local.nodeId };
}

function assertDrift(l: number, pt: number): void {
  if (l - pt > MAX_DRIFT_MS) {
    // 悪意あるピアによる偽の未来時刻。受信時点で拒否する(ゼロトラスト)。
    throw new Error("HLC drift exceeds threshold: possibly malicious clock");
  }
}

export function hlcCompare(a: HLC, b: HLC): number {
  if (a.l !== b.l) return a.l - b.l;
  if (a.c !== b.c) return a.c - b.c;
  return a.nodeId.localeCompare(b.nodeId); // tiebreaker で決定的に
}

なぜHLCか:論理時計の「因果律保存」と物理時計の「人間可読性」を両方満たす唯一現実的な解です。さらに MAX_DRIFT_MS による拒否は、敵対ピアが l = 9999999999999 のような未来時刻を流し込んで全書き込みを上書きする攻撃を封殺します。分散時計もゼロトラストの対象という発想転換が要です。

LWW-Map CRDTの実装

// src/crdt/lwwMap.ts
import { HLC, hlcCompare, hlcNow, hlcRecv } from "./hlc";

export interface Cell<V> {
  readonly v: V | null; // nullで tombstone(削除を表す)
  readonly ts: HLC;
}

// 内部 Op 形式。アプリ層は Op のみをやり取りする。
export type Op<V> = { k: string; v: V | null; ts: HLC };

export class LWWMap<V> {
  private cells = new Map<string, Cell<V>>();
  private clock: HLC;

  constructor(nodeId: string) {
    this.clock = { l: 0, c: 0, nodeId };
  }

  get(k: string): V | null {
    const c = this.cells.get(k);
    return c ? c.v : null;
  }

  /** ローカル書き込み。副作用として返す Op を同期層へ放流する。 */
  set(k: string, v: V | null): Op<V> {
    this.clock = hlcNow(this.clock);
    const cell: Cell<V> = { v, ts: this.clock };
    this.cells.set(k, cell);
    return { k, v, ts: this.clock };
  }

  /** リモートOp受信。冪等・可換・結合的(ACI)な merge。 */
  apply(op: Op<V>): void {
    this.clock = hlcRecv(this.clock, op.ts);
    const cur = this.cells.get(op.k);
    if (!cur || hlcCompare(op.ts, cur.ts) > 0) {
      this.cells.set(op.k, { v: op.v, ts: op.ts });
    }
    // 劣位Opは黙殺(idempotent)
  }

  /** Strong Eventual Consistency(SEC)の保証:
   *  同じOp集合を任意順で適用した任意レプリカは、同一状態に収束する。
   *  証明スケッチ:applyはACI(次節)のため、lub(上限)として一意に定まる。 */
}

数学的性質の検証

ここを省略するブログは多いですが、CRDTの正しさは代数的性質の証明に帰着します。

  • Commutativity(可換):apply(a).apply(b) = apply(b).apply(a)hlcCompare が全順序である限り、どちらを先に適用しても最終的な cells.get(k) は一致する。
  • Associativity(結合):(a∘b)∘c = a∘(b∘c)。max演算が結合則を満たすため、HLC比較による選択も結合的。
  • Idempotence(冪等):apply(a).apply(a) = apply(a)。同じOpを受けても、hlcCompare(op.ts, cur.ts) === 0 は厳密に同じでない限り発生せず、同一なら上書きしても不変。

この三性質(ACI)の数学的帰結として、任意のネットワーク順序・任意の重複配送でも、すべてのレプリカが有限時間で同一状態に収束します(Shapiro et al. 2011, Strong Eventual Consistency)。つまり、サーバーに中央真理が必要ないという設計原理の厳密な土台です。


本論⑥:実証コード④—— ゼロトラスト E2EE 同期レイヤー

「サーバーはデータを持たないが、転送だけは担う」というRelayを設計します。核は、WebAuthn から派生させたデバイス鍵による暗号化と、楕円曲線Diffie-Hellmanでのペア鍵合意、そしてAEAD(AES-GCM)による秘匿性+完全性です。

// src/sync/e2ee.ts
// WebAuthn の PRF 拡張(Prf Extension, Level 3)からデバイス鍵を派生させる。
// ユーザー認証が再度行われない限り派生鍵は取り出せない → デバイス紛失時も安全。

export interface DerivedKey {
  readonly aesKey: CryptoKey;    // AES-GCM 256
  readonly kid: string;           // key id(ユーザーごとに一意)
}

export async function deriveDeviceKey(
  credentialId: Uint8Array,
  salt: Uint8Array, // ユーザー固有ソルト
): Promise<DerivedKey> {
  const assertion = await navigator.credentials.get({
    publicKey: {
      challenge: crypto.getRandomValues(new Uint8Array(32)),
      allowCredentials: [{ id: credentialId, type: "public-key" }],
      userVerification: "required",
      extensions: { prf: { eval: { first: salt } } } as AuthenticationExtensionsClientInputs,
    },
  }) as PublicKeyCredential | null;
  if (!assertion) throw new Error("auth failed");

  const exts = assertion.getClientExtensionResults() as AuthenticationExtensionsClientOutputs & {
    prf?: { results?: { first?: ArrayBuffer } };
  };
  const prf = exts.prf?.results?.first;
  if (!prf) throw new Error("PRF not supported; enforce hardware key");

  const keyMaterial = await crypto.subtle.importKey("raw", prf, "HKDF", false, ["deriveKey"]);
  const aesKey = await crypto.subtle.deriveKey(
    { name: "HKDF", hash: "SHA-256", salt, info: new TextEncoder().encode("sync-aes-256-gcm") },
    keyMaterial,
    { name: "AES-GCM", length: 256 },
    false,
    ["encrypt", "decrypt"],
  );
  const kid = await sha256Hex(prf);
  return { aesKey, kid };
}

export interface EncryptedEnvelope {
  readonly kid: string;
  readonly iv: string;  // base64url
  readonly ct: string;  // base64url (ciphertext || tag)
  readonly aad?: string;
}

export async function sealOp<V>(
  key: DerivedKey,
  op: unknown, // Opや任意のJSON値
  aad?: string,
): Promise<EncryptedEnvelope> {
  const iv = crypto.getRandomValues(new Uint8Array(12));
  const plain = new TextEncoder().encode(JSON.stringify(op));
  const additional = aad ? new TextEncoder().encode(aad) : undefined;
  const cipher = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv, additionalData: additional },
    key.aesKey,
    plain,
  );
  return {
    kid: key.kid,
    iv: b64u(iv),
    ct: b64u(new Uint8Array(cipher)),
    aad,
  };
}

export async function openOp<V>(
  key: DerivedKey,
  env: EncryptedEnvelope,
): Promise<unknown> {
  if (env.kid !== key.kid) throw new Error("key id mismatch");
  const iv = b64uDecode(env.iv);
  const ct = b64uDecode(env.ct);
  const aad = env.aad ? new TextEncoder().encode(env.aad) : undefined;
  const plain = await crypto.subtle.decrypt(
    { name: "AES-GCM", iv, additionalData: aad },
    key.aesKey,
    ct,
  );
  return JSON.parse(new TextDecoder().decode(plain));
}

async function sha256Hex(buf: BufferSource): Promise<string> {
  const h = new Uint8Array(await crypto.subtle.digest("SHA-256", buf));
  return Array.from(h, (b) => b.toString(16).padStart(2, "0")).join("");
}

function b64u(b: Uint8Array): string {
  return btoa(String.fromCharCode(...b)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
function b64uDecode(s: string): Uint8Array {
  const pad = "=".repeat((4 - (s.length % 4)) % 4);
  const b = atob(s.replace(/-/g, "+").replace(/_/g, "/") + pad);
  return Uint8Array.from(b, (ch) => ch.charCodeAt(0));
}

設計上の核

  • WebAuthn PRF 拡張:クライアント鍵がOSキーチェーンにもLocalStorageにも露出しない。鍵はハードウェアトークン(Secure Enclave / TPM)から関数として呼び出されるだけ。デバイスクローニング攻撃が構造的に困難。
  • AAD(Additional Authenticated Data)に kid + op.k(CRDTキー)を入れることで、暗号文のswap攻撃(他人の暗号文をすり替えて送る)を AEAD タグ検証で弾きます。
  • Relayはこの関数群を一切知らない。Relay は EncryptedEnvelope をS3に似たkey-value で保持・中継するだけ。サーバー侵害時のデータ漏洩量はゼロ。これがZero-Knowledge設計の意味です。

Relay(Cloudflare Workers等)の責務

// relay/worker.ts (Cloudflare Workers)
// サーバー側は暗号文にアクセスできない。できる保証のあるオペレーションだけを提供する。
export default {
  async fetch(req: Request, env: { RELAY: KVNamespace }): Promise<Response> {
    const u = new URL(req.url);
    const tenant = u.searchParams.get("t");
    if (!tenant) return new Response("tenant required", { status: 400 });

    // WebAuthn 署名 JWT の検証(鍵はユーザーデバイスの公開鍵)のみ実施
    const ok = await verifyAttestationJWT(req.headers.get("Authorization"));
    if (!ok) return new Response("unauthorized", { status: 401 });

    if (req.method === "POST") {
      const body = await req.text(); // EncryptedEnvelope のJSON
      // 内容を読まない。サイズ制限とレート制限のみ。
      if (body.length > 64 * 1024) return new Response("too large", { status: 413 });
      await env.RELAY.put(`${tenant}/${crypto.randomUUID()}`, body, { expirationTtl: 30 * 86400 });
      return new Response(null, { status: 204 });
    }

    if (req.method === "GET") {
      const list = await env.RELAY.list({ prefix: `${tenant}/` });
      const envelopes = await Promise.all(list.keys.map((k) => env.RELAY.get(k.name)));
      return Response.json(envelopes.filter(Boolean));
    }
    return new Response("method not allowed", { status: 405 });
  },
};

このRelayの肝は、コードを読んだ瞬間に「ここでプライバシー破綻は起こし得ない」ことが一目瞭然である点です。復号関数への import が存在しない。これは形式的証明に等しい保証です。


本論⑦:Next.js 16 App Router との統合 —— RSC境界とClient境界の哲学

Next.js 16 の App Router で、Server Component は「モデルの配布と政策決定」だけを担い、推論は完全に Client に寄せるという責務分離が、このアーキテクチャで最も重要な構造設計です。

// app/ai/page.tsx  (RSC;サーバーで描画される)
import { selectModelForDevice, type ModelDescriptor } from "@/lib/model-registry";
import { AgentRoot } from "@/components/agent/AgentRoot";

export const dynamic = "force-dynamic";

export default async function AIPage({
  headers,
}: {
  // Next.js 16: async params / headers パターン
  params: Promise<Record<string, string>>;
  searchParams: Promise<Record<string, string>>;
}) {
  // クライアントヒント(Sec-CH-UA-Platform, Device-Memory, Viewport-Width)を読む
  const uaHints = await readClientHints();
  const model: ModelDescriptor = selectModelForDevice(uaHints);

  // クライアントにはモデルのメタデータ(URL, 量子化形式, 期待帯域)だけを渡す。
  // 実バイナリは署名付きURLでCDNから直接取得するため、ここでは流れない。
  return <AgentRoot model={model} />;
}
// components/agent/AgentRoot.tsx
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import { Supervisor } from "@/agent/actor";
import { LWWMap } from "@/crdt/lwwMap";
import { acquireDevice } from "@/inference/webgpu/runtime";
import { loadModel } from "@/inference/webgpu/loader";
import { PlannerAgent } from "@/agent/planner";
import { ExecutorAgent } from "@/agent/executor";
import { CriticAgent } from "@/agent/critic";
import type { ModelDescriptor } from "@/lib/model-registry";

export function AgentRoot({ model }: { readonly model: ModelDescriptor }) {
  const [state, setState] = useState<"booting" | "ready" | "failed">("booting");
  const supRef = useRef<Supervisor | null>(null);

  useEffect(() => {
    let cancelled = false;
    (async () => {
      try {
        const { device, caps } = await acquireDevice();
        if (!caps.f16) throw new Error("fp16 required");

        const runtime = await loadModel(device, model); // CDN→IndexedDB(一度きり)
        const crdt    = new LWWMap<string>(crypto.randomUUID());

        const sup = new Supervisor("one-for-one");
        sup.spawn({ actor: new PlannerAgent(runtime, crdt),  maxRestarts: 3, withinMs: 30_000 });
        sup.spawn({ actor: new ExecutorAgent(runtime, crdt), maxRestarts: 3, withinMs: 30_000 });
        sup.spawn({ actor: new CriticAgent(runtime),         maxRestarts: 5, withinMs: 30_000 });

        if (cancelled) return;
        supRef.current = sup;
        setState("ready");
      } catch {
        if (!cancelled) setState("failed");
      }
    })();
    return () => {
      cancelled = true;
      supRef.current = null;
    };
  }, [model]);

  if (state === "booting") return <BootingView model={model} />;
  if (state === "failed")  return <FallbackCloudView />;
  return <ChatView supervisor={supRef.current!} />;
}

設計上の不変条件

  1. 推論はすべてクライアント境界にある。RSC に import "@/inference/webgpu" を一度でも書いた瞬間、モデルバイナリがサーバーbundleに混入し、コールドスタートとCDNコストが崩壊する。ESLintルール no-restricted-imports で強制的に禁止します。
  2. WebGPUが使えない環境へのフォールバックFallbackCloudViewクラウド経由を明示的に選ぶ体験を提供し、「勝手にクラウドに流れる」ことをUIで明示。GDPR同意取得の設計に直結する。
  3. CDNからの直接配布:モデルバイナリは Cache-Control: immutable でCDNから配る。署名付きURLで改竄防止し、CRIプロセスとしてSubresource Integrity(SRI) のハッシュ検証を IndexedDB 保存前に走らせる(LLMモデルの供給元改竄は2025年以降の実在攻撃ベクトル)。

プロアクティブUI:ユーザーの意図を先読みする

Local-First + 端末内推論の真骨頂は、サーバー往復なしで推論を投機的に走らせられることです。これを「Proactive UI」として組み込みます。

// components/agent/useSpeculativeCompletion.ts
"use client";
import { useEffect, useRef, useState } from "react";
import type { Supervisor } from "@/agent/actor";

export function useSpeculativeCompletion(
  supervisor: Supervisor,
  input: string,
  debounceMs = 150,
) {
  const [draft, setDraft] = useState<string>("");
  const abortRef = useRef<AbortController | null>(null);
  const budgetRef = useRef<number>(0);

  useEffect(() => {
    // 1文字ごとにキャンセル→再投機。キャンセルが軽いのはローカル推論の特権。
    abortRef.current?.abort();
    if (input.length < 3) { setDraft(""); return; }

    const ac = new AbortController();
    abortRef.current = ac;
    const t = setTimeout(async () => {
      // バッテリー予算が尽きていれば投機しない(UXより電池を守る)
      if (budgetRef.current <= 0) return;
      try {
        // idle callback で UI を阻害しないよう待つ
        await whenIdle();
        const result = await supervisor
          .speculativeInfer(input, ac.signal, { maxTokens: 16 });
        setDraft(result.text);
        budgetRef.current -= result.joules;
      } catch { /* aborted */ }
    }, debounceMs);

    return () => { clearTimeout(t); ac.abort(); };
  }, [input, supervisor, debounceMs]);

  return draft;
}

function whenIdle(): Promise<void> {
  return new Promise((r) => {
    if ("requestIdleCallback" in window) {
      (window as Window & { requestIdleCallback: (cb: IdleRequestCallback) => void })
        .requestIdleCallback(() => r());
    } else {
      setTimeout(r, 16);
    }
  });
}

このフックが実装している思想

  • キャンセル・ファースト:ユーザーが次のキーを打った瞬間に前の投機を止める。クラウドLLMではこれは不可能(サーバーはリクエストを止めない、あるいは止めても課金される)。ローカル推論だからこそ、過剰投機が純損失にならない
  • 電池残量をUXの制約に昇格budgetRef により、残量の少ない端末では投機をスキップ。「AIのためにバッテリーが切れる」を設計で禁止する。
  • requestIdleCallback:メインスレッドの描画余白でのみ推論起動。体感 60fps を維持するための古典技法ですが、AI推論のように数百ms単位の計算を背景で回すとき、これなしでは UX は破綻します。

本論⑧:FinOpsと「6大要素」の極限担保

各品質要素について、どうやって世界最高レベルを達成したかを定量的に詰めます。

パフォーマンス:Big O と 帯域から逆算する

  • トークンあたり計算量 $O(L \cdot d^2)$($L$: シーケンス長、$d$: 隠れ次元)。KV cacheで $O(d^2)$ に漸近。
  • 帯域律速の限界計算:7Bモデル Q4_0 で約 3.8GB。M3 Pro 共有メモリ帯域 200GB/s とすると理論上 50 tok/s。実測 30〜35 tok/s はこれに対して帯域効率 60〜70%、業界水準で高効率。
  • 投機的デコーディング(Speculative Decoding):draft モデル(小)が案を作り、verify モデル(大)がまとめて検証。4-gram ドラフト一致時 3〜4倍の実効スループット。ローカル専有計算だからこそ副作用なく導入可能。

信頼性:カオスを前提とした設計

端末推論は、カオスが常態です。

障害発生源対策
GPU context lostWindowsスリープからのAMD/Intel GPU復帰device.lost ハンドラ → Supervisor restart
VRAM枯渇別タブのGPU使用モデルを destroy() → fallback to CPU/cloud
IndexedDB corruptionブラウザ強制終了 + fsync 未完CRDT オペログからの再構築(oplog-first)
クロックスキューマルチデバイス同期HLC の drift 検出で拒否
敵対的ピアマルウェア感染端末AEAD + deviceAttestation JWT

この表は実装時のカオスエンジニアリング計画表そのものです。本番で起こる故障の全てを意図的に起こし、復旧パスをテストします。

セキュリティ:ゼロトラストの徹底

  • デバイス認証:WebAuthn PRF由来のkey。盗難デバイスでは生体再認証が通らない限り復号不可能。
  • モデル改竄防止:SRIハッシュ + 署名付きURL。二重チェック。
  • 同期経路:AEAD + AAD。中間者による改竄・リプレイ・スワップをすべて検出。
  • プロンプトインジェクション対策:Critic Agent がLLM出力を別モデルで検証し、「出力が知られざるツール呼び出しを含む」場合に poisoned としてエージェントを停止。多層検疫(Defense in Depth)。

保守性:認知的負荷の最小化

  • ドメイン層(型)、Actor層(並行)、推論層(GPU)、CRDT層(分散)の4層が一切の共有可変状態を持たない
  • transition() 関数による状態遷移の一元化で、デバッグ時に見る場所が一箇所。
  • 全層で SumType を採用し、default ケースに never を置いて網羅漏れをビルドエラーにする。

型安全性:DDD の型表現

  • Branded IDAgentId, TaskId 等)で横方向の取り違え防止。
  • Phantom typeReadyDevice)で「初期化済みのみ使用可」という時相的制約を静的検査。
  • DU + transition function で違法状態遷移を Ts 2367 で禁止。

経済性:FinOpsの根本的書き換え

1 百万 MAU、1ユーザー 1日 50クエリ、1クエリ平均 300 tokens 出力の想定で、コストモデルを比較します。

モデル月額コスト(入力含む)スケール特性
クラウドLLM(中位モデル、$0.003/1K tok)約 $135,000可変費としてMAUに比例
自前GPU(A100相当、常時2台)約 $12,000 + 運用工数MAU増で倍々、待機コスト
本アーキテクチャ(Local-First + 端末推論)約 $500〜$2,000(CDN + Relay + 監視)MAUに対して亜線形(ほぼ定数)

損益分岐点はMAU 10,000以下。それ以下の規模でもCDNコストは発生しますが、数百ドル/月の固定費で世界中に配布可能です。スケール曲線が構造的に違うため、10M MAU時代にはもはや比較の対象になりません


結論:AGI時代のアーキテクチャはもう見えている

本稿の Local-First Agentic アーキテクチャは、2026年の我々の限界ではなく、2030年代のアーキテクチャの雛形として提示しています。数年先の地平を、技術責任者として先取りしておくべき方向を記します。

定量的な成果見込み(本アーキテクチャ採用時)

指標クラウドLLM中心Local-First Agentic改善
TTFT(Time To First Token)700〜1,500ms10〜80ms10〜100倍
p99 エンドツーエンド応答3〜6秒300〜800ms約6〜8倍
データ egress 量すべての入力/出力0(オプションで暗号化差分のみ)
月額推論コスト(1M MAU)$135k 前後$500〜$2k約70倍
オフライン可用性×◎(完全動作)-
GDPR/HIPAA 越境要規約整備 + DPAそもそも発生しない-
モデル差し替え容易性全ユーザー一括個別デバイス単位、段階ロールアウト-

AGI時代への布石

  1. Personal Model(個人化蒸留モデル):端末内でユーザー固有の LoRA を継続学習。ユーザーの文脈、語彙、業務慣習を「誰にも見られないまま」内面化。クラウドでは絶対に実現できないパーソナライゼーション。
  2. Multi-Agent Negotiation Protocol:ユーザーAの Planner と ユーザーBの Planner が、ユーザーの代理として意図を秘匿したまま合意形成する。これは現実的には Oblivious RAM やFully Homomorphic Encryptionではなく、ZK-SNARK による選択的開示 + CRDTベースの合意ログで段階的に実装可能になります。
  3. Persistent Context Economy:LLMがユーザー毎に「コンテキストウィンドウを使い切らない」永続記憶を持つと、ユーザー側にのみ蓄積された知識が差別化要因になる。クラウドRAGでは絶対に複製不可能なプライベートな情報資本が形成される。
  4. AIエージェントの法的主体化:EU AI Actの次期拡張で、「自律エージェントの責任主体」は配布者、運用者、デバイスオーナーに分岐して定義される流れ。推論がユーザーのデバイス上で完結する構造は、この責任問題に対する構造的回避手段でもあります。

結び:正しい問いを選ぶという経営判断

2026年のCTOが問うべき問いは、「どのクラウドLLMを選ぶか」ではありません。「推論はどこで行われるべきか」という、アーキテクチャ原論そのものです。

クラウドLLMは、汎用知能という凄まじい汎用資産を提供する一方で、レイテンシ・プライバシー・経済性という三つの構造的負債を背負わせます。一方、Local-First Agentic は、「ユーザーの隣にAIがいる」という物理的な近さを、型システム・分散アルゴリズム・暗号・GPUコンピュートの精緻な積み上げによって実装可能にします。

このアーキテクチャを今から自社で一つ実装しておくことは、単なる技術的優位ではなく、競合がクラウド単価の上昇に苦しむ3〜5年後、粗利構造そのもので差をつける経営判断になります。光速は交渉できず、法規制は緩まず、クラウドの単価は下がり切りません。しかし、ユーザーの手元にあるシリコンは、毎年勝手に強くなります。

この風を、先に捉える側に立ちませんか。

同様の課題、抱えていませんか?

あなたのビジネス課題も、最新の技術で解決できます。 まずは30分の無料技術相談から、状況をお聞かせください。

無料技術相談を予約する

プロジェクト単位(請負)・技術顧問、どちらにも対応可能です

技術ブログ一覧に戻る