320 lines
17 KiB
Markdown
320 lines
17 KiB
Markdown
---
|
||
license: apache-2.0
|
||
license_link: https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct/blob/main/LICENSE
|
||
language:
|
||
- zh
|
||
pipeline_tag: text-generation
|
||
library_name: transformers
|
||
base_model: Qwen/Qwen2.5-1.5B-Instruct
|
||
tags:
|
||
- text-generation
|
||
- qwen2.5
|
||
- lora
|
||
- json
|
||
- instruction-following
|
||
- chinese
|
||
- command-translation
|
||
---
|
||
|
||
# darkhorse-code-instruct-1.5b
|
||
|
||
> **把一个微调过的 1.5B 小模型,做成编辑器里的「自然语言 → 私域命令」翻译器。**
|
||
> 输入一句中文口语,输出**一行严格 JSON**,不是聊天回复。
|
||
|
||
```
|
||
输入:关掉倒数第5个文件
|
||
输出:{"intent": "command", "cmd": "close -5"}
|
||
```
|
||
|
||
```json
|
||
{"intent": "command", "cmd": "del src/P05-es/pdf2es.py"} // 删除类
|
||
{"intent": "reject", "reply": "您没有权限!"} // 越权/不合法
|
||
{"intent": "chat", "reply": "你还是好好工作吧,房贷还清了吗?车贷还清了吗?"} // 非命令输入
|
||
```
|
||
|
||
本模型是 **Qwen2.5-1.5B-Instruct + LoRA(r=16)** 指令微调后**合并**(merged)得到的自包含权重,
|
||
不需要 `peft`、不需要 adapter,`transformers` / vLLM 直接加载即可。
|
||
|
||
---
|
||
|
||
## 1. 它解决什么问题
|
||
|
||
`darkhorse-code` 是一个编辑器项目,它有一套**私域命令语法**(`open project` / `close -3` /
|
||
`config add -r k=v` …),命令语法固定、可校验,但用户不想背。于是把「用户口语 → 一条标准命令」
|
||
这个窄任务交给一个 1.5B 小模型:**小、快、可完全本地跑,且输出必须能被程序解析**。
|
||
|
||
它不是通用聊天模型,**不会**回答知识问题、不会写代码、不会陪你聊天 ——
|
||
遇到非命令输入,它的正确行为是回一句 20 字以内的短回复(或一句固定的「劝退」话术)。
|
||
|
||
## 2. 输出契约(三种信封)
|
||
|
||
模型**只输出一行 JSON**,`intent` 决定信封形状:
|
||
|
||
| intent | 形状 | 含义 |
|
||
|---|---|---|
|
||
| `command` | `{"intent":"command","cmd":"<标准命令>"}` | 翻译成功,`cmd` 可直接执行 |
|
||
| `reject` | `{"intent":"reject","reply":"<拒绝原因>"}` | 命中硬性规则,如路径越权 |
|
||
| `chat` | `{"intent":"chat","reply":"<≤20字短回复>"}` | 不是私域命令 |
|
||
|
||
两条固定话术(评测按**逐字**判定):
|
||
|
||
- 绝对路径出现在 `open file` / `new` / `del` / `rename` → `您没有权限!`
|
||
- 要求把运行目标存成全局配置 → `运行目标不能保存为全局`
|
||
- 闲聊超过 20 字说不完 → `你还是好好工作吧,房贷还清了吗?车贷还清了吗?`
|
||
|
||
## 3. 命令语法(模型学到的目标空间)
|
||
|
||
```
|
||
open project <绝对路径> | open file <相对路径>
|
||
close project|all|other|left|right|<序号> # 序号 0 起,负数从右往左,-1 是最后一个
|
||
config add|remove|update|get [-g|-p|-r] <key>=<value> # 未指定层级默认 -p
|
||
new file <相对路径> | new folder <相对路径> | new py|rs|md|c <名称>
|
||
del <相对路径> | run <名称>=<命令> | run del <名称>
|
||
rename <旧相对路径> <新相对路径>
|
||
project lang <语言> <绝对路径> | project delete <绝对路径> | project migrate
|
||
project edit "<路径>" "<名称>" <语言>
|
||
```
|
||
|
||
语言取值:`unknown/mix/java/c/python/rust/web/golang/document/kotlin`(中文表达做了映射:前端→web、Go→golang、文档→document…)。
|
||
|
||
## 4. 快速开始(transformers)
|
||
|
||
⚠️ **两条铁律,违反任一条效果都会明显掉:**
|
||
|
||
1. **必须使用本模型内置的 system prompt**(下面 `SYSTEM_PROMPT` 原文,逐字),
|
||
模型是在这份 prompt 下微调的;换 prompt = 训练/推理不一致。
|
||
2. **必须走模型自带的 chat template**(本仓库的 `chat_template.jinja`),
|
||
`tokenizer.apply_chat_template` 会自动读到它。
|
||
|
||
```python
|
||
import json
|
||
from transformers import AutoModelForCausalLM, AutoTokenizer
|
||
|
||
MODEL = "darkhorse-code-instruct-1.5b" # 换成你的 ModelScope 仓库 id
|
||
tok = AutoTokenizer.from_pretrained(MODEL)
|
||
model = AutoModelForCausalLM.from_pretrained(MODEL, torch_dtype="bfloat16", device_map="auto")
|
||
|
||
SYSTEM_PROMPT = """你是 darkhorse-code 编辑器的命令翻译器。把用户的话翻译成一条标准命令,只输出一行 JSON。
|
||
|
||
命令语法:
|
||
open project <绝对路径> | open file <相对路径>
|
||
close project|all|other|left|right|<序号>
|
||
close 的序号从 0 开始,负数从右往左,-1 是最后一个
|
||
config add|remove|update|get [-g|-p|-r] <key>=<value>,未指定层级默认 -p
|
||
new file <相对路径> | new folder <相对路径> | new py|rs|md|c <名称>
|
||
del <相对路径> | run <名称>=<命令> | run del <名称>
|
||
rename <旧相对路径> <新相对路径>
|
||
project lang <语言> <绝对路径> | project delete <绝对路径> | project migrate
|
||
project edit "<路径>" "<名称>" <语言>
|
||
|
||
规则:
|
||
1. 语言取值 unknown/mix/java/c/python/rust/web/golang/document/kotlin;中文表达要映射:Python→python、Go→golang、前端→web、文档→document、混合→mix、未知→unknown。
|
||
2. 绝对路径(以 / 或 C:/ D:/ 开头)只允许用在 open project、project lang/edit/delete 和 config 的值里;出现在 open file、new、del、rename 时一律拒绝,reply 固定为「您没有权限!」。
|
||
3. 路径里的正斜杠输出一律转成反斜杠。
|
||
4. 运行目标只能存项目配置,不能存全局;用户要求存全局时 reply 固定为「运行目标不能保存为全局」。
|
||
5. config 的值含空格时用双引号包裹。上下文给出已有运行目标时,新增运行目标索引 = 最大索引 + 1。
|
||
6. 不是私域命令的输入(闲聊、问知识、要你写文档)用 chat:reply 不超过 20 字;一句话说不完 20 字时 reply 固定为「你还是好好工作吧,房贷还清了吗?车贷还清了吗?」。
|
||
7. 同义命令输出规范形式:删除类写 del,关闭其他文件写 close other,重命名写 rename,新建目录写 new folder,run 的删除写 run del。
|
||
|
||
输出格式(只输出一行 JSON,不要解释、不要代码块):
|
||
{"intent":"command","cmd":"<标准命令>"}
|
||
{"intent":"reject","reply":"<拒绝原因>"}
|
||
{"intent":"chat","reply":"<短回复>"}"""
|
||
|
||
|
||
def translate(text: str, context: str | None = None) -> dict:
|
||
# 上下文按训练时的约定另起一行:'[上下文] <内容>'
|
||
user = f"[上下文] {context}\n{text}" if context else text
|
||
msgs = [{"role": "system", "content": SYSTEM_PROMPT},
|
||
{"role": "user", "content": user}]
|
||
prompt = tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
|
||
ids = tok(prompt, return_tensors="pt", add_special_tokens=False).to(model.device)
|
||
out = model.generate(**ids, max_new_tokens=128, do_sample=False, # 贪心,别采样
|
||
pad_token_id=tok.pad_token_id or tok.eos_token_id)
|
||
raw = tok.decode(out[0][ids["input_ids"].shape[1]:], skip_special_tokens=True).strip()
|
||
return json.loads(raw) # 建议外面再包一层 try:小模型偶尔会多写一句解释
|
||
|
||
|
||
print(translate("把倒数第三个文件关掉"))
|
||
# {'intent': 'command', 'cmd': 'close -3'}
|
||
print(translate("加一个运行目标 测试=pytest -q", context="当前项目: D:/Projects/demo"))
|
||
# {'intent': 'command', 'cmd': 'run 测试=pytest -q'}
|
||
```
|
||
|
||
## 5. vLLM 部署(推荐,OpenAI 兼容)
|
||
|
||
8 GB 显存(如 RTX 5060 Laptop)即可跑 bf16:
|
||
|
||
```bash
|
||
vllm serve ./qwen2.5-1.5b-cmd-merged \
|
||
--served-model-name darkhorse-code-instruct \
|
||
--chat-template ./qwen2.5-1.5b-cmd-merged/chat_template.jinja \
|
||
--dtype bfloat16 --max-model-len 32768 \
|
||
--gpu-memory-utilization 0.85 --max-num-seqs 32 \
|
||
--enable-prefix-caching --port 8001
|
||
```
|
||
|
||
```bash
|
||
curl http://127.0.0.1:8001/v1/chat/completions -H "Content-Type: application/json" -d '{
|
||
"model": "darkhorse-code-instruct",
|
||
"messages": [
|
||
{"role": "system", "content": "<上面那份 SYSTEM_PROMPT 原文>"},
|
||
{"role": "user", "content": "关掉倒数第5个文件"}
|
||
],
|
||
"temperature": 0, "max_tokens": 128
|
||
}'
|
||
```
|
||
|
||
要点:
|
||
|
||
- `temperature=0`(训练与评测都是贪心解码);**不要**用 `generation_config.json` 里的默认采样参数。
|
||
- 请求里的 `system` 必须是上面那份原文;本项目服务端做薄封装时会把 system 强制替换成它。
|
||
- `choices[0].message.content` 是**契约 JSON 字符串**,调用方需要 `json.loads`。
|
||
- `--enable-prefix-caching` 对这类场景收益明显:所有请求共享同一份 553 token 的 system prompt 前缀。
|
||
- 需要「输出结构绝对合法」时,可以上受约束解码(JSON Schema 限死三种信封),
|
||
代价是采样空间被约束,个别内容可能与贪心输出不同。
|
||
|
||
### 实测性能(RTX 5060 Laptop 8G / sm_120,bf16)
|
||
|
||
同一批 15 条真实指令压测,`--max-model-len 32768 --max-num-seqs 32 --enable-prefix-caching`:
|
||
|
||
| 并发 | 请求吞吐 | 生成吞吐 | 延迟 p50 | 延迟 p95 |
|
||
|---|---|---|---|---|
|
||
| 1 | 3.6 req/s | 72 tok/s | 251 ms | 389 ms |
|
||
| 8 | 23.0 req/s | 469 tok/s | 278 ms | 425 ms |
|
||
| 32 | **49.4 req/s** | **1002 tok/s** | 337 ms | 480 ms |
|
||
|
||
流式 TTFT(并发 1)**87 ms**;显存占用:权重 3.1 G + KV cache 3.46 GiB(129,680 token ⇒ 32K 上下文可并发 3.96 路)。
|
||
参考:同一模型用 CPU + transformers 逐条生成是 **4.1–6.4 s/条**(约 0.2 req/s)。
|
||
|
||
### 在 WSL2 上部署的三个额外环境变量
|
||
|
||
vLLM 官方只发 Linux 轮子,Windows 用户通常跑在 WSL2 里;此时还需要:
|
||
|
||
```bash
|
||
# 1) V2 Model Runner 需要 pinned memory/UVA,而 vLLM 在 WSL2 上默认关掉 pin memory
|
||
export VLLM_WSL2_ENABLE_PIN_MEMORY=1
|
||
# 2) WSL 里若没有 gcc,Triton 运行时编译 kernel launcher 会报 "Failed to find C compiler"
|
||
# 可装 gcc(sudo apt-get install -y gcc g++),或用一个 zig cc 包装脚本当 CC
|
||
export CC=/path/to/your/c-compiler
|
||
# 3) 默认的 FlashInfer 采样器在缺 cubin 时会 JIT(需要 nvcc);无 CUDA toolkit 时关掉它
|
||
export VLLM_USE_FLASHINFER_SAMPLER=0
|
||
```
|
||
|
||
另外 WSL2 无 CUDA toolkit 时建议 `--compilation-config '{"mode":0}'`(跳过 torch.compile,启动更快、不需要 C++ 编译器)。
|
||
|
||
## 6. 评测
|
||
|
||
评测集 **131 条**,全部**未出现在训练集模板**中的说法(含 27 条刻意构造的硬样本):
|
||
中文序数词+负索引、口语化包装(「帮我/麻烦/能不能」)、上下文注入(`[上下文] 当前项目: …`)、
|
||
同义命令归一(删除/删掉/移除 → `del`)、绝对路径拒绝、运行目标索引推断…
|
||
|
||
| 指标 | 基座 Qwen2.5-1.5B-Instruct | **本模型(LoRA 微调后)** |
|
||
|---|---|---|
|
||
| 输出是合法 JSON | 131/131 = 100% | **131/131 = 100%** |
|
||
| 与标准命令**完全一致**(exact) | 39/131 = 29.8% | **122/131 = 93.1%** |
|
||
|
||
> 基座 JSON 合法率也是 100%,是因为语法和输出格式就写在 system prompt 里 ——
|
||
> 但「知道语法」≠「会用语法」:55 个命令族里有 39 个族的准确率发生变化,
|
||
> 基座在序号、配置、闲聊、拒绝这些地方大面积翻车。
|
||
|
||
几族的对比(体现微调到底学到了什么):
|
||
|
||
| 命令族 | 基座 | 本模型 | 说明 |
|
||
|---|---|---|---|
|
||
| `close_index_neg`(倒数第 N 个) | 0/7 | **7/7** | 中文序数词 → 负索引 |
|
||
| `del`(删除/删掉/移除归一) | 5/9 | **9/9** | 同义命令规范形式 |
|
||
| `new_typed`(`new py/rs/md/c` 省后缀) | 0/4 | **4/4** | 类型子命令 |
|
||
| `run_add_ctx`(上下文推索引) | 0/5 | **5/5** | `max+1` 索引推断 |
|
||
| `reject_abs_dir` / `reject_abs_file` | 0/4 / 0/3 | **4/4 / 3/3** | 固定拒绝话术 |
|
||
| `open_project_fwd`(正斜杠归一) | 0/3 | **3/3** | `/` → `\` |
|
||
| `chat_long`(超 20 字固定话术) | 0/3 | **3/3** | 逐字背下话术 |
|
||
| `config_*`(add/get/update/remove) | 0/1~0/3 | **多数转正** | 仍是最弱的一族,见局限 |
|
||
|
||
**未满分的族(诚实列出,共 8 个)**:`config_get` 2/3、`config_remove` 1/3、
|
||
`hard/config` 2/3、`hard/close_index` 1/2、`hard/open_project` 1/2、`hard/reject_abs` 1/2、
|
||
`hard/reject_run_global` 0/1、`chat_short` 0/1。
|
||
典型错误是**配置层级判断**:用户说「去掉配置项 X」时,模型输出 `config remove -p X`,
|
||
而标准答案是 `config remove -r X`(这类「默认层级到底该取谁」的语义在数据里信号不够强)。
|
||
|
||
## 7. 输入输出示例(**真实推理输出**,取自上述评测集)
|
||
|
||
```
|
||
输入:关掉倒数第5个文件
|
||
输出:{"intent": "command", "cmd": "close -5"}
|
||
|
||
输入:打开 C:/Users/yujia/PycharmProjects/ai-notes
|
||
输出:{"intent": "command", "cmd": "open project C:\\Users\\yujia\\PycharmProjects\\ai-notes"}
|
||
(正斜杠已按规则归一为反斜杠)
|
||
|
||
输入:删除 C:/Users/yujia/PycharmProjects/tmf/src/utils.py
|
||
输出:{"intent": "reject", "reply": "您没有权限!"}
|
||
(绝对路径出现在 del 上 → 固定拒绝)
|
||
|
||
输入:[上下文] 已有运行目标: target3
|
||
加个运行目标 测试=.venv\Scripts\python.exe -m uvicorn app:api --port 8000
|
||
输出:{"intent": "command", "cmd": "run 测试=.venv\\Scripts\\python.exe -m uvicorn app:api --port 8000"}
|
||
|
||
输入:把 darkhorse.code.editor.tab_size 设置成 4,存到运行时
|
||
输出:{"intent": "command", "cmd": "config add -r darkhorse.code.editor.tab_size=4"}
|
||
|
||
输入:帮我建一个 rust 的模块,叫 order_service
|
||
输出:{"intent": "command", "cmd": "new rs order_service"}
|
||
|
||
输入:D:\Projects\Python\Code-Rag 是Kotlin项目
|
||
输出:{"intent": "command", "cmd": "project lang kotlin D:\\Projects\\Python\\Code-Rag"}
|
||
|
||
输入:你觉得我该不该辞职去创业,帮我分析一下利弊
|
||
输出:{"intent": "chat", "reply": "你还是好好工作吧,房贷还清了吗?车贷还清了吗?"}
|
||
```
|
||
|
||
## 8. 训练细节
|
||
|
||
| 项 | 取值 |
|
||
|---|---|
|
||
| 基座 | `Qwen/Qwen2.5-1.5B-Instruct` |
|
||
| 微调方式 | LoRA(`r=16`、`alpha=32`、`dropout=0.05`),训练后**合并**成自包含权重 |
|
||
| 可训练参数 | 约 1.18%(其余冻结) |
|
||
| 训练数据 | **1638 条**(模板 + 槽位合成,`--seed 42` 完全可复现,不用 LLM 生成) |
|
||
| 超参 | `lr=2e-4`、`warmup_ratio=0.03`、2 epoch、`max_len=704`、`micro_bs=1` × `grad_accum=8` → 410 步 |
|
||
| 耗时 / 显存 | 45.1 分钟 / 峰值 **7.68 GB**(单卡 8 GB 笔记本 GPU) |
|
||
| loss | train 0.6818 → 0.0079(末轮均值 0.0319);eval 0.039 → 0.0092 |
|
||
| 精度 | bf16 |
|
||
|
||
**数据为什么是合成的**:这个任务要学的是「**照抄**」而不是「记住」—— 命令里大量是路径、命令串、
|
||
文件名。生成器把它们做成随机槽位,模型必须学会把输入里的路径原样搬进输出,而不是背样本;
|
||
同时保证可复现、零 API 成本、不会把外部 LLM 的错误学进去。
|
||
|
||
**三个刻意设计的难点**:① 约 15% 样本带上下文行,既要会用上下文补全、也要会忽略无关上下文;
|
||
② 随机加「帮我/麻烦/能不能/顺手」前缀与「吧/,谢谢/呀」后缀,放大表达多样性;
|
||
③ 27 条硬样本**只进评测集**,用来量真实泛化而不是量记忆。
|
||
|
||
## 9. 已知局限(请务必先读)
|
||
|
||
1. **只懂 darkhorse-code 的命令语**。换个 CLI/编辑器的命令体系,必须重新造数据微调。
|
||
2. **不是聊天模型**:闲聊会被压成 ≤20 字短回复,甚至回那句固定的「劝退」话术 —— 这是设计目标,不是 bug。
|
||
3. **`config` 族最弱**(见 §6):配置层级(`-g/-p/-r`)的默认值判断会出错,接入方应对
|
||
`config` 类输出做二次校验(层级缺失时按自己的业务规则补默认值)。
|
||
4. **输出偶尔会被解析层救回来**:小模型可能把 JSON 包在 ```json 代码块里、或路径里写单反斜杠
|
||
(非法 JSON 转义)。建议接入方保留「抠 JSON + 修非法转义」的解析兜底,而不是直接 `json.loads`。
|
||
5. **上下文格式是约定**:必须写成 `[上下文] <内容>` 并**另起一行**再接用户原话,
|
||
这是训练时的格式,别自由发挥。
|
||
6. **1.5B 的常识/推理上限**:超出命令翻译的语义理解(比如反讽、多轮澄清)不要指望它。
|
||
|
||
## 10. 文件说明
|
||
|
||
| 文件 | 说明 |
|
||
|---|---|
|
||
| `model.safetensors` | bf16 合并权重(约 2.9 GB),自包含、无需 adapter |
|
||
| `chat_template.jinja` | **模型配套的 chat template**,请务必用它(vLLM `--chat-template` 指向它) |
|
||
| `config.json` / `generation_config.json` | Qwen2 结构(28 层、2 KV head、`max_position_embeddings=32768`) |
|
||
| `tokenizer.json` / `tokenizer_config.json` / `vocab.json` / `merges.txt` / `added_tokens.json` | tokenizer 全套 |
|
||
| `configuration.json` | ModelScope 标记文件 |
|
||
|
||
## 11. 许可与致谢
|
||
|
||
- 本模型以 **Apache-2.0** 许可发布,与基座 `Qwen/Qwen2.5-1.5B-Instruct` 一致;
|
||
使用前请同时遵守基座模型的许可条款。
|
||
- 感谢 Qwen 团队开源的 Qwen2.5 系列。
|
||
- 训练所用数据为由模板与槽位**程序生成**的合成数据,不含任何真实用户数据或第三方隐私内容。
|