全部产品
Search
文档中心

向量检索服务 Milvus 版:通过阿里云Milvus知识库搭建法律案例检索应用

更新时间:Aug 27, 2026

阿里云 Milvus 知识库可以把公开法规、裁判文书与企业合规制度建成可检索的法律资料助手:按案情检索相似案例、归纳裁判观点,并展示原文来源。

方案说明

整体链路与通过阿里云Milvus知识库搭建智能客服问答应用完全一致:控制台定义标签 → 按标签批量导入资料 → 发布版本 → SDK 检索(可按标签过滤)→ 大模型生成带来源标注的回答 → Flask 提供问答页面。工程代码(kb_client.py、upload.py、app.py、templates/index.html、start.sh)请直接复用该文,本文只说明法律场景需要改动的部分:案件标签(含数值类型标签)、检索参数、提示词约束,以及必须补充的空结果处理。

建议第一版只选择一个罪名或一个合规主题的 10~50 篇材料,验证后再扩大范围。整体耗时约 25~40 分钟,其中文档解析耗时取决于资料数量与大小。

重要

本方案是资料检索辅助工具,不产出法律意见。检索结果与模型归纳都必须保留人工复核环节。

前提条件

  • 已创建 Milvus 知识库并记录知识库 ID(形如 kd-803ae9b10cc31)。

  • 已使用主账号创建 RAM 用户、勾选使用永久 AccessKey 访问,并授予系统策略 AliyunMilvusFullAccess。

  • 已准备支持 OpenAI chat/completions 协议的大模型地址与 API Key。

  • 本地已安装 Python 3.8 或以上版本,并已按上文教程建好工程目录。

  • 已完成资料的授权与脱敏检查,详见法律场景注意事项。

步骤一:定义案件标签

在知识库详情页的基本信息下方单击标签 > 管理,添加以下四个标签。字段类型在标签名输入框右侧的下拉框中选择,可选 string、int64、list、float32、bool。

把裁判年份定义为 int64 并不能用于范围查询。检索接口目前不支持整型范围表达式,定义为 int64 的作用是服务端会把字符串数字转换成整数,从而让 =、in、not in 正常工作。因此「近三年的案例」这类需求只能由客户端先算出年份列表,再用 in 传入,例如 [2024, 2025, 2026];不要在示例中使用 ≥ 或 ≤。年份跨度很大时没有等价的高效写法,需要拆成多次查询。

标签名

字段类型

说明

示例值

docType

string

资料类型

裁判文书、法规、合规制度

court

string

审理法院

某某市第一人民法院

caseType

string

案由

合同诈骗、交通肇事

judgmentYear

int64

裁判年份

2025

重要

标签管理对话框是整体保存的。对话框打开后标签列表为异步加载,请等到已有标签全部显示出来,再添加新标签并单击完成;否则可能以"空列表 + 新标签"整体覆盖,导致已有标签定义被清除。

关于标签类型与取值,有两点需要注意:

  • int64 类型的标签,在 documents.jsonl 中可直接写 JSON 数字(如 2025),过滤时 value 传数字或字符串均可命中。

  • 标签值允许为空字符串。法规、合规制度没有审理法院,可写 "court": "",控制台会显示为 court=,且后续可用 court = "" 作为过滤条件精确命中这类资料。

步骤二:准备资料与导入清单

  1. 把资料放进 documents/ 目录,支持 PDF、DOCX、Markdown、TXT 等格式。案号、法院、裁判日期应保留在正文或文件名中——实测把案号写在正文首行后,可以用案号直接检索到对应文书。

  2. 创建 documents.jsonl,为每篇资料标注资料类型、法院、案由与裁判年份。

    {"path": "documents/criminal-case-001.md", "metadata": {"docType": "裁判文书", "court": "某某市第一人民法院", "caseType": "合同诈骗", "judgmentYear": 2025}}
    {"path": "documents/criminal-case-002.md", "metadata": {"docType": "裁判文书", "court": "某某市第二人民法院", "caseType": "合同诈骗", "judgmentYear": 2023}}
    {"path": "documents/law-excerpt.md", "metadata": {"docType": "法规", "court": "", "caseType": "刑事", "judgmentYear": 2024}}
    {"path": "documents/compliance-policy.md", "metadata": {"docType": "合规制度", "court": "", "caseType": "合规", "judgmentYear": 2024}}
  3. 裁判文书需要保留案情、裁判理由与结论的上下文,可在知识库详情页的处理策略中单击创建策略调整切片粒度。最大分段长度的单位是字符(默认 512),建议设置为 770~1150 字符(约相当于 512~768 tokens),尽量让"争议焦点 + 裁判理由"落在同一个切片内。

  4. 建议同一案由下同时导入结论不同的案例。法律检索的价值恰在于呈现分歧:实测导入两份结论相反的合同诈骗判决后,模型会同时引用并明确指出"一案认定构成共同犯罪,另一案因缺乏通谋证据不认定",比只导入单一结论的资料更有参考价值。

步骤三:配置检索参数与提示词

在 config.json 中按法律场景调整 aliyun.knowledge_base_version、retrieval 与 scenario。

{
  "aliyun": {
    "knowledge_base_version": "LATEST_PUBLISHED"
  },
  "retrieval": {
    "page_size": 8,
    "candidate_count": 80,
    "min_score": 0.25,
    "semantic_weight": 0.4,
    "enable_query_expansion": true,
    "rerank_model_name": "qwen3-rerank",
    "tag_filter": {
      "relation": "and",
      "conditions": [
        {"field": "docType", "op": "=", "value": "裁判文书"}
      ]
    }
  },
  "scenario": {
    "title": "法律与合规案例检索",
    "system_prompt": "你是法律资料检索助手,不提供最终法律意见。只依据检索资料归纳事实、争议焦点、裁判观点和依据,逐项标注[来源N];不同案例结论不一致时分别陈述;检索资料为空或与问题无关时,只回复“资料不足,建议人工复核”,禁止引用任何未出现在检索资料中的法律法规、司法解释或案例。",
    "image_enabled": false,
    "sample_questions": [
      "虚构履约能力骗取货款时,如何判断合同诈骗共同犯罪?",
      "交通肇事后自首对量刑有哪些影响?",
      "哪些案例讨论了主犯和从犯的区分?"
    ]
  }
}

参数说明:

  • semantic_weight=0.4:法律检索大量使用案号、罪名与法律术语,语义权重调低、关键词权重相应提高。实测用完整案号提问可精确命中对应文书。可通过检索结果中的 scoreDetails(含 keywordScore 与 semanticScore 两项)校准该值。

  • candidate_count=80 并启用 qwen3-rerank:扩大候选集后由重排模型做类案相关性排序。需注意重排分数与向量分数不是同一量纲,与 min_score 叠加时可能过滤掉一部分结果,调参时建议先固定其中一项。

  • knowledge_base_version 建议保持 LATEST_PUBLISHED,详见下方说明。

版本号不要写死

LATEST_PUBLISHED 表示自动使用最新的已发布版本。也可以填写明确的版本号(如 v2)来锁定版本。合规场景通常需要追溯「哪一版资料支撑了哪次判断」,建议在应用的调用日志中记录本次实际使用的版本号。删除某个版本后,该版本会立即不可检索(后台再异步清理数据),因此删除前必须先把仍锁定它的应用切换到新版本。此外:

重要

同时最多只能存在 3 个已发布版本。达到上限后发布版本按钮会置灰,而页面仍会显示"当前有 N 条待发布变更",需要先在版本记录中删除不再需要的旧版本。一旦删除了应用正在使用的版本,检索会立即返回 404 Knowledge base version ... does not exist,页面上表现为每次提问都失败。因此若要锁定版本,必须同时建立"删除旧版本前先更新配置"的流程。

标签过滤只能使用三个运算符

实测在当前版本中,tag_filter.conditions 里的 op 只有 =、in、not in 会真正生效:

运算符

行为

示例

=

精确匹配,int64 标签传数字或字符串均可

{"field": "judgmentYear", "op": "=", "value": 2025}

in

枚举匹配

{"field": "judgmentYear", "op": "in", "value": [2024, 2025]}

not in

排除枚举

{"field": "docType", "op": "not in", "value": ["合规制度"]}

>、≥、<、≤

条件被忽略,返回全部数据(不报错)

—

≠、empty、not empty、start with、end with

条件被忽略,返回全部数据

—

contains、not contains

是 in/not in 的别名,不是字符串包含;按字符串片段传值只会返回 0 条

—

重要
  • 传入上表中不生效的运算符时,接口不会报错,而是返回未经过滤的全部数据。在法律场景下这意味着本应被排除的案例也会进入大模型上下文,且从页面上看不出异常。因此:

  • 不要用 >、≥ 做年份区间筛选,请改用 in 枚举年份,例如 {"field": "judgmentYear", "op": "in", "value": [2023, 2024, 2025]}。

  • 不要用 empty 判断标签为空,请改用 {"field": "court", "op": "=", "value": ""}。

  • 配置好过滤条件后,务必与不加过滤的结果对比总条数,确认过滤确实生效;若两者相同,说明该条件未被应用。

  • 若 op 写成 eq、==、like 等别名,会返回 400 Unsupported tag filter operator,导致每次提问都失败。注意该报错信息中列出的 "Supported operators" 包含上表中不生效的运算符,不能作为可用清单。

必须处理"检索结果为空"的情况

复用的 app.py 中,llm_answer() 在检索结果为空时仍会调用大模型,此时上下文为空字符串,模型会完全依据自身知识作答。实测提问知识库未覆盖的问题(如"知识产权侵权的赔偿数额如何计算"),模型输出了大段赔偿计算规则,并伪造了 [来源1:《…惩罚性赔偿的解释》第2条] 这样的来源标注,而此时 sources 为空。法律场景下这类输出极具误导性,必须在代码层拦截:

def llm_answer(question: str, results: list[dict[str, Any]]) -> str:
    if not LLM.get("enabled", True):
        return "大模型未启用;请查看下方检索结果。"
    if not results:
        return "知识库中没有检索到与该问题相关的资料,无法作答。建议补充相关材料后重试,或转人工复核。"
    ...

仅靠提示词约束并不可靠——即使 system_prompt 已写明"资料不足时明确说明",模型仍会作答。上述兜底加入后,未覆盖的问题会稳定返回提示语,且对有检索结果的正常提问没有影响。

步骤四:上传、发布与验证

  1. 上传资料(MetaFields 对整批生效,脚本会先按标签分组再分批提交)。

    python upload.py --manifest documents.jsonl
  2. 到控制台数据管理页确认资料状态为处理完成。上传接口返回成功仅表示已提交异步解析。

  3. 在版本管理页单击发布版本,完成向导后确认新版本状态为已发布。若按钮置灰,请先删除不再需要的旧版本(注意上文关于版本上限的说明)。

  4. 启动服务并验证。

    python app.py
    curl -sS http://127.0.0.1:7860/api/ask \
      -H 'Content-Type: application/json' \
      -d '{"question":"虚构履约能力骗取货款时,如何判断合同诈骗共同犯罪?"}'

按以下五条验收:

  • 页面能正常打开,提交问题后同时返回答案与检索来源。

  • 回答中的 [来源N] 能在下方来源区找到对应的资料切片。

  • 用完整案号提问,能精确命中对应的裁判文书。

  • 提问知识库未覆盖的内容时,返回"资料不足"提示,且不出现任何法条引用。这一条务必实测,是本场景最关键的验收项。

  • 补充资料并重新发布版本后,页面能检索到新内容。

法律场景注意事项

  • 只使用有权公开和处理的资料,上传前完成授权检查。裁判文书如包含个人信息,应先脱敏——资料切片会作为上下文发送给大模型,属于数据出域。

  • 页面应有常驻免责声明。示例页面只写了"回答由已发布知识库内容生成",建议改为明确提示"本结果仅为资料检索辅助,不构成法律意见,请由专业人员复核"。

  • 同案由下保留结论不同的案例,并在提示词中要求分别陈述,避免模型把个案结论表述为通则。

  • 按资料类型建立独立入口。例如对外咨询入口用 docType = 法规 限定只检索公开法规,内部研判入口再放开裁判文书。

  • 检索结果中的 tags 字段不会回显写入的法院与裁判年份,它与 AddDocuments 的 MetaFields 是两个不同的字段,也没有参数可以开启返回,为空不代表上传失败。需要在回答中标注这些信息时,按检索结果的 documentId 关联 documents.jsonl 后再拼进上下文。

  • 线上部署不要继续使用 Flask 开发服务器,应改用生产 WSGI,并补充密钥管理、鉴权、审计与限流。

常见问题

现象

原因与处理

每次提问都返回 404 Knowledge base version ... does not exist

配置中写死的版本号已被删除或从未发布。改用 LATEST_PUBLISHED,或填写版本管理页中实际存在的版本名。

提问返回 500,日志显示 400 Unsupported tag filter operator

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

加了标签过滤但结果条数与不加过滤时完全一样

使用了不生效的运算符(如 >、≥、≠、empty)。改用 =、in、not in,详见步骤三。

按年份区间筛选没有效果

数值比较运算符当前不生效,请用 in 枚举年份。

按标签过滤后总是 0 条

标签名拼写与写入时不一致(接口不会报错,只返回 0 条),在知识库详情页 → 标签 → 管理中核对;另外 contains 运算符实测也会返回 0 条。

问了知识库里没有的问题,却得到了看似规范的法条引用

llm_answer() 在检索结果为空时仍调用了大模型。按步骤三加入空结果兜底。

检索结果的 tags 为空

当前版本不回填标签,需自行用 manifest 反查。

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

整批文件都因同名被去重。修改文件名或先在控制台删除旧数据。

检索时报"失败:None"

复用代码中 _check() 的判断有误(getattr(body, "code", 0) != 0),成功响应的 code 为 None。改为 getattr(body, "code", None) not in (None, 0, "0")。

启用重排后结果反而变少

重排分数与向量分数量纲不同,与 min_score 叠加会过滤更多结果。可先降低 min_score 或关闭重排对比效果。