GEMINI LABEN
MEMORY — Memory Bank の Memory Profiles が GA になりました。静的スキーマの構造化プロファイルにより、セッション中の検索を挟まずに情報へ到達できますSCOPE — プロファイルは取り込み時のスコープごとに分離され、スキーマとスコープの組み合わせに対して単一のプロファイルが維持されますINGEST — IngestEvents API が GA になり、イベントのストリーミング・メモリのリビジョン管理・メタデータの付与が扱えるようになりましたAUDIO — gemini-3.1-flash-tts-preview が streamGenerateContent 経由のストリーミングに対応し、読み上げ開始までの待ち時間が縮みましたCLASSROOM — 8月10日から、既にアクセス権を付与されている K-12 と高等教育の全年齢の学生が Gemini in Classroom を利用できるようになりますSUNSET — 停止日が迫っています。8月17日に画像生成モデル、20日に Grok 4.1 ファミリー、31日に gemini-robotics-er-1.6-preview ですMEMORY — Memory Bank の Memory Profiles が GA になりました。静的スキーマの構造化プロファイルにより、セッション中の検索を挟まずに情報へ到達できますSCOPE — プロファイルは取り込み時のスコープごとに分離され、スキーマとスコープの組み合わせに対して単一のプロファイルが維持されますINGEST — IngestEvents API が GA になり、イベントのストリーミング・メモリのリビジョン管理・メタデータの付与が扱えるようになりましたAUDIO — gemini-3.1-flash-tts-preview が streamGenerateContent 経由のストリーミングに対応し、読み上げ開始までの待ち時間が縮みましたCLASSROOM — 8月10日から、既にアクセス権を付与されている K-12 と高等教育の全年齢の学生が Gemini in Classroom を利用できるようになりますSUNSET — 停止日が迫っています。8月17日に画像生成モデル、20日に Grok 4.1 ファミリー、31日に gemini-robotics-er-1.6-preview です
記事一覧/高度な活用
高度な活用/2026-08-09上級

記憶が消えたのではなく、別のプロファイルに書かれていました — Memory profiles のスコープキーを実測で正す

Memory Bank のプロファイルはスキーマとスコープの組で一意になります。呼び出し側でスコープの形がわずかにぶれると、エラーを出さないまま別のプロファイルが増えていきます。分裂の量と、3種類の対処の効き目を同じ条件で測った記録です。

Gemini API207Memory Bank2Memory profiles2スコープ設計マルチテナント3個人開発95

プレミアム記事

朝の集計を眺めていて、手が止まりました。

壁紙アプリと、その姉妹アプリにあたる癒し系アプリ。両方の問い合わせ窓口に Memory Bank のプロファイルを噛ませて、利用者の設定や購読状態を後段へ渡す仕組みを試していたときのことです。取り込みの呼び出し回数が、その日にやり取りのあった利用者数のおよそ4.8倍ありました。

エラーは1件も出ていません。レスポンスも全部 200 です。ただ、1人につき1つで足りるはずのプロファイルが、5つ近く作られている計算になります。

しばらく眺めて、ようやく合点がいきました。記憶が消えていたのではなく、毎回わずかに違う住所に書かれていたのです。

Memory Bank のプロファイルは、スキーマとスコープの組に対して単一のものが保持されます。つまりスコープは「誰のものか」を指す識別子ではなく、保存先そのものを決める複合キーです。キーがぶれれば、そこにあるのは別のプロファイルであって、空のプロファイルは正常な応答として返ります。呼び出し側が {"user_id": 123} と書いた日と {"user_id": "123"} と書いた日で、行き先が変わる。この構造に気づくまで、私はずっと「記憶が定着しない」という誤った問題を解こうとしていました。

勘で直すのが嫌だったので、手元に小さなハーネスを書いて測りました。以下は API キーを使わず、(schema, scope) に対して単一プロファイルという意味論だけをローカルに再現したものです。数値はすべてこのハーネスを実際に走らせて得たもので、推定値は含みません。そして結果は、私が用意した「正しそうな対処」の順位を、ほぼ逆さまにしました。

スコープは識別子ではなく、保存先を決める複合キーでした

まず、模型を用意します。Memory Bank 側の実装を推測するのではなく、公表されている意味論——スキーマとスコープの組ごとに単一のプロファイルが source-of-truth として保たれる——だけをそのまま写します。

import json
 
SCHEMA = "user_pref_v1"
 
class ProfileStore:
    """Memory profiles を模した最小の store。
    (schema, scope) の組に対して単一のプロファイルを保持する。
    実 API を叩かず、キーの決まり方だけを忠実に再現する。"""
 
    def __init__(self, normalizer=None):
        self.data = {}
        # 正規化を差し替えられるようにしておく。ここが本稿の実験点
        self.norm = normalizer or (lambda s: json.dumps(s))
        self.writes = 0
 
    def key(self, scope):
        return (SCHEMA, self.norm(scope))
 
    def get(self, scope):
        # 見つからない場合は None。エラーではなく「まだ何もない」として返る
        return self.data.get(self.key(scope))
 
    def put(self, scope, profile):
        self.writes += 1
        self.data[self.key(scope)] = profile

ここで大事なのは get() が例外を投げないことです。存在しないスコープを引いても、返るのは「空」であって「間違い」ではありません。取り違えが起きたことを知る手段が、既定では用意されていない。この非対称さが、後の章でアラームを設計する動機になります。

私の構成では、利用者IDはアプリごとに採番されています。壁紙アプリの利用者42番と、癒し系アプリの利用者42番は別人です。ですからスコープには app_id が必要で、これは冗長な飾りではなく正しさの一部でした。この点は後で効いてきます。

呼び出し側の書き方を6通り並べて、分裂の量を測る

実際のコードベースを見返すと、スコープを組み立てている箇所は6通りに散っていました。どれも書いた時点では自然です。

種別実際の書かれ方書いたときの意図
ok{"app_id": app, "user_id": str(uid)}基準の形
int{"app_id": app, "user_id": uid}DBから来た整数をそのまま渡した
extra_tier{"tier": tier, "app_id": app, "user_id": str(uid)}プラン別に分けたかった
extra_device{"app_id": app, "user_id": str(uid), "device_id": device}より厳密に分離したかった
extra_locale{"app_id": app, "user_id": str(uid), "locale": locale}言語別の応答を想定した
pad{"app_id": app, "user_id": " " + str(uid)}連結の副産物。誰も意図していない

この分布で4,000ターン、200利用者、2アプリのトラフィックを生成し、素の json.dumps をキーに使った場合の当たり方を測ります。

import random, statistics
from collections import defaultdict
 
APPS = ["wallpaper", "relax"]
 
def gen(users=200, turns=4000, seed=20260809):
    """呼び出し側のばらつきを含むトラフィックを生成する。
    重みは実コードベースの出現比率に合わせた。"""
    rnd = random.Random(seed)
    out = []
    for _ in range(turns):
        app = rnd.choice(APPS)
        u = rnd.randrange(users)
        ctx = {
            "tier": rnd.choice(["free", "pro"]),
            "device": f"d{rnd.randrange(3)}",
            "locale": rnd.choice(["ja-JP", "en-US"]),
        }
        kind = rnd.choices(
            ["ok", "int", "extra_tier", "extra_device", "extra_locale", "pad"],
            [0.46, 0.14, 0.12, 0.12, 0.10, 0.06],
        )[0]
        base = {"app_id": app, "user_id": str(u)}
        s = {
            "ok": base,
            "int": {"app_id": app, "user_id": u},
            "extra_tier": {"tier": ctx["tier"], "app_id": app, "user_id": str(u)},
            "extra_device": {"app_id": app, "user_id": str(u), "device_id": ctx["device"]},
            "extra_locale": {"app_id": app, "user_id": str(u), "locale": ctx["locale"]},
            "pad": {"app_id": app, "user_id": " " + str(u)},
        }[kind]
        out.append((app, u, kind, s, ctx["tier"]))
    return out
 
 
def run(calls, norm):
    store = {}
    hits = miss = 0
    keys_per_tenant = defaultdict(set)
    by_kind = defaultdict(lambda: [0, 0])   # kind -> [hit, miss]
 
    for app, u, kind, s, tier in calls:
        k = (SCHEMA, norm(s))
        keys_per_tenant[(app, u)].add(k[1])
        p = store.get(k)
        if p is None:
            miss += 1
            by_kind[kind][1] += 1
            store[k] = {"owner": (app, u), "billing_plan": tier}
        else:
            hits += 1
            by_kind[kind][0] += 1
 
    n = hits + miss
    return {
        "hit": 100 * hits / n,
        "profiles": len(store),
        "frag": statistics.mean(len(v) for v in keys_per_tenant.values()),
        "writes": miss,
        "by_kind": dict(by_kind),
    }

素の json.dumps で走らせた結果です。

指標
参照が当たった割合52.1%
作られたプロファイル総数1,915個
テナント1件あたりの分裂数4.79個
取り込み呼び出し(=miss)1,915回

朝の集計で見えた「利用者数の4.8倍」が、ほぼそのまま再現しました。ここで胸が少し冷たくなりました。単なる無駄打ちではなく、利用者の履歴が5つに切り分けられていたという意味だからです。

種別ごとに分けると、犯人の順位が出ます。

種別当たった割合hit / miss
ok79.2%1,512 / 397
int44.3%228 / 287
extra_tier25.3%116 / 342
pad24.8%65 / 197
extra_locale22.8%87 / 294
extra_device16.2%77 / 398

最下位が extra_device だったことに、しばらく言葉が出ませんでした。あれは私が「より厳密に分離したい」と思って足したキーです。分離を強めたつもりの一行が、6種類のうちいちばん記憶を壊していました。理屈は単純で、device_id は同じ人でも端末を替えれば変わるからです。スコープに入れた瞬間、それは分離の粒度そのものになります。厳密さは、記憶の共有相手を減らす方向にしか働きません。

ok ですら 79.2% に留まっている点も見ておきたいところです。正しい形で書いた呼び出しも、直前に別の形で書き込まれていれば当たりません。1箇所のぶれが、正しい箇所の成績まで引き下げる。この波及があるので、「うちは9割方ちゃんと書けている」という感触は当てになりませんでした。

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
呼び出し側の書き方が6通りに散っただけで、参照が当たる割合が52.1%まで落ち、プロファイルが利用者1人あたり4.79個へ分裂していた計測結果
よく勧められる sort_keys による正規化が0.0ポイントしか動かず、キーのallowlistだけで90.0%(理論上限と一致)まで戻るまでの、そのまま走る比較ハーネス全コード
参照率がいちばん高く見えた読み出し側フォールバックが、4,000ターンで1,843件の越境読み取りを起こしていた検出と、それを事前に止める天井アラームとlintの実装
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Gemini Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • コピー&ペーストで使える実装コード付き
  • 毎日新しい上級ガイドを追加
  • ¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

API / SDK2026-08-03
Memory profiles の更新ポリシーを測って選ぶ — 確定値を守るガードが精度を16ポイント下げた記録
Memory Bank の Memory profiles が GA になり、構造化された記憶を後段へ渡せるようになりました。更新ポリシーを3種類つくって同条件で比較したところ、直感的に正しく見えた防御が精度を下げていました。計測コード一式と、フィールド別TTLへ切り替えるまでの記録です。
高度な活用2026-07-15
分類ラベルの近ミスはリトライで直らない — 閉じた語彙を正規化する層を挟んだ実測ノート
responseSchema の enum を外したラベルをリトライで直そうとすると同じ近ミスが返ります。壁紙アプリの30分類バッチで外れ方の分布を測り、別名表と埋め込み最近傍で正規化する受け入れ層を挟んだ実装と実測値をまとめました。
高度な活用2026-07-10
カテゴリを30から34に増やした日 — 8,000枚を全部かけ直さない差分再分類の設計
分類カテゴリを追加したとき、既存資産を全件かけ直す必要はありません。埋め込みで影響ラベルを絞り、確信度マージンで境界資産を拾う差分再分類の実装と実測値をまとめました。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →