物理知識庫 Demo

物理知識庫專案 的 Agentic Flow CLI 做的 Web UI,讓非技術人員也能直接在瀏覽器體驗問答功能。服務 OJT 展示(3 月底檢核)和接案作品集。

架構

瀏覽器 (index.html)
  │
  │ GET /api/note-map(載入時取得 title → garden URL 映射)
  │ POST /api/ask { question, history, shownQuestions }
  │
  ▼
Bun HTTP Server (server.ts)
  │
  │ 1. 元問題攔截(「剛才問了什麼」→ 直接回覆)
  │ 2. 問題增強:注入對話歷史 + 已出題目去重清單
  │ 3. spawn 子進程:bun run agent/src/index.ts <enriched_question>
  │
  ▼
Agent CLI(不改動)
  │ stderr → SSE 即時串流 agent 思考過程
  │ stdout → 最終答案
  │
  ▼
瀏覽器渲染 → linkifyNotes() 將筆記標題轉為 Garden 連結

關鍵設計:Demo 層 不修改 Agent CLI 本身,只在外層處理 Web/對話問題。Agent 透過子進程呼叫,零耦合。

檔案結構

~/Documents/physics-kb/demo/
├── server.ts    # Bun HTTP server(SSE 串流、問題增強、元問題攔截)
└── index.html   # 單頁面(暗色主題、Markdown + LaTeX 渲染、對話式 UI)

解決的問題

1. 關鍵字品質(grep 搜尋 0 結果)

問題:Haiku extractKeywords 會產出長複合詞(如「光纖傳輸」「全內反射」),但筆記用的是短詞(「光纖」「全反射」),grep 精確匹配找不到。

修法

  • llm.ts:prompt 加入「每個關鍵字 2-3 字短詞」規則
  • search.ts:GrepSearch 加 fallback——原始詞找到 < 3 篇時,自動拆成 2 字短詞重搜

2. 對話記憶

問題:CLI agent 是 stateless,不記得上一輪問答。追問「有相關題目嗎」會失敗。

方案演進

  1. Server 端全域變數記憶 → 跨 client 污染
  2. Regex 偵測追問 → 中文同一句話可以是獨立問題也可以是追問,不穩定
  3. Frontend 送完整歷史,每次都注入 → LLM 自己判斷要不要用背景。成本 ~200 tokens/次,可忽略

最終方案:Frontend 維護 questionHistory[],每次送最近 5 題。Server 格式化為:

(背景:之前的對話依序討論了:1.「庫侖定律是什麼?」 2.「給我題目」)
再給我一個

3. 出題去重

問題:連續要求出題時,agent 不知道已出過什麼,會重複。

修法:Frontend 從回答中提取 Q-ID(Q-xxx-xxx-xx 格式),累積為 shownQuestions[] 送給 server。Server 注入「已出過的題目:Q-xxx、Q-yyy,請不要重複」。

4. 非物理問題處理

問題:輸入「你好嗎」等非物理問題,Haiku 回傳非 JSON → crash。

修法

  • llm.ts:JSON parse 失敗拋出清楚錯誤
  • agent.ts:extractKeywords 失敗 → 回傳友善提示「我是物理知識庫助教,只能回答高中物理相關的問題」

5. 元問題攔截

問題:「剛才我問了什麼」不是物理問題,但也不該被拒絕。

修法:Server 層用 regex 攔截元問題,直接從 history 回覆,不經 agent。

6. 引用筆記連結到 Garden

問題:Agent 回答末尾的「引用筆記:庫侖定律、庫侖定律公式」是純文字,無法直接查看原始筆記。

做法:筆記已透過 Quartz Garden 部署到 Cloudflare(physics-garden.pages.dev),可直接連結。

機制(不改動 Agent 輸出,純粹在 Frontend 後處理):

  1. Server 啟動時掃描 notes/ 建立 { title → garden URL } 映射(307 筆)
  2. 新增 GET /api/note-map 回傳映射
  3. Frontend 載入時 fetch 一次
  4. 每次答案渲染後,linkifyNotes() 在 DOM 中把匹配的標題替換為 <a> 連結

防護措施

  • 長標題優先匹配(sorted by length desc),防止「庫侖定律」先匹配破壞「庫侖定律公式」
  • 跳過 < 3 字的標題,避免「力」「光」等短詞誤匹配
  • 跳過含 <a> 或 KaTeX 的節點,避免干擾已有連結和數學公式

對 Agent CLI 的修改

Demo 開發過程中回饋了幾個 agent 本身的改善:

檔案改動原因
agent/src/llm.tsextractKeywords prompt 加入「2-3 字短詞」「背景格式說明」規則grep 搜不到長複合詞;追問時需理解背景
agent/src/llm.tsJSON parse 失敗拋出清楚錯誤(非 SyntaxError非物理問題 debug 困難
agent/src/search.tsGrepSearch 加 fallback:長詞拆短重搜召回率不足的安全網
agent/src/agent.tsextractKeywords 失敗時 graceful return不讓 CLI 整個 crash

使用方式

線上 Demo(Fly.io)

https://physics-kb.fly.dev/

密碼:透過 fly secrets 設定(DEMO_PASSWORD)。

部署細節:

  • 主機:Fly.io 東京機房(nrt),shared-cpu-1x / 1GB RAM
  • 自動休眠:idle 時 machine 會停止,首次請求喚醒(冷啟動約 5-10 秒)
  • 密碼牆:所有訪客需輸入密碼才能使用,密碼存 sessionStorage(關 tab 即清除)
  • 限流:per-IP 每分鐘 10 次 /api/ask 請求
  • Secrets:ANTHROPIC_API_KEY + DEMO_PASSWORD,透過 fly secrets set 注入
# 部署/更新
cd ~/Documents/physics-kb
/home/matt/.fly/bin/flyctl deploy
 
# 管理
/home/matt/.fly/bin/flyctl secrets set DEMO_PASSWORD=新密碼
/home/matt/.fly/bin/flyctl logs           # 查看即時 log
/home/matt/.fly/bin/flyctl status         # 查看 machine 狀態

本地開發

cd ~/Documents/physics-kb/demo
~/.bun/bin/bun run server.ts
 
# 打開瀏覽器 → http://localhost:3456
# 本地不設 DEMO_PASSWORD 環境變數時,密碼牆自動跳過

前端功能

  • 5 個預設問題 chip,點擊即可發問
  • 即時顯示 Agent 思考過程(SSE 串流)
  • 思考完成後自動摺疊,用顏色標示搜尋/通過/失敗
  • Markdown + KaTeX LaTeX 渲染
  • 對話式介面,支援多輪追問
  • 引用筆記自動連結到 Quartz Garden(可點擊查看原始筆記)
  • 重新整理頁面 = 清空對話,不同 tab 互不干擾
  • 密碼牆(線上版):暗色主題一致,驗證失敗顯示錯誤訊息

風險與規模化注意事項

筆記連結同步風險

noteMap 在 server 啟動時建立,新增筆記後不會自動更新。完整流程:

新增筆記 → bash scripts/index.sh → 重啟 demo server → bash sync-notes.sh 部署 garden

忘記任一步的後果

  • 沒重啟 server → 新筆記無連結(純文字顯示,不影響功能)
  • 沒部署 garden → 連結 404(點了跳到空白頁)

標題匹配風險(規模化到 7000 篇時需關注)

風險嚴重度目前狀態防護
短標題誤匹配(「力」匹配「靜電力」中的子串)Ch1 無此問題(標題都 ≥ 3 字)跳過 < 3 字標題
標題互相包含(「庫侖定律」vs「庫侖定律公式」)已修復長標題優先匹配
307 → 7000 篇的前端效能未測試目前遍歷 307 title × N 個 DOM 節點,7000 篇可能需要改用 Trie 或限制只掃描引用區塊
不同章節的同名概念(如多章都有「能量守恆」)未發生noteMap 以 title 為 key,同名會被覆蓋。建構筆記時應確保標題唯一性

對話系統限制

  • 歷史只送最近 5 題,超長對話會丟失早期 context
  • 出題去重靠前端提取 Q-ID,如果 agent 回答中沒有標準 Q-ID 格式,去重失效
  • LLM 生成的「新題目」(非知識庫中已有的)無法去重追蹤

部署架構

~/Documents/physics-kb/
├── Dockerfile        # oven/bun:1,複製 notes/agent/demo/,安裝依賴
├── .dockerignore     # 排除 .git, sources/, node_modules/, .env, *.pdf
├── fly.toml          # app=physics-kb, nrt, 8080, shared-cpu-1x/1GB
├── demo/
│   ├── server.ts     # PORT/BUN_PATH 讀環境變數,密碼驗證 + 限流
│   └── index.html    # 密碼牆 UI + X-Demo-Password header
└── ...

server.ts 的部署適配

  • PORT:讀 process.env.PORT(Fly.io 透過 fly.toml [env] 注入 8080,本地預設 3456)
  • BUN:讀 process.env.BUN_PATH ?? "bun"(Docker 中 bun 已在 PATH,本地需完整路徑)
  • 密碼驗證:POST /api/verify-password + /api/askX-Demo-Password header 檢查
  • 限流:per-IP sliding window,用 X-Forwarded-For 取真實 IP

待辦

  • 回答品質:Verify FAIL 後的 retry 有時仍偏題(特別是出題場景)
  • 出題場景:agent generate prompt 未針對「出題」vs「解釋」做區分
  • 手機適配:修復 lookbehind regex 相容性問題(改用 replace callback)
  • 蘇格拉底模式:切換 generate prompt 實現引導式教學(對接 OJT SEL 方向)
  • Fly.io 部署:密碼牆 + 限流 + Docker 化

連結