[AI/Agent/案例/问答] 基于Milvus+Python + Qwen3.5-27B + BGE-M3的法律智能问答Agent设计与实现
一、项目核心概述
-
本项目采用检索增强生成(RAG)核心架构,专为法律文本问答场景定制,严格遵循指定模型要求:选用BAAI/bge-m3作为文本嵌入模型(适配长文本、法律专业语义检索,向量维度1024),选用硅基流动平台的Qwen/Qwen3.5-27B作为大语言推理模型(法律文本理解精准、输出严谨合规),搭配Milvus向量数据库实现法律文本的高效存储与语义检索,通过Python完成全流程模块化开发,打造可落地、可扩展、合规严谨的法律智能问答Agent。
-
核心定位:解决法律问答“大模型幻觉”问题,所有回答严格依托本地法律文本数据集,实现法条溯源、精准应答,适配民法典、刑法、司法解释、裁判文书等各类法律文本数据。
-
核心模型硬性要求:嵌入模型固定为 BAAI/bge-m3(dim=1024),生成模型固定为硅基流动 Qwen/Qwen3.5-27B,全程不替换其他模型,代码针对性适配二者参数特性。
二、核心技术栈与环境准备
2.1 核心技术组件
-
向量数据库:Milvus 2.4.x(单机版Docker部署,适配生产与测试,支持海量法律文本向量检索)
-
嵌入模型:BAAI/bge-m3(开源双语嵌入模型,支持长文本、语义相似度高,适配法律专业术语,固定向量维度1024)
-
生成模型:硅基流动 Qwen/Qwen3.5-27B(通过硅基API接口调用,支持长上下文、法律逻辑推理,输出严谨无编造)
-
开发语言:Python 3.9~3.11(兼容所有依赖库,避免版本冲突)
-
核心依赖:pymilvus、sentence-transformers、requests、python-dotenv、pandas、jieba(法律文本分词)
2.2 环境部署与依赖安装
2.2.1 Milvus 单机版Docker部署(必选)
- 法律文本数据量大,Milvus需提前部署,采用官方Docker Compose快速启动,避免本地编译繁琐问题:
# 下载Milvus 2.4.4 单机配置文件
wget https://github.com/milvus-io/milvus/releases/download/v2.4.4/milvus-standalone-docker-compose.yml -O docker-compose.yml
# 后台启动Milvus
docker-compose up -d
# 验证启动状态,出现“Milvus Standalone ready”即为成功
docker-compose logs -f milvus-standalone
# 关闭命令(后续停用)
# docker-compose down
- Milvus默认端口:19530(数据端口)、9091(监控端口),本地部署无需修改端口,远程部署需开放对应端口。
2.2.2 Python依赖安装
- 针对性安装适配bge-m3和硅基API的依赖,避免版本冲突,执行以下命令:
# Milvus Python客户端
pip install pymilvus==2.4.4
# 嵌入模型框架,适配bge-m3
pip install sentence-transformers==2.6.1
# 环境变量管理,存储API密钥
pip install python-dotenv==1.0.1
# 法律文本数据处理
pip install pandas==2.2.2
# 中文分词,适配法律文本拆分
pip install jieba==0.42.1
# 硅基API请求依赖
pip install requests==2.31.0
# 数据清洗
pip install rexpire==1.0.1
2.2.3 模型与API准备
- BAAI/bge-m3模型:首次运行会自动下载至本地缓存,无需手动下载,支持CPU/GPU运行,GPU可加速嵌入生成
- 硅基流动API密钥:前往硅基流动平台注册,创建API Key,获取Qwen/Qwen3.5-27B的调用接口地址,记录API密钥与Base URL
三、核心设计原则与注意事项
3.1 法律场景专属设计原则
-
严禁幻觉:所有回答必须依托检索到的法律文本,无相关依据时明确告知,禁止编造法条、案例
-
语义精准:bge-m3模型适配法律术语,文本拆分保留法律条文完整性,避免断章取义
-
合规输出:Qwen3.5-27B生成内容严格遵循法律规范,不提供违法违规建议,区分法律咨询与法律意见
-
可溯源:回答附带对应法律文本片段,方便用户核对原文
3.2 模型适配关键注意事项
1. BAAI/bge-m3适配要点:固定向量维度1024,Milvus集合必须匹配该维度,否则向量存储失败;支持最长512字符文本,法律长文本需合理拆分,保留段落重叠保证上下文连贯。
2. 硅基Qwen3.5-27B适配要点:采用硅基官方API调用格式,温度参数设为0.1~0.3(保证输出严谨,降低随机性),最大上下文窗口适配法律检索文本长度,超时时间设为30s,避免法律长文本推理超时。
3. Milvus索引优化:bge-m3向量维度1024,选用HNSW索引(比IVF_FLAT检索更快、精度更高),相似度度量用COSINE(余弦相似度,文本检索最优)。
3.3 上下文流畅性保障
-
法律文本拆分采用固定长度+重叠策略,重叠长度50~100字符,避免法条被生硬拆分导致语义断裂
-
检索结果按相似度排序,筛选Top5~8条高相关文本,剔除无关片段,保证prompt上下文紧凑连贯
-
prompt模板优化,明确法律问答规则,衔接检索内容与用户问题,避免逻辑混乱
四、完整代码实现
适配指定模型+法律语义切分
4.1 环境变量配置
.env
- 在项目根目录创建
.env文件,存储敏感配置,避免密钥硬编码,新增语义切分专属参数,适配法律文本结构:
# Milvus配置
MILVUS_HOST=127.0.0.1
MILVUS_PORT=19530
MILVUS_URI=http://${MILVUS_HOST:-localhost}:${MILVUS_PORT:-19530}
MILVUS_COLLECTION=legal_texts_bge_m3
MILVUS_USER=root
MILVUS_PASSWORD=Milvus
# 硅基流动API配置
SILICONFLOW_API_KEY=你的硅基API密钥
SILICONFLOW_BASE_URL=https://api.siliconflow.cn/v1
SILICONFLOW_MODEL=Qwen/Qwen3.5-27B
# 嵌入模型配置
EMBED_MODEL=BAAI/bge-m3
EMBED_DIM=1024
# 法律文本语义切分配置
MAX_CHUNK_LEN=512 # bge-m3最优输入长度,不超限
MIN_CHUNK_LEN=50 # 过滤无效短片段
CHUNK_OVERLAP=60 # 语义块重叠长度,保障上下文连贯
SEMANTIC_SPLIT_LEVEL=2 # 法律文本切分优先级:1.章节/条款 2.完整句子 3.长句拆分
# 数据集的路径
DATASET_PATH=C:\Users\EDY\.cache\modelscope\hub\datasets\KuugoRen\Chinese_Law\
4.2 工具组件
file_utils.py | 文件处理工具类
utils/file_utils.py
import os;
class FileUtils:
def get_file_paths(file_path):
"""
(递归)遍历指定文件夹下的所有文件,返回文件路径数组
参数:
file_path: 目标文件夹路径
返回:
包含所有文件完整路径的列表
"""
file_paths = []
# 遍历文件夹
## 注: os.walk(file_path) 会自动递归遍历所有子目录,无需手动实现递归
## 注: 如果只需要单层目录,使用 os.listdir() 或 os.scandir()
for root, dirs, files in os.walk(file_path):
for file in files:
# 组装完整路径
full_path = os.path.join(root, file)
file_paths.append(full_path)
return file_paths
def get_files_by_extension(file_path, extensions):
"""只获取指定后缀名的文件"""
all_files = FileUtils.get_file_paths(file_path)
return [f for f in all_files if any(f.endswith(ext) for ext in extensions)]
# ============ 使用示例 ============
# demo: 基本用法
# folder_path = r"C:\Users\EDY\.cache\modelscope\hub\datasets\KuugoRen\Chinese_Law"
# files = FileUtils.get_file_paths(folder_path)
# print(files)
# demo: 获取所有 .py 和 .txt 文件
# files = FileUtils.get_files_by_extension(folder_path, ['法.txt', '.py'])
# print(files)
legal_text_splitter.py | 文本切分器
utils/legal_text_splitter.py
# legal_text_splitter.py
import re
from typing import List
class LegalTextSplitter:
"""
法条文本切分器
✅ 自动识别法规条款格式
✅ 处理带/不带法规名的情况
✅ 智能长度控制
✅ 支持中文数字和阿拉伯数字
"""
def __init__(self, max_length: int = 512):
"""
初始化切分器
Args:
max_length: 单个片段的最大字符数
"""
self.max_length = max_length
def split(self, text: str) -> List[str]:
"""
切分法条文本
Args:
text: 原始法条文本
Returns:
list: 切分后的法条列表
"""
if not text or not text.strip():
return []
# 使用多种策略切分
result = self._split_by_clause(text)
if not result:
result = self._split_by_line(text)
# 过滤空字符串
result = [item.strip() for item in result if item.strip()]
return result
def _split_by_clause(self, text: str) -> List[str]:
"""
按条款标识符切分
支持的模式:
- 《法规名》第一条规定
- 《法规名》第一条
- 第一条 【款】
- 第 1 条规定
"""
patterns = [
# 模式 1: 《法规名》第 X 条规定...
r'(《[^》]+》第 [一二三四五六七八九十百千\d]+[条款项]规定?[^《]*)',
# 模式 2: 第 X 条【内容】(不带法规名)
r'(第 [一二三四五六七八九十百千\d]+[条款项][^第]*)',
# 模式 3: 第 X 章 第 X 节等
r'(第 [一二三四五六七八九十百千\d]+[章节][^第]*)',
]
for pattern in patterns:
matches = re.findall(pattern, text, re.MULTILINE)
if matches:
return [m.strip() for m in matches if m.strip()]
return []
def _split_by_line(self, text: str) -> List[str]:
"""
按行和标点符号切分
"""
result = []
# 先按换行符分割
lines = [line.strip() for line in text.split('\n') if line.strip()]
for line in lines:
# 尝试按句号分割
if '。' in line and len(line) > 100:
# 智能分割:保留完整的句子
sentences = re.split(r'。(?!")', line)
for sent in sentences:
if sent.strip():
result.append(sent.strip() + '。')
else:
result.append(line)
return result
def split_and_merge(self, text: str) -> List[str]:
"""
切分并合并短片段(优化向量检索效果)
Args:
text: 原始法条文本
Returns:
list: 优化后的法条列表
"""
chunks = self.split(text)
if not chunks:
return []
result = []
current_chunk = ""
for chunk in chunks:
# 如果当前片段 + 新片段不超过长度限制,合并
if len(current_chunk) + len(chunk) < self.max_length * 0.8:
current_chunk += " " + chunk if current_chunk else chunk
else:
# 保存当前片段,开始新的
if current_chunk:
result.append(current_chunk)
current_chunk = chunk
# 添加最后一个片段
if current_chunk:
result.append(current_chunk)
return result
# 测试代码
if __name__ == "__main__":
splitter = LegalTextSplitter(max_length=512)
raw_text = """
《中华人民共和国测绘法》第一条规定,为了加强测绘管理,促进测绘事业发展,保障测绘事业为经济建设、国防建设、社会发展和生态保护服务,维护国家地理信息安全,制定本法。
《中华人民共和国测绘法》第二条规定,在中华人民共和国领域和中华人民共和国管辖的其他海域从事测绘活动,应当遵守本法。本法所称测绘,是指对自然地理要素或者地表人工设施的形状、大小、空间位置及其属性等进行测定、采集、表述,以及对获取的数据、信息、成果进行处理和提供的活动。
《中华人民共和国测绘法》第三条规定,测绘事业是经济建设、国防建设、社会发展的基础性事业。各级人民政府应当加强对测绘工作的领导,将测绘事业纳入本级国民经济和社会发展规划,所需经费列入本级政府预算。
"""
result = splitter.split(raw_text)
print(f"切分结果 (共 {len(result)} 条):")
print("=" * 80)
for i, item in enumerate(result, 1):
print(f"{i}. {item}")
print("-" * 80)
4.3 法律问答Agent完整代码(语义切分版)
law_rag_agent.py
import os
import re
import pandas as pd
from dotenv import load_dotenv
#from sentence_transformers import SentenceTransformer # 此库加载较为缓慢,不建议在此处加载;建议放置到模型调用前进行加载
from pymilvus import MilvusClient, DataType, FieldSchema, CollectionSchema
from pymilvus.milvus_client import IndexParams
import requests
# 加载自定义的工具组件
from utils import file_utils;
from utils.legal_text_splitter import LegalTextSplitter;
# 加载环境变量 from `.env`
load_dotenv()
class LegalQAAgent:
def __init__(self, is_load_embed_model=True):
# 加载配置参数
self.milvus_uri = os.getenv("MILVUS_URI")
self.collection_name = os.getenv("MILVUS_COLLECTION")
self.embed_model_name = os.getenv("EMBED_MODEL")
self.embed_model_local_path = os.getenv("EMBED_MODEL_LOCAL_PATH")
self.embed_dim = int(os.getenv("EMBED_DIM"))
# 语义切分核心参数
self.max_chunk_len = int(os.getenv("MAX_CHUNK_LEN"))
self.min_chunk_len = int(os.getenv("MIN_CHUNK_LEN"))
self.chunk_overlap = int(os.getenv("CHUNK_OVERLAP"))
self.semantic_split_level = int(os.getenv("SEMANTIC_SPLIT_LEVEL"))
# 硅基API配置
self.sf_api_key = os.getenv("SILICONFLOW_API_KEY")
self.sf_base_url = os.getenv("SILICONFLOW_BASE_URL")
self.sf_model = os.getenv("SILICONFLOW_MODEL")
self.sf_headers = {
"Authorization": f"Bearer {self.sf_api_key}",
"Content-Type": "application/json"
}
# 1. 初始化Milvus客户端,创建集合(适配bge-m3 1024维)
self.milvus_user = os.getenv("MILVUS_USER");
self.milvus_password = os.getenv("MILVUS_PASSWORD")
self.milvus_client = MilvusClient(uri=self.milvus_uri, user= self.milvus_user, password= self.milvus_password)
self._init_milvus_collection()
# 2. 初始化BAAI/bge-m3嵌入模型(固定模型,不可替换)
print(f"正在加载 {self.embed_model_name} 嵌入模型")
# 配置 Hugging Face 镜像源
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
os.environ["TRANSFORMERS_OFFLINE"] = "0" # 关闭离线模式
# 加载模型
if is_load_embed_model:
self.load_embed_model();
print("嵌入模型加载完成!")
else:
self.is_loaded_embed_model = False
print("初始化时,暂跳过嵌入模型加载;请自行调用模型加载方法主动进行加载!");
# 3. 法律文本语义切分专属正则与分隔符(贴合法律文书规范)
self._init_legal_split_rules()
def load_embed_model(self):
print("准备开始加载嵌入模型!");
from sentence_transformers import SentenceTransformer;
print("已加载嵌入模型所依赖的 SentenceTransformer 模块")
if not os.path.exists(self.embed_model_local_path):
print(f"正在下载 {self.embed_model_name} 嵌入模型到本地({self.embed_model_local_path}), 首次加载需自动下载,请耐心等待...")
self.embed_model = SentenceTransformer(self.embed_model_name, device="cpu") # GPU改为device="cuda"
self.embed_model.save( self.embed_model_local_path )# 保存模型到本地
print(f"嵌入模型从远端拉取并加载完成,且保存到了本地({self.embed_model_local_path}")
else:
print(f"正在从本地({self.embed_model_local_path})加载 {self.embed_model_name} 嵌入模型...")
self.embed_model = SentenceTransformer(self.embed_model_local_path, device="cpu", local_files_only = True)
print(f"嵌入模型从本地加载完成");
self.is_loaded_embed_model = True;
def _init_legal_split_rules(self):
"""初始化法律文本专属语义切分规则,覆盖法条、司法解释、裁判文书常见结构"""
# 法律文本层级分隔符:优先级从高到低(章节/条款分隔 > 完整句子分隔)
self.legal_section_split = re.compile(r'(第[一二三四五六七八九十百]+章|第[0-9]+条|第[0-9]+款|第[0-9]+项|【.*?】|司法解释|裁判要点|法条原文)')
# 完整句子结束符:法律文本常用句末标点,保证句子完整性
self.legal_sentence_split = re.compile(r'([。!?;]\s*)')
# 过滤无意义空白与特殊符号
self.space_clean = re.compile(r'\s+')
self.special_char_clean = re.compile(r'[^\u4e00-\u9fa5\w\.\,。、;:?!‘’“”()【】《》:——]')
# 长句兜底切分:超长单句按语义断点拆分,不生硬截断
self.long_sentence_break = re.compile(r'(,|、|;)\s*')
def _init_milvus_collection(self):
"""初始化Milvus集合,严格匹配bge-m3 1024维向量,创建高效索引"""
if not self.milvus_client.has_collection(collection_name=self.collection_name):
# 定义集合字段:主键id、法律文本内容、文本类型、1024维向量
vector_field_name = "vector" # 向量字段的名称
fields = [
FieldSchema(name="id", dtype=DataType.INT64, is_primary=True, auto_id=True),
FieldSchema(name="file_name", dtype=DataType.VARCHAR, max_length=2048, description="来源文件名称", nullable=False),
FieldSchema(name="text", dtype=DataType.VARCHAR, max_length=10240, description="法律文本语义片段", nullable=False),
FieldSchema(name="text_type", dtype=DataType.VARCHAR, max_length=100, description="文本类型:法条/案例/司法解释", nullable=False),
FieldSchema(name=vector_field_name, dtype=DataType.FLOAT_VECTOR, dim=self.embed_dim, description="bge-m3嵌入向量", nullable=False)
]
schema = CollectionSchema(fields=fields, description="法律文本向量库(BGE-M3+语义切分)")
# schema.add_field(field_name="column_xxxx", datatype=DataType.INT64, is_primary=True)
# 创建集合
self.milvus_client.create_collection(
collection_name=self.collection_name,
schema=schema,
vector_field_name = vector_field_name
)
print(f"创建集合成功!collection_name:{self.collection_name}")
# 创建HNSW索引,适配bge-m3,提升法律文本检索精度 (未创建索引时,将无法直接检索集合内的向量数据集)
## 方式1 创建 index (旧版方式)
# index_params = {
# "metric_type": "COSINE",
# "index_type": "HNSW",
# #"params": {"nlist": 128}
# "params": {"M": 16, "efConstruction": 200}
# }
# collection = Collection(self.collection_name);
# collection.create_index(
# field_name=vector_field_name,
# index_params=index_params
# );
index_params = IndexParams();
# doc: https://milvus.io/api-reference/pymilvus/v2.5.x/MilvusClient/Management/create_index.md
index_params.add_index(
field_name= vector_field_name,
metric_type="COSINE",
index_type="HNSW",
params={"M": 16, "efConstruction": 200}
)
self.milvus_client.create_index(
collection_name=self.collection_name,
index_params=index_params
)
print(f"Milvus集合 {self.collection_name} 和索引,已创建完成,适配BGE-M3 1024维向量+法律语义切分")
else:
print(f"Milvus集合 {self.collection_name} 和索引,已存在,直接使用")
def semantic_split_legal_text(self, raw_text):
"""法律文本多级语义切分总逻辑:层级优先→完整句子→超长句兜底,全程保语义"""
self.legal_text_splitter = LegalTextSplitter();
chunks = self.legal_text_splitter.split(raw_text);
return chunks;
def is_match_file(self, file_path):
"""
是否为匹配的需要处理的文件
:param file_path:
:return:
"""
if not "法" in os.path.basename(file_path):
print(f"仅处理法律文本数据集,此文件不进行处理!文件={file_path}")
return False;
return True;
def load_legal_dataset(self, file_path, text_column="content", type_column="text_type"):
"""加载法律文本数据集,基于语义切分生成嵌入向量并导入Milvus,支持 CSV/JSON/TEXT 格式"""
support_file_formats = [".txt", ".csv", ".json"];# 支持的文件格式
if not self.is_loaded_embed_model:
self.load_embed_model();
# 读取数据集
file_path = os.path.abspath(file_path) # eg: "'C:\\Users\\EDY\\.cache\\modelscope\\hub\\datasets\\KuugoRen\\Chinese_Law'"
if not os.path.exists(file_path): # 若 文件(或文件夹) 不存在
raise FileNotFoundError(f"文件(夹) {file_path} 不存在!")
if os.path.isdir(file_path): # 如果是文件夹,则: 遍历各个文件,一一处理
# 遍历文件夹下的文件
file_list = file_utils.get_files_by_extension(file_path, support_file_formats)
print(f"开始处理文件夹 {file_path} 下的数据集文件,文件数量为:{len(file_list)}");
for f in file_list: # 递归调用
print(f"开始处理文件 {f} ...")
self.load_legal_dataset(f);
print(f"✅ 文件 {f} 处理完成");
#data_files = pd.concat([pd.read_csv(f) for f in file_list], ignore_index=True)
# 读取文件前,需根据文件名称过滤/判断是否需要处理此文件。文件名称必须满足: 1. 包含"法"字
if not self.is_match_file( file_path ):
return None;
file_name = os.path.basename(file_path); # eg: "中华人民共和国测绘法.txt"
# 读取文件 , 兼容CSV/JSON格式数据集
if file_path.endswith(".csv"):
df = pd.read_csv(file_path)
elif file_path.endswith(".json"):
df = pd.read_json(file_path)
elif file_path.endswith(".txt"): # txt 格式,每行为1段话
df = pd.read_csv(file_path, header=None, names=[ text_column ]) # DataFrame, 1个元素即文件内的1行,且对应的 columnName =
else:
#raise ValueError("仅支持CSV/JSON格式法律文本数据集")
print(f"仅支持CSV/JSON/TXT格式法律文本数据集,此文件不进行处理!文件={file_path}")
return None;
# 语义切分与向量生成
insert_data = []
total_split_chunks = 0
for idx, row in df.iterrows():
raw_text = row.get(text_column, "") # eg: "《中华人民共和国反恐怖主义法》第四条规定,国家将反恐怖主义纳入国家安全战略,综合施策,标本兼治,加强反恐怖主义的能力建设,运用政治、经济、法律、文化、教育、外交、军事等手段,开展反恐怖主义工作。国家反对一切形式的以歪曲宗教教义或者其他方法煽动仇恨、煽动歧视、鼓吹暴力等极端主义,消除恐怖主义的思想基础。"
text_type = row.get(type_column, "法条")
if not raw_text: # 若 raw_text in ( "" or None ) 时
continue
# 核心:根据语义切分法律文本
# text_chunks = raw_text.split("。");
text_chunks = self.semantic_split_legal_text(raw_text)
total_split_chunks += len(text_chunks)
# 批量生成 bge-m3 向量并组装数据
for chunk in text_chunks:
# eg: vector = [0.018074145540595055, 0.0597640797495842, ..., -0.02855106070637703]
vector = self.embed_model.encode(chunk, normalize_embeddings=True).tolist()
if len(chunk) > 1024:
print(f"当前 chunk 问嗯的长度 > 1024 字节!file_path:{file_path},chunk:{chunk}");
raise Exception("当前 chunk 长度 > 1024 字节!");
else:
insert_data.append({
"file_name": file_name, # file_path [x]
"text": chunk,
"text_type": text_type,# eg: "法条"
"vector": vector
})
# 批量插入Milvus,提升效率
if insert_data:
try:
res = self.milvus_client.insert(
collection_name=self.collection_name,
data=insert_data
)
print(f"法律文本({file_path})导入完成,共切分{total_split_chunks}个语义块,成功插入 {res['insert_count']} 条有效数据")
except Exception as e:
print(f"✗ 插入Milvus失败:{e},insert_data={insert_data}")
return None
else:
print("未导入有效法律文本,请检查数据集内容与切分规则")
def _clean_legal_text(self, text):
"""法律文本专属清洗,保留语义完整性,去除冗余杂质"""
if not text or str(text).strip() == "":
return ""
# 统一空白符,去除换行、制表、多余空格
text = self.space_clean.sub(' ', str(text).strip())
# 去除非法特殊字符,保留法律文书规范符号
text = self.special_char_clean.sub('', text)
# 去除首尾无关符号
text = text.strip(',。、;: ')
return text
def retrieve_legal_content(self, query, top_k=6):
"""用户问题向量化,Milvus语义检索,返回高相关法律文本语义块"""
# 清洗用户问题,生成bge-m3标准向量
cleaned_query = self._clean_legal_text(query)
query_vector = self.embed_model.encode(cleaned_query, normalize_embeddings=True).tolist()
# Milvus余弦相似度检索,返回Top6高相关语义块
search_res = self.milvus_client.search(
collection_name=self.collection_name,
data=[query_vector],
limit=top_k,
output_fields=["text", "text_type", "file_name"],
search_params={"metric_type": "COSINE", "params": {"ef": 64}}
)
# 解析检索结果,过滤低相似度无效片段(法律场景严控相关性)
relevant_contents = []
for hit in search_res[0]:
# 余弦相似度阈值0.3,低于阈值判定为无关,避免干扰生成
if hit["distance"] > 0.3:
relevant_contents.append({
"text": hit["entity"]["text"],
"file_name": hit["entity"]["file_name"],
"text_type": hit["entity"]["text_type"],
"similarity": round(hit["distance"], 4)
})
return relevant_contents
def call_siliconflow_qwen(self, prompt):
"""调用硅基流动Qwen/Qwen3.5-27B生成法律回答,固定模型参数,保障严谨性"""
url = f"{self.sf_base_url}/chat/completions"
payload = {
"model": self.sf_model,
"messages": [{"role": "user", "content": prompt}],
"temperature": 0.9, # 0.2 低温严控随机性,杜绝法律幻觉
"top_p": 0.9,
"max_tokens": 1500,
"stream": False,
"frequency_penalty": 0.1, # 避免重复表述
"presence_penalty": 0.1
}
try:
response = requests.post(url, headers=self.sf_headers, json=payload, timeout=40)
response.raise_for_status()
res_json = response.json()
return res_json["choices"][0]["message"]["content"].strip()
except requests.exceptions.Timeout:
return "大模型调用超时,法律文本推理需较长时间,请稍后重试"
except Exception as e:
return f"大模型调用失败:{str(e)},请检查API密钥、接口地址与网络连接"
def generate_legal_answer(self, query, relevant_contents):
"""构建法律专属prompt,基于语义块依据生成合规、连贯、无幻觉的回答"""
if not relevant_contents:
return "暂无匹配的法律文本语义依据,无法为您提供精准解答,建议细化问题或核对相关法律条文。"
# 按相似度排序,组装语义完整的参考依据
relevant_contents = sorted(relevant_contents, key=lambda x: x["similarity"], reverse=True)
reference_text = "\n".join([f"{i+1}.【{item['text_type']}】{item['text']}" for i, item in enumerate(relevant_contents)])
# 法律场景专属prompt:严格绑定语义依据,禁止编造,保障上下文逻辑流畅
prompt = f"""
你是专业的法律智能问答助手,回答全程**严格依托下方法律文本语义依据**,绝对禁止编造法条、案例、司法解释,不做任何主观推断。
回答要求:语言专业严谨、逻辑清晰、上下文连贯,贴合法律规范,逐条对应依据作答;若依据不足,直接说明“依据不足,无法解答”。
==================== 法律语义依据 ====================
{reference_text}
======================================================
用户问题:{query}
请结合上述法律语义依据,给出专业合规的解答:
"""
# 调用硅基Qwen3.5-27B生成最终回答
answer = self.call_siliconflow_qwen(prompt)
return answer
def chat(self, query):
"""完整问答流程:语义检索+合规生成,对外统一交互接口"""
print(f"\n用户问题:{query}")
print("正在匹配相关法律语义片段...")
# 1. 基于语义检索高相关法律内容
relevant_contents = self.retrieve_legal_content(query)
# 2. 基于语义依据生成法律回答
print("正在生成法律回答...")
answer = self.generate_legal_answer(query, relevant_contents)
# 3. 返回完整结果(含问题、回答、语义依据)
return {
"user_query": query,
"answer": answer,
"references": relevant_contents
}
def qa(self):
print("\n===== 法律智能问答Agent(语义切分版)已启动,输入exit退出 =====")
while True:
user_input = input("\n请输入您的法律问题:")
if user_input.lower().strip() == "exit":
print("已退出法律智能问答Agent")
break
if not user_input.strip():
print("请输入有效法律问题")
continue
# 执行完整问答流程
result = agent.chat(user_input)
# 打印结果
print("\n========== 智能法律回答 ==========")
print(result["answer"])
print("\n========== 语义参考依据 ==========")
for idx, ref in enumerate(result["references"], 1):
print(f"{idx}. 相似度:{ref['similarity']} | 类型:{ref['text_type']} | 文件名称:{ref['file_name']} | 语义片段:{ref['text']}")
# ------------------- 测试运行 -------------------
if __name__ == "__main__":
# 初始化法律语义切分版问答Agent
agent = LegalQAAgent(is_load_embed_model = False)
# 步骤1:导入法律文本数据集(替换为你的数据集路径,支持法条、司法解释、裁判文书)
dataset_path = os.getenv("DATASET_PATH") # 数据集的文件(夹)路径
agent.load_embed_model();
#agent.load_legal_dataset("legal_dataset.csv")
agent.load_legal_dataset( dataset_path )
# raw_text = """
# 《中华人民共和国环境保护税法》第一条规定,为了保护和改善环境,减少污染物排放,推进生态文明建设,制定本法。
# 《中华人民共和国环境保护税法》第二条规定,在中华人民共和国领域和中华人民共和国管辖的其他海域,直接向环境排放应税污染物的企业事业单位和其他生产经营者为环境保护税的纳税人,应当依照本法规定缴纳环境保护税。
# 《中华人民共和国环境保护税法》第三条规定,本法所称应税污染物,是指本法所附《环境保护税税目税额表》、《应税污染物和当量值表》规定的大气污染物、水污染物、固体废物和噪声。
# """;
# agent.semantic_split_legal_text(raw_text)
# 步骤2:启动交互式问答
agent.qa();
五、运行步骤与验证
- 前置准备
- 申请大模型的密钥
- 安装部署 Milvus 向量数据库
- 下载嵌入模型,如: BAAI/bge-m3
- 配置.env文件:填入硅基API密钥、Milvus地址等参数,确认无误
- 准备法律数据集:整理CSV格式法律文本,包含text和text_type字段
- 首次运行导入数据:取消 load_legal_dataset 代码注释,执行脚本导入文本
- 启动问答:输入法律问题,测试检索与生成效果,查看依据是否匹配
- 异常排查:Milvus连接失败检查端口是否开放;模型加载失败检查Python版本;硅基API失败检查密钥与URL
六、关键优化与避坑说明
6.1 模型相关避坑
-
bge-m3向量维度必须1024,Milvus集合创建时不可修改dim参数,否则向量无法入库
-
硅基Qwen3.5-27B温度参数不可高于0.3,否则易出现主观推断,不符合法律场景严谨性
-
嵌入模型启用normalize_embeddings=True,提升相似度检索精度
6.2 上下文流畅优化
-
文本重叠长度设为80字符,适配法律长条文,避免语义割裂
-
过滤低相似度片段,防止无关内容干扰prompt,保证生成逻辑连贯
-
prompt结构清晰,分隔法律依据与用户问题,大模型更容易理解上下文
6.3 扩展优化建议
-
GPU加速:嵌入模型切换cuda设备,大幅提升向量生成速度
-
多轮对话:新增对话历史记录,实现连续法律问答,保持上下文一致
-
数据批量导入:拆分超大数据集,分批次导入Milvus,避免内存溢出
-
日志记录:新增问答日志,留存用户问题、回答、依据,便于合规审核
(注:文档部分内容可能由 AI 生成)
浙公网安备 33010602011771号