自分が書いた1418行を3つのAIにレビューさせたら、実バグが11件出た 自分が書いた1418行を3つのAIにレビューさせたら、実バグが11件出た
はじめに
前に、ローカルLLMを dev-orchestra の 3 枚目のレビュアーとして組み込みました。 そのとき「次は実際の開発で回してみる」と書いたので、その回収です。
題材は自分のコードです。この一連の作業で書いたスクリプト類、 13 ファイル・1418 行。自分で書いて自分で動かしているだけで、 一度も他人のレビューを受けていません。
結果、11 件の実在するバグが出ました。 そのうち 3 件は、自力ではまず気づけなかったものです。
3 枚の内訳
レビュアーは互いの指摘が見えない状態で、同じ差分を並列に読みます。 出てきた指摘は私が全件トリアージしました。
| レビュアー | 提起 | 採用 | 却下 | 重複 |
|---|---|---|---|---|
| claude opus | 6 | 6 | 0 | 0 |
| codex gpt-6-sol | 6 | 4 | 0 | 2 |
| localllm qwen3.6-35B-A3B | 6 | 1 | 5 | 0 |
商用 2 枚は空振りゼロ。 codex の 2 件は claude と同じ箇所を別の言葉で書いたもので、 中身としては正しい指摘でした。
ローカルは 6 件中 1 件。 1 回のデータなので断定はしませんが、 中身を見ると傾向ははっきりしていました。後述します。
自力では気づけなかった 3 件
1. PowerShell の switch 内の continue はループに戻らない
対話モードで clear と打つと履歴をリセットする、という実装です。
while ($true) {
$line = Read-Host
switch ($line) {
'clear' { $messages.Clear(); continue } # ← ここ
}
$messages.Add(@{ role = 'user'; content = $line }) # 到達してしまう
...
}
switch の中の continue は、その switch を抜けるだけで、
外側の while には戻りません。結果、履歴をリセットした直後に
“clear” という文字列がそのままモデルに送られて、律儀に回答されていました。
指摘が言語仕様の話だったので、実際に再現させました。
[1] モデルに送信: msg1
[2] clear を検出 → continue
[2] モデルに送信: clear ← 指摘どおり
[3] モデルに送信: msg3
:repl ラベルを付けて continue repl にして直りました。
動いているように見えていたので、たぶん一生気づきませんでした。
2. ストリーミングで HTTP ステータスを見ていない
SSE を読む処理が、data: で始まる行だけを拾っていました。
エラー応答には data: 行がありません。JSON のエラーオブジェクトが返るだけです。
つまり**ループが一度も回らずに抜け、何も表示されないまま「成功」**します。
これが刺さったのは、指摘された当日にこの失敗を踏んでいたからです。 ローカルLLMにレビューさせようとして 400 が返ったとき、 出たメッセージは「HTTP Error 400: Bad Request」だけでした。 本当の原因(コンテキスト超過)はサーバが JSON で返していたのに、 こちらが読み捨てていたわけです。
修正後はこうなりました。
修正前: リクエストに失敗しました: HTTP Error 400: Bad Request
修正後: サーバがエラーを返しました: Context size has been exceeded.
3. レビュー結果を集計するスクリプト自体が壊れていた
これが一番こたえました。
3 枚の打率を記録するために、consolidated.json を読んで
レビュアーごとに集計するスクリプトを書いていました。
未トリアージの回を混ぜると数字が狂うので、警告を出す作りにしてあります。
untriaged = [r for r in rows if r["status"] == "untriaged"]
dev-orchestra が実際に入れる値は needs-triage でした。
untriaged という文字列はどこにも存在しません。
警告は永久に出ず、未トリアージの回を黙って記録し続けるところでした。 打率を測るための道具が、測定を歪める側に回っていたことになります。
しかもこれは、そのレビュー自体の出力を見れば確認できるバグでした。
consolidated.md に Triage: needs-triage と書いてあります。
ローカル LLM の却下 5 件
却下の中身を見ると、商用との差がはっきりしました。
自己矛盾している指摘(security, HIGH)
subprocess.Popenがshell=Trueでないのは良いが、スクリプトのパスが ハードコードされている。このパスが攻撃者に書き込み可能ならコードを注入できる。
自分で「ハードコードされている」と認めたうえで「もし動的なら危険」と論じています。 具体的な失敗経路がありません。
設計どおりの挙動を欠陥として挙げる(MEDIUM)
mainが stdin を全部メモリに読む。巨大な入力だとメモリ問題や タイムアウトを起こす可能性がある。
プロンプトを受け取る CLI なので、そういう設計です。
トレードオフを欠陥と呼ぶ(LOW, performance)
--sleep-idle-secondsを 43200 に設定している。アイドルだとモデルが降りるので、 次のリクエストでコールドスタートの penalty がある。
それは前回の記事で 意図して選んだトレードオフです。
共通するのは「〜な可能性がある」で終わっていることです。
商用 2 枚は逆でした。switch の continue がどう振る舞うか、
needs-triage という文字列が実際に入るか、確かめられる形で書いてきます。
採用した 1 件(エラー応答の本文を握り潰している件)は、まっとうな指摘でした。 重大度を CRITICAL としたのは過大でしたが、欠陥自体は実在します。
3 枚とも見逃した欠陥
一方で、全員が見逃したものがあります。しかも実害が大きいものでした。
修正後に掃除スクリプトをドライランで流したら、こう出ました。
[2] Lemonade Server を停止
[dry-run] 停止対象: llama-server (PID 89960)
このスクリプトは第1回で
旧環境を掃除するために書いたものです。当時 llama-server は
Lemonade が起動するバックエンドで、止めるのが正しい対象でした。
いま llama-server は、新環境そのものです。
$procs = Get-Process | Where-Object { $_.Name -match 'LemonadeServer|llama-server|ryzenai' }
-Execute していたら、掃除スクリプトが新環境を殺していました。
見逃されたのは当然とも言えます。コードは一行も変わっていません。 変わったのは「その名前が何を指すか」という、リポジトリの外側の事実です。 差分を読むレビューに、そこまで求めるのは酷でしょう。
静的に読むだけでは見つからないものがある、という当たり前の話でした。 実際に動かす価値は、レビューを 3 枚に増やしても消えません。
費用対効果
正直に書くと、面倒ではあります。
- 差分を絞る必要があった。記事や画像まで含めると 5535 行になり、無駄が多い
- 18 件を全部読んで採否を決めるのに、それなりの時間がかかる
- ローカル分は 5 件の却下を読む手間が増えるだけだった
それでも、11 件の実バグと引き換えなら安いと思いました。
とくに switch/continue と集計スクリプトの件は、
自分では発見の見込みが薄いものです。
3 枚目のローカルについては、**現時点では「無料の保険」**という位置づけです。 打率は低いですが、課金されないので却下の手間だけが原価です。 ただし数回まわして傾向が変わらなければ、外すことも考えます。
まとめ
- 自分のコード 1418 行に、実バグが 11 件あった
- 自力では気づけないものが 3 件。言語仕様、静かに失敗する経路、 そして測定する道具自体のバグ
- ローカル LLM は 6 件中 1 件。却下分は**「〜な可能性がある」で終わる**推測が多い
- 3 枚とも見逃した欠陥もある。 コードが変わらないまま、 名前の指す先が変わったケース。ドライランで見つかった
レビューを増やせば安心、という話ではありませんでした。 読ませるのと動かすのは別で、両方要ります。
次は、コンテキスト長を 16K から 64K に広げた件を掘ります。 第2回で 「KV キャッシュ量子化は f16 が収まるなら不採用」と判断しましたが、 その前提が長いコンテキストでも成り立つのかは、まだ測っていません。
関連記事 Related Posts
機密コードをクラウドに出さずにAIでレビューする(ローカルLLM × dev-orchestra) 機密コードをクラウドに出さずにAIでレビューする(ローカルLLM × dev-orchestra)
社外に出せないコードを、クラウドのAIに送らずにレビューする方法をまとめました。ローカルLLMをdev-orchestraのレビュアーに登録し、機密のものはローカルだけで、公開してよいものはクラウドのAIと併用します。ローカルLLMのレビューの実力と、呼び出し方ひとつでコードが外に出てしまう落とし穴も書きます。 社外に出せないコードを、クラウドのAIに送らずにレビューする方法をまとめました。ローカルLLMをdev-orchestraのレビュアーに登録し、機密のものはローカルだけで、公開してよいものはクラウドのAIと併用します。ローカルLLMのレビューの実力と、呼び出し方ひとつでコードが外に出てしまう落とし穴も書きます。
ローカルLLMをdev-orchestraのコードレビュアーに追加してみた ローカルLLMをdev-orchestraのコードレビュアーに追加してみた
自宅PCで動かしているQwenを、複数モデルで独立レビューするdev-orchestraの3枚目のレビュアーとして組み込みました。プロバイダがAPIではなくCLIをラップする設計だったので、薄いCLIを1枚挟んで繋いだ話です。翌日の公式対応と、Store版Pythonのパス罠についても追記しました。 自宅PCで動かしているQwenを、複数モデルで独立レビューするdev-orchestraの3枚目のレビュアーとして組み込みました。プロバイダがAPIではなくCLIをラップする設計だったので、薄いCLIを1枚挟んで繋いだ話です。翌日の公式対応と、Store版Pythonのパス罠についても追記しました。
thinking を切っていいのか測ったら、採点器のほうが2回壊れていた thinking を切っていいのか測ったら、採点器のほうが2回壊れていた
ローカルLLMのCLIでthinkingを既定で切っていました。速度だけ見て決めた判断の裏を取るため正答率を測ったら、同率でした。ただしそこに至るまでに、自分で書いた採点スクリプトのバグを2回踏んでいます。 ローカルLLMのCLIでthinkingを既定で切っていました。速度だけ見て決めた判断の裏を取るため正答率を測ったら、同率でした。ただしそこに至るまでに、自分で書いた採点スクリプトのバグを2回踏んでいます。