iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0

前言

網址處理完,剩下的是檔案:PDF、Word、HTML、Markdown、純文字,還有兩種書目格式。

把檔案讀進來不夠。書目資料躺在 front matter 裡沒有用,要能在 wiki 頁裡寫一個引用鍵、渲染出來變成(作者, 年份)、頁尾自動長出參考文獻,它才用得起來。

PDF:文字抽得出來,版面抽不出來

我用 unpdf 抽文字跟 metadata。各頁文字會併成一份,metadata 只用 Title 跟 Author,建立時間有抓到但沒接著用。

你拿到的是文字流,不是版面。兩欄排版的論文會變成兩欄交錯的一團,表格會變成一串沒有結構的數字,圖說會插在奇怪的地方。

對後續的 agent 編纂來說,文字順序稍微亂掉,還是比完全沒有這份資料有用。模型有能力從一團亂的文字裡抽出重點。要把版面還原,需要接專門的解析服務,我沒做。

掃描出來的 PDF 則完全沒轍,那是圖片,沒有文字層,需要 OCR。這條路現在還是會建成一頁,只是內文是空的。

標題先看 PDF 的 Title 欄,沒有才用內文第一行(八到一百六十字、開頭不是 arXiv、doi 或網址),再沒有才用檔名。metadata 常常是空的,有時候寫著「Microsoft Word - 未命名.doc」,Title 欄有字就照用,沒有另外過濾。另外會掃描前五千字找 DOI,找到就寫進 front matter。網頁匯入那條路找到 DOI 才會去 Crossref 補完整書目,上傳 PDF 這條不會。

Word

mammoth 把 .docx 轉成 HTML,再走跟網頁一樣的 HTML 轉 Markdown 流程。這條路很順,因為 .docx 本來就有結構化的樣式資訊,標題就是標題、清單就是清單。

比 PDF 好處理得多,這也是我會建議使用者「如果同時有 .docx 和 PDF,丟 .docx」的原因。

BibTeX 與 CSL-JSON

這兩個是書目格式,處理方式跟其他檔案不同:一個檔案會產生很多頁,一筆文獻一頁。

BibTeX 的剖析我花最久,因為它的語法有很多歷史包袱:

  • @string 巨集:定義一個縮寫之後在別的地方引用,包括月份縮寫
  • @comment 與 @preamble 要略過
  • 巢狀大括號:{The {LLM} Wiki} 裡面那層是用來保護大小寫的
  • 字串串接:"Part " # "One"
  • LaTeX 重音與符號:\"{o} 是 ö、\'{e} 是 é,還有 \&、\% 這些跳脫
  • 作者格式:Last, First and Last2, First2,要拆成陣列並且順序正確

剖析器是一個手寫的掃描迴圈,略過巨集跟註解的地方長這樣:

if (type === 'string') { // @string{name = "value"}: later bare tokens expand to it
  skipWs(); const sm = src.slice(i).match(/^([^=\s]+)\s*=/); if (sm) { i += sm[0].length; strings.set(sm[1].toLowerCase(), readValue()); }
  const close = src.indexOf('\n@', i); i = close < 0 ? src.length : close + 1; continue;
}
if (type === 'comment' || type === 'preamble') { const close = src.indexOf('\n@', i); i = close < 0 ? src.length : close + 1; continue; }

出處:src/bib.ts:69-73

CSL-JSON 相對乾淨,就是 JSON,主要處理的是 Zotero 匯出時欄位型別不一致的問題:同一個欄位有時候是字串有時候是數字,一律強制轉型。

匯入的行為是:一筆一頁,citation_key 已經存在的跳過(避免重複收藏),全部都跳過就回一個明確的訊息。回應裡會告訴你匯入幾筆、跳過幾筆,並附上一段可以直接用的提示詞,讓 agent 一次把這批新來源都編纂掉。

從 Zotero 同步

很多研究者的文獻本來就在 Zotero 裡,不會想重新匯入一次。

所以做了同步:使用者貼一把 Zotero 的 API key,選一個收藏夾,之後每小時增量同步一次。

export function scheduleZoteroSync(intervalMs = 3600_000): NodeJS.Timeout {
  …
  const { rows } = await pool.query<{ workspace_id: string }>(
    `SELECT workspace_id FROM zotero_links WHERE last_error IS NULL OR last_sync_at < now() - interval '6 hours'`);

出處:src/zotero.ts:187-194

那句 WHERE 藏著一個判斷:上次失敗過的連結不會每小時重試,要等六小時。對方的 key 被撤銷或收藏夾被刪掉時,我不想每小時去敲一次別人的 API,然後在自己的日誌裡累積一整天的錯誤。Zotero 的項目轉成我們的來源頁,作者、年份、DOI、期刊、標籤都對應過來。如果它有用 Better BibTeX 產生引用鍵,就沿用,這樣兩邊的鍵一致。

預設會連 PDF 附件一起抓下來抽全文,接在摘要後面。可以關掉。

書目的真值留在 Zotero,我們這邊是唯讀的副本,只負責編纂。雙向同步要處理衝突、合併、刪除傳播,我沒做。單向比較簡單:Zotero 管文獻,這裡管理解。

匯出的檔案是別人的輸入

反過來也支援匯出:把 raw/ 底下所有有引用鍵的頁面吐成 .bib 或 CSL-JSON,note 欄位記著它在知識庫裡的路徑。

匯入時我會小心處理別人的檔案,匯出時差點忘了自己也在產生別人要剖析的檔案。使用者的標題裡出現一個 & 或 %,寫進 .bib 就是一個壞掉的檔案,而壞掉的地方會在他把檔案餵給 pandoc 的時候才爆開,那時候他不會覺得是我這邊的問題。

所以有一份反向的跳脫表:

const LATEX_ESC: Record<string, string> = { '\\': '\\textbackslash{}', '{': '\\{', '}': '\\}', '&': '\\&', '%': '\\%', '#': '\\#', '_': '\\_' };
const textToLatex = (s: string) => s.replace(/[\r\n]+/g, ' ').replace(/[\\{}&%#_]/g, ch => LATEX_ESC[ch]);

出處:src/bib.ts:27-28

還有換行也被換成空白,BibTeX 的欄位值裡塞換行,有些剖析器會直接放棄那一筆。

用 pandoc 的語法

引用的寫法我直接抄 pandoc,因為學術圈的人本來就熟:

這個模式主張編纂而不是檢索 [@karpathy2026llmwiki]。
單一來源出版的三篇文獻 [@dunn2003single; @fauchie2023the] 也提到類似的取捨。
細節見原文的第三節 [@mensh2017ten, p. 12]。

渲染之後,[@karpathy2026llmwiki] 變成「(Karpathy, 2026)」,可以點,連到那份來源頁。頁面最下方自動出現參考文獻區塊,列出這一頁引用過的所有文獻,格式是作者、年份、標題、期刊、DOI。

作者的縮寫規則照學術慣例,一行就寫完了:

// (author, year): one author Chen; two Chen & Lin; three or more Chen et al.
export function citeLabel(e: BibEntry): string {
  const fam = e.authors.map(familyName);
  const who = fam.length === 0 ? e.title.slice(0, 24) : fam.length === 1 ? fam[0] : fam.length === 2 ? `${fam[0]} & ${fam[1]}` : `${fam[0]} et al.`;

出處:web/src/lib/cite.tsx:20-24

第一個分支是給沒有作者的來源用的:網頁、報告、法規常常查不到作者,這時候退回標題的前二十四個字,至少讀者看得出引用的是什麼,而不是一個空括號。

引用就是一條邊

解析引用鍵、對應到來源、渲染成(作者, 年份)、在頁尾長出參考文獻、引用到不存在的來源要被抓出來:這些事聽起來各要一套機制。

之前我們已經把 [@key] 併進連結的解析,引用就是一條指向該引用鍵的邊,跟 [[連結]] 存在同一張表、走同一套解析規則。所以待編纂的判斷、反向連結、圖譜、健檢,全部是既有的查詢,一行都不用改。

新寫的只有兩個:渲染時怎麼把引用鍵變成(作者, 年份),以及頁尾那份參考文獻怎麼組出來。

讓 agent 知道要這樣寫

功能做好了,但 agent 不會自己知道要用這個語法。

處理方式是改研究者模版的規則頁,明確寫出:引用來源時使用 [@citation_key],不要用括號寫作者年份,也不要只寫連結。因為規則放在 schema/ 而不是系統提示詞裡,使用者也能自己調整成他習慣的格式。

對話功能的系統提示詞也一起改了,回答問題時如果引用到有書目的來源,就用同樣的語法。

小結

引用定在一個鍵上。wiki 頁寫 [@karpathy2026llmwiki],旁邊出現(Karpathy, 2026);規則頁寫死這一種,agent 不會自己用括號編一個作者年份。兩邊點進去是同一份來源,那句話才對得到來源裡寫的。


上一篇
Day 15 - 擋間接提示詞注入
下一篇
Day 17 - 工作紀錄與失敗步驟
系列文
為你自己蓋一座會複利的知識庫——WikiBrain 共 22 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言