GEMINI LABEN
ROBOTICS — Gemini Robotics ER 2 が公開プレビューになりました。空間推論・エージェント的なコード実行・複数ロボットの協調に対応しますSTREAMING — gemini-robotics-er-2-streaming-preview は Live API による実時間ストリーミング向けで、音声と映像の双方向入力を扱えますSUNSET — 停止日が近づいています。8月17日に Imagen 系(移行先は gemini-3.1-flash-image)、20日に Grok 4.1 ファミリー、31日に gemini-robotics-er-1.6-preview ですSAMPLING — temperature・top_p・top_k が非推奨になりました。これらを設定として公開しているアプリケーションは、扱いの見直しが要りますLOGS — Interactions API の開発者ログに対応し、AI Studio のダッシュボードから閲覧できるようになりましたMODELS — Gemini 3.1 Pro は依然プレビューです。3.6 Flash は7月21日に GA、3.5 Flash-Lite は大量処理のサブエージェント用途に向きますROBOTICS — Gemini Robotics ER 2 が公開プレビューになりました。空間推論・エージェント的なコード実行・複数ロボットの協調に対応しますSTREAMING — gemini-robotics-er-2-streaming-preview は Live API による実時間ストリーミング向けで、音声と映像の双方向入力を扱えますSUNSET — 停止日が近づいています。8月17日に Imagen 系(移行先は gemini-3.1-flash-image)、20日に Grok 4.1 ファミリー、31日に gemini-robotics-er-1.6-preview ですSAMPLING — temperature・top_p・top_k が非推奨になりました。これらを設定として公開しているアプリケーションは、扱いの見直しが要りますLOGS — Interactions API の開発者ログに対応し、AI Studio のダッシュボードから閲覧できるようになりましたMODELS — Gemini 3.1 Pro は依然プレビューです。3.6 Flash は7月21日に GA、3.5 Flash-Lite は大量処理のサブエージェント用途に向きます
記事一覧/API / SDK
API / SDK/2026-08-07上級

ストリーミング応答の日本語が静かに欠ける — バイト境界とイベント境界を実測で切り分ける

ストリーミング表示の日本語だけが欠ける現象を、ローカルの疑似SSEサーバで実測しました。壊れていた層は二つあり、片方は例外にすらならず、英語のテストでは検出率がゼロでした。

Gemini API206ストリーミング12SSE2UTF-8日本語処理

プレミアム記事

壁紙アプリの説明文を Gemini に書かせて、生成中の文字を画面へ流し込む処理を書いておりました。個人開発でアプリを並行して回しておりますと、こうした表示まわりの実装は、つい後回しになりがちです。

動作は問題ないように見えました。文章が一文字ずつ増えていき、最後まで表示されます。手元では何度試しても崩れません。

気になったのは、実機で確認したときの末尾でした。文章が、ほんの少しだけ短いのです。

意味は通っています。日本語としても自然です。ただ、同じプロンプトを非ストリーミングで叩いたときの応答と並べると、最後の一文が丸ごとありませんでした。

エラーログは空でした。ステータスは 200 です。例外も出ておりません。

何も壊れていないように見えるのに、届いていない。この気持ち悪さを放置したくなくて、ローカルに疑似サーバを立てて測ることにしました。

結果として、壊れていた層は二つありました。しかもそのうち一つは、私が想定していた壊れ方と逆の形をしていました。

測定環境と、再現できる疑似サーバ

外部サービスに依存すると数字が揺れます。Gemini の streamGenerateContent?alt=sse と同じ形の応答を返すサーバを、ローカルに立てました。

  • Python 3.10.12 / Node.js v22.22.3
  • ペイロード: SSE イベント 8 個、全体 1,406 バイト。うち日本語本文は 85 文字(3 バイト文字なので 255 バイト、全体の 18.1%)
  • 比較用に、同じ構造で本文だけを英語にしたペイロード(1,315 バイト)も用意

肝心なのは、サーバ側が何バイト単位でソケットへ書き込むかを指定できることです。実際の経路では TCP の分割やプロキシのバッファリングで断片化しますが、それは観測も制御もできません。ならば、テスト側で強制するのが確実です。

# server.py — 指定バイト数ずつソケットへ書き込む疑似 SSE サーバ
import socket, sys, time, json
 
JA_PARTS = [
    "画像分類パイプラインの", "移行を進めています。", "停止予定のモデルを",
    "洗い出したところ、", "参照箇所は思ったより", "広範囲に散っていました。",
    "特に設定ファイル側の", "既定値が見落とされがちです。",
]
 
def build(parts, model="gemini-3.6-flash"):
    """Gemini の SSE 応答と同じ形の本文を組み立てます。"""
    out = []
    for i, p in enumerate(parts):
        obj = {
            "candidates": [{
                "content": {"parts": [{"text": p}], "role": "model"},
                "index": 0,
            }],
            "modelVersion": model,
        }
        if i == len(parts) - 1:
            obj["candidates"][0]["finishReason"] = "STOP"
            obj["usageMetadata"] = {
                "promptTokenCount": 48,
                "candidatesTokenCount": 96,
                "totalTokenCount": 144,
            }
        # ensure_ascii=False が要点です。日本語をそのまま UTF-8 で流します
        out.append("data: " + json.dumps(obj, ensure_ascii=False) + "\n\n")
    return "".join(out)
 
def serve(port, chunk):
    body = build(JA_PARTS).encode("utf-8")
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
    s.bind(("127.0.0.1", port))
    s.listen(8)
    conn, _ = s.accept()
    with conn:
        conn.recv(65536)  # リクエスト行は読み捨てます
        conn.sendall((
            "HTTP/1.1 200 OK\r\n"
            "Content-Type: text/event-stream; charset=utf-8\r\n"
            "Cache-Control: no-cache\r\n"
            f"Content-Length: {len(body)}\r\n"
            "Connection: close\r\n\r\n"
        ).encode())
        # ここが本題。chunk バイトずつ、意図的に細切れで送ります
        for i in range(0, len(body), chunk):
            conn.sendall(body[i:i + chunk])
            time.sleep(0.001)
    s.close()
 
if __name__ == "__main__":
    serve(int(sys.argv[1]), int(sys.argv[2]))

time.sleep(0.001) を挟んでいるのは、カーネルが送信バッファ内で結合してしまうのを防ぐためです。これを入れないと、指定したチャンクサイズが受信側に伝わらず、測定になりません。最初はこれを忘れて、どのチャンクサイズでも同じ結果が出て首をひねりました。

バイト境界 — 壊れるのは日本語だけでした

まず、受信したバイト列をチャンクごとに decode("utf-8", errors="replace") する、という書き方で測りました。ありがちな一行です。

サーバの書き込み単位を変えながら、本文中に U+FFFD(置換文字)が何個現れるかを数えます。あわせて、境界がマルチバイト文字の途中に落ちた回数を、ペイロードのバイト列から解析的に数えました。

書き込み単位境界の数文字を割った回数U+FFFD の数日本語本文85文字のうち破損
8 バイト175215424.7%
12 バイト117204023.5%
16 バイト87102611.8%
24 バイト5891810.6%
32 バイト435135.9%
48 バイト29484.7%
64 バイト21252.4%
128 バイト10121.2%
256 バイト5000%

割られた 1 文字あたり U+FFFD が 2 個または 3 個出ています。3 バイト文字を「2 バイト+1 バイト」で割ると 2 個、「1 バイト+2 バイト」で割ると 3 個になるためで、実測値もその通りに分かれました。

同じ測定を英語ペイロードで回した結果が、この記事を書こうと思った直接の理由です。

全 40 条件(5 実装 × 8 チャンクサイズ)で、破損はゼロでした。

ASCII は 1 バイト文字なので、境界がどこに落ちても文字を割れません。日本語は 3 バイトなので、1 文字につき「割ってはいけない位置」が 2 箇所あります。絵文字は 4 バイトで 3 箇所です。

# 文字幅ごとに、割ってはいけない位置がいくつあるかを確かめます
s = "完了しました🎉"
b = s.encode()
for i in range(1, len(b)):
    joined = b[:i].decode("utf-8", "replace") + b[i:].decode("utf-8", "replace")
    n = joined.count("�")
    if n:
        print(f"split@{i:2} -> U+FFFD={n}  {joined!r}")
# split@ 1 -> U+FFFD=3  '���了しました🎉'
# split@19 -> U+FFFD=4  '完了しました����'

4 バイトの絵文字は、一度の分割で最大 4 個の U+FFFD を生みます。日本語より脆いということです。UI に絵文字を混ぜる設計をしているなら、影響は日本語よりさらに大きくなります。

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

この記事の続きを読む

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

この記事で得られること
チャンクサイズ別に測った日本語の破損率(8バイトで本文の24.7%、256バイトで0%)と、誰でも再現できる疑似SSEサーバの全コード
同じ誤りがPythonでは例外、Node.jsでは無言のU+FFFD置換になる理由と、両ランタイムの正しい受け方
バイト境界を直しても残る第二の境界。イベントが丸ごと消えるのに応答200が返る構造と、CIに置く意地悪サーバの作り方
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

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

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

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

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

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

関連記事

API / SDK2026-07-17
途中で切れた Gemini のストリーミングを、引き直すか続きから書かせるか — 二重に払わないための分岐設計
モバイル回線でストリーミングが切れたあと、無条件に全部引き直していませんか。受信済み出力を続きに使う方式との費用差を式で出し、継ぎ目の壊れ方まで含めて分岐条件を決める設計をまとめます。
API / SDK2026-06-23
Gemini API の平均レイテンシは速いのに、たまに固まる — p95/p99 を守る運用メモ
平均TTFTは速いのに、一定割合のユーザーだけ応答が固まる — これはテールレイテンシ(p95/p99)の問題です。計測の取り方からモデルルーティング・ストリーミング予算・キャッシュ会計・リトライ設計まで、本番で効いた防衛策をコード付きで共有します。
API / SDK2026-05-26
iOS の URLSession で Gemini API のストリーミングがバックグラウンド復帰時に途切れる原因と対処
iOS アプリで Gemini API のストリーミング応答を受け取っている最中にアプリを一旦バックグラウンドに送って戻ると、接続が切れて続きが返ってこない――そんな現象に遭遇したときの原因切り分けと、URLSession 周りの最小修正をまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →