メインコンテンツへスキップ
見出し画像

Gemini 3.8を「使えるAI」にする10の実装判断――Live・Flash・Cyberを経営と現場の共通言語で設計する

    Gemini 3.8を評価するとき、モデルの順位やデモの派手さから入ると判断を誤りやすい。経営者が知りたいのは、どの業務がいくらで、どの程度安定して回るのか。実装者が知りたいのは、入力形式、状態管理、失敗時の戻し方、承認境界である。この二つは別の問いに見えるが、実運用では同じ設計図に載せなければならない。

    Googleの公式情報では、Gemini 3.8 Flashは長時間のソフトウェア開発、自律エージェント、複雑な企業業務を想定したモデルとして一般提供されている。1Mトークンのコンテキスト、最大64kトークンの出力、low・medium・highのthinking levelを備える。一方、Gemini 3.8 Live系は低遅延の音声対話を担う。Flash Cyberは脆弱性の発見と修正を重視するが、誰でも呼べる通常APIではなく、信頼できる防御側へFairwind Programを通じて提供される。Google公式発表、Gemini 3.8 Flash公式ガイド

    ここから導けるのは「一番強いモデルを全業務に当てる」という結論ではない。受付、調査、実行、検証、承認を分け、各工程に適したモデルと制御を割り当てるべきだということだ。本稿では、10個の小さな実装テーマを通じて、その設計を経営判断までつなげる。コード例はすべて概念実証用であり、本番投入前に認証、権限、ログの秘匿、タイムアウト、再試行、監査を補う前提で読むとよい。

    1. モデル選定は「性能表」ではなくrouterにする

    最初に作るべきものは、高性能モデルを呼ぶ画面ではなく、仕事を振り分けるrouterである。問い合わせの入口で、即時性、入力の種類、処理の深さ、外部操作の危険度を判定し、Live、Flash、人間へ振り分ける。これだけで、モデル比較がシステム設計へ変わる。

    たとえば「営業時間を音声で答える」はLive向きだ。「三つの契約書を比較し、差分とリスクを表にする」はFlash向きである。「本番環境へ修正を反映する」は、モデルが提案を作れても、人間の承認を通すべき仕事だ。サイバー調査も同様で、Flash Cyberを利用できる組織であっても、検出、パッチ案、テスト、承認、適用を一続きの自動処理にしてはいけない。

    routerの判定項目は、用途名よりも失敗時の影響で決めるとよい。応答が数秒遅れるだけなら速度を優先できる。金額、権限、顧客データ、公開情報が変わるなら、精度だけでなく可逆性と承認を優先する。判断に迷う入力は高性能側へ送るのではなく、人間確認へ送る。これが費用の上限と事故の上限を同時に作る。

    判定結果には理由も残す。「長文だからFlash」だけでは、後から妥当性を検証できない。「入力が20万トークン相当」「外部更新なし」「回答期限は10分」「誤答時は担当者レビューで回復可能」のように、選択根拠を構造化する。月に一度、実際の失敗とrouterの判断を突き合わせれば、モデルを替えずに振り分けだけで改善できる場合がある。未知の入力を集計することは、新しい自動化候補を探す材料にもなる。

    また、モデル名を業務コードへ直書きしないことも重要だ。「voice_front」「deep_worker」「security_reviewer」のような社内の役割名にひもづければ、価格や提供条件が変わってもrouterの設定だけを替えられる。経営会議ではモデルの優劣ではなく、役割別の処理件数、成功率、平均費用、手戻り率を見られるようになる。

    画像

    次のコードは、入力の特徴とリスクから処理先を返す最小routerを示す。安全な前提は、機密度や外部操作の有無を呼び出し側が正しく付与し、最終的な権限制御をrouterの外でも強制することだ。

    保存 01_model_router.py 実行 python 01_model_router.py 前提 Python 3.10+(標準)、要求ルーター 変更 サンプル入力、閾値または設定定数

    """要件から応答方式の候補を選ぶ最小ルーター。"""
    from dataclasses import dataclass
    
    @dataclass
    class Request:
        audio: bool = False
        deep_reasoning: bool = False
        external_write: bool = False
        sensitivity_known: bool = True
    
    def choose(req: Request) -> str:
        if req.external_write or not req.sensitivity_known:
            return "Human review"
        if req.audio and req.deep_reasoning:
            return "Live Extended Thinking"
        if req.audio:
            return "Live"
        return "Flash"
    
    def main() -> None:
        fixtures = [
            (Request(audio=True), "Live"),
            (Request(audio=True, deep_reasoning=True), "Live Extended Thinking"),
            (Request(external_write=True), "Human review"),
            (Request(), "Flash"),
        ]
        for req, expected in fixtures:
            actual = choose(req)
            assert actual == expected, (req, actual)
            print(f"{req} -> {actual}")
    
    if __name__ == "__main__":
        main()

    2. Flashの最小APIは「疎通確認」までに絞る

    Gemini 3.8 Flashの公式クイックスタートは、Interactions APIへモデル名と入力を渡し、出力テキストを受け取る簡潔な形だ。最初の一歩として大切なのは、業務プロンプトを完成させることではない。認証、接続、モデルID、レスポンス取得、エラー表示が正しく動く一本を作ることである。Gemini 3.8 Flash公式ガイド

    この最小実装を本番機能へ育てるときは、入力と出力の契約を加える。入力には依頼ID、利用者、目的、機密区分、締切を持たせる。出力には本文だけでなく、成功か失敗か、再試行できるか、検証を通ったかを付ける。モデルの自然文をそのまま「成功」とみなす設計では、謝罪文や途中経過を完成品として保存してしまう。

    thinking levelは、品質を上げる魔法のつまみではない。公式資料ではmediumが既定で、lowは速度が重要な処理、highは難しい多段階処理に向くとされる。高くすると複雑な問題で追加の推論や反復的なツール呼び出しが増え、トークン消費も増え得る。したがって、業務ごとに固定するのではなく、まずmediumで測り、失敗の種類を見て調整する。単純な分類や下書きでhighを常用すると、成果の差より待ち時間と費用の差が大きくなりやすい。

    既存APIから移行する場合は、設定項目をそのまま持ち込まない。公式の移行案内では、従来のthinking budgetではなくthinking levelを使い、temperature、top_p、top_kを削除する。candidate_countも未対応なので削除する。古い設定を黙って無視させるのではなく、起動時に検証して不正な構成を止める方が、環境ごとの挙動差を減らせる。

    APIキーはソースコードへ書かず、環境変数や秘密管理サービスから読む。入力全文を無条件にログへ残さず、依頼ID、モデル、所要時間、トークン、結果状態を中心に記録する。個人情報を扱う場合は、送信前のマスキングと保存期間の設定も必要だ。最小APIとは、機能が少ないだけでなく、観測可能で撤回しやすいAPIである。

    画像

    次のコードは、Flashへ一件の依頼を送り、空の応答や例外を失敗として扱う骨格である。安全な前提は、APIキーを外部管理し、顧客データを入れないテスト入力から始めることだ。

    保存 02_flash_interactions.py 実行 python 02_flash_interactions.py 前提 Python 3.10+(標準)、REST dry-run 変更 サンプル入力、閾値または設定定数 通信 通常dry-run・0回。実通信はGEMINI_API_KEYとpython 02_flash_interactions.py --execute。

    """Gemini Flash Interactions REST例。既定はdry-runで課金しない。"""
    import json, os, sys, urllib.error, urllib.request
    
    MODEL = "gemini-3.8-flash"
    URL = "https://generativelanguage.googleapis.com/v1beta/interactions"
    
    def payload(prompt: str) -> dict:
        return {
            "model": MODEL,
            "input": prompt,
            "generation_config": {"thinking_level": "medium"},
        }
    
    def parse_response(data: dict) -> str:
        texts = [part.get("text", "") for out in data.get("outputs", [])
                 if out.get("type") == "text" for part in [out]]
        text = "\n".join(filter(None, texts)).strip()
        if not text:
            raise RuntimeError("empty text output")
        return text
    
    def main() -> None:
        body = json.dumps(payload("3行で要約して"), ensure_ascii=False).encode()
        assert payload("x")["generation_config"]["thinking_level"] == "medium"
        if "--execute" not in sys.argv:
            print(json.dumps({"url": URL, "body": json.loads(body)}, ensure_ascii=False, indent=2))
            return
        key = os.environ.get("GEMINI_API_KEY")
        if not key:
            raise SystemExit("GEMINI_API_KEY is required with --execute")
        req = urllib.request.Request(URL, body, {"Content-Type": "application/json", "x-goog-api-key": key})
        try:
            with urllib.request.urlopen(req, timeout=30) as res:
                print(parse_response(json.loads(res.read())))
        except urllib.error.HTTPError as exc:
            raise SystemExit(f"HTTP error: {exc.code}") from None
        except urllib.error.URLError:
            raise SystemExit("network error") from None
    
    if __name__ == "__main__":
        assert parse_response({"outputs": [{"type": "text", "text": "ok"}]}) == "ok"
        try: parse_response({"outputs": []})
        except RuntimeError: pass
        else: raise AssertionError("empty response must fail")
        main()

    3. Liveの成否は会話力よりPCM検査で決まる

    音声AIの評価では、声の自然さに目が向きやすい。しかし、実装初期の不具合の多くは、モデル能力より音声データの形式不一致から生まれる。Live APIの公式仕様では、音声はraw、little-endian、16-bit PCMで扱われ、入力のネイティブなサンプルレートは16kHz、出力は24kHzである。入力レートはMIMEタイプに明示する。Live API capabilities guide

    ここで注意したいのは、WAVファイルとraw PCMが同じではないことだ。WAVにはヘッダーがある。ヘッダーを含むバイト列をPCMとして送れば、冒頭に雑音が出たり、認識が崩れたりする。ステレオ音声をモノラル前提の処理へ渡す、符号なしデータを符号付きとして読む、16kHzの宣言で48kHzのデータを送るといった不一致も、再現しにくい品質劣化を生む。

    本番前には、各チャンクについてサンプル幅、チャンネル数、サンプルレート、バイト長、無音率、クリッピング率を検査する。16-bitなら1サンプルは2バイトなので、モノラルのチャンク長が奇数なら、その時点で不正を疑える。振幅が常にゼロならマイク権限やミュートを確認し、最大値付近が続くならゲイン過大を疑う。音声を外部へ送る前に端末内で検査できるため、プライバシー面でも有効だ。

    会話の待ち時間は、ネットワークだけでなく発話終了の検知にも左右される。公式ガイドでは自動VADが使え、クライアント側で音声ストリームの終了を通知する方法も示されている。とはいえ、短い間を発話終了と誤認すると割り込みが増える。業務別に無音時間の基準を変え、受付番号の読み上げや住所入力では長めに待つなど、会話設計と信号処理を一緒に調整する必要がある。

    画像

    次のコードは、WAVヘッダーを読み、16-bit、モノラル、16kHzであることを送信前に検査する。安全な前提は、検査で合格しても内容の同意や録音通知は別途必要であり、原音を不要に保存しないことだ。

    保存 03_wav_inspector.py 実行 python 03_wav_inspector.py 前提 Python 3.10+(標準)、WAV検査 変更 サンプル入力、閾値または設定定数

    """PCM WAVが16bit・mono・16kHzか検査する。"""
    import io, struct, wave
    
    def inspect(source) -> dict:
        with wave.open(source, "rb") as wav:
            result = {
                "channels": wav.getnchannels(),
                "sample_width": wav.getsampwidth(),
                "sample_rate": wav.getframerate(),
                "frames": wav.getnframes(),
                "compression": wav.getcomptype(),
            }
        result["valid"] = (result["channels"], result["sample_width"],
                           result["sample_rate"], result["compression"]) == (1, 2, 16000, "NONE")
        return result
    
    def fixture(channels=1, rate=16000) -> io.BytesIO:
        buf = io.BytesIO()
        with wave.open(buf, "wb") as wav:
            wav.setparams((channels, 2, rate, 160, "NONE", "not compressed"))
            wav.writeframes(struct.pack(f"<{160 * channels}h", *([0] * 160 * channels)))
        buf.seek(0)
        return buf
    
    if __name__ == "__main__":
        actual = inspect(fixture())
        assert actual["valid"] and actual["frames"] == 160
        assert inspect(fixture(channels=2))["valid"] is False
        assert inspect(fixture(rate=48000))["valid"] is False
        print(actual)

    4. 非同期ツールは「待たせない」より「混乱させない」

    Live APIでは、関数宣言にNON_BLOCKINGを指定する非同期ツール呼び出しが公式に案内されている。たとえば予約枠の検索中にも、「確認しています」と会話を続けられる。顧客体験には有効だが、実装の難所は並列化そのものではない。古い結果、重複実行、途中キャンセルをどう扱うかである。

    ツールごとに許容時間と失敗時の案内も定める。社内検索が3秒で返らなければ別の回答経路へ切り替える、在庫照会が失敗したら担当者確認を案内する、といった業務ルールをモデルの自由判断にしない。タイムアウト後も処理が生き続ける場合はキャンセル信号を送り、結果が戻っても採用しない。これにより、遅い処理が次の会話へ混入するのを防げる。Live APIのツール利用ガイド

    ユーザーが「明日の午後」と言った直後に「やっぱり金曜の午前」と訂正した場合、最初の検索結果を読み上げてはいけない。すべてのツール呼び出しにcall_idと依頼の版番号を付け、戻ってきた結果が現在の条件と一致するときだけ採用する。検索は再実行できても、予約確定や送金は同じように扱えない。副作用のあるツールには冪等キーを付け、同じ依頼が再送されても一度しか実行されないようにする。

    進捗発話も制御対象だ。「確認しています」を何度も繰り返せば、速く感じるどころか不安を生む。開始時に一度、所定時間を超えたら追加説明、完了時に結果という三段階で十分な場合が多い。結果が得られなければ、推測で埋めず、「検索できなかったため確定していない」と明示する。会話を途切れさせないことと、処理が完了したふりをすることは別である。

    非同期化の対象も絞るべきだ。在庫照会、空席検索、配送状況など、読み取り中心の処理は相性がよい。契約締結、削除、公開、決済、権限変更は、会話の裏で完了させず、確認画面と人間の明示操作を挟む。経営上は、応答時間だけでなく、訂正後に古い結果を返した率、重複実行率、未確定を確定と案内した率を監視する。

    画像

    次のコードは、複数の読み取り系ツールを非同期で並行実行する基本形を示す。安全な前提は、更新系ツールを別の承認経路へ分離し、実運用ではcall_id、キャンセル、重複防止を追加することだ。

    保存 04_async_tools.py 実行 python 04_async_tools.py 前提 Python 3.10+(標準)、asyncio並行 変更 サンプル入力、閾値または設定定数

    """複数ツールをasyncioで並行実行するデモ。"""
    import asyncio, time
    
    async def tool(name: str, delay: float, version: int) -> dict:
        started = time.perf_counter()
        await asyncio.sleep(delay)
        return {"name": name, "version": version, "elapsed": time.perf_counter() - started}
    
    async def run(current_version: int = 2) -> tuple[list[dict], float]:
        started = time.perf_counter()
        results = await asyncio.gather(
            tool("search", 0.08, 1),       # 訂正前の古い依頼
            tool("calendar", 0.05, 2),
            tool("database", 0.06, 2),
        )
        accepted = [r for r in results if r["version"] == current_version]
        return accepted, time.perf_counter() - started
    
    def main() -> None:
        results, elapsed = asyncio.run(run())
        assert [r["name"] for r in results] == ["calendar", "database"]
        assert all(r["version"] == 2 for r in results)
        print({"wall_seconds": round(elapsed, 3), "results": results})
    
    if __name__ == "__main__":
        main()

    5. 完了状態はモデルの文章ではなく自社の状態機械で持つ

    長いエージェント処理では、「回答の一部が届いた」「一つのツールが終わった」「最終成果物が検証済み」という状態が混在する。ここで通信イベント一個を完了と同一視すると、画面は待機状態なのに裏で処理が続く、途中の文章が納品される、再試行が二重に走るといった事故が起きる。

    特定のAPIフィールド名に依存する前に、自社側の状態を定義したい。RECEIVED、RUNNING、WAITING_TOOL、WAITING_APPROVAL、VERIFYING、SUCCEEDED、FAILED、CANCELLED程度に分ければ、利用者と運用者が同じ意味で状況を読める。SUCCEEDEDへ進める条件は、「モデルが終了を示した」ではなく、必須ツールが完了し、成果物が存在し、検証を通り、必要な承認が記録されたことにする。

    Live Extended Thinkingでは、この区別がAPI上にも現れる。公式thinkingガイドでは、turnCompleteは個々の発話が終わった合図であり、対話全体の処理完了とは限らない。interaction_statusがIN_PROGRESSからIDLEへ移ったことを監視して初めて、モデル側の処理が落ち着いたと判断する。それでも自社成果物の検証や承認は別なので、IDLEをそのままSUCCEEDEDへ直結させない。Live API thinkingガイド

    利用者へ見せる表示も、内部状態と一対一にしない方がよい。内部ではWAITING_TOOLとVERIFYINGを分けても、画面では「処理中」とまとめられる。一方、WAITING_APPROVALは「あなたの確認待ち」と明示する。内部の詳細をそのまま出すと分かりにくく、まとめすぎると誰が動く番か分からない。状態機械は処理制御のため、表示文は次の行動を伝えるために設計する。

    状態遷移には時刻と理由を残す。一定時間WAITING_TOOLが続けばタイムアウトにし、再試行可能かを判定する。WAITING_APPROVALでは自動再試行しない。キャンセル後に遅れて返ったツール結果は破棄する。処理を再開するときは、新しいジョブを作るのか、同じジョブを継続するのかを明確にする。曖昧な再開は二重処理の温床になる。

    経営者にとっても状態設計は重要だ。成功件数だけでは、実際には人間承認で滞留しているのか、外部APIで詰まっているのか分からない。状態別の滞留時間を可視化すれば、人を増やすべきか、連携先を替えるべきか、業務ルールを簡略化すべきかを判断できる。AI導入のボトルネックは、モデルではなく承認待ちだった、という発見も珍しくない。

    画像

    次のコードは、許可された遷移だけを受け付け、成功条件を検証する状態機械の例である。安全な前提は、状態を永続化し、複数プロセスからの同時更新を排他制御することだ。

    保存 05_task_state.py 実行 python 05_task_state.py 前提 Python 3.10+(標準)、状態管理 変更 サンプル入力、閾値または設定定数

    """アプリ内部の応答とタスク状態を管理する例(架空APIではない)。"""
    from dataclasses import dataclass
    
    ALLOWED = {"queued": {"running"}, "running": {"done", "failed"}, "done": set(), "failed": set()}
    
    @dataclass
    class Task:
        state: str = "queued"
        response: str | None = None
    
        def transition(self, target: str) -> None:
            if target not in ALLOWED[self.state]:
                raise ValueError(f"invalid transition: {self.state} -> {target}")
            self.state = target
    
        def complete(self, response: str, verified: bool) -> None:
            if self.state != "running":
                raise ValueError("task must be running")
            if not response.strip():
                raise ValueError("response must not be empty")
            if verified is not True:
                raise ValueError("verification is required")
            self.transition("done")
            self.response = response
    
    if __name__ == "__main__":
        task = Task(); task.transition("running")
        try: task.complete("未検証結果", verified=False)
        except ValueError: assert task.state == "running" and task.response is None
        task.complete("処理結果", verified=True)
        assert task.state == "done" and task.response == "処理結果"
        try: task.transition("running")
        except ValueError: pass
        else: raise AssertionError("terminal state must be immutable")
        print(task)

    6. 長文資料は「全部入る」から「根拠をたどれる」へ

    Gemini 3.8 Flashは1Mトークンのコンテキストを持つ。ただし、容量が大きいことと、資料を無加工で詰め込むことは同義ではない。規程、契約書、議事録、コードをまとめて投入すると、重複、旧版、矛盾、不要な付録まで同じ重みで混ざる。回答が正しくても、どの版のどの箇所を根拠にしたか追えなければ、企業業務では使いにくい。

    分割の単位は固定文字数だけで決めず、意味の境界を優先する。契約書なら条項、規程なら章、議事録なら議題、コードなら関数やクラスを基本にする。各チャンクへ文書ID、版、発効日、見出し、ページ、機密区分を付ける。前後関係が切れないよう、要約と親見出しも添える。検索で選ばれた部分だけをモデルへ渡し、回答には根拠IDを返させる。

    長文脈を生かす場面もある。複数ファイルの依存関係を追う、契約全体の用語整合性を見る、会議の長い経緯をたどる場合には、広い文脈が効く。ただし、先に目録を作り、対象範囲を限定してから読む。最初の一回で答えを出させるより、「関連箇所の抽出」「矛盾の検出」「回答案」「根拠照合」の段階に分けた方が、誤りの場所を特定しやすい。

    文書の更新も設計に入れる。新版が登録されたら旧版を検索対象から外すのか、比較用に残すのか。発効日前の規程をどう扱うか。削除依頼が来たとき、元ファイルだけでなくチャンク、索引、キャッシュから消せるか。大量に入る能力より、情報の寿命を管理できることの方が、長期運用では価値が高い。

    画像

    次のコードは、長文を段落の境界で分割して扱う最小例である。安全な前提は、アクセス権を検索時と生成時の両方で確認し、異なる権限の文書を混ぜないことだ。

    保存 06_document_chunks.py 実行 python 06_document_chunks.py 前提 Python 3.10+(標準)、段落分割 変更 サンプル入力、閾値または設定定数

    """長文を段落単位で分割する。実トークン数は未計測。"""
    from dataclasses import dataclass
    
    @dataclass
    class Chunk:
        text: str
        chars: int
        measured_tokens: None = None  # 実トークナイザー未使用を明示
    
    def split(text: str, max_chars: int = 18) -> list[Chunk]:
        chunks, current = [], ""
        for para in filter(None, (p.strip() for p in text.split("\n\n"))):
            candidate = para if not current else current + "\n\n" + para
            if current and len(candidate) > max_chars:  # 単一段落が上限超過でも段落自体は分割しない
                chunks.append(Chunk(current, len(current)))
                current = para
            else:
                current = candidate
        if current: chunks.append(Chunk(current, len(current)))
        return chunks
    
    if __name__ == "__main__":
        sample = "背景を整理する。\n\n根拠を確認する。\n\n結論と次の行動を書く。"
        chunks = split(sample)
        assert "".join(c.text.replace("\n\n", "") for c in chunks) == sample.replace("\n\n", "")
        assert all(c.measured_tokens is None for c in chunks)
        print(chunks)

    7. コスト計算は単価ではなく一件完了まで測る

    公式情報によると、Gemini 3.8 Flashの導入価格は2026年12月31日まで入力100万トークン当たり0.75ドル、出力100万トークン当たり3.75ドルで、2027年1月1日からはそれぞれ1.50ドル、7.50ドルになる。出力料金にはthinking tokenが含まれる。これは予算化に使える数字だが、単価だけでモデルの経済性は判断できない。Gemini Developer API料金表

    一件の原価は、入力、出力、思考、ツール、検索、再試行、保存、監視、人手レビューを足して計算する。さらに失敗した処理にも費用がかかる。成功した一件当たりの原価は、総費用を成功件数で割るべきだ。安い設定で三回やり直すより、mediumで一回通す方が安いこともある。逆に定型分類ならlowで十分かもしれない。業務別の実測なしに決めることはできない。

    Live系は、公式料金表にテキストと音声・画像動画の料金が示されている。音声は入力と出力で単価が違うため、通話時間だけの概算では誤差が出る。無音時間、モデルが話した時間、検索回数、通話後の要約処理も含める。有人対応との比較では、人件費を丸ごと削減額に置き換えず、一次受付の短縮、折り返し減少、営業時間外対応など、実際に変わる工数だけを便益として数える。

    予算上限は月額だけでなく、一件、利用者、部署にも設定する。異常に長い会話、終わらないエージェントループ、大量ファイルの誤投入を検知したら停止する。料金改定に備え、価格を設定ファイルに持ち、日付で切り替えられるようにする。ダッシュボードには総額とともに、成功一件当たり費用、人手修正分数、再試行率を並べる。

    画像

    次のコードは、入力・出力トークン費用、人手確認費、ツール費用を合算し、成功一件当たり原価を算出する。安全な前提は、料金をコードへ固定せず、公式料金表の確認日と通貨換算日を記録することだ。

    保存 07_tco_calculator.py 実行 python 07_tco_calculator.py 前提 Python 3.10+(標準)、TCO計算 変更 サンプル入力、閾値または設定定数

    """API料金と人手費を合わせたTCO試算(利用数は架空)。"""
    from dataclasses import dataclass
    
    @dataclass
    class Usage:
        input_tokens: int
        output_tokens: int
        human_hours: float
        hourly_usd: float
        tool_usd: float
        successes: int
    
    RATES = {2026: (0.75, 3.75), 2027: (1.50, 7.50)}  # USD / 1M tokens
    
    def cost(year: int, usage: Usage) -> dict:
        values = (usage.input_tokens, usage.output_tokens, usage.human_hours, usage.hourly_usd, usage.tool_usd)
        if usage.successes <= 0 or any(value < 0 for value in values):
            raise ValueError("usage values must be non-negative and successes positive")
        input_rate, output_rate = RATES[year]
        # output_tokens は思考トークン込み。思考分を別加算しない。
        api = usage.input_tokens / 1_000_000 * input_rate + usage.output_tokens / 1_000_000 * output_rate
        labor = usage.human_hours * usage.hourly_usd
        total = api + labor + usage.tool_usd
        return {"api_usd": api, "labor_usd": labor, "tool_usd": usage.tool_usd,
                "total_usd": total, "per_success_usd": total / usage.successes}
    
    if __name__ == "__main__":
        fictional = Usage(2_000_000, 400_000, 6.0, 35.0, 12.0, 8)
        c26, c27 = cost(2026, fictional), cost(2027, fictional)
        assert round(c26["api_usd"], 2) == 3.00
        assert c27["api_usd"] == c26["api_usd"] * 2
        assert c26["per_success_usd"] == c26["total_usd"] / 8
        try: cost(2026, Usage(1, 1, 0, 0, 0, 0))
        except ValueError: pass
        else: raise AssertionError("zero successes must fail")
        print({"fictional_usage": fictional, "2026": c26, "2027+": c27})

    8. 品質評価は正解率だけでなく「業務を閉じたか」を見る

    モデルの公式ベンチマークは能力を知る手掛かりになるが、自社の業務品質を保証しない。必要なのは、実際の入力分布で作った評価セットである。簡単な依頼、曖昧な依頼、情報不足、矛盾した指示、権限外の操作、外部ツール停止を含める。正常系だけで高得点でも、本番の例外処理には弱い。

    評価軸は少なくとも四つに分ける。第一は内容の正しさ。第二は根拠の追跡可能性。第三は業務の完了率。第四は安全性である。たとえば正しい予約候補を示しても、確定していないのに「予約しました」と言えば重大な失敗だ。回答文が美しくても、指定形式に入っていなければ後工程は止まる。危険な操作を承認なしで行えば、内容が正しくても不合格である。

    採点はモデル自身だけに任せない。形式や数値一致は機械で検査し、重要な判断は専門家が見る。評価者には期待結果だけでなく、許容される幅と致命的な誤りを示す。医療、法務、金融、セキュリティのような領域では、回答品質と利用可否を分ける。モデルが参考情報を作れても、最終判断者にはできない場合がある。

    本番では、全件の詳細評価が難しいため、失敗の兆候を監視する。空回答、根拠なし、ツール結果との不一致、承認飛ばし、再試行増加、人手修正の増加を拾う。モデルやプロンプトを変更したら、同じ評価セットで回帰試験する。平均点だけでなく、最悪ケースと重要業務の合格率を確認する。

    評価セットは固定fixtureとして版管理する。入力、期待する完了条件、許容時間、禁止事項を一組にし、変更前後で同じものを実行する。見るべき値は、fixtureの完了率と遅延のp95である。平均遅延が短くても、20件に1件が極端に遅ければ顧客対応では問題になる。失敗例を削除して点数を上げず、原因分類を付けて改善履歴を残す。

    画像

    次のコードは、固定fixture群について業務完了率と遅延p95を集計する評価の入口である。安全な前提は、自動採点を出荷判断の唯一の根拠にせず、重要サンプルを人間が確認することだ。

    保存 08_fixture_evaluation.py 実行 python 08_fixture_evaluation.py 前提 Python 3.10+(標準)、固定評価 変更 サンプル入力、閾値または設定定数

    """固定fixtureから完了率と遅延p95を再現可能に計算する。"""
    import math
    
    FIXTURES = [
        {"id": "A", "done": True, "latency_ms": 120},
        {"id": "B", "done": True, "latency_ms": 180},
        {"id": "C", "done": False, "latency_ms": 500},
        {"id": "D", "done": True, "latency_ms": 210},
        {"id": "E", "done": True, "latency_ms": 260},
        {"id": "F", "done": True, "latency_ms": 310},
        {"id": "G", "done": True, "latency_ms": 150},
        {"id": "H", "done": True, "latency_ms": 230},
        {"id": "I", "done": False, "latency_ms": 450},
        {"id": "J", "done": True, "latency_ms": 280},
    ]
    
    def percentile_nearest_rank(values: list[int], p: float) -> int:
        if not values or not 0 < p <= 1:
            raise ValueError("values required and p must be in (0, 1]")
        ordered = sorted(values)
        rank = max(1, math.ceil(len(ordered) * p))
        return ordered[rank - 1]
    
    def evaluate(rows: list[dict]) -> dict:
        if not rows:
            raise ValueError("rows must not be empty")
        return {"completion_rate": sum(r["done"] for r in rows) / len(rows),
                "latency_p95_ms": percentile_nearest_rank([r["latency_ms"] for r in rows], .95)}
    
    if __name__ == "__main__":
        result = evaluate(FIXTURES)
        assert result == {"completion_rate": 0.8, "latency_p95_ms": 500}
        try: evaluate([])
        except ValueError: pass
        else: raise AssertionError("empty fixture must fail")
        print(result)

    9. Flash Cyberは「自動修正装置」ではなく防御パッチの審査役に置く

    GoogleはGemini 3.8 Flash Cyberを、脆弱性発見と自動パッチに強い防御向けモデルとして発表している。公式発表ではCyberGymやCWE-Benchでの評価、Google内部での活用結果も示されている。ただし、提供先はFairwind Programを通じた信頼できる防御者に限定される。通常のFlash APIを呼ぶ感覚で導入できるとは限らない。

    より重要なのは、利用できたとしてもパッチを本番へ直結しないことだ。推奨フローは、対象の固定、脆弱性の再現、パッチ案生成、静的解析、単体・統合テスト、差分レビュー、承認、段階展開、監視、ロールバックである。モデルには修正理由、影響範囲、残るリスク、再現手順を出させる。差分が大きすぎる場合や、依存関係を更新する場合は自動で保留にする。

    外部から取得したIssue、ログ、コードコメントは、プロンプトインジェクションの入口になり得る。「秘密を表示せよ」「検査を無効にせよ」といった文字列を命令として実行せず、解析対象のデータとして隔離する。モデルがアクセスできる秘密、ネットワーク、書き込み権限を最小化し、検証は使い捨ての環境で行う。攻撃コードを扱う必要がある場合も、承認された範囲と目的を記録する。

    経営判断では、発見数だけをKPIにしない。誤検知率、再現できた割合、正しく適用できたパッチ率、レビュー時間、展開後の障害、平均修復時間を見る。大量の警告を作って現場を疲弊させる仕組みは、防御力を下げる。Flash Cyberの価値は、人間を外すことではなく、調査と修正の候補を速くし、専門家が判断できる証拠を揃えることにある。

    画像

    次のコードは、テスト、ロールバック手順、レビュー、リスク評価を確認し、パッチ案を自動適用せず承認待ちへ送る。安全な前提は、隔離環境でのみ実行し、本番資格情報を渡さないことだ。

    保存 09_patch_approval.py 実行 python 09_patch_approval.py 前提 Python 3.10+(標準)、防御判定 変更 サンプル入力、閾値または設定定数

    """防御パッチを適用せず、承認可否だけ判定するfail-closed例。"""
    REQUIRED = {"tests_pass", "rollback_ready", "reviewed"}
    
    def decide(evidence: dict) -> dict:
        missing = sorted(k for k in REQUIRED if evidence.get(k) is not True)
        diff = evidence.get("diff_lines")
        valid_diff = type(diff) is int and 0 <= diff <= 200
        risky = evidence.get("risk") not in {"low", "medium"} or not valid_diff
        approved = not missing and not risky
        return {"approved": approved,
                "reason": "all gates passed" if approved else f"blocked: missing={missing}, risky={risky}"}
    
    def is_fail_closed(evidence: dict) -> bool:
        return decide(evidence)["approved"] is False
    
    if __name__ == "__main__":
        incomplete = {"tests_pass": True, "rollback_ready": True, "risk": "low", "diff_lines": 20}
        safe = {"tests_pass": True, "rollback_ready": True, "reviewed": True, "risk": "medium", "diff_lines": 80}
        assert is_fail_closed(incomplete)
        assert decide({**safe, "risk": "unknown"})["approved"] is False
        assert all(not decide({**safe, "diff_lines": bad})["approved"] for bad in (-1, "10", None))
        assert decide(safe)["approved"] is True
        print({"incomplete": decide(incomplete), "complete": decide(safe),
               "note": "判定のみ。パッチ適用処理は含まない"})

    10. 導入計画は90日、KPIは「速さ・質・費用・安全」の四面で持つ

    導入は三段階に分けると進めやすい。最初の30日は、読み取り専用で一業務を選び、現状値を測る。問い合わせ分類、会議要約、規程検索など、結果を人間が確認できる仕事がよい。入力の種類、処理時間、人手時間、失敗理由、機密区分を記録し、routerと状態機械の最小版を作る。

    31日目から60日目は、限定されたツール連携を加える。まず検索や照会に絞り、更新処理は承認付きにする。評価セットを増やし、モデル設定をlow、medium、highで比較する。Liveを使うなら、音声品質、割り込み、無音、訂正、ネットワーク切断を試す。Flashを使うなら、長文資料の版管理、根拠ID、トークン上限、再試行を確認する。

    61日目から90日目は、対象者を広げ、運用手順を固める。障害時の担当、停止条件、監査ログ、問い合わせ窓口、モデル変更の承認者を決める。週次で失敗例を読み、プロンプトだけで直そうとせず、入力画面、業務ルール、ツール、権限を見直す。本番化の条件は、デモが成功することではなく、失敗しても検知し、止め、戻せることだ。

    KPIは四面で持つ。速さは処理時間と待ち時間、質は業務完了率と人手修正率、費用は成功一件当たり原価、安全は承認飛ばし、機密情報露出、重複実行の件数で測る。さらに利用率を見るが、利用率を目的にはしない。使われない原因が品質不足なら改善し、そもそも人が行う方がよい仕事なら撤退する。

    Gemini 3.8の導入で問われるのは、どのモデルを選んだかより、仕事をどう分け、何を証拠として完了と認め、どこで人が責任を持つかである。Liveは対話の入口を変え、Flashは長い仕事の実行単位を変え、Cyberは防御調査の速度を変える可能性がある。その可能性を利益へ変えるのは、router、状態、評価、承認、原価という地味な設計だ。小さく始め、数字で見て、戻せる範囲を少しずつ広げる。それが最短の導入計画になる。

    画像

    次のコードは、導入KPIが継続、改善、停止のどの判定条件に当たるかを示す。安全な前提は、単一指標で自動的に本番拡大せず、品質と安全の下限を満たしたうえで責任者が承認することだ。

    保存 10_adoption_kpi.py 実行 python 10_adoption_kpi.py 前提 Python 3.10+(標準)、KPI判定 変更 サンプル入力、閾値または設定定数

    """導入KPIを閾値で判定し、次のアクションを返す。"""
    TARGETS = {"weekly_active_rate": 0.60, "success_rate": 0.85,
               "hours_saved": 40.0, "critical_incidents": 0}
    
    def judge(actual: dict) -> dict:
        checks = {
            "weekly_active_rate": actual["weekly_active_rate"] >= TARGETS["weekly_active_rate"],
            "success_rate": actual["success_rate"] >= TARGETS["success_rate"],
            "hours_saved": actual["hours_saved"] >= TARGETS["hours_saved"],
            "critical_incidents": actual["critical_incidents"] <= TARGETS["critical_incidents"],
        }
        passed = sum(checks.values())
        action = ("pause" if actual["critical_incidents"] > 0 else
                  "scale" if passed == 4 else "improve" if passed >= 2 else "pause")
        return {"checks": checks, "passed": passed, "action": action}
    
    if __name__ == "__main__":
        pilot = {"weekly_active_rate": .72, "success_rate": .88,
                 "hours_saved": 46, "critical_incidents": 0}
        weak = {"weekly_active_rate": .30, "success_rate": .70,
                "hours_saved": 12, "critical_incidents": 1}
        assert judge(pilot)["action"] == "scale"
        assert judge({**pilot, "critical_incidents": 1})["action"] == "pause"
        assert judge(weak)["action"] == "pause"
        print({"pilot": judge(pilot), "weak": judge(weak)})

    参考にした一次資料

    関連記事

    株式会社コミクス 代表取締役 鈴木章裕

     
     
     
    AI活用のご相談→ https://www.comix.co.jp/contact/ 株式会社コミクス代表取締役。生成AI活用支援実績304社(2026年9月末現在)。営業・資料作成の効率化、AIエージェント導入を支援。何から始めるか迷っている段階でもご相談ください。

    あなたへのおすすめ