美股收盤是台北凌晨四、五點。我起床是七點多。八點整,Telegram 的投資頻道會躺著一份晨報:每檔持倉的現價、當日漲跌、我的損益變化,還有一兩句「今天值得注意什麼」。
從收盤到晨報送達,中間隔了大約三小時。這三小時我在睡覺,系統在值班。今天把這條「盤後 pipeline」從頭到尾拆開——它是我整套系統裡最有「管家感」的一條流水線,也是我對「什麼事交給程式、什麼事交給 LLM」想得最清楚的地方。
整條 pipeline 是三個排程任務的接力:

**第一棒:04:00 快取更新。**昨天講過的限速抓取,把二十幾檔股票的收盤價寫進 JSON 快取。
**第二棒:06:00 持倉價格同步。**一支 Python 腳本(portfolio_price_sync)讀快取,更新持倉狀態檔——那是一份 Markdown 表格,記著每檔的成本、股數、現價、未實現損益。腳本做的事很純粹:查價、乘股數、算損益、改表格、蓋時間戳。全程沒有 LLM 參與。它一天只跑這一班(美股交易日的隔天清晨),趕在八點晨報之前,把剛出爐的收盤損益算好。
第三棒:08:00 晨報生成。這一棒才輪到 LLM 上場——而且照 Day 15 講的分工,它不在 NAS 排程裡,跑在訂閱定額側那個排程器(Claude)上:讀 NAS 這邊已經備好的持倉檔和行情快取,把算好的數字寫成人話。它的 prompt 大意是:「讀持倉狀態檔和行情快取,寫一份晨報:總損益變化、漲跌最大的三檔、需要注意的事項,控制在一則 Telegram 訊息內。」LLM 讀的是已經算好的數字,它的工作是摘要和排版,不是計算。
三棒之間刻意留了緩衝:第一棒(04:00)與第二棒(06:00)之間有近兩小時,第二棒與第三棒之間又隔兩小時。這些留白是給故障預留的餘裕——萬一凌晨哪一棒出狀況,中間還有重試、或我起床後手動補跑的空間,晨報仍趕得上八點。pipeline 排程留白,是給故障留的逃生梯。
這條 pipeline 最重要的設計決策,是「算錢的事絕不讓 LLM 碰」。
早期版本我偷懶,直接讓 LLM 讀原始快取和成本資料,叫它「順便算一下損益」。結果三天內出了兩次錯:一次把兩檔股票的股數看串了,一次算百分比時小數點漂移。錯得不多,正好錯在「看起來很合理」的範圍——這比錯得離譜更危險,因為你不會起疑。
LLM 是機率模型,不是計算器。它寫「台積電今天走勢偏弱」永遠不會錯太遠,但它算 (現價-成本)×股數 就是會偶爾出神。所以現在的分工是一刀切:
| 工作 | 執行者 | 理由 |
|---|---|---|
| 抓價格、寫快取 | Node.js 腳本 | 確定性 I/O,不需要智能 |
| 算損益、更新持倉檔 | Python 腳本 | 數字必須 100% 正確 |
| 摘要、排版、寫「人話」 | LLM | 這才是它的強項 |
| 判斷「今天有什麼值得注意」 | LLM | 模糊判斷,錯了代價低 |
順著這個分工,模型的選擇也跟著定了:跑腳本的 job 需要模型支援工具呼叫(exec),用低價的付費模型;純寫文的晨報 job 用免費模型就夠。任務的性質決定模型的下限,成本決定上限——這句話 Day 23 會展開成完整的路由策略。
portfolio_price_sync 的邏輯簡化後長這樣:
cache = load_json("cache/market/stocks_latest.json")
portfolio = parse_markdown_table("PORTFOLIO_STATUS.md")
for pos in portfolio:
quote = cache.get(pos.symbol)
if quote is None:
pos.price = pos.cost # 查無報價:目前拿成本價暫頂(見下方誠實補充)
pos.pnl = "N/A" # 損益標 N/A、不硬算
continue
pos.price = quote["price"]
if quote.get("stale"):
pos.note = "⚠️ 舊價" # stale:照樣拿舊價算損益,只是標記提醒
pos.pnl = (pos.price - pos.cost) * pos.qty
pos.pnl_pct = pos.pnl / (pos.cost * pos.qty) * 100
write_markdown_table("PORTFOLIO_STATUS.md", portfolio,
updated_at=now_taipei())
兩個值得注意的細節:
**持倉檔是 Markdown,不是資料庫。**這延續了整個系統「狀態全檔案化」的哲學(Day 13 講過):我隨時能用眼睛看這張表,LLM 讀它也零成本,版本差異用 git diff 一目瞭然。個人規模的持倉(二十幾檔),Markdown 表格綽綽有餘。
分清「舊價」和「沒有價」。快取裡帶 stale 標記的價格(前一天講的失敗沿用機制),腳本照樣拿它算損益、只在旁邊標一個「⚠️ 舊價」——昨天的收盤價拿來估未實現損益,八九不離十,比留白有用。至於完全查無報價那種,老實說我目前的做法還不夠乾淨:損益是標了 N/A 沒硬算,但價格欄我拿成本價暫頂,於是那一列的「市值」其實是成本、不是真市值,卻還照樣算進了總市值。這是我記在故障筆記裡、還沒補好的一個洞——與其在文章裡把它包裝成「無價不猜」的美德,不如老實標出來。這種「資料可疑時本該更保守、我卻還沒做到」的落差,正好也是明天那次事故要講的東西。
坑一:下游比上游早跑。有一次調整排程,手滑把持倉同步的時間往前挪,它在快取那一班還沒抓完時就開跑,讀到半新不舊的資料。修正之後我抓了個經驗法則(不是什麼強制設定,就是我自己排程時心裡的尺):上下游的間隔,盡量抓到上游正常耗時的 10 倍以上。上游跑 4 秒,下游至少隔幾分鐘;上游跑 10 分鐘,下游隔一小時以上。依賴「上一棒應該跑完了吧」的排程,遲早翻車。
**坑二:Telegram 訊息長度上限。**LLM 有一陣子越寫越長,晨報超過單則訊息上限被截斷,最慘的是被截在損益表格中間。修法很直接:在 prompt 裡把字數當成生成時就要守的約束(「控制在一則訊息內」),叫它寫短。LLM 的輸出長度是機率分布,不是承諾——與其事後切段,不如一開始就把長度預算講清楚,讓它在寫的時候就收斂。
晨報 pipeline 的本質是一條「確定性計算 + 機率性寫作」的裝配線:程式保證數字對,LLM 負責把數字講成人話,排程留白給故障逃生。
但如果進來的數字本身就是錯的呢?明天講一次真實事故:快取裡出現一筆 $864 的假股價——實際價格的五倍——它差點順著這條 pipeline 流進我的投資決策。
🔑 這篇的關鍵字
pipeline 分工哲學:程式算數、LLM 只負責寫文字(數字錯不起,文字風格可容錯)· 快取更新 → 持倉同步(算損益)→ 晨報生成 → 投遞 · LLM 的輸入要餵算好的結果不是原始資料 · 排程分三棒(04:00 抓價 / 06:00 算損益 / 08:00 發報,分開才好除錯)· prompt 裡明確指示如何處理異常標記
我是一名金融業資訊工程師,這是我半年來在家自架 AI Agent 系統的實錄。