跳到主要內容

Greatest notes compiler 知識蒸餾流程

 1. Greatest notes format

平常我就有作筆記的習慣,其中大部份的筆記都是非結構化的文字檔,部份是 markdown 格式,這樣在任何平台只要有文字編輯器,就能很方便檢視及編輯,而不必依賴特定的應用程式。

久了就慢慢累積了不少資料,有日記、工作日誌、筆記、雜記等等。為了方便檢視,我也作了一個小 app,可以讓我在一個畫面內快速檢視我的筆記列表,再點入去作進一步的檢視各別資料或統計資訊等。

雖說這些筆記幾乎是非結構化的,但其實還是有少量的規則,這樣我的程式才能作簡單的解析。例如日記或工作日誌基於日期的資料,至少有個日期標題和不定長度的內文,或是筆記類的資料至少也有個標題和不定長度的內文,等等。

漸漸的我盡量把類似性質的筆記的格式統一起來,最後我發現其實可以用一個很簡單的格式全部統一。它就是 Greatest notes 格式。

Greatest notes 文件是文字檔。內容由不定數量的項目組成,每個項目由一個標題和不定長度的內文組成。每一個項目由每一行開頭的">>> "開始,直到下一個">>> "或者是檔尾,就是一個項目。

例如:

>>> 這是一則筆記
內文
內文
內文
...

>>> 另一則筆記
內文
內文
...

格式很簡單,要寫一個解析器也很簡單。因為只需要 re 以 ">>> " 對整個檔案切割,再從每個項目的第一行取出標題,其餘為內容。

例如下面就是一段可以解析 Greatest notes 文件的 javascript 程式。

// javascript
let notes = raw.split(/^>>> /gm).map(note => {
    let i = note.indexOf('\n');
    let title = note.substring(0, i);
    let content = note.substring(i + 1);
    return {
        title: title !== '' ? title : '未命名筆記',
        content: content
    };
}).filter(note => note.content !== ''); // Remove empty notes.

因為這是一個極簡的格式,只以 ">>> " 對檔案內容作區隔。缺少巢狀結構和 metadata 等。但實際應用時,能在標題帶入某種簡單的格式作為 metadata 的額外補充。例如日記或日誌等資料,可以在標題中統一以一個日期開始等。

有了這個簡單的格式,要建立筆記就方便了。在標題加上少量的自定義 metadata 就能進一步增強文件的結構,有利於進一步整理及資料分析。

2. Greatest notes compiler

近幾年 LLM 崛起,我也和大家一樣利用 AI 輔助探索了許多感興趣或遭遇到的問題。

OpenAI ChatGPT 有個對我很方便的地方是,可以容易的把整個對話資料匯出備份。匯出的資料全部被打包成一個 zip,下載後解壓。裡面有一個對話資料有關的 json 檔案。

後來我利用 AI 輔助,寫了一個讀取並解析此檔的工具程式,可以在電腦上的 browser 檢視。之所以會另外作一個工具檢視,而不是只使用官方的 app。是因為我常常會回頭檢視歷史紀錄,而當對話很長時,檢視已經變得很不方便。我作的小工具主要是為了提供一個大綱模式,讓我可以快速跳至指定段落,方便閱讀。

因為前面我已經發展出 Greatest notes 格式和手機檢視小工具,所以很自然的,我也把 ChatGPT 的 json 格式的對話紀錄轉成 Greatest notes 格式,並匯入我的小工具,在手機上方便隨時檢閱。

但是長對話的問題還是存在。超長的文件不管用什麼方式檢視都是個問題。就算我把那些問答式的對話,或是些無關緊要或價值不大的對話刪除,轉換成 Greatest notes 後,許多對話都有幾萬字,整個文件將近一百萬字。

基於這個原因,我才發展出 Greatest notes compiler,可以把 Greatest notes 文件內容壓縮,提取摘要和重點,過濾掉不是重點的內容。最後一樣輸出另一個 Greatest notes,這樣我可以互相比對參考。

2.1 Greatest notes compiler pipeline

Greatest notes 編譯器是一個 pipeline 流水線,類似一般的程式語言的編譯和連結流程。

load_greatest_notes
   ↓                載入 Greatest notes
do_compile
   ↓                對個別 note 編譯,輸出個別 json
merge
   ↓                合併所有 json,輸出最終 Greatest notes
  完成

2.1.1 編譯流程

編譯流程首先載入 Greatest notes 後,分別對個別的 note 執行編譯。每個 note 編譯完成後會個別輸出一個 temp 的 json 檔案,類似編譯 c/c++ 程式時,會產生中間的 obj 檔案。分成幾個步驟如下:

compile_note
   ↓                對單一則 note 編譯
chunk_text
   ↓                對 note 以固定長度作切割
summarize_chunk
   ↓                對 chunk 內容作摘要提取 
extract_claims
   ↓                對 chunk 內容作要點提取
  完成

2.1.1.1 chunk_text

chunk_text 主要目的是對輸入的 note 內文作切割,避免單一 note 內文過大,造成後續編譯步驟出問題。

切割的方式很簡單很暴力,就是以固定長度對輸入文字切割。目前以 chunk_size = 2000 作區段大小處理。雖然用這麼簡單又暴力的方式切割文字,但實測結果並沒有什麼明顯的問題,所以也就暫時都這樣作了。

2.1.1.2 summarize_chunk

summarize_chunk 利用 LLM 的語言能力對輸入文字作摘要提取。目前使用的模型是 gemma3:12b-it-q4_K_M,測試結果對我而言很好。

提示詞如下:

Summarize the following conversation chunk.
只用繁體中文回答.

Return JSON:
{{
  "summary": "...",
  "main_points": []
}}

Text:
{text}

2.1.1.3 extract_claims

extract_claims 一樣利用 gemma3:12b-it-q4_K_M 作為輸入文字的重點提取引擎。

提取出來的重點除了文字以外還有一個 type,這是跟未來替編譯流程加上下一步驟有關:建立重點之間的關聯以建立知識圖譜。

提示詞如下:

Extract atomic claims from the text.

Constraints:
- Each claim must represent a DISTINCT semantic proposition.
- Do not produce multiple claims that can be merged.
- If two sentences imply the same normative rule, merge them.
- Prefer canonical, abstract, policy-level phrasing.
- Maximum 10 claims per chunk.
- Remove rhetorical emphasis.
- No restatements.
- 只用繁體中文回答.

If the text repeats an idea, output it only once.

Return JSON:
{{
  "claims": [
    {{
      "text": "...",
      "type": "normative | descriptive | inference"
    }}
  ]
}}

Text:
{text}

2.1.2 連結流程

連結流程比較簡單,也是類似 c/c++ 程式最後的 link 動作將所有 objs/libs 等連結成一個 exe 或 dll 等。而 Greatest notes compiler 的連結動作則是把所有個別 note 編譯成功後輸出的 json 檔,全部合拼再輸出成一個 Greatest notes 文件。

下面是編譯後的一篇 note 的範例。



留言

這個網誌中的熱門文章

單人撲克牌遊戲 - 蒙地卡羅

更多可在網頁玩的 單人撲克牌遊戲 ; 新增一個簡單的單人撲克牌遊戲: 蒙地卡羅 ,簡單介紹一下玩法。 下載 事先排列好5x5張牌。 每次移動一張可以配對的牌,並消除這對牌。在上下、左右及斜向相隣的二張牌,只要擁有同樣數字(不計花色),即可配對。 消除二張配對的牌後,剩餘的牌以往左往上的方式補滿空隙,接著在發新牌補滿後面的空格。 重覆步驟2~3,直到沒有牌可以配對及發完所有牌為止。 結果有二種。一個是勝利,成功的消除掉所有牌。另一個是Gameover沒有牌可以再作配對。

KillSudoku 4顆星精彩數獨詳解 - 鍊技巧

這題數獨(sudoku)題目估計為4+顆星,有點難度。解題需要應用多種技巧,過程非常精彩有趣,是個好題。 底下使用 KillSudoku 作詳細圖解。 1,使用基本排除法則,可以簡單填入6個數字。到此為止,開始使用 候選數法 來解題。如下所示,為填入6個數字後的狀態圖。 2,如下圖,使用進階排除法,在第9列和第4行可以先排除幾個候選數。 3,如圖,在第2行有一個 Naked Subset (3,4),可以對3,4候選數作排除。附帶提一下,反過來看在同一行裡面也可以說有另一個Hidden Subset(2,5,8)存在。Naked Subset和Hidden Subset常是一體二面同時存在,只不過對我們來說,Naked Subset是相對比較容易看的出來。 排除第2行的3,4後,又可以對第2列以外的3作排除,如下圖。 4,接著,在第5行又發現了一個 Naked Subset (3,7,8)。 對第5行三個Subset以外的候選數3,7,8作排除後,又接著產生可以對第5行以外的3作排除。 5,這一題解到此為止,開始進入高潮。大部份能解到3顆星題目的人,猜想應該就此卡住。以下開始需要應用更高級的鍊技巧,才能夠繼續進行。 應用X-Chains鍊技巧,可以找到一條由4條強連結組成的鍊,可以排除候選數2。這裡的鍊指的是由2條以上的強連結組成,而所謂的強連結是指在同一行、或同一列或同一個Box裡,由唯二的候選數構成的連結。如上圖中的第9行中,只有二個2,這二個2構成一條強連結。為什麼說這是一條強連結?因為在這條連結的AB二個端點中,肯定會有一個2存在,要麼是A點要麼是B點。鍊技巧就是將多條強連結串連起來作候選數排除的技巧,而X-Chains是高級的鍊技巧裡面的基本技巧。 接上圖,這樣一來就又可以應用基本排除方法,填入3個數字,如下圖所示。 6,接下來就是本題最精彩的部份,以下需要連續找到3條鍊,才能繼續往下解。 7,找出3條鍊後,剩下來的部份就沒什麼特別的了,只需要應用基本法就能把所有剩餘數字填完。

以lex/yacc實作算式計算機

前面我們透過 手工的方式 實作了一個簡易的算式計算機,現在我們要開始使用工具來作同樣的事,比較看看手工和使用工具有什麼不同的差別。首先要介紹的就是lex&yacc。 lex & yacc lex(Lexical Analyzar)及yacc(Yet Another Compiler Compiler)是用來輔助程式設計師製作語法剖析器的程式工具。lex的工作就是幫助我們將輸入的資料文字串流分解成一個個有意義的token,而yacc的工作就是幫我們分析這些token和我們定義的規則作匹配。下圖中所表示的是使用lex及yacc的一般工作流程。 首先看到yacc會讀入一個.y檔案,這裡.y檔案的內容就是我們使用類似(E)BNF語法定義的語法規則,yacc會分析這些語法規則後,幫我們產生可以用來解析這些規則的程式碼,而這個檔案一般名稱預設為y.tab.c,產生的程式碼裡面最重要的一個的函式叫作yyparse。 同yacc類似,lex也會讀入一個.l的檔案,這個檔案裡面定義的是如何從文字流裡解出token的規則,使用的方法是常規表示式(regular expression)。在圖的左側中間我們還可以看到有一個叫作y.tab.h的檔案從yacc產生出來並餵給lex作輸入,這個檔案是yacc根據在讀入的.y檔裡面所定義的token代號所產生出來的一個header,這樣yacc及lex產生出來的程式碼裡面就可以使用共通定義的代碼而不必各寫個的。lex分析過.l檔案後也會產生一個一般預設叫作lex.yy.c的原始碼檔案,裡頭最重要的一個函式叫作yylex。 最後,我們把yacc產生出來的y.tab.c還有lex產生出來的lex.yy.c,以及其它我們自己撰寫的原始碼檔案一起拿來編譯再作連結,最後產生出來的就是一個可以用來解析我們定義的語法的解析器工具。以上是整個lex及yacc的使用流程概觀。 常規表示式 在正式使用lex之前,我們首先來對常規表示法作一個基本的認識。常規表示法是一種用來表示字串樣式(pattern)的中繼語言,就好比前文所介紹的(E)BNF表示式一樣,都是用來描述其它語言的語言,只不過用途不太一樣罷了。 常規表示式使用一些中繼符號(meta-symbol)以及ASCII字元定義字串樣式,以下列出一些常規表示式所使用的符號。 . 表示除了換行字元...