代碼策略:能落在 Java 工程里的部分(教師 API 調(diào)用、標(biāo)注校驗(yàn)、評(píng)測(cè)指標(biāo)、數(shù)據(jù)加載)用 JDK 17 + Jackson + HttpClient 實(shí)現(xiàn);LoRA 訓(xùn)練、ms-swift、ModelScope 本地推理 等目前仍以 Python 生態(tài) 為主。
寫在前面:我為什么要整理這篇
做 AI 應(yīng)用落地時(shí),我習(xí)慣先把鏈路拆清楚:RAG 查政策、Agent 調(diào)工具,問答能力有了,并不等于線上就省心。真實(shí)用戶很少「一條消息一個(gè)意圖」——報(bào)銷和出差擠在一句話里、VPN 連不上卻不說急不急,系統(tǒng)得先讀懂再路由,然后才是 RAG 或工具。
我一開始也和大廠做法一樣:每條消息都走一遍大模型做請(qǐng)求理解。效果穩(wěn),但賬單不好看——很多流量在「還沒開始回答問題」時(shí)就燒掉了 API 額度。自然想到:能不能用更小、更便宜的模型專門干「讀懂 + 結(jié)構(gòu)化」?
試過直接把 Qwen3-0.6B 這類小模型頂上去,很快撞墻:JSON 經(jīng)常格式錯(cuò)誤,多意圖識(shí)別也不穩(wěn)。后來把精力放在蒸餾上:用大模型當(dāng)老師生成標(biāo)注,再訓(xùn)小模型。下面是我梳理后的完整筆記,希望對(duì)在 Java 棧做 AI 應(yīng)用、又要控推理成本的朋友有點(diǎn)幫助。
| 員工提問(真實(shí)口語) | 系統(tǒng)需要先理解什么 |
|---|---|
| 我下周三要出差去杭州,順便催一下上個(gè)月的報(bào)銷 | 兩個(gè)意圖、目的地、時(shí)間 → 多意圖拆分 |
| VPN 連不上急死了 | IT 支持、高緊急度 → 優(yōu)先直連處理 |
| 新來的實(shí)習(xí)生需要開通哪些系統(tǒng)權(quán)限 | 入職 + 權(quán)限 → 多半走 RAG 查制度 |
蒸餾在這里的角色很直白:不是讓小模型憑空學(xué)會(huì)業(yè)務(wù),而是把大模型已經(jīng)做對(duì)的判斷,沉淀成訓(xùn)練數(shù)據(jù),再「搬」給小模型。
這篇文章會(huì)寫什么
整篇按「能落地、能復(fù)現(xiàn)」組織,主要包含:
- 任務(wù)怎么定:把自然語言提問收成 JSON 工單(意圖、部門、緊急度、實(shí)體、路由)。
- 蒸餾在干什么:和微調(diào)差在哪、黑盒數(shù)據(jù)合成為什么適合 API 教師。
- 數(shù)據(jù)從哪來:教師標(biāo)注、Schema 過濾、ms-swift 訓(xùn)練格式——附 Java / Python 代碼。
- 怎么證明有效:JSON 合規(guī)率、意圖 F1、路由準(zhǔn)確率;基座 0.6B vs 教師模型的 Baseline 對(duì)比。
- Java 側(cè)怎么接:調(diào)用、校驗(yàn)、評(píng)測(cè);訓(xùn)練與推理仍用 Python 棧時(shí)的分工。
- 上線怎么想:小模型扛高頻理解、失敗時(shí) fallback 大模型或轉(zhuǎn)人工。
動(dòng)手前:環(huán)境怎么配
我本地復(fù)現(xiàn)時(shí)的分工是:Java 只跑教師調(diào)用 + 校驗(yàn) + 評(píng)測(cè);要跑通「合成數(shù)據(jù) → 基座評(píng)測(cè) → LoRA」再開 GPU + Python。下面是我實(shí)際用到的依賴清單。
GPU / Python 訓(xùn)練環(huán)境(可選)
- 云 GPU:阿里云 PAI-DSW、AutoDL、本地工作站等均可;顯存建議 ≥ 24GB(0.6B LoRA 訓(xùn)練相對(duì)輕量)。
-
鏡像:帶 PyTorch 2.3+、CUDA 12.x 的 Python 3.11 環(huán)境即可;若用 Qwen3,注意
transformers>=4.51,<5.0。
加載 API Key 與訓(xùn)練依賴
Python(GPU / 實(shí)驗(yàn)環(huán)境)
import os
from openai import OpenAI
# 建議通過環(huán)境變量注入,勿寫入代碼倉庫
# export DASHSCOPE_API_KEY=你的密鑰
client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url=os.getenv(
"DASHSCOPE_BASE_URL",
"https://dashscope.aliyuncs.com/compatible-mode/v1"
)
)
print(f'API Key 已加載:{os.environ["DASHSCOPE_API_KEY"][:5]+"*"*5}')
# 安裝訓(xùn)練專用依賴(基礎(chǔ)依賴已由安裝腳本完成,此處僅安裝 LoRA 訓(xùn)練所需的額外組件)
#
# 版本約束說明:
# transformers >=4.51 — Qwen3 架構(gòu)支持(4.45 沒有 qwen3,加載模型會(huì)報(bào) architecture not recognized)
# transformers <5.0 — 5.x 要求 PyTorch>=2.4,需與當(dāng)前鏡像中的 PyTorch 版本匹配
# ms-swift >=3.0 — Qwen3 訓(xùn)練支持(2.x 不支持 Qwen3,且參數(shù)名與 4.x 不同)
# modelscope >=1.20 — 舊版 modelscope 加載 Qwen3 時(shí)會(huì)委托給 transformers 失敗
%pip install -i https://mirrors.aliyun.com/pypi/simple/ 'transformers>=4.51,<5.0' 'ms-swift>=3.0' accelerate autoawq autoawq-kernels 'modelscope>=1.20.0'
Java(本地 / Spring 服務(wù)側(cè):API 調(diào)用與評(píng)測(cè))
和日常 Spring 項(xiàng)目一樣,引入 Jackson,用環(huán)境變量注入密鑰即可:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.17.2</version>
</dependency>
export DASHSCOPE_API_KEY=你的密鑰
# 可選:export DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode
下載基座模型
從 ModelScope 下載 Qwen3-0.6B 到本地,用于基座模型評(píng)測(cè)與后續(xù) LoRA 訓(xùn)練。
說明:模型下載與本地 GPU 推理目前依賴 ModelScope / PyTorch 生態(tài),保留 Python 命令。
# 下載 Qwen3-0.6B 模型(約 1.2GB,下載約 1-2 分鐘)
!modelscope download --model Qwen/Qwen3-0.6B --local_dir /mnt/workspace/model
一、任務(wù)設(shè)計(jì):把「讀懂問題」單獨(dú)拎出來
1.1 為什么值得單獨(dú)做一個(gè)「請(qǐng)求理解」層
在我做過的企業(yè)答疑架構(gòu)里,RAG 和 Agent 各管一塊;但真正上線后,第一條消息進(jìn)來,往往要先回答:該直連答復(fù)、走 RAG、拆成多段,還是轉(zhuǎn)人工?
- 簡(jiǎn)單的事實(shí)問題(「公司 WiFi 密碼是多少」)→ 直接回答
- 需要查政策的問題(「年假可以跨年使用嗎」)→ RAG
- 包含多個(gè)獨(dú)立請(qǐng)求(「請(qǐng)假 + 催報(bào)銷」)→ 拆分后分別處理
- 「我要投訴我的主管」→ 轉(zhuǎn)人工
這個(gè)判斷過程就是「請(qǐng)求理解」:把一句自然語言,轉(zhuǎn)成結(jié)構(gòu)化的工單信息。
同倉庫里的
MediaOpsIntentGateApplicationService是類似思路:規(guī)則先攔風(fēng)險(xiǎn),再由 LLM 輸出 JSON 決定放行 / 澄清 / 阻斷——和上文這套「請(qǐng)求理解 + 路由」是同一類問題,只是業(yè)務(wù)字段不同。
1.2 結(jié)構(gòu)化工單的定義
這類場(chǎng)景里,我習(xí)慣把工單收成下面五個(gè)字段(也是后文蒸餾與評(píng)測(cè)的錨點(diǎn)):
{
"intents": ["差旅申請(qǐng)", "報(bào)銷催辦"],
"department": "行政",
"urgency": "中",
"entities": {"出差日期": "下周三", "目的地": "杭州"},
"route": "multi_intent_split"
}
| 字段 | 類型 | 取值范圍 | 說明 |
|---|---|---|---|
| intents | 字符串?dāng)?shù)組 | 入職辦理、考勤請(qǐng)假、差旅申請(qǐng)、報(bào)銷催辦、年假查詢、IT支持、權(quán)限申請(qǐng)、制度咨詢 | 可多選 |
| department | 字符串 | HR、行政、IT、財(cái)務(wù) | 主要負(fù)責(zé)部門 |
| urgency | 字符串 | 高、中、低 | 根據(jù)時(shí)間敏感度和措辭判斷 |
| entities | 對(duì)象 | 自由鍵值對(duì) | 從提問中提取的關(guān)鍵參數(shù)(日期、金額、人名等) |
| route | 字符串 | direct_answer、rag_query、multi_intent_split、escalate | 路由決策 |
1.3 這個(gè)任務(wù)為什么適合蒸餾
「請(qǐng)求理解」有三個(gè)特點(diǎn),決定了它適合用小模型來做:
- 輸出格式固定(JSON schema 確定,不需要開放式生成)
- 不依賴外部知識(shí)(判斷意圖和緊急度只看輸入文本本身)
- 每條請(qǐng)求都要執(zhí)行(高頻調(diào)用鏈路,成本敏感)
大模型已經(jīng)能做好這件事了。蒸餾的目標(biāo)是:把大模型在這個(gè)任務(wù)上的判斷能力,遷移給一個(gè)推理成本更低的小模型。
二、蒸餾原理:和微調(diào)差在哪
2.1 我理解的「蒸餾」一句話
微調(diào)更像「教會(huì)模型做一件事」,數(shù)據(jù)多半來自人工標(biāo)注。蒸餾則是「讓強(qiáng)模型當(dāng)老師」,用它的輸出當(dāng)訓(xùn)練集,再訓(xùn)小模型——訓(xùn)練流程仍是 SFT,差別主要在數(shù)據(jù)從哪來:
| 微調(diào) | 蒸餾 | |
|---|---|---|
| 數(shù)據(jù)來源 | 人工標(biāo)注 | 教師模型生成 |
| 數(shù)據(jù)成本 | 高(需要領(lǐng)域?qū)<遥?/td> | 低(API 調(diào)用費(fèi)) |
| 數(shù)據(jù)規(guī)模 | 通常有限 | 可大規(guī)模生成 |
| 質(zhì)量上限 | 取決于標(biāo)注者水平 | 取決于教師模型能力 |
我實(shí)踐下來的結(jié)論是:不必先把教師模型訓(xùn)到極致,直接用當(dāng)前夠強(qiáng)的 API(我用的 qwen3.6-plus)批量打標(biāo),把精力放在過濾壞樣本、覆蓋場(chǎng)景上,性價(jià)比更高。
蒸餾在生產(chǎn)降本中已有大量應(yīng)用:
- 阿里巴巴發(fā)布的 DistilQwen2.5 系列(0.5B–7B)通過多個(gè)大模型協(xié)作做教師蒸餾,蒸餾后的輕量模型在指令遵循能力上顯著優(yōu)于同參數(shù)量的原始基座。
- DeepSeek-R1 將 671B 參數(shù)模型的 80 萬條推理軌跡蒸餾到 7B 學(xué)生模型,使其在數(shù)學(xué)推理上超越了 LLaMA-2 70B。
2.2 三條蒸餾路徑
根據(jù)「復(fù)制」的內(nèi)容不同,蒸餾可以分為三種方式:
路徑一:數(shù)據(jù)合成蒸餾 / 黑盒蒸餾(我這次采用的主線)
教師只通過 API 出結(jié)果,拿不到權(quán)重,所以叫「黑盒」。對(duì)商業(yè) API 場(chǎng)景最省事:生成標(biāo)注 → 過濾 → SFT 小模型,整條鏈路不依賴教師權(quán)重。
路徑二:知識(shí)蒸餾 KD / 白盒蒸餾(概念了解)
學(xué)生不僅學(xué)習(xí)教師的最終輸出,還學(xué)習(xí)教師輸出每個(gè) token 時(shí)的概率分布(軟標(biāo)簽)。比如教師在判斷部門時(shí),內(nèi)部可能給出「HR 70%、行政 25%、IT 5%」這樣的概率分布,最終輸出選概率最高的「HR」,但這個(gè)分布本身包含了「HR 和行政有點(diǎn)像」這種隱含知識(shí)(暗知識(shí)),比只看最終答案信息量更大。
白盒蒸餾就是讓學(xué)生去擬合教師的概率分布,而不僅僅學(xué)最終答案。之所以叫「白盒」,是因?yàn)樾枰L問教師模型的權(quán)重和內(nèi)部狀態(tài)。商業(yè) API 拿不到權(quán)重,用不了這種方式,但如果教師模型開源,白盒蒸餾的精度通常更高。
路徑三:推理壓縮(概念了解)
學(xué)生學(xué)習(xí)教師的思維鏈(Chain-of-Thought)軌跡,而非僅學(xué)最終答案。DeepSeek-R1 就是用這種方式,將 800K 條推理軌跡蒸餾給 7B 學(xué)生模型,使其在推理任務(wù)上超越了 70B 基座模型。適合需要多步推理的任務(wù)。
| 路徑 | 別名 | 需要什么 | 適用場(chǎng)景 |
|---|---|---|---|
| 數(shù)據(jù)合成蒸餾 | 黑盒蒸餾 | 只需教師 API | 結(jié)構(gòu)化任務(wù)、教師是商業(yè) API |
| 知識(shí)蒸餾 KD | 白盒蒸餾 | 需要教師權(quán)重 | 開源教師、需要更高精度 |
| 推理壓縮 | — | 需要教師推理輸出 | 多步推理任務(wù) |
2.3 為什么選數(shù)據(jù)合成蒸餾
落到「請(qǐng)求理解」這個(gè)具體任務(wù)上,我的選型理由是:
- 教師(qwen3.6-plus)只有 API,沒有權(quán)重 → 白盒 KD 不現(xiàn)實(shí)
- 任務(wù)是單步判斷(從輸入直接到結(jié)構(gòu)化輸出),不需要多步推理 → CoT 軌跡沒有必要
- 結(jié)構(gòu)化輸出可以用 JSON schema 自動(dòng)校驗(yàn)質(zhì)量 → 數(shù)據(jù)質(zhì)量有保障
因此,數(shù)據(jù)合成蒸餾(黑盒蒸餾) 是最合適的選擇。
三、數(shù)據(jù)合成:教師打標(biāo) + 過濾(附代碼)
蒸餾里我最花時(shí)間的是數(shù)據(jù),不是訓(xùn)練命令。我自己的流程固定三步:
- 造一批風(fēng)格多樣的員工提問(含多意圖、口語、含糊句)
- 教師模型按 Schema 出 JSON 標(biāo)注
- 程序過濾不合格樣本,再轉(zhuǎn)成 ms-swift 的
messages格式
完整跑一遍合成會(huì)消耗少量 DashScope 額度。若只想先看評(píng)測(cè)與訓(xùn)練,可直接用我整理的示例
train.jsonl(約 1400 條)跳過生成環(huán)節(jié)。
下面按步驟寫,代碼可直接拷到工程里改。
三、0 用 Qwen Code 合成訓(xùn)練數(shù)據(jù)--可以跳過也可以保留
以下部分內(nèi)容主要是希望能夠提供一個(gè)具體如何運(yùn)用大模型來生成具體的問答場(chǎng)景數(shù)據(jù)以及格式化的范本,我是覺得挺重要的,方便去做蒸餾。
如果你在 Section 0 中安裝了 Qwen Code,現(xiàn)在可以讓它幫你完成數(shù)據(jù)合成的全過程——生成提問、教師標(biāo)注、質(zhì)量過濾、格式化保存,一氣呵成。如果你不想使用 Qwen Code,可以跳過這一步,直接使用 resources/4_1/train.jsonl 中的預(yù)生成數(shù)據(jù),閱讀下方代碼理解流程。
將以下提示詞復(fù)制到 Qwen Code 中執(zhí)行:
我正在做模型蒸餾的訓(xùn)練數(shù)據(jù)合成。請(qǐng)幫我完成以下任務(wù),每步完成后向我展示結(jié)果。
環(huán)境信息
- 工作目錄:cd /mnt/workspace/aliyun_acp_learning/大模型ACP認(rèn)證教程/C4_交付上線
- 虛擬環(huán)境:source /mnt/workspace/llm_learn/bin/activate
- API Key 已配置在環(huán)境變量 DASHSCOPE_API_KEY 中(通過 config/load_key.py 加載)
- 教師模型:qwen3.6-plus(通過 DashScope 兼容 OpenAI 接口調(diào)用)
- API 地址:https://dashscope.aliyuncs.com/compatible-mode/v1
任務(wù)背景
我們要為一個(gè)"員工請(qǐng)求理解"任務(wù)生成蒸餾訓(xùn)練數(shù)據(jù)。任務(wù)是把員工的自然語言提問轉(zhuǎn)成結(jié)構(gòu)化 JSON 工單,包含 5 個(gè)字段:intents(意圖)、department(部門)、urgency(緊急度)、entities(實(shí)體)、route(路由)。
第 1 步:加載 API Key
運(yùn)行以下 Python 代碼加載 API Key 并初始化 client:
import os, json, re, sys
os.chdir(os.path.join(os.path.dirname(os.path.abspath('')), 'course_core'))
sys.path.insert(0, os.getcwd())
from config.load_key import load_key
load_key()
from openai import OpenAI
client = OpenAI(
api_key=os.getenv("DASHSCOPE_API_KEY"),
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1"
)
print(f"API Key 已加載:{os.environ['DASHSCOPE_API_KEY'][:5]}*****")
第 2 步:用大模型生成 30 條員工提問
調(diào)用 qwen3.6-plus 生成多樣化的員工提問。要求:
- 覆蓋 8 種意圖:入職辦理、考勤請(qǐng)假、差旅申請(qǐng)、報(bào)銷催辦、年假查詢、IT支持、權(quán)限申請(qǐng)、制度咨詢
- 混合正式、口語化、含糊等表達(dá)風(fēng)格
- 約 30% 為多意圖問題(一句話包含兩個(gè)請(qǐng)求)
- 部分問題包含具體日期、金額、人名、地點(diǎn)
生成完后展示前 10 條給我看看。
第 3 步:用教師模型標(biāo)注
對(duì)每條提問調(diào)用 qwen3.6-plus 進(jìn)行標(biāo)注,使用以下 system prompt:
"""
你是一個(gè)請(qǐng)求理解助手。分析用戶的提問,提取結(jié)構(gòu)化工單信息。
嚴(yán)格按以下 JSON 格式輸出,不要輸出任何其他內(nèi)容:
{
"intents": ["意圖"],
"department": "部門",
"urgency": "緊急度",
"entities": {},
"route": "路由"
}
字段取值范圍:
- intents(可多選):入職辦理、考勤請(qǐng)假、差旅申請(qǐng)、報(bào)銷催辦、年假查詢、IT支持、權(quán)限申請(qǐng)、制度咨詢
- department:HR、行政、IT、財(cái)務(wù)
- urgency:高、中、低
- entities:從提問中提取的關(guān)鍵參數(shù)(日期、金額、人名、地點(diǎn)、系統(tǒng)名稱等),如無則為空對(duì)象 {}
- route:direct_answer(簡(jiǎn)單事實(shí)問題)、rag_query(需要查閱政策文檔)、multi_intent_split(包含多個(gè)獨(dú)立意圖)、escalate(需要人工介入)
"""
調(diào)用時(shí)設(shè) temperature=0.1。標(biāo)注完后展示 3 條標(biāo)注結(jié)果給我看看。
第 4 步:質(zhì)量過濾
對(duì)每條標(biāo)注做三項(xiàng)檢查:
- JSON 能否正常解析
- 5 個(gè)必填字段(intents、department、urgency、entities、route)是否齊全
- 每個(gè)字段的值是否在上面 system prompt 定義的取值范圍內(nèi)
過濾掉不合格的樣本,告訴我總共生成了多少條、過濾了多少條、保留了多少條。
第 5 步:格式化并保存
將合格樣本轉(zhuǎn)成 ms-swift 的 messages 訓(xùn)練格式:
{"messages": [{"role": "system", "content": "system prompt 內(nèi)容"}, {"role": "user", "content": "員工提問"}, {"role": "assistant", "content": "JSON 標(biāo)注結(jié)果"}]}
每條一行,保存到 resources/4_1/my_train.jsonl。保存完后告訴我文件路徑和樣本數(shù)量。
Qwen Code 生成的數(shù)據(jù)保存在 resources/4_1/my_train.jsonl。后續(xù)訓(xùn)練時(shí),如果你想用自己生成的數(shù)據(jù),將訓(xùn)練命令中的 train.jsonl 替換為 my_train.jsonl 即可。課程預(yù)生成的 train.jsonl(約 1400 條)覆蓋更全面,建議正式訓(xùn)練時(shí)仍使用預(yù)生成數(shù)據(jù)。
這是我實(shí)際執(zhí)行后的結(jié)果數(shù)據(jù)過程:

第二步結(jié)果:

第三步:



保存成功了。
無論你是否使用了 Qwen Code,下面我們來詳細(xì)拆解數(shù)據(jù)合成的每一步,理解背后的設(shè)計(jì)思路。直接使用 resources/4_1/train.jsonl 中的預(yù)生成數(shù)據(jù),閱讀下方代碼理解流程。
3.1 教師模型的 System Prompt
教師模型(qwen3.6-plus)需要一個(gè)精確的 system prompt 來規(guī)范輸出格式。這個(gè) prompt 定義了 JSON schema 和每個(gè)字段的取值范圍。
Python
SYSTEM_PROMPT = """你是一個(gè)請(qǐng)求理解助手。分析用戶的提問,提取結(jié)構(gòu)化工單信息。
嚴(yán)格按以下 JSON 格式輸出,不要輸出任何其他內(nèi)容:
{
"intents": ["意圖"],
"department": "部門",
"urgency": "緊急度",
"entities": {},
"route": "路由"
}
字段取值范圍:
- intents(可多選):入職辦理、考勤請(qǐng)假、差旅申請(qǐng)、報(bào)銷催辦、年假查詢、IT支持、權(quán)限申請(qǐng)、制度咨詢
- department:HR、行政、IT、財(cái)務(wù)
- urgency:高、中、低
- entities:從提問中提取的關(guān)鍵參數(shù)(日期、金額、人名、地點(diǎn)、系統(tǒng)名稱等),如無則為空對(duì)象 {}
- route:direct_answer(簡(jiǎn)單事實(shí)問題)、rag_query(需要查閱政策文檔)、multi_intent_split(包含多個(gè)獨(dú)立意圖)、escalate(需要人工介入)"""
print("System Prompt 已定義")
print(f"Prompt 長(zhǎng)度:{len(SYSTEM_PROMPT)} 字符")
3.2 生成多樣化的員工提問
數(shù)據(jù)多樣性直接決定蒸餾上限。生成提問時(shí),我一般用多組 prompt 模板,盡量覆蓋:
- 覆蓋 8 種意圖類型 × 4 個(gè)部門 的場(chǎng)景組合
- 混合正式請(qǐng)求、口語化、含糊省略等多種表達(dá)風(fēng)格
- 約 30% 為多意圖問題(一句話包含兩個(gè)請(qǐng)求)
- 部分問題包含具體日期、金額、人名、地點(diǎn)等實(shí)體信息
Python(數(shù)據(jù)生成腳本仍在 Python 側(cè))
query_prompt_example = """請(qǐng)生成15條不同員工向公司內(nèi)部答疑機(jī)器人提出的問題。要求:
1. 覆蓋場(chǎng)景:入職辦理、考勤請(qǐng)假、差旅申請(qǐng)、報(bào)銷催辦
2. 表達(dá)風(fēng)格多樣:正式、口語化、含糊、簡(jiǎn)短、詳細(xì)
3. 約30%為多意圖問題(一句話包含兩個(gè)請(qǐng)求)
4. 部分問題包含具體的日期、金額、人名、地點(diǎn)
每條問題單獨(dú)一行,不要編號(hào),不要其他說明。
注意:場(chǎng)景中涉及的系統(tǒng)、工具、平臺(tái)只能使用虛擬名稱或阿里系產(chǎn)品(如釘釘、阿里云等),不要提及任何非阿里旗下的真實(shí)公司或產(chǎn)品名稱。"""
# 實(shí)際使用了 5 個(gè)不同側(cè)重的模板,覆蓋所有意圖類型和表達(dá)風(fēng)格
# 完整的數(shù)據(jù)生成腳本見 resources/4_1/generate_data.py
print("生成模板示例:")
print(query_prompt_example)
3.3 教師標(biāo)注
對(duì)每條生成的提問,用教師模型(qwen3.6-plus)生成結(jié)構(gòu)化標(biāo)注。標(biāo)注時(shí)使用 system prompt 嚴(yán)格約束輸出格式,temperature=0.1 降低隨機(jī)性以確保標(biāo)注一致性,每條標(biāo)注都經(jīng)過 JSON schema 校驗(yàn)后才能進(jìn)入訓(xùn)練集。
Python
def teacher_label(query, system_prompt=SYSTEM_PROMPT):
"""用教師模型標(biāo)注一條查詢"""
response = client.chat.completions.create(
model="qwen3.6-plus",
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": query}
],
temperature=0.1
)
return response.choices[0].message.content
test_query = "我下周三要出差去杭州,順便催一下上個(gè)月的報(bào)銷"
result = teacher_label(test_query)
print(f"輸入:{test_query}")
print("教師標(biāo)注:")
print(json.dumps(json.loads(result), ensure_ascii=False, indent=2))
3.4 質(zhì)量過濾
教師模型不是 100% 完美的。即使是 qwen3.6-plus 這樣的強(qiáng)模型,在批量標(biāo)注幾百條數(shù)據(jù)時(shí)也會(huì)出錯(cuò):JSON 寫到一半截?cái)?、漏掉某個(gè)字段、把部門寫成不存在的值。這些「壞樣本」一旦進(jìn)訓(xùn)練集,小模型會(huì)把錯(cuò)誤一并學(xué)走。所以我會(huì)固定加一層自動(dòng)化 Schema 校驗(yàn),不合格的直接丟棄。
以下是實(shí)際生產(chǎn)中最常見的三類問題:
| 問題類型 | 示例 | 處理方式 |
|---|---|---|
| JSON 格式錯(cuò)誤 | 輸出被 markdown 包裹、括號(hào)不匹配 | 提取 JSON + 解析失敗則丟棄 |
| 必填字段缺失 | 缺少 route 或 entities
|
字段齊全性檢查 |
| 取值越界 | department 寫成「法務(wù)部」 | 枚舉值校驗(yàn) |
過濾用的是「結(jié)果校驗(yàn)」(檢查輸出是否符合 schema),而非「過程校驗(yàn)」(檢查推理過程是否合理),對(duì)于結(jié)構(gòu)化提取任務(wù)這種方式簡(jiǎn)單有效。
AI 輔助編程與傳統(tǒng)編程的對(duì)比
| 維度 | AI 編程助手(自然語言驅(qū)動(dòng)) | Python / Java 代碼(手寫邏輯) |
|---|---|---|
| 上手難度 | 低,用自然語言描述規(guī)則 | 中,需要編寫校驗(yàn)代碼 |
| 靈活性 | 高,可隨時(shí)調(diào)整規(guī)則描述 | 中,需要修改代碼邏輯 |
| 可維護(hù)性 | 適合快速原型和實(shí)驗(yàn) | 適合生產(chǎn)環(huán)境長(zhǎng)期維護(hù) |
| 執(zhí)行效率 | 依賴 AI 的代碼生成能力 | 直接執(zhí)行,效率確定 |
| 適用場(chǎng)景 | 規(guī)則頻繁變化、快速驗(yàn)證 | 規(guī)則穩(wěn)定、需要版本控制 |
?? 提示:規(guī)則尚未穩(wěn)定時(shí),可先用自然語言驅(qū)動(dòng) AI 編程助手快速試過濾邏輯;規(guī)則確定后,再固化為 Java / Python 代碼納入版本管理。兩種方式互補(bǔ),不是二選一。
過濾代碼實(shí)現(xiàn)
Python
VALID_INTENTS = {"入職辦理", "考勤請(qǐng)假", "差旅申請(qǐng)", "報(bào)銷催辦", "年假查詢", "IT支持", "權(quán)限申請(qǐng)", "制度咨詢"}
VALID_DEPARTMENTS = {"HR", "行政", "IT", "財(cái)務(wù)"}
VALID_URGENCY = {"高", "中", "低"}
VALID_ROUTES = {"direct_answer", "rag_query", "multi_intent_split", "escalate"}
def validate_label(label_str):
"""校驗(yàn)教師標(biāo)注是否合格"""
try:
if '```' in label_str:
match = re.search(r'```(?:json)?\s*(.*?)\s*```', label_str, re.DOTALL)
if match:
label_str = match.group(1)
label = json.loads(label_str.strip())
except json.JSONDecodeError:
return None
required = ["intents", "department", "urgency", "entities", "route"]
if not all(k in label for k in required):
return None
if not isinstance(label["intents"], list) or len(label["intents"]) == 0:
return None
if not all(i in VALID_INTENTS for i in label["intents"]):
return None
if label["department"] not in VALID_DEPARTMENTS:
return None
if label["urgency"] not in VALID_URGENCY:
return None
if label["route"] not in VALID_ROUTES:
return None
if not isinstance(label["entities"], dict):
return None
return label
3.5 查看訓(xùn)練數(shù)據(jù)樣本
合成并過濾完成后,建議先抽樣查看數(shù)據(jù)內(nèi)容與分布,避免帶著系統(tǒng)性偏差進(jìn)入訓(xùn)練。
Python
train_data = []
with open("resources/4_1/train.jsonl", "r", encoding="utf-8") as f:
for line in f:
train_data.append(json.loads(line))
test_data = []
with open("resources/4_1/test_30.jsonl", "r", encoding="utf-8") as f:
for line in f:
test_data.append(json.loads(line))
print(f"訓(xùn)練集:{len(train_data)} 條")
print(f"測(cè)試集:{len(test_data)} 條")
# ... 意圖 / 路由 / 部門分布統(tǒng)計(jì)邏輯同上
3.6 訓(xùn)練數(shù)據(jù)格式
訓(xùn)練數(shù)據(jù)采用 ms-swift 框架要求的 messages 格式,每條樣本包含三個(gè)角色的對(duì)話:
{
"messages": [
{"role": "system", "content": "你是一個(gè)請(qǐng)求理解助手..."},
{"role": "user", "content": "我下周三要出差去杭州..."},
{"role": "assistant", "content": "{\"intents\": [\"差旅申請(qǐng)\"], ...}"}
]
}
模型訓(xùn)練時(shí),system 和 user 部分作為輸入,assistant 部分作為期望輸出。模型學(xué)習(xí)的是:給定 system prompt 和用戶提問,生成正確的結(jié)構(gòu)化 JSON。
說明:LoRA 訓(xùn)練使用 ms-swift CLI,依賴 GPU + Python 環(huán)境;參數(shù)與命令因版本而異,以 ms-swift 官方文檔 為準(zhǔn)。
四、怎么評(píng)測(cè):先立尺子,再訓(xùn)模型
訓(xùn)練前我會(huì)先跑一輪 Baseline:同一套測(cè)試集上,看教師「天花板」和基座 0.6B「起點(diǎn)」差多少。指標(biāo)不用 BLEU 那套,而是圍繞結(jié)構(gòu)化 JSON 設(shè)計(jì)。
4.1 評(píng)測(cè)指標(biāo)
原因很實(shí)際:輸出是固定 Schema 的 JSON,字符串像不像意義不大,能不能解析、字段對(duì)不對(duì)、路由對(duì)不對(duì)才決定能不能上線。
| 指標(biāo) | 含義 | 為什么重要 |
|---|---|---|
| JSON 合規(guī)率 | 輸出能否解析為合法 JSON 且包含所有必填字段 | 格式不對(duì)就沒法接入下游路由 |
| intents F1 | 意圖識(shí)別的 F1 值 | 意圖是多選的,F(xiàn)1 同時(shí)反映「找全了」和「沒找錯(cuò)」 |
| route 準(zhǔn)確率 | 路由決策是否正確 | 路由錯(cuò)了,后續(xù)處理模塊全跑偏 |
評(píng)測(cè)時(shí)我按語義集合算分,不按字符串逐字匹配。例如意圖同時(shí)包含「差旅申請(qǐng)」和「報(bào)銷催辦」,順序不同也算對(duì)。
Java(評(píng)測(cè)核心邏輯)
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
public final class RequestUnderstandingEvalSupport {
public record Prediction(String raw, Map<String, Object> parsed) {
}
public record EvalResult(int jsonValid, double intentF1, int routeAccuracy, int total) {
}
public static double computeIntentF1(List<String> predIntents, List<String> trueIntents) {
Set<String> pred = new HashSet<>(predIntents == null ? List.of() : predIntents);
Set<String> truth = new HashSet<>(trueIntents == null ? List.of() : trueIntents);
if (pred.isEmpty() && truth.isEmpty()) {
return 1.0;
}
if (pred.isEmpty() || truth.isEmpty()) {
return 0.0;
}
Set<String> inter = new HashSet<>(pred);
inter.retainAll(truth);
double precision = inter.size() * 1.0 / pred.size();
double recall = inter.size() * 1.0 / truth.size();
if (precision + recall == 0) {
return 0.0;
}
return 2 * precision * recall / (precision + recall);
}
@SuppressWarnings("unchecked")
public static EvalResult evaluatePredictions(List<Prediction> predictions, List<Map<String, Object>> groundTruths) {
int n = groundTruths.size();
int jsonValid = 0;
double intentF1Sum = 0;
int routeCorrect = 0;
for (int i = 0; i < n; i++) {
Map<String, Object> parsed = predictions.get(i).parsed();
if (parsed == null) {
continue;
}
jsonValid++;
List<String> predIntents = (List<String>) parsed.getOrDefault("intents", List.of());
List<String> trueIntents = (List<String>) groundTruths.get(i).getOrDefault("intents", List.of());
intentF1Sum += computeIntentF1(predIntents, trueIntents);
if (parsed.get("route") != null && parsed.get("route").equals(groundTruths.get(i).get("route"))) {
routeCorrect++;
}
}
return new EvalResult(jsonValid, n > 0 ? intentF1Sum / n : 0, routeCorrect, n);
}
public static void printEvalResults(EvalResult r, String modelName) {
System.out.printf("%n=== %s 評(píng)測(cè)結(jié)果 ===%n", modelName);
System.out.printf(" JSON 合規(guī)率: %d/%d (%.0f%%)%n",
r.jsonValid(), r.total(), r.jsonValid() * 100.0 / r.total());
System.out.printf(" intents F1: %.1f%%%n", r.intentF1() * 100);
System.out.printf(" route 準(zhǔn)確率: %d/%d (%.0f%%)%n",
r.routeAccuracy(), r.total(), r.routeAccuracy() * 100.0 / r.total());
}
public static String renderComparisonMarkdown(List<EvalResult> results, List<String> modelNames) {
StringBuilder sb = new StringBuilder();
sb.append("| 指標(biāo) | ").append(String.join(" | ", modelNames)).append(" |\n");
sb.append("| --- | ").append(" --- |".repeat(modelNames.size())).append("\n");
sb.append(row("JSON 合規(guī)率", results, r -> "%d/%d (%.0f%%)".formatted(
r.jsonValid(), r.total(), r.jsonValid() * 100.0 / r.total())));
sb.append(row("intents F1", results, r -> "%.1f%%".formatted(r.intentF1() * 100)));
sb.append(row("route 準(zhǔn)確率", results, r -> "%d/%d (%.0f%%)".formatted(
r.routeAccuracy(), r.total(), r.routeAccuracy() * 100.0 / r.total())));
return sb.toString();
}
private interface RowFormatter {
String format(EvalResult r);
}
private static String row(String label, List<EvalResult> results, RowFormatter fmt) {
StringBuilder sb = new StringBuilder("| ").append(label).append(" | ");
for (EvalResult r : results) {
sb.append(fmt.format(r)).append(" | ");
}
return sb.append("\n").toString();
}
}
4.2 教師模型評(píng)測(cè)(qwen3.6-plus)
先看看教師模型在測(cè)試集上的表現(xiàn),這是蒸餾的「能力上限」。
Java
// 偽代碼結(jié)構(gòu):讀取 test_30.jsonl → 逐條 teacherLabel → validateLabel → evaluatePredictions
public class TeacherEvalDemo {
public static void main(String[] args) throws Exception {
// List<TestItem> testData = loadTestJsonl("resources/4_1/test_30.jsonl");
// List<Prediction> preds = testData.stream()
// .map(item -> new Prediction(raw, LabelValidator.validateLabel(raw)))
// .toList();
// EvalResult r = RequestUnderstandingEvalSupport.evaluatePredictions(preds, groundTruths);
// RequestUnderstandingEvalSupport.printEvalResults(r, "教師模型(qwen3.6-plus)");
}
}
4.3 基座小模型評(píng)測(cè)(Qwen3-0.6B)
再看看未經(jīng)訓(xùn)練的小模型的表現(xiàn),這是蒸餾前的「起點(diǎn)」?;P托枋孪认螺d到本地(如 /mnt/workspace/model),下方通過 ModelScope + PyTorch 加載并進(jìn)行本地推理。
說明:JVM 側(cè)目前沒有與 ms-swift / ModelScope 等價(jià)的「一行代碼加載 Qwen3 并 generate」成熟方案;本地 0.6B 推理保留 Python。
實(shí)現(xiàn)思路(與 §4.2 教師評(píng)測(cè)對(duì)齊):
- 加載:從本地目錄讀入 Qwen3-0.6B 權(quán)重與分詞器(需 GPU,見 §「下載基座模型」)。
-
逐條推理:只讀測(cè)試集里的
query,拼上與訓(xùn)練/教師一致的SYSTEM_PROMPT,不把ground_truth喂給模型。 - 解碼:用 chat 模板生成回復(fù),截掉 prompt 部分,得到純 assistant 文本。
-
校驗(yàn):
validate_label判斷是否為合法 JSON 工單(與 §3.4 同一套規(guī)則)。 -
打分:
evaluate_predictions對(duì)比ground_truth,得到 JSON 合規(guī)率、意圖 F1、route 準(zhǔn)確率。
test_data 來自 test_30.jsonl(每行 query + ground_truth),與 train.jsonl 不重疊,用于衡量「未蒸餾前」的真實(shí)水平。
from modelscope import AutoModelForCausalLM, AutoTokenizer
import torch
# ---------- 1. 加載基座模型 ----------
# model_path:ModelScope 下載目錄(見前文 modelscope download)
model_path = '/mnt/workspace/model'
# trust_remote_code=True:Qwen 系列自定義 tokenizer / chat 模板在倉庫代碼里
tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True)
base_model = AutoModelForCausalLM.from_pretrained(
model_path,
dtype=torch.float16, # 半精度,省顯存;0.6B 單卡足夠
device_map='auto', # 自動(dòng)把層放到可用 GPU(無 GPU 會(huì)走 CPU,很慢)
trust_remote_code=True,
)
# ---------- 2. 在測(cè)試集上逐條生成 ----------
# test_data:已加載的 list[dict],元素形如 {"query": "...", "ground_truth": {...}}
print("正在評(píng)測(cè)基座模型(Qwen3-0.6B 本地推理)...")
base_preds = [] # 收集每條:原始輸出 raw + 校驗(yàn)后的 parsed(失敗則為 None)
for i, item in enumerate(test_data):
# 與教師標(biāo)注、后續(xù) SFT 訓(xùn)練保持同一對(duì)話結(jié)構(gòu)
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": item["query"]}, # 僅用用戶提問,不用 ground_truth
]
# apply_chat_template:轉(zhuǎn)成 Qwen3 認(rèn)識(shí)的「帶角色標(biāo)記」的 prompt 字符串
# add_generation_prompt=True:末尾加上 assistant 起始符,模型從該位置續(xù)寫
# enable_thinking=False:關(guān)閉 Qwen3 思考鏈,避免冗長(zhǎng)推理段干擾 JSON
text = tokenizer.apply_chat_template(
messages, tokenize=False, add_generation_prompt=True,
enable_thinking=False,
)
inputs = tokenizer(text, return_tensors="pt").to(base_model.device)
with torch.no_grad(): # 推理階段不算梯度,省顯存
outputs = base_model.generate(
**inputs,
max_new_tokens=512, # 結(jié)構(gòu)化 JSON 一般夠用
temperature=0.1, # 低溫,輸出更穩(wěn)定(與教師 API temperature 一致)
do_sample=True, # temperature>0 時(shí)需采樣;要完全確定性可改 do_sample=False
)
# 只解碼「新生成」的 token,去掉輸入 prompt,避免把用戶問題混進(jìn)結(jié)果
response = tokenizer.decode(
outputs[0][inputs.input_ids.shape[1]:],
skip_special_tokens=True,
)
# §3.4 同一套 Schema 校驗(yàn);不通過則 parsed=None,JSON 合規(guī)率計(jì)為失敗
parsed = validate_label(response)
base_preds.append({"raw": response, "parsed": parsed})
status = "OK" if parsed else "FAIL"
print(f" [{i+1}/{len(test_data)}] {status} | {item['query'][:50]}...")
# ---------- 3. 與標(biāo)準(zhǔn)答案對(duì)比,輸出 Baseline 指標(biāo) ----------
# ground_truth:測(cè)試集里預(yù)先固化的標(biāo)簽(教師打標(biāo)或抽檢修訂),模型推理時(shí)不可見
ground_truths = [item["ground_truth"] for item in test_data]
base_results = evaluate_predictions(base_preds, ground_truths)
print_eval_results(base_results, "基座模型(Qwen3-0.6B)")
基座模型典型失敗案例
評(píng)測(cè)分?jǐn)?shù)是一個(gè)總體概括,更有價(jià)值的是看看基座模型具體錯(cuò)在哪里:
if 'base_preds' in dir():
print("--- 基座模型典型失敗案例 ---")
shown = 0
for i, (pred, truth) in enumerate(zip(base_preds, ground_truths)):
if pred["parsed"] is None and shown < 3:
print(f"\n[案例 {shown+1}] JSON 解析失敗")
print(f" 提問:{test_data[i]['query']}")
print(f" 模型輸出(前200字):{pred['raw'][:200]}")
shown += 1
常見失敗模式:
- JSON 格式崩壞:缺括號(hào)、多余說明文字、字段錯(cuò)位
- 意圖識(shí)別錯(cuò)誤:多意圖漏識(shí)別、部門路由錯(cuò)誤
- 即使在格式正確的輸出中,意圖 F1 和 route 準(zhǔn)確率仍然很低
4.4 Baseline 對(duì)比
把基座模型和教師模型的結(jié)果放在一起對(duì)比。這兩組數(shù)據(jù)分別代表蒸餾的起點(diǎn)和天花板:
| 指標(biāo) | 基座模型 (Qwen3-0.6B) | 教師模型 (qwen3.6-plus) |
|---|---|---|
| JSON 合規(guī)率 | 15/31 (48%) | 31/31 (100%) |
| intents F1 | 23.7% | 83.9% |
| route 準(zhǔn)確率 | 5/31 (16%) | 28/31 (90%) |
上表是我當(dāng)時(shí)在同一測(cè)試集上跑出的參考值;換業(yè)務(wù)域或測(cè)試集后,數(shù)字會(huì)變,但「基座 JSON 崩、教師 穩(wěn)」的形態(tài)通常類似。
結(jié)論摘要:
- 基座模型的大部分輸出無法解析為合法 JSON,格式崩壞是最突出的問題。
- 即使在格式正確的輸出中,意圖識(shí)別和路由準(zhǔn)確率也很低,意味著絕大多數(shù)請(qǐng)求會(huì)被分發(fā)到錯(cuò)誤的處理模塊。
- 教師模型在各項(xiàng)指標(biāo)上都表現(xiàn)穩(wěn)定,說明大模型已經(jīng)很好地掌握了這個(gè)任務(wù)。
蒸餾的目標(biāo):讓 0.6B 模型從「幾乎不可用」提升到「接近教師水平」。即使無法完全追平教師,只要在各項(xiàng)指標(biāo)上有大幅提升,就已經(jīng)具備生產(chǎn)部署價(jià)值。
五、訓(xùn)練和微調(diào):其實(shí)是同一條路
Baseline 跑完,數(shù)據(jù)也過濾好了,后面就是常規(guī)的 SFT / LoRA——很多人問:這不就是微調(diào)嗎?
是,也不完全是。 流程一樣(數(shù)據(jù) → 基座 → 訓(xùn)練),沒錯(cuò),蒸餾在訓(xùn)練流程上和微調(diào)幾乎完全相同,都是準(zhǔn)備訓(xùn)練數(shù)據(jù)、加載基座模型、執(zhí)行 SFT 訓(xùn)練。唯一的區(qū)別在于數(shù)據(jù)來源:微調(diào)用人工標(biāo)注的數(shù)據(jù),蒸餾用教師模型生成的數(shù)據(jù)。
主要是為了強(qiáng)調(diào)數(shù)據(jù)來自教師 API,而不是人工一條條標(biāo)。我接下來的計(jì)劃是:
- ms-swift LoRA 訓(xùn)練(Python + GPU)
- 蒸餾后模型復(fù)評(píng)(同一套 JSON 合規(guī)率 / F1 / 路由準(zhǔn)確率)
- 成本收益估算(API 單價(jià) × 日調(diào)用量 vs 小模型托管成本)
接下來的蒸餾步驟實(shí)現(xiàn),我補(bǔ)充在下篇了,關(guān)注我更新最新的內(nèi)容哦。