はじめまして。今回は、話題のGoogleの最新AIエージェント開発環境「Google Antigravity」を活用して、個人開発で「会話ですべて完結するAI家計簿アプリ」を作ってみたので、その技術構成や開発中にハマったポイントについて共有したいと思います。
この記事のポイント
・Next.js App Router と Gemini 2.0 Flash を組み合わせたAIアプリ開発
・Google Antigravity とのペアプログラミングによる開発体験
・Vercel AI SDK v5 のハマりどころと解決策
既存の家計簿アプリも素晴らしいのですが、もっと自由に、友達にLINEする感覚で『これ買ったよ』と報告するだけで記録でき、仕訳もできると良いのでは?と思い、Antigravityも使ってみたかったので、自作することにしました。Antigravityを使ったペアプログラミングでの開発体験も合わせてご紹介します。
Google Antigravity とは?
今回の開発で特筆すべきは、Googleが提供する次世代のAIエージェント開発環境 Google Antigravity を全面的に活用したことです。
Antigravityは、単なるコード補完ツール(Copilot的なもの)ではありません。IDEの中に住んでいる「もう一人のエンジニア」として振る舞います。
・ターミナルの操作: npm run dev でサーバーを起動し、エラーが出ればログを読んで勝手に修正提案をしてくれます。
・ファイルの作成・編集: 必要なファイルを自動で作成し、修正も一括で行ってくれます。
計画と実行: 「こういうアプリを作りたい」と伝えると、実装プランを立て、それをタスク分解して一つずつ実行してくれます。
今回は私が「要件オーナー」となり、Antigravityという「優秀なジュニアエンジニア」に指示を出しながらペアプログラミングをする感覚で開発を進めました。 特に環境構築やライブラリのバージョン整合性チェックなど、面倒な作業を丸投げできたため、コア機能のロジック検討に集中できました。
実装前に実装計画を都度立ててくれるので、実装が開始される前に内容を確認し、実装計画をレビューすることでブラッシュアップしていけるのがとても良かったです。

(↑Comment on this lineとあるように、行毎にレビューをコメントで残すことができます。)
作ったもの
AI家計簿アシスタント "AI Kakeibo"
ただの記録ツールではなく、「会話を通じてお金の管理を楽しくする」ことを目指しました。 画面左側にはダッシュボードと履歴、右側にはチャットインターフェースを配置し、PCブラウザでも使いやすい Web First なデザインを採用しています。
こだわりの機能
- 曖昧な入力でもOK: 「コンビニでコーヒーとパン 800円」と打つだけで、AIが文脈を解析。「食費」カテゴリに自動分類し、日付は文言がなければ「今日」として処理します。
- 感情豊かなフィードバック: 単に「記録しました」だけでなく、「コーヒーいいですね、リフレッシュしてください!」など、少し人間味のある返事を返します(システムプロンプトで性格付け)。
- 即時反映のダッシュボード: Material UI の Card コンポーネントを使用し、今月の支出合計を大きく表示。前月比の概念(モックですが)も取り入れ、モチベーション維持を狙っています。
技術スタック
今回は私が今まで触ったことのない技術領域で実装をするというテーマもあり、以下の構成を採用しました。
・Framework: Next.js 15 (App Router)
・AI SDK: Vercel AI SDK (v5.0.108)
・Model: Google Gemini 2.0 Flash (gemini-2.5-flash)
・UI: Material UI (MUI)
・Validation: Zod
・Backend: Next.js Route Handlers
特に Vercel AI SDK Core (v5) と Gemini Flash の組み合わせは強力です。 Gemini Flash は「思考の速さ」が圧倒的で、今回のようなチャットボット用途では体感遅延がほぼゼロに近く、ユーザー体験を損ないません。 また、Vercel AI SDK v5 の streamText API は、単なるテキスト生成だけでなく、Function Calling(ツール実行) の複雑なやり取り(ツール呼び出し→実行→結果返却→最終回答生成)を非常にシンプルなコードで記述できます。
今回はGemini APIの無料枠で実装を進めていたので、リミットに達したタイミングで別モデルに切り替えながら開発をすすめました。Gemini 2.0 Flash → gemini-2.5-flash
実装の詳細と工夫
1. UI/UXデザインの選定 (Material UI vs Tailwind)
当初は Tailwind CSS での構築を検討していましたが、思うようなデザインを出力してくれなかったので方向転換をして、Googleの Material Design 3 (Material You) を採用しました。 MUI (Material UI) v6 を導入し、カラーパレットには温かみのあるオレンジを基調色(Primary Color)に設定しました。
const theme = createTheme({
palette: {
primary: { main: '#ff9800' }, // 親しみやすいオレンジ
background: { default: '#f5f5f5' },
},
components: {
MuiPaper: {
styleOverrides: {
root: { borderRadius: 16 }, // 角丸を多用して柔らかい印象に
},
},
},
});
2. システムプロンプトによる「人格形成」
AIを単なる事務的なボットにしないために、システムプロンプト(systemプロパティ)のチューニングには時間をかけました。
「あなたは親切で優秀なFPアシスタントです」「ユーザーを励ますような口調で」といった指示を与えることで、ユーザーが支出を記録した際に「無駄遣いしましたね」と咎めるのではなく、「記録できて偉いですね!」とポジティブな反応を返すように設計しています。これにより、継続率の向上を狙っています。
3. Vercel AI SDK Core (streamText) によるチャット実装
Vercel AI SDKは、以前はOpenAIStreamなどプロバイダごとのヘルパーを使うのが主流でしたが、最新版では streamTextという統一APIが推奨されています。これにより、Google, OpenAI, Anthropic などのモデルを切り替える際に、コードの変更が最小限で済みます。
import { google } from '@ai-sdk/google';
import { streamText, tool } from 'ai';
import { z } from 'zod';
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: google('gemini-2.5-flash'),
messages: messages.map((m: any) => ({
role: m.role,
// クライアント側の仕様に合わせてメッセージを整形(後述)
content: m.content || (m.parts?.map((p: any) => p.text).join('')) || '',
})),
system: 'あなたは優秀なFPアシスタントです...',
tools: {
addExpense: tool({
description: '支出を記録する',
inputSchema: z.object({ ... }), // ここがハマりポイントでした
execute: async ({ item, amount, ... }) => {
// DB保存処理
return { success: true, message: '記録しました' };
},
}),
},
maxSteps: 5, // ツール実行のために複数往復を許可
});
return result.toUIMessageStreamResponse();
}
2. クライアント側 (useChat) との連携
クライアント側は useChat フックを使うだけで、ストリーミング受信、入力管理、履歴表示がほぼ自動化されます。
'use client';
import { useChat } from 'ai/react';
export default function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat();
return (
// ... UI実装
);
}
非常にシンプルですが、今回ここでのデータ形式の不整合にかなり苦戦しました(後述)。
開発中にハマった壁と解決策
ここが本記事のメインです。最新のSDKとモデルを使ったことによる「ドキュメントの隙間」に落ちてしまい、いくつかのバグに遭遇しました。
※すべてAntigravity上での会話形式で解消した内容です。
事件1:
AIの返答が「虚無(空白)」になる
実装当初、チャットを送るとサーバーからレスポンスは返ってきている(ログには出ている)のに、画面上には中身のない空っぽの吹き出しが表示される現象が発生しました。
原因: streamTextのレスポンス形式としてtoUIMessageStreamResponse()を使用していましたが、この形式ではメッセージの中身がcontentプロパティではなく、partsという配列の中に分割されて格納される場合がありました(特にツール呼び出しを含む場合)。
一方、UI側はシンプルに {message.content} だけを表示しようとしていたため、何も表示されなかったのです。
解決策: UI側のレンダリングロジックを修正し、contentが空の場合はpartsからテキストを結合して表示するようにしました。
<Typography>
{m.content || (m.parts && m.parts.filter(p => p.type === 'text').map(p => p.text).join(''))}
</Typography>
事件2:
2通目を送ると「Invalid prompt」エラー
1往復目は成功するのに、2通目のメッセージを送るとサーバーがクラッシュする現象に遭遇しました。
原因: クライアント側に保存された「1つ目のAIの返信」は、先述の通り content が空で parts がある状態でした。 これをそのままサーバーに送り返して履歴として streamText に渡そうとすると、サーバー側で「contentがundefinedだぞ!」とバリデーションエラーになっていました。
解決策: サーバー側でメッセージを受け取る際にも、parts からの復元ロジックを追加しました。
messages: messages.map((m) => ({
role: m.role,
content: m.content || (m.parts?.map(p => p.text).join('')) || '',
})),
事件3:
AIが突然Pythonコードを書き始める(ツール認識失敗)
支出を記録しようとすると、AIがツールを実行する代わりに、Pythonのコード例を提示してしまっていました。
AIが「自分にはツールを実行する能力がない」と勘違いし、Pythonのコード例を提示してしまっている状態(ハルシネーションの一種)です。
原因: 使用しているaiSDK v5系では、ツール定義のスキーマ指定プロパティ名がinputSchemaでした。
しかし、以前のバージョンの知識や、一部のドキュメントの混同によりparametersというプロパティ名でスキーマを定義していました。 TypeScriptの型チェックを一見すり抜けてしまっていた(あるいは検証不足だった)ため、実行時にAIに正しくツール定義が渡っていませんでした。
解決策: tool() ヘルパーを正しく使い、プロパティ名を修正しました。
// NG
addExpense: tool({
parameters: z.object({ ... })
})
// OK (SDK v5)
addExpense: tool({
inputSchema: z.object({ ... })
})
この修正により、Geminiは正しく「あ、この関数を呼べばいいんだな」と認識し、裏側で関数が実行され、「記録しました」という自然な応答が返ってくるようになりました。
振り返り
開発してみた感想
今回初めてAntigravityでの開発を体験しました。以前、Claudeを使用していたこともありましたが、Antigravityのほうが自律的で、手戻りも少なく確実に良いものを開発できるなと感じました。
AIを活用した機能の実装自体も初めてでしたが、Vercel AI SDKのおかげもあり、かなり簡単に実装できるなといった印象でした。
Flutterで個人アプリを開発・リリースしたときは、数カ月もの期間を要して、かなり苦戦しながら公開した記憶があるのですが、これらのツールを使うことで、誰でも短期間で、かつハイクオリティなアプリケーションが開発できてしまうんだなと、時代の変化も感じる時間でした。
開発体験としてもとても楽しかったので、また何か自分で作ってみたいと思います。


