富士山は何メートルか
ある日、社内の AI(ローカル LLM)に富士山の高さを聞いてみました。temperature=0、seed 固定。理屈の上では「毎回同じ答え」が返るはずの設定です。ところが——
- 「富士山の高さは」→ 3,776.1 m
- 「富士山は何メートル」→ 3,776.12 m
- 「富士山はどのくらい高い」→ 3,776.24 m
同じことを聞いているのに、言い回しを変えただけで答えが 3 通りに割れました。 富士山の標高はひとつのはずです。これが LLM の「ぶれ」です。
雑談なら笑い話で済みます。しかし「昨日の売上は」「3 日前の取引の承認者は」「この送料は税込みか」——事実を扱う業務でこのぶれが出ると、笑えません。
私たちが取り組んだのは、この「ぶれ」を 賢さではなく 構造で消すことでした。LLM をもっと賢くしようとするのではなく、入り口で意味を正規化して、同じ意味の問いを必ず同じ答えに収束させる。今日はその実験記録(社内コードネーム SlimeLab)を紹介します。
発想:意味が同じなら、同じ「鍵」に畳む
キャッシュを思い浮かべてください。同じ質問が来たら、計算せずに前の答えを返す——速いし、何より 毎回同じ答えになります。
問題は「同じ質問」の判定です。普通のキャッシュは文字列が一致しないと効きません。「富士山の高さは」と「富士山は何メートル」は別の文字列なので、別物として扱われ、LLM が毎回ぶれた答えを作ってしまう。
そこで、意味が同じ問いを、決定論的に同じ鍵(キャッシュキー)へ畳む正規化層を入り口に置きました。鍵が一致すれば、ひとつの確定した答えを返せる。ぶれは原理的に消えます。
ポイントは「決定論的に」です。ここに私たちのこだわり——bit-exact(ビット完全)があります。
こだわり①:計算の真ん中を、ごまかさない
「3,776.12 m」と「3.77612 km」と「377,612 cm」は、同じ長さです。これを同じ鍵に畳むには単位を揃える必要があります。素朴にやると——
3.77612 * 1000 == 3776.12 # → False
# 実際の値: 3776.1200000000003
浮動小数点(float)で計算すると、末尾の桁がずれます。しかもこのずれは CPU やライブラリのバージョンで変わりうる。「計算結果がビット単位で再現する」という保証が、計算の真ん中では実は無いのです。通信(誤り訂正)やストレージ(ハッシュ照合)は何十年も「両端」でビット完全を作り込んできたのに、計算・変換の真ん中はずっと「だいたい合ってる」で済まされてきました。
私たちは単位変換を 有理数(分数)で閉じました。
from fractions import Fraction
Fraction("3776.12") # = 94403/25
Fraction("3.77612") * 1000 # = 94403/25 ← 厳密に一致
Fraction("377612") / 100 # = 94403/25 ← これも一致
94403/25 という既約分数で厳密に一致するので、3 つの表記は同じ鍵に畳めます。float の桁ずれは入り込みません。これが SlimeUnit(単位の正規化)です。
こだわり②:時刻は「基準日」を明示する
「3 日前」「先週の金曜」「2026/06/24」が同じ日を指すこともあります。でも「3 日前」は いつから見て 3 日前なのか? 基準日が無ければ決まりません。
世の中の日付ライブラリは、相対日付をその場の システム時刻(now) で解決します。便利ですが、これは 非決定論です。同じログを後で監査して「あの日のバッチを再現せよ」と言われても、now() が変わっているので再現できない。
そこで SlimeClock は、now() を一切使わず、基準日(as_of)を明示的に受け取って整数演算だけで解く設計にしました。
as_of = 2026-06-27(土)
3日前 → 2026-06-24
2026/06/24 → 2026-06-24
6月24日 → 2026-06-24 ← 全部同じ鍵
金融の「T+3 営業日決済」も同じ枠組みです。週末と祝日(自社の創立記念日まで含めた営業日カレンダー)を整数で飛ばして着地日を確定します。
T+3(as_of=6/27 土)
→ 土27・日28 は週末でスキップ
→ 月29(1)、火30 は創立記念日でスキップ、水7/1(2)、木7/2(3)
→ 2026-07-02
基準日とカレンダーさえ固定すれば、何度計算しても同じ日。サーバの時刻ズレに左右されません。
こだわり③:分からないものは、でっち上げない
ここが LLM と最も違うところです。
LLM は知らないことを聞かれても、それらしい答えを 作ってしまいます(ハルシネーション)。私たちの正規化層は逆で、判断material が足りないものは「分からない(保留)」のまま止めます。これを内部では Null Slot(空きスロット)と呼んでいます。確率的にでっち上げる代わりに、決定論的に「分からない」と言う。
たとえば人物の特定。SlimeWho は社内の人事データと突き合わせて人物参照を解決します。
- 「田中部長」「田中一郎」「t.tanaka@…(社内メール)」「営業の田中さん」「社員番号 E1001」→ すべて同一人物として同じ鍵へ
- ただの「田中」→ 社内に田中さんが 2 人いれば、どちらか分からないので保留。勝手に片方に決めない
「勝手に決めない」が決定的に重要です。稟議の承認者を取り違えたら監査で命取りになります。間違えるくらいなら、止まる。
※ 人物データは社外に出しません。照合は社内データ内で完結し、共有ログには社員番号も名前も出さず、復元不可能なハッシュ(仮名)だけを記録します。
割れたときだけ、裏を取る
冒頭の富士山に戻ります。「3,776.1 / .12 / .24」と答えが割れたとき、どれを正解として鍵に固定すべきか?
ここで LazyVerify(怠惰な裏取り)という仕組みを入れました。考え方はシンプルです——答えが一致しているときは何もしない。割れたときだけ裏を取る。
1. まず多数決。今回は 3,776.12 が最多(無料・オフライン)。
2. それでも決まらない/慎重を期す場合だけ、信頼できる出典を 1 回だけ参照。実際に Wikipedia の富士山ページを引くと「山体の最高地点は 3,776.12 m」と書かれていました。
3. 確定した答えを凍結。以後は参照せず、同じ鍵には同じ確定値を返す。
「割れたときだけ」なので、外部参照は実トラフィックのごく一部でしか発火しません。そして一度確定したら凍結されるので、その後はずっとビット完全に再現します。1 回だけの不確定を境界で確定し、中身は決定論に保つ——通信やストレージが「両端」でやってきたことを、計算の真ん中で再現した形です。
仕上げ:「誰が・いつ・いくら・何を」を 1 つの鍵に
ここまでの 3 つ(人物・時刻・金額)を束ねると、面白いことが起きます。次の 3 文は、表記がまったく違います。
- 「田中部長が 3 日前に 50 万円承認した」
- 「田中一郎が 2026/06/24 に 500,000 円承認した」
- 「t.tanaka@… が 6 月 24 日に ¥500000 を承認した」
これを正規化すると、3 文とも
( 承認者=E1001, 日付=2026-06-24, 金額=¥500000, 行為=承認 )
になり、同一の監査キー(sha256) に畳まれます。表記の揺れに関係なく「誰がいついくら何を」が一意に確定する。しかも先ほどの原則どおり、承認者が曖昧な「田中が承認した」は 保留扱いになり、別キーへ落ちます——誰だか分からない決裁を、確定ログに素通りさせない。
J-SOX のような内部統制監査では、まさにこの「承認者の一意特定」と「再現性」が肝です。表記の海外取引(円とドル)も、為替レートをその日付で固定すれば同じ鍵に乗ります。
結果
小さな実験ですが、手応えのある数字が出ました。
- 同義クエリの正規化:11 通りの言い回し → 4 つの鍵に収束(残り 7 件は確定済みの答えを再利用)
- ぶれの実測:生の LLM は temperature=0 でも同義 4 変種に 4 通りの別回答。正規化+確定で 完全に一致
- すべて LLM 不使用・浮動小数点 不使用・外部ライブラリ依存ゼロ。整数・有理数・ハッシュだけ
- ログには本文を一切残さない(ハッシュとフラグのみ)設計で、機密を保ったまま効果を計測可能
「賢い AI」ではなく「ぶれない仕組み」。確率的な層(LLM)はどうしても best-effort ですが、決定論的な層(正規化+確定)が安定を担保する。この役割分担が芯です。
なぜこれをやるのか
私たちは「計算結果はビット単位で再現すべきだ」という、地味だけれど大事な原則(bit-exact)を、業務の現場に持ち込もうとしています。計算が一致しても、代表に選んだ答えが嘘だったり、承認者が曖昧だったりすれば意味がない。 だから「一致(再現性)」に「正しさ(裏取り)」と「特定(誰だか確定)」を足す。そこまでやって初めて「決定論的であること=正しいこと」が成立します。
AI が賢くなるほど、AI が でっち上げないための足場が重要になります。分からないものを分からないと言い、揺れを構造で消し、確定したものは何度でも再現する——その地味な土台づくりの記録でした。
この記事の内容は社内の実験(PoC)に基づきます。製品仕様ではありません。SlimeClock / SlimeUnit / SlimeWho / LazyVerify は実験上のコードネームです。
