メインコンテンツへスキップ
#AI活用 約6分で読めます

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 系(lenghtconsloe.log: これも検出できた。OCR の精度が思ったより高く、変数名やメソッド名を正確に読み取っている。

ロジックのバグ(境界条件、非同期処理の競合): ここは怪しい。画像から読み取ったコードの断片だけでロジックの全体像を掴むのは難しいらしく、「この部分だけでは判断できないが、〜の可能性がある」という曖昧な指摘になることが多い。これはテキストで渡しても同じかもしれないが、画像経由だと余計にコンテキストが減る。

暗いテーマ・低解像度のスクリーンショット: ここが一番問題だった。Retina ディスプレイで撮った高解像度の画像ならまず読み取れるが、Slack で圧縮された JPEG や、フォントサイズが小さいスクリーンショットだと誤読が増える。0Ol1 の取り違えが実際に起きた。

画像サイズとコストの話

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 行目の lenghtlength では?」と返す、くらいの使い方なら十分実用になる。

向いていないのは、ロジックのバグ検出。画像に写っているコードは断片でしかなく、型情報もインポート関係も見えない。テキストでコード全体を渡して静的解析したほうが圧倒的に精度が高い。Vision API を使うのは、テキストが手に入らない状況——つまり画像しかない場面に限るべき。

もう一つ、行番号が画像に含まれていないと「何行目」という指摘ができなくなる。IDE の行番号を表示した状態でスクリーンショットを撮る、というのが前提になる。

結局のところ、このスクリプトが一番役に立ったのは「Slack に貼られたスクリーンショットのトリアージ」だった。人間が見る前に明らかな問題を指摘してくれるだけで、やり取りが 1 往復減る。それだけでも作った甲斐はあった。

参考リンク

#Claude #Vision API #バグ検出 #自動化 #TypeScript
シェア