<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>ロナク・ゾエブ · 記事</title><description>AIシステム、ソフトウェア開発、そして取り組んでいるプロジェクトについてのメモや記事です。</description><link>https://www.rounakzoeb.com</link><language>ja</language><item><title>長時間のタスクを担うAIエージェントのためのプラットフォーム：アーキテクチャとロードマップ</title><link>https://www.rounakzoeb.com/ja/writing/ai-agent-platform-architecture-and-roadmap</link><guid isPermaLink="true">https://www.rounakzoeb.com/ja/writing/ai-agent-platform-architecture-and-roadmap</guid><description>AIエージェントのためのプラットフォームの初期設計：永続的なタスク、ローカルとクラウドでの実行、権限、そして最初に計画しているアプリケーションについて。</description><pubDate>Mon, 19 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;共同開発者とともに、タスクを実行し、小さなアプリケーションを保守できるAIエージェントのためのプラットフォームに取り組んでいます。初期設計では、ローカルとクラウドでの実行、権限、永続的な状態を扱っています。この記事では、これまでに決めた設計上の選択と、最初に作る予定のものを説明します。&lt;/p&gt;
&lt;p&gt;構想しているのは、ローカル、クラウド、またはその両方で動作し、1回きりのチャットではなく、より長時間にわたる仕事を引き受けられるパーソナルAIアシスタントです。たとえば、毎週のリサーチの要約、受信箱の監視、あるいはエージェントが作成して最新の状態に保つ、技術文書を管理するための小さなアプリなどです。仮称はAI App Builderです。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;ステータス（2026年10月）：&lt;/strong&gt; 初期設計の草案はできており、コアは開発の初期段階です。プラットフォームは、私の既存のエージェントフレームワークである&lt;a href=&quot;https://www.rounakzoeb.com/ja/work/doc-processing-toolkit&quot;&gt;doc-processing-toolkit&lt;/a&gt;と、ツールライブラリである&lt;a href=&quot;https://www.rounakzoeb.com/ja/work/doc-processing-std-tools&quot;&gt;doc-processing-std-tools&lt;/a&gt;を基盤にしています。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;最初のワークフロー文書台帳&quot;&gt;最初のワークフロー：文書台帳&lt;/h2&gt;
&lt;p&gt;エンジニアリングのプロジェクトにおける文書台帳 (document register) とは、図面、仕様書、報告書といったプロジェクトの文書を、番号、改訂、ステータスとともにまとめ、随時更新していく一覧です。これが、最初に構築する予定の専門的なアプリケーションです。&lt;/p&gt;
&lt;p&gt;台帳は、エンジニアリング情報管理の一部です。エンジニアリング情報管理とは、プラントなどの資産に関する情報を、設計・建設から運用に至るまで、正確で、漏れがなく、追跡可能な状態に保つことです。大規模なプロジェクトでは数千もの文書が作られ、その多くが何度も改訂されます。台帳が文書の更新に追いつかなくなると、どの文書が欠けているのか、遅れているのか、置き換えられたのかがわからなくなります。これが最も重要になるのは引き渡しの時点で、請負者は、必要な文書がすべて納品されたことを示さなければなりません。&lt;/p&gt;
&lt;p&gt;この作業の多くは、今も手作業で行われています。情報は、文書管理システム、多数のスプレッドシート、共有ドライブ、メールに分散しています。文書番号や改訂は、図面の表題欄、表紙、ファイル名に記載されており、台帳のレイアウトもプロジェクトごとに異なります。文書管理システムのベンダーが追加してきたAI機能は、主に自社製品の中にあるデータを対象としています。そのため、スプレッドシートの台帳を文書と一致させておく作業は、今も人が行う事務作業です。&lt;/p&gt;
&lt;p&gt;ChatGPTやClaudeのような汎用のAIアシスタントは、チャットの中でいくつかの文書を読み、それについての質問に答えることができます。台帳に求められるのは、それ以上のことです。1回の会話に収まる量を超える文書を扱い、新しい改訂が届くたびに、プロジェクトの期間を通じて最新の状態に保ち、すべての値を出典までたどれるようにしなければなりません。また、多くのプロジェクトでは、文書をクライアント自身のシステムの外に出すことができません。&lt;/p&gt;
&lt;p&gt;計画では、エージェントにプロジェクト文書のフォルダを渡し、そこから台帳アプリケーションを構築させます。マスター文書台帳、送付台帳（トランスミッタル台帳：何を、誰に、いつ送ったか）、引き渡し用のビューは、いずれもその1つのアプリケーションの設定違いとして扱う想定です。AIが入力する各項目には、値、信頼度スコア、出典箇所を開く引用を付ける予定です。信頼度の低い行はレビューキューに回し、修正内容は学習データとして保存する計画です。&lt;/p&gt;
&lt;p&gt;そのために必要なものの大半は、モデルの周りのソフトウェアにあります。設計では、各文書を一度だけ解析して、ページと位置の情報を保持した番号付きのブロックに分け、エージェントはそれを1バッチずつ処理していきます。そのため、文書一式の規模が、モデルが一度に取り込めるテキストの量（コンテキストウィンドウ）によって制限されることはありません。各ステップでモデルのコンテキストに何をどのような形で入れるかを選ぶことを、コンテキストエンジニアリングと呼びます。ここでモデルが目にするのは、処理中のブロック、台帳の列、プロジェクトの採番ルールで、それ以外はほとんどありません。モデルが返す各値はブロックを指しているので、モデルに出典を引用させなくても、引用からページ上の正確な位置にたどり着けます。変更のない文書は読み直しません。ステップごとに異なるモデルを使うことができ、タスクを始める前に、プラットフォームがプロジェクトの文書をどのモデルに送ってよいかを確認します。&lt;/p&gt;
&lt;p&gt;この1つのワークフローに、プラットフォームが提供しようとしている機能の大半が必要になります。そのため、設計を試すのに適しています。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;台帳は多数の文書にまたがって構築され、常に最新の状態に保つ必要があります。つまり、中断に耐え、止まったところから再開しなければならない長時間のタスクです。&lt;/li&gt;
&lt;li&gt;できあがるのは、独自のデータ、インターフェース、バージョン履歴を持つアプリケーションで、人がそれを確認し、修正します。&lt;/li&gt;
&lt;li&gt;最新の状態に保つには、新しい文書が届いたことに気づく必要があります。しかも、確認のために数分ごとにモデルを呼び出して費用をかけることなく、です。&lt;/li&gt;
&lt;li&gt;プロジェクト文書を扱うため、権限と承認が重要です。また、モデルの呼び出しにはそれぞれコストがかかり、それを記録する必要があります。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;その後のワークフロー&quot;&gt;その後のワークフロー&lt;/h3&gt;
&lt;p&gt;台帳の次には、バックグラウンドで動き続けるワークフローを追加する予定です。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;フォルダ、Webサイト、データフィード、実行中のプロセスを監視し、意味のある変化があったときに対応する&lt;/li&gt;
&lt;li&gt;受信箱を監視し、届いたものを仕分けて、人が承認するための返信の下書きを作る&lt;/li&gt;
&lt;li&gt;より長時間にわたるメールのタスク（エージェントに文書を転送すると、確信が持てない点があればエージェントがメールで質問し、返信を受けて作業を続ける）&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;汎用のアシスタントも、すでに定期実行のタスクや他のツールとのコネクタを提供しています。AI App Builderでは、これらのワークフローはそれぞれ、独自の履歴、権限、コストの記録を持つ永続的なタスクであり、その結果を、人が確認するアプリケーションに入れることができます。モニターは何かが変化したときにだけモデルを呼び出し、その変化に必要なコンテキストだけをモデルに渡します。&lt;/p&gt;
&lt;h2 id=&quot;現状&quot;&gt;現状&lt;/h2&gt;
&lt;p&gt;ロードマップ上は、段階1（初期設計とアーキテクチャ）が現在の段階で、次が段階2（コアエージェントフレームワークとセキュリティ）です。最初のリリースまで、コードは非公開にしておく予定です。&lt;/p&gt;
&lt;p&gt;エージェントループには、私が自作したフレームワークである&lt;a href=&quot;https://www.rounakzoeb.com/ja/work/doc-processing-toolkit&quot;&gt;doc-processing-toolkit&lt;/a&gt;と、その関連ツールライブラリである&lt;a href=&quot;https://www.rounakzoeb.com/ja/work/doc-processing-std-tools&quot;&gt;doc-processing-std-tools&lt;/a&gt;を使います。AI App Builderは、この2つの上に、より汎用的なプラットフォームを構築するものです。フレームワークは内部インターフェースの背後に置いており、このインターフェースは後から拡張することも、置き換えることもできます（これについては「未解決の課題」で詳しく触れます）。&lt;/p&gt;
&lt;p&gt;私の他のプロジェクトと同様に、実装の大部分はコーディングエージェントが行い、私は方向性を定め、レビューし、最終的な承認をします（その仕組みは『&lt;a href=&quot;https://www.rounakzoeb.com/ja/writing/spec-first-test-first-building-with-ai-coding-agents&quot;&gt;仕様ファースト、テストファースト&lt;/a&gt;』で紹介しています）。リポジトリは、エージェントがそこから直接作業できるように整備しているところです。リポジトリ内の知識を記録の正本とし、アーキテクチャのルールは機械的に強制します。具体的には、短いマップファイル、信頼できる情報源としての&lt;code&gt;docs/&lt;/code&gt;フォルダ、そしてコードの横にチェックインした計画です。将来的には、エージェントがタスクボードから自らタスクを引き受け、プルリクエストを作成し、互いの作業をレビューできるようにしたいと考えています。OpenAIが&lt;a href=&quot;https://openai.com/index/harness-engineering/&quot;&gt;ハーネスエンジニアリングについての記事&lt;/a&gt;で説明している形に沿ったものです。&lt;/p&gt;
&lt;h2 id=&quot;タスクとアプリケーション&quot;&gt;タスクとアプリケーション&lt;/h2&gt;
&lt;p&gt;言語モデルは、車のエンジンのようなものです。動力を生み出しますが、それだけでは誰をどこへも連れて行けません。その動力をどう使うかを決めるのは、車のほかの部分です。ハンドルとブレーキ、燃料系統、何が起きているかを運転者に伝える計器類、そして何か問題が起きたときに重要になる安全装置です。AIエージェントにとって、この車のほかの部分にあたるのがハーネスです。エージェントが使えるツール、保持する記憶、その範囲内で動く権限、出力に対するチェック、そして人がエージェントを操るためのインターフェースです。&lt;/p&gt;
&lt;p&gt;ブレーキの弱い車にもっと強力なエンジンを積んでも、運転が安全になるわけではありません。同じように、より強力なモデルの上に構築したエージェントも、それを方向付けてチェックできるハーネスがなければ、より役に立つことはありません。AI App Builderでの私たちの作業の大半は、ハーネスに関するものです。新しいエンジンは数か月ごとに登場するため、車は作り直すことなく、より強力なエンジンを積めなければなりません。プロバイダー層によってモデルを変更でき、ツール、権限、チェック、コストの上限は、どのエンジンを積んでもそのまま残ります。&lt;/p&gt;
&lt;p&gt;設計の中心には、2つの考え方があります。永続的なタスクと、エージェントが作るアプリケーションです。&lt;/p&gt;
&lt;figure class=&quot;figure&quot; data-astro-cid-khyig4bw&gt;&lt;div class=&quot;mat&quot; data-zoom=&quot;/_astro/02_how_it_will_work-ja.BuCplXoQ.png&quot; data-alt=&quot;ユーザーの入力がタスクになり、AIエージェントがそれをローカル・クラウド・ハイブリッドの環境で実行する。サンドボックスとツールコネクタ (MCP) からなる実行環境の手前に権限とコスト管理があり、実行環境が生成アプリケーションを構築・更新する。プラットフォーム全体の監視がすべてにまたがる。&quot; data-astro-cid-khyig4bw&gt;&lt;picture data-astro-cid-khyig4bw=&quot;true&quot;&gt;&lt;img src=&quot;https://www.rounakzoeb.com/_astro/02_how_it_will_work-ja.BuCplXoQ_Z23BF6x.png&quot; alt=&quot;ユーザーの入力がタスクになり、AIエージェントがそれをローカル・クラウド・ハイブリッドの環境で実行する。サンドボックスとツールコネクタ (MCP) からなる実行環境の手前に権限とコスト管理があり、実行環境が生成アプリケーションを構築・更新する。プラットフォーム全体の監視がすべてにまたがる。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-khyig4bw=&quot;true&quot; width=&quot;3200&quot; height=&quot;1800&quot;&gt;&lt;/picture&gt;&lt;/div&gt;&lt;figcaption class=&quot;figure__caption small muted&quot; data-astro-cid-khyig4bw&gt;構想中のプラットフォームアーキテクチャ。設計段階であり、図の機能は計画中です。&lt;a class=&quot;figure__full&quot; href=&quot;https://www.rounakzoeb.com/_astro/02_how_it_will_work-ja.BuCplXoQ.png&quot; data-full-size data-alt=&quot;ユーザーの入力がタスクになり、AIエージェントがそれをローカル・クラウド・ハイブリッドの環境で実行する。サンドボックスとツールコネクタ (MCP) からなる実行環境の手前に権限とコスト管理があり、実行環境が生成アプリケーションを構築・更新する。プラットフォーム全体の監視がすべてにまたがる。&quot; data-astro-cid-khyig4bw&gt;View full size&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;タスクとは、プラットフォームがユーザーのために実行するあらゆる作業のことです。チャット、毎週のリサーチ要約のような定期実行のエージェント、フォルダを監視するモニター、メールのスレッドは、すべて同じ種類のオブジェクトです。そのため、記録、チェックポイント、再開の仕組みがすべてに同じように適用されます。タスクに関するあらゆる事実は1つのイベントログに記録され、それが唯一の信頼できる情報源になります。&lt;/p&gt;
&lt;p&gt;実行は、永続的 (durable) になるように設計しています。永続的なワークフローのためのライブラリであるDBOSを、ローカルではSQLite上で、クラウドではPostgres上で使う計画です。エージェントの各ステップがチェックポイントになるため、中断されたタスクは最後に完了したステップから再開します。その代わり、途中で中断されたステップは、そのステップの最初からやり直しになります。無人で進むタスクが人の承認を必要とするときは、その依頼をレコードとして保存するため、待っている間もメモリ上で何かを動かし続ける必要はありません。&lt;/p&gt;
&lt;p&gt;アプリケーションとは、エージェントがユーザーのために作り、保持するものです。独自のデータ、インターフェース、バージョン履歴を持ち、編集や共有もできます。エージェントは、権限で許された範囲内で、ユーザーのマシン上またはクラウド上（たとえばAWS上）に、アプリケーションのためのストレージを作成することもできます。リサーチダイジェストのようなシンプルなツールから、文書台帳のような専門的なものまでを想定しています。設計では、すべての機能を次の3種類のいずれかに分類します。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;プラットフォームの小さなコア機能&lt;/li&gt;
&lt;li&gt;薄く取り外し可能なケイパビリティパック（メール、音声、文書台帳など）&lt;/li&gt;
&lt;li&gt;エージェントが作るアプリケーション&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;CRM（顧客関係管理ツール）、学習ヘルパー、台帳トラッカーは、アプリケーションとして作ることを想定しています。エージェントがそのどれかを作れない場合は、専用のプラットフォームコードを追加するのではなく、コアを改善する方針です。特別扱いのコードを足すと、足りない部分が見えなくなってしまうからです。機能の提案をレビューするときにはこれをチェック項目として使い、プロダクト本来の目的から外れないようにしています。できることの幅は、エージェントが作るアプリケーションから生まれるようにし、プラットフォーム自体は小さく保つ考えです。プラットフォームに特別扱いのケースを1つ加えるたびに、それ専用のストレージ、インターフェース、障害の起こり方が加わり、2人で保守しなければならないものが増えます。&lt;/p&gt;
&lt;h3 id=&quot;アプリケーションのインターフェースの作り方&quot;&gt;アプリケーションのインターフェースの作り方&lt;/h3&gt;
&lt;p&gt;生成UI (generative UI) では、モデルがコンテンツだけでなくインターフェースも生成します。大まかに3つの種類があります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;モデルが選ぶ、あらかじめ用意されたコンポーネント。&lt;/strong&gt; カード、フォーム、グラフは開発者が作り、モデルはどれを表示するかを決めて、そこにデータを入れます。安全で一貫性がありますが、誰かが事前に想定したものに限られます。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;モデルが記述するインターフェース。&lt;/strong&gt; モデルが、既知のコンポーネントを並べたJSONのレイアウトのような構造化された記述を書き、アプリケーションがそれを描画します。より柔軟ですが、それでもコンポーネントの範囲に限られます。&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;モデルが書くインターフェース。&lt;/strong&gt; モデルがHTML、CSS、JavaScriptとしてインターフェースそのものを書き、それがサンドボックスの中で動作します。どんなインターフェースでも作れますが、一貫性、アクセシビリティ、安全性を保つのは3つの中で最も難しくなります。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Googleの生成UIの研究やClaudeのアーティファクト (artifacts) など、3つ目の種類の例の多くは、1つの回答のためにインターフェースを作るものです。AI App Builderでは、アプリケーションは保持され、バージョン管理され、共有され、そのエージェントによって何か月にもわたって保守されます。そのため、インターフェースは長く使えるものでなければなりません。&lt;/p&gt;
&lt;p&gt;AI App Builderは、3つ目の種類、つまり完全な生成UIを前提に設計しています。アプリケーションのインターフェースは、そのアプリケーションを作るエージェントが書くもので、決まった部品の組み合わせから作るのではなく、タスクに必要などんな形にもなれます。それでも、表、グラフ、フォーム、文書ビューアー、引用ビューアーなど、信頼できるコンポーネントを12個ほど用意します。現在のモデルはそれらを使ったほうが良い仕事をし、小さな編集も低コストで済むからです。これらは、今のエンジンにとっての運転支援機能のような役割を果たし、モデルが進歩していく間も品質を保ちます。新しいモデルがリリースされるたびに、評価で同じ編集タスクを再実行し、その支援がまだどれだけ必要かを確認します。ハーネスのほかの部分と同様に、私たちは今よりも高性能なエンジンを見据えて車を作っています。&lt;/p&gt;
&lt;p&gt;完全な生成UIは、プラットフォームにレンダラー以上のものを求めます。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;各アプリケーションは、サンドボックス化された専用のオリジンで動作します。&lt;/li&gt;
&lt;li&gt;すべてのバージョンを保持するので、まずい編集も1ステップで元に戻せます。&lt;/li&gt;
&lt;li&gt;インターフェースの一部を指し示して、変更を依頼できます。&lt;/li&gt;
&lt;li&gt;変更を保存する前に、プラットフォームが3つの画面サイズで描画し、エラー、アクセシビリティの問題、アプリケーションに許可されていない呼び出しがないかをチェックします。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;設計では、アプリケーションどうし、そしてアプリケーションとプラットフォームを互いに隔離します。各アプリケーションは別ドメイン上に専用のオリジンを持ち、厳格なコンテンツセキュリティポリシーを適用した、サンドボックス化されたiframeの中で動作します。プラットフォームにアクセスできるのは、MCP Appsをモデルにしたメッセージブリッジ経由のみです。（MCPはModel Context Protocolの略で、AIモデルをツールやデータに接続するためのオープン標準です。MCP Appsは、それを対話型のインターフェースに拡張したものです。）アプリケーションはAPIキーを保持せず、閲覧しているユーザーの権限で動作し、アプリケーションが行う有料の呼び出しは計測されます。すべてを1つのページで動かすよりもアプリケーションごとの準備は増えますが、不具合のあるアプリケーションや悪意のあるアプリケーションを、他のアプリケーションやユーザーの認証情報に近づけないことが狙いです。&lt;/p&gt;
&lt;p&gt;バージョンは追記のみなので、変更を元に戻すには、以前のバージョンを指し直すだけで済みます。バージョン履歴を後回しにすることも検討しましたが、使い勝手を大きく左右する主要な機能なので、最初から組み込みます。&lt;/p&gt;
&lt;h2 id=&quot;ローカルクラウドハイブリッド&quot;&gt;ローカル、クラウド、ハイブリッド&lt;/h2&gt;
&lt;p&gt;このプラットフォームは、3つの形で動作するように設計しています。ユーザーのマシン上でローカルまたはホスト型のモデルを使って動かすローカル、クラウド、そして1つのタスクがローカルとクラウドの両方のリソースを使うハイブリッドです。タスクごとに異なるAIモデルを使い分けられる設計です。&lt;/p&gt;
&lt;p&gt;目標は、意味のある場面でタスクを環境間で引き継げるようにすることです。たとえば、ノートPCで始めた作業をクラウドで続けるといった使い方です。設計では、1つのタスクを担当する環境は常に1つだけです。引き継ぎはステップの途中ではなく、エージェントのステップとステップの間、つまり復旧に使うのと同じチェックポイントで行います。その代わり、引き継ぎは実行中のステップが終わるまで待つことになります。当面、処理の移行はチェックポイントで行います。実行中の任意の処理を移行することは、後で検討する課題です。&lt;/p&gt;
&lt;h2 id=&quot;権限とコスト&quot;&gt;権限とコスト&lt;/h2&gt;
&lt;p&gt;設計では、ツール呼び出しの手前にポリシー層を置きます。ポリシー層は、各呼び出しをリスクとコンテキストに応じて確認し、許可するか、拒否するか、人に承認を求めるかを決めます。フェイルクローズ方式で、判断できない場合、その呼び出しは実行されません。リスクの低い読み取りはそのまま通り、リスクのある操作は人の判断を待つことになります。設計のその他の要素は次のとおりです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ファイルやコードを扱うツールはサンドボックスの中で動作し、外部へのネットワーク通信はデフォルトで無効にします。&lt;/li&gt;
&lt;li&gt;モデルにはシークレットの代わりにプレースホルダーを見せ、送信プロキシ (outbound proxy) が実際の値に差し替えます。これにより、認証情報がモデルのコンテキストに入らないようにします。&lt;/li&gt;
&lt;li&gt;予算ガードが、各呼び出しの前に金額を確保し、呼び出しの後に精算します。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;段階2では、これらをエージェントランタイムと並行して構築し、サンドボックスと権限チェックなしではどのエージェントも動かないようにする予定です。&lt;/p&gt;
&lt;p&gt;コスト追跡は、最初から設計に組み込んでいます。APIリクエストごとに使用量を1行記録し、リクエストの時点の価格で計算して、後から再計算はしない設計です。そのため、過去の実行は、課金された時点のコストのまま残ります。リトライもそれ自体が1つのリクエストなので、専用の行を持ちます。タイムアウトしたリクエストやリトライがコストログから漏れていたときに何が起きたかは、『&lt;a href=&quot;https://www.rounakzoeb.com/ja/writing/the-6x-gap-logged-vs-billed-llm-cost&quot;&gt;6倍の乖離&lt;/a&gt;』に書きました。トレースはデバッグ用、台帳はお金の管理用、そしてハッシュチェーン化したログ（各エントリーが直前のエントリーのハッシュを含むログ）は監査用です。当初は、どのユーザーにも自分のモデルのAPIキーを持ち込んでもらう形を想定しています。&lt;/p&gt;
&lt;h2 id=&quot;ロードマップ&quot;&gt;ロードマップ&lt;/h2&gt;
&lt;p&gt;ロードマップは5つの段階で構成しています。順序は目安であり、段階どうしが重なることもあります。音声やコンピューター操作 (computer use) などの機能は、他の機能と並行して開発する可能性があります。ロードマップはまだ調整中なので、あえて日付は設けていません。設計作業では、AIの支援があっても2人ですぐに作れる量をはるかに超える数の、最初のバージョン向けのアイデアが出ました。そのため、ロードマップはスコープで順序を決めています。&lt;/p&gt;
&lt;figure class=&quot;figure&quot; data-astro-cid-khyig4bw&gt;&lt;div class=&quot;mat&quot; data-zoom=&quot;/_astro/03_roadmap-ja.dWUrAK98.png&quot; data-alt=&quot;5段階のロードマップ。段階1（現在：初期設計とアーキテクチャ）、段階2（次の段階：コアエージェントフレームワークとセキュリティ）、続いて永続的な実行とハイブリッド実行、アプリケーションの生成と編集、自動化と最初のアプリケーション。&quot; data-astro-cid-khyig4bw&gt;&lt;picture data-astro-cid-khyig4bw=&quot;true&quot;&gt;&lt;img src=&quot;https://www.rounakzoeb.com/_astro/03_roadmap-ja.dWUrAK98_18Q4wC.png&quot; alt=&quot;5段階のロードマップ。段階1（現在：初期設計とアーキテクチャ）、段階2（次の段階：コアエージェントフレームワークとセキュリティ）、続いて永続的な実行とハイブリッド実行、アプリケーションの生成と編集、自動化と最初のアプリケーション。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-khyig4bw=&quot;true&quot; width=&quot;3200&quot; height=&quot;1800&quot;&gt;&lt;/picture&gt;&lt;/div&gt;&lt;figcaption class=&quot;figure__caption small muted&quot; data-astro-cid-khyig4bw&gt;開発ロードマップ。順序は目安であり、開発の進捗に応じて変わる可能性があります。&lt;a class=&quot;figure__full&quot; href=&quot;https://www.rounakzoeb.com/_astro/03_roadmap-ja.dWUrAK98.png&quot; data-full-size data-alt=&quot;5段階のロードマップ。段階1（現在：初期設計とアーキテクチャ）、段階2（次の段階：コアエージェントフレームワークとセキュリティ）、続いて永続的な実行とハイブリッド実行、アプリケーションの生成と編集、自動化と最初のアプリケーション。&quot; data-astro-cid-khyig4bw&gt;View full size&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;div class=&quot;table-scroll&quot; tabindex=&quot;0&quot;&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;段階&lt;/th&gt;
&lt;th&gt;内容&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;1. 初期設計とアーキテクチャ&lt;/strong&gt;（現在）&lt;/td&gt;
&lt;td&gt;プロダクト要件と主要なワークフロー、初期のアーキテクチャと技術設計、既存のエージェントフレームワークとツール群、開発と検証の進め方&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;2. コアエージェントフレームワークとセキュリティ&lt;/strong&gt;（次の段階）&lt;/td&gt;
&lt;td&gt;エージェントランタイムとモデルの選択、サンドボックスでの実行とツール連携、権限・承認・利用量の管理、実行履歴とコスト追跡&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;3. 永続的な実行とハイブリッド実行&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;永続的なタスク実行と中断からの復旧、ローカル・クラウド・ハイブリッドでの実行とタスクの引き継ぎ、デスクトップとの連携とコンピューター操作、音声の入出力とリアルタイムの対話&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;4. アプリケーションの生成と編集&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;独自のデータを持つ永続的なアプリケーション、アプリケーションの生成とビジュアル編集、バージョン履歴・ロールバック・共有、複数ユーザーでの共同作業&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;5. 自動化と最初のアプリケーション&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;定期実行のエージェント・トリガー・監視、外部とのやり取りにも対応する専用メール受信箱を持つエージェント、会議の支援、リサーチ・情報の監視・技術文書管理のための最初のアプリケーション&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;この順序の背景には、いくつかの判断があります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;永続化よりもセキュリティを先にします。サンドボックスと権限チェックなしでエージェントを動かすべきではないため、これらはランタイムと並行して段階2で構築します。&lt;/li&gt;
&lt;li&gt;監視とは、フォルダ、Webサイト、受信箱、データフィードなどをエージェントが見張り、変化があったときに対応することです。監視は、コストを抑える設計として計画しています。低コストで決定的なプローブ（モデルを使わない通常のコード）が、監視対象のフィンガープリント（内容が変わると変わる短い値）を計算し、意味のある変化があったときだけエージェントの実行を開始する想定です。1分ごとにモデルを呼び出すフォルダ監視なら、1日に1,440回のモデル呼び出しが発生します。ただし、変化が重要かどうかの判断にモデルが必要なモニターもあるため、モデルは必要な場面でだけ使うことを目指しています。&lt;/li&gt;
&lt;li&gt;各エージェントは専用のメールアドレスを持てるようにする予定で、CCに入れたり、転送したり、メッセージを送ったりできます。外部の相手とのやり取りはスレッドごとの権限と承認の範囲内で行う計画で、設計では相手からのメッセージを指示ではなくデータとして扱います。ただし、他のポリシーと同様に、これも実装してテストするまでは、プロンプトインジェクション（メッセージ内のテキストがエージェントの行動を別の方向へそらそうとすること）への防御とはみなせません。&lt;/li&gt;
&lt;li&gt;会議の支援には、情報の記録、要約、フォローアップのアクションが含まれます。どのように動作させるかは、細部をまだ詰めているところです。&lt;/li&gt;
&lt;li&gt;各段階は、実際の作業でのテストで締めくくる計画です。たとえば、中断したタスクが止まったところから再開できること、プロンプトインジェクションが認証情報に到達できないことなどです。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;未解決の課題&quot;&gt;未解決の課題&lt;/h2&gt;
&lt;p&gt;設計の中には、実際に作ってみないと決まらない部分もあります。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ローカル実行とクラウド実行の間の引き継ぎ（進行中のタスクを、ユーザーのマシンとクラウドの間で受け渡すこと）が、実際にどこまで可能か&lt;/li&gt;
&lt;li&gt;どのモニターを決定的なまま（モデルを呼び出さず、通常のコードでチェックする形）にでき、どのモニターにモデルが必要になるか&lt;/li&gt;
&lt;li&gt;会議の支援をどのように動作させるべきか&lt;/li&gt;
&lt;li&gt;私自身のエージェントフレームワークをコアに残し続けるかどうか。設計には、どのような場合に置き換えるかを記録しています。保守にかかるコストが、それによって節約できる分を上回った場合です。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&quot;ご意見をお寄せください&quot;&gt;ご意見をお寄せください&lt;/h2&gt;
&lt;p&gt;開発の進捗に合わせて、更新を投稿していきます。技術文書台帳を扱うお仕事をされている方には、台帳業務のうちどの作業に最も時間を取られているかを、ぜひ伺いたいと思っています。最初のアプリケーションで確実に押さえるべきなのは、まさにその点だからです。ご意見やご感想があれば、&lt;a href=&quot;https://www.rounakzoeb.com/ja/contact&quot;&gt;お問い合わせページ&lt;/a&gt;からご連絡ください。&lt;/p&gt;</content:encoded><category>AIエージェント</category><category>アーキテクチャ</category><category>コンテキストエンジニアリング</category></item><item><title>LLMアプリを最新に保つ：「モデル名の更新」で実際に必要だったこと</title><link>https://www.rounakzoeb.com/ja/writing/keeping-an-llm-app-current</link><guid isPermaLink="true">https://www.rounakzoeb.com/ja/writing/keeping-an-llm-app-current</guid><description>日本語の単語カードアプリを更新したところ、リトライ、画像の扱い、コスト追跡に問題が見つかりました。何を変更し、どう確認したかをまとめます。</description><pubDate>Mon, 12 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;日本語の単語カードアプリのモデル更新は、ちょっとした保守作業で済むと思っていました。ところが連携部分を確認してみると、リトライ、アップロードされた画像、コスト追跡に問題が見つかりました。この記事では、何を変更し、どうテストしたかを説明します。&lt;/p&gt;
&lt;p&gt;対象のアプリは、私の&lt;a href=&quot;https://japaneseflashcardspublic.streamlit.app/&quot;&gt;Japanese Flashcard Generator&lt;/a&gt;（&lt;a href=&quot;https://www.rounakzoeb.com/ja/work/japanese-flashcard-generator&quot;&gt;プロジェクトについて読む&lt;/a&gt;）です。学習ページの写真を、間隔反復型の単語カードアプリであるAnki用の単語カードに変換します。自分の日本語学習に使っていて、&lt;a href=&quot;https://github.com/rzoeb/Japanese_Flash_Cards&quot;&gt;オープンソース&lt;/a&gt;で公開しています。ここで紹介する更新は、2026年10月9日に公開しました。&lt;/p&gt;
&lt;p&gt;小さなStreamlitアプリ（Streamlitは、シンプルなWebアプリを作るためのPythonフレームワークです）で、1回の実行は次の4つのステップで構成されます。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Geminiの呼び出しで、写真が学習ページとして使えるかを確認します。&lt;/li&gt;
&lt;li&gt;LLMWhispererで、その写真にOCR（光学文字認識）をかけます。&lt;/li&gt;
&lt;li&gt;2回目のGemini呼び出しで、OCRテキストを画像と照合し、カードをJSONとして返します。このJSONはPydanticで検証します。このステップは、組み込みのFew-shotの例（何を出力すべきかをモデルに示すサンプルページ）で誘導できます。&lt;/li&gt;
&lt;li&gt;アプリはAnki用のカンマ区切りのテキストファイルをエクスポートし、その実行にかかったコストを表示します。&lt;/li&gt;
&lt;/ol&gt;
&lt;figure class=&quot;figure&quot; data-astro-cid-khyig4bw&gt;&lt;div class=&quot;mat&quot; data-zoom=&quot;/_astro/01_pipeline-ja.CK34wx_9.png&quot; data-alt=&quot;ページの写真 → アップロードチェック → 適合性チェック → 位置情報を除去したコピーでのOCR → Geminiによるクロスチェック → 検証済みのカード（画面上の表 + Anki用CSV）。コスト表示と、APIキーを各自で用意する方式の、6つのモデルから選べるモデル選択欄付き。&quot; data-astro-cid-khyig4bw&gt;&lt;picture data-astro-cid-khyig4bw=&quot;true&quot;&gt;&lt;img src=&quot;https://www.rounakzoeb.com/_astro/01_pipeline-ja.CK34wx_9_ZvaldL.png&quot; alt=&quot;ページの写真 → アップロードチェック → 適合性チェック → 位置情報を除去したコピーでのOCR → Geminiによるクロスチェック → 検証済みのカード（画面上の表 + Anki用CSV）。コスト表示と、APIキーを各自で用意する方式の、6つのモデルから選べるモデル選択欄付き。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-khyig4bw=&quot;true&quot; width=&quot;3200&quot; height=&quot;1800&quot;&gt;&lt;/picture&gt;&lt;/div&gt;&lt;figcaption class=&quot;figure__caption small muted&quot; data-astro-cid-khyig4bw&gt;2026年10月の更新後のパイプライン。&lt;a class=&quot;figure__full&quot; href=&quot;https://www.rounakzoeb.com/_astro/01_pipeline-ja.CK34wx_9.png&quot; data-full-size data-alt=&quot;ページの写真 → アップロードチェック → 適合性チェック → 位置情報を除去したコピーでのOCR → Geminiによるクロスチェック → 検証済みのカード（画面上の表 + Anki用CSV）。コスト表示と、APIキーを各自で用意する方式の、6つのモデルから選べるモデル選択欄付き。&quot; data-astro-cid-khyig4bw&gt;View full size&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;使っているモデルが廃止されたとき&quot;&gt;使っているモデルが廃止されたとき&lt;/h2&gt;
&lt;p&gt;アプリのデフォルト兼フォールバックのモデルだった&lt;code&gt;gemini-2.0-flash&lt;/code&gt;は、2026年6月1日に&lt;a href=&quot;https://ai.google.dev/gemini-api/docs/deprecations&quot;&gt;提供を終了&lt;/a&gt;しました。アプリのドロップダウンにあった8つのモデルのうち、デフォルトを含む5つが、提供終了または廃止になっていました。残る3つは今も動きますが、使える人は限られます。Googleの&lt;a href=&quot;https://ai.google.dev/gemini-api/docs/changelog&quot;&gt;変更履歴 (changelog)&lt;/a&gt;の2026年9月18日の項目は、Gemini 2.5モデルの利用を「過去に実際に利用したことのあるユーザー」に限定する一方で、非推奨ではないとしています。&lt;/p&gt;
&lt;p&gt;1つ驚いたことがあります。10月の時点でも、&lt;code&gt;models.get(&amp;quot;gemini-2.0-flash&amp;quot;)&lt;/code&gt;はメタデータを返していました。&lt;code&gt;models.list&lt;/code&gt;には含まれておらず、ドキュメント上も提供終了とされていたにもかかわらずです。「このモデルは存在するか？」を確かめる起動時チェックなら、通過していたでしょう。信頼できる情報源は、非推奨 (deprecations) のページでした。&lt;/p&gt;
&lt;h2 id=&quot;モデル名の変更だけでは済まなかった&quot;&gt;モデル名の変更だけでは済まなかった&lt;/h2&gt;
&lt;p&gt;まず連携部分を読み取り専用でレビューし、書面の計画を作成しました。計画の内容は、Googleのドキュメント、インストール済みのSDK、オフラインのHTTPプローブで確認しています。その後、コーディングエージェントが変更を加え、私がレビューしてマージしました（この進め方については『&lt;a href=&quot;https://www.rounakzoeb.com/ja/writing/spec-first-test-first-building-with-ai-coding-agents&quot;&gt;仕様ファースト、テストファースト&lt;/a&gt;』で紹介しています）。&lt;/p&gt;
&lt;p&gt;「主にモデル名の変更で済むか？」という問いに対する計画の答えは、「いいえ。ただし、ほぼそのとおり」でした。モデル名だけを変えていたら、アプリはGemini 3の上で、次のような状態のまま動いていたはずです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ドキュメントが使わないよう警告しているサンプリング設定&lt;/li&gt;
&lt;li&gt;機能しないリトライ&lt;/li&gt;
&lt;li&gt;プロンプトがブロックされるたびに起きるクラッシュ&lt;/li&gt;
&lt;li&gt;ユーザーのGeminiプロジェクト内のファイルをすべて削除するクリーンアップ処理&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;gemini-3には別の設定が必要&quot;&gt;Gemini 3には別の設定が必要&lt;/h3&gt;
&lt;p&gt;GoogleのGemini 3向けガイダンスは、&lt;code&gt;temperature&lt;/code&gt;、&lt;code&gt;top_p&lt;/code&gt;、&lt;code&gt;top_k&lt;/code&gt;を削除するよう求めており、temperatureを1.0未満に設定すると「ループや性能の低下など、予期しない動作につながるおそれがある」と警告しています。私の適合性チェックは、0.1を送っていました。&lt;/p&gt;
&lt;p&gt;Gemini 3では、thinking budget（思考予算）もthinking level（思考レベル）に置き換わっており、詳細はモデルによって異なります。3.8 Flashでは、&lt;code&gt;minimal&lt;/code&gt;レベルを指定するとエラーになり、levelとbudgetを同時に送ると400エラーが返ります。そこでアプリは、リクエストの設定をモデルファミリーごとに組み立てるようにしました。Gemini 3のモデルにはtemperatureを送らず、呼び出しごとに&lt;code&gt;thinking_level&lt;/code&gt;を明示的に指定します。こうしておけば、Googleがデフォルト値を変えても、アプリの動作が知らないうちに変わることはありません。レガシーの2.5モデルは、従来の設定のままです。&lt;/p&gt;
&lt;h3 id=&quot;一度も実行されなかったリトライ&quot;&gt;一度も実行されなかったリトライ&lt;/h3&gt;
&lt;p&gt;アプリは、Geminiの呼び出しを&lt;code&gt;tenacity&lt;/code&gt;のリトライデコレーターで包んでいました。その判定条件 (predicate) は、&lt;code&gt;google.api_core&lt;/code&gt;の例外とgRPCのステータスコードを探すものでした。しかし現行の&lt;code&gt;google-genai&lt;/code&gt; SDKは、HTTPステータスコードを持つ独自のエラーを送出するため、判定条件に一致しません。そもそも、エラーはデコレーターまで届いていませんでした。関数のcatch-allハンドラーがエラーを捕まえてエラー用の辞書を返していたため、デコレーターが例外を目にすることはなかったのです。さらに、アプリが現在使っているバージョンのSDKでは、設定しない限り何もリトライされません。実際に再現してみると、429（レート制限）のレスポンスに対して、呼び出しはちょうど1回だけでした。&lt;/p&gt;
&lt;p&gt;修正では、SDK自体の&lt;code&gt;HttpRetryOptions&lt;/code&gt;を使い（408、429、5xxのレスポンスに対して4回試行、タイムアウト180秒）、古いリトライのコードを削除しました。これで、直接の依存関係も2つ減りました。現在はテストで、429の後に200を返すモック化したトランスポートを通して実際のSDKクライアントを動かし、呼び出しがちょうど2回だったことを確認しています。&lt;/p&gt;
&lt;h3 id=&quot;誤ったファイルを削除したクリーンアップ&quot;&gt;誤ったファイルを削除したクリーンアップ&lt;/h3&gt;
&lt;p&gt;画像は、Files API経由でGeminiに送っていました。呼び出しのたびに、その後で&lt;code&gt;finally:&lt;/code&gt;ブロックが、APIキーのプロジェクト内にある全ファイルを一覧にして、すべて削除していました。&lt;/p&gt;
&lt;p&gt;APIキーを各自で用意する公開版のアプリでは、それは&lt;em&gt;訪問者の&lt;/em&gt;プロジェクトです。再現テストでは、プロジェクトにもともとあった無関係なファイル2つが削除されました。その間、アプリ上には何の異常も見えません。訪問者の他のアプリのファイルが、ただ消えていたはずです。&lt;/p&gt;
&lt;p&gt;現在、アプリはFiles APIを使っていません。画像はインラインで、リクエスト自体に含めて送ります。テストでは、フェイククライアントのFiles APIに何かが触れるとエラーを送出するため、全削除の処理が知らないうちに復活することはありません。&lt;/p&gt;
&lt;h2 id=&quot;画像をインラインで送る&quot;&gt;画像をインラインで送る&lt;/h2&gt;
&lt;p&gt;画像をインラインで送るとなると、リクエストのサイズが問題になります。Googleの&lt;a href=&quot;https://ai.google.dev/gemini-api/docs/image-understanding&quot;&gt;画像理解のガイド&lt;/a&gt;では、インライン画像を含むリクエストの上限を、プロンプトを含めて合計20 MBとしています。一方、より新しい&lt;a href=&quot;https://ai.google.dev/gemini-api/docs/file-input-methods&quot;&gt;ファイル入力のガイド&lt;/a&gt;では、&lt;a href=&quot;https://blog.google/innovation-and-ai/technology/developers-tools/gemini-api-new-file-limits/&quot;&gt;Googleが2026年1月に発表した上限の引き上げ&lt;/a&gt;を受けて、最大100 MB（PDFは50 MB）までのインラインのペイロードを認めています。アプリは、厳しいほうの数字を十分に下回るようにしました。&lt;/p&gt;
&lt;p&gt;以前の前処理では、1枚あたり約6〜10.5 MBのPNGを書き出していました。語彙 (Vocabulary) モードの実行では、組み込みの例の画像3枚と学習ページを送るため、1回のリクエストは約27〜37 MBになっていました。そこで、JPEG（長辺3,072 px、品質90、透過部分は白で塗りつぶし）に切り替えました。語彙モードの1回の実行（例の画像3枚と学習ページ1枚）を変更の前後で測定し、&lt;a href=&quot;https://github.com/rzoeb/Japanese_Flash_Cards/pull/11&quot;&gt;プルリクエスト&lt;/a&gt;に記録した結果は次のとおりです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;リクエストのペイロードは、&lt;strong&gt;34.8 MBから3.0 MB&lt;/strong&gt;に減りました。&lt;/li&gt;
&lt;li&gt;前処理時間は、&lt;strong&gt;45.7秒から2.1秒&lt;/strong&gt;に短縮しました。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;前処理時間は、リクエストを送る前にアプリ自身が行う画像の準備にかかる時間で、Geminiが応答するまでの時間は含みません。アプリ自体にはリクエストサイズの上限はなく、各画像を約2 MB以下にリサイズするようになっています。（次のセクションで説明する20 MBのアップロード上限はファイル単位のもので、アップロード欄を守るためのものです。）&lt;/p&gt;
&lt;p&gt;アプリが使うGemini 3のモデルでは、デフォルトの画像解像度がすでに「high」の設定と同等なので、解像度を指定する必要はありませんでした。&lt;/p&gt;
&lt;figure class=&quot;figure&quot; data-astro-cid-khyig4bw&gt;&lt;div class=&quot;mat&quot; data-zoom=&quot;/_astro/payload_and_time-ja.B52lIuNY.png&quot; data-alt=&quot;語彙モード1回分の実行を示すグラフで、タイトルは「画像のリサイズで、ペイロードが34.8 MBから3.0 MBに減少」。リクエストのペイロードは変更前が34.8 MB、変更後が3.0 MBで、Googleの画像ガイドにある20 MBの数字の位置に目印があります。前処理時間は変更前が45.7秒、変更後が2.1秒です。&quot; data-astro-cid-khyig4bw&gt;&lt;picture data-astro-cid-khyig4bw=&quot;true&quot;&gt;&lt;img src=&quot;https://www.rounakzoeb.com/_astro/payload_and_time-ja.B52lIuNY_ZhUDLz.png&quot; alt=&quot;語彙モード1回分の実行を示すグラフで、タイトルは「画像のリサイズで、ペイロードが34.8 MBから3.0 MBに減少」。リクエストのペイロードは変更前が34.8 MB、変更後が3.0 MBで、Googleの画像ガイドにある20 MBの数字の位置に目印があります。前処理時間は変更前が45.7秒、変更後が2.1秒です。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-khyig4bw=&quot;true&quot; width=&quot;3200&quot; height=&quot;1800&quot;&gt;&lt;/picture&gt;&lt;/div&gt;&lt;figcaption class=&quot;figure__caption small muted&quot; data-astro-cid-khyig4bw&gt;語彙モード1回分の実行について、リサイズしたJPEGへの切り替え前後を比較したもの。目印はGoogleの画像ガイドにある20 MBの数字です。前処理時間はアプリ自身の画像の準備にかかる時間で、Geminiの応答時間は含みません。&lt;a class=&quot;figure__full&quot; href=&quot;https://www.rounakzoeb.com/_astro/payload_and_time-ja.B52lIuNY.png&quot; data-full-size data-alt=&quot;語彙モード1回分の実行を示すグラフで、タイトルは「画像のリサイズで、ペイロードが34.8 MBから3.0 MBに減少」。リクエストのペイロードは変更前が34.8 MB、変更後が3.0 MBで、Googleの画像ガイドにある20 MBの数字の位置に目印があります。前処理時間は変更前が45.7秒、変更後が2.1秒です。&quot; data-astro-cid-khyig4bw&gt;View full size&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;公開アップロード欄の堅牢化&quot;&gt;公開アップロード欄の堅牢化&lt;/h2&gt;
&lt;p&gt;ライブ版のアプリは公開されているため、誰でもファイルをアップロードできます。リスクの1つが展開爆弾 (decompression bomb) です。デコードすると極端に大きく膨らむ小さなファイルのことで、20 KBのPNGが、APIの呼び出しやキーのチェックが行われる前に、約483 MBにまで展開されることがあります。現在は、何かをデコードする前に、ファイルヘッダーをもとにサイズ、形式、ピクセル数をチェックしています。上限は次のとおりです。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JPEGとPNGのみ&lt;/li&gt;
&lt;li&gt;1ファイルあたり20 MB (&lt;code&gt;MAX_UPLOAD_MB = 20&lt;/code&gt;)、1回の実行につき10ファイルまで&lt;/li&gt;
&lt;li&gt;JPEGは120メガピクセル（縮小スケールでデコード）、PNGは40メガピクセル&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;スモークテストでは、爆弾PNG、&lt;code&gt;.png&lt;/code&gt;に名前を変えたGIF、21 MBのファイル、11ファイルのバッチを、いずれもAPIを呼び出す前に拒否しました。&lt;/p&gt;
&lt;p&gt;もう1つの問題は位置情報でした。OCRのステップには、元のアップロードファイルがメタデータごと渡されていました。スマートフォンの写真にはGPS座標が含まれていることが多く、私自身のテスト写真にも含まれていました。修正にあたっては、画質を変えないことを条件にしました。現在は、EXIF、XMP、コメント、末尾のデータを除去し、圧縮された画像データはバイト単位でそのままコピーして、向きとカラープロファイルも保持しています。デコード後のピクセルは同一です。&lt;/p&gt;
&lt;p&gt;更新後に公開リポジトリのセキュリティレビューを行ったところ、コミットされた秘密情報は見つからず、重大度が高いと評価された問題もありませんでした。&lt;/p&gt;
&lt;p&gt;また、生成したカードの表をダウンロード (Download) ボタンの上に追加しました。ダウンロードをクリックしても、表は消えなくなりました。&lt;/p&gt;
&lt;h2 id=&quot;決まった日に変わる価格&quot;&gt;決まった日に変わる価格&lt;/h2&gt;
&lt;p&gt;執筆時点（2026年10月）で、Googleの&lt;a href=&quot;https://ai.google.dev/gemini-api/docs/pricing&quot;&gt;料金ページ&lt;/a&gt;には、StandardティアのGemini 3.8 Flashと3.6 Flashの料金が、2026年12月31日まで入力100万トークンあたりUS$0.75、出力100万トークンあたりUS$3.75と掲載されています。この料金は2027年1月1日に2倍になり、それぞれUS$1.50とUS$7.50になる予定です。モデルごとに価格を1つしか持たないと、元日以降、これらのモデルでのすべての実行について、アプリのコスト表示が実際の半分になってしまいます。Gemini 3.5 Flash-Liteについては、価格改定は発表されていません。&lt;/p&gt;
&lt;p&gt;そこで、アプリの価格レジストリの各エントリーに、&lt;code&gt;effective_from&lt;/code&gt;の日付を付けた&lt;code&gt;price_changes&lt;/code&gt;を持たせられるようにしました。思考トークンは、出力の料金で課金されます。&lt;code&gt;-latest&lt;/code&gt;のモデルエイリアスは除外しました。エイリアスがどのモデルを指しているかを、コスト表示の側では知りようがないからです。&lt;/p&gt;
&lt;p&gt;2026年10月9日の有料のスモークテストでは、語彙モードの1回の実行（例のページ3枚と学習ページ1枚）のコストは、2026年10月の価格で、&lt;strong&gt;3.8 Flashで約US$0.02&lt;/strong&gt;、&lt;strong&gt;3.5 Flash-Liteで約US$0.0065&lt;/strong&gt;でした。これはGeminiのトークン料金のみで、OCRはLLMWhispererの無料枠で実行しました。2026年10月時点で、この無料枠ではクレジットカードの登録なしで1日100ページまで処理できます。それを超えると、LLMWhispererは処理モードに応じてページ単位で課金し、料金はデジタルPDFのUS$0.001から、フォームや表を含む読み取りの難しいスキャンのUS$0.015までです。手計算の結果は、アプリの表示と完全に一致しました（適合性チェック1回でUS$0.001723）。有料のスモークテスト全体の費用は、上限US$2に対してUS$0.32でした。&lt;/p&gt;
&lt;h2 id=&quot;フェイクを使ったテスト&quot;&gt;フェイクを使ったテスト&lt;/h2&gt;
&lt;p&gt;今回の更新で、37件の自動テストを追加しました。内訳は、33件のオフラインテストと、4件のヘッドレスStreamlit UIテストです。オフラインテストでは、フェイクのGeminiクライアント、モック化したHTTPレイヤー、フェイクのOCRクライアント、メモリ上で生成した画像を使うため、APIキーは必要ありません。フェイクを選んだのは、そうすればテストが実際のAPIを呼び出さないため、速く実行でき、費用がかからず、毎回同じ結果になるからです。テストはリポジトリの&lt;a href=&quot;https://github.com/rzoeb/Japanese_Flash_Cards/tree/main/tests&quot;&gt;testsフォルダ&lt;/a&gt;にあります。テストスイートの初版を元のコードに対して実行したところ、11件のテストが失敗し、修正を書く前にバグを確認できました。&lt;/p&gt;
&lt;p&gt;このテストスイートには限界もあります。Python 3.10と3.12で動作しますが、まだCIには組み込んでいません。フェイクを使っているため、テストしているのは配管部分、つまりAPI呼び出しの周りにあるアプリ自身のロジックです。カードの品質を判断することはできません。Pydanticは出力の構造を確認しますが、日本語が正しいかどうかは、引き続きライブのスモークテストと、人がカードを読んで確かめることにかかっています。&lt;/p&gt;
&lt;h2 id=&quot;2回目のコードレビューで見つかったこと&quot;&gt;2回目のコードレビューで見つかったこと&lt;/h2&gt;
&lt;p&gt;2番目のレビュアーは、GitHub上のプルリクエストを自動でレビューする、GitHub Copilotのレビューエージェントでした。5件の異なる指摘が挙がり、どう対応するかを決める前に、それぞれを再現しました。4件は実際のバグで、それぞれの修正には、その修正がなければ失敗する回帰テストを付けました。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;透過のあるグレースケールPNGやパレットPNGが、Geminiに送るコピーで黒くなっていた。&lt;/li&gt;
&lt;li&gt;メタデータの除去で、JFIFのサムネイルが残っていた。&lt;/li&gt;
&lt;li&gt;ちょうど200,000トークンのプロンプトが、高いほうの料金区分で課金されていた。&lt;/li&gt;
&lt;li&gt;空のOCRテキストがステータスチェックを通過し、何もないところからカードを作るようGeminiに求めていた。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;5件目は、レジストリのレート制限のセクションが旧モデルしかカバーしていない、という指摘でした。これには手を付けませんでした。レート制限はアカウントの利用ティアによって変わるため、値を埋めれば数字をでっち上げることになるからです。&lt;/p&gt;
&lt;h2 id=&quot;アプリの両方のバージョンを更新する&quot;&gt;アプリの両方のバージョンを更新する&lt;/h2&gt;
&lt;p&gt;リポジトリには、長期間維持している2つのブランチがあります。&lt;code&gt;main&lt;/code&gt;は私自身のAPIキーを使い、&lt;code&gt;app_public&lt;/code&gt;は訪問者にキーの入力を求めます。Streamlit Community Cloudがデプロイするのは&lt;code&gt;app_public&lt;/code&gt;のほうです。どちらにも同じ修正が必要だったため、1つ目のプルリクエスト (&lt;a href=&quot;https://github.com/rzoeb/Japanese_Flash_Cards/pull/11&quot;&gt;#11&lt;/a&gt;) で&lt;code&gt;main&lt;/code&gt;に取り込み、2つ目のプルリクエスト (&lt;a href=&quot;https://github.com/rzoeb/Japanese_Flash_Cards/pull/12&quot;&gt;#12&lt;/a&gt;) で同じ変更セットを&lt;code&gt;app_public&lt;/code&gt;にマージして、コンフリクトの解消は1回で済ませました。現在、両ブランチは同じコミットを共有しています。&lt;/p&gt;
&lt;figure class=&quot;figure&quot; data-astro-cid-khyig4bw&gt;&lt;div class=&quot;mat&quot; data-zoom=&quot;/_astro/branch_flow-ja.BVWYmB6Y.png&quot; data-alt=&quot;2つのレーンを示した図です。mainから切ったフィーチャーブランチ上の変更セットが、PR #11を経てmainに入ります。app_public用の2つ目のブランチがそれをマージし、コンフリクトを1回で解消して、PR #12を経てapp_publicに入り、Streamlit Community Cloudが再デプロイします。&quot; data-astro-cid-khyig4bw&gt;&lt;picture data-astro-cid-khyig4bw=&quot;true&quot;&gt;&lt;img src=&quot;https://www.rounakzoeb.com/_astro/branch_flow-ja.BVWYmB6Y_ZWwhj1.png&quot; alt=&quot;2つのレーンを示した図です。mainから切ったフィーチャーブランチ上の変更セットが、PR #11を経てmainに入ります。app_public用の2つ目のブランチがそれをマージし、コンフリクトを1回で解消して、PR #12を経てapp_publicに入り、Streamlit Community Cloudが再デプロイします。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-khyig4bw=&quot;true&quot; width=&quot;3200&quot; height=&quot;1800&quot;&gt;&lt;/picture&gt;&lt;/div&gt;&lt;figcaption class=&quot;figure__caption small muted&quot; data-astro-cid-khyig4bw&gt;1つの変更セット、2つのブランチ、レビュー済みの2つのプルリクエスト。&lt;a class=&quot;figure__full&quot; href=&quot;https://www.rounakzoeb.com/_astro/branch_flow-ja.BVWYmB6Y.png&quot; data-full-size data-alt=&quot;2つのレーンを示した図です。mainから切ったフィーチャーブランチ上の変更セットが、PR #11を経てmainに入ります。app_public用の2つ目のブランチがそれをマージし、コンフリクトを1回で解消して、PR #12を経てapp_publicに入り、Streamlit Community Cloudが再デプロイします。&quot; data-astro-cid-khyig4bw&gt;View full size&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;未解決few-shotの例の漏れ&quot;&gt;未解決：Few-shotの例の漏れ&lt;/h2&gt;
&lt;p&gt;例の画像を有効にすると、レガシーのGemini 2.5 Proは、ページ上の漢字ではなく、組み込みの例にある7つの漢字をそのまま返しました。2.5 Flash-Liteは、2つのセットを混ぜて返しました。&lt;/p&gt;
&lt;p&gt;A/B比較の実行では、以前の前処理でも同じ漏れが起き、例を無効にすると漏れは起きませんでした。つまり、このバグは今回の更新より前からあったものです。テストの範囲では、Gemini 3のモデルには影響がなく、この問題がはるかに起きにくいようです。2.5系の問題はまだ修正しておらず、おそらくモデル自体の限界によるものです。選択肢は、例を別々の会話ターンとして送るか、2.5モデルでは例を無効にするかです。Googleは現在、2.5モデルの利用を過去に利用したことのあるユーザーに限定しているため、アプリではこれらを「legacy」と表示しています。&lt;/p&gt;
&lt;h2 id=&quot;振り返り&quot;&gt;振り返り&lt;/h2&gt;
&lt;p&gt;今では、モデル一覧を有効期限付きの依存関係として扱っています。プレビューモデルは2週間前の通知で廃止されることがあり、APIのメタデータのエンドポイントは提供終了したモデルの情報をまだ返していました。そのため、非推奨のページを確認するようにしています。価格にも日付が必要です。モデルごとに価格が1つだけでは、次の改定までしか正確ではないからです。最も深刻だった2つのバグ、つまり一度も動いていなかったリトライ処理と、アプリが所有していないファイルを削除していたクリーンアップは、どちらも表に出ないものでした。それを明らかにしたのは、フェイククライアントとモック化したトランスポートです。&lt;/p&gt;
&lt;p&gt;また、アプリの表面積を小さく保つようにしました。依存関係を減らし、Files APIもモデルエイリアスも使っていません。Googleは現在&lt;code&gt;generateContent&lt;/code&gt;を「legacy」と呼んでいますが、引き続き完全にサポートしています。そのため、同じ変更の中でAPIまで切り替えることはせず、そのまま使い続けることにしました。&lt;/p&gt;
&lt;p&gt;2026年10月18日時点で、ホスティング中のアプリはまだPython 3.10で動いています。Python 3.10は、2026年10月1日にサポート終了 (end of life) を迎えました。テストは3.12でも通るので、ホスティング中のアプリを3.12に移行する予定です。&lt;/p&gt;</content:encoded><category>LLMアプリの保守</category><category>Gemini</category><category>テスト</category><category>AI評価</category></item><item><title>6倍の乖離：LLMのコストログを請求額と突き合わせる</title><link>https://www.rounakzoeb.com/ja/writing/the-6x-gap-logged-vs-billed-llm-cost</link><guid isPermaLink="true">https://www.rounakzoeb.com/ja/writing/the-6x-gap-logged-vs-billed-llm-cost</guid><description>タイムアウトしたリクエストとリトライの費用がLLMのリクエストログから漏れていた経緯と、コストをより正確に追跡するために行った変更をまとめます。</description><pubDate>Mon, 05 Oct 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;2026年4月上旬、顧客向けのドキュメント処理ジョブを確認していたところ、プロバイダーの請求額が、私のアプリケーションのコストログの約6倍になっていることに気づきました。調べていくと、タイムアウトしてリトライされたリクエストが原因として浮かび上がりました。コストの計上を正すには、リクエストパイプラインのいくつかの部分にまたがる変更が必要でした。&lt;/p&gt;
&lt;p&gt;これが問題だったのは、このコストログを、作業の見積もり、予算の設定、そして顧客にジョブの費用を伝えるために使っているからです。&lt;/p&gt;
&lt;h2 id=&quot;ジョブの概要&quot;&gt;ジョブの概要&lt;/h2&gt;
&lt;p&gt;このジョブは、長い技術文書を1リクエストにつき1ページずつ翻訳するものでした。OpenAIのGPT-5.4 miniを使い、一時期はモデルの推論レベル (reasoning effort) を最高の&lt;code&gt;xhigh&lt;/code&gt;に設定して実行していたため、各ページで大きなプロンプトを送信し、その後長時間の推論が走りました。今振り返ると、そもそもこのタスクにとって、その設定は適切な選択ではなかったのかもしれません。&lt;/p&gt;
&lt;p&gt;リクエストは、私が設計・構築・保守しているPython製のAIエージェントフレームワーク、&lt;a href=&quot;https://www.rounakzoeb.com/ja/work/doc-processing-toolkit&quot;&gt;doc-processing-toolkit&lt;/a&gt;を経由していました。ツールキットは各リクエストを、tenacityライブラリのリトライデコレーターでラップしていました。試行は5回までで、指数バックオフ付きです。リクエストが成功すると、ツールキットはレスポンスのusageブロックをもとに、モデルレジストリの料金（各プロバイダーが公開している料金に基づくもの）でコストを算出し、ジョブのファイルに書き込みます。&lt;/p&gt;
&lt;h2 id=&quot;ログと請求額の突き合わせ&quot;&gt;ログと請求額の突き合わせ&lt;/h2&gt;
&lt;p&gt;突き合わせでは、プロバイダーの請求額を基準とし、ログのほうを検証の対象として扱いました。とはいえ、請求額にも限界がありました。プロバイダーのusage APIは、使用量をリクエスト単位ではなく、モデル別の時間バケット単位で返します。そのため、ある日にいくらかかったかはわかっても、どのページが原因かはわかりません。ジョブ自体のログのほうが粒度が細かかったので、このAPIのために管理者用キーを設定するのは見送りました。&lt;/p&gt;
&lt;p&gt;ログからは、すぐにパターンが見えてきました。ページを追うごとに、&lt;code&gt;APITimeoutError&lt;/code&gt;が出てはリトライが走っています。あるページでは、1回目の試行が約30分実行されたあとタイムアウトし、続く4回も同じ結果になって、そのページは失敗しました。同じパターンが多くのページで繰り返されていました。&lt;/p&gt;
&lt;p&gt;こうしたタイムアウトとリトライ、そして請求額の合計は、私が実際に確認したものです。両者のつながりは推論です。タイムアウトした各試行では、モデルはプロンプト全体を読み込んだはずで、接続が切れるまで最大30分にわたって推論していた可能性もあります。その処理は課金されたと考えられます。一方、ツールキットはレスポンスを一度も受け取らなかったため、料金を計算するためのusageブロックがなく、何も記録しませんでした。その後tenacityがリトライし、その試行もまた記録されませんでした。請求額はモデル別・時間バケット別の合計しか示さないため、個々の試行にいくらかかったかを請求額から確かめることはできません。&lt;/p&gt;
&lt;figure class=&quot;figure&quot; data-astro-cid-khyig4bw&gt;&lt;div class=&quot;mat&quot; data-zoom=&quot;/_astro/timeout_timeline-ja.C5T2w7MV.png&quot; data-alt=&quot;1ページ分のタイムラインです。約30分の試行が5回あり、いずれもタイムアウトで終わったあと、ページは失敗しています。上段には「プロバイダーの請求額からの推定」というラベルが付いており、各試行を課金された処理として示しています。下段はツールキットが記録したコストで、どの試行でもゼロトークンのままです。横のパネルではジョブ全体を比較しており、コストログが1倍、プロバイダーの請求額が約6倍で、「I used the provider&apos;s bill as the reference for reconciliation.」（突き合わせの基準にはプロバイダーの請求額を使った）という注記があります。&quot; data-astro-cid-khyig4bw&gt;&lt;picture data-astro-cid-khyig4bw=&quot;true&quot;&gt;&lt;img src=&quot;https://www.rounakzoeb.com/_astro/timeout_timeline-ja.C5T2w7MV_Z1gLLwd.png&quot; alt=&quot;1ページ分のタイムラインです。約30分の試行が5回あり、いずれもタイムアウトで終わったあと、ページは失敗しています。上段には「プロバイダーの請求額からの推定」というラベルが付いており、各試行を課金された処理として示しています。下段はツールキットが記録したコストで、どの試行でもゼロトークンのままです。横のパネルではジョブ全体を比較しており、コストログが1倍、プロバイダーの請求額が約6倍で、「I used the provider&apos;s bill as the reference for reconciliation.」（突き合わせの基準にはプロバイダーの請求額を使った）という注記があります。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-khyig4bw=&quot;true&quot; width=&quot;3200&quot; height=&quot;1800&quot;&gt;&lt;/picture&gt;&lt;/div&gt;&lt;figcaption class=&quot;figure__caption small muted&quot; data-astro-cid-khyig4bw&gt;1ページで、タイムアウトした試行が5回。ツールキットには何も記録されていません。課金の段はプロバイダーの合計額から推定したもので、試行の長さはおおよそです。&lt;a class=&quot;figure__full&quot; href=&quot;https://www.rounakzoeb.com/_astro/timeout_timeline-ja.C5T2w7MV.png&quot; data-full-size data-alt=&quot;1ページ分のタイムラインです。約30分の試行が5回あり、いずれもタイムアウトで終わったあと、ページは失敗しています。上段には「プロバイダーの請求額からの推定」というラベルが付いており、各試行を課金された処理として示しています。下段はツールキットが記録したコストで、どの試行でもゼロトークンのままです。横のパネルではジョブ全体を比較しており、コストログが1倍、プロバイダーの請求額が約6倍で、「I used the provider&apos;s bill as the reference for reconciliation.」（突き合わせの基準にはプロバイダーの請求額を使った）という注記があります。&quot; data-astro-cid-khyig4bw&gt;View full size&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;このジョブのために、既存の記録をバックアップしたうえで修正する、使い捨ての突き合わせスクリプトを書きました。スクリプトは各ページのタイムアウト回数を数え、説明のつかない残額（請求額からログの額を引いたもの）をそれらに均等に配分します。トークン数の推定には、コストの3分の1が入力、3分の2が出力という仮定の比率を使い、新しい数値には推定値であることを示す印を付けました。&lt;/p&gt;
&lt;p&gt;補正後の数値は今も概算です。より重要だったのは、今後のジョブでこうしたスクリプトが必要にならないよう、ツールキットを変えることでした。&lt;/p&gt;
&lt;h2 id=&quot;乖離の原因&quot;&gt;乖離の原因&lt;/h2&gt;
&lt;p&gt;ツールキットにあった3つの問題が重なっていました。&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;リクエストがブロッキング呼び出しでした。OpenAI、Google、Fireworksの呼び出しはストリーミングしておらず、Anthropicも一部のコードパスではストリーミングしていたものの、すべてではありませんでした。ブロッキング呼び出しでは、レスポンスとusageブロックは最後にまとめて届くか、まったく届かないかのどちらかです。そのため、29分目で切れたリクエストには、数えられるものが何も残りませんでした。&lt;/li&gt;
&lt;li&gt;リトライが記録されていませんでした。リトライデコレーターは失敗した試行をそのたびに破棄していたため、ツールキットがコストを計上できるのは常に最後の試行だけでした。&lt;/li&gt;
&lt;li&gt;長時間の推論が、30分のタイムアウトに何度も達していました。また、4つのプロバイダークライアントのうち3つでは、タイムアウトが明示的に設定されていませんでした。&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;4月に変えたこと&quot;&gt;4月に変えたこと&lt;/h2&gt;
&lt;p&gt;大きめの変更のときにいつもそうしているように、コードに手を付ける前に改善計画を書き、AIと一緒に何度か練り直しました（この進め方は『&lt;a href=&quot;https://www.rounakzoeb.com/ja/writing/spec-first-test-first-building-with-ai-coding-agents&quot;&gt;仕様ファースト、テストファースト&lt;/a&gt;』で書いています）。計画は3つの部分で構成されていました。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;4つのプロバイダーすべてでのストリーミング。すべてのプロバイダー呼び出しが、内部でストリーミングするようになりました。&lt;code&gt;Agent.run()&lt;/code&gt;の呼び出し側には従来と同じ結果オブジェクトが返り、新たに追加した&lt;code&gt;Agent.stream()&lt;/code&gt;でイベントを取得できます。チャンクを受信するたびにHTTPの読み取りタイマーがリセットされるので、アイドルタイムアウトが減ります。途中で失敗したストリームでも、それまでに届いたトークンは残ります。これは部分的な手がかりで、完全な使用量の報告にはなりません（最初のチャンクが届く前にリクエストがタイムアウトする可能性も残っています）。それでも、数えられるものが何も残らなかったブロッキング呼び出しに比べれば、はるかに良くなりました。&lt;/li&gt;
&lt;li&gt;クライアントのタイムアウトの明示的な設定。ストリーミングに加えるセーフティネットとして、読み取りタイムアウトを2時間にしました。&lt;/li&gt;
&lt;li&gt;リトライコストの計上。&lt;code&gt;RetryAccumulator&lt;/code&gt;がtenacityの&lt;code&gt;before_sleep&lt;/code&gt;コールバックにフックし、失敗した各試行を記録します。記録するのは、トークン数、コスト、トークン数の出どころ（&lt;code&gt;token_source&lt;/code&gt;）、エラー、所要時間です。実行の&lt;code&gt;cost&lt;/code&gt;にはそれらの試行が含まれ、&lt;code&gt;retry_count&lt;/code&gt;と&lt;code&gt;retry_cost&lt;/code&gt;を見れば、合計のうちどれだけがリトライによるものかがわかります。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;計画では、コードを書く前にいくつかの判断も確定させました。ストリーミングは常に有効とし、無効化するフラグは設けません。全体を切り替えるスイッチより、1つのプロバイダーに絞った修正のほうが保守しやすいからです。コストアラートはクライアントアプリケーション側に任せました。クライアントアプリケーションは、ストリームのイテレーションごとのコストイベントを監視して、早めに処理を止められます。&lt;/p&gt;
&lt;p&gt;計画では、失敗した試行についてのツールキットの計上ポリシーも定めました。後から追加した「ストリーム開始前の拒否」のルールも含めると、次のとおりです。&lt;/p&gt;
&lt;pre class=&quot;astro-code ai-and-sand&quot; style=&quot;background-color:#F6F0E7;color:#221E1B;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;python&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#5F534A;font-style:italic&quot;&gt;# The toolkit&amp;#39;s accounting policy for a failed attempt: (input tokens, output tokens) to bill.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1D2A5B&quot;&gt;def&lt;/span&gt;&lt;span style=&quot;color:#4338CA&quot;&gt; bill_failed_attempt&lt;/span&gt;&lt;span style=&quot;color:#221E1B&quot;&gt;(error, partial_output_tokens, input_estimate):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1D2A5B&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#4338CA&quot;&gt; rejected_before_stream&lt;/span&gt;&lt;span style=&quot;color:#221E1B&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#4338CA&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#221E1B&quot;&gt;):   &lt;/span&gt;&lt;span style=&quot;color:#5F534A;font-style:italic&quot;&gt;# an HTTP error response, or no connection was made&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1D2A5B&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#A12C17&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#221E1B&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A12C17&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#5F534A;font-style:italic&quot;&gt;                     # assumed not billed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1D2A5B&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#221E1B&quot;&gt; partial_output_tokens &lt;/span&gt;&lt;span style=&quot;color:#1D2A5B&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span style=&quot;color:#A12C17&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#221E1B&quot;&gt;:       &lt;/span&gt;&lt;span style=&quot;color:#5F534A;font-style:italic&quot;&gt;# generation had started&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1D2A5B&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#221E1B&quot;&gt; input_estimate, partial_output_tokens&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1D2A5B&quot;&gt;    if&lt;/span&gt;&lt;span style=&quot;color:#4338CA&quot;&gt; is_billable_error&lt;/span&gt;&lt;span style=&quot;color:#221E1B&quot;&gt;(&lt;/span&gt;&lt;span style=&quot;color:#4338CA&quot;&gt;error&lt;/span&gt;&lt;span style=&quot;color:#221E1B&quot;&gt;):        &lt;/span&gt;&lt;span style=&quot;color:#5F534A;font-style:italic&quot;&gt;# e.g. a timeout before any chunk arrived&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1D2A5B&quot;&gt;        return&lt;/span&gt;&lt;span style=&quot;color:#221E1B&quot;&gt; input_estimate, &lt;/span&gt;&lt;span style=&quot;color:#A12C17&quot;&gt;0&lt;/span&gt;&lt;span style=&quot;color:#5F534A;font-style:italic&quot;&gt;        # assume the prompt was read and billed&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span style=&quot;color:#1D2A5B&quot;&gt;    return&lt;/span&gt;&lt;span style=&quot;color:#A12C17&quot;&gt; 0&lt;/span&gt;&lt;span style=&quot;color:#221E1B&quot;&gt;, &lt;/span&gt;&lt;span style=&quot;color:#A12C17&quot;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;これらはプロバイダーの課金方法についての仮定であり、特定のプロバイダーが実際にいくら請求したかを証明するものではありません。HTTPのエラーレスポンスで拒否されたリクエストや、一度も接続できなかったリクエストは、コストがかからないものと仮定します。開始済みのストリームには、推定入力分に、届いた出力トークン分を加えたコストを計上します。最初のチャンクが届く前のタイムアウトのように、ツールキットが課金対象とみなすエラーには、推定入力分だけを計上します。推定値をもとにした数値には、後述するとおり、推定値であることを示すフラグを付けます。&lt;/p&gt;
&lt;p&gt;ストリーミングのブランチは4月18日にマージされました。それに先立つ第1フェーズでは、実装コードより先に受け入れ基準とプレースホルダーテストを書いています。その後1週間以内に、2つの追加修正が続きました。マージの5日後、各プロバイダーの最上位のエラーハンドラーがトークン数ゼロ・コストゼロを返しており、ストリームのコンシューマーが数えた分をすべて捨ててしまっていることに気づきました。約4分で途中終了したClaudeのストリームは、Anthropicからは課金されていたのに、ツールキットでは無料として記録されていました。2つの修正のうち1つ目では、エラーハンドラーが、ストリームのコンシューマーが数えた分を返すようにしました。その翌日には、アキュムレーターもフックも存在し、すべてのプロバイダー関数がアキュムレーターを受け取れるようになっていたにもかかわらず、それを生成する呼び出し元が1つもないことがわかりました。リトライの計上は事実上オフになっており、&lt;code&gt;cost&lt;/code&gt;は依然として成功した試行しかカバーしていませんでした。&lt;/p&gt;
&lt;p&gt;当初の6倍の食い違いに関係するのは、これら4月の変更、つまりストリーミング対応とその2つの追加修正です。&lt;/p&gt;
&lt;h2 id=&quot;その後見つかった計上の不具合&quot;&gt;その後見つかった計上の不具合&lt;/h2&gt;
&lt;p&gt;8月から10月にかけて、ツールキットのコストの記録方法に、さらに不具合が見つかりました。いずれも実際の誤りでしたが、4月の乖離の一因であることが示されたものはありません。3つのグループに分けられます。&lt;/p&gt;
&lt;h3 id=&quot;ツールキットが見ていなかったまたは数えていなかった試行&quot;&gt;ツールキットが見ていなかった、または数えていなかった試行&lt;/h3&gt;
&lt;p&gt;OpenAIとAnthropicのSDKは、デフォルトで失敗したリクエストを自らリトライします。これに気づいたのは、4月の作業から数か月後のことです。そのため、tenacityの5回の試行それぞれの裏に、最大2回のSDKリトライが隠れている可能性がありました。最悪の場合、リクエストは15回送られ、ツールキットに見えていたのはそのうち5回だけです。リリース0.4.0ではSDKクライアントを&lt;code&gt;max_retries=0&lt;/code&gt;で生成し、これまでSDKが処理していたステータスもtenacityでリトライするようにしました。最悪のケースは、15リクエストのうち5件しか見えない状態から、5リクエストすべてが見える状態になりました。&lt;/p&gt;
&lt;p&gt;10月には、0.5.2で、4月の不具合が新たな形で現れたものを修正しました。途中で打ち切られたOpenAIの応答が変更なしのまま5回再送されて実行が失敗し、課金された5回のリクエストに対して&lt;code&gt;cost&lt;/code&gt;は0.0となっていたのです。&lt;/p&gt;
&lt;h3 id=&quot;失敗した試行の誤った料金計算&quot;&gt;失敗した試行の誤った料金計算&lt;/h3&gt;
&lt;p&gt;9月には、ドキュメントのレビューで、&lt;code&gt;retry_cost&lt;/code&gt;が常に0.0であることが判明しました。フックは失敗した試行ごとのトークン数を記録していましたが、コストは、途中で切れたストリームに対してどのプロバイダーもゼロを設定するフィールドからコピーしていました。リリース0.4.0では、各試行を、実際に提供されたティアとキャッシュ分を反映したうえでレジストリの単価で計算し、入力トークン数が送信前の推定値である場合は、推定値としてフラグを立てます。&lt;/p&gt;
&lt;p&gt;同じリリースでは、逆方向の誤りも修正しました。何もストリーミングされないうちにプロバイダーが拒否したリクエストに、推定入力分のコストが計上されていたのです。現在はコストゼロとなり、&lt;code&gt;rejected_before_stream&lt;/code&gt;のラベルが付きます。コストの過大報告もまた、計上のバグです。それ以前にも、リリース0.3.0で、リトライを二重に数えていたレポートの合計を修正していました。リトライのコストをすでに含んだコストに、さらにリトライのコストを上乗せしていたのです。&lt;/p&gt;
&lt;h3 id=&quot;プロバイダーの課金と合っていなかった料金と使用量&quot;&gt;プロバイダーの課金と合っていなかった料金と使用量&lt;/h3&gt;
&lt;p&gt;8月には、ツールキットがリクエストごとにコストを記録するようにしました。現在、実行のコストは各リクエストのコストの合計です。段階制の料金は線形ではなく、トークンを合算すると、どの単独のリクエストも超えていない料金のしきい値を超えてしまうことがあるからです。その後の修正で、同じルールを失敗した試行にも適用しました。それまでは、完了済みの150kトークンと160kトークンの2つのリクエストを1つとして料金計算していたため、コストが2倍になっていました。&lt;/p&gt;
&lt;p&gt;ほかの食い違いは細部にありました。ストリーミングで送ったGeminiのpriorityリクエストは、標準料金で計算され、ダウングレードされたとしてフラグが立てられていました。ツールキットが、ストリーミングのエンドポイントからは決して送られてこないヘッダーから、提供されたティアを読み取っていたためです。Anthropicのキャッシュ書き込みは、リクエストしたキャッシュの有効期間 (TTL) で料金計算されており、実際に適用されたTTLが反映されていませんでした。リリース0.5.2では、Claudeの拒否応答 (refusal) の扱いを、Anthropicの課金方法に合わせて変更しました。拒否のカテゴリーによって、課金されるものとされないものがあるからです。続く0.5.4では、これまで一度も計上されていなかった、Geminiの画像モデル呼び出しにおけるthinkingトークンの料金を計算するようにしました。リリース時の確認では、これが呼び出しコストの約8分の1を占めていました。&lt;/p&gt;
&lt;figure class=&quot;figure&quot; data-astro-cid-khyig4bw&gt;&lt;div class=&quot;mat&quot; data-zoom=&quot;/_astro/cost_path-ja.DoLFL86F.png&quot; data-alt=&quot;リクエストのコストが通過する層を縦に積み重ねた図です。層は、SDKのリトライ層、ストリームコンシューマー、プロバイダーのエラーハンドラー、リトライフック、アキュムレーター、AgentResult、コストレポートです。各層の横には、そこで何が漏れていたかと、いつ修正したかを示しています。ストリームコンシューマー、エラーハンドラー、アキュムレーターは4月、リクエスト単位のコストは8月、レポートの合計は0.3.0、SDKのリトライとリトライフックは0.4.0、途中で打ち切られた応答は0.5.2です。&quot; data-astro-cid-khyig4bw&gt;&lt;picture data-astro-cid-khyig4bw=&quot;true&quot;&gt;&lt;img src=&quot;https://www.rounakzoeb.com/_astro/cost_path-ja.DoLFL86F_30Mda.png&quot; alt=&quot;リクエストのコストが通過する層を縦に積み重ねた図です。層は、SDKのリトライ層、ストリームコンシューマー、プロバイダーのエラーハンドラー、リトライフック、アキュムレーター、AgentResult、コストレポートです。各層の横には、そこで何が漏れていたかと、いつ修正したかを示しています。ストリームコンシューマー、エラーハンドラー、アキュムレーターは4月、リクエスト単位のコストは8月、レポートの合計は0.3.0、SDKのリトライとリトライフックは0.4.0、途中で打ち切られた応答は0.5.2です。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-khyig4bw=&quot;true&quot; width=&quot;3200&quot; height=&quot;1800&quot;&gt;&lt;/picture&gt;&lt;/div&gt;&lt;figcaption class=&quot;figure__caption small muted&quot; data-astro-cid-khyig4bw&gt;コストデータがツールキットを通過する途中で漏れていた箇所と、それぞれを修正した時期です。当初のインシデントに関係するのは4月の修正だけで、ほかは後から見つかりました。&lt;a class=&quot;figure__full&quot; href=&quot;https://www.rounakzoeb.com/_astro/cost_path-ja.DoLFL86F.png&quot; data-full-size data-alt=&quot;リクエストのコストが通過する層を縦に積み重ねた図です。層は、SDKのリトライ層、ストリームコンシューマー、プロバイダーのエラーハンドラー、リトライフック、アキュムレーター、AgentResult、コストレポートです。各層の横には、そこで何が漏れていたかと、いつ修正したかを示しています。ストリームコンシューマー、エラーハンドラー、アキュムレーターは4月、リクエスト単位のコストは8月、レポートの合計は0.3.0、SDKのリトライとリトライフックは0.4.0、途中で打ち切られた応答は0.5.2です。&quot; data-astro-cid-khyig4bw&gt;View full size&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;コスト追跡のテスト&quot;&gt;コスト追跡のテスト&lt;/h2&gt;
&lt;p&gt;この6か月（2026年4月〜10月）の間に、コスト追跡を、独自の仕様とテストを持つ1つの機能として扱うようになりました。&lt;/p&gt;
&lt;p&gt;テストでは、プロバイダーが実際に返す形のデータを使う必要があります。Google関連のあるバグでは、ツールキットがトークン内訳のキーを、enumに対して&lt;code&gt;str()&lt;/code&gt;を呼び出して生成していました。その結果、料金計算関数が&lt;code&gt;&amp;quot;AUDIO&amp;quot;&lt;/code&gt;を探しているところに&lt;code&gt;&amp;quot;MediaModality.AUDIO&amp;quot;&lt;/code&gt;が生成されていたため、音声トークンが1つも見つからず、Googleのツール呼び出しを伴う実行では、音声入力がテキストの料金で計算されていました。その間ずっと、料金エンジン自体のテストはグリーンのままでした。テストが、プロバイダー側のコードが実際には生成しない手書きのキーで、料金エンジンを呼び出していたからです。&lt;/p&gt;
&lt;p&gt;ゴールデンファイル（リクエストの固定比較）は、実際に送信される内容を固定します。ツールキットが何を送るかによって、プロバイダーが何に課金するかが決まります。サービスティア、キャッシュ制御、thinkingの設定です。ツールキットでは、リクエストボディのゴールデンファイルを100件以上管理しています。いずれも、実際の&lt;code&gt;Agent&lt;/code&gt;セッションを、送信されるリクエストボディをそのまま記録するクライアントに対して実行して生成したもので、ネットワークは一切使いません。変わらないはずのボディをコミットが変更すると、テストが差分付きで失敗します。ゴールデンファイルを最新に保つ作業は継続的に必要です。プロバイダーは時間とともにAPIを変更するからです。定期的に更新する方法を検討しています。&lt;/p&gt;
&lt;p&gt;網羅リスト (closed list) は、ツールキットを依存関係と足並みをそろえた状態に保ちます。あるテストでは、ツールキットが持つOpenAIのストリーム失敗コードの表を、SDK自身のリストと照合しています。SDKにコードが追加されると、そのコードをリトライするのか即座に失敗させるのかを誰かが判断するまで、このテストは失敗し続けます。&lt;/p&gt;
&lt;p&gt;小規模なライブ検証では、フィクスチャでは確かめられない前提を確認します。2026年9月23日のある検証（&lt;code&gt;gpt-5-nano&lt;/code&gt;が&lt;code&gt;gpt-image-2&lt;/code&gt;をホストする構成で、1024×1024、品質設定ごとに1回ずつ生成）では、OpenAIが公開している画像生成の品質別料金表が、ある品質設定では39%高すぎ、別の設定では40%低すぎました。現在ツールキットは、APIが報告する使用量 (usage) をもとにこれらの呼び出しの料金を計算しています。usageが報告されない場合に限って料金表にフォールバックし、そのときはコストを推定値としてフラグを立てます。&lt;/p&gt;
&lt;p&gt;推定値は推定値として明示します。すべての実行で、&lt;code&gt;token_source&lt;/code&gt;（&lt;code&gt;api_response&lt;/code&gt;、&lt;code&gt;stream_partial&lt;/code&gt;、&lt;code&gt;rejected_before_stream&lt;/code&gt;、&lt;code&gt;estimated&lt;/code&gt;のいずれか）と&lt;code&gt;cost_estimated&lt;/code&gt;フラグを報告します。計上ポリシー上は課金されたとみなす試行なのに、ツールキットがその使用量を計測できなかった場合は、$0.00ではなく、フラグ付きの推定値を報告します。このフラグが示すのは数値が推定値だということだけで、どれだけずれている可能性があるかは示しません。そのため、請求額との突き合わせは今でも行う価値があります。突き合わせを定常的に行えるよう、より自動化したチェックを検討しています。現在のコストモデルについては、&lt;a href=&quot;https://doc-processing-toolkit.vercel.app&quot;&gt;ツールキットのドキュメント&lt;/a&gt;で説明しています。&lt;/p&gt;
&lt;h2 id=&quot;llmのコスト追跡のためのチェックリスト&quot;&gt;LLMのコスト追跡のためのチェックリスト&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;コストログを、モデル別・日別に、定期的にプロバイダーの請求額と突き合わせましょう。請求額は合計しか示さない場合があるため、リクエスト単位の詳細は自分のログから得る必要があります。&lt;/li&gt;
&lt;li&gt;成功したものだけでなく、すべての試行を記録しましょう。失敗したリクエストでも、モデルが何らかの処理をしていれば課金されることがあります。&lt;/li&gt;
&lt;li&gt;リトライ層の数を把握しましょう。SDK、HTTPクライアント、自分のコードのすべてがリトライしている可能性があります。リトライ層は1つに絞り、それを記録しましょう。&lt;/li&gt;
&lt;li&gt;長い生成はストリーミングしましょう。ストリーミングするとアイドルタイムアウトが減り、ストリームが失敗しても部分的な使用量が残ります。ただし、完全な記録が保証されるわけではありません。&lt;/li&gt;
&lt;li&gt;リクエストごとに、実際に提供されたティアで料金を計算しましょう。合算したトークン数や、リクエストしたティアをもとに計算してはいけません。&lt;/li&gt;
&lt;li&gt;推定値は推定値として明示し、コスト計算のコードは、プロバイダーが実際に返す形のデータでテストしましょう。データの形は変わるので、常に最新の状態に保ちましょう。&lt;/li&gt;
&lt;/ul&gt;</content:encoded><category>LLMのコスト</category><category>オブザーバビリティ</category><category>テスト</category></item><item><title>仕様ファースト、テストファースト：AIコーディングエージェントでAIシステムを構築する方法</title><link>https://www.rounakzoeb.com/ja/writing/spec-first-test-first-building-with-ai-coding-agents</link><guid isPermaLink="true">https://www.rounakzoeb.com/ja/writing/spec-first-test-first-building-with-ai-coding-agents</guid><description>書面にした計画、回帰テスト、エージェントによるレビュー、実際のAPIに対する検証：コーディングエージェントを自分のプロジェクトにどう組み込んでいるか、そして何を自分の役割として残しているかを紹介します。</description><pubDate>Mon, 28 Sep 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;現在のプロジェクトのほとんどで、コーディングエージェントを使っています。方向性を定め、アーキテクチャを判断するのは私です。何を作りたいのか、それが何をする必要があり、何をする必要がないのかを決めます。そのうえで作業を計画し、エージェントが実装している間はその舵取りをし、変更をレビューし、何をリリースするかを決めます。ここでは、これまでにたどり着いたワークフローを、今も私自身が注意を払う必要がある部分も含めて紹介します。&lt;/p&gt;
&lt;p&gt;例の多くは、私が設計・構築・保守しているPython製のAIエージェントフレームワーク、&lt;a href=&quot;https://www.rounakzoeb.com/ja/work/doc-processing-toolkit&quot;&gt;doc-processing-toolkit&lt;/a&gt;から取っています。もともとはドキュメント処理エージェントのための基盤として始まり、現在はさまざまな種類のエージェントを構築するための汎用フレームワークになっています。Anthropic、OpenAI、Google、Fireworksのモデルを1つのインターフェースでアプリケーションに提供し、コスト追跡、MCPサーバーを通じたツールへの接続（MCP、つまりModel Context Protocolは、エージェントをツールに接続するための標準的な方法です）、長い会話をモデルの上限内に収めることなど、ほとんどのエージェントに必要な部分を担います。&lt;a href=&quot;https://doc-processing-toolkit.vercel.app&quot;&gt;ドキュメントサイト&lt;/a&gt;は公開しています。そのほかの例は、主にツールキットで構築したエージェントにツールを提供するために設計したMCPサーバーのライブラリ、&lt;a href=&quot;https://www.rounakzoeb.com/ja/work/doc-processing-std-tools&quot;&gt;doc-processing-std-tools&lt;/a&gt;と、&lt;a href=&quot;https://www.rounakzoeb.com/ja/work/japanese-flashcard-generator&quot;&gt;日本語の単語カード（フラッシュカード）アプリ&lt;/a&gt;から取っています。&lt;/p&gt;
&lt;p&gt;実装には主にClaude Codeを、レビューと批評には主にOpenAI Codexを使っています。ただし、Codexもコードを一部書きます。プルリクエストは、GitHub Copilotのクラウドエージェントが2回目のレビューを行うことがよくあります。&lt;/p&gt;
&lt;h2 id=&quot;リポジトリの整備&quot;&gt;リポジトリの整備&lt;/h2&gt;
&lt;p&gt;エージェントを取り巻くリポジトリの指示、ツール、チェックを、私はまとめてハーネスと考えています。ハーネスの役割は、各エージェントセッションに必要なコンテキストを与えることです。そうしておけば、結果が、私がプロンプトに何かを書き忘れたかどうかだけに左右されることはありません。&lt;/p&gt;
&lt;p&gt;私のリポジトリのほとんどには、Claude Codeが読み込む指示ファイル&lt;code&gt;CLAUDE.md&lt;/code&gt;が一式あります。ほかのエージェントであるCodexとGitHub Copilotは、同じファイルを&lt;code&gt;AGENTS.md&lt;/code&gt;として読み込みます。ツールキットには、メインのガイドに加えて、コードの構造、サポートするモデルの一覧、テスト、ドキュメントのそれぞれを扱う個別のガイドがあります。メインのガイドはルールとして書いています。その一部を挙げます。&lt;/p&gt;
&lt;pre class=&quot;astro-code ai-and-sand&quot; style=&quot;background-color:#F6F0E7;color:#221E1B;overflow-x:auto&quot; tabindex=&quot;0&quot; data-language=&quot;text&quot;&gt;&lt;code&gt;&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Model additions are JSON-only.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Costs are billed per request and by served tier.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Cached tokens are a subset of input tokens.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;A behaviour change a deployed client could notice ships with a kill switch.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Never push unless explicitly asked.&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;line&quot;&gt;&lt;span&gt;Stage an explicit file list. Never `git add -A` or `git add .`&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;平たく言えば、次のとおりです。モデルの追加は、データファイルにエントリーを1つ加えるだけで、新しいコードは書きません。コストはリクエストごとに、プロバイダーが実際に適用した料金ティアで計算し、キャッシュされた入力は入力の合計に含めて数えます。ツールキットをすでに使っているアプリケーションが気づきうる変更には、キルスイッチを付けます。キルスイッチとは、新しい挙動を再びオフにできる設定のことです。エージェントは、頼まれない限り共有リポジトリにプッシュせず、自分が変更したファイルだけを、1つずつ名前を指定してコミットします。&lt;/p&gt;
&lt;p&gt;最後の2つは、同じリポジトリのコピーで複数のエージェントが同時に作業することがあるために設けています。各エージェントは自分のファイルだけをコミットし、自分が触っていないファイルは他者の作業中のものとして扱います。過去のインシデントから生まれたルールもあります。あるルールは特定のgitのショートカットを禁止していますが、それはこのショートカットが、すでにコミット済みだった機能を黙って元に戻してしまったことがあるからです。&lt;/p&gt;
&lt;p&gt;git worktreeもよく使います。worktreeとは、同じリポジトリの追加の作業コピーで、それぞれが専用のブランチを持ちます。そのため、別々のworktreeで作業するエージェントは、互いのファイルに触れることなく同時に動けます。&lt;/p&gt;
&lt;p&gt;ガイドには、コードを見ただけではわからない設計上の事柄も記録しています。たとえば、エージェントとモデルのやり取りを回すコードは、プロバイダーごとにほぼ同一のコピーが4つ存在します。そのためガイドには、このコードを変更するときは4つすべてに反映しなければならないと書いてあります。そうしないと、1つのプロバイダーでは動くのに、ほかのプロバイダーでは気づかれないまま動かない、ということになります。4つのコピーは後で統合すると決めており、ガイドには、別のタスクの副作用としてそれを始めないようにと書いています。&lt;/p&gt;
&lt;p&gt;ドキュメント整備エージェントもあり、3つのツールそれぞれに用意しています。2026年2月にGitHub Copilotのエージェントとして始まり、4月にClaude CodeとCodexへ移植しました。コードを変更した後、このエージェントは、コード内の説明が今もコードと一致しているかを確認し、フォーマットを整え、警告なしでビルドできるまでドキュメントサイトを再ビルドし、READMEとAIツール向けのドキュメントバンドルを再生成し、ユニットテストを実行します。&lt;/p&gt;
&lt;p&gt;先送りすると決めた作業は、番号付きの技術的負債の台帳に登録します。台帳は、既知の暫定対応と先送りした作業をまとめた一覧で、先送りしたものが失われないようにしています。&lt;/p&gt;
&lt;figure class=&quot;figure&quot; data-astro-cid-khyig4bw&gt;&lt;div class=&quot;mat&quot; data-zoom=&quot;/_astro/harness_map-ja.v2JWKyEf.png&quot; data-alt=&quot;題が「リポジトリこそがハーネス」、副題が「リポジトリの指示、ツール、チェックが、各セッションに必要なコンテキストを与えます。」の図です。リポジトリの内側には、コード、テスト、ドキュメントを表す中央のノードを囲んで、CLAUDE.mdの階層（ルートガイドと、ソースアーキテクチャ、モデルレジストリ、テスト、ドキュメント向けのスコープ別ガイド）、Claude Code・Codex・Copilot向けに定義したドキュメント整備エージェント、技術的負債の台帳、ゲート（ユニットテスト、ruff format、mkdocs build --strict）があります。リポジトリの外側には、バッチごとに実装される計画と、実装記録があります。&quot; data-astro-cid-khyig4bw&gt;&lt;picture data-astro-cid-khyig4bw=&quot;true&quot;&gt;&lt;img src=&quot;https://www.rounakzoeb.com/_astro/harness_map-ja.v2JWKyEf_Z1AzpVh.png&quot; alt=&quot;題が「リポジトリこそがハーネス」、副題が「リポジトリの指示、ツール、チェックが、各セッションに必要なコンテキストを与えます。」の図です。リポジトリの内側には、コード、テスト、ドキュメントを表す中央のノードを囲んで、CLAUDE.mdの階層（ルートガイドと、ソースアーキテクチャ、モデルレジストリ、テスト、ドキュメント向けのスコープ別ガイド）、Claude Code・Codex・Copilot向けに定義したドキュメント整備エージェント、技術的負債の台帳、ゲート（ユニットテスト、ruff format、mkdocs build --strict）があります。リポジトリの外側には、バッチごとに実装される計画と、実装記録があります。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-khyig4bw=&quot;true&quot; width=&quot;3200&quot; height=&quot;1800&quot;&gt;&lt;/picture&gt;&lt;/div&gt;&lt;figcaption class=&quot;figure__caption small muted&quot; data-astro-cid-khyig4bw&gt;ツールキットのリポジトリにある、エージェントが読み込むものと、その作業が通過しなければならないチェック。&lt;a class=&quot;figure__full&quot; href=&quot;https://www.rounakzoeb.com/_astro/harness_map-ja.v2JWKyEf.png&quot; data-full-size data-alt=&quot;題が「リポジトリこそがハーネス」、副題が「リポジトリの指示、ツール、チェックが、各セッションに必要なコンテキストを与えます。」の図です。リポジトリの内側には、コード、テスト、ドキュメントを表す中央のノードを囲んで、CLAUDE.mdの階層（ルートガイドと、ソースアーキテクチャ、モデルレジストリ、テスト、ドキュメント向けのスコープ別ガイド）、Claude Code・Codex・Copilot向けに定義したドキュメント整備エージェント、技術的負債の台帳、ゲート（ユニットテスト、ruff format、mkdocs build --strict）があります。リポジトリの外側には、バッチごとに実装される計画と、実装記録があります。&quot; data-astro-cid-khyig4bw&gt;View full size&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;判断は計画に書く&quot;&gt;判断は計画に書く&lt;/h2&gt;
&lt;p&gt;大きめの作業は、書面にした計画から始めます。最初の草稿は私が書き、その後エージェント（たいていはClaude Code）と一緒に、確定するまで練り上げます。各版のレビューでは、HTMLのコメントタグ（&lt;code&gt;&amp;lt;!-- … --&amp;gt;&lt;/code&gt;）の中にコメントを書いてファイルそのものに書き込み、エージェントがそれを受けて計画を改訂します。変更点をプロンプトで説明するよりも、この方がうまくいきます。エージェントはgrepのようなテキスト検索ですべてのコメントを見つけられますし、各コメントは対象のテキストのすぐ横にあるので、エージェントはその文脈も一緒に受け取れるからです。長い内容をレビューするときは、タイピングでは時間がかかりすぎるため、コメントを音声で録音してエージェントに渡すこともあります。エージェントはそれを文字起こしし、コメントに対応します。計画には、問題とその根拠、設計、変更するコードの部分、リスク、テストの方法、完了をどう判断するかを書きます。また、コードを書く前に私が答えておくべき質問も列挙し、それ以外にもエージェントが確信を持てないことがあれば、必ず私に持ってくるようにしています。計画が確定したら、エージェントが進捗トラッカーを用意し、作業をバッチごとに進めます。&lt;/p&gt;
&lt;p&gt;一例として、『&lt;a href=&quot;https://www.rounakzoeb.com/ja/writing/the-6x-gap-logged-vs-billed-llm-cost&quot;&gt;6倍の乖離&lt;/a&gt;』で書いたコストの問題の後に立てた計画があります。この修正では、ツールキットがモデルの応答を受け取る方法を変え、ストリームとして少しずつ受け取るようにしました。そうすれば、途中で失敗したリクエストでも、何を使ったかの部分的な記録が残ります。計画では、コードを書く前に、ストリーミングは常に有効とし、無効にする設定は設けないこと、高額なコストに関するアラートはツールキットの上に構築されたアプリケーションに任せることを確定させました。計画には、作業を担うサブエージェント向けの指示書も添えました。サブエージェントとは、メインのセッションがタスクを引き渡す、別のエージェントセッションのことです。&lt;/p&gt;
&lt;p&gt;サブエージェントは、妥当な場面ではどこでも使っています。サブエージェントはそれぞれ自分のコンテキスト（エージェントセッションの作業用の記憶）で動くので、私が一緒に作業しているメインのエージェントは、自分のコンテキストを作業の調整に使えます。調整役を置かずに、1つのエージェントを同じタスクで何度も繰り返し実行する構成（「Ralph loop」の考え方）も試し、それを検証するためにGitHub Copilotのカスタムエージェントもいくつか作りました。最終的には、1つのメインのエージェントが、作業に必要な数のサブエージェントを調整する形に落ち着きました。大規模な調査タスクでは、Claude Codeのワークフローがとても役立っています。メインのエージェントが、ほかのエージェントの調整を小さなプログラムとして書くものです。&lt;/p&gt;
&lt;p&gt;ツールキットのあるリリース（0.5.4）では、計画が何回かのレビューラウンドを経ており、私の回答は計画内の表に記録されています。たとえば、すべてを1つのリリースで出すこと、残りのセキュリティ項目は次のリリースに回すこと、ツールキットの出力に対する3件の修正はキルスイッチなしで出すことを決めました。&lt;/p&gt;
&lt;p&gt;単語カードアプリの計画には、私の未解決の質問、すでに対象外と決めた事項（蒸し返されないように）、そしてエージェント向けの固定ルールを書きました。force-pushしない（すでに共有された作業を上書きしてしまうことがあるため）、APIキーを保存しているファイルに触れない、名前を指定したファイルだけをコミットする、支出上限に達したら止まる、というものです。こうしたルールは、フックで強制することもできます。フックとは、エージェントがツールを使う前後に自動で実行されるスクリプトのことです。操作の前に実行されるフックはその操作をブロックできるので、エージェントが特定のファイルを読んだり、特定の操作をしたりするのを止められます。&lt;/p&gt;
&lt;p&gt;このように計画するのは、作業の途中で未解決の質問に出くわしたエージェントは、たいていもっともらしい答えを選んでそのまま進めてしまい、その選択は大量の変更行の中では見落としやすいからです。先に質問に答え、新たな質問はエージェントに挙げさせることで、判断が文書として残り、結果をそれと照らし合わせて確認できます。&lt;/p&gt;
&lt;h2 id=&quot;ずれを検知するテスト&quot;&gt;ずれを検知するテスト&lt;/h2&gt;
&lt;p&gt;理にかなう場合は、テストをコードより先に、またはコードと同時に書き、挙動がずれたとき、つまり誰も意図していないのに挙動が変わったときに失敗するように設計します。&lt;/p&gt;
&lt;p&gt;ストリーミングへの変更では、新しいコードがまだ存在しない段階で、第1フェーズで91件の受け入れ基準（完成した作業が満たすべき条件）と74件のプレースホルダーテストを書きました。後続のフェーズで、それぞれのプレースホルダーを本物のテストに置き換えました。&lt;/p&gt;
&lt;p&gt;ゴールデンファイルは、特定のモデルとオプションの組み合わせに対して、ツールキットがプロバイダーに送信する内容をそのまま保存したものです。ツールキットには112件のゴールデンファイルがあり、記録されたリクエストは1,000件を超えます。既存モデルに対して送信する内容が少しでも変わると、テストが失敗し、その差分が表示されます。Anthropicに送信するリクエストに比較的大きな変更を加える前には、1つのコミットで既存のリクエストを先に保存し、変更をそれと照らし合わせて確認できるようにしました。0.5.4のリリースでモデルを追加した際には、ゴールデンファイルを再生成すると新しいファイルが9件作られ、既存のファイルは1件も変わりませんでした。これらの記録を最新に保つ作業は継続的に必要です。プロバイダーはAPIを変更するので、記録と、ユニットテストが使う記録済みの応答は、プロバイダーが現在受け付ける内容や返す内容と一致していなければならないからです。&lt;/p&gt;
&lt;p&gt;一部のテストは、ある機能を持つモデルをすべて名前で列挙しています。Claude Haiku 5.5を追加したときには、新しいモデルを各リストに加えるまで、そのうち3つのテストが失敗しました。これは意図したとおりの動きです。この失敗によって、新しいモデルがそれぞれのリストに入るべきかどうかを、誰かが判断することになるからです。似たテストとして、プロバイダーの公式Pythonライブラリが、ツールキットでまだテストしていない新しいメジャーバージョンになっていると失敗するものもあります。そのため、アップグレードが確認されないまま紛れ込むことはありません。&lt;/p&gt;
&lt;p&gt;バグ修正には、修正がなければ失敗する回帰テストを求めます。修正前には失敗し、修正後にはパスするテストなら、そのバグを確かに検出できるので、バグが再発したときにも捕まえられます。単語カードアプリのレビューラウンド（詳しい経緯は『&lt;a href=&quot;https://www.rounakzoeb.com/ja/writing/keeping-an-llm-app-current&quot;&gt;LLMアプリを最新に保つ&lt;/a&gt;』に書いています）では、5件の新しいテストのそれぞれが、対応する修正がなければ失敗し、修正があればパスしました。&lt;a href=&quot;https://www.rounakzoeb.com/ja/work/doc-processing-std-tools&quot;&gt;doc-processing-std-tools&lt;/a&gt;のあるリリースでは、独立したレビュアーが、修正前のバージョンで8件の新しいテストが失敗することを確認しました。&lt;/p&gt;
&lt;p&gt;テストガイドには、リリースごとのユニットテストの結果を記録しています。doc-processing-toolkit 0.5.4では、2026年10月9日に計測した、すべての変更が通過しなければならないユニットテストの結果は、9,834件がパス、8件がスキップでした。テストには、オプションの依存関係がインストールされていない場合など、特定の条件でスキップするよう印を付けることができます。ガイドはこの8件のスキップを基準とし、スキップ数がそれを大きく上回る場合は調査するよう書いています。テストが黙って無効化されている可能性があるからです。&lt;/p&gt;
&lt;p&gt;これらのユニットテストは、どのプロバイダーにも接続しません。プロバイダーの代わりにゴールデンファイルと、記録済みの応答や模擬の応答を使い、組み立てるリクエスト、モデルの一覧、コスト計算など、ツールキット自身のロジックを確認します。プロバイダーが実際にどう応答するかは、実際のAPIを呼び出すライブテストでカバーしており、それについては後述します。&lt;/p&gt;
&lt;h2 id=&quot;実装とレビュー&quot;&gt;実装とレビュー&lt;/h2&gt;
&lt;p&gt;メインのエージェント（私の場合はたいていClaude Code）が、計画をバッチごとに実装します。各バッチは、実装、ユニットテストの実行、実モデルに対する簡単なライブテスト、コミットという同じ順序で進めます。リリースの準備ができたら、プルリクエストをレビューにかけます。&lt;/p&gt;
&lt;p&gt;エージェント同士は互いに呼び出し合えます。Claude Code向けにはCodexとやり取りするためのスキルを、Codex向けにはClaude Codeとやり取りするためのスキルを用意しています（スキルとは、エージェントが必要なときに読み込む、ひとまとめにした指示のことです）。どちらも、Copilotのコマンドラインツールを通じて、GitHub Copilotのレビューエージェントにレビューを依頼することもできます。&lt;/p&gt;
&lt;p&gt;プルリクエストのレビューは、別のエージェント（Claudeのサブエージェント、Codex、またはCopilot）が行います。各指摘はまず再現し、そのうえで、修正するか、回答するか、後のために記録するかを私が決めます。doc-processing-std-toolsでは、ツールがプライベートネットワークのアドレスにアクセスするのを防ぐセキュリティチェックが、Python自身が持つ予約済みアドレスの一覧に依存しており、Python 3.10ではその一覧が一部のアドレスをパブリックとして扱うことを、Copilotが見つけました。修正では公式の一覧をコード内に書き込み、どのPythonのバージョンでも同じ結果になるようにしました。すべての指摘が修正につながるわけではありません。単語カードアプリでのある指摘には、対応すれば「数字をでっち上げることになる」という理由を添えて、修正の代わりに回答しました。&lt;/p&gt;
&lt;p&gt;CodexをClaudeの批評役として使うこともあれば、その逆もあります。8月には、あるリリースの出荷前に、Codexによるレビューを5回行いました。リリース全体のレビューが1回、領域ごとに分けたレビューが4回（プロバイダーとの接続とコスト計算、エージェントを実行するコード、新規モジュール、セキュリティ）です。修正は1つのコミットにまとめました。その1つは、リクエストがフォールバック経路をたどったときに失われていた、キャッシュされた入力の料金を元に戻す修正です。残りの指摘はツールキットのセキュリティ上の前提に関する問いだったため、私が判断する項目として技術的負債の台帳に登録しました。&lt;/p&gt;
&lt;p&gt;doc-processing-std-toolsのあるリリース（0.6.1）では、3つの新しいエージェントセッションが、何も編集せずに順番に変更をレビューしました。1回目のレビューでは、承認なしにエージェントがデスクトップを操作するのを防ぐための確認ステップを、すり抜ける方法が見つかりました。2回目のレビューでは修正を確認し、同じ入力をより長く表記すると、まだすり抜けられることがわかりました。3回目のレビューでは、リリースはマージ可能と報告されました。レビューを何回重ねても、コードが正しいことの証明にはなりません。この例では、2回目のレビューが最初の修正を検証したからこそ、その穴が見つかったのです。&lt;/p&gt;
&lt;p&gt;ドキュメントの見直しでは、古くなった記述だけでなく、コードのバグが見つかることもあります。9月には、ドキュメントの見直しによって、再試行したリクエストのコストを表すツールキットの数値が、ずっとゼロだったことが判明しました。&lt;/p&gt;
&lt;h2 id=&quot;実際のapiに対する検証&quot;&gt;実際のAPIに対する検証&lt;/h2&gt;
&lt;p&gt;ユニットテストはオフラインで実行します。ライブテストは、どのモデルをテストするかを選ぶスクリプトを通じて、プロバイダーの実際のAPIを呼び出します。毎回すべてのモデルをテストすると、費用は13倍になります。&lt;/p&gt;
&lt;p&gt;ライブ検証には、プロジェクトごとに予算を設けています。単語カードアプリの計画では、アプリのコスト表示がUS$2を超えた場合、または1回の実行がUS$0.50を超えた場合は、作業を止めて確認するようエージェントに指示していました。アプリの実際のAPIに対するライブテストは、レビュー対応後の再実行を含めて合計US$0.32でした。doc-processing-toolkit 0.5.4のライブ検証は約1ドルで、リリース記録には検証ごとの内訳が残っています。これらの上限は、こうした小規模なプロジェクトに合わせたものです。ほかのプロジェクトにも通用するのは、有料の呼び出しを始める前に、上限と、エージェントが止まって確認するポイントを決めておくという点です。&lt;/p&gt;
&lt;p&gt;ライブ検証は、モック（プロバイダーのAPIの代わりに使う模擬的なもの）では見つけられない問題を見つけます。doc-processing-toolkit 0.5.4のライブ検証では、Geminiの画像モデルがJPEG画像を返すのに、コードはすべてのファイルを&lt;code&gt;.png&lt;/code&gt;として保存していたことがわかりました。&lt;/p&gt;
&lt;p&gt;2026年10月9日の検証では、Googleの&lt;code&gt;gemini-nano-banana-2.1&lt;/code&gt;モデルに、60 MBのクリップを1つのリクエストに含めて送信しました。Googleの&lt;a href=&quot;https://ai.google.dev/gemini-api/docs/file-input-methods&quot;&gt;ファイル入力ガイド&lt;/a&gt;では、この方法で最大100 MB（PDFは50 MB）まで送信できるとされていますが、計画では古いページにあった20 MBという上限を前提にしていました。ツールキットは安全マージンとして、20 MBを超えるとGoogleの別のファイルアップロードサービスに切り替える仕組みを今も維持しています。その仕組みは、ドキュメントの&lt;a href=&quot;https://doc-processing-toolkit.vercel.app/concepts/multimedia/&quot;&gt;Multimediaのページ&lt;/a&gt;で説明しています。&lt;/p&gt;
&lt;p&gt;同じく10月9日に&lt;code&gt;claude-haiku-5-5&lt;/code&gt;で行った別の検証では、モデルがトークンをどう数えるかを計測しました。トークンは、プロバイダーが課金の単位とし、モデルの上限もその単位で測られるものです。約101,000トークンになると見込んでいた、繰り返しテキストからなる737,000文字のプロンプトは、264,555トークンになりました。このモデルは、この種の繰り返しの多いテキストを、1トークンあたり約2.8文字で数えるためです。そのため、文字数を基準にサイズを決めたプロンプトは、想定よりはるかに高くついたり、まったく収まらなかったりすることがあります。&lt;/p&gt;
&lt;figure class=&quot;figure&quot; data-astro-cid-khyig4bw&gt;&lt;div class=&quot;mat&quot; data-zoom=&quot;/_astro/release_loop-ja.CKA3LkKb.png&quot; data-alt=&quot;1回のリリースにおける10ステップのループです。計画を書く、コードより前に私にしか答えられない質問に答える、テストとゴールデンファイルを書く、バッチを実装する、オフラインのユニットテストのゲートを実行する（doc-processing-toolkit 0.5.4では9,834件がパス、8件がスキップ）、上限付きのライブスモークテストを実行する、コミットする、プルリクエストのレビューラウンド（Copilot、その後に独立したエージェント）、実装記録を書く、マージとタグ付け（私が行う）。ステップ4〜7はバッチごとに繰り返され、次のリリースではループが最初から再開します。&quot; data-astro-cid-khyig4bw&gt;&lt;picture data-astro-cid-khyig4bw=&quot;true&quot;&gt;&lt;img src=&quot;https://www.rounakzoeb.com/_astro/release_loop-ja.CKA3LkKb_2fW7EK.png&quot; alt=&quot;1回のリリースにおける10ステップのループです。計画を書く、コードより前に私にしか答えられない質問に答える、テストとゴールデンファイルを書く、バッチを実装する、オフラインのユニットテストのゲートを実行する（doc-processing-toolkit 0.5.4では9,834件がパス、8件がスキップ）、上限付きのライブスモークテストを実行する、コミットする、プルリクエストのレビューラウンド（Copilot、その後に独立したエージェント）、実装記録を書く、マージとタグ付け（私が行う）。ステップ4〜7はバッチごとに繰り返され、次のリリースではループが最初から再開します。&quot; loading=&quot;lazy&quot; decoding=&quot;async&quot; data-astro-cid-khyig4bw=&quot;true&quot; width=&quot;3200&quot; height=&quot;1800&quot;&gt;&lt;/picture&gt;&lt;/div&gt;&lt;figcaption class=&quot;figure__caption small muted&quot; data-astro-cid-khyig4bw&gt;計画からタグ付けまで、1回のリリースのループ。&lt;a class=&quot;figure__full&quot; href=&quot;https://www.rounakzoeb.com/_astro/release_loop-ja.CKA3LkKb.png&quot; data-full-size data-alt=&quot;1回のリリースにおける10ステップのループです。計画を書く、コードより前に私にしか答えられない質問に答える、テストとゴールデンファイルを書く、バッチを実装する、オフラインのユニットテストのゲートを実行する（doc-processing-toolkit 0.5.4では9,834件がパス、8件がスキップ）、上限付きのライブスモークテストを実行する、コミットする、プルリクエストのレビューラウンド（Copilot、その後に独立したエージェント）、実装記録を書く、マージとタグ付け（私が行う）。ステップ4〜7はバッチごとに繰り返され、次のリリースではループが最初から再開します。&quot; data-astro-cid-khyig4bw&gt;View full size&lt;/a&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;h2 id=&quot;実装記録&quot;&gt;実装記録&lt;/h2&gt;
&lt;p&gt;各リリースは、実装記録で締めくくります。記録には、適用した判断、項目ごとの作業内容とそのコミット、テストのエビデンス、コストを付記したライブ検証、計画からの逸脱、見つかった問題、フォローアップを書きます。検証していないものには「not verified」と記すので、記録の中では、計測したことと仮定したことが区別されます。&lt;/p&gt;
&lt;p&gt;記録そのものも訂正されます。ある記録では、ライブ検証が、シミュレートしたキー入力を、それを送信したツールが成功と報告したことを理由に成功と数えていましたが、実際にはキー入力は無視されていました。レビュー後、キー入力そのものを監視する検証に修正しました。別の記録では、プルリクエストの説明がユーザーインターフェースの確認結果を誇張しており、記録にはその訂正が記されています。&lt;/p&gt;
&lt;p&gt;全体のアーキテクチャとコードについての最終的な承認は、私が行います。&lt;/p&gt;
&lt;h2 id=&quot;今も私の判断が必要なこと&quot;&gt;今も私の判断が必要なこと&lt;/h2&gt;
&lt;p&gt;コードの多くはエージェントが書き、エージェントのレビューはしばしば設計上の問いを投げかけます。それでも、判断を下すのは私です。その出発点は、クライアントが何を必要としているかを理解し、全体のビジョンを定め、何を優先し、物事がどう動くべきかを決めることです。エージェントが実装を担う部分が増えるにつれ、私の役割のうちこの部分は以前より重要になっています。コードの1行1行をすべて説明できる必要はありませんが、システムがどう動くのか、そしてほかの選択肢と比べてなぜその設計を選んだのかは、説明できなければなりません。&lt;/p&gt;
&lt;p&gt;ツールキットでは、判断の対象にアーキテクチャが含まれます。各プロバイダーをどう接続するか、モデルの一覧をデータとして持つこと、各リクエストのコストをどう計算するか、フックと長い会話の短縮をどこに組み込むか、です。スコープも含まれます。何をリリースに含め、何を技術的負債の項目とし、何を対象外とするか、ということです。たとえば、新しいGemini APIへの移行は見送り、ツールキットの2つの機能（ハーネスのフックとスケジュール実行のエージェント）は先送りし、ストリーミングは無効にできないようにすると決めました。&lt;/p&gt;
&lt;p&gt;セキュリティのトレードオフも私が判断します。ツールキットのMCPサーバーは、ユーザーが信頼するマシン上で動くように設計されています。私はその制約を文書化し、信頼できないクライアントにサーバーを公開しないよう警告を添えることにして、プロダクト自体はそのままにしました。どの指摘をリリースのブロッカーとし、どれを修正案の概略とともに台帳に登録するかも、私が決めます。ツールキットをすでに使っているアプリケーションが気づきうる変更は、私が適用除外としない限りキルスイッチ付きで出荷し、適用除外とした場合は変更履歴 (changelog) に記載します。&lt;/p&gt;
&lt;p&gt;作業の中には、楽にならなかった部分もあります。曖昧な要件は、エージェントがもっともらしいが誤った方向で決めてしまう前に、私が気づけるかどうかに今もかかっています。そして計画がカバーできるのは、私が思いついた質問だけです。レビューの指摘を判断するにはプロダクトの知識が必要です。コードについては正しい指摘でも、行うべき変更としては間違っていることがあるからです。実運用に近い評価は自動化が難しいものです。ユニットテストは記録済みの応答や模擬の応答に対して実行され、ライブ検証は上限付きの小さなサンプルにすぎません。そのため、トークン数や、Googleの実際のサイズ上限は、実際のAPIに対して初めて表に出ました。変更をシステム全体に組み込むことにも注意が必要です。小さく見える変更が、4つのプロバイダーすべて、すでに使われているアプリケーション、あるいはセキュリティ境界に影響しうるからです。&lt;/p&gt;</content:encoded><category>AIコーディングエージェント</category><category>コードレビュー</category><category>テスト</category></item></channel></rss>