全部產品
Search
文件中心

Vector Retrieval Service for Milvus:通過阿里雲Milvus知識庫搭建企業制度問答應用

更新時間:Aug 28, 2026

阿里雲 Milvus 知識庫可以把分散在各部門的制度、流程與 SOP 建成統一的內部問答入口,員工用自然語言提問即可拿到流程步驟與原文依據,並通過部門標籤把檢索範圍限制在指定範圍內。

方案說明

整體鏈路與通過阿里雲Milvus知識庫搭建智能客服問答應用完全一致:控制台定義標籤 → 按標籤大量匯入資料 → 發布版本 → SDK 檢索(可按標籤過濾)→ 大模型產生帶來源標註的回答 → Flask 提供問答頁面。工程代碼(kb_client.py、upload.py、app.py、templates/index.html、start.sh)請直接複用該文,本文只說明制度情境需要改動的部分:部門標籤體系、檢索參數、制度助手提示詞,以及制度版本管理。

建議第一版只選擇一個部門的 5~20 篇資料,驗證後再擴大範圍。整體耗時約 20~30 分鐘。

前提條件

  • 已建立知識庫並記錄知識庫 ID(形如 kd-803ae9b10cc31)。

  • 已使用主帳號建立 RAM 使用者、勾選使用永久 AccessKey 訪問,並授予系統策略 AliyunMilvusFullAccess。

  • 已準備支援 OpenAI chat/completions 協議的大模型地址與 API Key。

  • 本地已安裝 Python 3.8 或以上版本,並已按上文教程建好工程目錄。

步驟一:定義部門與制度標籤

在知識庫詳情頁的基本資料下方單擊標籤 > 管理,添加以下三個標籤(類型均為 string):

標籤名

說明

樣本值

department

制度歸屬部門

財務部、行政部、IT部

docType

資料類型

制度、流程

effectiveDate

生效日期

2026-01-01

標籤值支援中文,可直接使用部門與制度類型的中文名稱。

重要

標籤管理對話方塊是整體儲存的。對話方塊開啟後標籤列表為非同步載入,請等到已有標籤全部顯示出來,再添加新標籤並單擊完成;否則可能以"空列表 + 新標籤"整體覆蓋,導致已有標籤定義被清除。已寫入資料的標籤值不會因此丟失,但建議補加標籤後核對詳情頁的標籤數量。

說明

標籤定義主要用於統一取值口徑。未定義的標籤名也可以隨資料寫入並用於過濾,但仍建議先定義再匯入,便於團隊協作與後續維護。

步驟二:整理制度資料與匯入清單

  1. 按部門整理資料,放入 documents/ 目錄。檔案名稱建議包含制度名稱與版本或生效日期,便於在回答的來源區中識別。

  2. 建立 documents.jsonl,為每篇資料標註部門、類型與生效日期。

    {"path": "documents/finance-travel.md", "metadata": {"department": "財務部", "docType": "制度", "effectiveDate": "2026-01-01"}}
    {"path": "documents/finance-reimbursement.md", "metadata": {"department": "財務部", "docType": "流程", "effectiveDate": "2026-02-01"}}
    {"path": "documents/hr-leave.md", "metadata": {"department": "行政部", "docType": "制度", "effectiveDate": "2026-01-01"}}
    {"path": "documents/it-troubleshoot.md", "metadata": {"department": "IT部", "docType": "流程", "effectiveDate": "2026-03-01"}}
    說明

    制度類資料建議在本文開頭寫明生效日期與制度負責人,並在舊版本本文中標註"已被 X 版替代"。大模型預設只能看到檢索片段的本文內容,effectiveDate 標籤不會自動進入提示詞,需要按步驟四改寫後才會帶上。

  3. 制度資料多為短條目,可在知識庫詳情頁的處理策略中單擊建立策略調整切片粒度。最大分段長度的單位是字元(預設 512),建議設定為 580~770 字元(約相當於 384~512 tokens),盡量讓一個流程步驟保持完整。

步驟三:配置檢索參數與提示詞

在 config.json 中按制度情境調整 retrieval 與 scenario。

{
  "retrieval": {
    "page_size": 6,
    "candidate_count": 48,
    "min_score": 0.35,
    "semantic_weight": 0.6,
    "enable_query_expansion": true,
    "rerank_model_name": "",
    "tag_filter": {
      "relation": "and",
      "conditions": []
    }
  },
  "scenario": {
    "title": "企業制度與流程問答",
    "system_prompt": "你是企業內部制度助手。只根據檢索到的發行制度回答;把流程整理成步驟,註明適用條件和所需材料,並標註[來源N];資料衝突或不足時明確提示聯絡制度負責人。",
    "image_enabled": false,
    "sample_questions": [
      "差旅報銷需要提交哪些材料?",
      "請假超過三天需要誰審批?",
      "電腦無法連網時應該走什麼報障流程?"
    ]
  }
}

參數說明:

  • min_score:沒有跨語料通用的推薦值,必須用真實問題按自己的語料校準。推薦做法是先設 min_score=0 取回一批結果並人工標註相關性,再按召回與誤召回的分布選閾值;更換語料、切換重排模型或調整 semantic_weight 後都需要重新校準。本文語料實測:取 0.2 時與問題完全不相關的制度(0.36~0.43 分)也會進入大模型上下文,既增加開銷也提高誤答風險;調到 0.35 後這些結果被過濾掉。

  • semantic_weight=0.6:制度提問常混合專有名詞與口語表達,語義與關鍵詞兼顧。

    該參數是最終分數的加權係數,計算方式為 score ≈ (1-semantic_weight) × keywordScore + semantic_weight × semanticScore(另可能疊加 rank feature),min_score 在這個最終分數上做後置過濾。

    未啟用重排時 semanticScore 是向量相似性,啟用重排後是重排模型分數,兩者量綱不同,因此調整權重或開關重排後必須結合 scoreDetails 重新校準閾值;調低權重並不必然壓低總分,只有當本批 semanticScore 高於 keywordScore 時才會下降。

  • tag_filter.conditions 預設留空,即檢索全部部門。僅當希望把某個入口限定在單一部門時才配置過濾條件,詳見步驟五。

重要

實際可用的運算子只有 =、in、not in 三個。其餘運算子雖然出現在 400 Unsupported tag filter operator 報錯列出的 "Supported operators" 清單中,但實測傳入後條件會被靜默忽略、返回未經過濾的全部資料。其中 contains、not contains 只是集合運算 in、not in 的別名,並不是字串包含,按字串片段傳值只會返回 0 條。因此:需要按數值或日期區間篩選時請改用 in 枚舉取值;需要判斷標籤為空白時請改用 = "";配置好過濾條件後,務必與不加過濾的結果對比總條數,確認過濾確實生效。另外,如果 field 寫成未定義的標籤名,介面同樣不報錯,只會返回 0 條;把 op 寫成 eq、==、equal、like 等別名會返回 400 Unsupported tag filter operator。

步驟四:讓回答帶上部門與生效日期

制度問答需要判斷"哪一版有效、適用於哪個部門",因此要把標籤值一併拼進大模型上下文。檢索結果中的 tags 欄位不會返回你寫入的標籤值:該欄位取自服務內部的 chunk 標籤增強特徵,與 AddDocuments 寫入的 MetaFields 是兩個不同的欄位,也沒有請求參數可以開啟返回,因此它為空白不代表上傳失敗或標籤丟失。標籤值需要在用戶端自我維護映射:按檢索結果的 documentId 關聯本地匯入清單或業務側中繼資料後,再拼進大模型上下文。

在 app.py 中 app = Flask(__name__) 之後加入映射:

META_BY_NAME: dict[str, dict[str, Any]] = {}
_manifest = Path("documents.jsonl")
if _manifest.is_file():
    for _line in _manifest.read_text(encoding="utf-8").splitlines():
        if _line.strip():
            _entry = json.loads(_line)
            META_BY_NAME[Path(str(_entry["path"])).name] = _entry.get("metadata") or {}

再改寫 llm_answer(),在拼接上下文時帶上標籤值,並在提問中告知當前日期:

def llm_answer(question: str, results: list[dict[str, Any]]) -> str:
    if not LLM.get("enabled", True):
        return "大模型未啟用;請查看下方檢索結果。"
    if not results:
        return "現有制度資料中沒有相關規定,請聯絡對應制度負責人確認。"
    blocks = []
    for index, item in enumerate(results, 1):
        title = field(item, "documentName", "DocumentName")
        meta = META_BY_NAME.get(title) or {}
        label = "、".join(f"{key}={value}" for key, value in meta.items())
        header = f"[來源{index}] {title}" + (f"({label})" if label else "")
        blocks.append(f"{header}\n{field(item, 'content', 'Content')}")
    context = "\n\n".join(blocks)
    today = datetime.date.today().isoformat()
    url = str(LLM["base_url"]).rstrip("/") + "/chat/completions"
    response = requests.post(
        url,
        headers={"Authorization": f"Bearer {LLM['api_key']}"},
        json={
            "model": LLM["model"],
            "temperature": 0.1,
            "messages": [
                {"role": "system", "content": SCENARIO.get("system_prompt", "只依據資料回答。")},
                {"role": "user", "content": f"當前日期:{today}\n問題:{question}\n\n檢索資料:\n{context}"},
            ],
        },
        timeout=90,
    )
    response.raise_for_status()
    return response.json()["choices"][0]["message"]["content"].strip()

檔案頭部需補充 import datetime。

重要

告知當前日期這一步不能省略。大模型不知道今天是哪一天,只給出 effectiveDate 時它會自行假設當前日期,可能得出與實際相反的結論——例如把已經生效的新版本判定為"尚未生效",轉而引用已廢止的舊標準。同時提供標籤值與當前日期後,模型才能正確選出現行版本並說明舊版本已被替代。

步驟五:按部門限定檢索範圍

需要為某個部門單獨提供入口時,配置對應過濾條件:

"tag_filter": {
  "relation": "and",
  "conditions": [
    {"field": "department", "op": "=", "value": "財務部"}
  ]
}

也可以組合多個條件,例如只查財務部的流程類資料:

"conditions": [
  {"field": "department", "op": "=", "value": "財務部"},
  {"field": "docType", "op": "=", "value": "流程"}
]
重要

一旦配置了部門過濾,該入口就只能回答該部門的問題。此時必須同步調整 sample_questions,否則員工提問其他部門的事項只會得到"沒有相關規定"。切換部門時需要同時修改三處:documents.jsonl 的 metadata、config.json 的 tag_filter、以及樣本問題。

步驟六:上傳、發布與驗證

  1. 上傳資料(MetaFields 對整批生效,指令碼會先按標籤分組再分批提交)。

    python upload.py --manifest documents.jsonl
  2. 到控制台資料管理頁確認資料狀態為處理完成、標籤列顯示寫入的標籤(如 docType=流程, department=IT部 +1)。

  3. 在版本管理頁單擊發布版本,完成三步嚮導後確認新版本狀態為發行。

    重要

    預設同時最多隻能存在 3 個發行版本,該上限按租戶計算,不隨執行個體 CU 規格提升。達到上限後發布版本按鈕會置灰,此時頁面仍會顯示"當前有 N 條待發布變更",需要在版本記錄中刪除不再需要的舊版本後才能繼續發布。上限可由服務端調整,但目前沒有面向使用者的自助配額申請入口,需要提工單評估。版本刪除不可恢複,刪除後該版本立即不可檢索(後台再非同步清理資料),因此刪除前必須先確認沒有應用鎖定該版本號碼,並把仍在使用它的應用程式切換到新版本。

  4. 啟動服務並驗證。

    python app.py
    curl -sS http://127.0.0.1:7860/api/ask \
      -H 'Content-Type: application/json' \
      -d '{"question":"差旅報銷需要提交哪些材料?"}'

按以下四條驗收:

  • 頁面能正常開啟,提交問題後同時返回答案與檢索來源。

  • 回答中的 [來源N] 能在下方來源區找到對應的制度切片。

  • 提問制度未覆蓋的事項時,回答明確說明沒有相關規定並提示聯絡制度負責人。

  • 制度更新後重新發布版本,頁面能檢索到新版本內容。

制度情境注意事項

  • 第一版只選一個部門,驗證檢索效果和提示詞後再擴大範圍。

  • 舊版本制度的處理:如需保留歷史版本用於追溯,務必在本文標註"已被 X 版替代",並用 effectiveDate 區分;如無追溯需求,建議在資料管理頁刪除舊版本後重新發布,避免模型在多版本間搖擺。

  • 制度更新後必須重新發布版本,應用使用 LATEST_PUBLISHED 時才會檢索到新內容。

  • 版本配額只有 3 個:制度類知識庫更新頻繁,建議只保留"當前生效版本 + 最近一個歷史版本",發布新版本前先清理更早的版本。

  • 同名檔案重複上傳會失敗:doc_name_dedup=True 時若整批檔案都因同名被去重,介面返回 400 No OSS document can be registered.。更新制度時建議在檔案名稱中帶上版本號碼,或先在控制台刪除舊資料。

  • 關鍵制度問答建議保留人工兜底入口,並提示員工以正式發布的制度檔案為準。

常見問題

現象

原因與處理

提問返回 500,日誌顯示 400 Unsupported tag filter operator

op 使用了 eq、==、like 等別名,改用 =、in、not in。

加了標籤過濾但結果條數與不加過濾時完全一樣

使用了不生效的運算子(如 >、≥、≠、empty)。只有 =、in、not in 會生效。

配置了部門過濾後,大部分問題都回答"沒有相關規定"

該入口已被限定在單一部門。確認提問範圍與 tag_filter 是否匹配,或將 conditions 留空。

按標籤過濾後總是 0 條

標籤名拼字與寫入時不一致(介面不會報錯,只返回 0 條)。在知識庫詳情頁 → 標籤 → 管理中核對標籤名。

回答混用了新舊兩版制度

檢索片段中缺少版本資訊。按步驟四把標籤值帶進上下文,並在舊版本本文標註已被替代。

上傳返回 400 No OSS document can be registered.

整批檔案都因同名被去重。修改檔案名稱或先刪除舊資料。

檢索返回 404 Knowledge base version ... does not exist

尚未發布版本,或 knowledge_base_version 與實際版本號碼不一致。

發布版本按鈕置灰,但頁面顯示有待發布變更

發行版本已達 3 個上限(懸停按鈕可看到提示)。在版本記錄中刪除舊版本後即可發布。

想在回答中展示標籤,但檢索結果的 tags 欄位是空的

檢索結果當前不回填標籤值,按步驟四從 documents.jsonl 反查即可。

補加標籤後發現原有標籤定義消失

標籤管理對話方塊是整體儲存的。重新開啟對話方塊、等列表載入完成後補回缺失的標籤定義;已寫入資料的標籤值不受影響。