dimesum

ホーム / ブログ / エンジニアリング

エンジニアリング

自然言語の経費パーサーを削除した

· 読了 約6分 ·

ルールベースのパーサーは1週間しか持たず、4つの平凡な文が金額を誤った人物に割り当て、1つの拒否ルールがガードの積み重ね全体を置き換えました。

書き終えたばかりのパーサーを削除しました。Dimesum が最初に試みた自然言語の経費入力はルールエンジンで、4つの平凡な文がそれを引退させました。dinner 12-08 400 は ¥12 と読み取られ、ravioli 600 は Ravindra という名前のメンバーを割り勘に入れ、kirana 500 は Kiran という名前のメンバーに請求し、refund -12000 は ¥12,000 の請求になりました。

文を理解することはモデルの仕事であり、他の何の仕事でもありません。ルールエンジンを置き換えたのは amount_only と呼ばれる1つの段です。テキストに曖昧さのない金額候補がちょうど1つあるときだけ返される単一の数字であり、人物についての主張は決してしません。候補が2つあれば金額はありません。1つのルールが、積み上がっていくガードの山を置き換えました。

4つの文がルールパーサーを終わらせた

削除されたパーサーは、私たちの AI 設計ドキュメントが定めるエンティティ一式をすべて扱っていました。メンバーとニックネームの照合、除外辞書、支払者の推定、カテゴリ辞書、そしてフィールドごとに調整された信頼度です。以下の4つの失敗にはいずれも明白な修正策がありました。どの修正策も、それ自身の死角を持つ新しいガードでした。

Dimesum のルールパーサーを引退させた4つの失敗。docs/tech/10-ai-design.md に 2026-08-20 として記録。
ユーザーが入力したものパーサーがしたことなぜ起きたか
dinner 12-08 400金額を ¥12 と読み取った人間は日付と合計を見る。正規表現は3つの数字を見て、最初のものを取る。
ravioli 600Ravindra という名前のメンバーを割り勘に加えたあいまいなニックネーム照合が、料理を人物として採点した。
kirana 500Kiran という名前のメンバーに請求した同じ照合器で、今度は店名だった。
refund -12000¥12,000 の請求を登録した数字は残り符号は消えたので、金額は逆向きを指した。

4つのうち2つは、違う服を着た1つのバグです。あいまい照合は料理と人物、店と人物を区別できません。文字レベルでは ravioliRavindra は本当によく似ているからです。より長い接頭辞の一致を求めれば ravi が壊れますが、それこそ照合器が存在する目的の事例です。

ガードを1つ足すたびに、言い回しが2つ現れる

ルールパーサーは特定の形で失敗します。自信を持って、しかも誤って答えるのです。空欄のフィールドはユーザーにタップ1回の手間をかけます。誤った割り勘は台帳への信頼を損ないますし、お金を分け合うトラッキングアプリでは台帳こそが製品です。上の4行は惜しい失敗ではなく、金額のバグです。

本当の論点は個々の欠陥ではなく、この終わりのない繰り返しです。日付ガードを足せば注文番号の事例が来ます。注文番号ガードを足せば部屋番号が来て、次に数量、次にテーブル番号が来ます。ガードの一覧は増え続けて収束しません。自然言語には列挙し切れる有限の言い回しの集合が存在しないからです。

変更前と変更後: 増えていくガードの積み重ねが、1つの拒否ルールに置き換わる 変更前: ルールパーサー(削除済み) dinner 12-08 400 金額の正規表現 日付ガード 注文番号ガード ニックネーム照合 除外辞書 ¥12 と読み取り、登録する 修正のたびに言い回しが2つ現れた 変更後: amount_only dinner 12-08 400 曖昧さのない金額候補が ちょうど1つあるか いいえ、数字が3つ 金額は空欄のまま はい、数字が1つ 最小単位で返す この段は人物について何も述べないので、 金額を誤った人物に割り当てることはない
削除されたパーサーはすべての文に答えた。置き換えた段は、数字を返すか何も返さないかのどちらかだ。
判断の記録

自然言語の入力は AI の機能であり、Dimesum のリポジトリのパターンにはどれも一致しません。この判断は 2026-08-20 に、4つの失敗とともに書き留められました。誰かがうっかりガードを作り直さないためです。

出荷したのは1つの数字と拒否

amount_only は、私たちの AI 設計ドキュメントにある劣化時の段を文字どおり実装したものです。その段には「クライアント側の金額正規表現で事前入力した素のフォーム」とあり、ティアは金額だけを返して他は何も返しません。説明も、カテゴリも、支払者も、参加者も、除外もありません。何のコストもかからず誰も呼び出さないので、テストと CI はこれを使って動きます。

曖昧さは拒否であって、勝敗の決着ではない

ルール全体は extract_amount_minor という1つの関数の中にあります。テキストにその候補がちょうど1つあるときだけ数字が返るので、order 90210 dinner 400flat 402 rent 15000 は勝者を選ばず空欄を返します。通貨記号の付いた数字は、裸の数字と並んでいても曖昧でないと見なされます。だから split 3 ways ¥1,200 はいまも ¥1,200 と読み取られます。

符号を打ち消された数字は、その絶対値ではなく、そもそも金額ではありません。-500minus 200、そして会計士の書く (500) はいずれも空で返ります。このフィールドは請求であり、数字を残して符号を落とせば、金額はテキストと逆向きを指すからです。括弧は数字そのもので閉じるときだけ数えられるので、(500 each) は括弧の注記のままです。

amount_only が1つの数字と回答なしをどう判断するか 金額になりうる数字をすべて集める 1つも無い、またはいずれかが 負の符号を持つか いいえ 通貨記号の付いた数字が ちょうど1つか(例: ¥1,200 や 500/-) いいえ 記号がまったく無く、 裸の数字がちょうど1つか はい amount_minor: 整数の最小単位、 ISO 4217 の指数を用いて はい いいえ 金額なし フィールドは空欄になる はい
3つの問い。そのうち2つは空欄で終わる。もっともらしい2つの金額から選ぼうとして、あの ¥12 のディナーが生まれた。

通貨が計算を決める

Dimesum で金額が取る表現は最小単位だけなので、ティアは 100 倍のハードコードではなく ISO 4217 の指数表で変換します。日本円には補助単位がまったく無く、×100 は ¥1,200 のレシートを百倍に膨らませます。通貨の最小単位より細かい数字は、丸めるのではなく拒否されます。誰かが入力した金額を丸めることは、金額を捏造することだからです。

インドの数の単位表記は、数字の書き方の一部であって、文を理解することの一部ではありません。1.2k2 lakh500/- はいずれも 1,200 と同じように解決されます。台帳の最大金額を超えるものは空で返るので、打ち間違えた数字は、下流で整数をあふれさせる代わりに1つのフィールドを空欄にします。

各パース段が主張してよいこと。2026年8月29日時点。
フィールドルールパーサー(削除済み)amount_only(稼働中)モデルティア(配線済み、鍵なし)
金額複数の数字から推測曖昧さのない数字1つ、なければ空欄文脈で読み取る
説明、カテゴリ辞書照合常に null文から抽出
参加者、除外あいまいな名前照合常に空実在のメンバー ID に解決
支払者言い回しから推定常に空名前付き、金額は null 可
全体の信頼度フィールドごとに調整0.3 固定パースごと
報告されるプロンプト存在しなかったnull、プロンプトは読まれなかったプロンプト ID とバージョン

稼働中の段は 0.6 の実用ゲートを決して越えない

私たちのパース契約は、全体の信頼度が 0.6 を下回るパースを放棄し、ユーザーを素のフォームに落とします。amount_only はどの回答でも 0.3 を報告し、この定数は調整されたものではなく構造的なものです。単独の金額はパースではないので、何を見つけようとこの段はゲートの半分に位置します。すべての回答で1つの定数を使うことが、それをそこに留めます。事例ごとのスコアは、いずれ誰かが上へ押し上げるスコアだからです。

このティアはプロンプトもまったく報告しません。prompt_idprompt_version も null で返ります。この段はプロンプトを読まなかったからです。名前を付ければ、ティアが一度も見ていないプロンプトのバージョンにすべての評価結果を帰することになります。そして、ある段をワンタップの提案に昇格させてよい唯一の道具は評価ハーネスであり、金額と参加者を合わせて 95% の精度を条件とします。

さらに2つのティアが宣言されていますが、どちらも稼働していません。cheap-fast ティアには openai/gpt-oss-120b 向けの Groq アダプターがありますが鍵はありません。中位のティアにはアダプターがありません。どちらを選んでも、ユーザーの最初のリクエスト時ではなく起動時に失敗します。LLM は呼び出しごとに課金され、課金対象の依存はフェイルクローズでなければならないからです。

自動では何も登録されないので、空欄はタップ1回で済む

空欄のフィールドの代償がこれほど小さいのは、Dimesum のどのキャプチャも金額を書き込めないからです。キャプチャは提案を作り、人がそれを確認し、その確認が経費を作ります。ブリーフの決定 D5 がこのルールを述べ、.go-arch-lint.yml がそれを強制します。取り込みコンテキストには expense や ledger への依存が一切許されないので、キャプチャが誤って仕訳を登録することはできません。CI は取り込みを失敗させます。私たちは実際に1つ追加して確かめました。

キャプチャはパーサーが何をしようと生のテキストを保持し、どのフィールドが未解決かを示します。クライアントは捏造した下書きを見せる代わりにその空欄を強調します。これが、何も言わないパーサーと推測するパーサーの違いです。確認は経費 ID を提案 ID から導くので、二度タップしても二重に請求されるのではなく再生されます。

盗む価値のあるルール

バグではなく、ガードを数えてください。毎週増えていくガードの一覧は、その仕事が理解であることを告げています。そして理解はモデルのものです。私たちの次の一歩は、contracts/parse_expense/eval/ にあるゴールデンセットを実際のモデルティアに対して走らせることです。その判定なしには、ここにあるものは何もワンタップの提案にはならないからです。

よくある質問

Dimesum はなぜルールベースの経費パーサーを削除したのですか?

4つの平凡な文が金額のバグを生み、追加したガードはどれも新たな言い回しを2つ表面化させたため、Dimesum はそれを削除しました。dinner 12-08 400 は ¥12 と読み取られ、ravioli 600 は Ravindra というメンバーを加え、kirana 500 は Kiran というメンバーに請求し、refund -12000 は ¥12,000 の請求になりました。文を理解するのはモデルの仕事です。

amount_only ティアは実際に何を返すのですか?

amount_only ティアは1つの数字だけを返し、他には何も返しません。テキストに曖昧さのない金額候補がちょうど1つあるときだけ金額で答え、参加者、支払者、説明、カテゴリを名指すことは決してありません。候補が2つあれば金額はまったく返りません。パース契約の残りはすべてモデルティアを待ちます。

amount_only の信頼度はなぜ 0.3 固定で本当のスコアではないのですか?

0.3 は調整値ではなく構造的な値です。私たちのパース契約は 0.6 を下回るパースを放棄してユーザーを素のフォームに落としますが、単独の金額はパースではないので、何を見つけようとこの段はそのゲートの半分に位置します。すべての回答で固定の定数を1つ使うことで、事例ごとのスコアが後から押し上げられるのを防ぎます。

自然言語のキャプチャは人の確認なしに台帳へ書き込めますか?

いいえ。キャプチャは提案を作り、人がそれを確認します。ブリーフの決定 D5 のとおりです。このルールは .go-arch-lint.yml で強制され、取り込みコンテキストに expense や ledger への依存を一切許さないので、キャプチャが仕訳へ手を伸ばせば CI が取り込みを失敗させます。経費を作るのは確認です。

モデルティアが使えないとき自然言語の経費パースはどうなりますか?

Dimesum は amount_only に劣化し、空欄を表示します。cheap-fast ティアは Groq の openai/gpt-oss-120b に配線されていて鍵なしで出荷され、中位のティアにはアダプターがないので、どちらを選んでもユーザーの最初のリクエスト時ではなく起動時に失敗します。いずれの場合もキャプチャは生のテキストを保持します。