Day 11 已驗證 Checkpoint 能在重啟後保留對話歷史:chat_once.py 執行兩次,即使中間關閉終端機,仍能接續對話。不過 chat_once.py 每次都是新的 Python 行程,與 Day 13 到 Day 18 的 main.py 服務分開,uvicorn main:app 也尚未實際呼叫 graph.py。
Day 24 的聊天頁面需要透過 HTTP API 與後端互動。今天會把已有 Router 與 Checkpoint 的 graph.py 接進 main.py,新增 POST /chat,並驗證完整服務重啟後仍能保留對話記憶。
Day 12 到 Day 17,plan_node、report_node、qa_node 內容一直在變動:Day 12 三個都是空殼,Day 13 只有 plan_node 換成真正呼叫 Planner,其他兩個到今天都還是 Day 12 的簡化版本。如果 Day 12 路由一做完就急著把 /chat 公開出去,等於每次 Node 內容調整都要重新對外公告一次介面,沒有意義。現在 Day 17 Daily Coach 也做完了,main.py 已經是一支穩定運作的 FastAPI 服務,這是把對話圖跟其他端點放在一起、一次到位的合理時機。
Day 11 的範例是自己在腳本裡寫死 thread_id="demo-user"。今天要讓每個使用者有自己的對話線,最簡單的做法是拿 user_id 直接算出 Thread ID,例如 f"user-{user_id}"。前端呼叫 /chat 時只需要帶 user_id 和訊息內容,完全不用知道「Thread」這個概念存在,這件事對前端來說應該是後端內部的實作細節。
今天先讓每位使用者只有一條對話線。日後若要支援多個不同主題的對話,前端需額外帶入對話 ID,而非只由後端以 user_id 產生唯一的 Thread ID。
/chat 跟 /plans、/progress/log 是兩條不相干的路今天的 /chat 呼叫 plan_node、report_node 時,只是讓模型用不同的系統提示詞回話,並不會真的呼叫 Day 13 的 generate_plan 去生成一份計畫、也不會呼叫 Day 18 的 log_progress 去寫進度記錄。使用者在聊天視窗說「我今天做完了」,教練只會回一句鼓勵,資料庫裡的任務狀態不會被改動,要標記完成還是得走 Day 15 的 PATCH /tasks/{task_id} 或 Day 18 的 POST /progress/log。
這是目前的簡化。若要讓聊天直接觸發資料庫操作,例如辨識「我做完了」後建立進度記錄,plan_node 與 report_node 必須加入資料庫查詢邏輯,並辨識使用者指涉的任務。今天先完成對話 API 與記憶持久化。
檔案位置: backend/schemas.py
狀態: 修改檔案(接續 Day 18 的內容,繼續往下加)
用途: 定義 /chat 端點的請求與回應格式
依賴: pydantic
class ChatRequest(BaseModel):
"""POST /chat 的請求格式"""
user_id: int
message: str = Field(min_length=1, description="使用者這輪說的話")
class ChatResponse(BaseModel):
"""POST /chat 的回應格式"""
intent: str
reply: str
POST /chat檔案位置: backend/main.py
狀態: 修改檔案(接續 Day 18 的內容,繼續往下加)
用途: 把使用者訊息送進對話圖,回傳教練的回應與判斷的意圖
依賴: schemas, graph
from graph import graph
from schemas import ChatRequest, ChatResponse
@app.post("/chat", response_model=ChatResponse)
def chat(payload: ChatRequest) -> ChatResponse:
"""把使用者訊息送進對話圖,回傳教練的回應與判斷的意圖"""
config = {"configurable": {"thread_id": f"user-{payload.user_id}"}}
result = graph.invoke(
{"messages": [{"role": "user", "content": payload.message}], "intent": ""},
config,
)
return ChatResponse(intent=result["intent"], reply=result["messages"][-1].content)
from graph import graph 這行是今天真正的重點,其他都只是包裝。graph 這個物件在 graph.py 被 import 的當下就已經建立好(模組層級的程式碼只會執行一次),main.py 啟動時只會連一次 checkpoints.sqlite,之後每次呼叫 /chat 都共用同一個已經編譯好的圖,不會每次請求都重新組圖、重新連線。
/chat 沒有用到 Depends(get_db),因為今天這支端點完全不碰 Day 5 的 app.db,它只碰 graph.py 自己管理的 checkpoints.sqlite,兩個資料庫從 Day 11 開始就是分開的,今天沒有讓它們產生任何交集。
啟動後端伺服器:
uvicorn main:app --reload
進入 http://127.0.0.1:8000/docs,用 POST /chat 依序測試:
第一句:
{
"user_id": 1,
"message": "我叫Alice,正在準備AWS Solutions Architect認證"
}
應該收到類似:
{
"intent": "qa",
"reply": "你好Alice!準備AWS SA認證是個很棒的目標..."
}
第二句(同一個 user_id):
{
"user_id": 1,
"message": "我剛剛說我叫什麼名字?"
}
reply 應該正確答出「Alice」,證明 thread_id 是根據 user_id 固定算出來的,兩次呼叫接到了同一條對話線。
這一步要驗證的不是「重新整理網頁」,而是整個 uvicorn 行程被終止之後,記憶還在不在。
uvicorn 的終端機,按 Ctrl+C,等它完全停止(不是 --reload 偵測到檔案變更那種自動重啟,是你自己手動終止行程)uvicorn main:app --reload
POST /chat:{
"user_id": 1,
"message": "我剛剛提到我在準備什麼認證?"
}
reply 應該正確答出「AWS Solutions Architect」,這代表 Day 11 證明過的 Checkpoint 機制,在真正的 API 服務裡一樣成立:對話記憶存在 checkpoints.sqlite 檔案上,不是存在服務行程的記憶體裡,行程整個重啟也不影響。
user_id 的對話互不干擾{
"user_id": 2,
"message": "我剛剛說我叫什麼名字?"
}
因為 user_id=2 是第一次呼叫,thread_id 是 user-2,跟 user-1 完全是不同的 Thread,模型應該答不出名字(甚至會反問你是誰),這證明不同使用者的對話確實被 Checkpoint 分開存放,不會互相污染。
/chat 反應很慢跟 Day 8、Day 9 遇到的情況一樣,Ollama 第一次載入模型需要暖機時間,屬於正常現象,跟今天的改動無關。
sqlite3.OperationalError: database is lockedcheckpoints.sqlite 目前是一個連線被整個 FastAPI 服務共用(Day 11 建立時就是這樣設計),如果同時有好幾個請求並發打進 /chat,理論上有機會遇到鎖定的狀況。今天先接受這個簡化:實際測試時一次只打一個請求,不會遇到問題;如果之後要應付真正的高併發流量,需要改用連線池或幫每個請求開獨立連線,這不在今天的範圍內。
user_id 卻好像還記得上一個使用者說的話檢查 thread_id 的組法是不是真的用了 payload.user_id,而不是不小心寫死成固定字串。今天的 Thread ID 是 f"user-{payload.user_id}",只要 user_id 不同,理論上一定是全新的 Thread,不會混到別人的對話。
/chat 回傳的 intent 有時候跟預期不一樣classify_intent(Day 12 寫的)只是關鍵字比對,不是完全精準的語意理解,這個限制今天沒有改變。如果測試時發現常見的說法一直被誤判,可以回頭調整 Day 12 router_node.py 裡的關鍵字清單,跟今天把圖接上 API 這件事是兩個獨立的問題。
/chat 說使用者「做完了」,為什麼資料庫裡的任務狀態沒有變如前面設計小節提到的,今天 /chat 只負責對話回應,不會觸發 Day 15、Day 18 的資料庫操作。要標記任務完成,目前還是得直接呼叫 PATCH /tasks/{task_id} 或 POST /progress/log,這是今天刻意保留的邊界,不是遺漏。
今天把 Day 11 到 Day 17 逐步養出來的對話圖,第一次接上真正對外服務的 API。POST /chat 讓前端(Day 24 會用到)能透過簡單的 HTTP 請求跟教練對話,Thread ID 根據 user_id 自動決定,不用前端操心。也重新驗證了一次 Checkpoint 機制:這次不是靠獨立腳本前後執行兩次,而是把整個服務行程真正關掉重開,對話記憶依然完整。
系統現在是這樣的:
Day 1 ✓ 產品定義完成
Day 2 ✓ 開發環境準備
Day 3 ✓ 專案架構設計
Day 4 ✓ 資料庫設計
Day 5 ✓ SQLite 資料庫建置
Day 6 ✓ FastAPI 基礎
Day 7 ✓ 使用者檔案 API
Day 8 ✓ 理解 LLM Agent 的本質
Day 9 ✓ 連接 Ollama 本機模型
Day 10 ✓ LangGraph 最小範例
Day 11 ✓ Thread 與 State 管理
Day 12 ✓ 簡化的意圖路由
Day 13 ✓ 計畫生成 Agent
Day 14 ✓ 簡化的排程邏輯
Day 15 ✓ 計畫 CRUD API
Day 16 ✓ 計畫審核與用戶反饋
Day 17 ✓ Daily Coach
Day 18 ✓ 進度記錄系統
Day 19 ✓ 把對話圖接上 API(今天)
Day 20 ⬜ 簡化版測驗系統
現在後端已經有兩條完整可用的路:一條是 /chat 這種對話式的互動,一條是 /plans、/tasks、/progress 這些結構化的 CRUD。明天要幫系統加上第三種能力:自動生成測驗題目、讓使用者作答、自動評分,用比對話更明確的方式檢驗使用者到底學會了沒有。