「如果沒有 LLM 賦能,這只是一個普通的記帳軟體;有了 LLM,它才是真正的智慧財務管家。」
到了階段四的重頭戲!今天我們要為我們的 「AI 智慧個人財務追蹤器 (Smart Finance Tracker)」 注入大語言模型的靈魂。我們要實現核心 AI 功能 —— 「智慧收支診斷與理財建議」,讓系統自動分析使用者近期的消費紀錄,產出客製化的理財改善策略與結構化 JSON 分析報告!
在 Next.js Route Handler (App Router) 中整合 OpenAI / Anthropic SDK。
運用 Structured Outputs (JSON Mode / Function Calling),確保 LLM 回傳嚴格符合前端與資料庫格式要求的 JSON 數據。
掌握在 Vibe Coding 模式下,如何引導 AI 寫出安全(保護 API Key 不洩漏)、強健且具備降級機制(Fallback)的 LLM API Route。
直接呼叫 LLM 很容易拿到自由發揮的純文字(Free-text),但前端 UI 需要結構化數據(如:adviceList: string[], riskScore: number)。我們必須引導 AI 正確設置 OpenAI 的 response_format 或使用 Vercel AI SDK。
我們正在 Next.js (App Router) 中開發 AI 理財診斷功能。
請幫我撰寫一個 API Route src/app/api/ai/analyze/route.ts。
要求:
transactions。openai SDK (v4+) 呼叫 gpt-4o-mini 模型。response_format: { type: "json_object" } 確保回傳嚴格的 JSON 結構:// src/app/api/ai/analyze/route.ts
import { NextResponse } from 'next/server';
import OpenAI from 'openai';
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
export async function POST(req: Request) {
try {
const { transactions } = await req.json();
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: '你是一位專業財務顧問,請分析交易資料並以 JSON 格式回應:{ "healthScore": number, "summary": string, "recommendations": string[] }',
},
{
role: 'user',
content: JSON.stringify(transactions),
},
],
response_format: { type: 'json_object' },
});
const aiResult = JSON.parse(response.choices[0].message.content || '{}');
return NextResponse.json(aiResult);
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 500 });
}
}
將這段 Code 拿去發送測試時,控制台直接跳出大大的 400 錯誤與 JSON 解析失敗:
BadRequestError: 400 'response_format' of type 'json_object' is not supported with this system message. 或者 SyntaxError: Unexpected token in JSON at position...
OpenAI JSON Mode 規範陷阱:
OpenAI 官方明確規定:當開啟 response_format: { type: "json_object" } 時,System 或 User Prompt 中必須明確包含 "JSON" 這個單字,否則 API 會直接拒絕請求(400 Error)。AI 雖然寫了 "以 JSON 格式回應",但如果模型認定 prompt 語意不明確,就很可能解析失敗。
缺少 JSON Parse 的防護與型別驗證:
就算 LLM 回傳了 JSON 字串,內容也可能缺少 recommendations 欄位或型別不符合預期(例如把 healthScore 回傳成字串 "85" 而非數字 85)。直接 JSON.parse 很容易造成前端 runtime 潰敗。
Client-side API Key 洩漏隱憂:
AI 雖然寫在 API Route(Server 端),但沒有加上伺服器端環境檢查,如果有人在 SDK 初始化時不小心把 dangerouslyAllowBrowser: true 開啟,Key 就直接暴露了。
結合 Zod 與嚴謹的 Prompt 工程向 Cursor 發出重構指令:
修正 Prompt:
「1. 請使用 zod 定義 AI 回傳的 Schema,並在解析完 JSON.parse 後使用 zodSchema.parse() 確保型別絕對安全。
2. 請優化 System Prompt,明確指定 JSON key 與型別範例,並要求 Prompt 遵循繁體中文回答。
3. 加上 JSON Parse 的 try-catch 降級機制(Fallback),避免 LLM 解析失敗時整個 API 崩潰。」
經過調整後,我們獲得了新的 LLM 整合介面:
// src/app/api/ai/analyze/route.ts
import { NextResponse } from 'next/server';
import OpenAI from 'openai';
import { z } from 'zod';
// 1. 定義嚴格的 AI 回傳結構驗證
const aiAnalysisSchema = z.object({
healthScore: z.number().min(0).max(100),
summary: z.string(),
recommendations: z.array(z.string()).min(1),
});
export type AIAnalysisResponse = z.infer<typeof aiAnalysisSchema>;
export async function POST(req: Request) {
if (!process.env.OPENAI_API_KEY) {
return NextResponse.json({ error: '未設定 OPENAI_API_KEY 環境變數' }, { status: 500 });
}
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
try {
const { transactions } = await req.json();
if (!Array.isArray(transactions) || transactions.length === 0) {
return NextResponse.json({ error: '請提供有效的交易紀錄' }, { status: 400 });
}
// 2. 呼叫 LLM API
const completion = await openai.chat.completions.create({
model: 'gpt-4o-mini',
temperature: 0.3, // 降低隨機性,提高 JSON 結構穩定度
response_format: { type: 'json_object' }, // 開啟 JSON Mode
messages: [
{
role: 'system',
content: `你是一位精明的個人財務顧問。請分析使用者的交易數據,並嚴格以 JSON 格式輸出繁體中文分析結果。
JSON 必須符合以下格式規範:
{
"healthScore": 85, // 0-100 的財務健康評分 (數字)
"summary": "一句話總結本月財務狀況",
"recommendations": ["建議1", "建議2", "建議3"] // 至少 3 條建議
}`,
},
{
role: 'user',
content: `以下是近期交易紀錄:\n${JSON.stringify(transactions, null, 2)}`,
},
],
});
const rawContent = completion.choices[0]?.message?.content;
if (!rawContent) {
throw new Error('LLM 未回傳任何內容');
}
// 3. 安全解析與 Zod 驗證
const parsedJson = JSON.parse(rawContent);
const validatedData = aiAnalysisSchema.parse(parsedJson);
return NextResponse.json(validatedData);
} catch (error: any) {
console.error('AI Analysis Error:', error);
// Zod 驗證失敗或 JSON 解析失敗時的 Fallback
if (error instanceof z.ZodError) {
return NextResponse.json({ error: 'AI 回傳格式不符預期', details: error.errors }, { status: 502 });
}
return NextResponse.json({ error: error.message || '理財診斷服務暫時不可用' }, { status: 500 });
}
}
Structured Outputs 是 Web 整合 LLM 的命脈:
Web 應用與單純聊天機器人(Chatbot)最大的不同在於「數據必須被 Component 渲染」。使用 response_format: { type: "json_object" } 配合低 temperature,能大幅降低 LLM 回傳亂碼或多餘文字的機率。
Zod 作為 LLM 回傳的防護閘門 (Validation Gate):
永遠不要盲目相信 LLM 產出的字串!在將 JSON 送給前端之前,透過 Zod 或 TypeBox 進行運行時型別檢查(Runtime Type Checking),是避免前端 UI 因為 null/undefined 而爆掉的最佳實踐。
今天我們成功把 OpenAI API 整合進了專案,讓我們的財務追蹤器擁有了精準的「AI 財務健康診斷」功能!
但你可能會發現:點擊診斷後,使用者需要等 2~3 秒等 LLM 完全生成完畢才能看到結果。這體驗還不夠極致!