本記事では、生成AIにantigravity CLIを使用しました。
- 金融監査AIのサンプルコードをAIエージェントに生成させたら、コードは正しく動くのに不正取引を検知できないバグが埋まっていた
- 原因はロジックではなく、モックデータの中に「これから検知すべき異常」が事前に混入していたこと
- これは機械学習・金融システムの文脈で Data Leakage / Look-ahead Bias と呼ばれる、よく知られた設計上の落とし穴
- AIエージェントが生成したコードは、文法・型は正しくても「業務の時系列」まではレビューしないと壊れていることがある
何が起きたか
AIエージェントに、以下のような簡易的な金融監査エージェントを生成させました。ルールベースのスクリーニングで検知した「疑わしい取引」を、口座の取引履歴と照合してリスクスコアを算出する、というものです。
from typing import Dict, Any, List
# データベースのモック(監査対象の口座履歴)
TRANSACTION_DB = {
"ACC001": [
{"amount": 5000, "location": "東京", "category": "食費"},
{"amount": 1000000, "location": "ロンドン", "category": "宝飾品"}, # 異常値
{"amount": 8000, "location": "東京", "category": "交通費"}
]
}
class FinancialAuditAgent:
def evaluate_alert(self, account_id: str, alert_data: Dict[str, Any]) -> Dict[str, Any]:
history = TRANSACTION_DB.get(account_id, [])
locations = {tx["location"] for tx in history}
risk_score = 0.0
reason = ""
if alert_data["location"] not in locations:
risk_score += 0.7
reason += "普段使用されていない場所での取引です。 "
if alert_data["amount"] > 500000:
risk_score += 0.2
reason += "取引金額が平均値を大きく上回っています。"
is_fraud = risk_score >= 0.8
return {
"is_fraud": is_fraud,
"risk_score": risk_score,
"reason": reason.strip() or "通常の取引パターン内です。"
}
このコードに「ロンドンで100万円の取引」というアラートを流し込んだところ、結果は以下の通りでした。
[Audit Agent] アラート評価中: 口座 ACC001 - 金額 1000000円
-> 口座履歴を確認: 過去の取引場所 = {'東京', 'ロンドン'}
✅ 【パス】監査クリア (スコア: 0.2) - 取引金額が平均値を大きく上回っています。
普段使わないロンドンでの高額決済という、明らかに高リスクなケースにもかかわらず、「監査クリア」と誤判定されました。スコアも0.8の閾値に届かず0.2止まりです。
原因:監査対象がすでに履歴に含まれていた
コードのロジック自体はどこも間違っていません。問題はTRANSACTION_DBというモックデータの中身です。
TRANSACTION_DB = {
"ACC001": [
{"amount": 5000, "location": "東京"},
{"amount": 1000000, "location": "ロンドン"}, # ← これから監査する取引そのもの
{"amount": 8000, "location": "東京"},
]
}
本来なら「これから監査すべき対象」であるロンドンでの100万円取引が、最初から「過去の正常な履歴」としてDBに登録されていたのです。正しい履歴データであれば、取引場所は {'東京'} だけのはずでした。
このデータを使って
locations = {tx["location"] for tx in history}
# => {'東京', 'ロンドン'}
を実行すると、判定式
if alert_data["location"] not in locations:
が False になります。ロンドンはすでにlocationsの中にあるので、「普段と異なる場所」として検知されず、加点(+0.7)が発生しません。結果として金額分の+0.2しか積み上がらず、閾値0.8を超えられませんでした。
なぜコードレビューで気づきにくいのか
実際にこのコードをレビューしても、多くの人は違和感を持たないはずです。
if alert_data["location"] not in locations:
このロジック自体は健全です。in の使い方も、リスクスコアの積み上げ方も、特に問題は見当たりません。壊れているのはコードではなく、コードが前提とするデータの方です。
- プログラムは正しい
- 入力データの前提が間違っていた
この種の「意味論(semantic)のバグ」は、静的解析やユニットテストでは検出しづらく、レビュアーがデータの中身まで時系列的に検証しない限り見逃されます。
このバグには名前がある
機械学習や金融システムの文脈では、この種の問題は
- Data Leakage
- Target Leakage
- Look-ahead Bias
と呼ばれています。評価・学習に使うデータの中に、本来はまだ知り得ないはずの未来の情報(今回で言えば監査対象そのもの)が混入してしまう、典型的な設計ミスです。
金融システムにおける正しい処理順序は
新しい取引 → 監査 → 問題なし → 履歴へ登録
であるべきところ、今回生成されたコードのデータ設計は
履歴へ登録 → 監査
という順序になっていました。これは未来の情報を参照して判断していることに等しく、異常検知の精度を根本から損ないます。
AIエージェントがなぜこのコードを書いたのか
これはPythonの文法ミスではありません。AIエージェントは「取引履歴のサンプルデータを作る」という要求を満たすために、サンプルとして手近にある取引(=監査対象そのもの)を履歴に含めてしまいました。
人間であれば「これはこれから評価するデータだから、履歴に入れてはいけない」と直感的に判断します。しかしLLMは時系列や業務フローを暗黙知として理解しているわけではなく、局所的な目的(それらしいサンプルデータを作ること)を優先した結果、こうした設計ミスを生成コードに埋め込むことがあります。
まとめ
- AIエージェントが生成したコードは、文法的にも型的にも正しく動作するのに、業務ロジック(時系列)が壊れていることがある
- 特にデータ生成・モック作成をAIに任せると、Data Leakage / Look-ahead Bias のような「データの前提」に関わるミスが紛れ込みやすい
- コードレビューだけでなく、「このデータはいつ時点で存在してよいものか」という時系列の整合性確認まで含めて初めて品質が保証される
AIエージェント時代のレビューでは、「動くから正しい」ではなく「このデータは未来を覗き見していないか」という視点を持つことが重要だと感じた事例でした。