Model Gallery 快速入门

Model GalleryPAI-DLC、PAI-EAS进行了封装,帮助您零代码,高效快捷地部署和训练开源大模型。本文以Qwen3-0.6B模型为例,为您介绍如何使用Model Gallery。该流程同样适用于其他模型。

前提条件

使用主账号开通PAI并创建工作空间。登录PAI控制台,左上角选择开通区域,然后一键授权和开通产品。

计费说明

本文案例将使用公共资源创建DLC任务和EAS服务,计费方式为按量付费,详细计费规则请参见DLC计费说明EAS计费说明

模型部署

部署模型

  1. 登录PAI控制台在左侧导航栏单击Model Gallery,搜索并找到Qwen3-0.6B选项卡,然后单击部署

    image

  2. 配置部署参数。在部署配置页面已为您预置了默认参数,直接单击部署 > 确定。部署大约需要等待5分钟,当处于运行中时,代表部署成功。

    默认使用公共资源部署,计费方式为按量付费。

    image

调用模型

  1. 查看调用信息。在服务详情页面,单击查看调用信息获取调用地址Token

    后续查看部署任务详情,您可通过在左侧导航栏单击Model Gallery > 任务管理 > 部署任务,然后再单击服务名称查看。

    image

  2. 体验模型服务。常用的调用方式如下:

    在线调试

    单击切换至在线调试页面,在请求的content中输入问题如:你好,你是谁。然后单击发送请求,右侧将返回大模型回答的结果。

    image

    使用Cherry Studio客户端

    Cherry Studio 是业界主流的大模型对话客户端,且集成了 MCP 功能,您可以方便地与大模型进行对话。

    连接到在PAI 部署的Qwen3模型

    使用Python SDK

    from openai import OpenAI
    import os
    
    # 建议将Token设置为环境变量,防止敏感信息泄漏
    # 环境变量配置方法请参见:https://help.aliyun.com/zh/sdk/developer-reference/configure-the-alibaba-cloud-accesskey-environment-variable-on-linux-macos-and-windows-systems
    token = os.environ.get("Token")
    # <调用地址>后面有 “/v1”不要去除
    client = OpenAI(
        api_key=token,
        base_url=f'访问地址/v1',
    )
    
    query = '你好,你是谁'
    messages = [{'role': 'user', 'content': query}]
    
    resp = client.chat.completions.create(model='Qwen3-0.6B', messages=messages, max_tokens=512, temperature=0)
    query = messages[0]['content']
    response = resp.choices[0].message.content
    print(f'query: {query}')
    print(f'response: {response}')

重要提醒

本文使用了公共资源创建模型服务,计费方式为按量付费。当您不需要使用服务时请停止或删除服务,以免继续扣费

image

模型微调

如果您希望模型在特定领域表现更好,可以通过在该领域的数据集上对模型微调来实现。本文以如下场景为例为您介绍模型微调的作用和步骤。

场景示例

在物流领域,常需要从自然语言中提取结构化信息(如收件人、地址、电话)。直接使用大参数模型(如:Qwen3-235B-A22B)效果好,但成本高、响应慢。为兼顾效果与成本,可先用大参数模型标注数据,再用这些数据微调小参数模型(如 Qwen3-0.6B),使其在相同任务上达到相近表现。此过程也被称为模型蒸馏。

相同结构化信息提取任务,使用原始的Qwen3-0.6B模型准确率14%,微调后准确率可达90%以上。
该场景同样使用在解决方案《10分钟微调:让0.6B模型媲美235B模型》中,您也可以按照此解决方案操作。

收件人地址信息示例

结构化信息提取示例

长沙市岳麓区桃花岭路189号润丰园B1202室 | 电话021-17613435 | 联系人江雨桐

{
  "province": "湖南省",
  "city": "长沙市",
  "district": "岳麓区",
  "specific_location": "桃花岭路189号润丰园B1202室",
  "name": "江雨桐",
  "phone": "021-17613435"
}

数据准备

为将教师模型(Qwen3-235B-A22B)在该任务中的知识蒸馏到 Qwen3-0.6B,需先通过教师模型的 API,将收件人的地址信息抽取为结构化 JSON 数据。模型生成这些 JSON 数据可能需要较长时间,因此,本文已为您准备好了示例训练集train_qwen3.json和验证集eval_qwen3.json,您可以直接下载使用。

在模型蒸馏中大参数量模型也被称为教师模型。本文所用数据均为大模型模拟生成,不涉及用户敏感信息。

生产应用的数据获取建议

如果后续需要应用到实际业务中,我们建议您通过这些方法来准备数据:

真实业务场景(推荐)

真实的业务数据能更好地反映业务场景,微调出来的模型能更好地适配业务。获取业务数据后,您需要通过编程将您的业务数据转化为如下格式的 JSON文件。

[
    {
        "instruction": "你是一个专业的信息抽取助手,专门负责从中文文本中提取收件人的JSON信息,包含的Key有province(省份)、city(城市名称)、district(区县名称)、specific_location(街道、门牌号、小区、楼栋等详细信息)、name(收件人姓名)、phone(联系电话) 收件人为欧阳文斌;天津市河西区珠江道21号南开大学科技园区3号楼B座;手机号码:023-53932018",
        "output": "{\"province\": \"天津市\", \"city\": \"天津市\", \"district\": \"河西区\", \"specific_location\": \"珠江道21号南开大学科技园区3号楼B座\", \"name\": \"欧阳文斌\", \"phone\": \"023-53932018\"}"
    },
    {
        "instruction": "你是一个专业的信息抽取助手,专门负责从中文文本中提取收件人的JSON信息,包含的Key有province(省份)、city(城市名称)、district(区县名称)、specific_location(街道、门牌号、小区、楼栋等详细信息)、name(收件人姓名)、phone(联系电话) 南宁市青秀区竹溪大道38号金源城旺府6栋:联系方式:23952529750:收件人:农丽霞",
        "output": "{\"province\": \"广西壮族自治区\", \"city\": \"南宁市\", \"district\": \"青秀区\", \"specific_location\": \"竹溪大道38号金源城旺府6栋\", \"name\": \"农丽霞\", \"phone\": \"23952529750\"}"
    }
]

JSON文件中包含多个训练样本,每个样本包括instruction(指令)和output(标准答案)两个字段:

  • instruction:包含用于指引大模型行为的提示词,以及输入的数据;

  • output:期望的标准答案,通常由人类专家或大参数量的模型(如 qwen3-235b-a22b 等)生成;

大模型生成

在业务数据不够丰富时,可以考虑使用大模型做数据增强,使数据的多样性和覆盖范围得到提升。为了避免泄漏用户隐私,本方案使用大模型生成了一批虚拟的地址数据,如下生成代码供您参考。

模拟数据集生成的示例代码

本示例代码将调用阿里云百炼中的大模型服务,您需要获取百炼API Key。代码中使用 qwen-plus-latest 生成业务数据,使用qwen3-235b-a22b 模型进行打标。

# -*- coding: utf-8 -*-
import os
import asyncio
import random
import json
import sys
from typing import List, Dict
from openai import AsyncOpenAI
import platform

# 创建异步客户端实例
client = AsyncOpenAI(
    # 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)

# 中国省份列表
provinces = [
    "北京市", "天津市", "河北省", "山西省", "内蒙古自治区", "辽宁省", "吉林省", "黑龙江省",
    "上海市", "江苏省", "浙江省", "安徽省", "福建省", "江西省", "山东省", "河南省",
    "湖北省", "湖南省", "广东省", "广西壮族自治区", "海南省", "重庆市", "四川省", "贵州省",
    "云南省", "西藏自治区", "陕西省", "甘肃省", "青海省", "宁夏回族自治区", "新疆维吾尔自治区"
]

# 收件人写法模板
recipient_templates = [
    "收件人{name}", "收件人:{name}", "收件人是{name}", "收件:{name}",
    "收件人为{name}", "{name}", "姓名:{name}", "姓名{name}",
    "联系人{name}", "联系人:{name}", "接收人{name}", "接收人:{name}",
    "收货人{name}", "收货人:{name}", "寄给{name}", "给{name}",
    "收件者{name}", "收件者:{name}", "接收者{name}", "接收者:{name}"
]

# 电话号码写法模板
phone_templates = [
    "tel:{phone}", "tel:{phone}", "mobile:{phone}", "mobile:{phone}",
    "手机号码{phone}", "手机号码:{phone}", "手机:{phone}", "手机{phone}",
    "电话:{phone}", "电话{phone}", "联系电话{phone}", "联系电话:{phone}",
    "号码:{phone}", "号码{phone}", "TEL:{phone}", "MOBILE:{phone}",
    "contact:{phone}", "phone:{phone}", "{phone}", "call:{phone}",
    "联系方式{phone}", "联系方式:{phone}", "电话号码{phone}", "电话号码:{phone}",
    "手机号{phone}", "手机号:{phone}", "电话号码是{phone}", "联系电话是{phone}"
]

# 生成虚拟手机号码(以2开头避免与真实号码重合)
def generate_mobile():
    prefixes = ['200', '201', '202', '203', '204', '205', '206', '207', '208', '209',
               '210', '211', '212', '213', '214', '215', '216', '217', '218', '219',
               '220', '221', '222', '223', '224', '225', '226', '227', '228', '229',
               '230', '231', '232', '233', '234', '235', '236', '237', '238', '239']
    return random.choice(prefixes) + ''.join([str(random.randint(0, 9)) for _ in range(8)])

# 生成固定电话
def generate_landline():
    area_codes = ['010', '021', '022', '023', '024', '025', '027', '028', '029', '0311', '0351', '0431', '0451']
    area_code = random.choice(area_codes)
    number = ''.join([str(random.randint(0, 9)) for _ in range(random.choice([7, 8]))])
    return f"{area_code}-{number}"

# 使用大模型生成收件人信息和地址信息
async def generate_recipient_and_address_by_llm(province: str):
    """使用大模型生成指定省份的收件人姓名和地址信息"""
    prompt = f"""请为{province}生成一个收件人的信息,包含:
1. 一个真实的中文姓名(可以是常见姓名,也可以是不那么常见的,要多样化)
2. 该省份下的一个城市名称
3. 该城市下的一个行政区名称(如区、县等)
4. 一个具体的街道地址(如路名+门牌号、小区名+楼栋号、商业大厦+楼层等,要真实)

请直接返回JSON格式:
{{"name": "收件人姓名", "city": "城市名", "district": "行政区名", "specific_location": "具体地址"}}

不要包含任何其他内容,只返回JSON。姓名要多样化,不要总是常见的张三李四。"""

    try:
        response = await client.chat.completions.create(
            messages=[{"role": "user", "content": prompt}],
            model="qwen-plus-latest",
            temperature=1.7,  # 提高温度让姓名更多样化
        )
        
        result = response.choices[0].message.content.strip()
        # 清理可能的markdown代码块标记
        if result.startswith('```'):
            result = result.split('\n', 1)[1]
        if result.endswith('```'):
            result = result.rsplit('\n', 1)[0]
        
        # 尝试解析JSON
        info = json.loads(result)
        return info
    except Exception as e:
        print(f"生成收件人和地址失败: {e}, 使用备用方案")
        # 备用方案
        backup_names = ["王建军", "李春燕", "张志华", "陈美玲", "刘德强", "赵敏慧", "孙文博", "周晓丽"]
        return {
            "name": random.choice(backup_names),
            "city": f"{province.replace('省', '').replace('市', '').replace('自治区', '')}市",
            "district": "市辖区", 
            "specific_location": f"人民路{random.randint(1, 999)}号"
        }

# 生成一条记录
async def generate_record():
    # 随机选择省份
    province = random.choice(provinces)
    
    # 使用大模型生成收件人和地址信息
    info = await generate_recipient_and_address_by_llm(province)
    
    # 生成收件人信息格式
    recipient = random.choice(recipient_templates).format(name=info['name'])
    
    # 生成电话号码(70%概率手机号,30%概率固话)
    if random.random() < 0.7:
        phone = generate_mobile()
    else:
        phone = generate_landline()
    
    phone_info = random.choice(phone_templates).format(phone=phone)
    
    # 组装地址
    full_address = f"{info['city']}{info['district']}{info['specific_location']}"
    
    # 组装数据
    components = [recipient, phone_info, full_address]
    
    # 随机打乱顺序
    random.shuffle(components)
    
    # 随机选择分割符
    separators = [' ', ',', ',', ';', ';', ':', ':', '、', '|', '\t', '', '  ', ' | ', ' , ', ' ; ', '/']
    separator = random.choice(separators)
    
    # 合并数据
    if separator == '':
        # 没有分割符的情况
        combined_data = ''.join(components)
    else:
        combined_data = separator.join(components)    
    return combined_data

# 生成批量数据
async def generate_batch_data(count: int) -> List[str]:
    """生成指定数量的数据"""
    print(f"开始生成 {count} 条数据...")
    data = []
    
    # 使用信号量控制并发数量,QPM=1500,设置为20个并发
    semaphore = asyncio.Semaphore(20)
    
    async def generate_single_record(index):
        async with semaphore:
            record = await generate_record()
            print(f"生成第{index+1}条数据: {record}")
            return record
    
    # 并发生成数据
    tasks = [generate_single_record(i) for i in range(count)]
    data = await asyncio.gather(*tasks)
    
    return data

# 保存数据到文件
def save_data(data: List[str], filename: str = "recipient_data.json"):
    """保存数据到JSON文件"""
    with open(filename, 'w', encoding='utf-8') as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
    print(f"数据已保存到 {filename}")

# 数据生成阶段
async def produce_data_phase():
    print("=== 第一阶段:开始生成收件人数据 ===")
    
    # 生成2000条数据
    batch_size = 2000
    data = await generate_batch_data(batch_size)
    
    # 保存数据
    save_data(data)
    
    print(f"\n总共生成了 {len(data)} 条数据")
    print("\n示例数据:")
    for i, record in enumerate(data[:3]):  # 显示前3条作为示例
        print(f"{i+1}. 原始数据: {record}")
        print()
    
    print("=== 第一阶段完成 ===\n")
    return True

def get_system_prompt():
    """返回系统提示词"""
    return """你是一个专业的信息抽取助手,专门负责从中文文本中提取收件人的结构化信息。

## 任务说明
请根据给定的输入文本,准确提取并生成包含以下六个字段的JSON格式输出:
- province: 省份/直辖市/自治区(必须是完整的官方名称,如"河南省"、"上海市"、"新疆维吾尔自治区"等)
- city: 城市名称(包含"市"字,如"郑州市"、"西安市"等)
- district: 区县名称(包含"区"、"县"等,如"金水区"、"雁塔区"等)
- specific_location: 具体地址(街道、门牌号、小区、楼栋等详细信息)
- name: 收件人姓名(完整的中文姓名)
- phone: 联系电话(完整的电话号码,包括区号)

## 抽取规则
1. **地址信息处理**:
   - 必须准确识别省、市、区的层级关系
   - 省份名称必须使用官方全称(如"河南省"而非"河南")
   - 直辖市的province和city字段应该相同(如都填"上海市")
   - specific_location应包含详细的街道地址、小区名称、楼栋号等

2. **姓名识别**:
   - 准确提取完整的中文姓名,包括复姓
   - 包括少数民族姓名

3. **电话号码处理**:
   - 提取完整的电话号码,保持原有格式

## 输出格式
请严格按照以下JSON格式输出,不要添加任何解释性文字:
{
  "province": "省份名称",
  "city": "城市名称", 
  "district": "区县名称",
  "specific_location": "详细地址",
  "name": "收件人姓名",
  "phone": "联系电话"
}"""

# 使用大模型预测结构化数据
async def predict_structured_data(raw_data: str):
    """使用qwen3-235b-a22b模型预测结构化数据"""
    system_prompt = get_system_prompt()
    
    try:
        response = await client.chat.completions.create(
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": raw_data}
            ],
            model="qwen3-235b-a22b",
            temperature=0.1,  # 降低温度以提高预测准确性
            response_format={"type": "json_object"},
            extra_body={"enable_thinking":False}
        )
        
        result = response.choices[0].message.content.strip()
        
        # 清理可能的markdown代码块标记
        if result.startswith('```'):
            lines = result.split('\n')
            for i, line in enumerate(lines):
                if line.strip().startswith('{'):
                    result = '\n'.join(lines[i:])
                    break
        if result.endswith('```'):
            result = result.rsplit('\n```', 1)[0]
        
        # 尝试解析JSON
        structured_data = json.loads(result)
        return structured_data
        
    except Exception as e:
        print(f"预测结构化数据失败: {e}, 原始数据: {raw_data}")
        # 返回空的结构化数据作为备用
        return {
            "province": "",
            "city": "",
            "district": "",
            "specific_location": "",
            "name": "",
            "phone": ""
        }

# 数据转换阶段
async def convert_data_phase():
    """转换数据格式并使用大模型预测结构化数据"""
    print("=== 第二阶段:开始转换数据格式 ===")
    
    try:
        print("开始读取recipient_data.json文件...")
        
        # 读取原始数据
        with open('recipient_data.json', 'r', encoding='utf-8') as f:
            raw_data_list = json.load(f)
        
        print(f"成功读取数据,共有 {len(raw_data_list)} 条记录")
        print("开始使用qwen3-235b-a22b模型预测结构化数据...")
        # 使用简单与明确的system message 有助于训练与推理速度的提高
        system_prompt = "你是一个专业的信息抽取助手,专门负责从中文文本中提取收件人的JSON信息,包含的Key有province(省份)、city(城市名称)、district(区县名称)、specific_location(街道、门牌号、小区、楼栋等详细信息)、name(收件人姓名)、phone(联系电话) 现在输入如下:" 
        output_file = 'recipient_sft_data.json'
        
        # 使用信号量控制并发数量
        semaphore = asyncio.Semaphore(10)
        
        async def process_single_item(index, raw_data):
            async with semaphore:
                # 使用大模型预测结构化数据
                structured_data = await predict_structured_data(raw_data)
                print(f"处理第{index+1}条数据: {raw_data}")

                conversation = {
                    "instruction": system_prompt + raw_data,
                    "output": json.dumps(structured_data, ensure_ascii=False)
                }
            
                return conversation
        
        print(f"开始转换数据到 {output_file}...")
        
        # 并发处理所有数据
        tasks = [process_single_item(i, raw_data) for i, raw_data in enumerate(raw_data_list)]
        conversations = await asyncio.gather(*tasks)

        with open(output_file, 'w', encoding='utf-8') as outfile:
            json.dump(conversations, outfile, ensure_ascii=False, indent=4)
        
        print(f"转换完成!共处理 {len(raw_data_list)} 条记录")
        print(f"输出文件:{output_file}")
        print("=== 第二阶段完成 ===")
        
    except FileNotFoundError:
        print("错误:找不到 recipient_data.json 文件")
        sys.exit(1)
    except json.JSONDecodeError as e:
        print(f"JSON解析错误:{e}")
        sys.exit(1)
    except Exception as e:
        print(f"转换过程中发生错误:{e}")
        sys.exit(1)

# 主函数
async def main():
    print("开始执行合并的数据处理流程...")
    print("这个程序将依次执行两个阶段:")
    print("1. 生成原始收件人数据")
    print("2. 使用qwen3-235b-a22b模型预测结构化数据并转换为SFT训练格式")
    print("-" * 50)
    
    # 第一阶段:生成数据
    success = await produce_data_phase()
    
    if success:
        # 第二阶段:转换数据
        await convert_data_phase()
        
        print("\n" + "=" * 50)
        print("全部流程执行完成!")
        print("生成的文件:")
        print("- recipient_data.json: 原始数据列表")
        print("- recipient_sft_data.jsonl: SFT训练格式数据")
        print("=" * 50)
    else:
        print("数据生成阶段失败,终止执行")

if __name__ == '__main__':
    # 设置事件循环策略
    if platform.system() == 'Windows':
        asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
    # 运行主协程
    asyncio.run(main(), debug=False) 

微调模型

  1. 在左侧导航栏单击Model Gallery,搜索并找到Qwen3-0.6B选项卡,然后单击训练

    image

  2. 配置训练任务参数。只需配置如下关键参数,其他参数默认即可。

    • 训练方式:默认选择SFT监督微调LoRA微调方法。

      LoRA是一种高效的模型微调技术,仅修改模型的部分参数,以节省训练使用的资源。
    • 训练数据集:先单击下载示例训练集train_qwen3.json,然后配置页面选择OSS文件或目录,单击image图标选择Bucket,单击上传文件将下载训练集上传到OSS中,并选择该文件。

      image

    • 验证数据集:先单击下载验证集eval_qwen3.json,然后单击添加验证数据集,按照和配置训练集相同的操作,上传并选择该文件。

      验证集用于在训练过程中判断模型的性能,帮助评估模型在未见过的数据上的表现。
    • 模型输出路径:默认会将微调后的模型存储到OSS中,如果OSS目录为空,请您新建目录并指定该目录。

    • 资源组类型:选择使用公共资源组,本次微调大约需要5GB显存,控制台中已经为您筛选出满足要求的规格,选择如:ecs.gn7i-c16g1.4xlarge

      后续部署其他模型时,您可参考估算大模型所需显存,计算模型训练需要的显存大小。
    • 超参数配置

      • learning_rate: 设置为0.0005

      • num_train_epochs:设置为4

      • per_device_train_batch_size:设置为8

      • seq_length:设置为512

      此超参数配置下模型在本文的测试数据上表现较好。当您使用模型微调解决您自己的业务问题时,如果准确率不高,也可以尝试调整超参数。您可以通过学习阿里云大模型 ACP 课程,深入了解超参数的作用,以及如何通过损失曲线来判断超参数的调整方向。

      然后单击训练 > 确定,训练任务进入创建中状态,当处于运行中时,开始微调模型。

  3. 查看训练任务并等待训练完成。模型微调大约需要10分钟,在微调过程中,任务详情页面将展示任务日志及指标曲线,待训练执行完成后,微调后的模型将存储到设置的OSS目录中。

    后续查看训练任务详情,您可通过在左侧导航栏单击Model Gallery > 任务管理 > 训练任务,然后再单击任务名称查看。

    image

    (可选)根据loss图像,调整超参数,提升模型效果

    在任务详情页面可以分别看到train_loss曲线(反映训练集损失)与 eval_loss曲线(反映验证集损失):

    imageimage

    您可以根据损失值的变化趋势,初步判断当前模型的训练效果:

    • 在结束训练前 train_loss 与 eval_loss 仍有下降趋势(欠拟合)

      您可以增加 num_train_epochs(训练轮次,与训练深度正相关) 参数,或适当增大 lora_rank(低秩矩阵的秩,秩越大,模型能表达更复杂的任务,但更容易过度训练)的值后再进行训练,加大模型的对训练数据的拟合程度;

    • 在结束训练前 train_loss 持续下降,eval_loss 开始变大(过拟合)

      您可以减少 num_train_epochs 参数,或适当减小lora_rank的值后再进行训练,防止模型过度训练;

    • 在结束训练前 train_loss 与 eval_loss 均处于平稳状态(良好拟合)

      模型处于该状态时,您可以进行后续步骤。

    受限于篇幅,本方案无法对微调参数做过多讲解。您可以学习阿里云大模型 ACP 课程 来了解微调命令中的关键参数、以及如何通过损失曲线来决定是否应该继续微调等细节。

部署微调后的模型

在训练任务详情页,单击部署按钮打开部署配置页,资源类型选择公共资源,部署0.6B的模型大约需要5GB显存,资源规格中已为您筛选出满足要求的规格,选择如:ecs.gn7i-c8g1.2xlarge,其他参数保持默认即可,然后单击部署 > 确定

部署过程大约需要5分钟,当处于运行中时,代表部署成功。

后续查看训练任务详情,您可通过在左侧导航栏单击Model Gallery > 任务管理 > 训练任务,然后再单击任务名称查看。

image

训练任务显示已成功后,如果部署按钮无法点击,代表输出的模型还在注册中,需要等待大约1分钟。

image

后续的模型调用步骤与调用模型相同。

验证微调后模型效果

在将微调后的模型部署到实际业务环境前,建议您先对其效果进行系统性的评测,确保模型具备良好的稳定性和准确性,避免上线后出现意料之外的问题。

准备测试数据

准备与训练数据不重合的测试数据,用于测试模型效果。本方案已为您准备好了测试集,在执行下面的准确率测试代码时会自动下载。

测试数据与训练数据不重合样本,这样可以更准确地反映模型在新数据上的泛化能力,避免因“见过的样本”导致分数虚高。

设计评测指标

评测标准应紧贴实际业务目标。以本方案为例,除了判断生成的 JSON 字符串是否合法,还应该关注对应的 Key、Value 的值是否正确。

您需要通过编程来定义评测指标,本案例评测指标实现,请参见下面准确率测试代码的compare_address_info方法。

验证模型微调后效果

执行如下测试代码,将输出模型在测试集上的准确率。

测试模型准确率代码示例

注意:请将Token、调用地址替换为您上文中获取的真实调用信息。

from openai import AsyncOpenAI
import requests
import json
import asyncio
import os


# 建议将Token设置为环境变量,防止敏感信息泄漏
# 环境变量配置方法请参见:https://help.aliyun.com/zh/sdk/developer-reference/configure-the-alibaba-cloud-accesskey-environment-variable-on-linux-macos-and-windows-systems
client = AsyncOpenAI(
    api_key=os.getenv("Token"),
    base_url="调用地址/v1"
)

# 您也可以调用百炼的Qwen3-0.6b模型,测试原始模型的准确率,但注意需要将 model="Qwen3-0.6B" 改成 model="qwen3-0.6b"
# client = AsyncOpenAI(
#     api_key=os.getenv("DASHSCOPE_API_KEY"),
#     base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
# )

system_prompt = """你是一个专业的信息抽取助手,专门负责从中文文本中提取收件人的结构化信息。

## 任务说明
请根据给定的输入文本,准确提取并生成包含以下六个字段的JSON格式输出:
- province: 省份/直辖市/自治区(必须是完整的官方名称,如"河南省"、"上海市"、"新疆维吾尔自治区"等)
- city: 城市名称(包含"市"字,如"郑州市"、"西安市"等)
- district: 区县名称(包含"区"、"县"等,如"金水区"、"雁塔区"等)
- specific_location: 具体地址(街道、门牌号、小区、楼栋等详细信息)
- name: 收件人姓名(完整的中文姓名)
- phone: 联系电话(完整的电话号码,包括区号)

## 抽取规则
1. **地址信息处理**:
   - 必须准确识别省、市、区的层级关系
   - 省份名称必须使用官方全称(如"河南省"而非"河南")
   - 直辖市的province和city字段应该相同(如都填"上海市")
   - specific_location应包含详细的街道地址、小区名称、楼栋号等

2. **姓名识别**:
   - 准确提取完整的中文姓名,包括复姓
   - 包括少数民族姓名

3. **电话号码处理**:
   - 提取完整的电话号码,保持原有格式

## 输出格式
请严格按照以下JSON格式输出,不要添加任何解释性文字:
{
  "province": "省份名称",
  "city": "城市名称", 
  "district": "区县名称",
  "specific_location": "详细地址",
  "name": "收件人姓名",
  "phone": "联系电话"
}"""


def compare_address_info(actual_address_str, predicted_address_str):
    """比较两个JSON字符串表示的地址信息是否相同"""
    try:
        # 解析实际地址信息
        if actual_address_str:
            actual_address_json = json.loads(actual_address_str)
        else:
            actual_address_json = {}

        # 解析预测地址信息
        if predicted_address_str:
            predicted_address_json = json.loads(predicted_address_str)
        else:
            predicted_address_json = {}

        # 直接比较两个JSON对象是否完全相同
        is_same = actual_address_json == predicted_address_json

        return {
            "is_same": is_same,
            "actual_address_parsed": actual_address_json,
            "predicted_address_parsed": predicted_address_json,
            "comparison_error": None
        }

    except json.JSONDecodeError as e:
        return {
            "is_same": False,
            "actual_address_parsed": None,
            "predicted_address_parsed": None,
            "comparison_error": f"JSON解析错误: {str(e)}"
        }
    except Exception as e:
        return {
            "is_same": False,
            "actual_address_parsed": None,
            "predicted_address_parsed": None,
            "comparison_error": f"比较错误: {str(e)}"
        }


async def predict_single_conversation(conversation_data):
    """预测单个对话的标签"""
    try:
        # 提取user content(去除assistant message)
        messages = conversation_data.get("messages", [])
        user_content = None

        for message in messages:
            if message.get("role") == "user":
                user_content = message.get("content", "")
                break

        if not user_content:
            return {"error": "未找到用户消息"}

        response = await client.chat.completions.create(
            model="Qwen3-0.6B",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_content}
            ],
            response_format={"type": "json_object"},
            extra_body={
                "enable_thinking": False
            }
        )

        predicted_labels = response.choices[0].message.content.strip()
        return {"prediction": predicted_labels}

    except Exception as e:
        return {"error": f"预测失败: {str(e)}"}


async def process_batch(batch_data, batch_id):
    """处理一批数据"""
    print(f"处理批次 {batch_id},包含 {len(batch_data)} 条数据...")

    tasks = []
    for i, conversation in enumerate(batch_data):
        task = predict_single_conversation(conversation)
        tasks.append(task)

    results = await asyncio.gather(*tasks, return_exceptions=True)

    batch_results = []
    for i, result in enumerate(results):
        if isinstance(result, Exception):
            batch_results.append({"error": f"异常: {str(result)}"})
        else:
            batch_results.append(result)

    return batch_results


async def main():
    output_file = "predicted_labels.jsonl"
    batch_size = 20  # 每批处理的数据量

    # 读取测试数据
    url = 'https://help-static-aliyun-doc.aliyuncs.com/file-manage-files/zh-CN/20250616/ssrgii/test.jsonl'
    conversations = []

    try:
        response = requests.get(url)
        response.raise_for_status()  # 检查请求是否成功
        for line_num, line in enumerate(response.text.splitlines(), 1):
            try:
                data = json.loads(line.strip())
                conversations.append(data)
            except json.JSONDecodeError as e:
                print(f"第 {line_num} 行JSON解析错误: {e}")
                continue
    except requests.exceptions.RequestException as e:
        print(f"请求错误: {e}")
        return

    print(f"成功读取 {len(conversations)} 条对话数据")

    # 分批处理
    all_results = []
    total_batches = (len(conversations) + batch_size - 1) // batch_size

    for batch_id in range(total_batches):
        start_idx = batch_id * batch_size
        end_idx = min((batch_id + 1) * batch_size, len(conversations))
        batch_data = conversations[start_idx:end_idx]

        batch_results = await process_batch(batch_data, batch_id + 1)
        all_results.extend(batch_results)

        print(f"批次 {batch_id + 1}/{total_batches} 完成")

        # 添加小延迟避免请求过快
        if batch_id < total_batches - 1:
            await asyncio.sleep(1)

    # 保存结果
    same_count = 0
    different_count = 0
    error_count = 0

    with open(output_file, 'w', encoding='utf-8') as f:
        for i, (original_data, prediction_result) in enumerate(zip(conversations, all_results)):
            result_entry = {
                "index": i,
                "original_user_content": None,
                "actual_address": None,
                "predicted_address": None,
                "prediction_error": None,
                "address_comparison": None
            }

            # 提取原始用户内容
            messages = original_data.get("messages", [])
            for message in messages:
                if message.get("role") == "user":
                    result_entry["original_user_content"] = message.get("content", "")
                    break

            # 提取实际地址信息(如果存在assistant message)
            for message in messages:
                if message.get("role") == "assistant":
                    result_entry["actual_address"] = message.get("content", "")
                    break

            # 保存预测结果
            if "error" in prediction_result:
                result_entry["prediction_error"] = prediction_result["error"]
                error_count += 1
            else:
                result_entry["predicted_address"] = prediction_result.get("prediction", "")

                # 比较地址信息
                comparison_result = compare_address_info(
                    result_entry["actual_address"],
                    result_entry["predicted_address"]
                )
                result_entry["address_comparison"] = comparison_result

                # 统计比较结果
                if comparison_result["comparison_error"]:
                    error_count += 1
                elif comparison_result["is_same"]:
                    same_count += 1
                else:
                    different_count += 1

            f.write(json.dumps(result_entry, ensure_ascii=False) + '\n')

    print(f"所有预测完成! 结果已保存到 {output_file}")

    # 统计结果
    success_count = sum(1 for result in all_results if "error" not in result)
    prediction_error_count = len(all_results) - success_count
    print(f"样本数: {success_count} 条")
    print(f"响应正确: {same_count} 条")
    print(f"响应错误: {different_count} 条")
    print(f"准确率: {same_count * 100 / success_count} %")


if __name__ == "__main__":
    asyncio.run(main())

输出结果:

所有预测完成! 结果已保存到 predicted_labels.jsonl
样本数: 400 条
响应正确: 361 条
响应错误: 39 条
准确率: 91.25 %
因模型微调随机数种子和大模型输出随机性的影响,您测试出的准确率可能与本方案的结果存在差异,这属于正常情况。

可以看到准确率为 91.25%,相比于原始Qwen3-0.6B模型的14%的准确率提高了很多,表明微调后的模型显著增强了在物流填单领域结构化信息抽取的能力。

本文为了您学习过程中减少训练时间,只设置 4 个训练轮次,准确率就已提升至 91.25%。您可以适当增加训练轮次来进一步提升准确率。其他场景下,建议您参考阿里云大模型 ACP 课程内容了解如何调整超参数。

重要提醒

本文使用了公共资源创建模型服务,计费方式为按量付费。当您不需要使用服务时请停止或删除服务,以免继续扣费

image

相关文档

  • 更多Model Gallery功能如评测,压缩等,请参见Model Gallery

  • 更多EAS功能如弹性伸缩、压测、监控报警,请参见EAS概述