17 KiB
license, license_link, language, pipeline_tag, library_name, base_model, tags
| license | license_link | language | pipeline_tag | library_name | base_model | tags | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| apache-2.0 | https://huggingface.co/Qwen/Qwen2.5-1.5B-Instruct/blob/main/LICENSE |
|
text-generation | transformers | Qwen/Qwen2.5-1.5B-Instruct |
|
darkhorse-code-instruct-1.5b
把一个微调过的 1.5B 小模型,做成编辑器里的「自然语言 → 私域命令」翻译器。 输入一句中文口语,输出一行严格 JSON,不是聊天回复。
输入:关掉倒数第5个文件
输出:{"intent": "command", "cmd": "close -5"}
{"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)
⚠️ 两条铁律,违反任一条效果都会明显掉:
- 必须使用本模型内置的 system prompt(下面
SYSTEM_PROMPT原文,逐字), 模型是在这份 prompt 下微调的;换 prompt = 训练/推理不一致。 - 必须走模型自带的 chat template(本仓库的
chat_template.jinja),tokenizer.apply_chat_template会自动读到它。
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:
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
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 里;此时还需要:
# 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. 已知局限(请务必先读)
- 只懂 darkhorse-code 的命令语。换个 CLI/编辑器的命令体系,必须重新造数据微调。
- 不是聊天模型:闲聊会被压成 ≤20 字短回复,甚至回那句固定的「劝退」话术 —— 这是设计目标,不是 bug。
config族最弱(见 §6):配置层级(-g/-p/-r)的默认值判断会出错,接入方应对config类输出做二次校验(层级缺失时按自己的业务规则补默认值)。- 输出偶尔会被解析层救回来:小模型可能把 JSON 包在 ```json 代码块里、或路径里写单反斜杠
(非法 JSON 转义)。建议接入方保留「抠 JSON + 修非法转义」的解析兜底,而不是直接
json.loads。 - 上下文格式是约定:必须写成
[上下文] <内容>并另起一行再接用户原话, 这是训练时的格式,别自由发挥。 - 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 系列。
- 训练所用数据为由模板与槽位程序生成的合成数据,不含任何真实用户数据或第三方隐私内容。