バリデーターを再利用すると、パイプラインでの失敗の意味が反転する
同じフェイルクローズチェックは、データの生成時には安全なスキップ処理ですが、その監査時には破壊的な削除処理となります。これを安全に移動する方法は次のとおりです。
本番環境で数ヶ月間稼働しているバリデーターは、再利用するのに最も魅力的なコードです。テスト済みで、チューニング済みであり、すでにデータの形状を把握しています。そのため、バリデーターが存在する前に書き込まれたレコードを監査する必要がある場合、古い行にそれを適用することは、1行の作業のように見えます。同じ関数、新しい入力セットです。その直感は、ロジックについては正しいですが、結果については間違っています。「無効」を返すチェックは、生成パスでは「これを書き込まないでください」を意味し、監査パスでは「そこにあるものを削除してください」を意味しますが、コードはその違いを区別できません。本稿では、それが引き起こす特定の失敗と、それを修正する3方向の判断について解説します。
共有チェッカーに潜む非対称性
翻訳された用語を生成し、保存する前にそれぞれを検証するパイプラインを考えてみましょう。検証ツールは、完全一致のショートカット、低コストの埋め込み距離テスト、そして曖昧な中間帯に対してはyes/noで回答する言語モデルという3つのステージを実行します。どのステージであれ、失敗したものは単に書き込まれません。呼び出し元は次に進み、翌日再試行します。
次に、その検証ツール内部のエラー処理を見てみましょう。
for _, r := range gray {
ok, err := e.verifyGray(ctx, r.ko, r.backKo)
if err != nil {
r.verdict = DropBacktranslate // treat an API error as a failed check
continue
}
if ok {
r.verdict = VerdictPass
} else {
r.verdict = DropBacktranslate
}
}
生成パス上では、これは正しく、エレガントでさえあります。タイムアウト、レート制限、不正な形式のレスポンス、これらはすべて「これを検証できなかったので、書き込みません」という結果に集約されます。フェイルクローズドです。偽陰性のコストは、1行のスキップと翌日の1回のリトライです。
同じ関数を既存の検証済みレコードに向けると、コストは逆転します。1回のAPIタイムアウトが、今や人間が承認した正しい行を削除対象としてマークします。バリデーターは変わっていません。影響範囲は変わりました。なぜなら、呼び出し元が「書き込まない」という決定を「破棄する」という決定に変えたからです。
罠は、この非対称性が呼び出し箇所では見えないということです。監査コードは、生成コードと同じくらい無害に読めます。
verdicts, err := engine.VerifyPairs(ctx, pairs)
for _, v := range verdicts {
if !v.Pass {
rejectRecord(ctx, v.ID) // looks symmetric, is not
}
}
戻り値からわからないこと
より深い問題は、ブール値の合格フラグが2つの異なる世界を1つに圧縮してしまうことです。「モデルがこのペアを検証し、一致しないと判断した」と「回答が得られなかった」は、どちらも Pass: false として扱われます。生成パスでは、どちらの結果も同じ安全なアクションにつながるため、その圧縮は無害です。監査パスでは、それらをどうしても区別する必要がありますが、型シグネチャがそれを許しません。
解決策は、バリデーターをより賢くすることではありません。バリデーターに判定を求めるのをやめ、証拠を求めるようにすることです。ほとんどのバリデーターは、すでにブール値以上のもの、つまり距離、信頼度、中間生成物を返します。破壊的な決定は、代わりにそれらの値を通じて行うようにしてください。
私たちの場合、戻り値の型はフラグとともにある距離を保持していました:
type PairVerdict struct {
Pass bool
BackKo string // the round-trip translation, empty if we never got one
Dist float64 // embedding distance, 0 when the shortcut matched
}
その距離は測定されたものであり、判定されたものではありません。APIエラーが大きな距離を捏造することはありません。なぜなら、呼び出しが失敗した場合、距離は一切生成されないからです。そのため、距離のしきい値はデータに関する主張であり、ブール値はプロセスに関する主張となります。
3方向の判定
監査には2つではなく3つの結果が必要であり、その3つ目が全体の要点です。
- 削除: 不一致の確固たる証拠。我々のパイプラインでは、それは上限帯を超える距離であり、正当な同義語が存在する領域をはるかに超えています。
- 維持: バリデーターがレコードを肯定的に合格させた場合。これには、言語モデルが曖昧なペアを見て承認した場合も含まれます。
- 保留: その他すべて。チェックが完了できなかった、ラウンドトリップが空で返ってきた、またはモデルが曖昧な帯域内で「いいえ」と回答した場合。調査結果を記録し、データはそのままにして、人間のためのキューに入れます。
保留バケットは、再利用を安全にするものです。それは「確信がない」という状態を、破壊的なアクションから作業項目に変換します。また、監査をクリーンアップ作業からトリアージ作業に変換します。これこそが、監査が本来あるべき姿なのです。
この設計には、微妙な順序のバグが潜んでおり、我々が実際に遭遇したため、詳しく説明する価値があります。我々のdistanceフィールドは、2つのまったく異なる状況でゼロになります。完全一致のショートカットが発動したとき(完全な合格)と、ラウンドトリップが空で返ってきたとき(評価の完全な失敗)です。distanceのみに基づいて書かれた2分岐のルールは、空のラウンドトリップを合格として処理し、合格レコードを書き込み、それによってべき等性フィルターを通じてその行を将来のすべての監査から除外してしまいます。その行は、一度も検証されることなく、永遠に検証済みとしてマークされてしまうでしょう。distanceに触れる前にラウンドトリップのアーティファクトが空であるかを確認することは、1行のコストでその穴を塞ぎます。
ドライランからの数値
3者間ルールで監査を構築し、何にも手を加える前に、300件のレガシーレコードに対してドライランモードで実行しました。興味深かったのは距離ヒストグラムです。すべてのレコードが上限しきい値を下回り、削除対象となるものは何もありませんでした。
さらに示唆に富んでいたのは、中間帯でした。「dermal filler injection」を意味する用語には4言語の翻訳があり、それらはすべてラウンドトリップで「hyaluronic acid injection」となり、距離は0.36でした。これらは正しい翻訳です。ラウンドトリップでは、手順の代わりに物質が記述されましたが、これはまさに言語モデルが「同一ではない」とマークする種類のずれです。2者間ルールでは、これら4つの行は、正しいにもかかわらず、4言語で削除されていたでしょう。
そのたった1つの観察結果が、設計全体を正当化しました。また、予期していなかった2つ目の発見ももたらしました。それは、削除ゼロという結果が、データがクリーンであることの証明にはならないということです。これは、部分的には母集団フィルターの産物でした。監査は、親エントリが確定状態にあるレコードのみを対象としていました。そして、最も有名な不一致の例、つまり翻訳がある医師の経歴の段落にまでずれてしまった用語は、未確定の親の下に存在していました。監査ではそれを見つけることができませんでした。バリデーターの性能は、渡される行の質に左右されます。
バリデーターを破壊的なパスに移行するためのルール
- 各パスで失敗した場合のコストを問うこと。 一方のパスがリトライという代償を払い、もう一方がデータの削除という代償を払う場合、それはバリデーターを再利用しているのではなく、新しい仕事を与えているのです。
- 破壊は判定ではなく、測定を通じてルーティングすること。 距離、カウント、差分はインフラの障害によって捏造されることはありませんが、真偽値は捏造されえます。
- 削除を追加する前に、保留用のバケットを追加すること。 監査に3つ目の結果がない場合、不確実性を損害として表現してしまいます。
- スコアだけでなく、アーティファクトを確認すること。 空の中間結果は、完全な結果と同じ数値を共有することがよくあります。そのケースを明示的にテストすること。
- べき等性の罠に注意すること。 再処理を避けるために「チェック済み」を記録すると、誤ってパスしたものが永続化してしまいます。その行は検証済みとしてマークされ、二度と見られることはありません。
- まずは実際のスライスでドライランを行い、分布を見ること。 削除がゼロ件という結果は情報です。データがクリーンであるか、しきい値が間違っているか、あるいは母集団フィルターが探していたまさにその行を除外しているかのいずれかです。
- 上書きする前にオリジナルを保存すること。 私たちのケースでは、ログテーブルの既存の未使用カラムに入れました。ロールバックのための保険は、通常、見た目よりも安価なものです。
一般的な形
これは実のところ、バリデーターに関する話ではありません。関数の安全性という特性が、その関数に固有のものではないという事実に関する話です。それらの特性は、関数とその呼び出し元のペアの中に存在します。フェイルクローズとは、「このチェックが完了できなかった場合に何が起こるか」という特性であり、何が起こるかは、return文の向こう側にあるコードによって完全に決定されます。
次に、成熟した検証ロジックを新しいデータセットに向けるとき、問うべきは、そのロジックがまだ適用できるかどうかではありません。問うべきは、「no」の意味がまだ適用できるかどうかです。もし、「no」と言うことが以前は「待機」を意味し、今では「破棄」を意味するようになったのなら、3つ目の言葉が必要です。