跟着LAP啥都学 跟着LAP啥都学

第 11 章 自然语言到 Bash:数据迁移与可执行评测

      +

      本章导读:以 nl2bash 语料为例演示指令微调的领域迁移——不新增训练代码, 只换一份数据,模型就多出「自然语言到 Shell 命令」的新能力。 上一章搭好的训练基建,本章原样复用。

      学完本章你将能够:

      1. 掌握领域语料到 Alpaca 格式的转换与过滤策略;

      2. 复用既有训练设施完成零修改训练;

      3. 实现 deny-by-default 的命令白名单沙箱执行;

      4. 设计并解读 EM / AST / safety / exec 四级客观评测。

      产出与代码:chapters/chapter11_nl2bash/(scripts/convert_data.py、chat/src/bash_chat.cpp、 eval/bash_eval.py;训练复用 chapters/chapter10_serving 的 sft_train)。

      11.1 原理概述:换数据,复用基建

      第 10 章把训练与推理拆成两个程序。本章在这套基建上完成一次领域迁移:让训完 的 GPT-2 学一个新任务——输入自然语言描述,输出对应的 bash 命令。 领域迁移的本质是换数据:模型结构没动、损失函数没动、训练循环没动,动的 只有数据。同一个 sft_train,换一份 JSON,就能产出另一个领域的模型:

      Diagram

      零修改不是运气,是第 7 章埋下的设计:所有指令微调数据都归一成 instruction / input / output 三元组,训练管道只认这个形状。本章的数据 转换沿用同一格式,sft_train 才能直接接收。

      为什么选 bash 当练手任务?三个理由,同时也是选微调数据集的通用标准:

      1. 输出短——命令通常 ≤ 30 个 token,在小模型的学习能力范围内;

      2. 可验证——命令能真实执行,跑一下就知道对错,不依赖裁判模型;

      3. 有现成语料——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 行一一对应。

      来源决定了语料的两个特性:

      1. 描述口语化、不规范——提问者写的不是教科书英语,正好接近真实用户输入;

      2. 命令真实可执行——都来自真实问答,正好支撑执行类评测。

      原始语料不能直接用于训练。转换脚本 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 相比,差异只有三处:

      1. 指令前缀固定(与训练一致——11.2 节说的「分布一致」在推理端的回报; 推理模板若与训练模板不同,模型面对的就是分布外输入,输出质量明显下降);

      2. max_new_tokens=64(命令很短,64 个 token 足够,也缩短生成时间);

      3. --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 都是合理答案。四个指标里,三个正确性指标分工递进:

      1. EM 最严苛——模型给出完全合法的替代命令,也判 0 分;

      2. AST-match 补上语义层——两棵 bash AST 规范化后一致即视为等价 (引号风格、空白差异不再误判;ls -l 与 ls -al 仍算不同);

      3. 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

      验收关注三点:

      1. 交互模式对常见描述(列目录、统计行数、查文件)给出能跑通的命令;

      2. --exec 模式下,白名单外的命令不执行,打印「跳过执行」;

      3. bash_eval.py 的四指标 JSON 正常落盘。

      本章小结

      • 领域迁移的本质是更换数据:nl2bash 语料转为 Alpaca JSON 后,训练代码 零修改——复用来自第 7 章的数据格式设计,不是运气。

      • 数据集剪裁本身就是能力设计:长度过滤、危险样本剔除划定了训练分布, 保留什么,模型就擅长什么。

      • 安全执行依赖多层防御:训练侧数据过滤 + 执行侧白名单 + timeout (deny-by-default 模式),训练与执行两头把关。

      • 评测四指标层层递进:多解性任务需要多角度指标(EM / AST / safety / exec),指标抬升顺序比总分更能定位问题。

      自测清单

      能不回看就答出这些,本章就算过关:

      1. convert_data.py 输出与 instruction-data.json 同构(sft_train 直接可训)

      2. 训练期间未改任何 C++ 代码(复用验证)

      3. bash_chat 对 5 条基础描述生成正确命令(ls/wc/grep/find 级别)

      4. --exec:白名单外命令被拒;yes 类死循环被 timeout 截断

      5. bash_eval.py 输出四指标 JSON,medium 比 small 的 AST-match 更高

      习题

      1. 把 --max-cmd-len 从 200 放宽到 500 重新训练,四指标怎么变? (长命令扩大训练分布 vs 小模型容量不足)

      2. 给 convert_data.py 增加「按命令首词分层采样」逻辑,让 grep/find/awk 的比例均衡,对比评测指标。

      3. 在 bash_eval.py 里增加 BLEU 或编辑距离指标,观察 EM=0 但编辑距离很小 的样本——它们离正确只差一个参数。

      4. 把 bash_chat 挂到第 10 章的 HTTP 服务后面(新端点 /v1/bash), 用 curl 远程生成命令。

      5. (思考)某命令首词同时出现在 allowlist 与 banned 里会怎样? 阅读两个集合的判断顺序,确认代码行为与预期一致。

      拓展阅读