SemIf 本地跑决策先管住输入 / 输出差异才好复现

文章导读
本地跑 SemIf 时两轮输出对不上,先别急着改模型或调参。多数情况下差异来自输入文件被临时替换、运行参数没记录、随机种子或环境变量没固定。把每轮运行的输入钉成一个可命名的版本,输出差异才有对照物;这一步做完,再判断差异是偶发还是稳定复现,后面的回归验证才有支点。
📋 目录
  1. Ⅰ 给每次 SemIf 本地运行保存输入文件和运行参数
  2. Ⅱ 对输入做版本标记避免临时改动混入
  3. Ⅲ 用同一输入连续运行记录输出差异出现的频率和位置
  4. Ⅳ 对比不同输入版本下的决策路径或输出摘要
  5. Ⅴ 把稳定复现的输入固化为回归样例
A A

本地跑 SemIf 时两轮输出对不上,先别急着改模型或调参。多数情况下差异来自输入文件被临时替换、运行参数没记录、随机种子或环境变量没固定。把每轮运行的输入钉成一个可命名的版本,输出差异才有对照物;这一步做完,再判断差异是偶发还是稳定复现,后面的回归验证才有支点。

处理顺序建议是:先固定输入与参数并留痕,再用同一输入连续运行观察差异分布,然后用不同输入版本做对照,最后把稳定复现的输入固化成回归样例。适用场景是本地单机运行、输入以文件形式给出的 SemIf 决策流程;如果 SemIf 内部还依赖远程服务或外部模型,输入管理只能控住其中一部分变量,其余差异需要结合运行环境确认。

给每次 SemIf 本地运行保存输入文件和运行参数

需要记录的字段不复杂,关键是每条运行都能回指到一份确定的输入。建议把输入按「场景_版本_序号」命名,参数写在同一个 RUN_ID 目录里的 manifest 文件中。

输入文件命名规则示例:scene-a_v20240513-01_input.json。同一轮有多个输入时加语义后缀,例如 _prompt.json、_rules.yaml。序号用两位递增,避免同一天多次运行互相覆盖。

运行参数记录字段建议至少包含:运行时间、完整命令行、SemIf 版本或代码提交标识、输入文件绝对路径、输入文件哈希、配置文件路径、影响决策的参数(阈值、top_k、温度、随机种子等)、依赖的环境变量名(值可不写全)、输出目录。

# runs/scene-a/v20240513-01/manifest.env 用途:记录本轮运行的输入与参数
RUN_ID=v20240513-01
INPUT=inputs/scene-a/v20240513-01/input.json
INPUT_SHA256=$(sha256sum "$INPUT" | cut -c1-12)
SEMIF_VERSION=$(semif `--version` 2>/dev/null || echo unknown)
ARGS='`--config` configs/semif.local.yaml `--top`_k 1 `--seed` 0'
OUTPUT_DIR=runs/scene-a/v20240513-01

把 manifest 和输出放在同一个 RUN_ID 目录下,之后查差异不用靠记忆还原当时条件。如果 SemIf 不支持某个参数,就原样记下命令行,不要补写没设过的值。

对输入做版本标记避免临时改动混入

版本标记的作用是防止「临时改一下输入再跑一次」混进同一批结果。建议输入目录按运行快照复制,快照本身只读,不再就地编辑。

标记方法可以用两种叠加:日期加序号,例如 v20240513-01,用于人类阅读和排序;哈希摘要,例如 sha256 前 12 位,用于内容指纹。文件名放日期序号,manifest 里存哈希,即使后来改过文件名,对应关系也不会丢。

# 用途:把当前输入固化成一份带标记的快照
RUN_ID=$(date +%Y%m%d)-01
DEST=inputs/scene-a/$RUN_ID
mkdir -p "$DEST"
cp ./input.json "$DEST/input.json"
sha256sum "$DEST/input.json" | cut -c1-12 > "$DEST/input.sha256"
chmod -w "$DEST/input.json"

已经跑过一轮的输入目录不再写回;确需修改就新建 RUN_ID。这样「输出对不上」时,第一步就能排除输入被就地改动这个原因。

用同一输入连续运行记录输出差异出现的频率和位置

确认差异是偶发还是稳定复现,最直接的做法是用同一份输入连续跑多次,输出分别保存,再比较每次的结果落在哪里。

SemIf 本地跑决策先管住输入 / 输出差异才好复现
#!/usr/bin/env bash
# 用途:固定输入版本,连续运行同一 SemIf 决策,观察输出差异
set -u
RUN_ID="${1:-$(date +%Y%m%d)-01}"
IN="inputs/scene-a/${RUN_ID}/input.json"
OUT_ROOT="runs/scene-a/${RUN_ID}"
mkdir -p "${OUT_ROOT}/diff"
for i in $(seq -w 1 5); do
  semif `--input` "${IN}" \
         `--config` configs/semif.local.yaml \
         `--seed` 0 \
         `--out` "${OUT_ROOT}/out-${i}.json" \
         > "${OUT_ROOT}/out-${i}.stdout.log" \
         2> "${OUT_ROOT}/out-${i}.stderr.log"
done

上面 `--seed` 只是占位,参数名按 SemIf 实际支持的写法替换;如果工具不提供固定种子的入口,就把这行去掉并在 manifest 里注明。

输出保存格式建议每个 RUN_ID 一个目录,结构化决策结果一份,stdout 与 stderr 分开落盘,diff 单独放一个子目录:

runs/scene-a/v20240513-01/
├── manifest.env
├── out-01.json
├── out-01.stderr.log
├── out-02.json
├── out-02.stderr.log
└── diff/
    ├── out-01_vs_out-02.diff
    └── summary.tsv

汇总时不必逐个人工比对,先用 diff 或 jq 抽出决策标签和路径摘要,写进 summary.tsv,再统计差异出现在第几次运行、落在哪一个决策步骤。同一位置反复出现,说明它可能对输入里的某个字段敏感。

对比不同输入版本下的决策路径或输出摘要

对比的目的不是看出「变了」,而是把输入变化和输出变化对齐。建议固定一张表的列名,每个输入版本占一行,填完再看规律。

输入版本标记输入哈希(前 12 位)关键参数摘要输出决策标签决策路径摘要首个差异位置是否稳定复现
v20240513-01(格式示例)<hash 占位>top_k=1, seed=0决策标签 AA→B→D第 2 步分支是
v20240513-02(格式示例)<hash 占位>top_k=1, seed=0决策标签 BA→B→C第 2 步分支否

示例只说明字段怎么落位,实际数值请填本地跑出来的结果。表填完之后看两点:同一输入版本内多次运行是否落在同一行结果;输入只改一个字段时,差异位置是否集中在同一个决策步骤。如果集中在同一步,通常说明该步骤对这个字段敏感,排查可以从那里开始。

把稳定复现的输入固化为回归样例

把稳定复现的输入连同参数、期望输出一起放进 cases 目录。之后改动 SemIf 或调整配置时,先跑这组样例,就能快速看出是否引入了新的差异。

cases/
└── scene-a-topk1/
    ├── input.json
    ├── params.env
    ├── expected/summary.json
    └── run_case.sh

运行命令占位符:bash cases/scene-a-topk1/run_case.sh,或在脚本内部读取自身路径后直接调用。

#!/usr/bin/env bash
# 用途:用固定输入跑一次 SemIf,并与期望摘要比对
set -eu
CASE_DIR=$(cd "$(dirname "$0")" && pwd)
. "${CASE_DIR}/params.env"
TMP="$(mktemp -d)"
semif `--input` "${CASE_DIR}/input.json" \
       `--config` "${CASE_DIR}/params.env" \
       `--out` "${TMP}/out.json"
# 只比对决策标签和路径摘要,不把整段日志当断言条件
jq -S '{label, path}' "${TMP}/out.json" > "${TMP}/summary.json"
if diff -q "${TMP}/summary.json" "${CASE_DIR}/expected/summary.json" >/dev/null; then
  echo PASS
else
  echo DIFF
  diff -u "${CASE_DIR}/expected/summary.json" "${TMP}/summary.json" || true
fi

断言条件建议只取决策标签和路径摘要这类稳定字段,日志里的时间戳、路径、耗时不要进比对,否则环境噪声会让回归误报。回归样例也不宜一次收太多,先把连续运行稳定、且能被清楚描述的输入收进来;不稳定复现的输入继续留在 runs 目录观察,暂时不要当成断言基线。