// Release Log entries — single source of truth for both ReleaseLogPage and PresentationPage. // Each entry: { // date, slug, milestone, tag, // title:{zh,en}, // summary:{zh,en}, // summaryBullets?: { zh: string[], en: string[] }, // optional, 2–4 short bullets per language // } // Tag → Badge variant (in ReleaseLogPage): feature→success, fix→warning, enhancement→default, ui→muted // Stats snapshot — manually updated when generating the presentation. // Numbers from prod backend GET /shows on 2026-05-01. // transcript_chunks count is estimated (~50 chunks/episode); zeabur-service-exec // hits Cloudflare 524 timeout so direct DB query was unreliable. // These are fallbacks only — pages now live-fetch from GET /stats (public). // Numbers updated 2026-05-04 to current rough magnitude so the fallback isn't // dramatically off when the API call fails. const STATS_AS_OF = '2026-05-04'; const STATS_CHANGES_COUNT = 35; const STATS_EPISODES_COUNT = 247; // transcripts.status = 'completed' const STATS_VECTORS_COUNT = 113000; // transcript_chunks rows // Tag labels (used by both pages). const TAG_LABELS = { feature: { zh: '新功能', en: 'Feature' }, fix: { zh: 'Bug 修復', en: 'Fix' }, enhancement: { zh: '系統優化', en: 'Enhancement' }, ui: { zh: '介面調整', en: 'UI' }, experiment: { zh: '實驗(未上線)', en: 'Experiment (Not Shipped)' }, }; const MILESTONE_LABELS = { 'v0.1': { zh: 'v0.1 — RAG MVP 基礎建設', en: 'v0.1 — RAG MVP Foundation' }, 'v0.2': { zh: 'v0.2 — 後台管理與排程', en: 'v0.2 — Admin & Schedule' }, 'v0.3': { zh: 'v0.3 — 真實 Cron 與平行轉錄', en: 'v0.3 — Real Cron & Parallel Queue' }, 'v0.4': { zh: 'v0.4 — 手機版與友善錯誤', en: 'v0.4 — Mobile & Friendly Errors' }, 'v0.5': { zh: 'v0.5 — 帳號驗證與查詢額度', en: 'v0.5 — Auth & Query Quota' }, 'v0.6': { zh: 'v0.6 — 部署不中斷正在跑的轉錄', en: 'v0.6 — Deploys Without Interrupting Transcriptions' }, 'v0.7': { zh: 'v0.7 — AI 設定集中化', en: 'v0.7 — AI Settings Consolidation' }, 'v0.8': { zh: 'v0.8 — 自動化驗證後門', en: 'v0.8 — Automated Verification Backdoor' }, 'v0.9': { zh: 'v0.9 — 每集 AI 摘要', en: 'v0.9 — Per-Episode AI Summary' }, 'v1.0': { zh: 'v1.0 — 公開上線:freemium 模式', en: 'v1.0 — Public Launch: Freemium Mode' }, 'v1.1': { zh: 'v1.1 — 收集回答品質回饋', en: 'v1.1 — Collecting Answer Quality Feedback' }, 'v1.2': { zh: 'v1.2 — 量化 RAG 答題準確度基線', en: 'v1.2 — RAG Accuracy Baseline, Measured' }, 'v1.3': { zh: 'v1.3 — 把資料背在保險上', en: 'v1.3 — Putting Your Data on Insurance' }, 'v1.4': { zh: 'v1.4 — 混合檢索:找到節目主寫的關鍵字', en: 'v1.4 — Hybrid Retrieval: Catching the Host\'s Own Keywords' }, 'v1.5': { zh: 'v1.5 — 更新日誌變好讀', en: 'v1.5 — Release Log, Browsable' }, 'v1.6': { zh: 'v1.6 — 搜尋結果看得更清楚', en: 'v1.6 — Search Results, Now Readable' }, 'v1.7': { zh: 'v1.7 — 搜尋準度大幅提升', en: 'v1.7 — Retrieval Quality, Materially Better' }, 'v1.8': { zh: 'v1.8 — 對話模式 agent 化', en: 'v1.8 — Chat Mode, Now Agentic' }, 'v1.9': { zh: 'v1.9 — 索引模式:多關鍵字精準搜尋', en: 'v1.9 — Index Mode: Precise Multi-Keyword Search' }, 'v2.0': { zh: 'v2.0 — 查詢體驗統一:一致引用卡 + 引導範例', en: 'v2.0 — Unified Query Experience: Consistent Citations + Guided Examples' }, 'v2.1': { zh: 'v2.1 — 修正聽錯的名字,搜尋找得回來', en: 'v2.1 — Fixing Misheard Names So Search Can Find Them' }, 'v2.2': { zh: 'v2.2 — AI 自動找出聽錯的名字,你只要點核准', en: 'v2.2 — AI Auto-Finds Misheard Names; You Just Approve' }, 'v2.3': { zh: 'v2.3 — 兩個新節目全集上線', en: 'v2.3 — Two New Shows, Full Back Catalogs' }, }; // Entries — newest milestone first; within each milestone newest date first. const RELEASE_LOG = [ // ─── v2.3 — External transcript bulk import: two new shows (7/10) ─── { date: '2026-07-10', slug: 'external-transcript-bulk-import', milestone: 'v2.3', tag: 'feature', title: { zh: '兩個新節目全集上線:塞掐 Side Chat(449 集)+ 台灣通勤第一品牌(562 集)', en: 'Two New Shows, Full Back Catalogs: Side Chat (449 eps) + Commute No.1 Brand (562 eps)', }, summary: { zh: '現在你可以直接搜尋、發問這兩個節目的全部歷史集數——超過 970 小時的內容,一句話就能找到「那個來賓說過什麼」。每集都有完整逐字稿與時間軸,搜尋結果可以直接跳到該分鐘、從該處播放。之後兩節目的新集數也會每天自動加入。背後我們用了新的批次匯入方式處理這一千多集:在雲端 GPU 上以 26 倍速轉錄完 972 小時的音訊,成本只有原本作法的五分之一左右,之後要再上大節目也能照這條路走。', en: 'You can now search and ask questions across the complete back catalogs of these two shows — over 970 hours of content, where one sentence is enough to find "what did that guest say". Every episode comes with a full transcript and timeline; search results jump straight to the exact minute and can play from there. New episodes of both shows will be added automatically every day. Behind the scenes, we built a new bulk-import path to handle the thousand-plus episodes: 972 hours of audio transcribed at 26× real-time speed on cloud GPUs, at roughly one-fifth the cost of our previous approach — a path we can reuse for the next big show.', }, summaryBullets: { zh: [ '塞掐 Side Chat 449 集+台灣通勤第一品牌 562 集,共 1,011 集全部可搜尋、可發問', '每集完整逐字稿+時間軸,搜尋結果直接跳到該分鐘、從該處播放', '兩節目新集數每天自動加入;批次匯入路徑可重複用於未來的大節目', ], en: [ 'Side Chat (449 eps) + Commute No.1 Brand (562 eps): all 1,011 episodes searchable and askable', 'Full transcript + timeline per episode; results jump to the exact minute and play from there', 'New episodes auto-added daily; the bulk-import path is reusable for the next big show', ], }, }, // ─── v2.3 — Mobile RWD overhaul (7/7, shipped & verified on-device) ─── { date: '2026-07-07', slug: 'mobile-rwd', milestone: 'v2.3', tag: 'ui', title: { zh: '手機版介面全面修整:小螢幕上不再東卡西卡', en: 'Mobile Interface Overhaul: No More Broken Layouts on Small Screens', }, summary: { zh: '之前用手機打開網站,會遇到一些惱人的小毛病:首頁節目卡片撐破畫面、按鈕文字折成兩行、對話裡的引用卡被層層邊距擠得很窄、後台表格右邊被切掉看不到。這版針對手機做了一輪總整理:節目卡片乖乖排成一欄、按鈕永遠一行、引用卡變寬更好讀、表格可以左右滑動看完整內容、對話輸入區在有對話時也不再佔掉半個畫面。iPhone Safari 實機逐項驗證通過。', en: 'Opening the site on a phone used to hit a bunch of small annoyances: show cards stretching past the screen, button labels wrapping onto two lines, citation cards in chat squeezed narrow by stacked margins, and admin tables cut off on the right. This release is a full mobile cleanup: show cards stack neatly in one column, buttons stay on one line, citation cards got wider and easier to read, tables scroll sideways to reveal everything, and the chat input dock no longer eats half the screen mid-conversation. Verified item by item on a real iPhone in Safari.', }, summaryBullets: { zh: [ '首頁節目卡、按鈕、對話引用卡、後台表格在小螢幕上全部正常顯示', '對話進行中,底部輸入區自動收合範例問題,聊天內容看得更多', 'iPhone Safari 真機逐項驗證通過', ], en: [ 'Show cards, buttons, chat citations and admin tables all render correctly on small screens', 'During a conversation the input dock collapses the example questions, showing more of the chat', 'Verified item by item on a real iPhone in Safari', ], }, }, // ─── v2.2 — Search result memory / cache (6/10) ─── { date: '2026-06-10', slug: 'r4-rag-result-cache', milestone: 'v2.2', tag: 'enhancement', title: { zh: '查過的問題會「記住」:重複或熱門查詢幾乎秒回', en: 'Searches Are Now Remembered: Repeat and Popular Queries Come Back Almost Instantly', }, summary: { zh: '以前每次搜尋,系統都要從頭算一遍(把問題轉成向量、翻遍逐字稿、整理結果),所以同一個問題問第二次也一樣慢。這版加了「查詢結果記憶」:在同一個節目下,相同的搜尋第二次起會直接拿上次的結果,幾乎是秒回。最有感的是首頁那排「引導範例」問題——我們會在背景先幫每個節目把範例查好放著,你一點就出來。語意搜尋的重複查詢,從約十秒縮到半秒內,三種查詢模式(語意/關鍵字/對話)都吃得到這個加速。如果節目重新轉錄、或修正了聽錯的名字,記憶會自動失效、重新算最新的結果,不會給你舊資料。', en: 'Until now, every search recomputed from scratch (turning your question into a vector, scanning the transcripts, assembling the results), so asking the same question a second time was just as slow. This release adds "search result memory": within the same show, an identical search returns last time\'s result from the second time onward — almost instantly. You\'ll feel it most on the home page\'s guided-example questions: we now warm those up for each show in the background, so they appear the moment you click. Repeat semantic searches drop from about ten seconds to under a second, and all three search modes (semantic / keyword / conversational) benefit. If a show is re-transcribed or has misheard names corrected, the memory invalidates automatically and recomputes the latest result — you never get stale data.', }, summaryBullets: { zh: [ '同一節目下相同搜尋第二次起幾乎秒回(語意重複查詢從 ~10 秒降到 <1 秒)', '首頁「引導範例」問題會在背景預先查好,一點就出', '語意/關鍵字/對話三模式都加速;節目內容更新後記憶自動失效、不給舊資料', ], en: [ 'Identical searches within a show are near-instant from the second time on (repeat semantic queries: ~10s → <1s)', 'Home-page guided-example questions are warmed up in the background so they appear on click', 'All three modes (semantic / keyword / conversational) sped up; memory auto-invalidates after content updates so results stay fresh', ], }, }, // ─── v2.2 — Faster deploys (6/8) ─── { date: '2026-06-08', slug: 'o2-prebuilt-base-image', milestone: 'v2.2', tag: 'enhancement', title: { zh: '系統更新上線變快了:新版部署從約十分鐘縮到兩分多鐘', en: 'Deploys Got Faster: Shipping an Update Now Takes ~2 Minutes Instead of ~10', }, summary: { zh: '每次我們修 bug 或上新功能,系統都要重新打包、重新部署才會生效。以前這個過程要約十分鐘,因為每次都把一堆笨重的元件從頭重裝一遍。這版把那些笨重元件「預先打包好、重複沿用」,更新上線縮短到約兩分半。對你而言:修正和新功能能更快出現在網站上——網站本身的操作沒有任何改變。', en: 'Every time we fix a bug or ship a feature, the system has to repackage and redeploy before it goes live. That used to take about ten minutes because it reinstalled a pile of heavy components from scratch each time. This release pre-packages those heavy components once and reuses them, cutting deploys to roughly two and a half minutes. For you: fixes and new features reach the site faster — nothing about using the site itself changes.', }, summaryBullets: { zh: [ '新版部署時間從 ~10 分縮短到 ~2.5 分', '修正與新功能能更快上線', '網站操作無可見變化(純後台部署優化)', ], en: [ 'Deploy time cut from ~10 min to ~2.5 min', 'Fixes and new features reach the site sooner', 'No visible change to using the site (a behind-the-scenes build optimization)', ], }, }, // ─── v2.2 — ASR Detection over existing episodes + observable jobs (6/4) ─── { date: '2026-06-04', slug: 'asr-homophone-full-backfill', milestone: 'v2.2', tag: 'feature', title: { zh: '對「舊集」也能一鍵跑 AI 偵測聽錯的名字 — 大批校正看得到進度、可中途取消', en: 'AI Detection Now Covers Older Episodes Too — Long Correction Jobs Show Progress and Can Be Cancelled', }, summary: { zh: '接續「AI 自動找出聽錯的名字」:以前只有「新轉錄」的集會自動偵測,偵測功能上線前的舊集(全站數百集)一直沒被掃過。這版讓管理者可以對任一節目的舊集一鍵跑偵測,找出聽錯的名字產生待審候選——會先估算成本、你確認後才跑,且只產候選、不會動到逐字稿。同時把「大批校正/偵測」這種長時間作業變透明:跑的時候看得到進度(第幾集/共幾集)、失敗的片段會列出來、可以中途取消(已跑完的部分保留、不回滾)。另外,核准一條候選時可以勾「順便套用到舊集」,一個動作同時讓規則生效+回頭修舊集;也新增「批次還原」一鍵把曾被校正的集還原回原始 ASR。', en: 'A follow-up to "AI auto-finds misheard names": detection previously ran only on newly-transcribed episodes, so the hundreds of episodes that predate the feature were never scanned. Admins can now run detection over any show\'s older episodes with one click — it estimates cost first, runs only after you confirm, and only produces pending candidates (it never touches the transcript). It also makes long detect/apply jobs transparent: live progress (episode N of M), a list of failed chunks, and a cancel control (completed work is kept, not rolled back). Approving a candidate can now also apply it to existing episodes in one action, and a new "batch restore" reverts previously-corrected episodes back to their original ASR.', }, summaryBullets: { zh: [ '對任一節目的「舊集」一鍵跑 AI 偵測;先估成本、確認後才跑、只產候選不改文字', '大批偵測/校正作業看得到進度(第幾集/共幾集)+ 失敗片段清單 + 可中途取消(已完成保留)', '核准候選可勾「順便套用到舊集」;新增「批次還原」一鍵還原回原始 ASR', ], en: [ 'One-click AI detection over a show\'s older episodes; estimates cost, runs on confirm, candidates only', 'Long jobs show live progress + failed-chunk list + a cancel control (completed work kept)', 'Approve-and-apply to existing episodes; new "batch restore" to original ASR', ], }, }, // ─── v2.2 — ASR Correction: reversibility + transcript consistency (6/2) ─── { date: '2026-06-02', slug: 'asr-correction-reversibility-and-content-sync', milestone: 'v2.2', tag: 'enhancement', title: { zh: '校正可還原 + 逐字稿全文跟上 — 改錯了能一鍵還原原始 ASR,逐字稿頁不再顯示舊錯字', en: 'Corrections Are Now Reversible + Transcript Stays Consistent — One-click Restore to Original ASR; the Transcript Page No Longer Shows Stale Typos', }, summary: { zh: '把 ASR 校正補上兩個安全性/一致性的洞:(1) 可還原——校正套用前會先保存原始的 ASR 文字,後台逐字稿頁新增「還原原始逐字稿」(管理者限定),萬一規則改錯了可以一鍵把整集還原回原始、並重算搜尋索引;(2) 一致性——以前批次回填只改了片段與搜尋索引、沒同步「整集逐字稿全文」,導致逐字稿頁仍顯示舊錯字。現在校正會一起更新全文,且補了一條「強制重算」把先前已回填但全文沒跟上的集數也修正。對一般使用者來說,逐字稿頁看到的字會跟搜尋結果一致;對管理者來說,校正不再是不可逆的單行道。', en: 'Closes two safety/consistency gaps in ASR correction: (1) reversibility — the original ASR text is now snapshotted before a correction is applied, and the admin transcript page gains a "Restore original" action (admin-only) to revert an entire episode to its original text (and recompute the search index) if a rule was wrong; (2) consistency — backfill previously updated segments + the search index but not the full-episode transcript text, so the transcript page still showed old typos. Corrections now update the full text too, plus a "force resync" path repairs episodes that were backfilled before this fix. For readers, the transcript page now matches search results; for admins, corrections are no longer a one-way street.', }, summaryBullets: { zh: [ '校正前保存原始 ASR 文字;後台逐字稿頁「還原原始逐字稿」一鍵還原整集(管理者限定)', '校正同步更新「整集逐字稿全文」,逐字稿頁不再顯示舊錯字', '補強制重算,修好先前已回填但全文沒跟上的集數', ], en: [ 'Original ASR text snapshotted before correction; admin "Restore original" reverts a whole episode', 'Corrections now also update the full transcript text — the transcript page no longer shows old typos', 'Force-resync path repairs episodes backfilled before this fix', ], }, }, // ─── v2.2 — AI Homophone Detection: review polish (6/2) ─── { date: '2026-06-02', slug: 'asr-correction-ux-and-aihub-json', milestone: 'v2.2', tag: 'enhancement', title: { zh: '候選審核小升級 — 核准時可直接改正字,偵測更換模型也不會卡住', en: 'Candidate Review Polish — Edit the Correction at Approval Time; Model Swaps No Longer Break Parsing', }, summary: { zh: '接續上一版的「AI 自動找聽錯的名字」,這版補兩個小地方:(1) 後台審核候選時,「正字」欄位可以直接編輯——AI 偶爾對應到相近但不完全對的名字,現在你不用先駁回再重建,核准當下改好就生效;(2) 偵測背後換不同 AI 模型時,回應格式略有差異(多包一層、夾雜說明文字、全形引號等)以前會讓系統解析失敗、整批抓不到,現在解析更耐受,換模型不會再「靜默回 0」。對一般使用者沒有直接變化,主要讓後台校正流程更順、未來可用的模型更多。', en: 'A follow-up to last release\'s "AI auto-finds misheard names", with two small touches: (1) in admin candidate review, the "correct" field is now editable — the AI occasionally maps to a near-but-not-exact name, and you can now fix it at approval time instead of rejecting and rebuilding; (2) when swapping the AI model behind detection, providers format responses slightly differently (extra wrapping, surrounding prose, full-width quotes) which used to break parsing and silently return nothing — parsing is now more tolerant, so model swaps no longer "silently return 0". No direct change for end users; mainly a smoother admin workflow and more usable model options.', }, summaryBullets: { zh: [ '後台候選審核:「正字」可編輯,核准當下微調再生效', '偵測解析更耐受不同模型的回應格式(包裹/夾雜文字/全形引號/鍵名變體)', '換偵測模型不再因解析失敗而靜默回 0', ], en: [ 'Admin candidate review: "correct" is editable — tweak before approving', 'Detection parsing tolerates provider formatting variation (wrapping / prose / full-width quotes / key variants)', 'Swapping the detection model no longer silently returns 0 due to parse failure', ], }, }, // ─── v2.2 — AI Homophone Detection (6/2) ─── { date: '2026-06-02', slug: 'asr-llm-homophone-postprocess', milestone: 'v2.2', tag: 'feature', title: { zh: 'AI 自動找出聽錯的名字 — 不用你一個個發現,AI 掃描整集、列出疑似聽錯的人名給你核准', en: 'AI Auto-Finds Misheard Names — Instead of Spotting Them One by One, AI Scans Each Episode and Queues Suspected Mishearings for Your Approval', }, summary: { zh: '上一版(v2.1)你可以手動建「錯字→正字」對照表,但前提是你得先「發現」哪些名字被聽錯——這很難規模化。這一版讓 AI 幫你找。每當一集轉錄完成,系統會請 AI 比對「這個節目已知的人」(主持人、出現過的來賓,加上你已建的字典),掃描整集逐字稿、揪出疑似被聽成同音字的名字,整理成一份「待審核候選」放進後台,你只要按「核准」或「駁回」。核准後它就變成正式校正規則、跨集生效;駁回後就不再提醒。關鍵設計是「只認名單內的人」——AI 只會把錯字對應回節目真實出現過的名字,不會自己亂猜、亂改正常字詞,所以你看到的候選幾乎都是真的聽錯。我們在「這又沒有很屌」實測 5 集,AI 撈出了一批真實聽錯,例如 杜忠祐/杜仲佑→杜宗祐、阿鳴/阿明→阿名、力友→Leo王、嘎咪比→Gummy B、不來美→布萊梅、趙亦凡→趙翊帆 等,整批掃描成本不到 $0.02。(順帶把後台「批次回填」太慢會卡住的問題也修了,現在秒回。)', en: 'Last release (v2.1) let you hand-build a typo→correct dictionary — but only if you first "spotted" which names were misheard, which doesn\'t scale. This release lets AI find them for you. Whenever an episode finishes transcribing, the system asks an AI to scan the full transcript against "the people this show knows" (hosts, past guests, plus your existing dictionary), flags names that were likely heard as homophones, and queues them as "pending candidates" in admin — you just click Approve or Reject. Approve turns it into a real correction rule that works across episodes; Reject dismisses it. The key design is "only names on the list": the AI only maps a misheard word back to a name the show has actually used — it won\'t guess wildly or rewrite normal words, so almost everything you see is a genuine mishearing. We tested 5 episodes of "這又沒有很屌" and the AI surfaced a batch of real mistakes — e.g. 杜忠祐/杜仲佑→杜宗祐, 阿鳴/阿明→阿名, 力友→Leo王, 嘎咪比→Gummy B, 不來美→布萊梅, 趙亦凡→趙翊帆 — for under $0.02 total. (We also fixed the admin "Backfill" preview that was timing out; it\'s instant now.)', }, summaryBullets: { zh: [ '轉錄完成後 AI 自動掃整集,揪出疑似被聽成同音字的人名', '只對應「節目已知的人」(主持人 + 來賓 + 你建的字典)→ 不亂猜、不誤改正常字', '後台「待審核候選」區:一鍵核准(跨集生效)或駁回(不再提醒)', '實測「這又沒有很屌」5 集撈出一批真實聽錯,整批成本 < $0.02', '順手修好後台批次回填預覽 timeout(98 秒 → 3 秒)', ], en: [ 'After transcription, AI scans the whole episode and flags names likely heard as homophones', 'Only maps to "people the show knows" (hosts + guests + your dictionary) → no wild guesses, no collateral edits', 'Admin "Pending Candidates" section: one-click Approve (works across episodes) or Reject (dismiss)', 'Tested 5 episodes of "這又沒有很屌": surfaced a batch of real mishearings for under $0.02 total', 'Also fixed the admin backfill-preview timeout (98s → 3s)', ], }, }, // ─── v2.1 — ASR Correction Dictionary (6/1) ─── { date: '2026-06-01', slug: 'asr-correction-dictionary', milestone: 'v2.1', tag: 'feature', title: { zh: '修正聽錯的名字 — 後台建「錯字→正字」對照表,新轉錄自動修、舊逐字稿一鍵回填,搜尋從此找得回來', en: 'Fixing Misheard Names — Build a Typo→Correct Dictionary in Admin; New Transcripts Auto-Fix, Old Ones Backfill in One Click, and Search Finds Them Again', }, summary: { zh: '語音轉錄(Whisper)偶爾會把專有名詞聽成同音錯字——例如把樂團「滅火器」聽成「咪有企」、把來賓「杜宗祐」打成「杜忠祐」。問題是:當逐字稿裡存的是錯字,你用正確名字去搜就什麼都找不到。這版在後台新增「ASR 校正」分頁,讓你建立一份「錯字→正字」對照表(每條可綁定特定節目、也可設成全站通用)。建好之後,之後每一集新轉錄都會在存檔前自動套用修正;至於已經存在的舊逐字稿,你可以按「回填」一次重算修好。為了讓你心裡有底,回填前系統會先估「這條規則會動到幾段逐字稿、大約花多少錢」,你確認後才真的執行。另外為了避免誤傷,儲存規則前會顯示這個錯字目前在範圍內命中幾段,而且比對採「整詞精確比對」——只換完全一樣的詞,不會把字拆開亂改。首發就替「這又沒有很屌」這個節目修好了 6 組最常見的錯字、共 1507 段逐字稿(總成本約 $0.05):杜忠祐→杜宗祐、阿鳴/阿明→阿名、方品龍→方品融、龍虎報→龍虎豹、咪有企→滅火器。現在用這些正確名字搜尋,都能命中既有內容了。', en: 'Speech-to-text (Whisper) occasionally mishears proper nouns into homophone typos — e.g. the band "滅火器" heard as "咪有企", or the guest "杜宗祐" written as "杜忠祐". The problem: when a transcript stores the wrong spelling, searching the correct name finds nothing. This release adds an "ASR Correction" tab in admin where you build a typo→correct dictionary (each rule can target a specific show or apply site-wide). Once set, every new transcription auto-applies the fixes before saving; for transcripts that already exist, a "Backfill" button recomputes and repairs them in one pass. So you know what you\'re committing to, the system first estimates "how many segments this rule will touch and roughly how much it costs" before any backfill, and only runs once you confirm. To avoid collateral damage, saving a rule shows how many segments the typo currently matches in scope, and matching is whole-term exact — it only replaces the identical term, never splitting words apart. The launch already fixed the 6 most common typos for the show "這又沒有很屌" across 1,507 transcript segments (total cost ~$0.05): 杜忠祐→杜宗祐, 阿鳴/阿明→阿名, 方品龍→方品融, 龍虎報→龍虎豹, 咪有企→滅火器. Searching those correct names now hits existing content.', }, summaryBullets: { zh: [ '後台新增「ASR 校正」分頁:建立「錯字→正字」對照,每條可綁節目或設全站通用', '新轉錄存檔前自動套用修正;舊逐字稿按「回填」一次重算修好', '回填前先估「會動到幾段 + 大約花多少錢」,確認後才執行', '儲存規則前顯示命中段數,採「整詞精確比對」只換完全相同的詞、不誤傷', '首發修好「這又沒有很屌」6 組錯字、共 1507 段(約 $0.05),正確名字現在搜得到', ], en: [ 'New "ASR Correction" admin tab: build a typo→correct dictionary, each rule show-scoped or site-wide', 'New transcriptions auto-apply fixes before saving; old transcripts repaired via one-click "Backfill"', 'Backfill first estimates "how many segments + roughly how much it costs", runs only on confirm', 'Saving a rule shows match count; whole-term exact matching replaces only identical terms, no collateral damage', 'Launch fixed 6 typos for "這又沒有很屌" across 1,507 segments (~$0.05) — correct names now searchable', ], }, }, // ─── v2.0 — Unified Query Experience (5/31) ─── { date: '2026-05-31', slug: 'semantic-topk-bump-and-show-more', milestone: 'v2.0', tag: 'enhancement', title: { zh: '語意搜尋一次給你更多片段 — 預設看 10 集、「顯示更多」往下加,不用重新查', en: 'Semantic Search Now Surfaces More — 10 Episodes Shown First, "Show More" Reveals the Rest Without Re-querying', }, summary: { zh: '語意模式過去固定只回 8 個片段——這不是刻意的設計,而是前端送查詢時沒帶「要幾筆」這個參數、後端就用了預設值 8。這版把語意查詢一次抓 25 筆(後端 API 本來就支援、不用改),讓你看到更多相關片段。為了不一次把整面卡片倒給你,結果預設先顯示 10 集,下方多一顆「顯示更多」按鈕、每按一次往下多顯示 5 集,全部顯示完按鈕就消失。重點是「顯示更多」純粹是把已經抓回來的結果往下展開,不會重新打一次搜尋——所以瞬間反應、不耗額外資源。排序完全沒變(只是看得更多、更深),跟以前一樣由相關度高到低。為什麼是抓 25 不是抓更多(譬如 50)?因為後端在組裝每個片段的前後文、高亮、摘要時是逐筆處理,抓越多初始回應越慢;25 是「給足深度」與「回應夠快」之間的平衡點。索引與對話模式的顯示不受影響。', en: 'Semantic mode previously returned a fixed 8 clips — not by design, but because the frontend never sent a "how many" parameter, so the backend used its default of 8. This release fetches 25 clips per semantic query (the backend API already supported this — no backend change), surfacing more relevant clips. To avoid dumping a wall of cards at once, results show the first 10 episodes initially with a "Show more" button below that reveals 5 more episodes per click, disappearing once everything is shown. Crucially, "Show more" simply expands results already fetched — it does NOT fire a new search — so it is instant and costs nothing extra. Ordering is unchanged (you just see more, deeper) — still highest-relevance first. Why 25 and not more (e.g. 50)? The backend assembles each clip\'s surrounding context, highlights, and summary one clip at a time, so fetching more makes the initial response slower; 25 balances depth against responsiveness. Index and Chat mode displays are unaffected.', }, summaryBullets: { zh: [ '語意查詢一次抓 25 筆(原本固定 8;後端 API 本就支援、零後端改動)', '結果預設顯示 10 集 + 「顯示更多」每次 +5,全部顯示完按鈕消失', '「顯示更多」是展開已抓回的結果、不重新搜尋 → 瞬間反應、零額外成本', '排序完全不變(仍由相關度高到低)—— 只是看得更多更深', '抓 25 非 50:後端逐筆組前後文/高亮/摘要,抓太多初始回應變慢,25 是深度與速度平衡點', ], en: [ 'Semantic queries now fetch 25 clips (was a fixed 8; backend API already supported it — zero backend change)', 'Results show 10 episodes first + "Show more" reveals 5 per click, button disappears when all shown', '"Show more" expands already-fetched results, no new search → instant, zero extra cost', 'Ordering unchanged (still highest-relevance first) — you just see more, deeper', '25 not 50: the backend assembles context/highlights/summary per clip, so over-fetching slows the initial response; 25 balances depth vs speed', ], }, }, { date: '2026-05-31', slug: 'unified-segment-citation-card', milestone: 'v2.0', tag: 'ui', title: { zh: '三種搜尋模式的引用卡長相統一了 — 每段都能單獨試聽、或跳到逐字稿看上下文', en: 'Citation Cards Unified Across All Three Modes — Each Clip Now Plays In Place or Jumps to the Transcript', }, summary: { zh: '在這版之前,同樣一段「引用」在索引、語意、對話三個模式長得完全不一樣:索引是一種卡、語意和對話是另一種卡、列舉題(「歌單哪幾集」)甚至只給集名沒有片段。而且每張卡只有一顆按鈕、把「試聽」跟「跳到逐字稿」綁在一起——你想原地聽一下,結果整頁跳走了。這版把三個模式的引用卡收斂成同一種「片段卡」,外觀與行為一致:每張卡都顯示片段文字(關鍵字高亮)+ 集標題 + 時間戳,並把動作拆成兩顆獨立按鈕——「播放此段」在原地用底部播放器試聽、頁面不跳走;「跳到逐字稿」才導航過去看完整上下文。高亮也統一:索引模式多關鍵字用兩色區分(橘色實線 / 青色虛線,色盲也能靠線型分辨),語意/對話用單色。列舉題現在保留「集清單」之餘,每集還能就地「展開查看各段」、不離頁看到該集的命中片段。另外對話的引用數量不再被檢索深度灌爆——每集最多先顯示 5 段、其餘用「顯示更多」漸進載入,只列出答案真正引用到的段落。', en: 'Before this release, the same "citation" looked completely different across the Index, Semantic, and Chat modes — Index had one card style, Semantic/Chat another, and enumeration answers ("which episodes are playlists") gave only episode names with no clips. Worse, each card had a single button that fused "preview" with "jump to transcript" — try to listen in place and the whole page navigated away. This release unifies all three modes onto one shared "segment card" with consistent look and behavior: every card shows the clip text (keyword-highlighted) + episode title + timestamp, with two SEPARATE buttons — "Play" previews in the sticky player WITHOUT leaving the page, and "Jump to transcript" navigates over for full context. Highlighting is unified too: Index multi-keyword search uses a two-color rotation (orange solid / cyan dashed — distinguishable by line style for color-blind users), Semantic/Chat use single color. Enumeration answers now keep the episode list AND let each episode expand its matching clips in place without navigating. Chat citation counts are no longer flooded by retrieval depth — each episode shows at most 5 clips first with a "Show more" to load the rest, displaying only the clips the answer actually cited.', }, summaryBullets: { zh: [ '索引/語意/對話三模式的引用卡收斂成同一種「片段卡」,外觀行為一致', '「播放此段」(原地試聽不跳頁)與「跳到逐字稿」(看上下文)拆成兩顆獨立按鈕', '高亮統一:索引多關鍵字兩色(橘實線/青虛線,色盲可辨)、語意/對話單色', '列舉題保留集清單 + 每集可就地「展開查看各段」不離頁', '對話引用每集先顯示 5 段 + 「顯示更多」,只列答案真正引用的段落(與檢索深度解耦)', ], en: [ 'Index / Semantic / Chat citations unified onto one shared segment card with consistent look + behavior', '"Play" (preview in place, no navigation) split from "Jump to transcript" (view context) into two separate buttons', 'Unified highlighting: Index two-color (orange solid / cyan dashed, color-blind safe), Semantic/Chat single color', 'Enumeration answers keep the episode list + each episode expands its matching clips in place', 'Chat shows 5 clips per episode + "Show more", listing only the clips the answer cited (decoupled from retrieval depth)', ], }, }, { date: '2026-05-31', slug: 'per-show-mode-example-prompts', milestone: 'v2.0', tag: 'feature', title: { zh: '新節目沒人搜過也有引導 — 每個模式給你「這個節目」可以怎麼問的範例', en: 'Guidance Even for Brand-New Shows — Each Mode Now Suggests Example Questions Tailored to That Show', }, summary: { zh: '查詢頁三個模式的輸入框過去只有通用提示,新使用者進到一個節目常常不知道「針對這個節目」可以問什麼。原本有「7 日熱搜」chip 引導,但剛上架或冷門的節目沒有足夠搜尋紀錄,引導就消失了。這版補上兩層引導:(1) 三個模式的輸入框各有一句符合該模式的提示語(索引引導你打關鍵字、語意引導你描述、對話引導你問需要跨集統整的問題);(2) 輸入框上方的 chip:熱搜夠多就顯示熱搜,**冷啟動時改顯示「範例」chip** —— 這些範例是後端用該節目已有的每集摘要、來賓名單等素材,請 LLM 為每個模式各預先生成 2–3 題「這個節目風格」的問題(譬如索引給「阿嬤家」這種關鍵字、對話給「整理不同來賓對 Diss Track 的看法」這種統整題)。點任何一顆 chip 就自動帶入並執行查詢。範例是「事先產好存起來」的,不會在你每次查詢時即時生成、不增加查詢延遲與成本;新節目轉錄+摘要跑完會自動產生,後台也能一鍵補產。', en: 'The three query-mode inputs previously had only generic placeholders, so new users landing on a show often did not know what they could ask "about this show". Trending-query chips helped, but freshly-added or low-traffic shows lack enough search history, so the guidance vanished. This release adds two layers of guidance: (1) each mode\'s input has a mode-specific placeholder (Index nudges you toward keywords, Semantic toward a description, Chat toward cross-episode synthesis questions); (2) the chip row above the input: when trending is plentiful it shows trending, but on cold-start it shows "Example" chips instead — pre-generated by the backend, which feeds each show\'s existing per-episode summaries + guest lists to an LLM to produce 2–3 mode-appropriate example questions per mode (e.g. Index gets keyword entities like "阿嬤家", Chat gets synthesis questions like "compare what different guests think about Diss Tracks"). Clicking any chip populates and runs the query. Examples are pre-generated and cached — never produced live during your query, so no added latency or cost; they generate automatically after a new show finishes transcription + summaries, and admins can backfill on demand.', }, summaryBullets: { zh: [ '三模式輸入框各有符合該模式的提示語(索引=關鍵字、語意=描述、對話=跨集統整)', '冷啟動節目(熱搜不足)顯示 LLM 預產的「範例」chip,點擊帶入並執行', '範例用該節目的每集摘要 + 來賓名單當素材、為每模式各產 2–3 題該節目風格問題', '預產 + 快取,不在查詢時即時生成(零延遲/成本);轉錄摘要完成自動產 + 後台可一鍵補', '上線後三個收錄節目皆已補齊範例(各模式 3 題)', ], en: [ 'Each mode\'s input has a mode-specific placeholder (Index = keywords, Semantic = description, Chat = cross-episode synthesis)', 'Cold-start shows (sparse trending) show LLM-pre-generated "Example" chips; clicking populates + runs', 'Examples use the show\'s per-episode summaries + guest lists, generating 2–3 show-styled questions per mode', 'Pre-generated + cached, never live at query time (zero latency/cost); auto-generates after summaries finish + admin backfill', 'All three catalogued shows backfilled at launch (3 examples per mode)', ], }, }, // ─── v1.9 — Index Mode: Precise Multi-Keyword Search (5/31) ─── { date: '2026-05-31', slug: 'keyword-index-mode', milestone: 'v1.9', tag: 'feature', title: { zh: '第三種搜尋模式「索引」上線 — 多關鍵字精準比對', en: 'Third Search Mode "Index" Is Live — Precise Multi-Keyword Matching', }, summary: { zh: '查詢頁三個 tab 裡的「索引」過去是「即將推出」佔位,這版正式接通 — 你現在可以一次打多個關鍵字(譬如「馬世芳 歌單」),系統用「嚴格比對」找出真的同時講到這些詞的內容,跟另外兩個模式定位完全不同:「語意」是找意思相近(就算用詞不一樣也撈得到)、「對話」是問一個問題請 AI 整理答案,而「索引」就像對整個節目下 Ctrl+F、要求每個關鍵字都要命中。結果分三層、由準到寬呈現:第一層是「同一段話裡同時出現所有關鍵字」(最精準);第二層是「同一集裡這些詞分散在不同段落/標題/簡介」(同集相關但不在同一句);只有當前兩層都掛蛋時,第三層才退而求其次列出「任一關鍵字命中」的結果,避免你看到一片空白。每個關鍵字在結果裡用兩種顏色高亮區分(橘色實線=逐字稿命中、青色虛線=標題/簡介命中),第二層的每一集還能就地展開看到實際命中的各個段落,不用跳頁。單次最多回 100 筆、5 秒逾時保護。上線前在 prod 跑四個情境驗證全綠:「歌單」單詞(第一層滿載 100 筆+第二層 56 集折疊+分頁)、「馬世芳 歌單」(第二層就地展開 26 段、兩色高亮都對)、「馬世芳 滅火器」(前兩層 0 命中、正確退到第三層 — 也順帶暴露「滅火器」被 Whisper 聽成錯字、逐字稿裡根本沒這三個字,屬已知的 ASR 錯字 backlog)、空查詢(空狀態正確)。另外修掉一個剛接通就會踩到的小 bug:每次用索引搜尋,後端記錄搜尋事件的 API 都會回 422 錯誤(它的合法模式清單沒加進新的 index),現在補上了。', en: 'The "Index" tab in the query page — previously a "coming soon" placeholder — is now fully wired up. You can type multiple keywords at once (e.g. "馬世芳 歌單") and the system finds content that genuinely mentions all of them, using strict matching. This positions it distinctly from the other two modes: "Semantic" finds meaning-similar content (even with different wording), "Chat" answers a question via the AI, and "Index" is like running Ctrl+F across the entire show, requiring every keyword to hit. Results come in three tiers, precise to loose: Tier 1 is "all keywords appear in the same passage" (most precise); Tier 2 is "the keywords are spread across different segments / title / description within the same episode" (same-episode relevant but not in one sentence); only when both Tiers 1 and 2 come back empty does Tier 3 fall back to listing "any keyword matches" so you never face a blank screen. Each keyword is highlighted in two colors (orange solid = transcript hit, cyan dashed = title/description hit), and every Tier 2 episode can be expanded in place to see the actual matching segments without navigating away. Capped at 100 results per search with a 5-second timeout. Four prod scenarios verified green before launch: single word "歌單" (Tier 1 maxed at 100 + Tier 2 with 56 episodes collapsed + pagination), "馬世芳 歌單" (Tier 2 inline-expanded 26 segments, both highlight colors correct), "馬世芳 滅火器" (0 hits in Tiers 1-2, correctly fell back to Tier 3 — incidentally exposing that "滅火器" was misheard by Whisper as a typo and the three characters appear nowhere in the transcript, a known ASR-typo backlog item), and an empty query (correct empty state). Also fixed a small bug that fired the moment the feature went live: every index search made the backend\'s search-event-logging API return a 422 error (its list of valid modes had not been updated to include the new "index" mode) — now patched.', }, summaryBullets: { zh: [ '第三種搜尋模式「索引」上線,與「語意」「對話」三模式到齊', '一次打多個關鍵字、嚴格比對(每個詞都要命中),像對整個節目下 Ctrl+F — 跟語意(意思相近)、對話(問 AI)定位不同', '結果由準到寬分三層:同段全中/同集分散/任一詞命中(僅前兩層皆空才退此層)', '關鍵字兩色高亮(橘=逐字稿、青=標題/簡介)+ 第二層可就地展開看實際命中段落', '上線前 prod 四情境驗證全綠;煙霧測試順帶再現「滅火器」Whisper ASR 錯字(已知 backlog)。另修掉每次索引搜尋都噴的 422 事件記錄 bug', ], en: [ 'Index mode is now live — the third and final search mode joins Semantic and Chat', 'Type multiple keywords with strict AND matching (every word must hit), like Ctrl+F across the whole show — distinct from Semantic (meaning-similar) and Chat (ask the AI)', 'Three-tier results, precise to loose: same-passage / same-episode-scattered / any-keyword fallback (only when the first two are empty)', 'Two-color keyword highlighting (orange = transcript, cyan = title/description) + expand-in-place to see Tier 2 matching segments', 'Four prod scenarios verified green; smoke test incidentally re-surfaced the "滅火器" Whisper ASR-typo (known backlog). Also fixed a 422 on the search-event API that fired on every index search', ], }, }, // ─── v1.8 — Chat Mode: Agentic (5/21~) ─── { date: '2026-05-31', slug: 'eval-runner-eval-context-plumbing', milestone: 'v1.8', tag: 'enhancement', title: { zh: '評分跑分把「哪一題」「第幾輪」一路標記到資料庫 — RCA 一句 SQL 就能撈', en: 'Eval Runs Now Tag Every Trace With Item + Turn — RCA Is One SQL Query Away', }, summary: { zh: '昨天的評分框架升級把觀察工具裝起來、把資料庫雙寫倉也建好了,但「評分跑分時把每個 LLM 呼叫 / 工具呼叫貼上題號跟輪次」這條最後一段沒通 — 結果是雙寫倉裡題號欄位全部空白、跨輪比對 RCA 沒法做。這次補完那段,使用者體感沒變化,但未來任何對話品質改動的事後 RCA 時間從「翻 log 拼湊一兩小時」變成「一句 SQL 撈出全部」。具體做法:跑分腳本每次出題前自己生一個 run_id(時間戳 + 隨機字尾)、每送一題就在 HTTP 標頭塞三個欄位(run_id / 題號 / 第幾輪)。後端只在管理員身分 + 三欄齊全時才把這些上下文綁進當下這個 request、結束就回 None;外部使用者完全不會誤觸發、資料庫不會被污染。實作過程踩到兩個 surprise:(1) 雙寫倉的 JSONB 欄位之前沒人真的寫進去,本次第一次跑就被資料庫拒收(驅動程式不認 Python dict、要先轉 JSON 字串再 CAST)— hotfix 修掉、現在每筆乾淨落地。(2) 線上環境本來以為 trace 是開的、實際還是關的 — 切開後再跑才看到 34 個 span 全部落到資料庫、8 個題目 + 多輪題 mt03 三輪全部都有。也附了一個示範腳本,未來任何「為什麼這版退步了」的 RCA,直接拿 SQL 比對兩個 run_id 的 search query 就能看出 agent 是不是改了措辭。', en: 'Yesterday\'s eval framework upgrade installed the observability tooling and the dual-sink data warehouse, but the last mile — tagging every LLM/tool call within an eval run with its item id and turn index — was never wired up. Result: locator columns in the warehouse were empty, cross-run RCA SQL was impossible. This change closes that gap. No user-facing change, but post-hoc RCA for any chat-quality regression now drops from "1-2 hours scraping logs" to "one SQL query". How: the eval runner generates a run_id (timestamp + random suffix) per invocation and attaches three headers per request (run_id / item_id / turn_idx). Backend honors them only when the caller is an admin AND all three headers are present; prod user traffic is silently ignored, the warehouse stays clean. Two surprises along the way: (1) the warehouse\'s JSONB columns had never actually been written to before — the asyncpg driver rejected Python dicts on first real INSERT (needed JSON serialization + explicit CAST). Hotfix in same commit, now every span lands cleanly. (2) Tracing was assumed ON in prod but was actually OFF; after toggling, 34 spans landed for the calibration run (8 distinct items + multi-turn mt03 across 3 turns). A demo SQL script ships alongside, so future "why did this version regress?" questions can be answered by SQL-diffing the agent\'s search queries between two run_ids.', }, summaryBullets: { zh: [ '評分跑分時的每個 LLM/工具呼叫,現在都帶有 run_id / 題號 / 輪次 三個欄位落到資料庫', '未來 RCA「為什麼這版退步?」一句 SQL 就能比兩版 agent search query 差異', '只認管理員身分 + 三欄齊全才綁定 — 外部使用者流量不會誤污染資料倉', '過程踩到兩個 surprise:JSONB 寫入機制壞著(hotfix 修掉)+ trace 線上其實是關的(已開)', '本次跑完驗證:8 題 calibration + 多輪題 mt03 三輪 共 34 個 span 全部乾淨落地', ], en: [ 'Every LLM/tool call in an eval run now carries run_id / item_id / turn_idx in the warehouse', 'Future "why did this version regress?" RCA is now one SQL diff between two run_ids on agent search queries', 'Admin-only + three-headers-present gate ensures prod user traffic never pollutes the eval warehouse', 'Two surprises caught along the way: JSONB write path was broken (hotfix in same commit) + tracing was OFF in prod despite assumption (now toggled ON)', 'Verification run landed 34 spans across 8 calibration items + multi-turn mt03 across 3 turns, all columns populated', ], }, }, { date: '2026-05-30', slug: 'langfuse-sdk-overhead-rca', milestone: 'v1.8', tag: 'experiment', title: { zh: '量測證實「外部觀察工具拖慢系統」是誤判 — RCA 兩輪後清白', en: 'Measurement Cleared Langfuse Cloud SDK: "It Slows Us Down" Was a False Alarm', }, summary: { zh: '昨天 ship 評分框架升級時量到「打開 trace 觀察會讓 P95 延遲 +3.4 秒」、當下推給 Langfuse 雲服務本身慢、開了「自架 Langfuse 評估」當下一條 follow-up。今天 RCA 兩輪。第一輪 review 想「會不會其實是我們自己寫的 PG 雙寫沒做非阻塞」、寫了完整提案 + 設計 + 規格 + 任務、apply 開始時 grep 才發現雙寫 code path 從來沒被觸發過(runner 沒呼叫對應 binding 函式)— 假設破滅、暫存 change。第二輪做了 spike:在 trace 內部四個可疑 Langfuse 呼叫各包 timing wrapper、推到 prod 量。結果:每個 op 都 0.1-0.5 ms 級別、每 span total ~1 ms、30 span/query 也只 ~28 ms total overhead。**完全跟官方說的 0.1ms async-batched 一致**。對比同期跑出來的整體請求 P95 反而比昨天 4.4 OFF baseline **還快 2.3 秒** — 不可能是 SDK 加了 +3.4s。**結論:昨天那輪 +3.4s 是跨 session 量測雜訊**(兩個獨立 session 之間 prod 流量背景 / cache / 網路條件不同)、SDK 是清白的、自架 follow-up 取消。教訓:跨 session P95 比較不能當 RCA attribution 證據、要同 session 內 timing instrumentation 才算數。Spike 工具留在 codebase 預設關、未來想再量直接 toggle 一個環境變數。', en: 'Yesterday\'s eval framework ship measured "+3.4s P95 latency when tracing turned on" and we blamed Langfuse Cloud SDK, opening "evaluate self-hosting Langfuse" as the next follow-up. Today: two rounds of RCA. Round 1 hypothesised it was our own PG dual-sink not doing fire-and-forget; wrote full proposal + design + spec + tasks, then grep during apply found the PG sink code path was never triggered (runner never called the binding function). Hypothesis falsified, change parked. Round 2 spike: wrapped four suspect Langfuse SDK calls inside trace_span with `time.perf_counter` timing, deployed to prod, measured. Result: each op 0.1-0.5 ms, per-span total ~1 ms, 30 spans/query = ~28 ms total overhead. **Exactly matches the 0.1ms async-batched figure in official Langfuse docs**. Overall request P95 with tracing ON was actually **2.3s faster** than yesterday\'s 4.4 OFF baseline — definitively not a +3.4s SDK overhead. **Conclusion: yesterday\'s +3.4s was cross-session measurement noise** (prod traffic / cache / network differed between two independent measurement sessions). SDK is innocent, self-host follow-up cancelled. Lesson: cross-session P95 comparison is invalid as RCA attribution evidence; same-session timing instrumentation is required. Spike tooling stays in codebase default-off; re-measurement is one env var toggle away.', }, summaryBullets: { zh: [ '昨天 4.4 量到 +3.4s P95 推 SDK 慢 → 今天 RCA 兩輪後證實 SDK 清白、是跨 session 量測雜訊', '加 timing wrapper 量到實際 SDK overhead 0.947 ms/span(與官方 docs 0.1ms 量級一致)', '自架 Langfuse follow-up 取消(自架主要動機「拉延遲」不存在)', '教訓:跨 session P95 比較不能當 RCA attribution、要同 session 內 timing 證據才算數', ], en: [ 'Yesterday\'s 4.4 measurement attributed +3.4s P95 to SDK overhead → today\'s 2-round RCA cleared the SDK, was cross-session noise', 'Timing wrappers showed real SDK overhead is 0.947 ms/span (matches official 0.1ms async-batched figure)', 'Self-host Langfuse follow-up cancelled (primary motivation "pull down latency" does not exist)', 'Lesson: cross-session P95 comparison invalid as RCA attribution evidence; same-session timing required', ], }, }, { date: '2026-05-30', slug: 'eval-framework-upgrade', milestone: 'v1.8', tag: 'enhancement', title: { zh: '評分基準升級:6 個新指標 + 兩支「PR 前先驗證」CLI 工具', en: 'Evaluation Framework Upgrade: 6 New Metrics + 2 Pre-PR Verification CLIs', }, summary: { zh: '升級內部評分基準,避免下次再撞到 2026-05-28 IDF 實驗的觀察盲點。這次的改動使用者體感**沒直接變化**,但對「未來任何 retrieval / prompt 改動的事前驗證流程」是個底層升級。三件事:(1) 接入業界標準的評分套件 DeepEval,新增 6 個語意層級指標 — 像是「答案的關聯度」「檢索片段是否真的覆蓋到答案」「答案是否忠實於檢索片段」等,補既有「比對段落 ID 是否一字不差」這種容易誤判的脆弱指標。實際跑全 34 題基準時,這次新指標就抓到一個關鍵 insight:表面看起來退步 0.1 的 chunk_recall 其實**不是真的退步** — agent 撈到的相鄰段落(差 13 秒)內容跟正確段落實質重疊、答案品質完全沒變,只是 ID 比對嚴格。新指標的「語意 precision 0.92」直接證實 retrieval 仍健康,避免我們花一整週去追一個假退步。(2) 加進 PR template 的兩支 CLI:`retrieve_probe.py` 可以在改 retrieval 前對特定問題跑 episode-scoped 排序檢查、確認 ground-truth 段落還在前 K 名;`prompt_fingerprint_diff.py` 可以在改 prompt 前對比兩個 backend 看 agent 對同一問題會不會生出不同的 search query — 這兩支工具會自動 surface 之前要花幾小時 RCA 才發現的副作用。(3) 接入 Langfuse Cloud trace 觀察,每次 eval 跑都能在網頁上看 chat_agent_turn → llm_call → tool_call 三層 span tree,未來 RCA 不用再翻 log 拼湊。**一個未達設計目標的點**:原本希望 Langfuse SDK 對 prod 流量延遲增量 < 100ms,實際量到 P95 +3.4 秒、retrieval 重的題目 +5-6 秒,遠超預期。對策是維持 prod 預設關閉 tracing(只在 eval 跑時開),並開了「自架 Langfuse 評估」做為下一條 follow-up(自架 LAN 內延遲應該能拉回 <200ms)。', en: 'Internal evaluation baseline upgrade — addresses the observability blind spots that caused the 2026-05-28 IDF experiment failure. **No direct user-visible change**, but a foundational upgrade for future retrieval/prompt change verification. Three things: (1) Integrated DeepEval, adding 6 new semantic-level metrics — answer relevancy, contextual recall (whether retrieved chunks cover the GT answer), contextual precision, faithfulness, semantic answer similarity, entity recall. These complement the existing brittle "chunk-ID byte-exact match" indicator. On the full 34-item baseline, the new metrics immediately surfaced an important insight: the apparent chunk_recall -0.1 regression was **not actually a regression** — the agent had landed on adjacent chunks (13s away) with substantively overlapping content, answer quality unchanged. New `contextual_precision=0.92` directly confirmed retrieval was still healthy, saving us a week of chasing a phantom regression. (2) Two new CLIs added to PR template: `retrieve_probe.py` runs episode-scoped ranking checks against specific items to confirm GT chunks are still in top-K before merging retrieval changes; `prompt_fingerprint_diff.py` compares two backends to surface whether the agent rephrases its search queries differently after a prompt change — both tools auto-surface side effects that previously required hours of RCA to find. (3) Langfuse Cloud tracing integration — every eval run emits a chat_agent_turn → llm_call → tool_call 3-level span tree viewable in the web UI, so future RCA no longer requires log-scraping. **One design miss**: the original target was <100ms Langfuse SDK latency overhead on prod traffic; actual measurement was P95 +3.4s, retrieval-heavy queries +5-6s, far exceeding expectations. Mitigation: keep prod tracing default-OFF (eval-only), opened follow-up "evaluate self-hosting Langfuse" — LAN-local RPC should bring it back under 200ms.', }, summaryBullets: { zh: [ '新增 6 個語意層級指標(DeepEval 4 個內建 + GEval 自寫 2 個),補既有「段落 ID 一字不差」這種脆弱指標', '新指標立刻抓到:本次全 34 題 baseline 對比上輪「退步 0.1」是假警報、實質檢索健康,省下一週追假退步', '兩支 PR 必跑 CLI:retrieve_probe.py 事前驗 GT 段落還在 top-K、prompt_fingerprint_diff.py 事前驗 agent 不會自己改 search query', '接 Langfuse Cloud trace 觀察 — eval 跑時可看每個 agent step 的 span tree,未來 RCA 更快', '未達設計目標:Cloud SDK 對 prod 延遲增量遠超預期(P95 +3.4s),prod 預設關閉、自架評估 follow-up 升優先', ], en: [ 'Added 6 semantic-level metrics (4 DeepEval built-ins + 2 GEval custom rubrics), complementing the brittle "chunk-ID byte-exact match" indicator', 'New metrics immediately caught: this run\'s apparent "chunk_recall -0.1 regression" vs prior baseline was a false alarm — retrieval still healthy, saved a week of phantom-regression chasing', 'Two PR-mandatory CLIs: retrieve_probe.py pre-validates GT chunks still in top-K, prompt_fingerprint_diff.py pre-detects agent re-phrasing search queries', 'Langfuse Cloud tracing — eval runs emit a full span tree viewable in web UI, making future RCA significantly faster', 'Design miss: Cloud SDK latency overhead far exceeded target (P95 +3.4s vs <100ms goal); prod tracing default-OFF, self-host evaluation promoted as follow-up', ], }, }, { date: '2026-05-28', slug: 'retrieve-quality-step1-idf-and-prefilter-failed', milestone: 'v1.8', tag: 'experiment', title: { zh: 'IDF 加權實驗(兩 Layer 都試失敗、已 revert)', en: 'IDF Weighting Experiment (Two Layers Tried, Both Reverted)', }, summary: { zh: '為了改善「跨集問題」的內部 chunk_recall(從 0.482 想拉到 0.55+),這週試了兩條方向,**兩條都沒過、全部回滾**,使用者體感沒變化、整理一下教訓寫進路線圖避免下次再撞。第一條(Layer A)是給段落關鍵字打分時加上「IDF 加權」— 罕見詞(譬如「振奮」)權重給 1.0、常見詞(「什麼」)給 0.05。直覺上應該讓罕見詞主導排序。實際跑完 34 題內部 baseline:chunk_recall 0.482 → 0.382(退 0.1),7 題退步。Root cause:在 podcast transcript 領域,IDF 假設「罕見詞 = 答案信號」**完全不成立** — 罕見詞往往是「整集主題」(譬如「伴手禮」在那一集滿地都是),加權後反而把「跟主題相關但不是答案」的段落推上去、把真答案擠出 top-K。第二條(Layer B)是改 chat agent 的 prompt,看到「EP134」「第 134 集」這類明確集數 reference 時強制走「先找集數、再在那集內搜尋」。本來假設純 prompt 改、不會動 retrieval。實際 chunk_recall 0.482 → 0.340 退更慘。Root cause:prompt 改了 agent 對同一問題重新措辭 search query 的方式,下游 ts_rank 收到不同 token、命中不同段落 — **prompt change 從來不是 orthogonal 於 retrieval**。兩條教訓 + 一條「show-wide DB probe 是 false positive validator」教訓寫進工程紀錄。**下一步轉向評估框架升級**(per-question tool trace 落地、episode-scoped retrieval probe、span-level metric),先有更好的觀察工具才能避免下次類似盲點再撞。Prod 沒實質變動,僅留下一個未使用的 token-frequency 資料表(無 schema 衝擊,未來實驗可能 reuse)。', en: 'Tried two directions this week to push internal cross-episode chunk_recall from 0.482 toward 0.55+; **both failed and were reverted**, no user-visible change, lessons captured to avoid repeats. Layer A added "IDF weighting" to lexical scoring — rare tokens (e.g. "振奮") get 1.0 weight, common tokens ("什麼") get 0.05. Intuitively rare tokens should dominate ranking. Actual 34-item baseline: chunk_recall 0.482 → 0.382 (-0.1), 7 cases regressed. Root cause: in the podcast-transcript domain, the IDF assumption "rare token = answer signal" **completely fails** — rare tokens are usually the episode\'s overall topic (e.g. "伴手禮" appears everywhere within that one episode), so weighting them up promotes "topic-related-but-not-answer" chunks and pushes the actual answer out of top-K. Layer B changed the chat agent\'s prompt to mandate "find episode first, then search within it" for explicit EP-references like "EP134" / "第 134 集". Assumed to be pure prompt nudge with zero retrieval risk. Actual chunk_recall 0.482 → 0.340 (worse than A). Root cause: the new prompt changed how the agent re-phrased its search query for the same user question, downstream ts_rank received different tokens and hit different chunks — **prompt changes are never orthogonal to retrieval**. Two lessons plus a third ("show-wide DB probe is a false positive validator for episode-scoped retrieval") are now in engineering notes. **Next direction pivots to evaluation framework upgrade** (per-question tool trace, episode-scoped retrieval probe, span-level metrics) — better observability tooling first, before touching retrieval or prompts again. No prod functional change; only a leftover unused token-frequency table (no schema impact, possible future-experiment reuse).', }, summaryBullets: { zh: [ '本週試 IDF 加權 + EP-ref dispatch 兩條 retrieval 改善方向 — 兩條都退步、全部 revert,使用者體感沒變化', 'Layer A (IDF 加權) root cause:podcast domain 罕見詞 = 整集主題、不是答案信號 — 加權把錯段落推上去', 'Layer B (EP-ref prompt) root cause:prompt 改了 agent 對 search 工具的 query 措辭、下游 retrieval 跟著飄', '下動轉向:先升級評估框架(trace 落地 + episode-scoped probe)再動 retrieval,避免再撞觀察盲點', ], en: [ 'Tried two retrieval-improvement directions (IDF weighting + EP-ref dispatch) — both regressed and were reverted, no user-visible change', 'Layer A (IDF weighting) root cause: in podcast domain, rare tokens are episode topics not answer signals — weighting them up promoted wrong chunks', 'Layer B (EP-ref prompt) root cause: prompt nudge changed how the agent worded its search query, downstream retrieval drifted with it', 'Next pivot: upgrade evaluation framework first (tool trace + episode-scoped probe) before touching retrieval again, to avoid blind spots', ], }, }, { date: '2026-05-27', slug: 'judge-pronoun-attribution-check', milestone: 'v1.8', tag: 'enhancement', title: { zh: '判分 AI 學會看出「把別人的故事套到你問的那個人身上」', en: 'The Grading AI Now Catches "Wrong-Person Story Attribution"', }, summary: { zh: '修內部「打分 AI」的兩個盲點。第一個盲點是物理層:打分 AI 過去看到的資料只有「摘要」(每段截斷到 500 字),跟回答用的 AI 看到的完整段落(最多 8000 字)不一樣 — 等於用刪節版去判斷一個寫了完整劇本的演員,根本沒上下文。第二個盲點是邏輯層:過去判分 AI 只比對「答案 vs 期望摘要的表面用詞」,看到 b23 那種「段落講的是迪拉跟呂安初遇,但回答 AI 把『他』『我』全套用到 Leo 王身上」的代詞錯誤套用,判分 AI 完全抓不到、還給 0.95 高分。本次修法:(1) 餵完整段落給判分 AI、不再用 500 字截斷版 (2) 新增第 4 個指標「代詞主體驗證」三態:完美對齊 / 合理推測 / 張冠李戴 (3) 用 b23 案例當示範教 AI 怎麼判。完整 34 題重跑後,三大主要指標全部往上:跨集問題抓取準確度 0.382→0.482、答案事實正確性 0.831→0.892、拒答適切性 0.971→1.000;代詞驗證分佈 4 完美對齊 / 1 合理推測 / 0 張冠李戴 / 29 不適用 — 0 個張冠李戴反映前兩週的 dataset + retrieval 修正真實生效,回答 AI 不再撈到無關段落了。對使用者體感無直接影響,但內部評分基準變得更可信,後續任何對話品質改造都有更準的反饋。', en: 'Fixed two blind spots in the internal grading AI. Physical blind spot: the grading AI was only seeing a "summary" of each tool result (truncated to 500 chars), while the answering agent saw the full 8000-char chunk — like grading an actor based on a CliffsNotes summary of the script. Logical blind spot: the rubric only compared surface wording between answer and expected, so cases like b23 (where chunks tell a 迪拉-呂安 first-meeting story but the agent maps all the "he/I/we" pronouns onto Leo 王) sailed through with factual=0.95. This change: (1) feeds the full chunk text to the grading AI (no more 500-char truncation), (2) adds a 4th indicator "pronoun attribution check" with three states (grounded / inferred / hallucinated), (3) uses b23 as the canonical example to teach the rubric. After re-running all 34 items, the three main metrics all moved up: cross-episode retrieval precision 0.382→0.482, answer factual correctness 0.831→0.892, refusal appropriateness 0.971→1.000. Pronoun-attribution distribution: 4 grounded / 1 inferred / 0 hallucinated / 29 null — zero hallucinations reflects that the prior two weeks\' dataset + retrieval fixes genuinely solved the b23-class problem (agents no longer reach unrelated chunks). No direct user-facing change, but our internal grading baseline is now trustworthy, giving future chat-mode improvements honest feedback.', }, summaryBullets: { zh: [ '修判分 AI 物理層盲點:從看「500 字摘要」改成看「完整 8000 字段落」,跟回答 AI 看的對齊', '新增第 4 個指標「代詞主體驗證」三態:完美對齊 / 合理推測 / 張冠李戴', '全 34 題重跑:跨集準確度 0.382→0.482、事實正確性 0.831→0.892、拒答適切性 0.971→1.000,三大指標全提升', '0 個張冠李戴 = dataset + retrieval 前置修正真實生效;後續對話模式改造有可信反饋', ], en: [ 'Fixed grading AI physical blind spot — now sees the full 8000-char chunk instead of a 500-char summary, matching what the answering agent sees', 'Added 4th indicator "pronoun attribution check" with three states: grounded / inferred / hallucinated', 'Full 34-item re-eval: cross-episode precision 0.382→0.482, factual 0.831→0.892, refusal 0.971→1.000 — all three main metrics up', '0 hallucinations confirms prior dataset + retrieval fixes genuinely solved the b23-class problem; future chat-mode work now has trustworthy feedback', ], }, }, { date: '2026-05-27', slug: 'b23-dataset-and-retrieval-rca-fix', milestone: 'v1.8', tag: 'fix', title: { zh: '從一題對話 mode 答錯案例挖出 4 層連鎖問題', en: 'A Single Wrong-Answer Bug Exposed Four Layers of Hidden Issues', }, summary: { zh: '從一題對話模式答錯的案例(user 問「迪拉跟 Leo 王怎麼從不認識變成合作夥伴?」),user 親自聽逐字稿驗證後挖出 4 層連鎖問題。第一層是檢索:原本回答把 EP129(迪拉跟「呂安」的故事,跟 Leo 王完全無關)誤當成正確來源。第二層是 LLM 代詞解析:AI 拿到無關段落後自動把段落內的「他」「我」誤套到 query 主體(Leo 王),表面看引用真實逐字稿、實際語意全錯。第三層是內部評分機制:判分用的 LLM 沒有檢查代詞指向是否一致,所以這種「表面 grounded 實際 hallucinate」的回答竟然拿到高分通過。第四層是內部測資本身:dataset 標的「正確段落」之一(EP116 @187.48)也標錯了 — 那段其實是小老虎跟 Leo 王相識的故事,迪拉只是搭橋安排演出,跟「迪拉跟 Leo 王第一次見面」是兩件事。修法:(1) dataset 修兩題的標註 — b23 移除 EP116 標錯段、b22 補上 7 個分散的人物證據段落 (2) 後端 episode finder 新增 guest-index 派遣路徑,當 query 含 ≥2 個可辨識的來賓名稱時,加跑來賓索引找出真有那些人參與的集數 (3) 內部 diagnose 工具擴到 top-500 + 可看前後段落 (4) 把 b20 揭露的另一個 retrieve 召回根本問題(某些段落連 top-500 都沒撈到)紀錄為下一個 follow-up。Prod 重跑:b23 真實提升 chunk_recall 0→0.5,其他題 deep_dive 對照組全部維持。內部評分指標的代詞驗證 + LLM 代詞紀律則記為兩條獨立 follow-up,不在本次 scope。', en: 'A single wrong-answer case in chat mode (user asked "how did 迪拉 and Leo 王 go from strangers to collaborators?") cascaded into four hidden issues, exposed only after the user personally listened to the cited transcript. Layer 1 was retrieval: the answer was sourced from EP129 (a story about 迪拉 and 呂安, entirely unrelated to Leo 王). Layer 2 was LLM pronoun resolution: the AI took the unrelated chunk and auto-mapped pronouns like "he/I" to the query subjects, producing an answer that looked grounded but was semantically wrong. Layer 3 was the internal evaluation grader: the LLM judge didn\'t verify pronoun-referent consistency, so "looks grounded but actually hallucinated" answers got high scores. Layer 4 was the dataset itself: one of the "correct ground-truth chunks" (EP116 @187.48) was also mislabelled — that segment is actually about 小老虎 meeting Leo 王 (迪拉 was just the venue arranger), not 迪拉 meeting Leo 王. Fixes: (1) dataset corrections for two items — b23 removed the mislabelled EP116 chunk; b22 added 7 person-evidence chunks; (2) backend episode finder gained a guest-index dispatch path that triggers when a query mentions ≥2 known guest names; (3) admin diagnose endpoint extended to top-500 with neighbour-chunk context; (4) the orthogonal retrieve_hybrid recall gap exposed by b20 (some ground-truth chunks not even in top-500) is filed as a separate follow-up. Prod re-eval: b23 chunk_recall improved from 0 → 0.5; deep_dive control group all unchanged. LLM pronoun grounding + judge pronoun-attribution checks remain separate follow-ups, deliberately out of scope.', }, summaryBullets: { zh: [ '一個對話模式答錯案例挖出 4 層問題:retrieval miss + LLM 代詞 hallucinate + judge 抓不到 + dataset 也標錯', 'b23 真實提升:chunk_recall 0→0.5;dataset 內部測資的代詞驗證紀律寫進 spec', '後端新增 guest-index 派遣路徑,未來涉及多人關係的 query 可走來賓索引而非主題索引', '內部 diagnose 工具擴到 top-500 + 看前後段落,揭露 b20 的另一個 retrieve 召回根本問題(留 follow-up)', 'LLM 代詞解析 grounding + judge 代詞驗證留為兩條獨立 follow-up', ], en: [ 'A single wrong-answer case exposed 4 layers: retrieval miss + LLM pronoun hallucination + judge blind spot + dataset mislabel', 'b23 real improvement: chunk_recall 0→0.5; dataset pronoun-referent verification discipline now in spec', 'Backend added guest-index dispatch — multi-person-relationship queries can route via guest index instead of topic index', 'Admin diagnose extended to top-500 with neighbour context, exposing a separate retrieve_hybrid recall gap (filed as follow-up)', 'LLM pronoun grounding + judge pronoun-attribution checks filed as two independent follow-ups', ], }, }, { date: '2026-05-27', slug: 'eval-baseline-citation-bug-revalidation', milestone: 'v1.8', tag: 'enhancement', title: { zh: '對話模式內部評分數據洗乾淨了,找到下一步該動哪裡', en: 'Chat-Mode Eval Baseline Cleaned; Next Bottleneck Identified', }, summary: { zh: '回頭洗一筆內部數據污染。上週新增「主題預過濾」檢索工具後,agent 在這條路徑撈到的段落沒被內部評分機制納入計算(程式有個白名單漏更新),導致連續好幾天的「跨集問題」評分數據都被低估到 0 — 我們以為改善沒效果,其實是評分本身在說謊。修白名單之後(commit 287e73b)重跑全 34 題乾淨基準,發現「跨集問題」真實的內部 chunk_recall 是 0.283,而非過去誤以為的 0.244;過去三個 archive change 的對照基準需要重新解讀。重洗過程順手把 4 道跨集題(b20/b21/b22/b23)的內部 retrieve 結果與 ground truth 命中位置全部列出來放大鏡 audit,發現 b22 / b23 退步的真實原因不是 rerank 排序問題(rerank 改不到 root cause),而是 b22 屬於「LLM 答案策略題」、b23 是「主題預過濾的候選集找錯」— 這個發現直接讓原本計畫的 voyage-rerank-tune-b22-b23 change 可以改寫為兩條更精準的子 change(topic-finder 強化 + meta-question 答案策略),省下一輪錯誤方向的調整。對使用者直接體感沒變化,但內部評分數據從此可信,且下一步的優化方向更明確。', en: 'Cleaned up internal eval data contamination from the past week. After adding the "topic-prefilter" retrieval tool last week, chunks returned via that path were not being counted by the internal scoring mechanism (an unsynced whitelist), making the "cross-episode" question scores look flat at zero for several days — we thought our improvements were not working, when actually the scoring itself was lying. After fixing the whitelist (commit 287e73b), we re-ran the full 34-item clean baseline and found the real internal chunk_recall for cross-episode questions is 0.283, not the 0.244 we had been comparing against; three recently archived changes need their "vs baseline" verdicts re-read. While we were at it, we put all four cross-episode questions (b20/b21/b22/b23) under a microscope — listing their retrieved chunks vs ground-truth chunk positions — and discovered the real reason b22/b23 regressed is NOT a rerank ordering problem (rerank cannot reach the root cause). b22 is actually an "LLM answer-strategy" question type and b23 is a "topic-prefilter picks the wrong candidate episodes" problem. This finding lets us rewrite the originally planned voyage-rerank-tune-b22-b23 change into two more targeted sub-changes (topic-finder hardening + meta-question answer strategy), saving a round of misdirected tuning. No direct user-visible behavior change, but internal eval data is now trustworthy, and the next direction of work is much clearer.', }, summaryBullets: { zh: [ '修內部評分白名單漏更新(commit 287e73b),洗掉過去一週「跨集問題」評分被低估到 0 的污染數據', '真實內部基準 cross_episode chunk_recall = 0.283(過去以為的 0.244 已 deprecated)', '對 b20/b21/b22/b23 做完整放大鏡 audit,揭露「b22/b23 退步」真因不在 rerank', '原本計畫的 voyage-rerank-tune-b22-b23 change 可重寫為兩條精準子 change,省一輪錯誤方向的調整', ], en: [ 'Fixed internal-scoring whitelist gap (commit 287e73b), cleaning up a week of "cross-episode" scores misreported as 0', 'Real internal cross_episode chunk_recall = 0.283 (the 0.244 we had been comparing to is now deprecated)', 'Full microscope audit on b20-b23 revealed b22/b23 regressions are NOT rerank issues', 'Originally planned voyage-rerank-tune change can be rewritten as two targeted sub-changes, saving a round of misdirected work', ], }, }, { date: '2026-05-26', slug: 'retrieval-cross-episode-episode-prefilter', milestone: 'v1.8', tag: 'enhancement', title: { zh: '跨集問題不再被別集話題沖淡', en: 'Cross-Episode Questions No Longer Diluted by Off-Topic Episodes', }, summary: { zh: '改善「跨集主題型」問題的檢索路徑。過去你問「節目裡迪拉怎麼描述中老年人的開工想法跟年輕時的差異?」這種需要跨集合成觀點的問題,agent 會把全節目所有集數的相關段落混在一起排序,結果常被別集主題接近但實際不相關的段落擠進前 5 名(譬如「家常味」主題的話題會混進「開工」主題的回答池),合成的答案就會被稀釋。這次新增了一個檢索工具,agent 會先把問題裡的主題(譬如「開工」)拿去找出真的有講這個主題的候選集(譬如 EP134 開工歌單那集),只在那幾集內做檢索 — 找不到候選才退回全節目搜尋。Prod 驗證:cross_episode 4 題中 2 題 agent 自發採用新工具(採用率 50%),且 b20 那題的回答從「在多集合成」變成「鎖定 EP134 馬力全開的開工歌單」單集為基礎正確作答,不再被別集污染。但內部 chunk_recall 指標還沒改善(0.244 持平)— 因為「縮對候選池」之後,主集內排序前 5 仍然漏掉黃金段落,瓶頸從「跨集污染」轉到「單集內排序」,已立後續 follow-up 處理。', en: 'Improves retrieval for cross-episode topical questions. Previously, asking something like "how does 迪拉 describe middle-aged vs. young people\'s feelings about starting work after the new year?" — which needs synthesizing a view across episodes — would dump all show-wide chunks into one ranked pool, often letting topic-adjacent but actually unrelated chunks (e.g. "家常味" chunks bleeding into "開工" questions) crowd out the top 5, diluting the synthesized answer. We added a new retrieval tool that first finds the candidate episodes that actually discuss the topic the question is about (e.g. EP134\'s "馬力全開的開工歌單" for "開工"-topic questions), then searches only within those candidates — falling back to a full-show search if no candidates match. Prod verified: 2 of 4 cross-episode questions had the agent self-adopt the new tool (50% adoption rate), and the b20 question\'s answer shifted from "synthesizing across episodes" to "grounded in EP134 alone" with no off-episode pollution. The internal chunk_recall metric did not move (0.244 flat) — once the candidate pool is correctly narrowed, the top-5 ranking inside the main episode still misses the gold-standard chunks. The bottleneck shifted from "cross-episode pollution" to "in-episode ranking", which is filed as a separate follow-up.', }, summaryBullets: { zh: [ '新增「主題預過濾」檢索工具:先找對的集再檢索,不再讓別集話題擠進回答池', 'Agent 自發採用率 cross_episode 50%(4 題中 2 題),不靠改 SYSTEM_PROMPT 而靠工具描述讓 LLM 自己選對工具', 'b20 案例驗證:回答從「跨集合成」變成「鎖定 EP134 單集為基礎」,無別集污染', '內部指標 chunk_recall 沒改善 — 縮對池後,主集內前 5 排序仍漏黃金段落,已立 follow-up', ], en: [ 'New "topic-prefilter" retrieval tool: narrows to the right episodes first, no more off-episode chunks crowding the answer pool', 'Cross-episode adoption rate 50% (2/4) — achieved via tool description, not SYSTEM_PROMPT changes (avoids prompt saturation)', 'b20 validated: answer shifted from "synthesized across episodes" to "grounded in EP134 alone", no pollution', 'Internal chunk_recall metric flat — bottleneck moved to in-episode ranking; filed as separate follow-up', ], }, }, { date: '2026-05-26', slug: 'multi-turn-epref-resolution-fix', milestone: 'v1.8', tag: 'fix', title: { zh: '對話模式追問特定集數時不再亂掉', en: 'Chat Mode No Longer Loses Track of Episodes Across Follow-Up Questions', }, summary: { zh: '修對話模式多輪追問的兩個討厭問題。第一個:你先問「EP140 第二彈是哪兩位來賓?」,agent 答對楊大正跟張凱婷;緊接著追問「他們推薦哪間餐廳?」,agent 卻會回「集數編號 140 的格式是否正確?請提供 UUID」直接放棄 — 因為它在第一輪識別了 EP140 但沒記住,第二輪不知道你在問哪集。第二個:類似情境下 agent 有時不放棄,反而漂到別集 — 你問 EP19 的動漫歌單,追問「大家推薦了哪些歌?」結果跳到 EP112 來答,再追問「這些歌出自哪部作品?」更扯,agent 編造「EP112 和 EP46 是否是完整的節目編號?」整個迷路。修法是讓 agent 在識別到某一集後自動把焦點鎖定到那一集,後續追問如果沒明確說別的集就會自動繼續在那一集裡查 — 純機械邏輯,不靠改 SYSTEM_PROMPT 讓 LLM「學」這個習慣。Prod 驗證:mt02/mt03/mt04 三題的「放棄」「漂到別集」「編造集號」全部消失;同時 mt01 的「第三集是什麼」這類序數指代問題仍 fail(屬另一個 mechanical fix follow-up,刻意不在這次 scope 內以保持隔離)。', en: 'Fixes two annoying chat-mode multi-turn behaviors. First: ask "who were the two guests on the EP140 second course episode?", agent correctly says 楊大正 and 張凱婷; immediately follow up with "which restaurants did they recommend?", and agent returns "is episode number 140 in the correct format? please provide UUID" — it identified EP140 in turn 1 but didn\'t remember, so turn 2 has no idea which episode you mean. Second: in similar scenarios agent sometimes doesn\'t give up — it drifts to a completely different episode. Ask about EP19\'s anime playlist, follow up with "what songs did they recommend?" and it switches to EP112 instead; follow up once more with "what works are those songs from?" and agent makes up "EP112 and EP46 — are those complete episode numbers?", fully lost. Fix: when agent resolves an episode reference, the tool layer auto-pins focus to that episode; subsequent turns that don\'t mention a different episode will transparently scope queries to the pinned one. Pure mechanical wiring — no SYSTEM_PROMPT changes asking the LLM to "learn" this habit. Prod verified: mt02/mt03/mt04 all stopped giving up / drifting / fabricating episode numbers. mt01\'s "what\'s the third episode?" ordinal-reference bug remains failing (deliberately out of scope for this change to keep cause-and-effect isolated — separate follow-up).', }, summaryBullets: { zh: [ '多輪對話中追問「他們推薦哪間餐廳」這種延續題不再被 agent 反問「請提供 UUID」放棄', '不會再從 EP19 追問突然漂到 EP112、EP46 這種完全不相關的集數', '靠 tool 層機械 auto-pin,不動 SYSTEM_PROMPT(避免 prompt 飽和風險)', '序數指代問題(「第三集是什麼」)仍 fail — 屬另一個 follow-up,本次刻意隔離', ], en: [ 'Follow-up questions like "what restaurants did they recommend" no longer get refused with "please provide UUID"', 'Asking about EP19 then following up no longer drifts to unrelated episodes like EP112 or EP46', 'Achieved via mechanical auto-pin at the tool layer — no SYSTEM_PROMPT changes (avoids prompt saturation risk)', 'Ordinal reference bug ("what\'s the third episode") still fails — separate follow-up, deliberately out of scope', ], }, }, { date: '2026-05-26', slug: 'eval-judge-incorporate-tool-grounding', milestone: 'v1.8', tag: 'enhancement', title: { zh: '對話模式的品質量得更準了', en: 'Chat Mode Quality, Now Measured More Sharply', }, summary: { zh: '把「對話模式答得好不好」的內部量尺整個重做。過去用「答案裡有沒有出現某幾個關鍵詞」算分,遇到語意對但用詞不同(譬如預期「公務人員」、agent 答「電信局」,其實是同個意思)會誤判扣分;也擋不住「兜兩邊話術」這種典型錯誤(譬如問「為什麼不挑振奮歌」、agent 答「他也推薦振奮人心的歌」直接違反題目前提);更看不出 agent 在多輪對話「第三集」這類序數指代時,到底有沒有正確抓到應該指的那一集。新版量尺改用 LLM 看完整 agent 工具呼叫紀錄(不只看最後一句回答)來打分,且加了三個新指標:判斷答案內提到的集數數字跟系統實際撈到的集數對不對得上(抓 LLM 數字嘴砲)、抓答案是否違反問題前提(contradict_check)、抓多輪序數指代有沒有解析到正確集數(ordinal_resolution_check)。同步重做 dataset schema 支援「必中集合 / bonus 集合」兩層 + 「重疊段落擇一即算」三層 chunk ground truth,解過去 chunk 切分跨邊界時會被冤枉扣分的問題。對使用者直接體感沒有變化,但這層基礎建設讓往後修對話模式的功能有更精準的反饋,避免再被指標本身的盲點帶歪方向。', en: 'Rebuilt the internal yardstick for "how good is the chat mode\'s answer". The old grader scored by checking whether specific keywords appeared in the answer, which mis-judged semantically correct answers using different wording (e.g. expected "civil servant", agent said "telecom bureau employee" — same meaning); it also couldn\'t catch "argue both sides" failures (e.g. ask "why didn\'t he pick an upbeat song", agent answers "he also recommended upbeat songs" — directly contradicts the question premise); nor could it tell whether the agent correctly resolved "the third episode" in multi-turn ordinal references. The new grader uses an LLM that sees the full agent tool-call trace (not just the final answer text) and adds three new indicators: whether the count the agent mentions matches what the tool actually returned (catches LLM number hallucination), whether the answer violates the question premise (contradict_check), and whether multi-turn ordinal references resolve to the correct episode (ordinal_resolution_check). Also reworked the dataset schema to support two-tier "must-hit / bonus" episode sets and three-tier "either one counts" chunk ground truth, fixing cases where overlapping chunk boundaries unfairly penalized correct retrieval. No direct user-visible behavior change — this infrastructure makes future chat-mode work measurable instead of vibes-based, so we don\'t get misled by metric blind spots.', }, summaryBullets: { zh: [ '評分改用 LLM 看完整 agent 工具紀錄打分,解語意同義被誤判的問題', '抓「兜兩邊話術」「LLM 數字嘴砲」「多輪序數指代抓錯集數」三類過去看不出來的錯誤', 'Dataset 改三層 ground truth 結構,重疊段落不再冤枉扣分', '使用者體感沒變,純粹讓往後對話模式的優化決策有可信的反饋', ], en: [ 'Switched to LLM-judge grading over full agent tool trace, fixing semantic-synonym false negatives', 'Catches three previously invisible failure modes: argue-both-sides, LLM number hallucination, multi-turn ordinal mis-resolution', 'Three-tier ground-truth structure stops overlapping chunk boundaries from being unfairly penalized', 'No user-visible change — pure foundation work so future chat-mode improvements have trustworthy feedback', ], }, }, { date: '2026-05-24', slug: 'agentic-severe-residual-fix-2026-05', milestone: 'v1.8', tag: 'fix', title: { zh: '對話模式查特定集數不再亂回別集', en: 'Chat Mode No Longer Returns the Wrong Episode for "EP1" Queries', }, summary: { zh: '修兩個對話模式偷偷會出錯的場景。第一個:問「EP1 講什麼」這種具體集數參考,後端 SQL 之前用模糊比對抓「title 含 EP1 字串」的集數,結果 EP10、EP100、EP146 也會被撈到,再按發布日期排序取最新 → 你問 EP1 系統卻回 EP146。改成正規表達式精確比對「EP 1 後面不接其他數字」之後,問 EP1 找不到就老實說查不到,不再撈到別集。第二個:問「節目主持人陣容怎麼變化」這類問題,過去資料庫並沒有主持人變動紀錄,agent 卻會從集數列表硬推「初期主持人是 X、後期是 Y」這種編造內容。SYSTEM_PROMPT 補了「主持人變動類問題沒明確紀錄就必須拒答」的強制規則。順手在後台 admin debug_trace 模式加了個 enumeration_state 觀測欄位,未來 diagnose 多輪對話「第 N 集」這類問題時可以直接看到 state 內容。本輪也試了 post-generation 把不在 tool result 的 EP 號碼標 [未驗證] 的策略,但實測 LLM judge 看到滿屏 [未驗證] 反而把答案打成更差 → 已 revert,留 helper 供未來 silent 模式用。', en: 'Fixes two quiet bugs in chat mode. First: asking about a specific episode like "what does EP1 talk about?" used to return the wrong episode (e.g. EP146) because the backend SQL did fuzzy substring matching on titles — EP10/EP100/EP146 all contain "EP1" as a substring, and the most recent one would win. Now uses a word-boundary regex that matches "EP 1 not followed by another digit" exactly. Asking EP1 when it doesn\'t exist now honestly returns "not found" instead of substituting a different episode. Second: asking "how has the host roster changed over time?" used to make the agent infer a host timeline from the episode list and fabricate names. The SYSTEM_PROMPT now contains an explicit rule that this question must be refused unless there\'s an explicit host-change marker in a tool result. Also added an enumeration_state field to the admin debug_trace view for diagnosing "what was the 3rd episode?"-style multi-turn questions. A post-generation "[未驗證]" annotation experiment was reverted — the LLM judge penalized answers cluttered with that tag instead of rewarding the honesty. Helper kept in code for a future silent-mode rework.', }, summaryBullets: { zh: [ '問「EP1 有講什麼」不會再被回 EP146 之類完全不同的集數', '問「主持人陣容變化」不會再從集數列表硬推「初期主持人是某某」這種編造', 'Admin debug_trace 模式新增 enumeration_state 觀測,多輪 ordinal 對話 diagnose 變直接', '剩下 multi-turn「第 N 集」這類 LLM 不遵守序數規則的情況留下一輪用 mechanical resolution 處理', ], en: [ 'Asking "what about EP1?" no longer returns a completely different episode like EP146', 'Asking "how has the host roster changed?" no longer fabricates host names from the episode list', 'Admin debug_trace now exposes enumeration_state for easier multi-turn ordinal diagnosis', 'Remaining multi-turn "Nth episode" misfires (LLM ignoring ordinal instruction) deferred to a future mechanical-resolution change', ], }, }, { date: '2026-05-24', slug: 'agentic-grounding-prompt-tune-v2', milestone: 'v1.8', tag: 'fix', title: { zh: '節目沒講的內容不會再亂編', en: 'Chat Mode Stops Making Things Up', }, summary: { zh: '修對話模式幻覺問題。上次補的「不能編造」清單只是寫進 SYSTEM_PROMPT,這次根據實測量化結果(嚴重幻覺率 22.5% 反而比之前 baseline 20% 還差),先把 9 個嚴重案例 + 11 個輕微案例的 root cause 分類過一輪,發現 78% 是「tool 查到相關但不含答案的段落,agent 就從這些 noise 裡推論編造」——例如問「嘻哈饒舌歌唱比賽冠軍是誰?」搜到的其實是大嘻哈時代的評審 panel,agent 就把評審名字當冠軍寫出來。對症加了兩個 few-shot 範例(noise 拒答 + show overview 不存在節目拒答),加上把 grounding 規則段提前到緊跟 tool-eager 之後。結果:嚴重幻覺率從 22.5% 砍到 12.5%(-44%),整體回答品質從 0.5375 提升到 0.6625(+12.5pp)。試過再加第三個「negative trap」範例反而讓 prompt 稀釋導致 regress,所以 round 2 直接 revert,剩下的 12.5% 留到下一輪用 tool layer 的方式處理(譬如加 get_show_overview 專屬 tool)。', en: 'Fixes hallucination in chat mode. The previous "do not fabricate" list only made it into SYSTEM_PROMPT without measurement — turned out the eval showed severe hallucination rate 22.5% (worse than the 20% baseline). This round we first classified the 9 severe + 11 mild cases by root cause and found 78% were "tools returned topic-related but answer-empty chunks, and the agent inferred a fabrication from the noise" — e.g. asking "who won the hip-hop championship?" surfaced judge panel chunks from the show 大嘻哈時代, and the agent wrote the judges in as winners. Targeted fix: two few-shot examples (noise refusal + show-overview-on-missing-show refusal) plus moving the grounding rule section to immediately after the tool-eager instruction. Result: severe hallucination rate 22.5% → 12.5% (-44%), overall answer quality 0.5375 → 0.6625 (+12.5pp). A round 2 attempt to add a third "negative trap" few-shot diluted the prompt and regressed severe back to 17.5%, so it was reverted. The remaining 12.5% will be tackled in the next round via a tool-layer approach (e.g. a dedicated get_show_overview tool).', }, summaryBullets: { zh: [ '問「也好吃」這種不存在的節目,不會再憑名稱腦補節目介紹', '問「嘻哈饒舌冠軍」這種 podcast 沒提的問題,會老實答「節目未提及」不會把評審當冠軍', '整體回答品質從 0.5375 升到 0.6625(+12.5pp)— grounding 變嚴 ≠ 全拒答爛答', '剩下的硬骨頭(negative trap 陷阱題)prompt 已到飽和,留待下一輪改 tool', ], en: [ 'Asking about a non-existent show (e.g. 也好吃) no longer triggers a made-up show description', 'Asking about facts the podcast never mentioned (e.g. "who won the rap championship?") now honestly returns "not mentioned" instead of promoting a judge to winner', 'Overall answer quality jumped 0.5375 → 0.6625 (+12.5pp) — stricter grounding did not collapse into uniform refusals', 'The remaining hard cases (negative-trap questions) have hit prompt saturation; next round will tackle them at the tool layer', ], }, }, { date: '2026-05-24', slug: 'agentic-prompt-grounding-and-ordinal-tool', milestone: 'v1.8', tag: 'fix', title: { zh: '對話模式找「最新 / 最舊集數」變聰明', en: 'Chat Mode Recency Lookup Now Works', }, summary: { zh: '修對話模式抓不到「最新一集 / 最舊五集 / 2024 年最後一集歌單 / 上週最舊一集」這類 recency 問題的問題。之前問「最新一集的來賓是誰?」對話 agent 會直接放棄回「無法確認」,因為手上沒有任何能 sort + limit 的工具。這次補了一個 list_episodes tool,可以指定要幾集 + 最新 / 最舊 + 可選 topic / 年份 filter,agent 看到 recency 問題就會用它。同時把既有的「找某段日期的集數」工具也補了排序 + 數量上限參數,「上週最舊一集」這類問題現在會自己算 datetime range + limit=1 直接答出真實 EP 編號。順手把對話 agent 的 SYSTEM_PROMPT 補了「絕對不能編造」清單(節目名 / 來賓姓名 / EP 編號 / 集數標題 / 來賓引號內的話 / 統計數字)— 部分場景有效(問不存在的來賓直接說查不到、不亂編節目名),但 LLM judge eval 量化顯示 hallucination severe rate 沒有顯著下降,需要下一輪 prompt 重做才能根治。', en: 'Fixes "find latest / oldest episodes" queries in chat mode. Previously, asking "who was the guest on the latest episode?" would have the chat agent give up with "cannot confirm" because no existing tool could sort + limit. This change adds a list_episodes tool — specify how many episodes, newest or oldest, with optional topic / year filters. The existing date-range tool also gained sort + limit kwargs, so "the oldest episode from last week" now correctly computes a 7-day window with limit=1. Also added a "do not fabricate" list to the chat agent SYSTEM_PROMPT (show names, guest names, EP numbers, episode titles, quoted lines, counts) — partial wins in simple refusal cases (asking about a non-existent guest returns an honest "not found" instead of making one up), but LLM-as-judge evaluation shows hallucination severe rate did not materially drop; a second prompt iteration is needed.', }, summaryBullets: { zh: [ '問「最新三集是哪些」「最舊五集是哪些」「2024 年最後一集歌單」現在會直接答出真實 EP', '問「上週最舊一集」會自己算 7 天日期範圍 + 取最舊 1 集,不再放棄', '問不存在的來賓 / 不存在的引號內句子,會老實說「查不到」不再亂編', '對「節目整體風格」這類推論題,目前還沒嚴格加註「請以節目實際內容為準」— 留下一輪 prompt 重做', ], en: [ 'Asking "latest 3 episodes / oldest 5 episodes / last 2024 playlist episode" now returns real EPs', '"Oldest episode from last week" auto-computes a 7-day range + limit=1, no more give-up', 'Asking about non-existent guests or fabricated quotes now honestly returns "not found"', 'Inference questions ("what\'s the show\'s overall style?") do not yet append the "please verify against the actual content" disclaimer — left for the next prompt iteration', ], }, }, { date: '2026-05-23', slug: 'admin-quota-bypass-fix', milestone: 'v1.8', tag: 'fix', title: { zh: 'Admin 帳號跑 eval 不再被自己擋', en: 'Admin Accounts No Longer Block Themselves on Quota', }, summary: { zh: '修一個藏起來但卡住整條 eval pipeline 的問題:admin 帳號跟一般使用者一樣每天扣 30 次 chat quota,跑一輪 token-truncate eval gate(34 record × 平均 3 turn ≈ 102 個 chat 請求)開頭就把自己 quota 燒光,後續全部撞 HTTP 429 quota_exhausted,結果這輪 eval 的 answer_match 跌到 0.025(垃圾值)完全沒驗到 token-truncate 真實 fix 效果。修法簡單:backend `/query` endpoint 進來先看 `user.role`,admin 直接 bypass quota 扣減(total_queries 仍 +1 保留可觀測),response 回 `quota_remaining=-1` sentinel 表示無上限。一般使用者扣 quota / 收 429 行為完全不變。修完重跑同一輪 eval:35 個連續 admin chat request 全 HTTP 200、answer_match 從 0.025 → 0.595(A6 達標 ≥ 0.5),順便驗證了上一個 token-truncate fix 在 b20「中老年人開工想法」這題 0 個 turn 爆 context(之前固定 209751 tokens 噴 5xx)。', en: 'Fixes a hidden-but-pipeline-blocking issue: admin accounts shared the same daily 30-chat quota as regular users, so running a single token-truncate eval gate (34 records × ~3 turns ≈ 102 chat requests) burned through the admin quota in the first batch and the rest of the run hit HTTP 429 quota_exhausted, dragging answer_match down to 0.025 (garbage) and completely missing the real token-truncate fix signal. Fix is simple: the backend `/query` endpoint now checks `user.role` first — admins skip quota decrement entirely (total_queries still increments for observability) and the response carries `quota_remaining=-1` as the unlimited sentinel. Non-admin quota behavior and 429 handling are unchanged. After the fix, the eval re-run: 35 sequential admin chat requests all returned HTTP 200, answer_match jumped from 0.025 → 0.595 (meets A6 ≥ 0.5), and the b20 "middle-aged people on starting work" question (which previously blew 209751 tokens into a 5xx) returned 0 truncated turns — also confirming the earlier token-truncate fix is doing its job.', }, summaryBullets: { zh: [ 'Admin 帳號跑 chat 不再扣 quota、不會撞自己 429', 'total_queries 仍計數,admin 使用量還是看得到', '一般使用者扣 quota / 收 429 的行為完全不變', '解開 eval pipeline 卡住:token-truncate fix answer_match 0.025 → 0.595', ], en: [ 'Admin chat requests no longer decrement quota and never hit self-induced 429', 'total_queries still increments so admin usage stays observable', 'Non-admin quota decrement and 429 behavior are unchanged', 'Unblocks the eval pipeline: token-truncate fix answer_match 0.025 → 0.595', ], }, }, { date: '2026-05-23', slug: 'landing-redesign-hotfix-transcript-and-audio', milestone: 'v1.8', tag: 'fix', title: { zh: '逐字稿、音訊播放、節目簡介都修好了', en: 'Transcript Reading, Audio Playback, and Show Descriptions All Fixed', }, summary: { zh: '修了介面改版上線後抓到的 4 個視覺問題:(1) 點進逐字稿頁原本一集 80 分鐘的對話會擠成一大塊牆,看不到段落切點 — 現在自動切成 100 段左右,每段都帶時間戳,要找某一句話直接掃就好。(2) 「從此處播放」按鈕原本根本沒出現,整個站沒有任何方式聽音訊 — 現在按鈕固定在逐字稿頁右上角,點下去開始播 + 底部浮出迷你 player,切回查詢頁繼續播不中斷。(3) 搜尋結果點「跳到這段內容」原本只跳到頁首,現在會 scroll 到對應時間的段落並標亮。(4) 首頁節目卡片原本會看到 `
` `
` 這些原始 HTML 標籤外漏,現在乾淨的純文字 + 自動換行。順手也修了一個 pre-existing bug:逐字稿用 UUID 對應 segment 但用 parseInt 解析,導致段落內容對不上 — 一起整理掉。',
en: 'Fixes 4 visual regressions found after the landing redesign shipped: (1) Transcript pages used to render an 80-minute episode as one giant wall of text — now auto-split into ~100 paragraphs, each with a timestamp, so you can scan to find a quote. (2) The "Play here" button never rendered, leaving the site with no way to play audio — now it sits fixed on the transcript top bar, clicking starts playback + a mini player floats at the bottom and keeps playing when you navigate back to the query page. (3) "Jump to this segment" links from search results used to only scroll to the top — now they scroll to the matching timestamp paragraph and highlight it. (4) Show description cards on the home page used to leak raw `
` `
` HTML tags — now clean plain text with auto line-breaks. Also picked up a pre-existing bug where transcript paragraphs used parseInt to map UUID segment ids, mis-targeting content — patched along the way.',
},
summaryBullets: {
zh: [
'逐字稿不再整集擠一塊,自動切成多段帶時間戳',
'逐字稿頁右上角「從此處播放」按鈕回來了,底部浮出 mini player',
'音訊跨頁繼續播,不會被切回查詢頁中斷',
'搜尋結果「跳到這段內容」真的會 scroll 到對應段落',
'首頁節目簡介不再顯示原始 HTML 標籤',
],
en: [
'Transcripts no longer render as one wall of text — auto-split with timestamps',
'"Play here" button now appears on the transcript page; mini player floats at the bottom',
'Audio keeps playing across page navigation, no more interruption',
'"Jump to this segment" links actually scroll to the correct paragraph',
'Home-page show descriptions render as clean text, no more raw HTML tags',
],
},
},
{
date: '2026-05-23', slug: 'agent-token-budget-and-tool-truncate', milestone: 'v1.8', tag: 'fix',
title: {
zh: '對話模式不會再因為單題太複雜直接出錯',
en: 'Chat Mode No Longer Crashes on Heavy Cross-Episode Questions',
},
summary: {
zh: '修一個邊界但會炸的問題:問「節目裡迪拉怎麼描述中老年人的開工想法跟年輕時的差異」這類需要跨多集多輪 tool 查詢的問題時,AI 累積太多搜尋結果,整個對話超過 gpt-4o 128K context 上限,prod 直接 HTTP 500 噴錯給使用者。修法三層:(1) AI 每次 tool 查詢結果送回給模型前先截到 8K 字(前端 admin 觀測還是看得到全長)(2) 每輪 AI 呼叫前估算 messages 累積 token 數,超 100K 就從前面砍掉最舊的 tool 結果(保留 system prompt 跟最後一輪對話)(3) 真的不小心爆 context 時 catch BadRequestError 改回使用者友善訊息「這題涉及內容太多,我只能列出部分結果;試試把問題拆小,譬如指定單一集數」標 agent_truncated 收尾,不再 5xx。Prod smoke 5/5 全 HTTP 200,b20「中老年人開工想法」這題之前固定爆 209751 tokens 現在穩定回答。',
en: 'Fixes an edge-but-explosive issue: questions like "How does Dila describe middle-aged people\'s views on starting work versus when they were young?" require cross-episode multi-turn tool lookups, and the accumulated search results pushed the conversation past gpt-4o\'s 128K context limit, returning HTTP 500 to the user. Three-layer fix: (1) tool results are truncated to 8K chars before being sent back to the model (admin trace still sees the full result); (2) before each LLM round, estimate accumulated message tokens; if over 100K, pop the oldest tool results (keeping system prompt + last conversation turn); (3) if context truly overflows, catch BadRequestError and return a user-friendly message "This question covers too much content; I can only list partial results — try narrowing your question, e.g. specify a single episode" with agent_truncated=True, no more 5xx. Prod smoke 5/5 all HTTP 200; the b20 question that previously hit 209751 tokens consistently now answers stably.',
},
summaryBullets: {
zh: [
'對「跨多集 / 多輪查詢」類問題不再 5xx',
'AI 看到的 tool 結果有上限(8K 字),admin trace 仍保留全長',
'超過 context budget 自動裁掉最舊的 tool 結果,保留 system prompt + 最後對話',
'真的爆掉改回使用者友善訊息,不再露出內部錯誤',
],
en: [
'Multi-episode / multi-turn lookup questions no longer return 5xx',
'Tool results sent to the model are capped (8K chars); admin trace keeps full content',
'When over budget, oldest tool results are dropped; system prompt + last turn preserved',
'If context truly overflows, a friendly user-facing message replaces the raw error',
],
},
},
{
date: '2026-05-22', slug: 'landing-and-mode-orchestration-redesign', milestone: 'v1.8', tag: 'ui',
title: {
zh: '首頁 + 三模式編排大改版:一頁挑節目、看懂模式、直接出發',
en: 'Home Page + Mode Orchestration Redesign: One Page to Pick, Understand, Go',
},
summary: {
zh: '把「未登入看 marketing 頁、登入後才看到節目」拆兩條路徑的舊設計,收成單一首頁:不管有沒有登入,一打開就看到完整節目清單,hero 區依登入狀態切換(marketing → 「嗨,XXX」+ 剩餘額度)。新增三模式介紹卡(索引 / 語意 / 對話),讓你在點進去之前心裡有底「這個模式適合什麼問題」。查詢頁從原本兩 tab 加上索引 tab(先佔位,backend 之後接),三 tab 順序固定,預設 tab 看登入狀態決定(登入→對話、未登入→索引)。Lock card 文案重寫:未登入版(🔒)只強調「不想花時間重聽找答案?」+ Google 登入按鈕;額度用完版(⏳)改寫成「申請更多次數」直接接到既有 modal,不再寫「重置時間」誤導使用者。對話 source 從「chip 列 + 卡片列」雙區整合成單一「答案參考來源(共 N 集 · M 段引用)」episode-grouped panel。語意結果改 flat list + 同集多段折成「+N 同集」chip + 相關度 bar。音訊播放器掛 router 外層跨頁不打斷(速度 1.0 / 1.25 / 1.5x,localStorage 記住)。逐字稿依停頓 ≥ 1.5 秒或講者變化自動聚段。',
en: 'Merged the「marketing page if anon, show grid only after login」two-path design into a single Home Page: regardless of auth state, the full show list shows immediately; hero swaps (marketing → "Hi, XXX" + remaining quota). Three-mode educational card row added (Index / Semantic / Chat) so you know which mode fits which question before clicking. Query page got a third Index tab (placeholder; backend lands later); three tabs fixed order, default decided by auth state (signed-in→Chat, anon→Index). Lock card copy rewritten: anonymous (🔒) emphasizes "tired of re-listening?" + Google login; quota-exhausted (⏳) says "request more usage" and opens the existing modal, no longer claiming a "reset date" we never built. Chat source area consolidated into one episode-grouped panel "Answer sources (N episodes · M citations)". Semantic results render as flat RRF list with "+N in same episode" expand chips + relevance bar (no raw score). Audio player sits outside the router so navigation does not interrupt playback (speed 1.0 / 1.25 / 1.5x, persisted). Transcript aggregates segments into paragraphs by silence gap ≥ 1.5s or speaker change.',
},
summaryBullets: {
zh: [
'不論登入與否,首頁一打開就看到完整節目清單',
'查詢頁新增第三個 tab「索引」(先佔位,backend 之後接)',
'Lock card 文案重寫:不再寫「免費 N 次 / 重置時間」這類我們沒做的承諾',
'對話 source 整合成單一 episode-grouped panel,同集多段不重複標題',
'音訊播放器跨頁不打斷,速度 1.0/1.25/1.5x localStorage 記住',
'逐字稿依停頓自動分段,比原本一句一句好讀',
],
en: [
'Home page shows the full show list immediately, signed-in or not',
'Query page adds a third "Index" tab (placeholder; backend lands later)',
'Lock card copy rewritten — no more "free N uses / reset date" claims we never delivered',
'Chat source area consolidated into one episode-grouped panel',
'Audio player survives navigation; speed 1.0/1.25/1.5x persisted',
'Transcript view aggregates segments by silence gap — easier to read',
],
},
},
{
date: '2026-05-22', slug: 'retrieval-episode-reference-handling', milestone: 'v1.8', tag: 'enhancement',
title: {
zh: '問「EP134 講什麼?」現在真的回 EP134 內容了',
en: '"What does EP134 talk about?" Now Actually Returns EP134 Content',
},
summary: {
zh: '這版我們在跑回歸測試時抓到一個系統性問題:語意搜尋對「EP 134 講什麼?」這類「特定集數內容」的問題撈出來的段落經常不是 EP134 — 因為「EP134」這個字串在 episode 內文裡幾乎不會出現(內容是音樂、家常味、開工歌單⋯不是「EP134」三個字),embedding 跟 BM25 都沒辦法把問題對到正確段落。修法:在 `/search` endpoint 加一層 EP-reference 偵測,看到「EP134」這種 token 就把它轉成 episode UUID 再餵到 retrieval filter。chat agent 路徑早就有同等能力(agent 會主動呼 find_episode_by_ref tool),這次只是把 public 語意搜尋補齊。實測 Recall@5 從 0.23 拉到 0.42(+19pp),「EP134 開工歌單觀念」這類題目從 0 命中變 1.0 命中。',
en: 'This release catches a systemic issue uncovered during regression testing: the semantic search endpoint frequently retrieves wrong-episode segments when the user asks "what does EP134 talk about?" — because the string "EP134" almost never appears verbatim in transcripts (content is about music, home cooking, work playlists... not "EP134" three characters), so neither embedding nor BM25 can map the question to the right chunks. Fix: added an EP-reference detection layer in `/search` endpoint that converts tokens like "EP134" into episode UUIDs and feeds them to the retrieval filter. The chat agent path already had equivalent capability (the agent proactively calls find_episode_by_ref), this release just brings the public semantic search up to parity. Measured Recall@5 jumped from 0.23 to 0.42 (+19pp); questions like "EP134\'s work playlist concept" went from 0 hit to 1.0 hit.',
},
summaryBullets: {
zh: [
'問「EP X」的特定集數題型,搜尋結果現在精準命中該集',
'Recall@5 baseline 0.23 → 0.42(+19pp)',
'只動 public search endpoint,chat 對話模式不變',
],
en: [
'Episode-specific questions ("EP X") now hit the right episode',
'Recall@5 baseline 0.23 → 0.42 (+19pp)',
'Only touches public search endpoint; chat mode unchanged',
],
},
},
{
date: '2026-05-22', slug: 'enable-agentic-chat-default-on', milestone: 'v1.8', tag: 'feature',
title: {
zh: 'Phase 2 翻牌:對話模式預設改用 AI 自主決策',
en: 'Phase 2 Flip: Chat Mode Default Switched to AI-Driven Agent',
},
summary: {
zh: '把 ENABLE_AGENTIC_CHAT 從「測試中」翻成預設開啟,所有人切到對話模式就直接走新的 AI agent loop(過去兩週 dogfood 同 30 題 0/30 看到「技術問題」訊號)。同場補一個容易漏的細節:agent 拿到節目跟段落資料後,原本沒把這些回填給前端 source 區,導致對話模式 source 卡片整個空白;現在從 agent 內部的 tool 結果撿出 chunk 跟 episode,回填給既有的「引用 chip」+「相關集數卡」兩塊區塊,使用者一打開就有完整資料呈現(UIX 細節後續另一個 change 接手優化)。Flag 留 30 天 kill-switch,若 prod 出問題可立即顯式翻回。同場補 LLM-as-judge 對 multi-turn-40 dataset 跑一輪當 quality gate 證據,並開兩個新 runbook(eval-pipeline / agentic-chat-observation)給後續 14 天觀察期值班參考。',
en: 'Flipped ENABLE_AGENTIC_CHAT from "in testing" to default-on; everyone landing in chat mode now goes straight to the new AI agent loop (the last two weeks of dogfood ran the same 30-question set with 0/30 "technical issue" signal). Bundled fix for an easy-to-miss gap: after the agent finished its tool calls, the episode and chunk data was never mapped back to the frontend source panel, leaving the chat-mode source area completely blank. Now the response mapper collects chunks from search tools and episodes from listing/lookup tools and feeds the existing "citation chips" + "related episodes card" containers, so users see complete source info from day one (UIX polish is owned by a follow-up change). The flag stays as a 30-day kill-switch; explicit ENABLE_AGENTIC_CHAT=false still falls back to the rule-based pipeline. Also captured: LLM-as-judge against the multi-turn-40 dataset as the quality gate evidence, plus two new runbooks (eval-pipeline / agentic-chat-observation) for the 14-day observation-window on-call.',
},
summaryBullets: {
zh: [
'對話模式 default 翻 on:使用者打開就是新的 AI agent 行為',
'補齊回應的 source 資料:紫色 chip + 集數卡兩塊區塊不再空白',
'保留 30 天 flag kill-switch;prod 出問題顯式翻回 false 立即生效',
'LLM-as-judge 對 40 題 multi-turn 跑一輪當 gate 證據(answer_match +2.7pp)',
'同場開兩個 runbook:eval pipeline 全圖 + 14 天觀察期值班 SOP',
],
en: [
'Chat-mode default flipped on — landing in chat mode now goes straight to the AI agent',
'Source data mapped back: purple chips + episode cards no longer blank in chat mode',
'30-day flag kill-switch retained; explicit ENABLE_AGENTIC_CHAT=false still works as a fallback',
'LLM-as-judge against 40-turn multi-turn dataset as gate evidence (answer_match +2.7pp)',
'Two new runbooks: eval pipeline overview + 14-day observation-window SOP',
],
},
},
{
date: '2026-05-22', slug: 'chat-tool-error-isolation', milestone: 'v1.8', tag: 'fix',
title: {
zh: '對話模式錯誤訊息變友善了',
en: 'Chat Mode: Error Messages Got Friendlier',
},
summary: {
zh: '上週開始 dogfood 對話模式時,偶爾會撞到「系統查詢時遇到了技術問題」這種 user 體感超差的回覆——我們追了完整 trace 之後發現是內部資料庫某個欄位名拼錯了,AI 工具呼叫失敗後又把原始錯誤訊息(含內部 exception 名稱)直接翻給 user。這版三層一起修:修了拼錯的 SQL、AI 每呼一個工具用 SAVEPOINT 隔離(一個工具炸不會連坐後續工具)、加上結構化錯誤格式 `{ok, kind, internal_message, user_hint}` 讓 AI 看 `kind` 判斷下一步、user 視角只看 `user_hint`,並且在 system prompt 明文禁止 AI 輸出內部錯誤類別名或「技術問題」這種字眼。修完同樣 30 題 dogfood,「技術問題」訊號從 1/30 降到 0/30;Phase 2 翻 default 前最後一個 blocker 清掉了。',
en: 'When we started dogfooding chat mode last week, the bot occasionally replied "the system encountered a technical issue during the query" — terrible UX. Full trace investigation traced it to: an internal column-name typo in one SQL, AI tool failure then leaked the raw exception class name to the user. This release fixes all three layers: corrected the SQL typo, wrapped every tool call in a SAVEPOINT so one tool failing does not cascade into the next, and introduced a structured error envelope `{ok, kind, internal_message, user_hint}` where AI uses `kind` for routing decisions and users only see `user_hint`. The system prompt now explicitly forbids leaking internal exception class names or "technical issue"-style phrasings. After the fix, the same 30-question dogfood run showed "technical issue" signals drop from 1/30 to 0/30; the last blocker for flipping the Phase 2 default is cleared.',
},
summaryBullets: {
zh: [
'使用者不會再看到「系統查詢時遇到了技術問題」這種裸露的內部錯誤訊息',
'AI 每呼一個工具用 SAVEPOINT 隔離,一個工具撞 bug 不會把後續工具都拖下水',
'錯誤訊息改成結構化格式:AI 看 kind 決定怎麼處理、user 看 user_hint',
'dogfood 30 題 failure signal 從 1/30 降到 0/30',
],
en: [
'Users no longer see raw "the system encountered a technical issue" messages',
'Every tool call is now SAVEPOINT-isolated — one tool failing does not cascade into the next',
'Error format restructured: AI reads `kind` to decide next steps; user only sees `user_hint`',
'30-question dogfood failure-signal rate dropped from 1/30 to 0/30',
],
},
},
{
date: '2026-05-21', slug: 'agent-trace-telemetry', milestone: 'v1.8', tag: 'enhancement',
title: {
zh: '對話模式裝上「黑盒紀錄器」協助除錯',
en: 'Chat Mode Gains a Black-Box Recorder for Debugging',
},
summary: {
zh: '對話模式翻 default 前的觀測投資——把 AI 每一輪 LLM 呼叫的 latency 與 token、每個工具呼叫的參數與結果、各階段時間花在哪裡,全部結構化記錄成 trace 留下來;admin 帶 `?debug_trace=true` 才看得到完整內容,一般使用者回應形狀不變。同場補一支 `dogfood_trace_dump.py` 把 30 題對話批次跑完落盤,這份 trace 立刻派上用場:抓到 q03「45 歲開工歌觀點」失敗的真正原因是一個內部 SQL column 名拼錯——直接催生隔天的 chat-tool-error-isolation 修復。',
en: 'Observability investment before flipping the chat-mode default. Every per-round LLM call (latency, tokens), every tool dispatch (name, args, result), and every stage timing now ships as a structured trace. Admins see the full payload by appending `?debug_trace=true`; regular users see no change in response shape. Bundled with a `dogfood_trace_dump.py` script that batches 30 questions into a saved trace dump — which immediately paid off: it traced q03 ("45-year-old work-song views") down to a single misspelled SQL column name, directly triggering the next-day chat-tool-error-isolation fix.',
},
summaryBullets: {
zh: [
'每輪 LLM 呼叫、每個工具呼叫、每個階段時間都有結構化 trace',
'admin 帶 `?debug_trace=true` 看完整 trace;一般使用者不受影響',
'`dogfood_trace_dump.py` 批次跑完落盤,幫忙抓到一個關鍵 SQL bug',
],
en: [
'Per-round LLM calls, per-tool dispatches, and per-stage timings are all structured into trace',
'Admins see the full trace via `?debug_trace=true`; regular users see no change',
'`dogfood_trace_dump.py` batches a dump; helped trace a critical SQL bug to its root',
],
},
},
{
date: '2026-05-21', slug: 'chat-agentic-tool-routing', milestone: 'v1.8', tag: 'feature',
title: {
zh: '對話模式升級:AI 自己決定要查什麼、查幾次',
en: 'Chat Mode Upgraded: AI Decides What to Look Up and How Many Times',
},
summary: {
zh: '之前的對話模式走「先嵌入向量、做混合檢索、再讓 LLM 寫答案」的固定流程,不管問題長什麼樣都是同一條 pipeline;遇到「歌單有哪幾集?」「第三集講什麼?」這種要分兩步看的問題就常常卡住。這版改成 agent loop:AI 拿到問題後自己決定要呼哪個 tool(找節目、找來賓、找日期、查摘要、查段落、show 概覽、pin/unpin 集數…11 個 callable),可以連續呼叫多次直到湊到夠的資訊再回答。順手修了「先列舉再用序數」(你問「歌單有哪幾集?」之後追問「第三集講什麼?」)的多輪 carry bug — 之前三套主流 agent 框架測試都跟丟,這版用 system prompt 教 LLM 對應到上一輪列舉結果的第三筆 ep_id。目前 feature flag 預設關閉,先自己 dogfood 兩週、跑 eval 比對通過後才會翻 default。',
en: 'The previous chat mode ran a fixed pipeline (embed → hybrid retrieval → LLM writes answer) regardless of the question — so multi-step questions like "Which episodes have playlists?" → "What\'s episode 3 about?" often broke. This release switches to an agent loop: the AI receives the question and decides which tool to call (find by topic / guest / date, get episode summary, get segments, show overview, pin/unpin episode… 11 callables total), can chain multiple calls until it has enough info, then answers. Also fixed the "enumerate then ordinal reference" multi-turn carry bug — all three major agent frameworks failed this in our bake-off, but a system-prompt instruction mapping "the third episode" to the previous enumeration\'s [N-1] ep_id resolves it cleanly. Feature flag is OFF by default for now — self-dogfood two weeks, run the eval gate, then flip default.',
},
summaryBullets: {
zh: [
'對話模式從固定 pipeline 改成 agent 自己呼 tool,可連續呼叫直到資訊足夠',
'11 個 tool 涵蓋:找節目(topic / guest / date / ref)、查摘要 / 段落、show 概覽、pin/unpin 集數',
'修了「歌單有哪幾集?→ 第三集講什麼?」多輪追問 bug(bake-off 三套框架都跟丟)',
'目前 feature flag 預設關閉,先 dogfood 兩週、跑 eval gate 通過才翻 default',
],
en: [
'Chat mode switched from fixed pipeline to agent-driven tool calls; can chain multiple calls until satisfied',
'11 tools cover: finding episodes (topic / guest / date / ref), getting summary / segments, show overview, pin/unpin episodes',
'Fixed multi-turn "enumerate then ordinal" bug ("Which episodes have playlists?" → "What\'s episode 3 about?") that broke all three frameworks in our bake-off',
'Feature flag is OFF by default — two weeks of dogfood + eval gate must pass before flipping the default',
],
},
},
// ─── v1.7 — Retrieval Quality Fix (5/13–5/19) ───
{
date: '2026-05-19', slug: 'celery-publish-routing-fix-and-f2-smoke', milestone: 'v1.7', tag: 'fix',
title: {
zh: '外部服務告警系統補上兩個 silent drop 漏洞',
en: 'Fixed Two Silent-Drop Bugs Holding Back the Outage Alert System',
},
summary: {
zh: '前一版 ship 的「外部服務出問題自動暫停 + 寄信通知」(circuit breaker) 跑 prod smoke 時抓到兩個 silent drop bug:(1) 後台「重新生成摘要」按鈕回 200 enqueued=true,但 Celery worker 其實沒收到任務 — 原因是 FastAPI 的 async 路由直接呼叫 sync 的 Celery publisher,跟 asyncpg event loop 衝突;(2) worker 真的接到失敗任務後,本來要寫進 task_failure_log 表的 async coroutine 因為 worker 殘留的 closed event loop 沒被執行 → 失敗計數永遠是 0 → circuit 永遠不會自動 open → 告警信永遠不會寄。兩個 bug 都是把 async/sync 接合改用獨立的 thread + 新 event loop 隔離解掉。順手把 F1 殘留的 cron_tick 任務跑去 default queue 的 leak(beat schedule 沒指定 queue)一起補。實測:拿一把假 aihub key 灌 5 個摘要任務 → 90 秒內後台「服務狀態」aihub 變紅、出現「手動恢復」按鈕、點完按鈕後馬上變綠 + 出現「已手動恢復」提示。',
en: 'After the previous "external service auto-pause + email alert" (circuit breaker) shipped, prod smoke caught two silent-drop bugs: (1) the admin "regenerate summary" button returned 200 enqueued=true, but the Celery worker never actually received the task — caused by FastAPI\'s async route directly calling the sync Celery publisher, conflicting with the asyncpg event loop; (2) once a worker did receive a failing task, the async coroutine that should write to task_failure_log silently dropped because of the worker\'s residual closed event loop after task crash → failure count stayed at 0 → circuit never opened → alert email never sent. Both fixed by isolating the async/sync boundary into a dedicated thread with a fresh event loop. Also patched the F1 leftover where cron_tick leaked to the default queue (beat schedule entries missing options.queue). Verified end-to-end: feeding 5 summary tasks to a fake aihub key → within 90 seconds the admin Service Status page shows aihub as red with a "Manual Resume" button → clicking it flips back to green with a "Manually Resumed" toast.',
},
summaryBullets: {
zh: [
'後台「重新生成摘要」publish 從 silent drop 變成 fail-loud 503',
'失敗記錄與 circuit breaker 真的會在外部服務掛掉時自動 open',
'後台「服務狀態」分頁可看到紅綠狀態跟「手動恢復」按鈕',
'F1 cron_tick leak 一起補(beat schedule 顯式指定 control queue)',
],
en: [
'Admin "regenerate summary" publish flipped from silent drop to fail-loud 503',
'Failure logging + circuit breaker now actually open when an external service goes down',
'Service Status admin tab shows green/red badges + "Manual Resume" button',
'Bundled fix for F1 cron_tick leak (beat schedule now explicitly routes to control queue)',
],
},
},
{
date: '2026-05-19', slug: 'task-failure-monitoring-and-circuit-breaker', milestone: 'v1.7', tag: 'feature',
title: {
zh: '外部 LLM / 寄信服務出問題會自動暫停 + 寄信通知',
en: 'External LLM / Email Service Outage Auto-Pause + Email Notification',
},
summary: {
zh: '建立後端任務失敗監控 + circuit breaker:當 aihub / openai / zsend 任一外部服務在 5 分鐘內失敗超過閾值,自動把該服務的 circuit「open」、後續任務自動暫停 5 分鐘再 retry(不會 worker 一直空轉燒 quota),同時寄一封 ZSend 告警信通知 admin。每 30 分鐘自動探測一次外部服務、通則就自動恢復;也可在後台「服務狀態」分頁手動點「恢復」按鈕(會收到 recovery 信)。後台 admin 新增「服務狀態」分頁列出三個外部 provider 的當前狀態(綠/紅 badge + 暫停起時 + 影響任務數 + 最後探測時間 + 手動恢復按鈕)。失敗事件全部寫進新表 `task_failure_log` 留歷史紀錄。這版同時把 5/10 凌晨同日抓到的「EP2 transcribe 連續失敗無人察覺」「topic backfill 批次全失敗沒 alert」兩個 silent failure 漏洞補上。',
en: 'Built backend task failure monitoring + circuit breaker: when any external service (aihub / openai / zsend) fails more than the threshold within a 5-minute window, the breaker automatically "opens" for that service, subsequent tasks self-pause for 5 minutes and retry (no busy-spinning that burns quota), and a ZSend alert email goes out to admin. The system auto-probes every 30 minutes and recovers when the service responds; admin can also click "Manual Resume" in the Service Status tab (which sends a recovery email). Adds a new "Service Status" admin tab showing the three external providers\' current state (green/red badge + paused-since + affected task count + last probe + manual resume button). All failure events are recorded in a new `task_failure_log` table for historical reference. This release also closes the two silent-failure gaps caught on 5/10 morning ("EP2 transcribe failed repeatedly with nobody noticing" and "topic backfill batch all failed with no alert").',
},
summaryBullets: {
zh: [
'外部服務失敗超閾值自動暫停 + 寄信告警,不再 silent 燒 quota',
'後台「服務狀態」分頁可看綠/紅 badge + 暫停起時 + 一鍵手動恢復',
'每 30 分鐘自動探測,外部服務恢復後自動 close',
'全部失敗事件落 `task_failure_log` 表留歷史',
],
en: [
'External service failures over threshold auto-pause + alert email — no more silent quota burn',
'Service Status admin tab shows green/red badge + paused-since + one-click manual resume',
'Auto-probes every 30 minutes, breaker auto-closes when service recovers',
'All failure events recorded in `task_failure_log` for history',
],
},
},
{
date: '2026-05-18', slug: 'celery-routing-and-dispatcher-fix', milestone: 'v1.7', tag: 'fix',
title: {
zh: '新節目轉錄永遠優先處理,不再被 batch 任務堵',
en: 'New Episode Transcription Always First, No Longer Blocked by Batch Backfills',
},
summary: {
zh: '5/10 發現 EP20(Axios 那集)排在轉錄佇列裡 9 個多小時沒被處理 — 因為當時前面排著 100 個 topic 補跑任務,後端只有一個 worker 全部一個一個吃完才輪到。這版把後端任務分四條 queue(轉錄 / topic / 摘要 / 控制)並設優先級:轉錄任務 priority=9(最高)、topic / 摘要 priority=2(最低)、其他控制類 priority=5。同一個 worker 同時訂閱四條 queue,broker 按優先級 pop — 新節目進來不管前面排多少 topic 都會被先抽出來。另外 dispatcher 加 `dispatched_at` 欄位 + `FOR UPDATE SKIP LOCKED`,徹底解掉「同一筆連發兩個 Celery 任務」的競態 bug。Prod 已驗 dispatcher 新 schema 跑通、worker 訂閱四條 queue 且優先級設定正確。',
en: 'On 5/10 we found EP20 (the Axios episode) sat in the transcription queue for 9+ hours unprocessed — 100 topic backfill tasks were queued ahead and the single worker chewed through them one by one. This release splits backend tasks into four queues (transcribe / topic / summary / control) with priorities: transcribe at priority=9 (highest), topic/summary at priority=2 (lowest), control defaults at priority=5. The same worker subscribes to all four queues; the broker pops by priority, so new episodes get pulled ahead regardless of how many topic tasks are queued. The dispatcher also gained a `dispatched_at` column + `FOR UPDATE SKIP LOCKED` to eliminate the race where the same row could be dispatched twice. Prod verified: dispatcher running the new schema, worker subscribed to four queues with correct priority binding.',
},
summaryBullets: {
zh: [
'轉錄優先級拉到最高(priority=9),topic / 摘要降到最低(priority=2)',
'新節目轉錄永遠優先 pick,不會排在 topic 補跑任務後面',
'Dispatcher 加 `dispatched_at` + `FOR UPDATE SKIP LOCKED` 解雙派 race',
'Worker 啟動時自動 reset 卡 running 又沒實際在跑的 row 回 pending',
],
en: [
'Transcription priority raised to top (priority=9), topic / summary dropped to lowest (priority=2)',
'New-episode transcription always picked first, never queued behind topic backfills',
'Dispatcher gained `dispatched_at` + `FOR UPDATE SKIP LOCKED` to fix double-dispatch race',
'Worker startup auto-resets rows stuck at running-without-actual-execution back to pending',
],
},
},
{
date: '2026-05-18', slug: 'citation-display-unify', milestone: 'v1.7', tag: 'ui',
title: {
zh: '對話模式的引用區塊重整,列舉題集數卡跟段落引用不再並排',
en: 'Chat Citation Layout: Episode Cards and Excerpts No Longer Side-by-Side',
},
summary: {
zh: '之前在對話模式問「歌單哪幾集」「馬世芳上過哪幾集」這類列舉題,答案上方會出現一張一張的集數卡(episode card),下方又會出現紫色 chunk 段落 chip — 同一集可能在兩處都出現,使用者不容易看出兩者差異(卡片點開整集、chip 跳到該段秒數)。這版改為主從佈局:列舉題時集數卡放主視覺、底下「為什麼這幾集被選 (N 個段落)」摺疊區預設收起、展開後才看得到 chunk chips;單純內容題(例如「EP143 講了什麼」)沒有列舉就維持原本的 chip inline 渲染。三題 sample 視覺驗證完整通過 — 列舉題、內容題、topic-trigger(單獨打「歌單」) 都正確。',
en: 'Previously, asking enumeration questions in chat ("which episodes have guest X", "which episodes are music playlists") would show episode cards above the answer AND chunk chips below — the same episode could appear in both places, with no visual cue about the difference (card opens the full episode, chip jumps to a specific second). This release adopts a main+supporting layout: enumeration questions render episode cards as the primary visualization, with a collapsible "Why these episodes (N excerpts)" section below that defaults to collapsed and reveals the chunk chips when expanded. Content questions (e.g. "what did EP143 discuss") without enumeration keep the original inline chip rendering. Three sample queries visually verified end-to-end — enumeration, content, and topic-trigger ("playlist" alone) all render correctly.',
},
summaryBullets: {
zh: [
'列舉題:上方集數卡為主視覺、下方「為什麼這幾集被選 (N 個段落)」摺疊區預設收起',
'內容題(enum 為空):完全維持原本 chip inline 渲染、無摺疊',
'展開摺疊後點 chip 仍正確觸發 citation_click event 跟跳秒導航',
'中英雙語:zh `為什麼這幾集被選 (N 個段落)` / en `Why these episodes (N excerpts)`',
],
en: [
'Enumeration: episode cards as primary, collapsible "Why these episodes (N excerpts)" defaults to collapsed below',
'Content (empty enum): original inline chip rendering unchanged, no collapse',
'Citation_click event and timestamp-jump navigation work correctly after expanding',
'i18n: zh `為什麼這幾集被選 (N 個段落)` / en `Why these episodes (N excerpts)`',
],
},
},
{
date: '2026-05-18', slug: 'disabled-user-appeal-flow', milestone: 'v1.7', tag: 'feature',
title: {
zh: '帳號被停權時有正式申訴入口,不再是死路',
en: 'Disabled Accounts Now Have a Formal Appeal Channel',
},
summary: {
zh: '之前如果某個帳號被加進黑名單,使用者走 Google 登入後會收到 403 錯誤,前端只顯示一行「無法登入」就結束 — 沒有任何申訴管道,只能私訊管理員。這版加了正式申訴流程:OAuth callback 拒絕 disabled 帳號時改 redirect 回首頁帶 `?auth_error=account_disabled&appeal_enabled=true&email=...` 參數(不直接吐 403 JSON 把瀏覽器搞爆),SPA 偵測到參數後渲染 Lock card 第三狀態(🚫 icon + 「提出申訴」按鈕)→ 點開申訴 Modal 填事由(1-2000 字、自動帶 email、IP 每日限 5 次)→ 後端寫進新表 `account_appeals` → 每日台北 09:00 cron 寄 digest email 給 admin。後端三道防線:reason 空白 / 超 2000 字回 400;unknown email 與 active user email 為防列舉攻擊都靜默回 accepted 但不寫表;同 IP 每日第 6 次 429。所有 admin 行為走既有 ZSend email 不開新後台 UI(MVP),未來有量再開審核 dashboard。',
en: 'Previously, when an account got blacklisted, the user would hit a 403 error after Google login and the frontend just showed a one-line "cannot log in" — no appeal channel, just a dead end. This release adds a formal appeal flow: when the OAuth callback rejects a disabled account, it now redirects back to the home page with `?auth_error=account_disabled&appeal_enabled=true&email=...` query params (instead of returning raw 403 JSON which would break the SPA). The SPA detects the params and renders the Lock card third state (🚫 icon + "Submit Appeal" button) → clicking opens the Appeal Modal with a reason textarea (1-2000 chars, auto-fills email, IP rate-limited 5/day) → backend writes to the new `account_appeals` table → a daily 09:00 Taipei cron sends a digest email to admins. Backend has three defenses: empty/over-2000 reason returns 400; unknown email and active-user email both silently return accepted without writing to the DB (to prevent account enumeration); 6th request from the same IP per day returns 429. Admin handling uses existing ZSend email; no new admin UI in MVP — a review dashboard ships later if volume justifies it.',
},
summaryBullets: {
zh: [
'OAuth callback 對 disabled 帳號改 redirect `?auth_error=account_disabled&appeal_enabled=...&email=...`,不直接吐 403 JSON 破壞 SPA',
'Lock card 加第三狀態 disabled(🚫 icon + 申訴 CTA)+ 新 AppealModal(reason textarea 1-2000 字、auto-fill email)',
'新表 `account_appeals` + 每日 09:00 台北 digest email 寄給 admin',
'反濫用:unknown email / active user 都靜默 accepted 不寫表(防列舉攻擊);同 IP 每日 5 次後 429',
],
en: [
'OAuth callback for disabled accounts redirects to `?auth_error=account_disabled&appeal_enabled=...&email=...` instead of returning raw 403 JSON (which would break the SPA)',
'Lock card gains third state disabled (🚫 icon + appeal CTA) + new AppealModal (reason textarea 1-2000 chars, auto-fills email)',
'New `account_appeals` table + daily 09:00 Taipei digest email to admins',
'Anti-abuse: unknown email / active user silently return accepted without writing to DB (prevents enumeration); 6th IP-request per day returns 429',
],
},
},
{
date: '2026-05-18', slug: 'aihub-graphql-adapter-migration', milestone: 'v1.7', tag: 'fix',
title: {
zh: '後台 AI Hub 用量數字之前一直顯示 0,現在已可正常追蹤實際花費',
en: 'AI Hub Usage Now Tracked Correctly (Previously Always 0)',
},
summary: {
zh: '上一版「服務用量」分頁的 AI Hub 那一欄之所以空白,是因為 collector 對著一個不存在的網址 `aihub.zeabur.app/v1/usage` 打 — 那是當初寫程式時憑感覺猜的 URL,從 2026-05-09 上線後整整 9 天都沒人發現 DB 裡 aihub provider 連一筆 row 都沒寫進去。這版改走 Zeabur 官方 GraphQL endpoint (`api.zeabur.com/graphql`) 的 `aihubMonthlyUsage` query,schema 直接從 zeabur/ai-sdk 公開 repo 取出,本機 curl 驗過 totalSpend $77.54 與 CLI `zeabur ai-hub status` 顯示完全吻合。adapter 跨月 range 會自動拆成多次 query、超過 6 個月 raise,5xx 改回 raise 不再 fail-open(信任 Zeabur SLA),4xx 立即 raise。Prod 觸發一輪 collector 後 aihub row 直接寫進 DB、admin 後台看得到實際花費數字。同場補一條工程紀律:未來寫 adapter 一定要先 curl 證明 endpoint 真的活著、或查官方 SDK schema,不能用感覺猜。',
en: 'In the previous release, the "Service Usage" tab\'s AI Hub column was blank because the collector pointed at a non-existent URL `aihub.zeabur.app/v1/usage` — a guessed URL from when the code was first written. For 9 days after the 2026-05-09 launch, nobody noticed that not a single aihub provider row had been written to the DB. This release switches to Zeabur\'s official GraphQL endpoint (`api.zeabur.com/graphql`) using the `aihubMonthlyUsage` query, schema pulled directly from zeabur/ai-sdk public repo. Local curl verified totalSpend $77.54 matches `zeabur ai-hub status` exactly. The adapter auto-splits cross-month date ranges into per-month queries, raises ValueError beyond 6 months, raises on 5xx after retries (trust Zeabur SLA, no more fail-open), and raises immediately on 4xx. After triggering one collector cycle in prod, aihub rows landed in the DB and the admin tab now shows real spend. Engineering discipline added: always curl-verify an endpoint or check the official SDK schema before writing an adapter — no more guessing URLs.',
},
summaryBullets: {
zh: [
'AI Hub adapter 從猜測 REST endpoint 改走官方 GraphQL `aihubMonthlyUsage` query — schema 從 zeabur/ai-sdk repo 取',
'本機 curl 驗證 totalSpend $77.54 與 `zeabur ai-hub status` 完全吻合,prod 觸發 collector 後 admin 看得到真實數字',
'5xx 改回 raise(不再 fail-open)— 信任 Zeabur SLA,沉默失敗比噪音危險',
'工程紀律:未來寫外部 adapter 一律先 curl 驗 endpoint 或查官方 SDK,不能憑感覺猜 URL',
],
en: [
'AI Hub adapter switched from guessed REST endpoint to official GraphQL `aihubMonthlyUsage` query — schema from zeabur/ai-sdk repo',
'Local curl verified totalSpend $77.54 matches `zeabur ai-hub status` exactly; admin tab shows real numbers after one prod collector cycle',
'5xx now raises (no more fail-open) — trust Zeabur SLA; silent failure is more dangerous than noise',
'Engineering discipline: always curl-verify an endpoint or check the official SDK before writing an adapter — no more guessed URLs',
],
},
},
{
date: '2026-05-18', slug: 'multi-provider-usage-monitoring', milestone: 'v1.7', tag: 'enhancement',
title: {
zh: '後台多了「服務用量」分頁,看 OpenAI 月花費 + 預算超標自動寄信告警',
en: 'Admin Gets "Service Usage" Tab — OpenAI Monthly Spend + Auto Budget Alerts',
},
summary: {
zh: '以前 admin 要知道這個月在 OpenAI 跟 Zeabur AI Hub 上燒了多少錢,得分別開兩個外部後台手動翻。這版在後台多了「服務用量」分頁:上方雙 banner(黃色 ≥ 80%、紅色 ≥ 95%)即時顯示每個 provider 累積花費 vs 預算(v1 hardcoded:AI Hub $80 / OpenAI $30),下面是 30 天 stacked bar chart 視覺化日支出。每天台北 09:00 跑 alert worker,達 80% / 95% 自動寄信告警(per-day 去重不洗信箱)。OpenAI 那邊接通了直接從官方 organization/costs API 拉資料;AI Hub 那邊本來打算抓 `aihub.zeabur.app/v1/usage`,但發現是猜錯的 endpoint(正確是 Zeabur GraphQL `aihubMonthlyUsage`),所以這版 AI Hub 那一欄會空白、collector 走 fail-open 不阻塞 OpenAI 寫入,AI Hub 真實接入會在下個 follow-up change 補完。同場加映:每次 commit 前都會 gitleaks 掃 secret 不會把 key 寫進歷史。',
en: 'Previously, to know how much was spent on OpenAI and Zeabur AI Hub this month, admins had to open two separate external dashboards. This release adds a "Service Usage" tab: dual banners at the top (yellow ≥ 80%, red ≥ 95%) showing per-provider monthly spend vs budget (v1 hardcoded: AI Hub $80 / OpenAI $30), with a 30-day stacked bar chart below. An alert worker runs daily at 09:00 Taipei and sends email alerts at 80% / 95% thresholds (deduped per day so the inbox stays sane). OpenAI is fully wired up via the organization/costs API; AI Hub was initially pointed at `aihub.zeabur.app/v1/usage` which turned out to be a guessed endpoint that does not exist (the correct one is Zeabur GraphQL `aihubMonthlyUsage`), so AI Hub column will be blank in this version and the collector now fail-opens on 5xx/timeout instead of polluting alerts. The AI Hub real wiring lands in a follow-up change.',
},
summaryBullets: {
zh: [
'後台「服務用量」分頁:黃 80% / 紅 95% 兩級 banner + 30 天日花費 stacked bar chart(純 inline SVG,沒裝 chart 套件)',
'OpenAI 走 `organization/costs` API 自動每小時抓一次,admin 可即時看到月累積',
'達 80% / 95% 預算門檻會寄信告警(每天 09:00 台北跑、per-day 去重)',
'AI Hub adapter 改 fail-open:5xx / timeout 直接 skip 不阻塞 collector;真實接入待 follow-up change 用 GraphQL 重做',
],
en: [
'"Service Usage" admin tab: yellow 80% / red 95% banners + 30-day stacked bar chart of daily spend (pure inline SVG, no chart library added)',
'OpenAI auto-collected hourly via `organization/costs` API; admins see running monthly total',
'80% / 95% budget thresholds trigger email alerts (runs daily 09:00 Taipei, deduped per day)',
'AI Hub adapter now fail-opens on 5xx/timeout — does not block the rest of the collector; real wiring lands in a follow-up via Zeabur GraphQL',
],
},
},
{
date: '2026-05-18', slug: 'backfill-progress-admin-tab', milestone: 'v1.7', tag: 'enhancement',
title: {
zh: '後台 Queue 分頁上方多了「進度概覽」,一眼看出轉錄/摘要/分類做到哪',
en: 'Admin Queue Tab Gains "Processing Overview" — See Transcription / Summary / Topic Progress at a Glance',
},
summary: {
zh: '以前要知道「現在所有節目轉錄到第幾集了、AI 摘要補到哪、主題分類進度多少」,得自己下 SQL 或翻 log。這版在後台 Queue 分頁最上方加了「進度概覽」區塊:三條 progress bar 分別顯示轉錄、AI 摘要、主題分類的完成比例(分母是已 publish 的集數),下面再給最近 24 小時的新增量跟失敗任務數,需要時可展開看失敗的 task 名稱跟錯誤訊息範例。整塊每 30 秒自動更新一次,網路斷掉會顯示「更新失敗,重試中」但不影響底下原本的 queue 排程表。對 admin 來說,平常巡視一眼就知道 backfill 進度健不健康,不用再開 DB query。',
en: 'Previously, checking "how many episodes are transcribed across all shows, how many have AI summaries, how far along is topic classification" required ad-hoc SQL or log spelunking. This release adds a "Processing Overview" panel at the top of the admin Queue tab: three progress bars for transcription, AI summary, and topic classification (denominator = published episodes), plus a Last-24-Hours section showing newly-completed counts and any failed tasks, with an expandable view for task name + sample error message. Refreshes every 30 seconds; on network failure it shows a soft "Refresh failed, retrying" warning without disrupting the queue table below. Admins now get health-at-a-glance for backfill progress without opening a DB client.',
},
summaryBullets: {
zh: [
'新 endpoint `GET /admin/processing-stats`:admin role gate + CSRF,回傳轉錄/摘要/topic_seg 三維度的 episode/segment count 與 24h 變化、失敗統計',
'前端 `