研究文件知識庫:附引用的檢索問答子系統
指導教授的實驗室需要一個研究文件知識庫:文件上傳一次,同時保存原始檔與 Markdown 文字版,可檢索,並由語言模型依庫內文件回答、附引用出處。本人自願參與,獨立完成其中附引用的檢索問答子系統,並以公開的繁體中文閱讀理解資料集 DRCD 評估檢索與問答。
- 期間
- 2026-08 起(08-19 受指派,08-26 至 09-10 開發與審查修改,09-23 完成評估;持續參與實驗室開發)
- 角色
- 子系統獨立完成(5 個套件、測試與文件),於例會報告、依審查意見修改,向指導教授展示
- 技術
- TypeScript、React、Node.js、zod、zhtw-js、markitdown(Python 子行程)、vitest
- 規模
- 5 個套件、47 項自動化測試;DRCD 測試集 3,493 題檢索評估、抽樣 200 題端對端評估

背景與需求
實驗室原本沒有集中管理研究文件的知識庫。子系統以外掛方式整合進實驗室既有系統,不改動既有核心;需求方訂定的規格如下:
- 多本知識庫並存,類似 NotebookLM;
- 每份文件存兩份:原始檔供人預覽與下載,轉換後的 Markdown 供檢索與語言模型讀取;
- Markdown 與 PDF 在頁面上直接預覽;
- 提供問答、結構、讀原文三個工具介面,以及供其他程式寫入的程式化入口。
三個難點
- 中英混寫與簡繁漂移 研究筆記常中英混寫,檢索模組設計為無外部相依,不倚賴中文斷詞器;語言模型被要求用繁體時仍可能輸出簡體,以工具查詢時也可能用簡體寫出知識庫名稱。
- 工具存取的邊界 呼叫端要能問答、查結構、讀原文,但整份文件不直接回傳給呼叫端;負責作答的子 agent 不能再委派問題(否則遞迴),作答時也不應寫入知識庫。
- 回答與文件的對應 回答要附出處,並分開量化:檢索是否找到正確段落、回答是否正確、引用是否指向正確段落,以及斷詞設計本身的貢獻。
儲存與轉換
儲存格式知識庫與資源的紀錄分存兩張資料表,以 zod schema 驗證。檔案放在資料目錄下的 library/v1/<知識庫 id>/:原始檔為 original/<資源 id>__<安全檔名>,轉換結果為 markdown/<資源 id>.md,另有 index.json、ask-log.json 兩個側車檔。紀錄只存相對檔名,搬移根目錄不必改寫紀錄;安全檔名把路徑分隔字元與控制字元換成底線,超過 120 字元時截斷並保留副檔名。
轉換管線ingest() 是唯一的程式化寫入入口,頁面上傳、貼上文字與 library_ingest 工具共用。流程是先把原始檔寫入 original/、狀態設為 converting,再走轉換接縫:轉換器以 registerConverter() 註冊,依優先度由高到低嘗試,只依檔名與媒體類型決定是否接受。
- markitdown(優先度 10):以子行程執行
python -X utf8 -m markitdown <檔案>,涵蓋 PDF、Office、EPUB、HTML 等 16 種副檔名;逾時 120 秒、輸出上限 64 MB,空輸出視同錯誤。 - 內建文字轉換器(優先度 0,無外部相依):Markdown 與純文字原樣保留,CSV、JSON 包成程式碼區塊,HTML 去除 script 與 style 後保留標題、段落與清單。
- 後援與狀態:轉換器拋出錯誤時交給下一個;成功時狀態落在 ready,每個轉換器都出錯或沒有轉換器接受時落在 error 並寫入原因,原始檔保留、仍可預覽與下載。
ingest()等轉換落定並重建索引後才回傳。
入庫時索引
每次內容變動(建立知識庫、入庫落定、刪除資源)時,知識庫服務整份重建該知識庫的 index.json,內容包括每份資源的 id、名稱、類別與狀態,前 12 個一至三級標題組成的大綱,開頭 280 字元的純文字摘要,以及依標題切好、附預先統計詞頻表的 chunk。structure() 與 search() 都只讀這一個檔再計分,查詢時不讀文件、不重新斷詞;單元測試在入庫後改寫磁碟上的 Markdown,確認結構與檢索仍由索引正確供應。
重建策略讀取時以 zod 驗證;檔案不存在、無法解析、不符 schema,或與持久紀錄不一致(知識庫標題、資源數,或任一資源的 id、名稱、類別、狀態不同)時,第一次讀取就自動重建並寫回;早於索引功能的知識庫、手動修改過的檔案與改名因此自我修復,一般情況仍只需一次讀檔。
採用側車檔索引可由原始資料重建,問答紀錄只會附加;兩者放在文件旁的 JSON 檔,不必新增資料表、處理資料表的版本相容,檔案缺失或過期時也能自我修復。
問答紀錄ask-log.json 保留最新 200 筆,每筆記錄提問來源(頁面或工具)、問題、回答、是否有依據、出處與時間;頁內問答與透過工具的問答寫進同一份紀錄。
檢索:TF-IDF 加中日韓雙字組
chunk 切法Markdown 在一至三級標題處切段,每個 chunk 記下最近的標題供引用顯示;超過 1,800 字元的段落在空行處再切,單一過長的段落保持完整,不在句中切斷。
斷詞轉小寫後取兩類詞:拉丁字母與數字組成的詞(底線視為分隔,gl_FragColor 成為 gl 與 fragcolor);連續漢字切成相鄰兩字的重疊雙字組,例如「入庫時索引」得到入庫、庫時、時索、索引,孤立的單一漢字保留為單字詞。不需斷詞器與詞典,中英混寫的查詢同時命中兩類詞。
計分Q 為查詢詞集合,N 為該知識庫的 chunk 數,df(t) 為含詞 t 的 chunk 數,tf(t) 為詞在 chunk 內的次數,L 為 chunk 的字元數:
score = Σ_{t∈Q} tf(t) × (1 + ln(N ÷ (1 + df(t)))) ÷ √(1 + L ÷ 100)
罕見詞權重較高,長度項避免長段落因字多佔優;分數為 0 者不列入,問答取前 8 段為摘錄。沒有關鍵詞命中的總覽式提問(例如「簡單介紹一下」)改以各文件的前兩個 chunk 為依據;知識庫沒有可讀內容時直接婉拒,不呼叫模型。
簡繁處理工具的知識庫參數先比對 id,再比對經 zhtw-js 正規化(去頭尾空白、轉繁體)的標題,以簡體寫出的名稱也能對到繁體標題;頁內問答的回答以 zhtw-js 確定性轉為繁體(臺灣用語)。
附引用的問答
頁內問答ask() 把前 8 段摘錄編號,附上文件名與標題後連同問題送給語言模型;系統 prompt 要求只根據摘錄作答、以行內引用標出處、摘錄沒有答案時直說。回答上限 2,048 token,期限 60 秒並與瀏覽器端的取消訊號合併;回傳的出處即送進模型的摘錄,依資源與標題去重、依相關度排序。
透過工具提問其他程式以 library_ask 提問時,工具啟動一個全新的檢索子 agent:
- 委派 prompt 帶入知識庫標題、id 與問題,指示先以
library_structure看資源、大綱與摘要,再以library_read讀相關文件,作答時以與資源名稱完全相同的[名稱]標出處; - 子 agent 只帶入委派 prompt,不繼承呼叫端的內容;讀到的全文留在子 agent 自己的對話中,呼叫端只收到回答與出處;
- 出處由回答中的
[名稱]對照資源名稱回收,整筆問答以工具來源寫入紀錄;子 agent 未正常完成時回報為工具錯誤。
無法啟動子 agent 時改走頁內問答的直接路徑,同樣記為工具來源。出處採名稱比對回收,結果是確定性的,子 agent 不必輸出結構化 schema。
子 agent 的工具權限委派時以工具過濾移除 library_ask 與 library_ingest,知識庫工具只留唯讀的 library_structure 與 library_read:子 agent 對知識庫只能讀、不能寫入,也無法把問題再委派給自己,不會遞迴。子 agent 的角色設定另外明文禁止再委派,且固定套用,呼叫端只能在其後附加說明。
工具介面問答與結構以整本知識庫為單位,讀原文時再以結構清單中的資源 id 指定文件;library_read 回傳轉換後的 Markdown,上限 20,000 字元,library_ingest 與頁面上傳共用 ingest()。找不到知識庫時,錯誤訊息會列出現有知識庫讓呼叫端自行更正;回傳值都有不允許額外欄位的 JSON schema。
頁面介面
- 三區版面:來源欄(上傳、貼上、拖放,列出類型、大小、日期或轉換狀態)、問答主區(持久討論串,透過工具的提問另加標示)與可收合的預覽面板。
- 引用:頁內問答的出處取自送進模型的摘錄,每份文件一個可點的標籤,點選即在預覽面板開啟被引文件,網址同步更新。
- 預覽:面板分 Markdown 與原始檔兩種模式;原始檔以 iframe 載入,PDF 預設開原始檔,文字與圖片也可直接預覽。
- 深連結:頁面狀態與網址 hash 雙向同步;開啟頁面與 hashchange 時由網址還原,狀態改變時以
history.replaceState改寫網址,不增加瀏覽紀錄。貼上連結即可開到指定知識庫與文件的預覽。
檔案存取的安全檢查
知識庫管理、問答與紀錄走 JSON 介面;上傳、行內預覽與附件下載另走資料路由,檢查如下:
- 每次請求重新驗證:啟用身分驗證時,每個資料路由請求都重新驗證,未通過回 401。
- 檔案路徑不取自用戶端:只從網址取資源 id,實際路徑由伺服器依紀錄組成(根目錄、知識庫 id、入庫時產生的
<UUID>__<安全檔名>);未知 id 回 400。 - 上傳上限預設 100 MB:先檢查宣告的 Content-Length,讀取串流時再累計實際位元組,超過即回 413 並中斷連線。
- 回應標頭:Content-Type 取自入庫時記錄的媒體類型,Content-Disposition 區分行內與附件並以 RFC 5987 格式編碼檔名,另加
nosniff與no-store。
評估
資料集與語料DRCD v1.3 測試集:台達研究院釋出的通用領域繁體中文機器閱讀理解資料集,段落取自維基百科(CC BY-SA 3.0),共 378 篇文章、1,000 個段落、3,493 題,每題附標準答案與出處段落。每篇文章寫成一份 Markdown,每個段落放在自己的 ## <段落 id> 標題下;段落都短於 1,800 字元,每段恰好成為一個 chunk。378 份文件全部經 ingest() 放進同一本知識庫,檢索由入庫時索引供應。
實驗一:檢索(全部 3,493 題)每題呼叫 search(知識庫, 問題, 100),記錄標準段落的名次。對照組為單字 TF-IDF:chunk、拉丁詞處理與計分公式完全相同,只把漢字雙字組換成逐字單字,差距只來自斷詞設計。以匯出的計分函式對 index.json 重新計分,3,493 題名次與線上 search() 完全一致,確認量測的就是部署中的程式路徑。Hit@k 為標準段落排進前 k 名的題目比例(k = 8 即 ask() 送給模型的摘錄數);MRR@100 為名次倒數的平均,未進前 100 名記 0;比例附 Wilson 95% 信賴區間。
| 指標(3,493 題) | 本子系統(中日韓雙字組) | 對照組(單字) |
|---|---|---|
| 標準段落排第一(Hit@1) | 76.8%(95% 信賴區間 75.4–78.2) | 53.7% |
| 標準段落進入前 5 名(Hit@5) | 96.3% | 82.5% |
| 標準段落進入送給模型的 8 段(Hit@8) | 98.1%(95% 信賴區間 97.5–98.5) | 87.5% |
| 標準文章進入前 8 段 | 98.7% | 90.6% |
| MRR@100 | 0.853 | 0.662 |
實驗二:端對端問答(抽樣 200 題)以 mulberry32 亂數產生器、種子 20260923 洗牌後取前 200 題,逐題依序呼叫 ask(),即頁內問答的完整路徑(檢索、模型作答、轉繁體)。答案命中指任一標準答案出現在回答中,比對前兩邊都經 NFKC 正規化、轉繁、轉小寫,並去除空白、標點與引用標記,以全部 200 題為分母;引用到正確段落指編號引用 [k] 對應的第 k 段摘錄就是標準段落,以取得回答的 198 題為分母;另依標準段落是否在摘錄內分組,區分錯誤出在檢索或生成。
| 指標 | 結果 |
|---|---|
| 回答包含標準答案 | 96.5%(193/200,95% 信賴區間 93.0–98.3) |
| 標準段落進入 8 段摘錄 | 99.5%(197/198) |
| 標準段落在摘錄中時的答案命中 | 98.0%(193/197) |
| 回答附有引用 | 100%(198/198) |
| 引用到正確段落 | 98.0%(194/198) |
| 回答延遲中位數 | 9.4 秒 |
評估報告附資料集雜湊、逐題名次與回答紀錄,以及可重跑的程式。
測試與文件
47 項自動化測試於 2026-09-23 重跑,全數通過。核心服務與工具的測試組起真實的儲存層(暫存目錄中的 JSON 後端),不以 mock 取代;工具測試經真實的工具執行路徑驅動子 agent,子 agent 由腳本化的替身扮演;介面測試以 jsdom 與 Testing Library 執行。
| 範圍 | 項數 | 涵蓋 |
|---|---|---|
| 核心服務:切段、斷詞、計分 | 11 | 標題切段、超長段落再切、雙字組斷詞、罕見詞排序、大綱 |
| 核心服務:轉換 | 8 | 格式判斷,Markdown、CSV、HTML 轉換,安全檔名 |
| 核心服務:入庫、索引與問答 | 13 | 雙份保存、轉換出錯時保留原始檔、空知識庫婉拒、總覽式提問、索引寫入與重建、紀錄上限 |
| 工具介面 | 6 | 委派 prompt 與工具過濾、紀錄寫入、子 agent 異常結束、兩條退回路徑、簡體標題解析 |
| 頁面介面 | 9 | hash 位址雙向轉換、頁首與討論串、Markdown 與 PDF 預覽、引用標籤導回預覽 |
文件:子系統文件與 6 份套件 README 皆有三語版本,以語言對應紀錄檔追蹤一致性,子系統文件含由原始碼產生的 API 章節;2 份設計決策紀錄記載問題、決策、替代方案、影響與驗證。
例會報告與審查修改
第一版在第三次例會報告,依回饋調整頁面版面與問答輸入框,並在實測以工具查詢後加入簡繁不敏感的標題比對。第四次例會由需求方審查,確認儲存形式與入口,會後提出四項修改要求:
| 審查要求 | 完成方式 |
|---|---|
| 介面依既有設計語言調整 | 移除頁內重複的知識庫欄,側欄成為唯一切換入口;改用既有的對話框與元件;三區版面;引用改為可點的標籤 |
| Markdown 與 PDF 分開預覽 | 預覽面板提供 Markdown 與原始檔兩種模式 |
| 入庫時生成結構與索引 | 每次內容變動重建 index.json,缺失或過期時自動重建 |
| 問答、結構、讀原文三個存取介面 | 問答委派檢索子 agent,並排除問答與寫入工具;結構直讀索引;讀原文直讀 Markdown |
同一輪另完成問答紀錄的保存與合流、深連結、三語文件與設計決策紀錄,完成後逐項回報。
本人負責範圍
- 附引用的檢索問答子系統由本人獨立完成:5 個套件,涵蓋儲存與轉換、入庫時索引、檢索、問答與子 agent 委派、檔案存取與頁面介面;47 項自動化測試;三語文件與 2 份設計決策紀錄。
- 自願參與;於第三、第四次例會報告並回應審查提問,完成四項審查要求並逐項回報。
- 依展示腳本向指導教授展示操作流程。
- 以 DRCD 對實際部署的組合做評估,並回報結果。
- 產品方向與驗收條件來自需求規格與審查意見;實作與評估把 AI 程式助理納入流程,由本人審閱、測試與提交。
時間線
| 日期 | 事件 |
|---|---|
| 2026-08-19 | 知識庫需求建立並指派給本人 |
| 2026-08-26 | 提交第一版(5 個套件、26 項單元測試);第三次例會報告 |
| 2026-08-30 | 第四次例會審查;提出四項修改要求 |
| 2026-09-09 | 四項修改完成並逐項回報 |
| 2026-09-10 | 調整子系統的組合設定;完成展示腳本 |
| 2026-09 | 向指導教授展示 |
| 2026-09-23 | DRCD 評估完成並回報;47 項測試重跑全數通過 |