SemIf 的语义决策结果怎么复现 / 从输入快照和随机种子查起?

文章导读
想把一次 SemIf 决策结果搬到另一套环境复现,卡住的通常不是判定逻辑本身,而是当时喂进去的输入、当次运行的随机性参数和版本信息没有被完整留下。语义判定类组件的结果一般由三部分决定:原始输入、配置与模型/规则版本、随机性来源(种子、采样、并行度、批大小)。可行的路径是先锁定一个目标输出,再把这三部分逐一冻结,在受控环境里做对照,而不是直接在新环境重跑一遍再拿结果对比。
📋 目录
  1. Ⅰ 找出一次想复现的 SemIf 决策输出并保存原始输入
  2. Ⅱ 检查运行环境里影响随机性的开关是否一致
  3. Ⅲ 用相同输入在受控环境跑两次确认差异是否仍出现
  4. Ⅳ 若差异消失,对比两次环境快照找出改动项
  5. Ⅴ 把复现步骤写成命令注释便于他人验证
A A

想把一次 SemIf 决策结果搬到另一套环境复现,卡住的通常不是判定逻辑本身,而是当时喂进去的输入、当次运行的随机性参数和版本信息没有被完整留下。语义判定类组件的结果一般由三部分决定:原始输入、配置与模型/规则版本、随机性来源(种子、采样、并行度、批大小)。可行的路径是先锁定一个目标输出,再把这三部分逐一冻结,在受控环境里做对照,而不是直接在新环境重跑一遍再拿结果对比。

复现 SemIf 决策,建议按「输入快照 → 随机性开关 → 受控双跑 → 环境快照 diff → 注释化交接」五步走。先用调用标识把原始输入、输出、时间戳和配置一起存下来;若同一 seed 在同一环境连跑两次仍不一致,说明还有未固定的非确定性来源,需要结合并行度、批处理和外部依赖确认,而不是先怀疑判定逻辑写错。

找出一次想复现的 SemIf 决策输出并保存原始输入

先确定要复现哪一条输出。建议按调用标识(request_id 或 trace_id)来锁定,因为同一个请求在日志里往往分散在多行,按时间戳找容易串到相邻请求。锁定后立刻把四样东西落到同一个目录:输入原文、输出原文、时间戳、调用标识,再附一份当时的生效配置。

repro/semif-<request_id>/
  input.raw.txt      # 原始输入字节,不要存二次格式化后的 JSON
  output.raw.json    # 当次输出的完整原文
  meta.json          # started_at / finished_at / request_id / trace_id
  config.snapshot    # 当时生效的配置
  versions.txt       # 依赖版本与模型或规则包版本

输入建议存原始字节:JSON 的 key 顺序、空白符、换行和不可见字符,都可能被部分语义判定实现当成输入特征,二次序列化后再存,等于换了一份输入。meta.json 里至少写清 request_id、trace_id、起止时间、seed、模型或规则包版本、prompt 或模板版本、容器镜像 digest。如果日志里只落了一小段输入预览,需要临时打开输入落盘开关,再触发一次同类请求,才能拿到完整原文。

grep -F REQUEST_ID app.log | head -n 20
# 看日志里到底落了哪些字段,缺哪项就在下次运行时补上

检查运行环境里影响随机性的开关是否一致

很多人以为语义判定是确定性的,实际上采样开关、并行度和批大小都会让同一条输入走出不同分支。下面这份清单里的键名只是占位,需要按你实际的配置前缀替换,然后逐项和原环境对齐。

SEMIF_SEED=            # 主种子,未设置时组件可能回退到时间或其他随机源
SEMIF_SAMPLING=        # greedy / sample,采样开关本身就会改变结果分布
SEMIF_TOP_P=
SEMIF_TEMPERATURE=
SEMIF_NUM_SAMPLES=
SEMIF_BATCH_SIZE=
SEMIF_WORKERS=         # 并行线程或进程数
SEMIF_CACHE=           # 是否命中历史缓存
SEMIF_MODEL_REVISION=
SEMIF_TEMPLATE_VER=

读取顺序建议这样:先看启动命令行有没有覆盖配置,再看环境变量,最后看配置文件里的默认值。启动命令行的优先级通常最高,容易成为两次运行之间被忽略的差异点。

ps -ef | grep semif              # 看命令行覆盖了哪些参数
env | grep -i semif              # 看环境变量注入
docker inspect <container> | grep -i -E 'env|image'   # 容器场景

并行度和批大小值得单独留意:浮点累加顺序、batch 内 padding、共享缓存的命中情况,都可能让同一输入在不同并发下走到不同分支。这类差异不会报错,只会安静地改变输出。

用相同输入在受控环境跑两次确认差异是否仍出现

不要在原来那套繁忙环境里做对照,先把输入拷到一台固定配置的机器上跑。固定 seed 跑两次,可以判断 SemIf 本身是否还有未暴露的非确定性;放开 seed 再跑两次,可以观察随机性对结果的影响幅度。

SemIf 的语义决策结果怎么复现 / 从输入快照和随机种子查起?
#!/usr/bin/env bash
set -euo pipefail
IN=${1:-input.raw.txt}
RUN_ID=${2:-run1}
OUT_DIR=./repro/out/${RUN_ID}
mkdir -p ${OUT_DIR}

SEMIF_SEED=12345 \
SEMIF_SAMPLING=greedy \
SEMIF_WORKERS=1 \
semif-cli decide \
  `--input` ${IN} \
  `--out` ${OUT_DIR}/output.json \
  `--trace` ${OUT_DIR}/trace.json \
  2> ${OUT_DIR}/stderr.log > ${OUT_DIR}/stdout.log

两次输出分别落在 repro/out/run1/output.json 和 repro/out/run2/output.json,然后直接用 diff 比。判读方式可以先按下面三种走:

  • seed 相同、环境相同,两次输出一致,但和原环境不一致:问题更可能在版本或配置差异上,进入下一步做环境快照对比。
  • seed 相同、环境相同,两次输出仍不一致:说明还有非确定性来源没被固定,优先查并行度、批处理、缓存和外部服务调用。
  • seed 不同或采样开关不同:先别急着比对结果,这两项本身就是预期会出现差异的变量。

若差异消失,对比两次环境快照找出改动项

如果在受控环境跑通、差异消失,说明原环境里发生了变化。这时需要把两边的快照都采下来做 diff,采集范围建议覆盖四类:依赖版本、配置项、输入路径、运行参数。

python -m pip freeze > versions.txt          # 或 poetry export / conda list
sha256sum input.raw.txt >> versions.txt     # 输入本身也要留指纹
env | grep -iE '^(SEMIF|MODEL|CUDA|OMP)' | sort > env.snapshot
ps -ef | grep semif > cmdline.snapshot
cat /path/to/semif.config.* > config.snapshot

diff -u old/env.snapshot new/env.snapshot
diff -u old/versions.txt new/versions.txt

除版本和配置外,还要留意输入路径本身:路径不同导致分词器或预处理读到不同资源、挂载卷里换了模板文件,都会让结果漂移。如果 SemIf 会调用外部服务,需要一并记录服务端版本和服务端返回的 request id,服务端换版本同样会改变决策,这类差异在本地快照里是看不到的,需要结合对方环境的说明确认。

把复现步骤写成命令注释便于他人验证

排查结束后,把关键信息写成注释贴在脚本头部或提交信息里,别人拿到 repro 目录就能自己跑一遍,不用再问你当时设了什么。模板可以直接套用:

# [input]  repro/semif-12345/input.raw.txt  sha256=<hash>
# [config] SEMIF_SEED=12345 SEMIF_SAMPLING=greedy SEMIF_WORKERS=1
# [cmd]    semif-cli decide `--input` input.raw.txt `--out` out.json
# [expect] decision=approve reason_code=RULE_3
# [actual] decision=review  reason_code=RULE_7
# [diff]   model_revision A -> B,其余配置未变

注释里的 expect 写原环境的输出,actual 写新环境的输出,diff 只记已确认的改动项;还没确认的猜测不要写进去,否则下一个人会把猜测当结论继续传。写清输入指纹和配置之后,复现过程就从个人终端里的临时操作,变成了可以被别人复算的记录。