Vision APIにコード画像を渡してバグを自動検出するスクリプトを書いた
Claude Opus 4.6のVision APIでコードのスクリーンショットからバグや問題点を自動検出する仕組みの実装と、実際に試してわかった精度の限界
Slackに「ここ動かないんですけど」とコードのスクリーンショットが貼られる。よくある光景だと思う。
テキストで送ってくれればそのまま読めるのに、なぜか画像で来る。IDE のダークテーマで撮った、行番号付きのスクリーンショット。これをいちいち目で読んで「あ、セミコロン抜けてますね」とか返すのが地味にだるかった。
それなら Vision API に食わせて自動で指摘させればいいのでは、と思って作ったのがこの仕組み。
やりたいことと構成
やることは単純で、コードのスクリーンショット画像を Claude Opus 4.6 の Vision API に渡し、「このコードにバグや問題があれば指摘して」と頼む。返ってきた指摘を構造化された JSON で受け取り、行番号と問題の概要を一覧にする。
import Anthropic from "@anthropic-ai/sdk";
import { readFileSync } from "node:fs";
const client = new Anthropic();
async function detectBugs(imagePath: string) {
const imageData = readFileSync(imagePath).toString("base64");
const ext = imagePath.split(".").pop()?.toLowerCase();
const mediaType =
ext === "png" ? "image/png" : "image/jpeg";
const response = await client.messages.create({
model: "claude-opus-4-6",
max_tokens: 4096,
messages: [
{
role: "user",
content: [
{
type: "image",
source: {
type: "base64",
media_type: mediaType,
data: imageData,
},
},
{
type: "text",
text: `この画像に写っているコードを読み取り、バグ・潜在的な問題・typoを検出してください。
結果は以下のJSON形式で返してください:
{
"language": "検出した言語",
"bugs": [
{
"line": "行番号(読み取れた場合)",
"severity": "error | warning | info",
"description": "問題の説明",
"suggestion": "修正案"
}
]
}
JSONのみ出力してください。`,
},
],
},
],
});
const text =
response.content[0].type === "text"
? response.content[0].text
: "";
return JSON.parse(text);
}
ポイントは画像をテキストより前に置くこと。Anthropic の公式ドキュメントにも「画像はテキストの前に配置したほうが精度が上がる」と書いてある。長いドキュメントをクエリの前に置くのと同じ理屈。
実際に食わせてみた結果
手元にあったスクリーンショットをいくつか試した。
意図的に仕込んだ典型的バグ(off-by-one、null チェック漏れ、未使用変数): ほぼ拾ってくれた。行番号付きのスクリーンショットなら、行番号も正確に返してくる。
typo 系(lenght、consloe.log): これも検出できた。OCR の精度が思ったより高く、変数名やメソッド名を正確に読み取っている。
ロジックのバグ(境界条件、非同期処理の競合): ここは怪しい。画像から読み取ったコードの断片だけでロジックの全体像を掴むのは難しいらしく、「この部分だけでは判断できないが、〜の可能性がある」という曖昧な指摘になることが多い。これはテキストで渡しても同じかもしれないが、画像経由だと余計にコンテキストが減る。
暗いテーマ・低解像度のスクリーンショット: ここが一番問題だった。Retina ディスプレイで撮った高解像度の画像ならまず読み取れるが、Slack で圧縮された JPEG や、フォントサイズが小さいスクリーンショットだと誤読が増える。0 と O、l と 1 の取り違えが実際に起きた。
画像サイズとコストの話
Vision API は画像を 28×28 ピクセル単位の「ビジュアルトークン」に変換して課金する。1000×1000px の画像で約 1,296 トークン。Opus 4.6 の入力は $5/MTok なので、画像 1 枚あたり約 $0.0065。出力側のトークンと合わせても、1 回の検出で $0.01〜0.02 程度。コスト面は問題にならない。
ただし、Opus 4.6 は「Standard」解像度ティアのモデルで、長辺 1,568px・最大 1,568 ビジュアルトークンにリサイズされる。4K スクリーンショットを送っても縮小されるので、細かい文字が潰れる可能性がある。送る前に自分でリサイズして確認するほうが確実。
// 画像を送る前にリサイズする例(sharpを使用)
import sharp from "sharp";
async function prepareImage(
inputPath: string,
maxLongEdge = 1568
): Promise<Buffer> {
const metadata = await sharp(inputPath).metadata();
const { width = 0, height = 0 } = metadata;
const longEdge = Math.max(width, height);
if (longEdge <= maxLongEdge) {
return readFileSync(inputPath) as unknown as Buffer;
}
return sharp(inputPath)
.resize({
width: width > height ? maxLongEdge : undefined,
height: height >= width ? maxLongEdge : undefined,
fit: "inside",
})
.png() // 可逆圧縮でテキストの劣化を防ぐ
.toBuffer();
}
JPEG で圧縮すると文字がにじむ。コードのスクリーンショットは PNG で送るのが無難。
Batch API で大量のスクリーンショットを処理する
CI で撮ったスクリーンショットをまとめてチェックしたい場合、Batch API を使えば入出力とも半額になる。Opus 4.6 なら入力 $2.50/MTok、出力 $12.50/MTok。
import Anthropic from "@anthropic-ai/sdk";
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
const client = new Anthropic();
async function batchDetect(imagePaths: string[]) {
const requests = imagePaths.map((path, i) => {
const data = readFileSync(path).toString("base64");
return {
custom_id: `bug-check-${i}`,
params: {
model: "claude-opus-4-6" as const,
max_tokens: 4096,
messages: [
{
role: "user" as const,
content: [
{
type: "image" as const,
source: {
type: "base64" as const,
media_type: "image/png" as const,
data,
},
},
{
type: "text" as const,
text: "このコード画像のバグを検出し、JSON形式で返してください。",
},
],
},
],
},
};
});
// JSONLファイルを作成してアップロード
const jsonl = requests
.map((r) => JSON.stringify(r))
.join("\n");
writeFileSync("/tmp/batch_requests.jsonl", jsonl);
// Batch APIで送信
const batch = await client.beta.messages.batches.create({
requests,
});
return batch.id;
}
数十枚のスクリーンショットをまとめて投げて、結果をポーリングで取得する。リアルタイム性は要らないケースに向いている。
わかった限界と使いどころ
試してみて、「使える場面」と「使えない場面」がはっきりした。
向いているのは、コードレビューのスクリーンショットから明らかな typo やシンタックスエラーを拾うケース。Slack に貼られた画像を Bot が自動で拾って「3 行目の lenght は length では?」と返す、くらいの使い方なら十分実用になる。
向いていないのは、ロジックのバグ検出。画像に写っているコードは断片でしかなく、型情報もインポート関係も見えない。テキストでコード全体を渡して静的解析したほうが圧倒的に精度が高い。Vision API を使うのは、テキストが手に入らない状況——つまり画像しかない場面に限るべき。
もう一つ、行番号が画像に含まれていないと「何行目」という指摘ができなくなる。IDE の行番号を表示した状態でスクリーンショットを撮る、というのが前提になる。
結局のところ、このスクリプトが一番役に立ったのは「Slack に貼られたスクリーンショットのトリアージ」だった。人間が見る前に明らかな問題を指摘してくれるだけで、やり取りが 1 往復減る。それだけでも作った甲斐はあった。