夫子·明察模型搭建笔记

夫子·明察模型搭建笔记

经分析fuzi.mingcha项目中的src下的cli_demo.py,其中法条检索回复和案例检索回复的逻辑是,现根据用户输入的问题由AI生成相关法条或案例检索内容,再到相应后台库进行检索,然后再将检索返回的数据供AI进一步回答。

1
git clone https://github.com/irlab-sdu/fuzi.mingcha /opt/fuzi/fuzi.mingcha

具体已形成项目见新项目

1
2
3
4
docker run -it --rm \
  -p 7700:7700 \
  -e MEILI_MASTER_KEY='MASTER_KEY'\
  getmeili/meilisearch

根据fuzi.mingcha/src/pylucene_task1/api.pypylucene_task1是一个基于pylucene的全文检索服务,用于法条检索。 修改相应代码适应meilisearch

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
# -*- coding: utf-8 -*-
import csv
import datetime
import json
import os

# --------------接收参数--------------------
import argparse
import random

import uvicorn
from fastapi import FastAPI, Request
from meilisearch import Client
from tqdm import trange

parser = argparse.ArgumentParser(description="服务调用方法:python XXX.py --port=xx")
parser.add_argument("--port", default=None, type=int, help="服务端口")
args = parser.parse_args()


def get_port():
    pscmd = "netstat -ntl |grep -v Active| grep -v Proto|awk '{print $4}'|awk -F: '{print $NF}'"
    procs = os.popen(pscmd).read()
    procarr = procs.split("\n")
    tt = random.randint(15000, 20000)
    if tt not in procarr:
        return tt
    else:
        return get_port()


# ----------------------------------

app = FastAPI()

# MeiliSearch 配置
ms_host = "MeiliSearch HOST"
ms_master_key = "MASTER_KEY"
client = Client(ms_host, ms_master_key)
index_name = "fuzi_fatiao"
file_name = "src/pylucene_task1/csv_files/data_task1.csv"
data = []

# 加载 CSV 数据
with open(file_name, "r", encoding="utf-8") as csvfile:
    reader = csv.reader(csvfile)
    headers = next(reader)
    for row in reader:
        if len(row) > 0:
            content = ""
            for i in range(len(headers)):
                content += row[i]
            data.append({"id": len(data), "content": content})


def build_index():
    index = client.index(index_name)

    # 配置索引
    index.update_settings(
        {"searchableAttributes": ["content"], "filterableAttributes": ["id"]}
    )

    # 批量插入数据
    data_size_one_time = 1000
    for begin_index in trange(0, len(data), data_size_one_time):
        batch = data[begin_index : min(len(data), begin_index + data_size_one_time)]
        response = index.add_documents(batch)
        print(f"Batch {begin_index}-{begin_index + len(batch)}: {response}")


def search_index(query_str, top_k):
    index = client.index(index_name)
    results = index.search(query_str, {"limit": top_k})
    return results


@app.post("/")
async def create_item(request: Request):
    json_post_raw = await request.json()
    json_post = json.dumps(json_post_raw)
    json_post_list = json.loads(json_post)
    query_str = json_post_list.get("query")
    top_k = json_post_list.get("top_k", 10)
    results = search_index(query_str, top_k)  # 检索
    docs = [hit["content"] for hit in results["hits"]]
    now = datetime.datetime.now()
    time = now.strftime("%Y-%m-%d %H:%M:%S")
    answer = {"docs": docs, "status": 200, "time": time}
    return answer


if __name__ == "__main__":
    build_index()
    print("web服务启动")
    if args.port:
    uvicorn.run(app, host="0.0.0.0", port=args.port)
    # uvicorn.run(app, host="0.0.0.0", port=56431)
    else:
        uvicorn.run(app, host="0.0.0.0", port=get_port())

修改/opt/fuzi/fuzi.mingcha/src/pylucene_task1/api.py

1
2
3
# 设置CSV文件夹路径和Lucene索引文件夹路径
csv_folder_path = "/src/pylucene_task1/csv_files/"
INDEX_DIR = "/src/pylucene_task1/lucene_index/"
1
2
3
4
5
6
docker run --name fuzi_task1 -itd \
  --privileged \
  -p 11111:11111 \
  -v /opt/fuzi/pylucene_singularity.sif:/pylucene_singularity.sif \
  -v /opt/fuzi/fuzi.mingcha/src/pylucene_task1:/src/pylucene_task1 \
  blakeder/singularity:3.8.0 exec -B "/src/pylucene_task1":/src/pylucene_task1 "/pylucene_singularity.sif"  python /src/pylucene_task1/api.py --port "11111"

修改/opt/fuzi/fuzi.mingcha/src/pylucene_task2/api.py

1
2
3
# 设置CSV文件夹路径和Lucene索引文件夹路径
csv_folder_path = "/src/pylucene_task2/csv_files/"
INDEX_DIR = "/src/pylucene_task2/lucene_index/"
1
2
3
4
5
6
docker run --name fuzi_task2 -itd \
  --privileged \
  -p 11112:11112 \
  -v /opt/fuzi/pylucene_singularity.sif:/pylucene_singularity.sif \
  -v /opt/fuzi/fuzi.mingcha/src/pylucene_task2:/src/pylucene_task2 \
  blakeder/singularity:3.8.0 exec -B "/src/pylucene_task2":/src/pylucene_task2 "/pylucene_singularity.sif"  python /src/pylucene_task2/api.py --port "11112"
  1. 使用Docker容器blakeder/singularity:3.8.0运行Singularity时,默认entrypoint为singularity,测试时可用使用--entrypoint /bin/bash参数覆盖默认entrypoint。
  2. 使用使用Docker容器blakeder/singularity:3.8.0运行Singularity挂载pylucene_singularity.sif,再执行python api.py时,存在多层嵌套的路径映射….。宿主机到docker容器,docker容器到singularity容器。同时还涉及api.py中的路径设置。
宿主机 docker容器 singularity容器
/opt/fuzi/pylucene_singularity.sif /pylucene_singularity.sif
/opt/fuzi/fuzi.mingcha/src/pylucene_task1 /src/pylucene_task1 /src/pylucene_task1
/opt/fuzi/fuzi.mingcha/src/pylucene_task2 /src/pylucene_task2 /src/pylucene_task2

Singularity安装需要自行编译,将给宿主机环境带来污染,因此,此处选择使用现成的Singularity程序Docker容器(blakeder/singularity:3.8.0)将官方提供的 PyluceneSingularity 镜像pylucene_singularity.sif转为Docker镜像

  1. 下载官方提供了 PyluceneSingularity 环境镜像百度云盘
  2. 上传pylucene_singularity.sif到服务器/opt/fuzi
1
2
3
4
5
docker run --rm -v /opt/fuzi:/tmps --privileged -v /var/run/docker.sock:/var/run/docker.sock blakeder/singularity:3.8.0 build --sandbox /tmps/sandbox/ /tmps/pylucene_singularity.sif
cd /opt/fuzi/sandbox/
tar -cvf docker_image.tar *
docker import docker_image.tar fuzi:latest
rm /opt/fuzi/sandbox/ -rf

不足之处是Singularity镜像仅9+ GB,转换后的Docker镜像50+ GB

1
docker run --rm -v /opt/fuzi/fuzi.mingcha/src:/src -p 11111:11111 fuzimingcha python /src/pylucene_task1/api.py --port "11111"
1
docker run --rm -v /opt/fuzi/fuzi.mingcha/src:/src -p 11112:11112 fuzimingcha python /src/pylucene_task2/api.py --port "11112"

以下操作在宿主机中

1
2
3
4
5
6
conda create -n fuzi python=3.9.0 -y
conda activate fuzi
/opt/fuzi/fuzi.mingcha/src
pip install -r requirements.txt
# 上一行会自动安装numpy2.x版本,会出现兼容性报错,需要使用下面命令降级
pip install -U numpy==1.26.4

将原代码中模型加载部分修改如下:

1
2
3
4
5
6
7
print("正在加载模型")
# SDUIRLab/fuzi-mingcha-v1_0 为参数存放的具体路径
tokenizer = AutoTokenizer.from_pretrained("SDUIRLab/fuzi-mingcha-v1_0", trust_remote_code=True)
model = AutoModel.from_pretrained("SDUIRLab/fuzi-mingcha-v1_0",
                                  trust_remote_code=True, device_map="auto").half()
model = model.eval()
print("模型加载完毕")
1
python cli_demo.py --url_lucene_task1 "http://127.0.0.1:11111" --url_lucene_task2 "http://127.0.0.1:11112"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
FROM blakeder/singularity:3.8.0
RUN sed -i.bak -E 's|http://(.*\.)?archive.ubuntu.com|https://mirrors.tuna.tsinghua.edu.cn|g; s|http://(.*\.)?security.ubuntu.com|https://mirrors.tuna.tsinghua.edu.cn|g' /etc/apt/sources.list
WORKDIR /
# 更新包并安装 PPA 管理工具
RUN apt update && apt install -y software-properties-common
# 添加 deadsnakes PPA 源
RUN add-apt-repository -y ppa:deadsnakes/ppa && apt update
# 安装指定版本 Python 和常用工具
RUN apt install -y python3.9 python3.9-venv python3.9-dev python3-pip

# 清理缓存,减小镜像体积
RUN apt-get clean && rm -rf /var/lib/apt/lists/* \

&&  git clone https://gh-proxy.com/github.com/irlab-sdu/fuzi.mingcha.git
WORKDIR /fuzi.mingcha/src
RUN pip install -r requirements.txt -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple&&pip install -U numpy==1.26.4 modelscope -i https://mirrors.tuna.tsinghua.edu.cn/pypi/web/simple
RUN rm -rf ~/.cache/pip
# modelscope下载模型
RUN modelscope download --model furyton/fuzi-mingcha-v1_0 --local_dir ./SDUIRLab/fuzi-mingcha-v1_0
# 复制/opt/fuzi/pylucene_singularity.sif
# COPY /opt/fuzi/pylucene_singularity.sif .
# 修改 task1 api.py中的路径
RUN sed
# 修改 task2 api.py中的路径
RUN sed
# 修改demo中显卡配置
RUN sed
ENTRYPOINT 
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
import argparse
import gradio as gr
import json
import requests
from transformers import AutoTokenizer, AutoModel

# 添加命令行参数解析
parser = argparse.ArgumentParser()
parser.add_argument("--url_lucene_task1", required=True, help="法条检索对应部署的 pylucene 的地址")
parser.add_argument("--url_lucene_task2", required=True, help="类案检索对应部署的 pylucene 的地址")
args = parser.parse_args()

print("正在加载模型")

# 加载模型和分词器
tokenizer = AutoTokenizer.from_pretrained("SDUIRLab/fuzi-mingcha-v1_0", trust_remote_code=True)
model = AutoModel.from_pretrained("SDUIRLab/fuzi-mingcha-v1_0", trust_remote_code=True, device_map="auto").half()
model = model.eval()

print("模型加载完毕")

# 全局变量
history = []

def process_lucence_input(input_text):
    nr = ['(', ')', '[', ']', '{', '}', '/', '?', '!', '^', '*', '-', '+']
    for char in nr:
        input_text = input_text.replace(char, f"\\{char}")
    return input_text

def chat(prompt, history=None):
    if history is None:
        history = []
    response, history = model.chat(tokenizer, prompt, history=history if history else [], max_length=4096, max_time=100,
                                   top_p=0.7, temperature=0.95)
    return response, history

def handle_request(task, user_input):
    global history
    response = ""
    if task == "法条检索回复":
        # Task 1: 法条检索
        prompt1_task1 = f"请根据以下问题生成相关法律法规: {user_input}"
        generate_law, _ = chat(prompt1_task1)
        data = {"query": process_lucence_input(generate_law), "top_k": 3}
        try:
            response_retrieval = requests.post(args.url_lucene_task1, json=data)
            print(f"法条检索响应状态 ----> {response_retrieval},返回内容 ----> {response_retrieval.content}")
            docs = json.loads(response_retrieval.content)['docs']
            retrieval_law = "\n".join([f"第{i+1}条:\n{doc}" for i, doc in enumerate(docs)])
        except Exception as e:
            print(f"在`{args.url_lucene_task1}`进行法条检索时发生错误 ----> {e}")
            retrieval_law = ""
        prompt2_task1 = f"请根据下面相关法条回答问题\n相关法条:\n{retrieval_law}\n问题:\n{user_input}"
        response, _ = chat(prompt2_task1)
    elif task == "案例检索回复":
        # Task 2: 类案检索
        prompt1_task2 = f"请根据以下问题生成相关案例: {user_input}"
        generate_case, _ = chat(prompt1_task2)
        data = {"query": process_lucence_input(generate_case), "top_k": 1}
        try:
            response_retrieval = requests.post(args.url_lucene_task2, json=data)
            print(f"案例检索响应状态 ----> {response_retrieval},返回内容 ----> {response_retrieval.content}")
            docs = json.loads(response_retrieval.content)['docs']
            max_len = 1000
            retrieval_case = "\n".join([f"第{i+1}条:\n{doc[int(-max_len / len(docs)):]}" for i, doc in enumerate(docs)])
        except Exception as e:
            print(f"在`{args.url_lucene_task2}`进行案例检索时发生错误 ----> {e}")
            retrieval_case = ""
        prompt2_task2 = f"请根据下面相关案例回答问题\n相关案例:\n{retrieval_case}\n问题:\n{user_input}"
        response, _ = chat(prompt2_task2)
    elif task == "三段论推理判决":
        # Task 3: 三段论推理
        prompt_task3 = f"请根据基本案情,利用三段论的推理方式得到判决结果,判决结果包括:1.罪名;2.刑期。\n基本案情:{user_input}"
        response, _ = chat(prompt_task3)
    elif task == "司法对话":
        # Task 4: 司法对话
        response, history = chat(user_input, history)
    return response

# 创建 Gradio 界面
task_options = ["法条检索回复", "案例检索回复", "三段论推理判决", "司法对话"]
task_placeholders = {
    "法条检索回复": "欢迎使用 基于法条检索回复 任务,此任务中模型首先根据用户输入案情,模型生成相关法条;根据生成的相关法条检索真实法条;最后结合真实法条回答用户问题。您可以尝试输入以下内容:\n\n\n小李想开办一家个人独资企业,他需要准备哪些信息去进行登记注册?",
    "案例检索回复": "欢迎使用 基于案例检索回复 任务,此任务中模型首先根据用户输入案情,模型生成相关案例;根据生成的相关案例检索真实案例;最后结合真实案例回答用户问题。您可以尝试输入以下内容:\n\n\n被告人夏某在2007年至2010年期间,使用招商银行和广发银行的信用卡在北京纸老虎文化交流有限公司等地透支消费和取现。尽管经过银行多次催收,夏某仍欠下两家银行共计人民币26379.85元的本金。2011年3月15日,夏某因此被抓获,并在到案后坦白了自己的行为。目前,涉案的欠款已被还清。请问根据上述事实,该如何判罚夏某?",
    "三段论推理判决": "欢迎使用 三段论推理判决 任务,此任务中模型利用三段论的推理方式生成判决结果。您可以尝试输入以下内容:\n\n\n被告人陈某伙同王某(已判刑)在邵东县界岭乡峰山村艾窑小学法经营“地下六合彩”,由陈某负责联系上家,王某1负责接单卖码及接受投注,并约定将收受投注10%的提成按三七分成,陈某占三,王某1占七。该地下六合彩利用香港“六合彩”开奖结果作为中奖号码,买1到49中间的一个或几个数字,赔率为1:42。在香港六合彩开奖的当天晚上点三十分前,停止卖号,将当期购买的清单报给姓赵的上家。开奖后从网上下载香港六合彩的中奖号码进行结算赔付,计算当天的中奖数额,将当期卖出的总收入的百分之十留给自己,用总收入的百分之九十减去中奖的钱,剩余的为付给上家的钱。期间,二人共同经营“地下六合彩”40余期,收受吕某、吕永玉、王某2、王某3等人的投注额约25万余元,两人共计非法获利4万余元。被告人陈某于2013年11月18日被抓获,后被取保候审,在取保期间,被告人陈某脱逃。2015年1月21公安机关对其网上追逃。2017年6月21日被告人陈某某自动到公安机关投案。上述事实,被告人陈某在开庭审理过程中亦无异议,并有证人王某1、吕某、吕永玉、王某3等人的证言,扣押决定书,扣押物品清单,文件清单,抓获经过,刑事判决书,户籍证明等证据证实,足以认定。",
    "司法对话": "欢迎使用 司法对话 任务,此任务中您可以与模型进行直接对话。"
}

with gr.Blocks() as demo:
    gr.Markdown("## 夫子·明察 司法大模型 Web 服务")

    # 顶部任务选择
    with gr.Row():
        task = gr.Dropdown(label="选择任务", choices=task_options, value=task_options[0])

    # 下方 "用户输入" 和 "模型回复" 并列
    with gr.Row():
        with gr.Column():
            user_input = gr.Textbox(label="用户输入", placeholder=task_placeholders[task_options[0]], lines=16)
        with gr.Column():
            output = gr.Textbox(label="模型回复", lines=16)

    # 提交按钮放置在下方中央
    with gr.Row():
        submit_button = gr.Button("提交", elem_id="submit_button")

    # 动态更新 Placeholder
    def update_placeholder(selected_task):
        return task_placeholders[selected_task]

    task.change(fn=update_placeholder, inputs=[task], outputs=[user_input], show_progress=False)

    # 绑定按钮点击事件
    submit_button.click(handle_request, inputs=[task, user_input], outputs=output)

# 启动 Gradio 服务
demo.launch(server_name="0.0.0.0")