<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>開発ツール</title><link>https://blog.ast.moe/tags/%E9%96%8B%E7%99%BA%E3%83%84%E3%83%BC%E3%83%AB/</link><description>Recent posts from 五里霧中</description><generator>Hugo</generator><item><title>コーディングエージェントに画像を溜め込まないためのVisual Storeを作った</title><link>https://blog.ast.moe/blog/2026-09-16/</link><pubDate>Wed, 16 Sep 2026 18:55:53 +0900</pubDate><guid>https://blog.ast.moe/blog/2026-09-16/</guid><description>&lt;p&gt;&lt;a href="https://github.com/ieee0824/visual-store"&gt;Visual Store&lt;/a&gt;というローカル画像ストレージを作りました。&lt;/p&gt;
&lt;p&gt;コーディングエージェントでUIの実装やブラウザーの描画を確認していると、スクリーンショットを何枚も撮ることがあります。その画像をすべて会話へ表示していると、画像そのものだけでなく、会話履歴のデータ量も増えていきます。&lt;/p&gt;
&lt;p&gt;今回は、画像を保存することと、AIに画像を見せることを分けるための仕組みを作りました。&lt;/p&gt;
&lt;h2 id="きっかけはcodexの長期セッション"&gt;きっかけはCodexの長期セッション&lt;/h2&gt;
&lt;p&gt;以前、Omoikaneの開発をCodexで長時間続けていたところ、デバッグ用のスクリーンショットが会話履歴へ大量に蓄積しました。&lt;/p&gt;
&lt;p&gt;そのときの調査内容は、&lt;a href="https://blog.ast.moe/blog/2026-09-11-2/"&gt;大量の無圧縮PNGを含むCodexの長期セッションでremote compactが失敗したので復旧した&lt;/a&gt;に書いています。&lt;/p&gt;
&lt;p&gt;有効なコンテキストを調べると、画像は次のような状態でした。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;PNG 40枚
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;画像本体 約42 MB
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;Base64 data URL 約56 MB
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;このPNGを画素を変えずに再圧縮したところ、画像本体は約277 KBまで縮みました。その後、同じセッションでremote compactを成功させることもできました。&lt;/p&gt;
&lt;p&gt;ただ、この方法はすでに会話履歴へ入ってしまった画像を後から修復するものです。そもそも、デバッグ画像を撮るたびに全部を会話へ入れる必要はありません。&lt;/p&gt;
&lt;p&gt;そこで、保存した画像を参照IDで管理し、必要になったときだけ一枚を取り出すことにしました。&lt;/p&gt;
&lt;h2 id="保存する画像と見る画像は別"&gt;保存する画像と見る画像は別&lt;/h2&gt;
&lt;p&gt;デバッグ中には、次の二つの要求があります。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;この画像は後で調べるために保存しておきたい
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;今この画像をAIに見せたい
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;これは同じではありません。&lt;/p&gt;
&lt;p&gt;例えばブラウザーのレンダリング比較で100枚のスクリーンショットを撮ったとしても、100枚をすべてその場でモデルへ渡す必要はありません。撮影した画像を残し、ラベルやフレーム番号から候補を絞り、差分がありそうな一枚だけ確認すればよい場合もあります。&lt;/p&gt;
&lt;p&gt;Visual Storeでは、次のような流れにします。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;画像を保存
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;参照IDだけ受け取る
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;一覧やメタデータから対象を絞る
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;必要な一枚だけ取得
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; ↓
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;その時点で初めて画像を表示
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;保存しただけでは画像を表示しません。CLIも画像本体やBase64を標準出力へ流さず、小さなJSONだけを返します。&lt;/p&gt;
&lt;p&gt;これは、画像ファイルを小さくすればモデルの画像トークンも同じ割合で減る、という話ではありません。画像の保存サイズと、モデルが画像を見たときのコンテキスト消費は別です。Visual Storeが扱うのは、保存した画像を無条件に会話へ入れないことです。&lt;/p&gt;
&lt;h2 id="visual-storeの構成"&gt;Visual Storeの構成&lt;/h2&gt;
&lt;p&gt;Visual StoreはRustで実装した単一のローカルCLIです。CLI名は&lt;code&gt;vstore&lt;/code&gt;です。&lt;/p&gt;
&lt;p&gt;メタデータはSQLiteで管理し、画像やVP9セグメントの物理データはローカルファイルシステムへ保存します。ネットワークサービスではなく、実行時にFFmpegやImageMagick、APIキー、ネットワーク接続も必要ありません。&lt;/p&gt;
&lt;p&gt;画像は物理ファイルのパスではなく、次のような論理参照で扱います。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;visual://&amp;lt;store_id&amp;gt;/images/&amp;lt;image_id&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;参照IDにはcodecや物理ファイルの場所を入れていません。画像の保存形式がPNGからVP9セグメントへ変わっても、画像の&lt;code&gt;image_id&lt;/code&gt;と参照は変わらないようにしています。&lt;/p&gt;
&lt;p&gt;SQLiteでは、観測した画像と、その画像の現在の保存表現を分けています。&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;images
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 観測そのもの。image_id、run、stream、frame_no、メタデータ
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;representations
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; 現在有効なPNGまたはVP9セグメント
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;segments / frame_locations
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; VP9セグメントと、その中のフレーム位置
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt;blobs
&lt;/span&gt;&lt;/span&gt;&lt;span style="display:flex;"&gt;&lt;span&gt; SHA-256で管理する物理データ
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;この分離があるので、保存形式の変更と観測履歴の変更を別に扱えます。&lt;/p&gt;</description></item></channel></rss>