Go back

JevのChoice/Score/Noulは、PlanとValidateの判定に使えるか

Posted on:

前回の記事の終わりで、JevをIssueの判断に使えるかもしれない、と書きました。具体的にどう組み込むかには、一行も踏み込んでいませんでした。

この思いつきは、「Issue駆動のループエンジニアリング」という、GitHub Issueを起点にPlan、Validate、Implement、Test、Review/Merge、Automationという6段階を回す開発ワークフローを扱った本の、第3章「Plan:完了条件とリスクゲートを決める」に対応します。書いている当人が、まだその章の判定を数値に落とし込めていませんでした。

Jevには型付きの判定が3種類あります。Choice、Score、Noulです。前回ではその名前と役割を紹介しただけで、実際に手を動かしたのはレシートの仕訳を判定するサービスの記事でした。そこで呼んだのはChoiceだけです。ScoreとNoulは名前を知っているだけで、頭の中で正確に区別できていたかというと、怪しいものです。

ChoiceとScoreを混同していた

Scoreを、Choiceとだいたい同じものくらいに考えていました。どちらも選択肢の中から答えを選ぶ判定に見えるからです。だが並べてみると、扱っている尺度がそもそも違います。

Choiceは、決まった選択肢の中から1つを選ぶ判定です。返ってくるのは、選ばれたラベル、全選択肢にわたる確率分布、そして確信度(分布がどれだけ一点に集中しているか)です。一つの正解がある分類に向いています。

Scoreは、順序のある段階に対して採点する判定です。低いほうから高いほうへ並んだ尺度に、確率で重み付けされた位置と確信度を返します。段階のあいだに順序があること自体がChoiceとの違いになります。Choiceは選択肢どうしに順序を仮定しませんが、Scoreは順序尺度そのものを扱います。比較可能なScoreを複数並べれば、そのままランキングにもなります。

Noulは、真偽を0から1の確率で返す判定です。ChoiceやScoreと違って、別立ての確信度フィールドを持ちません。真偽の分布そのものが確率に現れるからです。0.5に近い値は「はい」と「いいえ」が拮抗しているという意味で、そのまま確信度の低さとして読めます。

3つとも、同じstateに対して並列に問えます。systemOne()のquestionsに複数のキーを渡せば、1回のリクエストでまとめて評価が返ってきます。前回のレシート仕訳でも、account(Choice)とreadable(Noul)を同じリクエストの中で並べていました。

3つの出力の違いを図にすると、こうなります。

この違いが頭に入ってから、Issueの判定という思いつきを読み返すと、リスクの3段階が引っかかりました。危険度は、Choiceで選ぶような分類ではありません。Low・Medium・Highという順序のある段階です。Scoreの出番ではないでしょうか。

第3章「Plan」に判定を重ねる

第3章「Plan」は、着手前に何を決めておくかという章です。

着手前にまず区別するのは、ゼロイチ開発か、運用中システムへの機能追加かです。運用中の機能追加なら、既存コードの意図やドメイン前提をdocsに外部化する作業がもう一つ要ります。二択(実際にはもっと選択肢を足してもよい)で答え、選んだ結果によってその後の分岐が変わります。ルーティングに向くChoiceの使い方そのものです。

実装はしていませんが、次のように書けるはずです。

const result = await client.systemOne({
  state: { issueBody },
  questions: {
    projectType: choice(
      "このIssue本文 `issueBody` は、ゼロイチ開発と運用中システムへの機能追加のどちらに近いか。",
      {
        greenfield: "既存コードへの依存がない、新規の開発",
        brownfield: "稼働中のシステムに対する機能追加・修正",
      }
    ),
  },
});

ゼロイチか運用中かが決まったら、次は本でいうAI Readyかどうかを判定します。3条件がすべて揃っているかどうかで決まる状態で、完了条件が明文化されているか、テスト・型検査などの自動検証が揃っているか、触ってよい範囲が区切られているかです。1つでも欠けていれば、AI Readyではありません。

3条件は、どれもIssue本文に対する独立したyes/no判定です。ここはNoulが向いています。3本のNoulを並列に聞けば、1回のリクエストで済みます。

const result = await client.systemOne({
  state: { issueBody },
  questions: {
    hasDoD: noul(
      "このIssue本文 `issueBody` に、完了条件が明文化されているか。",
      {
        true: "完了の判定基準が具体的に書かれている",
        false: "完了条件が書かれていない、または曖昧",
      }
    ),
    hasVerification: noul(
      "このIssue本文 `issueBody` に、テストや型検査など自動検証の方針が書かれているか。",
      {
        true: "自動検証の手段が明記されている",
        false: "検証手段への言及がない",
      }
    ),
    hasScope: noul(
      "このIssue本文 `issueBody` に、触ってよい範囲が区切られているか。",
      {
        true: "対象ファイルやディレクトリが具体的に示されている",
        false: "触ってよい範囲の指定がない",
      }
    ),
  },
});

3本の確率がすべて閾値を超えたときだけ「AI Ready」とみなす、という判定自体はコード側に残ります。今は人間かエージェントが読んで判断しているはずの3条件チェックを、確率つきのゲートに置き換えられる、というだけの話です。

「AI Ready」のあとにも、着手前に確認すべき条件がいくつか続きますが、ここでは踏み込みません。それらをすべてクリアしたら、変更のリスクをLow / Medium / Highの3段階で見積もります。本の中でこの判断は人間が行うことになっていて、Lowは文言やスタイルの変更でロールバックが容易なもの、Mediumは機能追加で完了条件とPRレビューで監視するもの、Highは認証・認可や本番インフラの変更で方式を人間が先に決めるもの、というカテゴリ例をもとに判断します。

3段階は順序尺度です。ChoiceではなくScoreが対応します。

本にはこう書かれています。

迷ったら一段階上げます。

最初に読んだときは、経験則としてそのまま流していました。だがScoreの仕組みを知ったあとに読み返すと、この一文がそのままコードに翻訳できることに気づきました。

Scoreが返すscoreは、段階の番号そのものではありません。確率で重み付けした平均の位置で、たとえばMediumとHighに確率が割れていれば1.7のような小数になります。整数からのズレそのものが、どれだけ隣の段階へ寄っているかを表しています。確信度が閾値を下回るとき、四捨五入ではなく切り上げにすれば、それだけで「迷ったら一段階上げる」になります。人間の勘に頼っていた一文が、丸め方向を変えるだけの一行に対応します。

const RISK_LEVELS = [
  "Low: 文言・スタイルの変更など、ロールバックが容易なもの",
  "Medium: 機能追加など、完了条件とPRレビューで監視できるもの",
  "High: 認証・認可や本番インフラの変更など、方式を人間が先に決めるべきもの",
];

const result = await client.systemOne({
  state: { issueBody },
  questions: {
    risk: score(
      "このIssue本文 `issueBody` が示す変更のリスクは、どの段階に近いか。",
      RISK_LEVELS
    ),
  },
});

const levelIndex =
  result.risk.confidence < RISK_CONFIDENCE_THRESHOLD
    ? Math.ceil(result.risk.score) // 迷っているときは切り上げる
    : Math.round(result.risk.score);

const label = ["Low", "Medium", "High"][levelIndex];

RISK_CONFIDENCE_THRESHOLDの値はここでは決めていません。実際のIssueに対してScoreがどんな確信度を返すのかを見てから決めるべき数字で、これも次に試すべきことの一つです。

リスクレベルがどう出ても変わらない部分もあります。default branchへの直接push、本番やSecretの操作・データ削除の自動実行、DBマイグレーションの方針や認証・認可の方式決定を人間の判断なしにエージェントだけで決めること、の3つです。これらは、本の中で常に守るべき「ハードガード」(本で定義されている用語)とされています。ここはJevに聞く話ではありません。リスクレベルに関係なく、Scoreの確信度がどれだけ高くても崩さない、コード側で機械的にブロックする前提です。その判定根拠として、コードが確信度から導いたリスク段階のラベル(Low/Medium/High)と確信度の値を、そのままIssue本文に書き残します。

ここまでの流れを図にすると、Choice・Noul・Scoreが一つのゲートの中に並びます。

「問題なし」をどう疑うか

着手前の判定はここまで型が当てはまりましたが、着手したあとの検証にも同じことが言えるでしょうか。本の第4章「Validate:敵対的検証という段階」を読み返すと、もう一つ気になる箇所がありました。

作成者に自分のコードを評価させると、見逃しが増えます。だから独立したチェッカーエージェントに、敵対的なレビューをさせます。設計要件は4つあります。独立性、反証の役割づけ(「レビューして」ではなく「反証を試みろ」と懐疑者として指示すること)、接地(一次情報に当たること)、そして判断できる出力(指摘に深刻度と根拠をつけさせること)です。

敵対的検証には、2つの失敗モードがあるといいます。一つは過剰指摘で、健全な成果物にも何か報告してしまいます。もう一つは手抜きの合格判定で、ろくに検証せず「問題なし」を返します。過剰指摘は気づきやすいものです。手抜きの合格判定は気づきにくく、最大の失敗モードとされています。

本にはこう書かれています。

「問題なし」という結果ほど、人間が中身を見る必要があります。

指摘を却下する判断だけは人間が確認し、作成者単独では判断させません。直す指摘は作成者が処理し、却下は人間が最終確認します。ここだけ役割が非対称になっています。

ここにNoulを差し込めないか、というのが今回いちばん怪しい思いつきです。チェッカーが「問題なし」を返したとき、その「問題なし」を鵜呑みにせず、二段構えで確かめます。検証コメントそのものに対して、Noulでもう一問聞きます。「この検証は、具体的な引用や根拠を伴っているか」。

const result = await client.systemOne({
  state: { reviewComment },
  questions: {
    grounded: noul(
      "このレビューコメント `reviewComment` は、具体的な引用やコード箇所などの根拠を伴っているか。",
      {
        true: "引用・行番号・具体的な差分など、検証したことがわかる根拠が書かれている",
        false: "根拠が示されず、結論だけが書かれている",
      }
    ),
  },
});

ここまで書いて、groundedという問い自体がJevに向いていないことに気づきました。行番号やファイルパスが書かれているかどうかは、文章の意味を読み取る話ではなく、パターンとして存在するかどうかの話です。正規表現で探せば済み、確率で返してもらう意味がありません。

Jevに聞く価値があるのは、その先です。引用されている行が、実際に指摘している内容と対応しているか。ここは中身を読んで判断する必要があり、パターンマッチでは済みません。根拠が「付いているか」はコードのチェックで足り、根拠が「妥当か」だけがJevの出番になります。本の第4章にも、チェッカーはこの判断まではできない、という趣旨の一文があり、この線引きはJevを挟んでも変わりません。

Issue本文だけで、足りるのか

レシート仕訳の記事で学んだことが、そのままここにも効きます。stateに渡す情報の質が、判定の質を決めます。OCRテキストがノイズだらけでも、Choiceはそれでも何かを選んでしまいます。Issueの判定も、同じ構造を持っているはずです。Issue本文だけをstateに渡した場合、そこに書かれていない依存関係や影響範囲まで、Jevが汲み取ってくれるわけではありません。人間がIssueをざっと読むのと大差ない情報量しか渡せていない可能性があります。

依存グラフや影響範囲まで一緒にstateへ入れられれば、リスク判定の精度はもっと上がるはずです。依存Issueがcloseされているか、テストファイルが存在するか、CIの設定があるか。これらは事実として確認できることで、Jevに聞くことではありません。Claude Codeのスキルのような仕組みからghコマンドやファイルシステムを直接調べ、その結果をstateに足せばよいだけです。Validateの根拠チェックで見たのと同じ線引きが、ここにもそのまま当てはまります。事実として確認できることはコード側の仕事で、Jevの出番は、事実をどれだけ集めても最後に残る、解釈の部分だけです。

stateを事実で厚くしたうえで、Jevに何を聞けばいいのかはまだ詰めていません。集めた事実(依存Issueが2件未クローズ、対象ファイルが認証まわりを含む、といった具体的な情報)を渡して、それでもScoreにリスクを採点させる、という形になるはずですが、どの事実を集めれば判定の精度が上がるのかは、まだ試していません。

そして、ここに書いたコードはどれも動かしていません。Choiceでゼロイチと運用中を分けるところも、Noulを3本並べるところも、Scoreに確信度で一段階上げる処理を足すところも、Noulで検証コメントの根拠を問うところも、全部が思考実験のままです。前回のように、実際の入力を1本読ませて手が止まった、という記録はまだありません。

前回の終わりに書いた思いつきは、今ならScoreとNoulという型の名前で言い直せます。ただし、名前が付いたところで、まだ何も動いていません。次に試すべきは、実際のIssueを1本渡して、Scoreが返す確信度が、本当に「迷っている」と呼べる値を返すかどうかを見ることでしょう。