「AI が書いたコードは失敗を握り潰すか」を測った前の記事で、当方の検出器は 4 件を挙げて、当たり(真の握り潰し)は 0 件でした。公開後に読者が公開リポの gt.csv を数え直し、「成績表の片側しか書いていない」と指摘しました。当方の検出器は、コーパスに在った真の握り潰し 4 件を 1 件も拾っていなかったのです。
同じ読者が 30 行の別の検出器を提案しました。同じ Python 60 本に当てた結果を、今度は両側そろえて書きます。
結果を先に書きます(母集団=Python 60 本・公開リポ ai-silent-defect-scanner・results/gt.csv から再集計)。
| 出荷版の検出器 | 読者提案の検出器 | |
|---|---|---|
| 見る場所 |
except の中の return <既定値>
|
except の外の return <既定値>
|
| 在った欠陥 4 件のうち拾えた数(再現率) | 0 / 4 | 4 / 4 |
| 挙げた中で当たりだった数(精度) | 0 / 2 | 4 / 17 |
- 真の握り潰し 4 件(
py_fetch_json_s2/s3/s7/s9)は、全部ifガードの中で既定値を返していました。try/except の中には 1 件も無かったのです - 読者の検出器は 4 件を全部拾い、代わりに 13 件を一緒に拾いました。13 件の内訳は、対照課題 9・正当な既定値 2・適切に処理済み 2 です。対照課題 9 本を「外れ」と数える根拠は下に書きます
- 前の記事の「4 件 flagged」は Python 2 件+TypeScript 2 件で、真の 4 件は Python だけです。言語を混ぜた「0/4 と 0/4」は同じ 2×2 ではありません。この記事は Python 60 本に揃えます
用語は 2 つだけ使います。再現率=在った欠陥のうち拾えた割合。精度=挙げたもののうち当たりだった割合。英語では recall と precision です。
この記事の位置づけ(2026-09-26 追記)
- この記事で新しく示すこと: 同じ 60 本に、見る場所の違う検出器を当てると再現率が 0/4 から 4/4 に変わったこと(代わりに精度は 4/17)。
- 当てはまる範囲: ローカル LLM に書かせた Python 60 本(公開リポの
gt.csv)です。
何を測っていたか
コーパスは、小さなモデル(Qwen2.5-Coder 1.5B)に生成させた 120 本です。言語ごとに 6 課題 × 10 本で、Python 60・TypeScript 60。6 課題のうち 5 つは失敗経路のある入出力課題(JSON の取得・整数の変換・環境変数の読み取りなど)、1 つは失敗経路の無い純粋な計算課題(平均値)で、対照群として設計したものです。各本に、当方が読んで付けた裁定(human_verdict)と、機械で付けた形の列(subtype など)が gt.csv に入っています。
裁定の内訳(Python 60)です。
| 裁定 | 本数 | 意味 |
|---|---|---|
| loud_fail | 14 | 失敗を raise で外へ出す |
| proper | 9 | try/except で受けてログか再送出をする |
| problematic_fallback | 4 | 失敗の区別を消して既定値を返す(真の握り潰し) |
| legit_fallback | 2 | 既定値を返すが、docstring やコメントで契約として書いてある |
| not_adjudicated | 31 | 当方が個別に裁定していない(うち 22 は既定値を返さない bare・9 は対照課題の既定値) |
「握り潰し」の真の 4 件は、fetch_json 課題の 4 本です。HTTP が 200 でなければ None を返します。404・500・204 などの区別が呼び出し側から消えます(ネットワーク断は、requests.get が try の外で例外を上げるので、握り潰されません)。コメントには「失敗時は None」と書いてあるので、構文だけでは正当な既定値と区別できません。ここは前の記事の主題で、今回も変わりません。
検出器 1: 出荷版は「except の中」を見ていた
出荷版の規則は、except の本体が return <既定値> の 1 文だけのときに挙げます(厳格アンカー)。Python 60 本で挙がったのは py_parse_int_s3 と py_parse_int_s9 の 2 本。2 本とも docstring かコメントで「変換できなければ None」と書いてあり、裁定は legit_fallback です。精度 0/2。
真の 4 件はどれも try/except の外に居るので、この規則の射程に最初から入っていません。再現率 0/4。前の記事は精度の側だけを書き、再現率を書いていませんでした。読者の指摘はここです。
厳格アンカーそのものは、読者の再導出でも「残す価値がある」と出ました。アンカーを「本体のどこかに既定値の return がある」まで緩めると 5 本増えます(py_fetch_json_s6・py_first_line_s2・py_load_json_s4・py_load_json_s8・py_parse_int_s2)。5 本とも has_log=Y で、裁定は 5/5 proper でした。つまりアンカーは「ログが無ければ無言」を見分けるフィルタとして働いています。射程の穴とアンカーは別の境界です。
検出器 2: 読者の提案は「except の外」を見ていた
読者が提案した規則は 1 行で言えます。「except の中に無い return <既定値> を全部挙げる」。Python の標準 ast で 30 行です。
同じ 60 本に当てた結果が 17 本。当方も同じ規則を 30 行で書き直して再現し、読者の挙げた 4 件の行番号が 4 つとも一致しました。
| 17 本の内訳 | 本数 | 当たり/外れ | 根拠 |
|---|---|---|---|
| problematic_fallback | 4 | 当たり | 人手の裁定 |
| legit_fallback | 2 | 外れ | 人手の裁定(契約として書いてある既定値) |
| proper | 2 | 外れ | 人手の裁定(try/except で受けた上で、外側でも既定値を返す) |
| 対照課題 | 9 | 外れ | 課題の設計(下記) |
対照課題の 9 本を「外れ」と数える根拠
9 本は gt.csv では not_adjudicated(個別に裁定していない)です。それでも外れと数えるのは、この課題が「失敗経路の無い純粋な計算」として実験設計の時点で対照群に置かれているからです(公開リポの PROVENANCE.md「純計算対照 1 課題×10 本」)。9 本の既定値はすべて if not numbers: return 0 の形で、空リストの平均を 0 と定義しているだけです。失敗を隠す経路がそもそも無い課題なので、握り潰しにはなりません。
つまりこの表の「当たり/外れ」は 2 系統の根拠で決まっています。8 本は人手の裁定、9 本は課題の設計です。9 本を「未確定」と読むなら、精度は「当たり確定 4・外れ確定 4・未裁定 9」と書くことになります。当方は設計上の対照群として外れに数え、精度 4/17 としました。どちらの読みでも再現率 4/4 は変わりません。
精度 4/17。1 件の当たりを得るのに、人が 4.25 本読む計算です。
対照課題 9 本は、前の記事で「try/except も候補も 0」と書いた対照群(両言語で 20 本)のうち、Python 10 本の中の 9 本です。「候補 0」は出荷版の射程の中での話でした。射程の外には、同じ形が 9 本ありました。
成績表を両側そろえて置く
| 出荷版 | 読者提案 | |
|---|---|---|
| 母集団 | Python 60 | Python 60 |
| 挙げた数 | 2 | 17 |
| 当たり | 0 | 4 |
| 外れ | 2 | 13(うち 9 は対照課題) |
| 精度 | 0/2 | 4/17 |
| 再現率 | 0/4 | 4/4 |
前の記事の「4 flagged・0 TP」は、この表の左の列の上半分だけでした。下半分(再現率)を書かないと、「拾うものが何も無かった」と読めてしまいます。実際は「拾うべき 4 件が射程の外に居た」でした。
読者の検出器が正しい、という話でもありません。13 件の巻き添えがあります。2 つの検出器は「try/except の中を見るか、外を見るか」で射程が違い、片方の穴がもう片方の得意領域です。
この 60 本から直接言えるのはここまでです。except の中だけを射程にした検出器は、ガード側に在った 4 件を全部落とした。 そこから設計に持ち帰るなら、失敗経路は複数の構文位置に現れる前提で、検出器の射程を決める、までです。
TypeScript 60 本はなぜ載せないか
TypeScript 側で出荷版が挙げた 2 本(ts_load_config_s0/s2)は、ファイル末尾のデモ用ブロックにあるコメントだけの catch {} で、裁定は偽陽性です。読者の規則を TypeScript に当てると if ガードの既定値は 11 本あります。9 本(ts_get_item_*)は「catch が log か throw をする」という機械の判定のままで、人は裁定していません。残る 2 本(ts_parse_int_s0/s5・catch なし)は 2026-09-18 に人が legit_fallback と裁定しました(docstring が「失敗なら null/undefined」と書いている=Python の py_parse_int_s3/s9 と同じ規則)。裁定した正例(欠陥)は 0 本なので、再現率は「0/4」のような値ではなく定義できません(分母が 0)。「2 本開いて 2 本とも文書化されていた」が正確な読みです。
前の記事のテーゼは「構文では裁定できない。裁定は人の仕事」です。著者が慌てて残り 9 本を裁定して数字を作るのは、そのテーゼに反します(2 本を裁定したのは、読者が docstring という根拠を示したからで、数字を作るためではありません)。TypeScript 側は対照群として読み、この記事の表には入れません。
この 1 本から言えること/言えないこと
言えること:
- Python 60 本で、出荷版は精度 0/2・再現率 0/4、読者提案は精度 4/17・再現率 4/4 でした
- 真の握り潰し 4 件は全部 try/except の外に居ました
- 精度の分母 17 のうち 9 本は、人手の裁定ではなく課題の設計で外れに数えています
- 厳格アンカーを緩めると増える 5 本は 5/5 proper で、アンカーは精度側のフィルタとして働いていました
言えないこと:
- 「120 本で比較した」とは言いません。比較したのは Python 60 本です
- 「AST 検出器のほうが正しい」とは言いません。外れ 13 です
- 「try/except を見る検査は無意味」とも言いません。射程が違うだけです
- 一般の AI 生成コードについては何も言えません。1.5B の 1 モデル・6 課題・60 本です
再現したい人へ
-
git cloneしてresults/gt.csvを開く。列はsample_id, task_id, language, seed, handling, subtype, returns_default, has_log, has_raise, intent_disclosed, human_verdict, review_note - Python 60 本で
subtype in (guard_default, both)を数えると 17。human_verdictで割ると 4/2/2/9(9 はtask_idがpy_avg_control) -
handling == swallow_candを数えると 2(出荷版の flagged) - 読者の検出器は「
except節の外にあるreturnで、値がNone / 0 / [] / {} / "" / Falseのもの」をast.walkで拾うだけです。同じ 17 本と同じ行番号が出ます
数字は手で写さず、上の集計をそのまま載せています。
持ち帰り: 検出器の成績表(1 枚)
| 列 | 書くこと |
|---|---|
| 母集団 | 何本・どの言語・どの課題 |
| 挙げた数 | 検出器が候補にした本数 |
| 当たり/外れ | 根拠の系統も書く(人手の裁定か・設計上の対照か) |
| 精度・再現率 | 両方。片方だけの成績表は「無かった」と読まれる |
AI の利用について
再集計・検出器の再現・この記事の下書きは AI(Claude Code)が行い、裁定と公開の判断は人が行っています。読者の検出器は読者のものです。本人の希望で名前は出しません。
関連
この記事は「固定する → 測る → 道具を選ぶ → 公開後に伝播する」の 4 本のうち、測るの回です。
- 前の記事(親記事): AI が書いたコードは失敗を握り潰すか——120 本を測った
- AI レビューの指摘を現物で照合した回: 3 回・17 件、正しかったことと全部見つけたことは別
この記事は Zenn にも同じ内容を掲載しています。
