Files
ModelHub XC 5b07c289be 初始化项目,由ModelHub XC社区提供模型
Model: yujianboisme/qwen2.5-1.5b-darkhorse-code-fine-tuning
Source: Original Platform
2026-09-29 03:31:14 +08:00

320 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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 系列。
- 训练所用数据为由模板与槽位**程序生成**的合成数据,不含任何真实用户数据或第三方隐私内容。