跳到主要内容

RSI 快速开始

七天实战

对应子站 Day 1 晚上的写作改进循环,以及 Day 2 的评估管线。7 天学会 RSI 提供交互练习;本文给出可在本地复现的最小脚本。

你将搭出什么​

一个不依赖特定厂商 SDK 的教学循环:对「把一段松散需求收成可测试的函数说明」做两轮改进。判据是规则,不是模型自评。模型接口留成函数,便于你接现有网关;没有密钥时,draft 可以用手工字符串代替,循环结构仍然成立。

完成后你应能:

  • 固定任务规格和评分器版本,使两轮分数可比。
  • 让反思引用失败规则的编号,而不是空泛建议。
  • 只改策略里的 constraints 字段,拒绝改 safety。
  • 在回归失败时丢弃补丁。

环境​

python3 --version   # 3.10+
mkdir -p ~/rsi-lab && cd ~/rsi-lab

不安装额外框架也能跑通规则评估。若你要接模型,使用你已有的 HTTP 网关即可,把密钥放在环境变量,不要写进策略文件。

任务与评分器​

任务是把需求收成四段说明:目标、输入、输出、边界。评分规则公开、可复算:

编号规则分值
R1含有「目标」段且不少于 20 字2
R2输入列出至少一个字段名2
R3输出描述可观察结果,而不是「返回一个好结果」2
R4至少一条明确的拒绝或边界条件2
R5全文不出现「等等」「自行发挥」2

满分 10。这五条是回归规则,改进器可以读失败编号,不能删除规则。

import re

RULES = {
"R1": lambda t: ("目标" in t) and len(t) >= 20,
"R2": lambda t: bool(re.search(r"输入[::].*\w+", t)),
"R3": lambda t: ("输出" in t) and ("好结果" not in t),
"R4": lambda t: any(k in t for k in ("拒绝", "边界", "不得")),
"R5": lambda t: ("等等" not in t) and ("自行发挥" not in t),
}

def evaluate(text: str) -> tuple[int, list[str]]:
failed = [k for k, fn in RULES.items() if not fn(text)]
score = 2 * (len(RULES) - len(failed))
return score, failed

evaluate 是纯函数。版本号写死在日志里:scorer_version = "rules-r1r5-v1"。以后改规则必须升版本,旧分数作废。

策略对象​

policy = {
"safety": "不得编造不存在的 API;不得建议关闭测试。",
"constraints": "用四段中文说明:目标、输入、输出、边界。每段写具体名词。",
"format": "纯文本,不要 Markdown 标题层级超过两级。",
}
WHITELIST = {"constraints"}

safety 不在白名单。改进函数如果返回的补丁含有其他键,整份补丁拒绝。

执行、反思、改进​

def execute(task: str, policy: dict, lesson: str = "") -> str:
# 教学桩:没有模型时返回一个故意残缺的草稿,方便看到失败编号。
# 接上模型后,把 task、policy、lesson 拼进提示词,返回模型文本。
base = f"目标:根据需求整理说明。输入:用户句子。输出:一个好结果。{policy['constraints']}"
if lesson:
base = lesson + "\n" + base
return base

def reflect(failed: list[str]) -> dict:
catalog = {
"R1": "目标段过短或缺失,下轮第一句写清要交付的工件名称。",
"R2": "输入段没有字段名,下轮至少写 user_text。",
"R3": "输出使用了不可观察的「好结果」,改成可检查的四段说明。",
"R4": "缺少拒绝条件,下轮写明不得编造 API。",
"R5": "出现了含糊词,下轮删除「等等」「自行发挥」。",
}
if not failed:
return {"grounded": False, "text": ""}
lines = [f"{k}: {catalog[k]}" for k in failed if k in catalog]
return {"grounded": bool(lines), "text": "\n".join(lines), "cites": failed}

def refine(policy: dict, lesson: dict) -> dict | None:
if not lesson.get("grounded"):
return None
patched = dict(policy)
extra = "必须逐条回应失败规则:" + ",".join(lesson["cites"])
patched["constraints"] = policy["constraints"] + " " + extra
# 只允许 constraints 变化
if set(patched) - set(policy):
return None
if patched["safety"] != policy["safety"]:
return None
return patched

反思文本由失败编号生成,因此天然带证据。接上模型写反思时,增加校验:反思里出现的编号必须是 failed 的子集,否则 grounded = False。

把一轮跑起来​

task = "把『帮我写个登录』收成可测试的函数说明。"
score1, failed1 = evaluate(execute(task, policy))
lesson = reflect(failed1)
candidate = refine(policy, lesson)
assert candidate is None or candidate["safety"] == policy["safety"]
score2, failed2 = evaluate(execute(task, candidate or policy, lesson["text"]))
print("v1", score1, failed1)
print("v2", score2, failed2)
print("scorer", "rules-r1r5-v1")

期望:第一轮至少失败 R3 或 R4;第二轮若约束被拼进草稿,R4/R5 有机会消失。若你换成真实模型,不要用分数的绝对值作结论,先看失败集合是否缩小,再看有没有新的规则失败(回归)。

回归门禁​

准备三道不参与文案优化叙事的固定题,例如「导出 CSV」「重置密码」「查询订单」。每一题用同一 evaluate。补丁采纳条件:

  • 当前任务失败集合是上一轮的真子集,或分数不下降。
  • 三道回归题没有新增失败。
  • safety 字符串哈希不变。

任一不满足,丢弃 candidate,教训写入记忆时标记 accepted=False。

REGRESSION = [
"导出最近 7 天订单为 CSV",
"为已登录用户重置密码",
"按订单号查询状态",
]

def regression_pass(policy: dict, baseline: list[list[str]]) -> bool:
for task, old_failed in zip(REGRESSION, baseline):
_, failed = evaluate(execute(task, policy))
if any(f not in old_failed for f in failed):
return False
return True

基线 baseline 在实验开始时算一次并写入 baseline.json。改进循环运行期间不许重写这个文件。这就是「评估集不在白名单」。

日志最少要有的字段​

每一轮追加一行 JSON:

{
"round": 2,
"scorer_version": "rules-r1r5-v1",
"score": 8,
"failed": ["R2"],
"lesson_cites": ["R3", "R4"],
"patch_keys": ["constraints"],
"accepted": true
}

没有 scorer_version 的日志不能放进「提升了多少」的句子里。子站实验室若只展示动画,你本地仍应留下这份日志,方便 Day 6 做防退化对照。

接到真实模型时改哪里​

只替换 execute。提示词结构建议固定为四块:安全段(原样注入,不经改进器)、约束段(可改进)、任务、上一轮教训。温度在对比实验里固定,否则分数变化分不清是策略还是采样。

评审器如果改用 LLM-as-Judge,保留规则分作为下限:规则失败时,评审器分数不得覆盖为满分。Day 2 会专门做评审器校准;快速开始阶段不要让模型给自己打唯一分数。

常见卡点​

  • 第二轮分数上升是因为你改了规则。检查 scorer_version。
  • 反思写了「需要更具体」,但没有编号。应判为不 grounded,禁止改进。
  • 补丁把「不得关闭测试」从安全段删掉。白名单校验应直接拒绝。
  • 回归题被改进器看见期望输出。回归题只提供任务文本,不提供满分范文。

检查清单​

  • 规则 R1–R5 可重复运行,结果不变。
  • 第一轮日志含失败编号。
  • 补丁只改 constraints。
  • 回归三题没有新增失败才接受补丁。
  • 安全段哈希在两轮之间相同。

下一篇:开发指南。七天安排见 从零到一。

把教训写成可检索记录​

快速开始的反思是即时字符串。从第三轮起,字符串会互相覆盖。加一个最小教训库,字段固定,避免「自由 JSON」:

memory = []  # 教学用列表;生产可换成表

def remember(lesson: dict, accepted: bool) -> None:
memory.append({
"cites": lesson.get("cites", []),
"text": lesson.get("text", ""),
"accepted": accepted,
"hits": 0,
})

def retrieve(failed_now: list[str]) -> str:
ranked = [m for m in memory if set(m["cites"]) & set(failed_now)]
ranked.sort(key=lambda m: (m["accepted"], m["hits"]), reverse=True)
if not ranked:
return ""
ranked[0]["hits"] += 1
return ranked[0]["text"]

检索键是失败编号的交集,不是整段语义向量。编号系统稳定时,这种检索比「把所有聊天嵌入再碰运气」更容易解释。Day 3 会把错误模式从规则编号推广到代码异常类型,字段仍然保持「证据 → 动作 → 是否被接受」。

对照实验:关掉门禁会怎样​

用同一草稿跑两次:

  • A:按上文,回归失败则丢弃补丁。
  • B:只要当前题分数上升就接受,不看回归。

在 B 里故意把约束写成「为了简洁可以省略输入字段」。当前题若不再检查字段,分数可能上升;回归题「按订单号查询状态」会新增 R2。A 拒绝这次补丁,B 接受并在下一道题上支付代价。把两次的 accepted 和回归失败集写进实验笔记。这个对照是 Day 6 防退化的缩小版,现在做完,后面不会把门禁当成可选项。

评审器 JSON 的最小形状​

当你把 execute 换成模型,仍建议规则分先行。若额外调用评审模型,强制 JSON:

{"score": 7, "quotes": ["输出:一个好结果"], "deductions": ["R3 不可观察"]}

校验:quotes 必须是候选文本的子串;不是子串则丢弃该条评审,防止评审器编造引用。score 若与规则分冲突,以规则分为准,并把冲突记入日志字段 judge_conflict: true。冲突率连续三轮高于 30% 时,停止改进循环,先修评审提示词。这是评估器校准,不是再改业务策略。

采样方差​

温度大于 0 时,同一策略的分数会跳动。比较两版策略的正确做法:

  1. 每版采样 3 次,记录失败集合。
  2. 比较失败集合的并集是否缩小,而不是比较某一次最高分。
  3. 若并集相同,宣布「未观察到稳定改进」,即使最高分更高。

教学脚本可以先把温度设为 0。做不到时,不要用单次样本写「提升了 30%」。

目录约定​

rsi-lab/
policy.json # safety 与 constraints
baseline.json # 回归失败集,运行中只读
rules.py # R1–R5 与版本号
trace.jsonl # 每轮一行
memory.json # 教训库

policy.json 可被补丁覆盖,覆盖前把旧文件复制为 policy.prev.json。回退就是把 prev 拷回。不要依赖编辑器的撤销历史当版本管理;至少在实验目录里 git init,每一轮接受补丁就提交一次,消息写失败编号。

和子站实验的对应​

本地脚本子站
R1–R5 规则分Day 2 质量评估三板斧中的规则化评分
教训编号Day 3 Reflexion 的失败轨迹
只改 constraintsDay 4 把提示词当可进化对象
回归三题Day 6 防退化
trace.jsonl实验室里的循环回放

子站可以没有你的密钥。本地脚本的日志格式保持稳定,便于你把一晚的实验贴回笔记,而不是只截一张分数图。

收尾标准​

你的 trace.jsonl 里应能找到:一轮 accepted: false(证明门禁真的会拒绝),一轮 accepted: true,以及相同的 scorer_version。三者缺一,就还没开始 RSI,只是生成了两段不同的文字。

继续阅读 开发指南,把单文件脚本拆成执行器、评估器、反思器、改进器和门禁五块。

第二道练习:数字保持​

把任务换成产品介绍改写,并增加规则 R6:规格中出现的每一个百分数和整数都必须在输出中原样出现。规格里放 99.9%、120 字、7 天三个数。第一版草稿故意只留 7 天。日志应出现 R6。改进器只允许把「逐字保留规格中的数字」写进 constraints。回归题各带一组不同的数字,防止策略背下 99.9% 这一串。

做完后比较两条轨迹:登录说明那题的失败集合,和数字保持题的失败集合。它们不应共用同一条没有编号的「写具体一点」。若你的教训库把两条都收成这句话,说明检索键太粗,回到编号。

提交给自己的最小包​

打包 policy.json、baseline.json、trace.jsonl、rules.py。换一台机器只跑评估、不跑模型,分数应与日志一致。对不上就说明评估依赖了隐藏状态,例如本机时区或没钉死的随机数。这个包也是日后写进更新日志的证据,比截图可靠。

把契约测试先跑绿​

在接模型之前,把上一节开发指南中的 rule_r6、check_trace、should_stop 抄进 rsi_loop.py,并写测试。测试数据不要用你准备拿来「展示提升」的那道题,否则你会为了让测试好过而改题。用固定字符串:

  • 规格 锁定 15 分钟,每日 5 次,可用性 99.9%,输出 尽快重试,R6 失败。
  • 同一规格,输出原样包含三处数字和单位,R6 通过。
  • 一行 jsonl 同时 regression_new=true 与 accepted=true,check_trace 必须报违规。

三条例子全部按预期失败或通过之后,再把 execute 换成模型。顺序反了,你会用模型的发挥解释测试失败。

本地没有密钥时,execute 返回常量草稿即可,token 字段填 0。should_stop 在 token 上限为 0 时会立刻预算停止,因此无模型演练把 max_tokens 设成一个很大的数,只练习轮数和平台期。把这个例外写进 README,避免以后有人以为预算被关掉了。