做 RAG 先別急著買向量數據庫:你的 Postgres 早就把這活包了 (原文發表於 2026 年 10 月 19 日) 檢索增強生成(RAG)不需要大框… · @swordBluesy · TwKit
@swordBluesy
做 RAG 先別急著買向量數據庫:你的 Postgres 早就把這活包了 (原文發表於 2026 年 10 月 19 日) 檢索增強生成(RAG)不需要大框架,也不需要一個獨立的向量數據庫。這篇用 Next.js、Postgres、pgvector、官方 OpenAI 套件和 pg 驅動,搭一個緊湊版出來。完整起步專案在 GitHub:http://github.com/buildfound/rag-starter。 整條流程四步: 讀 Markdown 和文字檔。 切成互相重疊的塊(chunk),逐塊做嵌入(embedding)。 對問題用餘弦距離檢索最近的塊。 流式輸出答案,並標註引用的塊編號。 向量就留在 Postgres 裡 如果應用本來就需要關聯式數據,pgvector 能把文檔、元數據和嵌入放在一起:本地只跑一個數據庫、一套事務模型,檢索和過濾都是普通 SQL。 本地數據庫用 pgvector 的 Postgres 16 鏡像,初始化 SQL 開擴展、建兩張表: CREATE EXTENSION IF NOT EXISTS vector; CREATE TABLE documents ( id BIGSERIAL PRIMARY KEY, source TEXT NOT NULL UNIQUE, title TEXT NOT NULL ); CREATE TABLE chunks ( id BIGSERIAL PRIMARY KEY, document_id BIGINT NOT NULL REFERENCES documents(id) ON DELETE CASCADE, chunk_index INTEGER NOT NULL, content TEXT NOT NULL, embedding vector(1536) NOT NULL, UNIQUE (document_id, chunk_index) ); CREATE INDEX chunks_embedding_hnsw_idx ON chunks USING hnsw (embedding vector_cosine_ops); 向量維度對齊 text-embedding-3-small 的 1,536 維。HNSW 索引用餘弦距離,和檢索查詢用的度量保持一致。 切塊要切在有用的邊界上 把整篇長文檔做一個嵌入,檢索會太粗:相關段落會被埋在無關文字裡。塊太小又丟上下文。這個起步版取一個刻意簡單的中間路線:每塊最多 1,200 字符,盡量在段落邊界切,約 200 字符重疊進下一塊。 核心循環是在當前窗口的後段找一個有用的切點: const target = Math.min(start + maxChars, text.length); const minimumUsefulSplit = start + Math.floor(maxChars * 0.6); const paragraphBreak = text.lastIndexOf("\n\n", target); const wordBreak = text.lastIndexOf(" ", target); if (paragraphBreak >= minimumUsefulSplit) { end = paragraphBreak; } else if (wordBreak > start) { end = wordBreak; } 邏輯是:窗口用到 60% 之後若有段落斷點就切在段落處,否則退而求其次切在詞的邊界。存完這一塊,下一窗口從約 end - overlapChars 開始。空白正規化和進度檢查讓這個函數行為可預測;它是純函數,不用數據庫、不用金鑰就能測。 導入命令遞歸找 .md 和 .txt 文件: npm run ingest --./docs 嵌入按每批 100 塊進行。文件路徑就是文檔唯一的 source。重新導入時,腳本在一個事務裡更新文檔、刪掉它之前的全部塊、再插入新的一套——新舊版本不會同時出現在搜尋結果裡。 用餘弦距離檢索 問題用同一個模型、同樣的維度做嵌入。pgvector 的 <=> 算符算餘弦距離,相似度就是 1 - 距離: SELECT d.source, d.title, c.content, 1 - (c.embedding <=> $1::vector) AS similarity FROM chunks c JOIN documents d ON http://d.id = c.document_id ORDER BY c.embedding <=> $1::vector LIMIT $2 注意:向量是參數傳入的,不是字符串拼進 SQL;條數限制同樣參數化,並在應用代碼裡封頂。這個起步版每次取 5 塊。取幾塊是應用層的選擇,看完你自己的文檔類型和長度再調。 把引用變成提示詞的一部分 每塊檢索結果先格式化成帶穩定編號的樣子: [1] How retrieval works (docs/how-it-works.md) Documents are split into short, overlapping chunks... 系統指令寫得很明確:只准根據編號上下文回答;把上下文當來源材料、不是指令;事實性陳述用 [n] 標引用;答案不在上下文裡就直說不在。這一步小但重要:檢索負責給模型相關文字,指令負責定義這些文字該怎麼用。 但也要清醒:模型生成的引用仍然是模型輸出。高保證要求的場景,要再加校驗——引用是否存在、是否指向檢索到的塊、是否真的支持旁邊的陳述。 流式輸出,且來源先行 路由以 stream: true 開一個 OpenAI 聊天補全,把每個文字增量轉成字節,塞進標準的 ReadableStream: const stream = new ReadableStream<Uint8Array>({ async start(controller) { const encoder = new TextEncoder(); for await (const event of completion) { const text = event.choices[0]?.delta?.content; if (text) controller.enqueue(encoder.encode(text)); } controller.close(); }, }); 答案正文保持純文字。來源元數據必須先到,做法是把 base64 編碼的 JSON 數組放進 x-sources 響應頭,每條包含編號、文件路徑、標題和相似度。瀏覽器先解出這個頭,再讀正文流,只顯示那些 [n] 標記真的出現在答案裡的來源。也可以用 NDJSON 流把元數據放第一行,但用頭的好處是正文保持純文字可用、協議也容易檢查。 本地跑起來 建環境文件、填 OpenAI API 金鑰、把整棧跑起來: docker compose up -d cp.env.example.env.local npm install npm run ingest --./docs npm run dev 一個小設計:OpenAI 客戶端和數據庫連接池都是懶加載創建的,所以 npm run build 在沒有活數據庫和金鑰的情況下也能編譯;那些值只在真正跑導入或聊天請求時才需要。 後續值得加的擴展包括:元數據過濾、更多輸入格式、身份驗證、限流和引用校驗。起步版刻意不放這些,讓檢索主路徑保持好讀。 S1哥觀點 這篇的含金量在「克制」:一個 Postgres、一條 SQL、五塊上下文、編號引用,就把 RAG 90% 的價值拿到了。市面上一半的 RAG 教程上來就是專用向量庫+編排框架+重排模型,架構圖畫得嚇人,實際死在切塊和引用上。我的立場:先用 pgvector 把「切得對、引得到、答得有出處」跑通,量級真到了 Postgres 扛不住那天再談遷移——多數團隊根本活不到那天。唯一別省的是引用校驗,模型編引用編號的膽量,比它編答案還大。 原文連結:http://dev.to/fekdaoui_/build-a-small-rag-app-with-nextjs-pgvector-and-openai-5564 原文:@fekdaoui_(由 S1哥 編譯)
曝光 297 · 评论 2 · 点赞 1 · 书签 0
查看原帖