
你有沒有遇到過這種情況想做一道菜網上搜了一堆菜譜但每個菜譜都默認你有某種食材或調料而你想問的是“能不能不放花生”“有沒有替代豬肉的辦法”“宮保雞丁和魚香肉絲都用什么技法”這類需要把菜譜拆開、跨菜譜比較的問題。普通搜索返回的是整篇文章LLM直接回答又容易一本正經地胡說八道。我去年一直在琢磨怎么把菜譜變成可查詢、可推理的結構化知識最后做了這個“圖 RAG 烹飪問答系統”底層用 Neo4j 存菜譜知識圖譜用 Milvus 做語義向量召回再把兩者的結果組裝成上下文丟給 LLM 生成答案。這個項目已經跑通了一個可演示的版本思路也可以遷移到其他垂直領域的知識問答。這篇文章是完整的工程實踐記錄涉及數據建模、環(huán)境安裝、代碼實現和排坑過程適合正在做 RAG 應用、或者想了解怎么把圖數據庫和向量數據庫結合在一起使用的開發(fā)者參考。1. 為什么烹飪問答需要“圖向量”雙路召回1.1 菜譜天然是圖結構菜譜不是一段純文本它是一張關系網。一道菜包含多種食材、多種調料、多個步驟和技法食材之間有替代關系比如“花生”可以被“腰果”替代但“醬油”很難被“鹽”替代菜肴屬于某個菜系又和另一道菜共用同樣的技法。這種多對多的關系用關系型數據庫硬存也行但查詢“哪些菜不需要用油”“糖和蜂蜜能不能互換”就會寫出一大堆 JOIN而且越查越復雜。圖數據庫就是為這種關系網絡準備的Neo4j 里的節(jié)點和關系能直接表達“A 是 B 的替代品”“C 使用了 D 技法”查詢起來非常自然。早期我想用純向量數據庫做這個問答因為菜譜文本可以先向量化再檢索實現簡單。但很快發(fā)現一個問題向量檢索擅長的是“語義相似”它知道“宮保雞丁”和“雞丁炒花生米”在語義上有點像但它不知道雞丁和花生米之間是否存在某種依賴關系也無法推理“去掉花生米之后這道菜的結構還成立嗎”。圖數據庫恰恰補上了這塊短板——它能把菜譜中的實體和關系暴露給后續(xù)的生成步驟讓 LLM 不只是“讀到”一段文本而是“看到”一個可理解、可驗證的知識結構。1.2 純向量檢索的短板與圖 RAG 的互補邏輯我一開始只用了向量檢索效果怎么說呢問它“魚香肉絲有沒有不放豬肉的做法”它能搜到一些魚香肉絲菜譜但給出來的答案就是把菜譜原封不動貼出來沒有任何替代建議因為它根本沒檢索到“豬肉”這個實體在食材關系網中的位置。后來我在檢索結果里強行塞入實體關系效果立刻變好。這讓我意識到RAG 的上下文不一定是“一堆文本塊”還可以是“一個從知識圖譜里提取出來的子圖”。這種把圖結構作為上下文的一部分喂給 LLM 的做法就是所謂圖 RAG。具體來說這個系統的雙路召回邏輯是先用 Milvus 做語義粗篩從所有菜譜中找到與用戶問題最相關的 Top-K 個菜譜然后把這幾個候選菜譜在 Neo4j 里對應的節(jié)點撈出來沿著關系向外擴展一跳拿到關聯的食材、技法、替代關系最后把“候選菜譜文本 圖擴展出的關系三元組”合并成結構化的上下文交給 LLM 生成回答。向量負責“廣度”圖負責“深度”LLM 負責“組織語言和推理”。1.3 系統整體架構與數據流概覽整個系統由四個模塊組成菜譜數據處理、Neo4j 圖譜、Milvus 向量庫、LLM 服務編排。處理流程如下收集菜譜原始文本我用的是開源中文菜譜數據集大約 4000 道菜。用規(guī)則模板 簡單命名實體識別從菜譜中抽取出菜名、食材列表、調料列表、步驟、技法、口味標簽。將抽取結果節(jié)點化寫入 Neo4j菜譜節(jié)點、食材節(jié)點、技法節(jié)點、口味節(jié)點以及它們之間的關系。同時把菜譜的核心信息拼接成一段文本例如“菜名魚香肉絲食材豬里脊、木耳、胡蘿卜…技法炒口味魚香……”向量化后寫入 Milvus。用戶提問時先用 LLM 做一層查詢理解提取食材/菜名等實體然后用這些實體直接到 Neo4j 查相關圖譜同時把原始問題送入 Milvus 進行向量檢索。把圖譜查詢結果和向量召回結果合并構造 prompt調用 LLM 生成最終答案。流程圖我故意不畫文字描述可能更直觀。整個系統用 Python 編寫Neo4j 和 Milvus 都是獨立服務LLM 可以接 OpenAI 兼容接口也可以接本地部署的模型我后來為了省錢換成了 Ollama 跑的 Qwen。下面從建模開始逐層拆解。2. 知識圖譜建模用 Neo4j 把菜譜變成可以推理的圖2.1 菜譜數據獲取與清洗數據是項目的起點。我用了一個開源的中文菜譜數據集包含菜名、原料列表、步驟、菜系等字段。原始數據質量參差不齊最大的問題有三個同一食材有不同寫法“馬鈴薯”和“土豆”“花生米”和“花生仁”調料和食材混在一個字段里步驟是長文本很難直接拆出技法。我的處理思路是先做詞表歸一化把常見同義詞映射到一個標準名稱再根據一個預置的食材調料詞典把原料字段分割成結構化列表。這個詞典我維護了大概 800 個常見條目雖然不能覆蓋全部食材但覆蓋了訓練數據集里 90% 以上的情況。清洗后的數據格式大致是{ name: 魚香肉絲, cuisine: 川菜, ingredients: [豬里脊, 木耳, 胡蘿卜, 冬筍, 泡椒, 蔥姜蒜], seasonings: [醬油, 醋, 糖, 鹽, 淀粉], steps: [里脊切絲加鹽、淀粉腌制備用, 木耳、胡蘿卜、冬筍切絲, 熱鍋涼油下肉絲滑熟, 爆香泡椒、蔥姜蒜加入配菜翻炒, 調入醬油、醋、糖勾芡出鍋], techniques: [炒, 滑炒, 勾芡], flavors: [魚香, 酸甜, 微辣] }清洗這一步容易被忽略但它是整個項目的地基。如果食材沒有歸一化圖里的同一個食材可能會拆成好幾個節(jié)點替代關系就會斷裂。我后來特意加了一個合并步驟把 Neo4j 中同名的食材節(jié)點合并這樣關系才能聚攏。2.2 節(jié)點與關系設計Recipe、Ingredient、Technique、Flavor、替代關系知識圖譜沒有唯一正確的設計但有一個原則節(jié)點要代表“會被反復查詢的實體”關系要代表“會被反復提問的語義”。我的圖譜包含以下節(jié)點類型Recipe菜譜屬性有 name、cuisine、description。Ingredient食材/調料屬性有 name、category食材還是調料。Technique技法屬性有 name例如炒、蒸、炸、烤。Flavor口味標簽屬性有 name例如魚香、麻辣、清淡。DietaryTag膳食標簽屬性有 name例如素食、清真、無麩質。這個節(jié)點對替代推薦特別有用。關系類型及含義關系起點終點含義CONTAINSRecipeIngredient菜譜包含該食材USES_TECHNIQUERecipeTechnique菜譜使用了某種技法HAS_FLAVORRecipeFlavor菜譜具有某種口味CAN_SUBSTITUTEIngredientIngredient兩種食材在特定場景下可以互換RELATED_RECIPERecipeRecipe共用至少兩種食材的菜譜相似CAN_SUBSTITUTE關系是圖譜里最有價值的部分。我從兩個渠道構建一部分是人工整理的常見替代規(guī)則比如花生替代腰果、豬油替代植物油、白糖替代冰糖另一部分是從“共用食材和技法”的關系中推導候選替代關系再人工過濾。最終得到約 600 條替代邊不多但足夠覆蓋常見問題。2.3 Cypher 導入從 CSV 批量創(chuàng)建圖譜數據清洗完成后我用 Cypher 語句把結構化數據導入 Neo4j。這里不推薦逐條執(zhí)行 CREATE 語句數據量大時太慢。我直接導 CSV 文件。步驟如下把菜譜、食材、技法、口味分別導出為 CSV 文件。把菜譜-食材關系、菜譜-技法關系等導出為關系 CSV 文件。用LOAD CSV WITH HEADERS FROM file:///recipes.csv AS row逐行創(chuàng)建節(jié)點。為了加速創(chuàng)建關系先為節(jié)點創(chuàng)建唯一約束例如CREATE CONSTRAINT recipe_name_unique FOR (r:Recipe) REQUIRE r.name IS UNIQUE。核心導入語句示例// 創(chuàng)建食材節(jié)點 LOAD CSV WITH HEADERS FROM file:///ingredients.csv AS row MERGE (i:Ingredient {name: row.name}) SET i.category row.category; // 創(chuàng)建菜譜節(jié)點 LOAD CSV WITH HEADERS FROM file:///recipes.csv AS row MERGE (r:Recipe {name: row.name}) SET r.cuisine row.cuisine, r.description row.description; // 創(chuàng)建關系 LOAD CSV WITH HEADERS FROM file:///recipe_ingredient.csv AS row MATCH (r:Recipe {name: row.recipe_name}) MATCH (i:Ingredient {name: row.ingredient_name}) MERGE (r)-[:CONTAINS]-(i);這里有個注意點CSV 導入時如果數據里有中文逗號或換行符容易導致解析錯位。我的解決辦法是導出 CSV 時統一用\t作為分隔符然后在LOAD CSV里指定FIELDTERMINATOR \t。另外源數據里食材名稱可能帶前后空格導入前要清洗否則MATCH匹配不上。2.4 Neo4j 安裝與配置的坑Windows 桌面版、APOC 插件Neo4j 的安裝值得一提因為不同系統差別很大。我在 Windows 上用的是 Neo4j Desktop。下載安裝后需要創(chuàng)建一個本地數據庫設置密碼。常見問題有兩個一是初始密碼要求比較復雜容易記不住建議直接存到密碼管理器里二是導入 CSV 時提示“Couldnt load the external resource”這是因為默認導入目錄是數據庫的import文件夾不是任意路徑。把 CSV 文件放到數據庫目錄/import/下就能解決。另一個繞不開的坑是 APOC 插件。我原本想用apoc.load.json解析數據但 Neo4j Desktop 里啟用 APOC 需要手動把插件 jar 包放進plugins目錄并且在配置文件里開啟。不同 Neo4j 版本對 APOC 版本要求不同如果對不上啟動會報錯。我的建議是Noe4j 5.x 用對應的 APOC 5.x 版本不要圖新去官方 GitHub 的 Releases 頁面找匹配版本。如果你和我一樣只是簡單導入 CSV其實不用 APOC核心 Cypher 就能完成。所以 APOC 不是必需品遇到問題可以跳過。安好之后我推薦在瀏覽器打開http://localhost:7474用 Neo4j Browser 檢查圖譜。寫幾個 Cypher 查詢看看節(jié)點有沒有建立關系。我第一次導入后發(fā)現很多菜譜節(jié)點沒有連上食材排查后發(fā)現是食材名稱里多了一個空格用TRIM函數清理后就正常了。這種小問題很耗時間但踩過坑之后就好了。3. 語義向量庫用 Milvus 承載菜譜的語義檢索3.1 文本切分與向量化模型選擇圖譜負責確定性關系向量庫負責把菜譜文本的整體語義納入檢索。我的做法是把每道菜生成一段“菜譜摘要文本”格式為菜名魚香肉絲菜系川菜食材豬里脊、木耳、胡蘿卜、冬筍技法炒、滑炒、勾芡口味魚香、酸甜這段文本直接送入 embedding 模型生成向量。不需要做復雜的分塊因為每道菜的信息量不大一段文本足以表達核心語義。如果某道菜步驟特別多我可以把步驟也拼在后面但實驗下來對召回結果影響不大。模型方面我嘗試過text2vec-large-chinese和bge-large-zh-v1.5最終選了后者。原因是英文中文混合場景下 bge 更穩(wěn)且向量維度是 1024Milvus 支持起來沒有壓力。如果你用 OpenAI 的 embedding 接口也行但中文語義檢索效果未必比本地模型好而且有網絡和費用考量。3.2 Milvus 安裝非 Docker 方式與集合設計Milvus 是個分布式向量數據庫常規(guī)推薦用 Docker 啟動但如果你在 Windows 上不想裝 Docker其實也有辦法。Milvus Lite 是一個可以嵌入到 Python 應用中的輕量版本適合開發(fā)測試。不過我的項目用的是 Milvus 2.3 standalone 的 Windows 非 Docker 安裝方式這里必須說清楚官方本身不支持 Windows 直接運行 Milvus 服務但可以通過 WSL2 運行或者使用 Docker Desktop 的 Linux 容器。如果不用 Docker我的做法是在一臺固定機器上用 Linux 虛擬機裝 Milvus standalone然后本地 Python 通過局域網訪問。WSL2 的安裝方式相對簡單很多教程寫“Milvus Windows 安裝非 Docker”實際都是在 WSL2 里跑。你可以把下面的命令在 WSL2 的 Ubuntu 里執(zhí)行。# 下載 milvus standalone 安裝腳本 wget https://github.com/milvus-io/milvus/releases/download/v2.3.4/milvus-standalone-docker-compose.yml # 編輯 docker-compose.yml把映射端口改成自己需要的 # 啟動 docker compose up -d這本質上還是 Docker 容器但省掉了安裝 Docker Desktop 的步驟。如果你連 Docker 都不想要還有一個 Milvus Lite 方案pip install milvus-lite然后直接用默認本地存儲跑。我的建議是開發(fā)階段用 Milvus Lite部署階段再用真正的 Milvus 服務。兩者 API 幾乎一致切換成本很小。Milvus 集合設計比較簡單。我創(chuàng)建了一個recipe_vectors集合字段如下字段名類型說明idINT64主鍵我用自增整數recipe_nameVARCHAR(255)菜譜名稱textVARCHAR(2048)摘要文本embeddingFLOAT_VECTOR(1024)向量內容建集合時要注意索引參數。我用的是IVF_FLAT索引nlist128。查詢參數設nprobe16。說實話數據量只有幾千條用FLAT索引暴力搜索也很快但為了測試更大的數據集還是建了 IVF 索引。3.3 Attu 管理界面版本匹配問題Milvus 自帶命令行沒有可視化界面很多人會裝 Attu 這個 GUI 工具來查看數據。這里有個高頻坑Attu 和 Milvus 的版本必須匹配。比如 Milvus 2.3.x 的客戶端就不能用太老的 Attu 連接我一開始裝了 Attu 2.3 的早前版本連接時總是報錯“Unsupported media type”后來升級到 v2.4 才正常。建議安裝前先去 Attu 的 GitHub Releases 頁面看一下說明確認支持你的 Milvus 版本。另外連接地址要寫對如果 Milvus 跑在 WSL2 里你在 Windows 瀏覽器里訪問的是localhost:8000但在代碼里連接 Milvus 時要用 WSL2 的 IP通??梢酝ㄟ^ifconfig查到不能直接用 localhost否則連接會被拒絕。3.4 寫入與查詢向量——近鄰搜索參數我用pymilvus完成寫入和查詢。寫入的核心代碼from pymilvus import connections, Collection, FieldSchema, CollectionSchema, DataType connections.connect(aliasdefault, hostlocalhost, port19530) fields [ FieldSchema(nameid, dtypeDataType.INT64, is_primaryTrue), FieldSchema(namerecipe_name, dtypeDataType.VARCHAR, max_length255), FieldSchema(nametext, dtypeDataType.VARCHAR, max_length2048), FieldSchema(nameembedding, dtypeDataType.FLOAT_VECTOR, dim1024), ] schema CollectionSchema(fields, descriptionrecipe semantic vectors) collection Collection(recipe_vectors, schema) collection.create_index(embedding, {index_type: IVF_FLAT, metric_type: COSINE, params: {nlist: 128}})查詢時我把用戶問題向量化后調用collection.searchsearch_params {metric_type: COSINE, params: {nprobe: 16}} results collection.search( data[question_vector], anns_fieldembedding, paramsearch_params, limit5, output_fields[recipe_name, text] )這里有個小經驗距離度量我用COSINE而不是L2。因為文本向量的模長和菜譜長度有一定關系余弦相似度更能反映語義方向的一致。如果發(fā)現召回結果里混入一些奇怪菜譜可以調大nprobe或limit再結合圖譜過濾。4. 檢索增強編排怎么把圖和向量結果喂給 LLM4.1 兩階段召回向量粗篩 圖擴展精排回到最初的痛點用戶問“魚香肉絲有沒有不放豬肉的做法”如果只做向量召回系統會返回魚香肉絲的菜譜文本但不會返回“可以用雞胸肉替代豬里脊”的結論。必須讓圖數據庫參與進來。我的編排流程是先用 LLM 對問題進行實體識別提取出菜名和食材名。例如這個問題會提取出{ recipes: [魚香肉絲], ingredients: [豬肉] }。然后走兩路召回向量召回計算問題向量在 Milvus 里返回 Top-5 菜譜名比如[魚香肉絲, 青椒肉絲, 木須肉]。圖譜召回拿著實體去 Neo4j 查先找魚香肉絲節(jié)點再查它包含哪些食材重點查 “豬肉” 這個食材節(jié)點有哪些替代關系同時查共享“炒”技法或相近口味的其他菜譜。兩路召回結果取并集。如果向量召回中的菜譜在圖譜里不存在就以圖譜為準如果圖譜里有關系但向量沒召回也沒關系圖譜數據會直接進入上下文。4.2 上下文組裝如何形成 LLM 可讀的 JSON這是整個系統最巧妙的部分。LLM 不能直接讀圖所以我把圖查詢結果序列化成三元組列表并和向量召回的文本拼在一起。一個典型的上文結構如下{ question: 魚香肉絲有沒有不放豬肉的做法, vector_recall: [ {recipe_name: 魚香肉絲, text: 菜名魚香肉絲食材豬里脊、木耳、胡蘿卜...}, {recipe_name: 青椒肉絲, text: 菜名青椒肉絲食材豬里脊、青椒...} ], graph_facts: [ {type: contains, from: 魚香肉絲, to: 豬里脊}, {type: contains, from: 魚香肉絲, to: 木耳}, {type: can_substitute, from: 豬里脊, to: 雞胸肉, constraint: 口感略相似調味不變}, {type: can_substitute, from: 豬肉, to: 牛肉, constraint: 適合不喜歡豬肉脂肪的情況}, {type: technique, from: 魚香肉絲, to: 滑炒} ] }我把這些 JSON 直接拼到 prompt 里要求 LLM 優(yōu)先使用 graph_facts 中的關系做推理不要憑空編造食材替代。這樣做的好處是讓生成過程有據可循回答“能替代”或“不能替代”時能引用圖譜里的關系給出理由。4.3 調用 LLM 生成答案OpenAI 兼容接口 提示詞模板LLM 調用我用的是 OpenAI 兼容接口這樣可以方便地切換不同后端。下面的代碼演示了如何構造請求import json from openai import OpenAI client OpenAI(base_urlhttp://localhost:11434/v1, api_keynone) def generate_answer(context: dict) - str: sys_prompt 你是資深中餐大廚。請根據提供的食譜知識和食材關系用簡潔明確的語言回答問題。如果圖譜中給出了替代關系請直接引用。如果沒有明確的替代關系請說明食譜中沒有明確記錄替代方案。不要編造關系。 user_prompt f問題{context[question]}\n\n檢索信息\n{json.dumps(context, ensure_asciiFalse)}\n\n請根據以上信息回答。 resp client.chat.completions.create( modelqwen2.5:7b, messages[{role: system, content: sys_prompt}, {role: user, content: user_prompt}], temperature0.3 ) return resp.choices[0].message.content我后來把模型切成 Qwen 2.5 7B 跑在 Ollama 上回答質量雖然比 GPT-4 略遜色但速度夠快且離線可用。如果需要更好的推理能力可以換成更大的模型或 GPT-4 系列不過 prompt 里的 graph_facts 要寫得足夠清晰否則大模型也會忽略。4.4 一個完整問題的檢索鏈路代碼演示我把整個檢索和生成過程封裝成一個answer()函數def answer(question: str): # 1. 用LLM做實體識別 entities extract_entities(question) # 2. 向量召回 vec_docs vector_search(question, top_k5) # 3. 圖譜召回 graph_facts graph_search(entities) # 4. 合并上下文 context {question: question, vector_recall: vec_docs, graph_facts: graph_facts} # 5. 調用LLM生成答案 final generate_answer(context) return finalextract_entities也是調用 LLM用一個簡單的提示詞讓它輸出 JSON。vector_search就是上一節(jié)提到的 Milvussearch。graph_search則是幾個 Cypher 查詢的組合核心代碼// 根據菜譜名找食材 MATCH (r:Recipe {name: $recipe_name})-[:CONTAINS]-(i:Ingredient) RETURN i.name // 查食材的替代關系 MATCH (a:Ingredient {name: $ingredient_name})-[:CAN_SUBSTITUTE]-(b:Ingredient) RETURN a.name, b.name // 查共享食材的相似菜譜用共現食材數排序 MATCH (r:Recipe {name: $recipe_name})-[:CONTAINS]-(i:Ingredient)-[:CONTAINS]-(other:Recipe) WHERE other r WITH other, count(i) AS shared ORDER BY shared DESC LIMIT 3 RETURN other.name, shared這些查詢在 Python 中用neo4j驅動執(zhí)行返回的 Record 轉成字典列表。要點是一個問題可能涉及多個食材和菜譜因此每個實體都要查一次然后把結果合并注意去重。5. 實測效果與排坑記錄從“答非所問”到“懂行老饕”5.1 測試問題與檢索結果對比系統跑通后我做了幾組測試下面用表格列出典型問題和效果對比。問題純向量 RAG 的回答圖 向量 RAG 的回答魚香肉絲有沒有不放豬肉的做法給出了魚香肉絲的完整菜譜讓用戶自己做取舍直接指出可用雞胸肉或牛肉替代豬里脊并說明圖譜中有明確替代關系同時推薦了青椒肉絲作為類似選擇的參考宮保雞丁可以不放花生嗎介紹宮保雞丁的做法建議把花生去掉提示花生在宮保雞丁中主要提供酥脆口感如果沒有花生可以用腰果或杏仁替代并說明這是基于食材替代關系得出的結論哪些菜適合蒸返回了幾個包含“蒸”字的菜譜片段從圖譜中直接檢索到標簽為“蒸”的技法節(jié)點關聯的菜譜列表并額外推薦了“粉蒸肉”“蒸魚”等經典菜可以看出圖結構帶來的增量信息不是“另一個文本塊”而是推理路徑。LLM 的回答不再是羅列而是“判斷依據”。5.2 關鍵坑一實體對齊與同義詞擴展圖 RAG 最容易被忽視的坑是實體對齊。如果我提取出的食材是“豬里脊”而圖譜中的食材叫“里脊肉”查詢就匹配不到。我做了三層處理第一層在實體識別時提示 LLM 盡量輸出標準名稱第二層在 Neo4j 查詢時對每個實體名生成一個同義詞集合例如通過apoc.text.phonetic或自定義同義詞表第三層也是最簡單的在提取實體后先做一次模糊匹配用 Neo4j 的CONTAINS或 Levenshtein 相似度找到標準名。我實際用了近似匹配MATCH (i:Ingredient) WHERE i.name CONTAINS $keyword OR $keyword CONTAINS i.name RETURN DISTINCT i.name LIMIT 5但這會產生很多噪聲所以最終方案是維護一個同義詞映射字典例如{雞胸肉: [雞脯肉, 雞胸], 里脊肉: [豬里脊, 里脊]}。識別出的實體先查字典沒有的話再走模糊匹配。這個字典隨著測試中發(fā)現的漏配逐漸擴充。5.3 關鍵坑二Neo4j 連接池與并發(fā)查詢當查詢邏輯變復雜Neo4j 驅動的連接管理也會出問題。我之前每個請求都新創(chuàng)建一個GraphDatabase.driver()很快報“Too many open files”。正確做法是全局維護一個 driver 實例讓它內部管理連接池from neo4j import GraphDatabase driver GraphDatabase.driver(bolt://localhost:7687, auth(neo4j, password)) # 在應用啟動時初始化不要每次查詢都調用 driver() 構造器查詢時用driver.session()會自動從池中獲取連接用完釋放。但要注意如果某個查詢耗時較長應該將 session 內的事務盡量簡短。我把一個復雜的圖譜查詢拆成多個獨立小查詢用多線程并發(fā)執(zhí)行總耗時反而更低。5.4 關鍵坑三Milvus 無 Docker 安裝后的服務狀態(tài)管理我最初用 Milvus Lite 開發(fā)后面換到獨立的 Milvus 服務后遇到最折騰的問題是服務啟動依賴 etcd。Milvus standalone 的 Docker Compose 會同時啟動 etcd、minio 和 milvus 三個容器哪個掛了都會起不來。用docker compose logs milvus查看日志時最常見的錯誤是 etcd 連接超時。我的解決方式是在啟動 Milvus 之前先確認 etcd 容器健康或者在 docker-compose.yml 里增加健康檢查和 restart 策略。如果你在 Windows 下用 WSL2注意要把 WSL2 本機內存分配調大否則 Milvus 啟動時容易 OOM。還有一個跟部署相關的點pymilvus客戶端和服務端版本要匹配。我客戶端從 2.2 升到 2.3 后舊代碼里connections.connect沒變但返回的結果集類型變了output_fields字段名稱大小寫也略微不同。遇到問題后去 GitHub Issues 一搜發(fā)現很多人遇到同樣的問題。所以固定依賴版本非常重要別一有新版本就升級。6. 還能怎么玩圖 RAG 在其它領域的擴展思路烹飪問答只是圖 RAG 的一個切入點。這套“知識圖譜建立關系結構 向量庫建立語義索引 LLM 做生成推理”的組合可以遷移到很多場景。比如醫(yī)療領域的“用藥禁忌查詢”藥物之間的相互作用可以用圖關系表示癥狀描述和藥物說明書文本可以向量化問“高血壓患者能不能吃布洛芬”LLM 就能從圖中找到藥物節(jié)點之間的禁忌關系而不是靠猜。又比如工業(yè)維修問答設備故障代碼、零件依賴關系、歷史維修記錄正好是圖結構向量召回相關故障描述圖擴展出可能的原因和零件關系生成答案時自然帶上維修步驟。我在遷移到這個系統時最大的體會不是寫代碼而是想清楚“哪些知識必須用圖表達哪些知識用向量表達”。如果關系是顯式的、可枚舉的例如“A 替代 B”“A 屬于 B”放圖數據庫如果關系是模糊的、語義的例如“這個菜譜看起來像另一道菜”“這段描述和用戶問題相關”放向量數據庫。兩者不是替代關系而是分工。這套項目的代碼我放在 GitHub 上了有需要的朋友可以留言交流。最后分享一個小技巧測試圖 RAG 系統時不要只問你準備好的問題試著讓朋友隨便說一句“我想吃酸甜口的但沒有雞肉”看系統能不能正確處理“酸甜”口味和“雞肉”缺失這兩個約束。如果這個都能回答得像樣說明你的圖譜和提示詞工程真的過關了。