庫(kù)自建指南:Ollama embedding與SQLite向量檢索實(shí)踐)
1. 為什么我要折騰一個(gè)本地知識(shí)庫(kù)1.1 從信息焦慮到動(dòng)手自建我的筆記散落在四五個(gè)地方Obsidian 里有一批技術(shù)筆記Zotero 里躺著幾百篇論文的標(biāo)注瀏覽器書簽夾里塞滿了稍后讀微信收藏里還有一堆截圖和鏈接。每次想找某個(gè)具體知識(shí)點(diǎn)都得挨個(gè)翻一遍翻不到就只能重新搜。這種狀態(tài)持續(xù)了大半年我終于受不了了。市面上的云筆記和知識(shí)管理工具我基本都試過(guò)問(wèn)題集中在兩點(diǎn)一是數(shù)據(jù)不在自己手里二是搜索能力太弱——關(guān)鍵詞匹配根本理解不了我想找的是關(guān)于向量檢索性能優(yōu)化的內(nèi)容這種語(yǔ)義需求。所以我決定自己搭一套本地 embedding 向量檢索 每日自動(dòng)同步數(shù)據(jù)全部落在本地 SQLite 里用 Ollama 跑 embedding 模型用 Python 寫同步腳本Obsidian 作為前端展示層。這套方案解決的核心問(wèn)題是讓我的所有筆記和資料能被語(yǔ)義搜索而不是只能靠關(guān)鍵詞。比如我搜怎么讓模型記住上下文它能找出我寫的關(guān)于對(duì)話歷史管理和記憶機(jī)制的筆記哪怕里面根本沒(méi)出現(xiàn)上下文這三個(gè)字。適合誰(shuí)參考有一定 Python 基礎(chǔ)、愿意花一個(gè)周末折騰、對(duì)數(shù)據(jù)隱私和檢索質(zhì)量有要求的同學(xué)。如果你完全沒(méi)寫過(guò)代碼這篇文章也能讓你看懂整體思路但實(shí)操部分可能需要先補(bǔ)一下 Python 基礎(chǔ)。1.2 整體架構(gòu)長(zhǎng)什么樣先上一張我腦子里的架構(gòu)圖文字版數(shù)據(jù)源層Obsidian 倉(cāng)庫(kù)Markdown 文件、Zotero 導(dǎo)出的筆記、手動(dòng)整理的文本處理層Python 腳本負(fù)責(zé)讀取文件、切分文本、調(diào)用 Ollama 生成 embedding存儲(chǔ)層SQLite 數(shù)據(jù)庫(kù)存文本塊、向量、元數(shù)據(jù)來(lái)源、時(shí)間、標(biāo)簽檢索層Python 腳本接收查詢生成查詢向量在 SQLite 里做余弦相似度計(jì)算返回 Top-K 結(jié)果展示層Obsidian 插件或簡(jiǎn)單的本地 Web 頁(yè)面展示搜索結(jié)果調(diào)度層系統(tǒng)定時(shí)任務(wù)Linux 用 cronWindows 用任務(wù)計(jì)劃程序每天凌晨跑一次同步這個(gè)架構(gòu)的關(guān)鍵取舍是不引入向量數(shù)據(jù)庫(kù)。很多人第一反應(yīng)是上 Chroma、Milvus、Qdrant 這些專業(yè)向量庫(kù)但我實(shí)測(cè)下來(lái)個(gè)人知識(shí)庫(kù)的數(shù)據(jù)量級(jí)幾千到幾萬(wàn)條文本塊用 SQLite 存向量、Python 里做暴力檢索完全夠用。十萬(wàn)條數(shù)據(jù)以內(nèi)一次全量檢索在普通筆記本上也就幾百毫秒。引入向量數(shù)據(jù)庫(kù)反而增加了部署復(fù)雜度和維護(hù)成本對(duì)個(gè)人項(xiàng)目來(lái)說(shuō)是過(guò)度設(shè)計(jì)。另一個(gè)取舍是embedding 模型的選擇。我最終選了 Ollama 上的nomic-embed-text而不是 OpenAI 的 embedding API。原因很簡(jiǎn)單數(shù)據(jù)不出本地沒(méi)有調(diào)用費(fèi)用而且中文效果經(jīng)過(guò)實(shí)測(cè)可以接受。后面會(huì)詳細(xì)講模型選型的對(duì)比過(guò)程。2. 環(huán)境準(zhǔn)備與工具選型2.1 Ollama 安裝與模型拉取Ollama 是我這套方案里最核心的依賴負(fù)責(zé)跑 embedding 模型。安裝本身不復(fù)雜但國(guó)內(nèi)網(wǎng)絡(luò)環(huán)境下拉取模型是個(gè)大坑我踩了好幾次。安裝步驟Linux 為例# 官方安裝腳本 curl -fsSL https://ollama.com/install.sh | sh # 驗(yàn)證安裝 ollama --versionWindows 和 macOS 直接去官網(wǎng)下載安裝包即可。安裝完成后Ollama 默認(rèn)會(huì)把模型存在系統(tǒng)盤的用戶目錄下。如果你的系統(tǒng)盤空間緊張embedding 模型雖然不大但多拉幾個(gè)也占地方可以修改模型存儲(chǔ)路徑# Linux/macOS設(shè)置環(huán)境變量 export OLLAMA_MODELS/data/ollama/models # 寫入 shell 配置文件使其永久生效 echo export OLLAMA_MODELS/data/ollama/models ~/.bashrc source ~/.bashrcWindows 下則在系統(tǒng)環(huán)境變量里新增OLLAMA_MODELS指向你想要的目錄然后重啟 Ollama 服務(wù)。拉取 embedding 模型ollama pull nomic-embed-text這里就是第一個(gè)大坑ollama 下載太慢了。我試過(guò)好幾次進(jìn)度條卡在某個(gè)百分比不動(dòng)等半小時(shí)都沒(méi)反應(yīng)。后來(lái)總結(jié)出幾個(gè)應(yīng)對(duì)方法換時(shí)間段凌晨或清晨拉取速度明顯好于晚上配置鏡像源部分社區(qū)維護(hù)的鏡像可以加速具體地址會(huì)變動(dòng)建議自行搜索最新可用的手動(dòng)下載模型文件如果實(shí)在拉不動(dòng)可以找離線安裝包把模型文件放到OLLAMA_MODELS目錄下對(duì)應(yīng)的文件夾里注意手動(dòng)放置模型文件時(shí)目錄結(jié)構(gòu)要和 Ollama 預(yù)期的保持一致否則服務(wù)啟動(dòng)后識(shí)別不到。建議先ollama pull一個(gè)小模型比如all-minilm看看它落在哪個(gè)目錄、目錄結(jié)構(gòu)是什么樣再照著放。2.2 embedding 模型怎么選熱詞里embedding模型排行是個(gè)高頻搜索我實(shí)際對(duì)比過(guò)幾個(gè)能在 Ollama 上跑的模型模型維度中文效果速度體積我的評(píng)價(jià)nomic-embed-text768良好快~274MB綜合最優(yōu)首選all-minilm384一般極快~46MB英文場(chǎng)景夠用中文偏弱mxbai-embed-large1024良好中等~670MB效果略好但資源占用高bge-m31024優(yōu)秀較慢~1.2GB中文最強(qiáng)但吃資源我的選擇邏輯是個(gè)人知識(shí)庫(kù)以中文為主但不需要極致效果速度和資源占用更重要。nomic-embed-text在中文語(yǔ)義檢索上表現(xiàn)穩(wěn)定768 維的向量存 SQLite 也不占太多空間。如果你主要處理英文資料all-minilm完全夠用速度快到飛起。如果你對(duì)中文檢索質(zhì)量要求極高、機(jī)器配置也好可以上bge-m3。這里有個(gè)經(jīng)驗(yàn)不要盲目追求排行榜第一的模型。排行榜上的評(píng)測(cè)集和你的實(shí)際數(shù)據(jù)分布可能差很遠(yuǎn)而且大模型意味著每次同步都要花更多時(shí)間。我建議先用nomic-embed-text跑起來(lái)覺(jué)得效果不夠再換。2.3 Python 環(huán)境與依賴Python 版本建議 3.10 以上我用的是 3.11。依賴不多核心就幾個(gè)pip install requests numpyrequests用來(lái)調(diào) Ollama 的 HTTP APInumpy用來(lái)做向量運(yùn)算。如果你不想用 numpy純 Python 列表也能算余弦相似度但數(shù)據(jù)量大了會(huì)慢很多建議還是裝上。提示如果你在 Windows 上遇到pip安裝慢的問(wèn)題可以配置國(guó)內(nèi)鏡像源這個(gè)網(wǎng)上教程很多不展開(kāi)。2.4 SQLite 管理工具SQLite 本身是 Python 內(nèi)置的不需要額外安裝。但你需要一個(gè)工具來(lái)查看和調(diào)試數(shù)據(jù)庫(kù)內(nèi)容。我推薦DB Browser for SQLite簡(jiǎn)稱 DB4S開(kāi)源跨平臺(tái)界面直觀能直接執(zhí)行 SQL、查看表結(jié)構(gòu)、導(dǎo)出數(shù)據(jù)。熱詞里提到的 db browser for sqlite 和 db4s 就是它。下載安裝后直接打開(kāi)你的.db文件就能看到所有表和數(shù)據(jù)。調(diào)試階段我?guī)缀趺刻於家盟匆谎巯蛄坑袥](méi)有正確寫入、文本塊切分是否合理。3. 核心實(shí)現(xiàn)從文件到向量3.1 數(shù)據(jù)庫(kù)表結(jié)構(gòu)設(shè)計(jì)先建表。我的設(shè)計(jì)比較簡(jiǎn)單兩張表一張存文本塊和向量一張存文件同步狀態(tài)。CREATE TABLE IF NOT EXISTS chunks ( id INTEGER PRIMARY KEY AUTOINCREMENT, source_file TEXT NOT NULL, chunk_index INTEGER NOT NULL, content TEXT NOT NULL, embedding BLOB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE IF NOT EXISTS file_sync ( file_path TEXT PRIMARY KEY, last_modified REAL NOT NULL, last_synced TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE INDEX IF NOT EXISTS idx_source_file ON chunks(source_file);幾個(gè)設(shè)計(jì)要點(diǎn)向量存成 BLOB。numpy 數(shù)組可以轉(zhuǎn)成 bytes 存進(jìn) SQLite 的 BLOB 字段讀取時(shí)再轉(zhuǎn)回來(lái)。這樣比存 JSON 字符串省空間讀寫也快。import numpy as np def vector_to_blob(vec): return np.array(vec, dtypenp.float32).tobytes() def blob_to_vector(blob): return np.frombuffer(blob, dtypenp.float32)file_sync 表用來(lái)做增量同步。每次同步前先查這個(gè)文件有沒(méi)有變過(guò)對(duì)比last_modified沒(méi)變就跳過(guò)避免重復(fù)計(jì)算 embedding。這個(gè)優(yōu)化在文件多的時(shí)候效果非常明顯。chunk_index 記錄文本塊在原文件中的順序檢索到結(jié)果后可以按文件聚合展示時(shí)更有上下文。3.2 文本切分策略文本切分是整套系統(tǒng)里最容易被忽視、但影響最大的環(huán)節(jié)。切得太碎語(yǔ)義不完整切得太大檢索精度下降。我的策略是按段落切分帶重疊def split_text(text, chunk_size500, overlap100): paragraphs text.split(\n\n) chunks [] current for para in paragraphs: if len(current) len(para) chunk_size: current para \n\n else: if current: chunks.append(current.strip()) # 處理超長(zhǎng)段落 if len(para) chunk_size: for i in range(0, len(para), chunk_size - overlap): chunks.append(para[i:i chunk_size]) current else: current para \n\n if current: chunks.append(current.strip()) return chunkschunk_size500是我反復(fù)調(diào)整后的值。中文 500 字大約對(duì)應(yīng) 300-400 個(gè) token正好在 embedding 模型的最佳輸入范圍內(nèi)。overlap100是為了避免關(guān)鍵信息剛好被切在邊界上導(dǎo)致語(yǔ)義丟失。實(shí)操心得Markdown 文件里的代碼塊、表格、標(biāo)題這些結(jié)構(gòu)切分時(shí)最好單獨(dú)處理。我一開(kāi)始沒(méi)管結(jié)果代碼塊被從中間切開(kāi)檢索出來(lái)的片段完全沒(méi)法看。后來(lái)加了個(gè)判斷遇到 包裹的代碼塊就整體保留不參與切分。3.3 調(diào)用 Ollama 生成 embeddingOllama 提供了 HTTP API默認(rèn)監(jiān)聽(tīng)11434端口。生成 embedding 的接口是/api/embeddingsimport requests def get_embedding(text, modelnomic-embed-text): resp requests.post( http://localhost:11434/api/embeddings, json{model: model, prompt: text}, timeout60 ) resp.raise_for_status() return resp.json()[embedding]這里有個(gè)性能問(wèn)題逐條調(diào)用 API 很慢。我的知識(shí)庫(kù)有幾千個(gè)文本塊逐條調(diào)要跑十幾分鐘。優(yōu)化方法是批量處理但 Ollama 的 embeddings 接口一次只接受一個(gè) prompt。我的做法是用多線程并發(fā)調(diào)用from concurrent.futures import ThreadPoolExecutor def batch_embed(texts, modelnomic-embed-text, workers4): with ThreadPoolExecutor(max_workersworkers) as executor: results list(executor.map(lambda t: get_embedding(t, model), texts)) return resultsworkers4是我測(cè)試下來(lái)比較穩(wěn)的并發(fā)數(shù)。再高容易把 Ollama 服務(wù)壓垮反而變慢。這個(gè)值取決于你的機(jī)器配置建議從 2 開(kāi)始試。注意Ollama 默認(rèn)只加載一個(gè)模型實(shí)例并發(fā)請(qǐng)求會(huì)排隊(duì)。如果你發(fā)現(xiàn)并發(fā)沒(méi)效果可能是 Ollama 的OLLAMA_NUM_PARALLEL環(huán)境變量沒(méi)設(shè)置??梢栽O(shè)為 4 試試但會(huì)占用更多內(nèi)存。3.4 增量同步邏輯完整的同步流程是這樣的import os import sqlite3 from pathlib import Path def sync_vault(vault_path, db_path): conn sqlite3.connect(db_path) cursor conn.cursor() for md_file in Path(vault_path).rglob(*.md): mtime os.path.getmtime(md_file) cursor.execute( SELECT last_modified FROM file_sync WHERE file_path ?, (str(md_file),) ) row cursor.fetchone() if row and abs(row[0] - mtime) 1: continue # 文件沒(méi)變跳過(guò) # 文件變了刪除舊數(shù)據(jù) cursor.execute(DELETE FROM chunks WHERE source_file ?, (str(md_file),)) # 讀取、切分、生成 embedding content md_file.read_text(encodingutf-8) chunks split_text(content) embeddings batch_embed(chunks) for idx, (chunk, emb) in enumerate(zip(chunks, embeddings)): cursor.execute( INSERT INTO chunks (source_file, chunk_index, content, embedding) VALUES (?, ?, ?, ?), (str(md_file), idx, chunk, vector_to_blob(emb)) ) cursor.execute( INSERT OR REPLACE INTO file_sync (file_path, last_modified) VALUES (?, ?), (str(md_file), mtime) ) conn.commit() conn.close()這個(gè)邏輯的關(guān)鍵是先刪后插。文件變了就把這個(gè)文件的所有舊 chunk 刪掉重新生成避免殘留過(guò)期數(shù)據(jù)。雖然有點(diǎn)粗暴但對(duì)個(gè)人知識(shí)庫(kù)來(lái)說(shuō)完全夠用而且邏輯簡(jiǎn)單不容易出錯(cuò)。4. 檢索與展示4.1 余弦相似度檢索檢索的核心就是算余弦相似度。SQLite 本身不支持向量運(yùn)算所以我把所有向量讀到內(nèi)存里用 numpy 算def search(query, db_path, top_k10): query_vec np.array(get_embedding(query), dtypenp.float32) conn sqlite3.connect(db_path) cursor conn.cursor() cursor.execute(SELECT id, source_file, content, embedding FROM chunks) rows cursor.fetchall() conn.close() results [] for row in rows: vec blob_to_vector(row[3]) # 余弦相似度 similarity np.dot(query_vec, vec) / (np.linalg.norm(query_vec) * np.linalg.norm(vec)) results.append((similarity, row[0], row[1], row[2])) results.sort(keylambda x: x[0], reverseTrue) return results[:top_k]這個(gè)實(shí)現(xiàn)是全量暴力檢索。十萬(wàn)條數(shù)據(jù)以內(nèi)一次檢索大概 200-500 毫秒完全可以接受。如果你數(shù)據(jù)量更大可以考慮用sqlite-vec擴(kuò)展或者換向量數(shù)據(jù)庫(kù)但個(gè)人知識(shí)庫(kù)基本到不了那個(gè)量級(jí)。實(shí)操心得numpy 的np.dot對(duì) float32 數(shù)組有優(yōu)化比純 Python 循環(huán)快幾十倍。另外如果向量已經(jīng)歸一化過(guò)余弦相似度就退化成點(diǎn)積可以省掉除法。Ollama 返回的 embedding 不一定是歸一化的但你可以自己歸一化后存進(jìn)去檢索時(shí)直接點(diǎn)積。4.2 在 Obsidian 里展示結(jié)果檢索腳本跑通后展示層我試過(guò)兩種方案方案一Python 腳本輸出 Markdown 文件。檢索結(jié)果寫成一個(gè).md文件放到 Obsidian 倉(cāng)庫(kù)里用 Obsidian 打開(kāi)看。簡(jiǎn)單粗暴但每次都要手動(dòng)跑腳本。方案二Obsidian 插件調(diào)用本地 API。寫一個(gè)簡(jiǎn)單的 HTTP 服務(wù)包裝檢索邏輯Obsidian 插件發(fā)請(qǐng)求拿結(jié)果。這個(gè)體驗(yàn)最好但需要寫插件門檻高一些。我目前用的是方案一的變體寫了個(gè)search.py命令行傳查詢?cè)~結(jié)果直接打印到終端同時(shí)生成一個(gè)臨時(shí) Markdown 文件。夠用不折騰。python search.py 向量檢索性能優(yōu)化輸出格式[0.87] /vault/notes/embedding-optimization.md 向量檢索的性能瓶頸主要在距離計(jì)算... [0.82] /vault/notes/rag-pipeline.md 檢索階段如果用暴力搜索十萬(wàn)條數(shù)據(jù)...4.3 每日自動(dòng)同步自動(dòng)同步用系統(tǒng)定時(shí)任務(wù)。Linux 下編輯 crontabcrontab -e加入一行每天凌晨 3 點(diǎn)跑同步0 3 * * * /usr/bin/python3 /path/to/sync.py /var/log/kb-sync.log 21Windows 下用任務(wù)計(jì)劃程序創(chuàng)建一個(gè)每天觸發(fā)的任務(wù)操作選啟動(dòng)程序程序填python.exe的路徑參數(shù)填sync.py的路徑。注意定時(shí)任務(wù)里的 Python 路徑一定要寫絕對(duì)路徑環(huán)境變量可能和你在終端里不一樣。我踩過(guò)這個(gè)坑腳本手動(dòng)跑沒(méi)問(wèn)題定時(shí)任務(wù)就是跑不起來(lái)查了半天發(fā)現(xiàn)是python3找不到。5. 踩坑記錄與排查技巧5.1 Ollama 相關(guān)坑坑一模型下載卡住不動(dòng)。前面提過(guò)換時(shí)間段或找離線包。還有一個(gè)隱藏問(wèn)題Ollama 下載是斷點(diǎn)續(xù)傳的如果卡住了可以 CtrlC 中斷再重新 pull它會(huì)從斷點(diǎn)繼續(xù)不用從頭下。坑二Ollama 服務(wù)啟動(dòng)失敗。常見(jiàn)原因是端口被占用。11434端口如果被別的程序占了Ollama 起不來(lái)。用lsof -i :11434Linux/macOS或netstat -ano | findstr 11434Windows查一下把占用進(jìn)程干掉或者改 Ollama 的監(jiān)聽(tīng)端口??尤齟mbedding 結(jié)果為空。有時(shí)候 API 返回 200 但embedding字段是空的。這通常是因?yàn)檩斎胛谋咎L(zhǎng)超過(guò)了模型的最大輸入長(zhǎng)度。nomic-embed-text的最大輸入是 8192 token一般不會(huì)超但如果你切分邏輯有 bug 傳了個(gè)超長(zhǎng)文本進(jìn)去就會(huì)這樣。加個(gè)長(zhǎng)度檢查if len(text) 6000: text text[:6000]坑四Ollama 占用內(nèi)存越來(lái)越高。長(zhǎng)時(shí)間跑批量 embeddingOllama 的內(nèi)存占用會(huì)漲。我的做法是每處理 500 個(gè) chunk 就重啟一次 Ollama 服務(wù)或者設(shè)置OLLAMA_MAX_LOADED_MODELS1限制同時(shí)加載的模型數(shù)。5.2 SQLite 相關(guān)坑坑一并發(fā)寫入鎖。SQLite 默認(rèn)是寫鎖同一時(shí)間只能一個(gè)連接寫。如果你一邊跑同步一邊跑檢索可能會(huì)遇到database is locked。解決辦法是同步和檢索錯(cuò)開(kāi)時(shí)間或者用 WAL 模式conn.execute(PRAGMA journal_modeWAL)WAL 模式下讀寫可以并發(fā)對(duì)個(gè)人知識(shí)庫(kù)這種讀多寫少的場(chǎng)景很合適??佣﨎LOB 字段讀取報(bào)錯(cuò)。numpy 的frombuffer要求 bytes 長(zhǎng)度是 4 的倍數(shù)float32 是 4 字節(jié)。如果存的時(shí)候不是 float32 或者長(zhǎng)度不對(duì)讀出來(lái)就會(huì)報(bào)錯(cuò)。統(tǒng)一用np.float32存讀的時(shí)候也指定dtypenp.float32??尤龜?shù)據(jù)庫(kù)文件越來(lái)越大。刪除了 chunk 之后SQLite 文件不會(huì)自動(dòng)縮小。需要手動(dòng)執(zhí)行VACUUMVACUUM;我一般每個(gè)月跑一次能把文件縮小不少。5.3 檢索質(zhì)量相關(guān)坑坑一搜出來(lái)的結(jié)果不相關(guān)。最常見(jiàn)的原因是切分太碎一個(gè) chunk 里沒(méi)有完整語(yǔ)義。調(diào)大chunk_size或者優(yōu)化切分邏輯。另一個(gè)原因是 embedding 模型不適合你的數(shù)據(jù)換模型試試??佣嗨贫确?jǐn)?shù)都很低。如果所有結(jié)果的相似度都在 0.3 以下說(shuō)明查詢和文檔的語(yǔ)義空間對(duì)不齊。檢查一下查詢和文檔是不是用了同一個(gè)模型生成 embedding——必須用同一個(gè)模型不同模型的向量空間不通用??尤形臋z索效果差。nomic-embed-text的中文能力中等如果效果不滿意換bge-m3。另外查詢時(shí)可以在前面加個(gè)指令前綴比如為這個(gè)句子生成表示以用于檢索相關(guān)文章有些模型對(duì)指令前綴敏感能提升效果。5.4 常見(jiàn)問(wèn)題速查表問(wèn)題現(xiàn)象可能原因解決方法Ollama 下載卡住網(wǎng)絡(luò)問(wèn)題換時(shí)間段、用離線包、斷點(diǎn)續(xù)傳embedding 返回空輸入超長(zhǎng)截?cái)辔谋镜?6000 字符以內(nèi)database is locked并發(fā)讀寫沖突開(kāi)啟 WAL 模式、錯(cuò)開(kāi)同步和檢索時(shí)間檢索結(jié)果不相關(guān)切分太碎/模型不匹配調(diào)大 chunk_size、確認(rèn)查詢和文檔同模型定時(shí)任務(wù)不執(zhí)行路徑或環(huán)境問(wèn)題用絕對(duì)路徑、檢查日志數(shù)據(jù)庫(kù)文件過(guò)大刪除后未回收空間定期執(zhí)行 VACUUM同步速度慢逐條調(diào)用 API多線程并發(fā)、增量同步跳過(guò)未變文件6. 后續(xù)可以怎么擴(kuò)展這套系統(tǒng)跑了一個(gè)多月基本滿足我的需求。后面我打算做幾個(gè)擴(kuò)展接入更多數(shù)據(jù)源。目前只同步了 Obsidian 倉(cāng)庫(kù)下一步想把 Zotero 的筆記也導(dǎo)進(jìn)來(lái)。Zotero 可以導(dǎo)出 Markdown放到一個(gè)固定目錄同步腳本加個(gè)路徑就行。熱詞里如何將zotero的筆記導(dǎo)入obsidian是個(gè)高頻問(wèn)題其實(shí)用 Zotero 的 Better BibTeX 插件導(dǎo)出 Markdown 再放進(jìn) Obsidian 倉(cāng)庫(kù)是最順滑的方案。加個(gè)簡(jiǎn)單的 Web 界面。用 Flask 或 FastAPI 包一層瀏覽器里直接搜比命令行方便。這個(gè)不難幾十行代碼的事。檢索結(jié)果重排序。先用向量檢索召回 Top-50再用一個(gè)小的交叉編碼器模型重排序精度能提升不少。不過(guò)這會(huì)增加復(fù)雜度和延遲看需求決定要不要上。多模態(tài)擴(kuò)展。圖片、PDF 里的文字也可以提取出來(lái)做 embedding不過(guò)這就復(fù)雜了暫時(shí)不折騰。我個(gè)人在實(shí)際操作中的體會(huì)是這套方案最大的價(jià)值不是技術(shù)本身而是它逼著我把散落各處的筆記整理到了一起。以前筆記到處放現(xiàn)在有了統(tǒng)一的同步流程反而養(yǎng)成了隨手記、定期整理的習(xí)慣。技術(shù)是手段知識(shí)管理才是目的。如果你也在糾結(jié)要不要自建知識(shí)庫(kù)我的建議是先用最簡(jiǎn)單的方案跑起來(lái)哪怕只是 Obsidian 加個(gè)全文搜索插件也比什么都不做強(qiáng)。等真的覺(jué)得不夠用了再逐步加 embedding、加自動(dòng)同步一步步來(lái)別一上來(lái)就追求完美架構(gòu)。