TL;DR
- 妻がレシートを見て手で仕分けしていた家計の按分を、アプリにしました。妻の電卓仕事がなくなりました。レシートを撮る ⇒ Gemini が品目を読んで「子ども費 / 夫婦生活費 / 対象外」に振り分ける ⇒ 人が確認して保存 ⇒ 月ごとの集計を見る、という流れです
- 自分が手を動かした時間は2〜3時間。内訳は仕様固めが7割、レビューが2割、動作確認が1割で、コードは1行も書いていません
- AI が書いたテストは330本。テストを通ったあとに見つかったコードのバグは1件
- 効いたのは「実装する前に、AI に仕様書をレビューさせた」ことです。人がレビューしてOKを出した仕様書から、AI が9件のあいまいな点を見つけました
- 構成は AWS Lambda + DynamoDB + S3 のサーバーレスで、OCR は Gemini の無料枠。ランニングコストはほぼゼロです
- コードは GitHub に置いています ⇒ https://github.com/risong-ku/ocr-gemini-kakeibo
はじめに
我が家では夫婦で家計を按分していて、支出を「夫婦生活費」「子ども費」「その他(個人の支出)」の3つに分ける必要があります。この分け方が少し特殊で、市販の家計簿アプリの「食費・日用品・交際費」のようなカテゴリとは噛み合いません。欲しい機能は、レシートをアップロードして品目ごとに3つのどれかに分類できればよい、というシンプルなものですが、この独自ニーズにぴったり合うアプリがありませんでした。それまでは妻がレシートを目で見て品目ごとに仕分けし、電卓で集計していました。
もう一つ動機があります。『実践Claude Code入門』でスペック駆動開発(仕様駆動開発)の大枠は理解していて、業務でも断片的に使うことはあったのですが、一度個人開発で最初から最後まできちんとこの流れに沿ってやってみたかったのです。個人用の MVP なので、うまくいかなくても困るのは自分だけ。試すにはちょうどよい題材でした。
この記事では、その過程で人と AI がそれぞれ何をやって、何が効いたか、どこでハマったかをまとめます。
※ 開発環境は Claude Code Orchestra(後述)で、オーケストレーターに Claude Fable 5.1、仕様レビューと実装に gpt-6-astra を使いました
※ 夫婦2人・年500枚程度のレシートを前提にした構成です
作ったもの
レシートを iPhone で撮ると、Gemini が店名・日付・品目を読み取って、品目ごとにカテゴリの候補を付けて返してきます。人はそれを確認画面で直して保存します。AI が迷った品目は黄色くハイライトされるので、そこだけ見ればよい設計です。
保存したものは月ごとに集計され、日別の積み上げグラフとレシート一覧が見られます。対象外にした金額も行ごとに出るので、「あのお金どこ行った?」になりません。
機能はこれだけです。精算の計算まではやらず、CSV で落として妻の既存のやり方につなぐところまでにしました。
開発の進め方
環境
Claude Code Orchestra という構成で開発しました。Claude Code をオーケストレーターにして、Codex CLI(OpenAI)にレビューや実装を委譲する形です。松尾研究所テックブログの記事の雛形に準拠しています。
| 役割 | 使ったもの |
|---|---|
| オーケストレーター(会話、判断の整理、委譲) | Claude Fable 5.1(Claude Code) |
| 仕様レビュー、実装計画、実装 | gpt-6-astra(Codex CLI、reasoning effort は high) |
| 仕様書の並列執筆、監査、README などの大量の読み書き | Claude Opus のサブエージェント |
手順
やった順に書くとこうなります。
- 文書の構成を先に決める。 いきなり作りたいものを説明するのではなく、「仕様の原典になる文書を何にするか」を AI と合意しました。CLAUDE.md と5つの文書(要求定義、機能設計、技術仕様、リポジトリ構造、開発ガイドライン)に、それぞれ何を書くかを決めています
- 音声で喋りまくる。 作りたいものを音声入力で喋り続けると、AI が質問を何度か返してきました。品目単位で分類するか、ホスティングはどうするか、精算計算まで入れるか。答えると、5文書が9個のエージェントで並列に書かれ、最後に検証エージェントが文書間の食い違い10件を直しました
- 人と AI でクロスレビュー。 私は機能設計書のデータモデル・分類ルール・認証の3つの節を重点的に読み、保留になっていた判断4件を決めました。同時に astra に実装計画を立てさせたところ、計画と一緒に仕様のあいまいな点が9件返ってきました(後述)
- 実装。 astra が基盤 ⇒ サービス ⇒ API ⇒ 画面の4回に分けてテスト駆動で実装しました。1回ごとに pytest と ruff を通してコミット
- スモークテスト。 本物の Gemini に実際のレシート2枚を渡して読み取りを確認
- 実機テスト。 ローカルに DynamoDB と S3 の代わり(moto のサーバーモード)を立てて iPhone から一周し、AWS にデプロイして本番でもう一周
人と AI の分担
| 工程 | 人がやったこと | AI がやったこと |
|---|---|---|
| 文書構成 | 5文書の構成と進め方を指定 | 雛形の提示 |
| 仕様固め | 音声で構想を喋る。質問3回に答える | 質問の設計、コスト試算、5文書の執筆と食い違いの検証 |
| 仕様レビュー | 3節を読み、判断4件・データモデル4点・認証3点を決める | 反映。astra があいまいな点9件を指摘 |
| 実装 | なし | 本体と330本のテストすべて |
| 検証 | Gemini の API キー発行、レシート撮影、iPhone で一周、結果を報告 | スモーク用スクリプトとローカル環境の作成、不具合の原因特定と修正 |
| 運用改善 | 使って気づいた要望6件を伝える | 実装とデプロイ |
| 公開 | リポジトリ名・公開範囲・ライセンス・履歴の扱いを決める | 秘密情報と個人情報の監査2回、README 整備、push |
自分が割いた時間は2〜3時間で、その7割が仕様固めです。「動くまで指示を重ねる」いわゆるバイブコーディングはしておらず、仕様 ⇒ テスト ⇒ 実装の順番を崩しませんでした。
アーキテクチャと選定根拠
構成はこうなりました。
iPhone Safari
│ 撮影 → ブラウザ内で画像を縮小(長辺1600px・JPEG)
▼
Lambda Function URL(https、認証なし)
▼
AWS Lambda(Python 3.12 / arm64)
└ Mangum → FastAPI
├ 解析: Gemini 2.5 Flash(REST 直叩き・構造化出力)
├ 保存: S3(画像)→ DynamoDB(レシート+品目)
└ 集計: DynamoDB を全件 Scan して月で絞る
自分だけで作っていたら、たぶん知っている範囲で EC2 に Flask を載せていたと思います。AI に仕様を伝えたら選択肢と試算が返ってきて、私は判断だけをしました。どこで何を選んだかを書きます。
ホスティング:サーバーレス
最初は EC2 を考えていましたが、月1,000円は家計簿にしては高い。そこで「家の遊休 GPU PC を24時間動かせばタダでは」と言ったところ、電気代を試算しました。そうすると月900〜1,500円で、EC2 と変わらないか高くつく。それならと Lambda + DynamoDB + S3 にしました。どれも無料枠があり、夫婦2人の利用量ではまず超えません。
入口:API Gateway ではなく Lambda Function URL
Lambda の前には API Gateway を置くのが定番ですが、今回は Lambda に直接 URL を付ける Function URL にしました。理由は2つで、API Gateway は統合タイムアウトが29秒(REST API)で、Gemini の解析が遅いと切られること。もう一つはリクエスト課金があること。Function URL は無料で、Lambda のタイムアウト(60秒に設定)まで待てます。利用者は2人で認証も自前なので、API Gateway の認可やスロットリングは要りませんでした。
画像:ブラウザ側で縮小してから送る
ここは AI に指摘されるまで知らなかった制約です。Lambda の同期呼び出しはリクエストが6MBまでで、multipart や base64 を経由するとさらに小さくなります。iPhone のカメラ原画は HEIC で3〜10MB あるので、そのままでは送れません。
対策は、ブラウザの canvas で長辺1600pxに縮小し、JPEG 品質0.8で書き出してから送ることです。200〜600KB に収まります。ついでに canvas は必ず JPEG で吐くので、HEIC ⇒ JPEG の変換も勝手に済み、サーバー側に画像ライブラリ(Pillow など)を入れずに済みました。Lambda の zip が小さくなり、メモリも実行時間も節約できます。1600px あればレシートの品目の文字は読めます。
同じ理由で、保存した画像を Lambda 経由で返す API は作っていません。レスポンスも6MB制限があり、画像配信に Lambda の実行時間を使うのはもったいないからです。集計画面は画像を表示しません。
OCR:Gemini 2.5 Flash を REST で直叩き、構造化出力で JSON を強制
SDK は使わず、httpx で generateContent に直接 POST しています。依存が減って Lambda の zip が軽くなるのと、リクエストの中身が全部見えるので、うまく読めないときにプロンプトを直しやすいからです。
リクエストは、画像(inline_data)と、分類ルールを書いたプロンプトと、返してほしい JSON の形(responseSchema)を1回で送ります。JSON の形を強制するので、返ってきたものをそのままパースできます。プロンプトには「child = ベビー・子ども向けと分かるもの」「couple = 家族で使うもの全般」「excluded = 個人の買い物」という定義に加えて、「オムツ M → child」「ビール → couple(酒は食費)」のような具体例を10件ほど入れています。判断がつかない品目は couple に倒した上で uncertain: true を付けさせ、確認画面でその行だけ黄色くする仕組みです。
実際のレシートを HEIC のまま渡しても読めました。1枚15〜20秒かかります。
※ 実装時は REST v1beta の generationConfig で responseMimeType / responseSchema を指定しましたが、現在の公式ドキュメントは別の名称で説明しています。使うときは最新版を見てください
データ:DynamoDB は単一テーブル、集計は全件 Scan で割り切る
テーブルは1つで、パーティションキーがレシート ID、ソートキーが META(レシート本体)か ITEM#001(品目)です。品目の行には日付と店名もコピーして持たせています。集計と CSV を品目の行だけで完結させて、レシート本体と突き合わせる処理をなくすためです。
月次集計は GSI を作らず、全件 Scan してアプリ側で月を絞っています。年500枚 × 6品目で1MB/年に届かないので、DynamoDB の Scan(1ページ1MB)なら数年分でも数ページで終わります。増えて重くなったときのために、月をキーにした GSI への移行手順だけ先に仕様書に書いてあります。
レシート ID は UUID ではなく ULID にしました。先頭がタイムスタンプなので、辞書順に並べるだけで作成順になり、一覧の並び替えに追加のキーが要りません。
保存のタイミング:S3 への書き込みは「保存ボタンを押したとき」
最初の仕様では、解析のときに画像を S3 に置いていました。これは私のレビューで直した箇所です。7月末のレシートを8月にアップロードすることはザラにあるので、解析時に保存すると S3 のキーに入る年月が「アップロードした月」になってズレる。それに、確認画面で保存せずに離脱すると画像だけ S3 に残ります。
そこで、解析時は Gemini にメモリ上で画像を渡すだけにして、S3 への書き込みは確認画面で保存ボタンを押したときに移しました。年月は確定したレシートの日付から取れますし、保存しなかった画像はそもそも S3 に行かなくなりました。ブラウザは縮小済みの画像を手元に持ち続けて、保存時にもう一度送ります。1回200〜600KBなので実害はありません。
認証:共有パスフレーズ1つと署名付き Cookie
利用者は夫婦2人なのでアカウントは作らず、共有パスフレーズ1つで入る形です。ログインすると itsdangerous で署名した Cookie を90日発行し、以降はサーバーが署名を検証するだけで DB は触りません。
ログインの試行回数制限はあえて設けていません。2人利用では総当たりより「自分たちが打ち間違えて締め出される事故」の方が現実的なので、代わりにパスフレーズを12文字以上にして、長さでカバーしています。Cookie の中身にはパスフレーズのハッシュの先頭8文字を入れていて、パスフレーズを変えると全端末が即ログアウトになります。
保存の安全側:まとめて書けない DynamoDB への対処
これも astra に指摘された話です。DynamoDB の BatchWriteItem は1回に25件までで、全体としては「全部成功か全部失敗か」になりません。品目が多いレシートは複数回に分かれるので、途中で落ちると半端なデータが残る。そこで失敗したら書けた分を消して500を返すようにし、あわせて保存ボタンの連打で同じレシートが2件できないよう、確認画面を開いたときに発行する client_token を保存時に送って、同じトークンなら2回目以降は既存の ID を返すようにしました。
デプロイ:zip をクロスビルド
Lambda へは zip でデプロイします。開発機が macOS なので、Lambda の aarch64 向けに uv pip install --python-platform aarch64-manylinux2014 --only-binary :all: で依存を展開してから zip にしています。boto3 は Lambda のランタイムに同梱されているので zip には入れません。3.3MB で収まっています。
仕様駆動開発で効いたこと
人のレビューを通った仕様書から、AI が曖昧な仕様を9件見つけた
私のレビューが終わって「もう実装に入れる」という状態の仕様書を astra に渡し、「実装計画を立てて。仕様のあいまいな点や矛盾は黙って解決せずに列挙して」と指示しました。返ってきたのは11ステップの計画と、次の9件です。
| # | astra の指摘 | 何が起きていたか | どう決めたか |
|---|---|---|---|
| 1 | Gemini のタイムアウト30秒 × 2試行 + 待ち1秒 = 61秒で、Lambda の60秒を超える | 節ごとに数字を書いたが足し算していなかった | 25秒に直した(25+1+25=51秒) |
| 2 | 読み取れない日付や整数でない金額を「警告付きで返す」と書いてあるが、返す JSON にその表現がない | 書き忘れ | 書き足した |
| 3 | BatchWriteItem は25件単位で、途中で失敗しうる。「DB だけ残ることはない」という記述と矛盾 | 私が制約を知らなかった | 失敗時の後始末と client_token を人が決めた |
| 4 | CSV の未認証は401と書いてあるのに、別の節では画面系はログインへリダイレクト。公開ルートの一覧に /static が抜けている | 節ごとの食い違い | そろえた |
| 5 | CSV に店名が要るのに、品目の行には日付しかコピーしていない | 設計意図と矛盾 | 店名もコピーする |
| 6 | 保存時の合計金額を、サーバーが信じるのか計算し直すのか、ズレていたら拒否するのか書いていない | テストの期待値が決まらない | 見たまま保存する、と人が決めた |
| 7 | 機能設計書は「Phase B で本物の Gemini でプロンプト調整」、技術仕様書は「Phase B は本物の API を使わない」 | 並列執筆した文書同士の矛盾 | Phase B の最後にスモークを置く、と人が決めた |
| 8 | 分類ルールの例に「値引き行 = 対象外」とあるが、規則の方は「値引きは対象品目と同じカテゴリ」 | AI が文書を直したときに入れた矛盾を、別の AI が見つけた | 規則の方を正とした |
| 9 | 画像の却下基準、環境変数名、CSV の並び順などが未定義 | 細部の書き忘れ | 書き足した |
3番と6番と7番は人が決めるべき話で、あとの6件は「書いていない」か「矛盾している」で、答えは仕様書の中にありました。実装のあとに見つかっていれば、それぞれ1往復の手戻りになっていたはずです。
1番は単なる足し算で、タイムアウトの秒数、リトライ回数、待ち時間がそれぞれ別の節にあって、単体ではどれも妥当に見えました。人が読んで気づかなかったのはこういうところです。3番は「S3 に書いてから DynamoDB に書くので DB だけ残ることはない」と仕様書に書いてあったのが、そもそも前提が崩れていた例です。8番は、並列で仕様書を書き直したときに片方の AI が入れた矛盾を、実装前に別の AI が拾ってくれました。
ついでに、依存パッケージの抜け(python-multipart)と、雛形コードの品目連番が2桁のまま(仕様は3桁)という実装側のズレも一緒に指摘されました。
人と AI で見つけるものが違った
同じ仕様レビューで、私が見つけたのは「S3 の年月がアップロード月になる」「品目連番2桁では1レシート99品目まで」という使う側の都合でした。astra が見つけたのはタイムアウトの合計や BatchWriteItem の制約という基盤側の話です。役割を決めたわけではないのに、自然にそう分かれました。今回はたまたまかもしれませんが、人が仕様を読んで気づけるのは「自分が使う場面」で、基盤の制約は AI に読ませた方が拾えそうだ、というのが実感です。
実装のバグは1件
4回に分けた実装は、すべて astra の1回の実行で pytest と ruff が通りました。テスト数は 126 ⇒ 200 ⇒ 300 ⇒ 311 と増え、その後の改善分を足して330本になりました。
テストを通ったあとに見つかったコードのバグは1件です。確認画面を開くときに crypto.randomUUID() を呼んでいたのですが、この API は https か localhost でしか使えません。iPhone の実機テストは同じ Wi-Fi の Mac に http://192.168.x.x でつなぐので、ここでエラーになりました。ロジックの間違いというより、実行環境についての知識の穴です。本番は https なので起きません。
動かして初めて分かったこと
仕様駆動で進めても、動かすまで分からないことはあります。ただ、今回出た6件は全部「要望」の話で、コードのバグではありませんでした。
| # | 気づき | 対応 |
|---|---|---|
| 1 | iPhone で写真ライブラリから選べない。<input type="file"> に capture="environment" を付けるとカメラ起動に固定される |
属性を外して、ライブラリとカメラの両方を選べるように |
| 2 | 外食は品目に分ける意味がない。居酒屋は品目が税抜表示で、合計が合わない | 「1行にまとめる」ボタンを追加。店名1行・レシート記載の合計・夫婦生活費にまとまる。Gemini に店の種類を判定させず、人が1タップで決める |
| 3 | 妻から「レシートを捨てちゃうことがある」 | 画像なしの手入力登録 |
| 4 | 対象外にした金額がどこにも出ず、消えたように見える | 一覧の各行に小計と「対象外 N円」を出す |
| 5 | −500円のクーポン行だけ夫婦生活費にして、本体は対象外のまま保存した。夫婦生活費の合計だけ500円減った | プラスの品目がないカテゴリに値引き行があれば警告 |
| 6 | まとめた行の「外食(店名)」という表記が変 | 店名だけに |
学び、良かったこと
-
What さえ決まれば、少なくともこの規模なら形になる
大袈裟ではなく、今回そう感じました。私がやったのは「何を作りたいか」を喋って、出てきた文書を読んで、判断を返すことだけです。How の部分、つまりサーバーレスの構成も、画像の縮小も、DynamoDB の書き込みの後始末も、全部 AI が出してきました
-
AI に書かせる前に、AI にレビューさせる
これが一番効きました。人がレビューを終えた仕様書から9件出た、というのが全てで、モデルの性能を実装の段階だけで使うのはもったいない。実装前に仕様を叩かせると、実装後の往復が減ります。バグが少なかったのは astra の実装力というより、この順番のおかげだと思っています
-
仕様固めに7割。コードを書いていない時間が長いけど、結局これが一番速かった
2〜3時間のうち7割が文書の相談とレビューで、傍から見ると何も作っていない時間です。でも実装は4回とも一発で通り、手戻りは要望ベースの6件だけ。仕様が固まっていれば AI は速い、というのを身をもって感じました
-
とはいえ、How を自分で実践できる力は要る
一方で、AI が出してきた How が妥当かどうかは、自分で判断しなければなりません。S3 に書くタイミングの問題に気づけたのは、自分で似た構成を組んだことがあったからですし、BatchWriteItem の話を「なるほど」で済ませられたのは、DynamoDB を触ったことがあるからです。結局、自分で一回やったことがある領域にしか突っ込めなかったな、と思います。AI 開発の時代だからこそ、むしろ技術の基礎を身につけることが大事だと感じました
リポジトリ
今回のコードと仕様書は GitHub に公開しています。README にローカルで動かす手順(AWS なしで iPhone から試せます)と、AWS へのデプロイ手順を書いてあります。
まとめ
本記事では、夫婦の家計按分という独自ニーズに合う家計簿アプリを、仕様駆動開発と gpt-6-astra でどう作ったかを振り返りました。
振り返ってみると、自分がやったことは
- 何を作りたいかを喋る
- 出てきた仕様書を読んで判断する
- 動かして、気づいたことを伝える
の3つだけで、コードは1行も書いていません。それでも実装は4回とも一発で通り、コードのバグは1件、動かして出た要望は6件でした。
効いたのは「実装する前に AI に仕様をレビューさせる」という順番です。人がレビューを通した仕様書からでも9件のあいまいな点が出て、たぶん大半は実装後なら手戻りになっていたと思います。要するに、AI に実装させる前に仕様を読んで、仕様が曖昧な点をあぶり出させる、さらに自分も仕様書を読み込んで気づいた点はとことん突っ込む、こういった地道なクロスレビューのサイクルを回したことがポイントでした。
同時に、AI が出してきた How が妥当かどうかを判断できないと結局こわい、というのも正直な実感です。AI 時代だからこそ技術の基礎をしっかりと身につけるべき、と改めて思いました。
個人開発で仕様駆動開発を試してみたい方、AI コーディングでどこまで任せられるか知りたい方の参考になれば幸いです。
参考文献
仕様駆動開発・開発環境
- 西見公宏, 吉田真吾, 大嶋勇樹『実践Claude Code入門 ―現場で活用するためのAIコーディングの思考法』技術評論社, 2025
https://gihyo.jp/book/2026/978-4-297-15354-0 - Taisei Ozaki(松尾研究所テックブログ)「Claude Code Orchestra: Claude Code × Codex CLI × Gemini CLIの最適解を探る」
https://zenn.dev/mkj/articles/claude-code-orchestra_20260120 - OpenAI, Codex CLI
https://learn.chatgpt.com/docs/codex/cli - OpenAI, GPT-6 Astra(Models)
https://developers.openai.com/api/docs/models/gpt-6-astra - Anthropic, Claude Fable 5.1
https://www.anthropic.com/claude/fable - Anthropic, Claude Code Overview
https://code.claude.com/docs/en/overview
AWS
- Creating and managing Lambda function URLs
https://docs.aws.amazon.com/lambda/latest/dg/urls-configuration.html - Lambda quotas(同期呼び出しのペイロード 6MB)
https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html - Quotas for configuring and running a REST API in API Gateway(統合タイムアウト 29 秒)
https://docs.aws.amazon.com/apigateway/latest/developerguide/api-gateway-execution-service-limits-table.html - BatchWriteItem — Amazon DynamoDB API Reference(25件ずつ・途中で失敗しうる)
https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html - Amazon DynamoDB pricing — On-demand capacity mode(無料枠、PITR 料金)
https://aws.amazon.com/dynamodb/pricing/on-demand/ - AWS Free Tier now offers $200 in credits and 6-month free plan(2025 年 7 月の無料枠変更)
https://aws.amazon.com/about-aws/whats-new/2025/07/aws-free-tier-credits-month-free-plan/ - Mangum(ASGI アプリを Lambda で動かすアダプタ)
https://github.com/Kludex/mangum
Gemini API
- Structured outputs | Gemini API
https://ai.google.dev/gemini-api/docs/structured-output - Image understanding | Gemini API(対応形式に HEIC、inline data の上限)
https://ai.google.dev/gemini-api/docs/image-understanding - Gemini Developer API pricing(Gemini 2.5 Flash の無料枠)
https://ai.google.dev/gemini-api/docs/pricing
ブラウザ API
- MDN, Crypto: randomUUID() method(安全なコンテキスト限定)
https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID - MDN, capture — HTML attribute
https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/capture - MDN, HTMLCanvasElement: toBlob() method
https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toBlob
ライブラリ・ツール
- FastAPI
https://fastapi.tiangolo.com/ - itsdangerous — URL Safe Serialization
https://itsdangerous.palletsprojects.com/en/stable/url_safe/ - Chart.js
https://www.chartjs.org/docs/latest/ - moto — Server Mode
https://docs.getmoto.org/en/latest/docs/server_mode.html - uv — Resolution(platform-specific resolution)/ pip compatibility(--only-binary)
https://docs.astral.sh/uv/concepts/resolution/
https://docs.astral.sh/uv/pip/compatibility/

