本簡報以 16:9 橫向為主
請旋轉手機
或改用平板 / 桌機觀看
上半場把 SSE / AbortController 串好,讓回答逐字浮現而不是空等五秒;
下半場把 D13 預告的 Embedding 補完,再用 RAG pipeline 把公司 FAQ 與會議紀錄變成會引用來源的問答機器人。
今天先讓 Streaming 把 UX 補滿,
下半場 Embedding 接上 RAG pipeline,一次到位。
預設 API 等 3-15 秒才一次吐完整段;改 Server-Sent Events 把 token 一個個推出去,使用者第 1 秒就看到第一個字。
感知速度跟「總生成時間」沒關係,跟「第一個字什麼時候出現」有關。Streaming 把 5 秒的等待從沉默變成過程。
text/event-stream、Cache-Control no-cache、Connection keep-alive。再呼叫 res.flushHeaders() 立刻把 header 寫出去。
client.messages.stream({ model, messages, max_tokens }),把整段 prompt 丟進去,不要等回傳。
for await (const chunk of stream),當 type 是 content_block_delta 就 res.write('data: ' + chunk.delta.text + '\\n\\n')。雙換行是 SSE 事件邊界。
data: [DONE]\\n\\n 當訊號,再 res.end()。前端看到 [DONE] 就關閉 reader。
req.on('close', () => stream.controller.abort())。客戶端關頁也要記得停上游,不然繼續燒 token。
fetch('/api/stream', {method:'POST', body}),再用 res.body.getReader() 拿 reader。{value, done},TextDecoder 解 utf-8、累積到 buffer、按 \\n\\n 切。最後一段沒收完留回 buffer 等下一輪。line.replace(/^data:\s*/,'')、看到 [DONE] 就 return;其餘 JSON.parse 取 delta,out.textContent += delta 即時上畫面。關鍵不是 SSE 多潮,是這三件事都別漏:UTF-8 別截斷、buffer 別吞事件、parse 失敗要 try/catch。
.out.streaming::after { content:''; animation: cursor 1s steps(2) infinite; }。stream 結束 removeClass('streaming') 游標自動消失。prefers-reduced-motion 要降為靜態半透明。
controller = new AbortController(),fetch 帶 signal: controller.signal。取消按鈕 onclick = () => controller.abort()。TCP 真的斷,後端 req 'close' 也會觸發 → 上游一起停。
controller = null、清 buffer。再送一次必須乾淨重來,不然會接在舊文字後面或殘留 abort error。
err.name !== 'AbortError' 才印錯誤;AbortError 不是 bug 是使用者意願,靜默處理就好。
會打字 + 能停 + 能重來——這三件事到齊,使用者才會把它當「成熟的 ChatGPT 介面」而不是 Demo。
不講數學、不講 transformer。
講三件事:什麼是 embedding、cosine 怎麼比、向量庫該選誰。
講完直接接 RAG,不會像 D13 那樣「學了沒地方用」。
voyage-3 / text-embedding-3-large / cohere-embed-v3。回傳一個 1024 或 1536 維的向量。一次呼叫成本 < 0.0001 美元。
不要把 Embedding 跟 LLM 混淆:Embedding 是「把意義量化」,LLM 是「依語意產生新文字」。RAG 把兩者串起來。
pip install chromadb,沒裝 server 也能跑。原型 / 內部小工具首選。資料量 < 10 萬 chunk 都流暢。
選型原則:先估你 6 個月內能蒐到多少 chunk。< 10 萬 → Chroma;已有 Postgres → pgvector;千萬以上 → 託管。不要為「未來可能 scale」付現在的營運成本。
所有 RAG 系統都是這五步。差別只在每一步的細節決策——chunk 多大、top_k 取幾、retrieval 門檻多少、prompt 怎麼寫。
{doc_id, heading, chunk_id}。chunk 太大檢索不準、太小語意斷裂。
doc_id / heading / chunk_id / updated_at。Generate 階段引用 / 過濾 / 顯示來源全靠這個。新手最常踩的雷:「整篇文章塞一個 chunk」(retrieval 不準、token 爆炸)、「每句話一個 chunk」(語意斷裂、cosine 不可靠)、「不存 metadata」(答案無法引用、無法溯源)。
<chunk id="..." source="...">...</chunk> XML 包起來。模型對結構化標記比 markdown 敏感。
RAG 編造的細節 90% 是 prompt 沒寫好,不是 retrieval 抓錯。先把這四條釘進 system prompt,幻覺率立刻砍半。
Golden Set / Cost / Hallucination Rate。
今天先把概念植入腦海,明天 D16 完整跑 Evals + Guardrails + 成本控管 dashboard。
今天先把這三個欄位的記錄表建好,明天 D16 把完整 eval / guardrail / cost dashboard 補上。沒有指標的 RAG 不能上線。
兩個情境+一個 fallback 補充+一個自我演練「幻覺獵人」。
做完你會有一條可直接改吃自家文件的 RAG 雛型。
glob('faq/*.md') 讀 30 篇 MD,同時抓 frontmatter 的 title / category,存成 {doc_id, content, meta} 列表。
{doc_id, heading, chunk_id}。30 篇 → 約 180-240 chunk。
voyage-3 或 text-embedding-3-large(1024 維)。存進 Chroma 本機庫,不需要雲端。批次 100 一次,省一半時間。
<chunks><chunk id="hr-leave#year-leave">年假計算規則為...</chunk><chunk id="hr-leave#approval">請假需主管...</chunk></chunks>
{{ user_question }} — 員工原話直接傳,不要先讓另一個 LLM 改寫,會多 1 次延遲與成本。
把這四塊釘穩,RAG 答案就會長得跟客服系統一樣穩定——有來源、有邊界、有拒答。
meeting_date、attendees。檢索時可加 filter「最近 30 天」、「內含 PM 名字」,大幅提升精準度。
exp(-days/180)。半年前的會議和昨天的會議都 0.8 相似,應該優先抓昨天那筆。
情境 B 顯示 RAG 不是一個 prompt 通天。同樣 5 步、不同領域要做領域特化:metadata、chunk 邊界、檢索 ranking、輸出格式都要改。
RAG 上線後最快的口碑殺手是「它說得很有信心,但全錯」。讓它敢拒答,是 RAG 變熟的第一個成熟跡象。
rag-v1-YYYYMMDD.png。baseline 不可重跑。自評最大的盲點是「自己做的 RAG 自己覺得沒幻覺」。三件組 = 對未來 24h 的自己 + 三個假想他者,比同儕審查還狠。
今天從「會用聊天」升級成「會把公司文件變成可問的助理」。明天 D16 把它變成可上線、可驗收、可控成本的東西。
Pipeline 五步學一天就會。難的是文件本身有沒有被人類整理過——
標題分層清不清楚、有沒有更新時間、條款有沒有寫完整。
如果文件本身就亂,RAG 救不了。
你今天從「會寫 Agent」升級成「能把公司文件變成會引用的問答系統」。
明天我們把它接 eval 框架、加防護、做成本看板——讓老闆相信它可以上線。