第 11 章 自然语言到 Bash:数据迁移与可执行评测
|
本章导读:以 nl2bash 语料为例演示指令微调的领域迁移——不新增训练代码, 只换一份数据,模型就多出「自然语言到 Shell 命令」的新能力。 上一章搭好的训练基建,本章原样复用。 学完本章你将能够:
产出与代码: |
11.1 原理概述:换数据,复用基建
第 10 章把训练与推理拆成两个程序。本章在这套基建上完成一次领域迁移:让训完
的 GPT-2 学一个新任务——输入自然语言描述,输出对应的 bash 命令。
领域迁移的本质是换数据:模型结构没动、损失函数没动、训练循环没动,动的
只有数据。同一个 sft_train,换一份 JSON,就能产出另一个领域的模型:
零修改不是运气,是第 7 章埋下的设计:所有指令微调数据都归一成
instruction / input / output 三元组,训练管道只认这个形状。本章的数据
转换沿用同一格式,sft_train 才能直接接收。
为什么选 bash 当练手任务?三个理由,同时也是选微调数据集的通用标准:
-
输出短——命令通常 ≤ 30 个 token,在小模型的学习能力范围内;
-
可验证——命令能真实执行,跑一下就知道对错,不依赖裁判模型;
-
有现成语料——TellinaTool/nl2bash 提供近万对齐样本。
第二条尤其重要。第 7 章的 LLM-as-judge 评分带裁判的主观性;bash 命令的 评测完全客观。能客观验证的任务,适合用小模型做能力实验——这条选型 标准在后续章节同样适用。
11.2 数据:nl2bash 从哪来,往哪去
微调需要成对的「自然语言描述 + bash 命令」样本。nl2bash 语料可以直接用:
它出自 Tellina 项目(Lin et al., LREC 2018)。研究者从 StackOverflow 的
「如何用命令做 X」类问答中自动挖掘对齐对,整理成两个行对齐的文件——
all.nl 存自然语言描述,all.cm 存 bash 命令,第 i 行一一对应。
来源决定了语料的两个特性:
-
描述口语化、不规范——提问者写的不是教科书英语,正好接近真实用户输入;
-
命令真实可执行——都来自真实问答,正好支撑执行类评测。
原始语料不能直接用于训练。转换脚本 scripts/convert_data.py 先做四件事:
# scripts/convert_data.py(节选)
for nl, cm in zip(nls, cms):
nl_s, cm_s = nl.strip(), cm.strip() # 去首尾空白
if not nl_s or not cm_s: continue # 空行
if len(nl_s.encode()) > args.max_nl_len: continue # NL > 400 字节
if len(cm_s.encode()) > args.max_cmd_len: continue # 命令 > 200 字节
if any(tok in cm_s for tok in ["rm -rf /", ":(){:|:&};:"]):
continue # 危险样本
kept.append({
"instruction": "Translate the following natural language "
"request into a bash one-liner.",
"input": nl_s, # 自然语言描述
"output": cm_s, # bash 命令
})
四条过滤分别处理:空行、超长描述(默认上限 400 字节)、超长命令(默认上限 200 字节)、危险样本。长度上限不只为了省上下文:语料中的超长命令集中在 多行 heredoc、嵌套 awk 这类复杂写法上,小模型学不动。过滤后训练分布集中在 短命令上,等于刻意收窄任务范围。
|
数据集剪裁本身就是模型能力设计:保留哪部分分布,模型就擅长哪部分任务。 |
指令前缀固定不变——所有样本共用同一个 instruction,差别只在 input。
这个细节在推理端有回报:bash_chat 把用户的任何输入装进同一个模板,
模型见到的分布与训练时完全一致。
11.3 训练:一行 C++ 都不用改
数据就绪后,训练命令只改两处参数:--data 指向新 JSON,--out 换输出
文件名。输出格式与第 7 章的 instruction-data.json 同构,sft_train
本体一行不改:
# 复用 chapter10_serving 的训练二进制(scripts/run.sh train 会切到 chapter10_serving 执行,
# data/ 经两章共享的 data/ 软链接指向仓库根 data/,路径两侧通用)
# 参数与第 10 章的 sft_train CLI 完全一致:--data/--weights/--size/--epochs/--batch/--lr/--out
./scripts/run.sh train \
--data data/bash-instruction-data.json \
--out data/bash-sft-small.pth \
--size small --epochs 5 --batch 8 --lr 5e-5
训练完成后,权重落在 --out 指定的路径。也可以使用 chapter11_nl2bash 内置的
后台训练脚本(自动检查权重文件,缺失时引导下载):
SIZE=medium EPOCHS=3 BATCH=4 ./scripts/train-background.sh
本章没有引入新算法。它的价值在另一处:证明第 7 章统一的 Alpaca 格式确实 让新增领域不需要改训练代码。
11.4 推理:bash_chat 与安全沙箱
推理侧的改动集中在 chat/src/bash_chat.cpp。与第 10 章的 sft_chat
相比,差异只有三处:
-
指令前缀固定(与训练一致——11.2 节说的「分布一致」在推理端的回报; 推理模板若与训练模板不同,模型面对的就是分布外输入,输出质量明显下降);
-
max_new_tokens=64(命令很短,64 个 token 足够,也缩短生成时间); -
--exec模式:生成后先过安全检查,再执行。
--exec 会把模型生成的命令交给 shell 执行。模型可能生成任意命令,包括
删除文件这类危险操作,因此执行前必须过一道闸:
白名单与黑名单
bool check_safe_command(const std::string& cmd) {
static const std::vector<std::string> banned = {
"rm", "mv", "cp", "dd", "sudo", "reboot", "mkfs", "curl", "wget",
"ssh", "bash", "sh", "python", "eval", "exec", /* ... */};
std::string tok = first_token(cmd); // 取首 token 的 basename
if (tok.empty()) return false;
for (const auto& b : banned) if (tok == b) return false;
for (const auto& a : safe_allowlist()) // ls/cat/grep/find/awk/sed...
if (tok == a) return true;
return false;
}
检查逻辑分三步:取命令首 token 的 basename;命中黑名单(rm、dd、sudo 等) 直接拒绝;命中白名单(ls、cat、grep 等)才放行。两个名单都不命中, 一律拒绝。
这是安全工程里的 deny-by-default 模式:默认拒绝一切,只显式放行清单内 成员。黑名单挡已知危险,白名单放行已知安全;真正兜底的是白名单——黑名单永远 列不全,单独依赖它时,任何名单外的新命令都会被放行。与之相对的 allow-by-default(默认全放行、只拉黑)不适合「执行模型生成的命令」场景: 模型可能生成任何字符串,黑名单无法穷举。
实际执行通过 popen("timeout --foreground 3 bash -lc '<quoted cmd>'") 完成:
timeout 3 防死循环(模型可能生成 yes 这类不退出的命令),shell_quote
用单引号转义命令内容,防止 shell 二次解释。REPL 里 ! 前缀表示生成后
立即执行。
白名单是执行侧的闸门,还有一层防御在训练侧:如果训练语料里满是危险命令,
模型会频繁生成它们。convert_data.py 在源头剔除 rm -rf /、fork 炸弹等
危险样本,模型接触不到这些模式,生成概率自然低。数据过滤(训练侧)+
白名单(执行侧) 两层配合,比任何单点防护都可靠。
实测效果——常见描述直接给出能跑的命令:
=== 输入自然语言描述,输入 quit 退出 ===
NL> list all the files in the current directory
$ ls
NL> count lines in file.txt
$ wc -l file.txt
11.5 评测:四个指标逼近「对不对」
开放对话的评测依赖裁判打分;bash 命令可以客观评测。eval/bash_eval.py
逐样本调用 bash_chat 生成命令,统计四个指标:
| 指标 | 定义 | 工具 |
|---|---|---|
EM |
预测命令 === 标准命令(去空白) |
字符串比较 |
AST-match |
两边 bash AST 规范化后一致(EM 但写法不同也算对) |
bashlex(可选依赖) |
safety_pass |
预测通过白名单检查 |
与 bash_chat 同一套名单 |
exec_safe |
沙箱试跑(只读 PATH、timeout 2s)返回码 0 |
subprocess |
为什么需要四个指标?bash 命令评测有一个绕不开的特性——多解性:同一句
「列出所有 txt 文件」,ls .txt、find . -name ".txt"、ls | grep .txt
都是合理答案。四个指标里,三个正确性指标分工递进:
-
EM 最严苛——模型给出完全合法的替代命令,也判 0 分;
-
AST-match 补上语义层——两棵 bash AST 规范化后一致即视为等价 (引号风格、空白差异不再误判;
ls -l与ls -al仍算不同); -
exec_safe 补上行为层——命令真的能跑通且退出码为 0 才算通过。
safety_pass 与正确性正交,检查的是「这条命令能否被安全执行」。三个 正确性指标合起来,才接近「模型是否真正理解任务」的判断。
评测的 exec 沙箱收紧到 PATH=/usr/bin:/bin, HOME=/tmp:即使模型生成
「合法但危险」的命令,破坏面也被压到最小。注意评测沙箱与交互沙箱
(--exec)是两套实现:评测要客观复现(固定 PATH),交互要拦截危险
命令(白名单),设计目标不同。
对比不同模型规模,一条循环即可:
for m in small medium; do
python bash_eval.py --model ../data/bash-sft-$m.pth --size $m \
--n 200 --out result-$m.json
done
结果落盘为 result-<size>.json,含 metrics 汇总
({"em": …, "ast_match": …, "safety_pass": …, "exec_safe": …, "n": …})
与逐样本明细。模型从 small 升级到 medium 后,通常 AST-match 先于 EM 抬升
——语义对了,字面还差。看指标抬升的顺序,比只看总分更能定位问题。
11.6 编译运行与预期输出
按顺序执行以下命令,完成数据下载、转换、训练与推理:
cd chapters/chapter11_nl2bash
./scripts/download-data.sh # nl2bash 语料 ~2 MB
./scripts/download-weights.sh # GPT-2 官方权重(small + medium,约 2 GB)
python3 scripts/convert_data.py --shuffle --out data/bash-instruction-data.json
# [完成] 9082 条样本写入 ...(实际条数随过滤参数变化)
./scripts/run.sh train --data data/bash-instruction-data.json \
--out data/bash-sft-small.pth --size small --epochs 5 --batch 8 --lr 5e-5 # 或 train-background.sh
./scripts/run.sh chat --model data/bash-sft-small.pth --size small --no-cuda
# 单条模式:
./scripts/run.sh chat --model data/bash-sft-small.pth --size small --no-cuda \
"find all .txt files modified in the last 7 days" --exec
验收关注三点:
-
交互模式对常见描述(列目录、统计行数、查文件)给出能跑通的命令;
-
--exec模式下,白名单外的命令不执行,打印「跳过执行」; -
bash_eval.py的四指标 JSON 正常落盘。
本章小结
-
领域迁移的本质是更换数据:nl2bash 语料转为 Alpaca JSON 后,训练代码 零修改——复用来自第 7 章的数据格式设计,不是运气。
-
数据集剪裁本身就是能力设计:长度过滤、危险样本剔除划定了训练分布, 保留什么,模型就擅长什么。
-
安全执行依赖多层防御:训练侧数据过滤 + 执行侧白名单 + timeout (deny-by-default 模式),训练与执行两头把关。
-
评测四指标层层递进:多解性任务需要多角度指标(EM / AST / safety / exec),指标抬升顺序比总分更能定位问题。
自测清单
能不回看就答出这些,本章就算过关:
-
convert_data.py 输出与 instruction-data.json 同构(sft_train 直接可训)
-
训练期间未改任何 C++ 代码(复用验证)
-
bash_chat 对 5 条基础描述生成正确命令(ls/wc/grep/find 级别)
-
--exec:白名单外命令被拒;yes类死循环被 timeout 截断 -
bash_eval.py 输出四指标 JSON,medium 比 small 的 AST-match 更高
习题
-
把
--max-cmd-len从 200 放宽到 500 重新训练,四指标怎么变? (长命令扩大训练分布 vs 小模型容量不足) -
给 convert_data.py 增加「按命令首词分层采样」逻辑,让 grep/find/awk 的比例均衡,对比评测指标。
-
在 bash_eval.py 里增加 BLEU 或编辑距离指标,观察 EM=0 但编辑距离很小 的样本——它们离正确只差一个参数。
-
把
bash_chat挂到第 10 章的 HTTP 服务后面(新端点/v1/bash), 用 curl 远程生成命令。 -
(思考)某命令首词同时出现在 allowlist 与 banned 里会怎样? 阅读两个集合的判断顺序,确认代码行为与预期一致。
拓展阅读
-
nl2bash 数据仓库(Tellina 项目,语料一手来源): https://github.com/TellinaTool/nl2bash
-
Lin et al., 2018, NL2Bash: A Corpus and Semantic Parser for Natural Language Interface to the Linux Operating System(语料与任务原始论文): https://arxiv.org/abs/1802.08979
-
GPT-2 medium 官方模型卡(权重下载): https://huggingface.co/openai-community/gpt2-medium
-
上一章:第 10 章 HTTP 服务 —— 本章复用的训练基建