iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Vibe Coding

Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript系列 第 20 篇

# Day 20 :在 Web 中整合 LLM API:使用 OpenAI / Anthropic SDK 實現產品核心 AI 功能

  • 分享至 

  • xImage
  •  

「如果沒有 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。

Human vs. AI:提示詞與協同開發

直接呼叫 LLM 很容易拿到自由發揮的純文字(Free-text),但前端 UI 需要結構化數據(如:adviceList: string[], riskScore: number)。我們必須引導 AI 正確設置 OpenAI 的 response_format 或使用 Vercel AI SDK。

給 AI 的指令(Prompt)

我們正在 Next.js (App Router) 中開發 AI 理財診斷功能。
請幫我撰寫一個 API Route src/app/api/ai/analyze/route.ts。

要求:

  1. 接收 POST 請求,Body 包含交易陣列 transactions。
  2. 使用 openai SDK (v4+) 呼叫 gpt-4o-mini 模型。
  3. Prompt 設計:分析使用者的消費結構,給出財務健康評分(1-100)與 3 條具體建議。
  4. 使用 OpenAI 的 response_format: { type: "json_object" } 確保回傳嚴格的 JSON 結構:
    { "healthScore": number, "summary": string, "recommendations": string[] }
  5. 包含完美的環境變數檢查與 Error Handling。

AI 產出的結果

// 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 });
  }
}

翻車與除錯過程 (Debug Experience)

將這段 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...

AI 哪裡有問題?

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 就直接暴露了。

修正與引導(Human Decision)

結合 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 崩潰。」

修正後版本 (Refactored Code)

經過調整後,我們獲得了新的 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 });
  }
}

JavaScript / 前端筆記

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 完全生成完畢才能看到結果。這體驗還不夠極致!


上一篇
# Day 19:接入第三方服務:支付系統 (Stripe) 或 Email 發送 (Resend) 串接
下一篇
# Day 21:串流回應處理:Web Application 的 SSE (Server-Sent Events) 與 Streaming UI 實作
系列文
Vibe Mode 開啟:30 天用 AI 打造網頁,邊做邊學 JavaScript 共 25 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言