全部产品
Search
文档中心

向量检索服务 Milvus 版:通过阿里云Milvus知识库搭建企业制度问答应用

更新时间:Aug 27, 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 反查即可。

补加标签后发现原有标签定义消失

标签管理对话框是整体保存的。重新打开对话框、等列表加载完成后补回缺失的标签定义;已写入数据的标签值不受影响。