物理知識庫 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,不記得上一輪問答。追問「有相關題目嗎」會失敗。
方案演進:
Server 端全域變數記憶→ 跨 client 污染Regex 偵測追問→ 中文同一句話可以是獨立問題也可以是追問,不穩定- 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 後處理):
- Server 啟動時掃描
notes/建立{ title → garden URL }映射(307 筆) - 新增
GET /api/note-map回傳映射 - Frontend 載入時 fetch 一次
- 每次答案渲染後,
linkifyNotes()在 DOM 中把匹配的標題替換為<a>連結
防護措施:
- 長標題優先匹配(sorted by length desc),防止「庫侖定律」先匹配破壞「庫侖定律公式」
- 跳過 < 3 字的標題,避免「力」「光」等短詞誤匹配
- 跳過含
<a>或 KaTeX 的節點,避免干擾已有連結和數學公式
對 Agent CLI 的修改
Demo 開發過程中回饋了幾個 agent 本身的改善:
| 檔案 | 改動 | 原因 |
|---|---|---|
agent/src/llm.ts | extractKeywords prompt 加入「2-3 字短詞」「背景格式說明」規則 | grep 搜不到長複合詞;追問時需理解背景 |
agent/src/llm.ts | JSON parse 失敗拋出清楚錯誤(非 SyntaxError) | 非物理問題 debug 困難 |
agent/src/search.ts | GrepSearch 加 fallback:長詞拆短重搜 | 召回率不足的安全網 |
agent/src/agent.ts | extractKeywords 失敗時 graceful return | 不讓 CLI 整個 crash |
使用方式
線上 Demo(Fly.io)
密碼:透過 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/ask的X-Demo-Passwordheader 檢查 - 限流: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 化
連結
- 知識庫專案:物理知識庫專案
- OJT:115 OJT 創新專案
- 技術基礎:qmd 原子化筆記方案