iT邦幫忙

2026 iThome 鐵人賽

DAY 29
0
Claude AI

從零開始 Claude Code:30 天打造 AI 任務管理 Web App系列 第 29 篇

[Day 29] 把 Groq AI 任務拆解正式接進 Flask Web App

  • 分享至 

  • xImage
  •  

做到 Day 28 的時候,我的專案其實已經有兩套可以獨立運作的東西。
一套是原本的任務管理 Web App:

Flask
  ↓
MySQL
  ↓
新增 / 編輯 / 刪除任務
  ↓
狀態、分類、篩選

另一套則是 Day 25~27 完成的 AI 任務拆解:

groq_api_test.py
	↓
Groq API
	↓
openai/gpt-oss-20b
	↓
Structured Outputs
	↓
parse_tasks()
	↓
5~8 筆任務

但前面這兩條線一直都是分開的。
我可以在網頁管理任務,也可以在終端機叫 AI 拆解任務,但還不能直接在 Web App 裡使用 AI。
所以 Day 29 的目標很明確:

把 Groq AI 任務拆解正式接進 Flask Web App。

而且不是 AI 一產生內容就直接寫進資料庫,而是:

輸入大型目標
   ↓
AI 拆解
   ↓
先預覽
   ↓
使用者確認
   ↓
再寫進 MySQL

這也是今天最主要的流程。


今天完成後的實際流程

最後實作出來的流程是:

 首頁
  ↓
AI 任務拆解
  ↓
輸入大型目標
  ↓
Groq API
  ↓
取得 5~8 筆 title / description
  ↓
網頁顯示預覽
  ↓
確認建立
  ↓
一次寫進 MySQL
  ↓
回首頁看到任務

而且確認建立時:

不會再呼叫一次 Groq

使用者在預覽頁看到什麼,最後存進資料庫的就是同一批內容。
這點是我今天特別想保留的設計。


先把 AI 核心從測試程式拆出來

前幾天 AI 的所有程式都放在:

groq_api_test.py

但裡面其實混了兩種東西。
一種是真正可以重用的 AI 核心:

SYSTEM_PROMPT
TASKS_SCHEMA
parse_tasks()
Groq client
API 呼叫
錯誤處理

另一種只是終端機測試需要的:

sys.argv
print()
sys.exit()
token 顯示
命令列提示

如果 Flask 直接去:

from groq_api_test import ...

雖然不一定不能跑,但結構會很奇怪。
所以 Day 29 新增了一個:

ai_service.py

現在的分工變成:

ai_service.py
→ AI 共用核心

groq_api_test.py
→ CLI 測試入口

app.py
→ Flask Web App

而且 ai_service.py 不會 import:

Flask
db
Task
Category

它只負責一件事:

大型目標
   ↓
呼叫 Groq
   ↓
驗證結果
   ↓
回傳 tasks

這樣 AI 邏輯和 Web 邏輯就分開了。


哪些內容搬進 ai_service.py?

Day 27 已經測過的核心內容直接沿用:

BASE_URL
MODEL
SYSTEM_PROMPT
TASKS_SCHEMA

MIN_TASKS
MAX_TASKS
TASK_KEYS

parse_tasks()

Structured Outputs

reasoning_effort="low"

max_completion_tokens=2048

Prompt 沒有重新改。
JSON Schema 沒有改。
parse_tasks() 規則也沒有改。
所以 Day 29 做的是「整合」,不是重新調模型。


新增 AI 拆解頁面

Day 29 新增了一個獨立頁面:

/ai/breakdown

我沒有把整個 AI 表單直接塞進首頁。
因為首頁本來已經有:

任務列表
狀態篩選
分類篩選
新增任務

再加一大塊 AI 預覽會變得太擠。
所以首頁只增加:

AI 任務拆解

按下後才進 AI 頁面。
https://ithelp.ithome.com.tw/upload/images/20261013/20183857Vg04OB0fU2.png
AI 頁面則有一個大型目標輸入框。
例如:

我想開始學攝影,希望兩個月後可以自己拍出一組看起來完整的作品。

按下:

產生預覽

之後才會呼叫 Groq。
https://ithelp.ithome.com.tw/upload/images/20261013/20183857DurMYSPXLy.png


大型目標也要先驗證

前端雖然有表單驗證,但我不想只依賴 HTML。
後端還是會先做:

goal = (request.form.get("goal") or "").strip()

所以:

空字串
純空白
缺少 goal

都不會呼叫 Groq。
而是直接顯示:

請輸入大型目標

目前另外設定:

最多 1000 字

前端有:

maxlength=1000

後端也會再檢查一次。
這樣即使有人繞過 HTML,也還是會被後端擋住。


AI 不直接寫資料庫

這是今天最重要的設計之一。
如果流程是:

輸入目標
  ↓
Groq
  ↓
直接 INSERT

問題是使用者根本還沒看 AI 拆得好不好。
而我前面已經實際看到:

同樣一個大型目標,重新產生後,任務切法可能不一樣。

所以如果沒有預覽,AI 一產生就直接寫進資料庫,使用者根本沒機會選。
因此現在一定先:

AI 拆解
 ↓
預覽

預覽結果先不寫 MySQL

產生預覽後,畫面會列出:
https://ithelp.ithome.com.tw/upload/images/20261013/20183857z8NyYuYD6G.png
這個時候:

MySQL 還沒有增加資料

只有按:

確認建立 N 筆任務

才會真的寫入。


預覽和確認之間怎麼保存資料?

這裡是今天一個滿重要的設計問題。
因為 Flask 是:

request
   ↓
response
   ↓
request

預覽是一個 request。
確認建立又是下一個 request。
那第二次 request 要怎麼知道剛才 AI 產生了哪些任務?
我最後沒有加入:

session
新資料表
server-side cache

而是直接用 hidden input:

<input type="hidden" name="tasks_json" ...>

概念是:

AI 回傳 tasks
   ↓
放進預覽頁
   ↓
同時放進 hidden tasks_json
   ↓
 按確認
   ↓
tasks_json POST 回 Flask

這個方法很簡單,也不用新增 SECRET_KEY 或暫存資料表。


confirm 不會重新呼叫 Groq

這點我有特別驗證。
流程是:

POST /ai/breakdown
   ↓
Groq 呼叫 1 次
   ↓
產生預覽

POST /ai/breakdown/confirm
   ↓
Groq 呼叫 0 次
   ↓
直接寫入剛才那批預覽

所以不會發生:

預覽看到 A
  ↓
按確認
  ↓
AI 又重新產生 B
  ↓
資料庫存 B

使用者看到的,就是最後存進去的。


hidden input 不能直接相信

hidden input 雖然藏在頁面裡,但使用者還是可以開 DevTools 修改。
所以確認建立時,後端不能直接:

收到 tasks_json
    ↓
直接寫 DB

而是會重新:

parse_tasks()

確認:

是不是 JSON
是不是 {"tasks": [...]}
是不是 5~8 筆
是不是只有 title / description
是不是 string
是不是空字串

全部通過才繼續。


今天還真的抓到一個 tasks_json Bug

一開始實作時,預覽頁存的是:

[
  {
    "title": "...",
    "description": "..."
  }
]

也就是只有 list。
但 parse_tasks() 從 Day 26 開始期待的格式其實是:

{
  "tasks": [
    {
      "title": "...",
      "description": "..."
    }
  ]
}

所以原本流程變成:

預覽正常
  ↓
按確認
  ↓
parse_tasks(tasks_json)
  ↓
格式錯
  ↓
 400

也就是:

每次確認都一定失敗。

後來把 hidden input 改成保存完整:

{
  "tasks": [...]
}

確認 route 才可以直接重用原本的 parse_tasks()。
這個 Bug 也讓我更確定:

前面先把資料格式固定好,後面整合時真的比較容易抓問題。


AI 任務怎麼對應資料庫?

AI 目前只負責:

title
description

不讓 AI 決定:

status
category
due_date

所以確認建立時,每一筆都會變成:

title
→ AI 回傳內容

description
→ AI 回傳內容

status
→ todo

category_id
→ None

due_date
→ None

也就是使用者之後在首頁看到的是:

待處理
未分類
未設定截止日期

https://ithelp.ithome.com.tw/upload/images/20261013/20183857ZnCnIE97xg.png


title 寫入前再做長度檢查

目前 Task model 的 title 是:

db.String(200)

所以 AI 產生的 title 在真正寫入以前,會再檢查:

是否超過 Task.title.type.length

也就是直接從 model 取得長度。
不是另外硬寫:

200

這樣如果 model 之後改長度,驗證比較不容易忘記一起改。


批次寫入只 commit 一次

假設 AI 產生 7 筆任務。
我不希望這樣:

第 1 筆
commit

第 2 筆
commit

第 3 筆
commit

因為如果第 4 筆失敗,前面 3 筆已經進資料庫了。
最後就會變成:

原本 7 筆
	↓
只留下 3 筆

所以現在是:

db.session.add_all(...)
db.session.commit()

整批只 commit 一次。
如果過程出錯:

db.session.rollback()

最後就會:

全部成功
   或
全部失敗

不會只留下半批。


為什麼 AI 任務是反向寫入?

這是今天一個比較有趣的小細節。
AI 拆出來的任務本來是有順序的。
例如:

1. 規劃內容
2. 設計版面
3. 建立網站
4. 測試網站

我希望首頁看到的也是這個順序。
但目前首頁的排序方式是:

created_at DESC
id DESC

意思是「最新建立的任務排在最上面」。
如果照 1 → 4 的順序寫進資料庫,id 會一路變大:

1 → id 25
2 → id 26
3 → id 27
4 → id 28

首頁又會把 id 比較大的排前面,所以最後顯示會變成:

4
3
2
1

這樣就把 AI 原本的任務順序顛倒了。
所以目前的做法是:
寫入資料庫時先從最後一筆開始:

4
3
2
1

這樣 id 會變成:

4 → id 25
3 → id 26
2 → id 27
1 → id 28

等首頁再用 id DESC 排序後,就會重新顯示成:

1
2
3
4

所以「反向寫入」不是因為 AI 的資料本身要倒過來,而是為了配合首頁「新資料排前面」的排序方式。
雖然同一批 AI 任務的 id 大小會和原本順序相反,但首頁顯示時仍會維持 AI 原本的 1→N 閱讀順序。


Groq API 錯誤也不能直接變 500

AI 正式接進 Web App 後,錯誤處理就不能再只靠終端機 print。
目前會處理像:

沒有 GROQ_API_KEY
401 Authentication
429 Rate Limit
json_validate_failed
其他 API Status Error
連線失敗
逾時
finish_reason=length
空內容
parse_tasks() 驗證失敗

使用者看到的都是比較簡單的中文訊息。
例如:

AI 這次產生的格式有誤,請再產生一次。

而不是:

failed_generation
request_id
完整 API response

更不會顯示 API Key。


json_validate_failed 目前不 retry

Day 27 已經實際遇過:

json_validate_failed

那次是模型生成 JSON 時少了一個冒號。
今天正式接進 Web 後,我最後還是決定:

先不自動 retry

所以現在如果遇到:

json_validate_failed

就顯示:

AI 這次產生的格式有誤,請再產生一次。

讓使用者自己重新按:

重新產生預覽

先把流程維持簡單。


OpenAI client 加上 timeout

AI API 接進網頁後,如果 API 卡住,使用者就會一直看著頁面等。
所以 client 現在有:

timeout=30

避免單次請求無限制等待。
不過這裡還有一個要注意的地方。
OpenAI SDK 本身仍然可能對:

429
5xx
連線錯誤

做自己的 retry。
所以:

timeout=30

不代表整個使用者等待時間一定只會有 30 秒。
這部分目前沒有另外修改,留作已知限制。


確認按鈕有簡單防連點

確認建立表單現在有:

onsubmit="this.querySelector('button[type=submit]').disabled = true;"

意思是:

第一次按確認
	↓
submit event
	↓
按鈕 disabled
	↓
送出 POST

所以一般快速連點時,可以降低同一個表單送出兩次的機會。
不過我實際操作時完全沒看到按鈕「變灰」。
後來檢查才發現原因很簡單。
第一個:

沒有 :disabled CSS

所以外觀不一定明顯。
第二個:

本機 Flask + MySQL 太快

按下去後馬上:

POST
 ↓
commit
 ↓
302
 ↓
首頁

根本沒有停留在 disabled 狀態給我看。
所以這段現在只是:

基本前端防連點

不是完整的伺服器端防重複提交。
例如:

兩個分頁同時確認

還是可能建立兩批。
但對目前本機單人專案來說,我先不繼續擴大。


實際用真實 Groq 跑完整流程

自動測試跑完後,我還用真的 Groq 跑了一次。
輸入:

我要準備期末專題

Groq 產生:

8 筆任務

在預覽階段:

MySQL 筆數沒有增加

按下確認後:

Groq 呼叫次數仍然只有 1

代表 confirm 沒有再叫一次 AI。
然後 8 筆一次寫進 MySQL。
最後回首頁也可以按照:

第 1 → 第 8 筆

的順序看到。


測試結果

今天測試其實做得滿多。
主要確認:

✓ AI 頁面可以打開

✓ 首頁有 AI 任務拆解入口

✓ 空白目標不呼叫 Groq

✓ 1000 字可以送出

✓ 1001 字會被擋

✓ 預覽時 DB 不新增

✓ confirm 不重新呼叫 Groq

✓ 預覽與寫入內容一致

✓ status = todo

✓ category_id = None

✓ due_date = None

✓ hidden JSON 非法時不寫入

✓ 批次失敗會 rollback

✓ InnoDB transaction 正常

✓ API 錯誤顯示友善訊息

✓ json_validate_failed 不 retry

✓ CLI 仍然可以正常呼叫 Groq

✓ SYSTEM_PROMPT 沒變

✓ TASKS_SCHEMA 沒變

✓ parse_tasks() 規則沒變

✓ 首頁正常

✓ AI 預覽頁正常

✓ MySQL 寫入正常

Day 29 實際改了哪些檔案?

這次有修改:

ai_service.py
app.py
templates/ai_breakdown.html
templates/index.html
static/css/style.css
groq_api_test.py

其中:

ai_service.py

是新檔案。
沒有修改:

Task model
Category model
schema.sql
config.py
requirements.txt

資料庫 schema 也完全不用加新欄位。


Day 29 做完後,整個專案終於接起來了

Day 28 以前,其實還是:

Web App ── MySQL

和:

Groq ── AI 任務拆解

兩條分開的線。
Day 29 做完後,第一次變成:

使用者
  ↓
Flask Web App
  ↓
Groq AI
  ↓
Structured Outputs
  ↓
 預覽
  ↓
使用者確認
  ↓
MySQL
  ↓
任務列表

到這裡,這個專案才真的符合:

AI Task Manager

這個名字。


Day 29 還有哪些已知限制?

目前功能已經可以用,但還有一些已知限制。
第一個:

預覽頁重新整理
	  ↓
瀏覽器重新送 POST
	  ↓
可能再呼叫一次 Groq

第二個:

timeout=30

但 SDK 自身 retry 可能讓實際等待時間更久。
第三個:

disabled

只有基本前端防連點。
沒有伺服器端 idempotency。
第四個:

json_validate_failed

目前 Web 上只有用假的錯誤物件測過,實際瀏覽器操作時還沒有剛好遇到真的一次。
這些目前都不是阻塞問題,所以先留著。


今天最大的收穫

今天我覺得最重要的其實不是:

成功呼叫 Groq API

因為這件事 Day 25 就已經做到了。
今天真正重要的是:

把 AI 變成 Web App 裡一個完整的使用流程。

API 能呼叫,只代表:

技術上能用

但真正放進產品流程後,還要考慮:

輸入怎麼驗證?

AI 結果要不要先預覽?

確認時要不要重新呼叫?

資料怎麼在兩次 request 之間保存?

寫資料庫失敗怎麼 rollback?

怎麼避免錯誤直接變成 500?

AI 結果怎麼對應現有 Task?

這些才是 Day 29 真正花時間的地方。


上一篇
[Day 28] 功能先停一下:整理專案、清掉暫時程式與確認目前狀態
下一篇
[Day 30] 最後一天:完整驗收、整理專案,替 30 天畫下句點
系列文
從零開始 Claude Code:30 天打造 AI 任務管理 Web App 共 30 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言