AIに経理の計算式を直させたら、別の拠点で数字が壊れた|採用前の答え合わせ手順

AIに経理の計算式を直させたら、別の拠点で数字が壊れた|採用前の答え合わせ手順 AI導入の判断

AIに経理の計算式のバグを指摘してもらいました。指摘は正しく、直したら数字も合いました。ただし、私が見せた拠点では。見せていなかった別の拠点に同じ式を当てたら、そこだけ数字が壊れていました。

結論を先に書きます。AIが直した計算式は、直した本人(AI)が確かめた範囲でしか検証されていません。採用する前に、全部の拠点 × すでに終わった月で機械的に走らせて、直したところ以外が1円も動いていないことを確かめる。これだけで、静かに壊れるのをほぼ防げます。

私は清掃業を経営していて、プログラマーではありません。この記事は、うちのMacで実際に動いている月末着地予想のスクリプトと、その古いバックアップを読み比べて書いています。金額は社外に出せないので、数字そのものではなく、壊れ方と確かめ方だけを書きます。


そもそも何を計算させているか

うちは拠点が5つあります。拠点ごとに「その日の売上」と「その日の利益」を1日1列ずつ入力する表があって、そこから月末にいくらで着地しそうかを毎月自動で出しています。

計算の考え方はとても単純で、日割りです。

着地 = ここまでの累計 ÷ 経過日数 × その月の日数

この式の中で、いちばんもめるのが 経過日数 です。「今月、何日ぶん入力されているのか」を、表から機械に判断させないといけない。ここが1日ずれるだけで、着地の金額はまるごとずれます。


AIの指摘は、実際に正しかった

先に公平に書いておきます。AIの指摘そのものは当たっていました。当時のコード(2026年8月18日のバックアップに残っています)は、こうなっていました。

elapsed = sum(1 for v in svals if isinstance(v, (int, float)) and v != 0)
cum_s   = sum(v for v in svals if isinstance(v, (int, float)))
cum_p   = sum(v for v in pvals[:elapsed] if isinstance(v, (int, float)))

読み方だけ書きます。svals が1日ごとの売上、pvals が1日ごとの利益です。ここには2つ問題がありました。

  • 経過日数を「売上が0でない日の個数」で数えていた。月の途中に休業日が1日あると、その日は数に入らない。実際には日が経っているのに「まだ経っていない」ことになる
  • 売上と利益で、足す範囲が違っていた。売上は月ぜんぶを足しているのに、利益は先頭の elapsed 日ぶんしか足していない

1つ目のほうが実害が大きい。割り算の分母が小さくなるので、休業した拠点ほど着地が高く出ます。休んだのに景気よく見えるという、いちばんタチの悪い間違い方です。AIはここを見つけてきました。ありがたい指摘です。


「直った」ように見えた

問題は、そのあとに出てきた直し方でした。

売上だけで判断すると休業日を取りこぼすのなら、利益の行を見ればいい——という案です。売上が0でも、その日に営業していれば固定費のマイナスが立つ。だから利益の行のほうが「日が経ったかどうか」を素直に表している、という理屈です。説明としては、筋が通っています。

そして実際に、そのとき私が確かめた拠点では正しい数字が出ました。休業日もちゃんと日数に入り、完了した月の予測と実績も一致しました。ここで採用していたら、たぶん何か月も気づきませんでした。


別の拠点で、静かに壊れた

他の拠点にも当ててみたら、1つだけ明らかにおかしい値が出ました。原因は入力の習慣でした。

その拠点は、まだ来ていない将来の日付の欄にも、1日あたりの固定費をあらかじめマイナスで入力してありました。数か月先の分まで、先に埋めてある。

そうすると何が起きるか。利益の行で経過日数を数えると、月末までまるごと「もう経過した」と判定されます。今日が月の半ばでも、分母は月の日数になる。累計は半月ぶんしかないのに、月ぜんぶで割ってしまう。着地予想が半分近くまで沈みます。

しかもこれは、エラーになりません。処理は最後まで通り、表はきれいに出て、Slackにも届きます。数字が1つだけ低いだけです。1拠点の分ですから、全体の合計を眺めているぶんには「今月ちょっと弱いな」で終わってしまう。気づけるとしたら、その拠点の数字を単体で見たときだけです。

結局いまは、こう直してあります。「売上が入っている最後の日」を経過日数とし、その間の休業日は日数に含めるという数え方です。

active  = [i for i, v in enumerate(svals) if isinstance(v, (int, float)) and v != 0]
elapsed = active[-1] + 1          # 売上が入っている最後の日=経過日数
closed  = elapsed - len(active)   # その間にあった休業日の数
cum_s   = sum(v for v in svals[:elapsed] if isinstance(v, (int, float)))
cum_p   = sum(v for v in pvals[:elapsed] if isinstance(v, (int, float)))

これなら休業日も日数に入り、売上と利益が同じ範囲で足されます。そして利益の行は判定に一切使いません。コードには、なぜ使わないのかを日本語のコメントで残してあります。理由を書いておかないと、次に誰か(AIを含む)が同じ「改善」をまた提案してくるからです。


2つのAIが同じことを言っても、正しさの証明にはならない

この件でいちばん効いた教訓はこれです。

うちは、同じ質問を Claude・Codex・Gemini の3つに互いの答えを見せずに投げる仕組みを持っています。食い違ったところが「誰かの見落とし」の目印になるので、金額の話ではこれを通すようにしています。

このときは、2つのAIが独立に、ほぼ同じ改善案を出しました。別々の会社の、別々のモデルが、同じ結論に達した。私はそれを「まず間違いない」と受け取りました。受け取ってしまったのが間違いでした。

よく考えれば当たり前で、2つのAIが見ていたデータは同じです。同じ表を読んで、同じ「利益の行のほうが素直だ」という一般論に行き着いただけ。将来の日付に固定費が先に入っているという、うちの表だけの癖は、どちらも見ていませんでした。一致しているのは、正しいからではなく、同じところを見落としているからかもしれない。

なので、3つの回答をまとめる役のAIには、指示文にこう書き込んであります。

食い違った箇所は必ず自分で実データを読んで検算し、どちらが正しいか裁定すること。3つのうち2つが一致していても、それは正しさの証明にならない。必ず自分で確かめること。

AIを増やしても、多数決は検算になりません。検算になるのは実データだけです。


採用する前にやる答え合わせ(そのまま使えます)

いまは、計算式に手を入れるときは必ずこの順番でやっています。拠点や月をいくつも抱えている人なら、そのまま真似できます。

1. 直す前に、いまの答えを全部保存する

うちのMacには git(変更履歴を管理する道具)が入っていません。なので、出力そのものをファイルに保存して比較しています。これで十分です。

cd ~/scripts/keiri
cp predict.py predict.py.bak-$(date '+%Y%m%d-%H%M%S')

mkdir -p 検証/before
for m in 4 5 6 7 8; do
  .venv/bin/python predict.py $m > 検証/before/$m月.txt
done

Pythonは .venv/bin/python を必ず指定します。うちのMacには Xcode Command Line Tools が入っておらず、/usr/bin/python3 は動きません。

2. 直したあと、同じことをして1行ずつ比べる

cd ~/scripts/keiri
mkdir -p 検証/after
for m in 4 5 6 7 8; do
  .venv/bin/python predict.py $m > 検証/after/$m月.txt
done

diff -r 検証/before 検証/after

diff は2つのフォルダの中身を比べて、違う行だけを見せてくれるコマンドです。ここで出てくる差分について、1行ずつ「なぜ変わったか」を説明できなければ採用しない。これがルールです。

直したのが休業日の数え方なら、休業日があった拠点の行しか動かないはずです。関係ないはずの拠点が1円でも動いていたら、それは想定外のことが起きている証拠です。理由が説明できるまで止めます。

3. 完了した月は「予測=実績」になるはず

すでに終わった月なら、日割りの予測は定義上ぴったり実績と一致します。ずれたら予測が外れたのではなく、集計が壊れています。だからうちは、完了した月の正しい売上と利益をルールを書いたファイルに文字で残しています。

## 検算のしかた(結論を出す前に必ず)
完了した月で「着地予想 = 実績」になるか確かめる。
6月の正解値: 売上 ○,○○○,○○○ / 利益 ○,○○○,○○○
predict.py 6 がこの値と一致しなければ、ロジックが壊れている。

正解が1つも書き残されていないと、計算式を変えたとき、それが良くなったのか壊れたのか誰にも判定できません。AIにも判定できません。ここに書いた正解値は、AIに作業させるとき最初に読ませています。

4. 本番の通知を巻き込まない

これは地味ですが大事です。うちの場合、計算だけをする predict.py は画面に出すだけですが、それを呼び出す定期実行用のスクリプトのほうは、実行すると本番のSlackに投稿します。検証のつもりで走らせると、途中の変な数字が全員に届きます。

検証するときは計算部分だけを単体で動かす。どうしても全体を試したいときは、別のフォルダにコピーして、Slackの宛先ファイルを持っていかない。この2つは分けておいたほうがいいです。


AIに直させるときの頼み方

頼み方も変えました。効いているのは3つです。

  1. 「確かめていない対象はどれか」を必ず言わせる。「直しました」で終わらせない。どの拠点・どの月で実際に走らせて確認したのかを書かせると、たいてい1〜2件しか見ていません。それが分かるだけで扱いが変わります
  2. データの癖を、先にファイルに書いて渡す。「将来の日付に固定費が先に入っている拠点がある」「シート名に全角と半角が混ざっている」といった、表を見ただけでは分からないことです。うちはこれを1つのルールファイルにまとめて、AIが作業前に必ず読む場所に置いています
  3. 直した理由をコードのコメントに日本語で残させる。「利益の行は判定に使わない。なぜならこういう入力があるから」と書いてあれば、次に同じ提案が出てきたときに即座に却下できます。やられた失敗は、コードの中に書いておかないと必ずもう一度やられます

逆に、やめたことが1つあります。AIの説明が筋が通っているかどうかで採否を決めるのをやめました。筋が通っているのは当たり前です。通っていない説明はそもそも出てきません。判断材料は説明ではなく、実データに当てた結果の差分だけにしました。


明日やるなら、この順番で

  1. すでに終わった月を1つ選び、自動計算の結果と、手で数えた実績を並べる
  2. 一致したら、その月の正解の数字を文字でファイルに書き残す。これが以後の合否の基準になる
  3. いまの計算結果を、対象(拠点・部門・店舗)ごとに全部ファイルへ保存する。これが「直す前」の記録になる
  4. 計算式に手を入れたら、同じものを作って diff で比べる
  5. 変わった行を1行ずつ説明できるか確かめる。できないものが1つでもあれば採用しない

1と2は30分で終わります。3以降は、一度書けば毎回コピペで済みます。いちばん効くのは5番です。「たぶん大丈夫」で通した変更が、あとから静かに効いてきます。


まとめ

  • AIが直した式は、AIが見た範囲でしか検証されていない。見せなかったデータでは何が起きるか分からない
  • 1つの拠点で正しく見えることは、他の拠点で正しいことの根拠にならない。壊れるのはたいてい入力の習慣が違うところ
  • 2つのAIが独立に同じ答えを出しても、正しさの証明にはならない。同じデータを読んで同じ見落としをしているだけかもしれない
  • この種の壊れ方はエラーを出さない。表もきれいに出るし、通知も届く。数字が1つ違うだけ
  • 採用前に全対象 × 完了した月で走らせ、diff で差分を見る。説明できない差分が1行でもあれば採用しない
  • 完了した月の正解値を文字で書き残す。正解が無ければ、良くなったのか壊れたのか誰にも判定できない
  • データの癖と、却下した案の理由はコードのコメントとルールファイルに残す。残さないと同じ提案が何度でも戻ってくる
  • 検証は計算部分だけを単体で動かす。定期実行のスクリプトごと走らせると本番の通知が飛ぶ

そもそも「完了した月で予測と実績を突き合わせる」という検算のやり方そのものは 資金繰り予測を自動化しても、最後は残高照合が要る に詳しく書いています。計算式を直す前に、まず正解を1か月ぶん手に入れてください。正解が無い状態でAIに直させるのが、いちばん危ない順番です。

コメント

タイトルとURLをコピーしました