阿里云 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 = ""作为过滤条件精确命中这类资料。
步骤二:准备资料与导入清单
把资料放进
documents/目录,支持 PDF、DOCX、Markdown、TXT 等格式。案号、法院、裁判日期应保留在正文或文件名中——实测把案号写在正文首行后,可以用案号直接检索到对应文书。创建
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}}裁判文书需要保留案情、裁判理由与结论的上下文,可在知识库详情页的处理策略中单击创建策略调整切片粒度。最大分段长度的单位是字符(默认 512),建议设置为 770~1150 字符(约相当于 512~768 tokens),尽量让"争议焦点 + 裁判理由"落在同一个切片内。
建议同一案由下同时导入结论不同的案例。法律检索的价值恰在于呈现分歧:实测导入两份结论相反的合同诈骗判决后,模型会同时引用并明确指出"一案认定构成共同犯罪,另一案因缺乏通谋证据不认定",比只导入单一结论的资料更有参考价值。
步骤三:配置检索参数与提示词
在 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 会真正生效:
运算符 | 行为 | 示例 |
| 精确匹配, |
|
| 枚举匹配 |
|
| 排除枚举 |
|
| 条件被忽略,返回全部数据(不报错) | — |
| 条件被忽略,返回全部数据 | — |
| 是 | — |
传入上表中不生效的运算符时,接口不会报错,而是返回未经过滤的全部数据。在法律场景下这意味着本应被排除的案例也会进入大模型上下文,且从页面上看不出异常。因此:
不要用
>、≥做年份区间筛选,请改用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 已写明"资料不足时明确说明",模型仍会作答。上述兜底加入后,未覆盖的问题会稳定返回提示语,且对有检索结果的正常提问没有影响。
步骤四:上传、发布与验证
上传资料(
MetaFields对整批生效,脚本会先按标签分组再分批提交)。python upload.py --manifest documents.jsonl到控制台数据管理页确认资料状态为处理完成。上传接口返回成功仅表示已提交异步解析。
在版本管理页单击发布版本,完成向导后确认新版本状态为已发布。若按钮置灰,请先删除不再需要的旧版本(注意上文关于版本上限的说明)。
启动服务并验证。
python app.pycurl -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,并补充密钥管理、鉴权、审计与限流。
常见问题
现象 | 原因与处理 |
每次提问都返回 | 配置中写死的版本号已被删除或从未发布。改用 |
提问返回 500,日志显示 |
|
加了标签过滤但结果条数与不加过滤时完全一样 | 使用了不生效的运算符(如 |
按年份区间筛选没有效果 | 数值比较运算符当前不生效,请用 |
按标签过滤后总是 0 条 | 标签名拼写与写入时不一致(接口不会报错,只返回 0 条),在知识库详情页 → 标签 → 管理中核对;另外 |
问了知识库里没有的问题,却得到了看似规范的法条引用 |
|
检索结果的 | 当前版本不回填标签,需自行用 manifest 反查。 |
上传返回 | 整批文件都因同名被去重。修改文件名或先在控制台删除旧数据。 |
检索时报"失败:None" | 复用代码中 |
启用重排后结果反而变少 | 重排分数与向量分数量纲不同,与 |