SignalForge
自架的情報系統:把雜訊收進來,整理成去重後的事件、跨日的變化與正在成形的趨勢,最後產出一份每項內容都查得到出處的每日重點。
這個專案從開發到維運都是我一個人做的。

問題
Feed reader 給你的是「今天的文章」,但「文章」這個單位不對。
五家媒體報同一次 release,其實是同一件事。今天發布的文章,很多根本不是新資訊,只是上週那件事的評論、一個剛被證實的傳聞,或是早就在追蹤的數字又動了一下。把這些分清楚才是真正的工作,而摘要工具不做這件事:丟五篇文章進去,拿回來的是五份摘要。
SignalForge 的單位是事件,不是文章。每個 story 都記在 ledger 裡,今天收到的內容先和已知的狀態比對,記下來的是「什麼變了」:
| 變化類型 | 意義 |
|---|---|
NEW | 第一次看到這個事件 |
UPDATE | 已知的 story 出現真正的新細節 |
ESCALATION | 情況變得更嚴重 |
RESOLUTION | 有結果了 |
REVERSAL | 往反方向發展 |
CONFIRMATION | 傳聞或單一來源的報導被證實了 |
RUMOR | 有報導,但來源還不足以定案 |
NO_MATERIAL_CHANGE | 有報導,但沒有新資訊 |
NO_MATERIAL_CHANGE 最重要。被標成這樣的 story 會更新 ledger,但刻意不進每日重點。
架構
整條管線分成多個階段,中間由 Postgres 保存狀態;Curator 和 Editor 是兩個獨立的 agent session,只透過 tool 讀寫資料庫。
- 收集 十個 collector 把 watchlist 指定的內容全部抓回來。這一層不做任何編輯判斷。 來源每一筆在進來的那一刻就標為不可信任
- 策展 一個 agent session 讀過當天每一筆,把文章歸成事件、和 ledger 比對、判斷什麼變了。 寫入逐筆決策、story ledger 判斷這是哪個 story、變化類型、重要性
- 撰寫 另一個獨立的 session,全新的 context,只看得到策展後的素材,負責寫每日重點。 讀取素材、附掛的來源、結構化事實 文字發生了什麼、為什麼重要、和昨天比變了什麼
- 驗證與發布 程式碼逐一檢查每個來源 id 和事實引用。引用了不存在的東西,整份重點退回重寫。 輸出每日重點、story 資料列、正在成形的趨勢
Postgres 是唯一的正式儲存。收集到的項目、決策、跨日 ledger、素材、草稿、發布的每日重點和趨勢,全部都是資料列,管線和網頁閱讀介面讀的是同一批資料。每次執行都用 lineage 隔離資料,因此合成資料、實驗執行與正式環境可以共用同一個資料庫,不會互相干擾。
工程決策
需要留下來的東西,都不放在 agent session 裡
原因:session 可能結束、context 塞爆,或跟著背後的 provider 一起掛掉。每個決策、每個 story、每份草稿,產生的當下就透過 tool 寫進 Postgres。
結果:Curator 跑到第 50 筆當掉,可以換另一個模型從第 51 筆接手,因為前 50 筆決策已經在資料庫裡。從 JSON 檔案搬到 Postgres 的時候,只改了儲存層,其他一行都沒動。
Curator 和 Editor 是兩個 session,工具集不重疊
原因:Curator 看得到原始的資料流,也能改 ledger。Editor 只看得到 Curator 附在素材上的東西,而且完全沒有寫入的工具。所以 Editor 在結構上不可能把 Curator 已經丟掉的 story 撈回來,也不可能拿更差的資訊偷偷重做一次策展。
結果:Editor 失敗的代價很低。沒有值得保留的對話,管線直接用存好的素材開一個全新的 Editor session 重來。
數字用 fact id 傳
原因:只存在於模型輸出裡的數字沒有出處。每日重點裡的每個數字都是一個 fact id,渲染的時候才從結構化事實庫查出數值、單位和時間戳。
取捨:查不到的 fact id 會直接顯示成缺口,不會拿別的數字補上。
驗證交給程式碼
原因:不能把「再問一次模型」當成驗證機制。驗證器會檢查每個引用的來源 id 是不是真的收集過、每個事實引用查不查得到、每日重點有沒有符合必要的結構;失敗時直接指出是哪個欄位。
結果:管線從來不從 assistant 訊息裡 parse JSON。兩個 session 都是透過有驗證的 tool call 提交,驗證失敗就退回,並附上 session 可以照著修正的理由。
Agent 看到之前,不做任何編輯過濾
原因:「看起來不重要」是 Curator 的判斷。把這個判斷提前到 collector,管線就會在沒人發現的情況下慢慢看不到東西。Collector 只能因為完全重複、損壞、不支援的格式或違反來源政策而丟掉一筆。
取捨:Curator 要讀完當天收到的每一筆,忙碌的日子遠超過一千筆。成本跟著收集量走,和最後進入每日重點的數量無關,這是最大的一筆營運成本。
相似度只用來縮小範圍
原因:兩筆項目是不是在講同一件事,要讀過文字才能判斷,而且判斷會明確記錄下來。系統裡沒有任何一個相似度門檻會自己把兩筆合併。
結果:對當天項目的詞彙搜尋、對 ledger 的歷史搜尋,都只是產生候選,交給模型之後由模型決定。向量欄位和索引已經建好,但還不在決策路徑上。
安全模型
每一筆收集到的項目都是別人寫的外部文字,runtime 的設計就從這個前提出發。
- 外部內容在進來的那一刻就標為不可信任,在任何 agent 看到之前就完成,後面每一層都不需要再記得這件事。透過收集項目做的 prompt injection 是本系統要防護的威脅。
- Agent runtime 受限:沒有 shell、除了一個只能讀政策文件的讀取器之外沒有檔案系統、沒有任意 HTTP、沒有憑證。每次執行都會檢查並記錄,不只靠設定。
- Agent 從不持有憑證。 Collector 和研究層在伺服器端執行,交給 agent 的是內容,不是金鑰。模型的登入資訊屬於安裝好的 agent runtime,唯讀。
- 閱讀介面只讀、預設只在本機。 Postgres 和網頁應用預設綁 loopback,每個回應都帶嚴格的 CSP,還有一個測試會走過閱讀介面底下每個檔案,只要有任何一處碰得到模型就讓建置失敗。
正在成形的趨勢
趨勢是連續幾天反覆出現,但單獨看任何一天都還不足以構成 story 的訊號。因為模型產生的標籤每天可能不同,系統不拿標籤判斷是不是同一個趨勢,改比對背後的 story 集合,看重疊程度。這是管線裡最不成熟、也最可能再改的部分。
限制
這些限制決定了這個設計值不值得參考。
- 單人、自架。 一個讀者、一個資料庫,閱讀介面前面沒有登入。公開的實例是只讀的,只提供管線已經發布的內容。
- 每日重點的品質受限於模型和來源涵蓋範圍。 能寫成確定性規則的部分都交給程式;需要語意判斷的部分仍由模型負責。
- 個人化有規格,還沒接上。 興趣設定檔有驗證,但 agent 還沒讀它,也還沒有任何東西會從你實際讀了什麼去學習。
- 歷史價值要靠累積的 ledger。 全新安裝的前幾天,什麼都是
NEW。 - 不提供支援。 公開是因為這個設計可能值得讀、值得改。沒有人在待命。
開源實作
signal.meowcoder.com:我自己的實例,照管線發布的樣子公開:今天的每日重點、歷史和趨勢。github.com/tc3oliver/signalforge:管線、閱讀介面、migration 和驗證測試,MIT 授權。docs/ARCHITECTURE.md:為什麼長這樣,以及每個保證是在哪裡強制的。docs/SECURITY.md:威脅模型,以及每一條背後對應的程式碼和測試。docs/INTELLIGENCE_BACKLOG.md:接下來要做的事,附證據和驗收條件。
pnpm verify 是判斷一個改動能不能進的指令:型別檢查、不允許跳過任何測試的完整測試、加上閱讀介面的建置。CI 在真實的 Postgres 上跑它。pnpm demo
不需要模型、金鑰或網路,就能產生三天的合成歷史,讓你在設定任何東西之前先看到閱讀介面跑起來的樣子。