← 返回信息流

dev.to #ai短讯

终端智能体任务工厂:复现递归式任务扩展

dev.to作者:Михаил教程AI评分:50/100

Agent Lab Journal 发布指南,介绍如何为终端智能体构建可验证的任务工厂。重点在于通过手工编写环境、参考解和验证器来生成递归复杂度的测试任务,以解决手动创建任务效率低的问题。

终端智能体任务工厂:复现递归式任务扩展

指南
术语表
俄语

终端智能体的任务工厂:逐步验证,复现递归式任务扩展。

难度:高级 · 阅读时间:75 分钟 · 更新时间:2026 年 10 月 10 日

手工为终端智能体编写难以验证的任务效率低下:每个任务都需要环境、参考解决方案以及相互一致的测试用例。完全合成的任务成本较低,但指令、测试用例和容器往往会出现偏差。这会导致产生无法解决的任务,或者出现毫无理由就能通过的测试。本指南旨在构建一条中间路径。你从已经验证过的一个任务开始。模型会提出一个更难的版本,编写新的解决方案和测试用例,而结果必须在干净的容器中通过一系列机械化的检查关卡才能被接受。被接受的任务随后将成为下一层级的种子。最后,你衡量智能体在每一层级的成功率下降幅度,以及在此过程中被关卡拒绝的候选任务数量。

本文的内容与不包含的内容。本文包含完整的设计方案、可运行的代码骨架以及一个经过演示的任务示例。本文不报告实测数据。我们尚未针对特定模型运行此流水线以撰写本文,因此下文中的所有结果表格均为供你填写的空模板。请将示例中出现的任何数字视为说明性内容,而非研究发现。

  1. 问题概述

要训练或评估终端智能体,你需要具备三个一致部分的任务:智能体读取的指令、环境(包含文件和工具的容器镜像)以及自动化检查器(智能体完成后运行的测试)。人们能写出高质量的任务,但数量很少。语言模型能生成大量任务,但其错误往往出现在可预测的地方:测试用例检查了指令从未要求的内容、镜像中缺少必需的工具、参考解决方案“通过”仅仅是因为测试用例为空,或者解决方案实际上从未运行过。递归扩展之所以有效,是因为每个新任务都继承了一个已经可用的基础。你只需验证差异部分,这比验证整个新任务要容易得多。

  1. 最终成果
  • 一种脚本可检查的任务目录格式:包含指令、Dockerfile、参考解决方案、测试用例和需求映射。
  • extend.py 步骤:通过你配置的命令向任意模型请求严格 JSON 格式的更难子任务。
  • validate.py 步骤:包含七个检查关卡。它们在关闭网络连接的沙箱容器中运行。
  • grow.py 循环:构建深度为 0 → 1 → 2 → 3 的血统树,并记录每一次拒绝及其原因。
  • measure.py 步骤:对每个任务让智能体运行 N 次,报告每个深度的成功率(附带威尔逊置信区间),以及每条血统线的成对下降率。
  1. 前置条件
  • Linux 或 macOS,配备 Docker(无根模式或标准模式)、Python 3.11+,主机上安装 pytest 用于本地语法检查。
  • 某个能从 stdin 接收提示词并在 stdout 输出模型响应的命令。称之为 $EXTENDER_CMD。它可以封装任何提供商的 CLI 或 SDK。本文不假设特定的提供商。
  • 某个针对任务容器运行智能体的命令。称之为 $AGENT_CMD。第 9 节定义了契约。
  • 预算。每次扩展尝试都会消耗一次模型调用以及多次容器运行,而测量阶段需要对每个任务进行 N 次智能体回合。在扩大规模前请估算此项成本。
  1. 任务格式

每个任务都是一个目录。智能体只能看到 instruction.md 以及 Dockerfile 放入镜像中的任何内容。测试用例和参考解决方案是在智能体完成后挂载的,永远不会被烘焙到镜像中。

tasks/ logreport-d0/ task.json # id, parent, depth, requirement ids, superseded tests instruction.md # what the agent reads Dockerfile # environment; must not COPY tests/ or solution.sh env/ # files copied into the image (fixtures) solution.sh # reference ("oracle") solution tests/ test_outputs.py # pytest; each test tagged with a requirement id `` seed 的 task.json: ``json

{ "id": "logreport-d0", "parent": null, "depth": 0, "requirements": { "R1": "Read every *.log file in /app/logs", "R2": "Write /app/report.json mapping HTTP status code (string) to count (int)" }, "superseded_tests": [] } `` 需求映射表承担实际工作。G5 门控使用它来检查指令和测试是否描述的是同一件事,而合成任务往往正是在这里发生漂移。 5. 具体案例:种子任务 我们故意使用一个非常小的种子任务,以便每一层级的内容都能被人阅读。本案例的重点在于机制本身。种子任务的难度在此并不重要。 instruction.md(深度 0) ``plaintext

/app/logs/*.log 中包含组合格式的访问日志。 生成 /app/report.json:一个 JSON 对象,其键为作为字符串的 HTTP 状态码,值为具有该状态码的请求数量,对所有文件进行汇总统计。[R1] [R2] `` Dockerfile ``docker

FROM python:3.12-slim@sha256:<pin-a-digest-you-have-pulled> RUN pip install --no-cache-dir pytest==8.3.3 WORKDIR /app COPY env/logs /app/logs `` 通过摘要(digest)固定基础镜像。否则,“干净的环境”在多次运行之间可能会悄然发生变化,导致你在不同层级看到的差异实际上可能源于镜像漂移。 solution.sh ``shell

#!/usr/bin/env bash set -euo pipefail python3 - <<'PY' import json, glob, re, collections pat = re.compile(r'"\S+ \S+ \S+" (\d{3}) ') c = collections.Counter() for f in sorted(glob.glob("/app/logs/*.log")): with open(f, encoding="utf-8", errors="replace") as fh: for line in fh: m = pat.search(line) if m: c[m.group(1)] += 1 json.dump(dict(c), open("/app/report.json", "w"), sort_keys=True) PY `` tests/test_outputs.py ``python

import json, pathlib, pytest

REPORT = pathlib.Path("/app/report.json")

@pytest.mark.req("R2") def test_report_exists_and_is_object(): data = json.loads(REPORT.read_text()) assert isinstance(data, dict) assert all(isinstance(k, str) and isinstance(v, int) for k, v in data.items())

@pytest.mark.req("R1", "R2") def test_counts_match_fixture(): # Expected counts are computed once from env/logs by a human and frozen here. expected = {"200": 7, "301": 1, "404": 2, "500": 1} assert json.loads(REPORT.read_text()) == expected `` 上述预期计数属于我们的测试数据(fixture)。请根据你自己的 env/logs 计算预期值,并将其冻结在测试文件中。如果预期值是在测试运行时使用与解决方案相同的逻辑计算的,那么测试只能证明代码与其自身一致。 在 tests/conftest.py 中注册标记: ``python

def pytest_configure(config): config.addinivalue_line("markers", "req(*ids): requirement ids covered by the test") `` 在继续之前,手动验证种子任务,使用你将为子任务使用的相同验证器(第 7 节)。未能通过自身门控的种子任务会将缺陷传递给所有基于它构建的任务。 6. 扩展步骤 扩展器获取完整的父任务,并必须返回一个子任务(JSON 格式)。子任务增加一项新功能,除非明确替代某项要求,否则保留父任务的所有要求。每一步都尽可能少地改变内容,因此当子任务未通过某个门控时,你可以知道是哪项新增功能导致了失败。 prompts/extend.txt 你正在扩展一个经过验证的终端任务。你将接收父级指令、Dockerfile、环境文件列表、参考解决方案、测试用例和需求映射表。 生成一个更难的子任务,该任务必须满足以下条件: - 仅增加一项新能力,这是胜任的工程师在实际终端工作中需要实现的(例如新的输入格式、新的约束条件、新的输出、错误处理、性能边界); - 保留所有父级要求,除非在“superseded_tests”中列出并附有理由; - 能够在给定的镜像内离线解决,包括你在 Dockerfile 中以固定版本添加的任何包; - 具有确定性测试,预期值已冻结;切勿使用与解决方案相同的代码来计算预期值; - 使用需求 ID 为每个测试标记 @pytest.mark.req(...),并在指令中将每个需求 ID 提及为 [Rn]。 仅返回包含以下键的 JSON: instruction_md, dockerfile, env_files (路径 -> 内容,仅限文本), solution_sh, tests_py, requirements (ID -> 文本), superseded_tests (测试名称列表), rationale. ``python

import json, os, pathlib, shlex, subprocess, sys

def read_task(d: pathlib.Path) -> dict: env = {str(p.relative_to(d)): p.read_text(errors="replace") for p in (d / "env").rglob("*") if p.is_file()} return { "task": json.loads((d / "task.json").read_text()), "instruction_md": (d / "instruction.md").read_text(), "dockerfile": (d / "Dockerfile").read_text(), "env_files": env, "solution_sh": (d / "solution.sh").read_text(), "tests_py": (d / "tests" / "test_outputs.py").read_text(), }

def extend(parent: pathlib.Path, child: pathlib.Path) -> None: prompt = pathlib.Path("prompts/extend.txt").read_text() payload = prompt + "\n\nPARENT:\n" + json.dumps(read_task(parent), indent=1) out = subprocess.run(shlex.split(os.environ["EXTENDER_CMD"]), input=payload, capture_output=True, text=True, timeout=900, check=True).stdout start, end = out.find("{"), out.rfind("}") spec = json.loads(out[start:end + 1])

p = json.loads((parent / "task.json").read_text()) child.mkdir(parents=True) (child / "tests").mkdir() for rel, content in spec["env_files"].items(): dst = child / rel if not dst.resolve().is_relative_to(child.resolve()): raise ValueError(f"env path escapes task dir: {rel}") dst.parent.mkdir(parents=True, exist_ok=True) dst.write_text(content) (child / "instruction.md").write_text(spec["instruction_md"]) (child / "Dockerfile").write_text(spec["dockerfile"]) (child / "solution.sh").write_text(spec["solution_sh"]) (child / "tests" / "test_outputs.py").write_text(spec["tests_py"]) (child / "tests" / "conftest.py").write_text( (parent / "tests" / "conftest.py").read_text()) (child / "task.json").write_text(json.dumps({ "id": child.name, "parent": p["id"], "depth": p["depth"] + 1, "requirements": spec["requirements"], "superseded_tests": spec.get("superseded_tests", []), "rationale": spec.get("rationale", ""), }, indent=2))

if __name__ == "__main__": extend(pathlib.Path(sys.argv[1]), pathlib.Path(sys.argv[2])) `` 注意:环境文件由模型重写。如果父级夹具较大或是二进制文件,请从父级复制它们,并仅让模型添加文件。否则,即使被告知要保留夹具,模型仍可能悄悄更改它们。 7. 七道关卡 只有当扩展通过所有关卡时才会被接受。每个关卡都在基于子任务自身 Dockerfile 构建的新容器中运行,且禁用网络 (--network none)。如果需要网络才能通过的关卡,则说明这是一个有缺陷的任务,而非运气不佳的运行结果。 GateCheckDefect 它捕获 G0 BuildImage 使用 --no-cache 构建;Dockerfile 未 COPY tests/ 或 solution.sh。缺少工具、未固定版本的安装、测试文件泄露到镜像中 G1 Novelty(新颖性)父级解决方案未能通过子级测试。“扩展”内容毫无用处 G2 Solvable(可解)子级解决方案在 3 次全新运行中的 3 次均通过子级测试。不可解的任务、不稳定的测试 G3 Regression(回归)继承的父级测试(剔除已被取代的测试)在子级镜像中对子级解决方案均能通过。早期需求的无声丢失 G4 Non-vacuous(非空洞)空操作解决方案(true)未能通过子级测试。在未修改的环境中即可通过的测试 G5 Alignment(对齐)每个需求 ID 均出现在指令和至少一个测试标记中;每个测试都有标记;不存在未知 ID。指令与测试之间的漂移 G6 Leakage(泄露)指令不包含测试中预期的字面值;镜像文件系统中不包含测试文件。可通过复制直接回答的任务 G1 和 G4 是人们最常忽略的关卡,它们能捕捉到合成任务中最常见的缺陷:那些无法真正区分正确解决方案与错误解决方案的测试。 validate.py ``python

import ast, hashlib, json, pathlib, re, shutil, subprocess, sys, tempfile

DOCKER_RUN = ["docker", "run", "--rm", "--network", "none", "--memory", "2g", "--cpus", "2", "--pids-limit", "512"]

def image_tag(task: pathlib.Path) -> str: h = hashlib.sha256() for p in sorted([task / "Dockerfile", *(task / "env").rglob("*")]): if p.is_file(): h.update(str(p.relative_to(task)).encode()); h.update(p.read_bytes()) return f"taskfactory/{task.name}:{h.hexdigest()[:12]}"

def build(task): df = (task / "Dockerfile").read_text() if re.search(r"^\s*(COPY|ADD)\s+.*(tests|solution\.sh)", df, re.M): raise GateError("G0", "Dockerfile copies tests or solution") tag = image_tag(task) r = subprocess.run(["docker", "build", "--no-cache", "-q", "-t", tag, str(task)], capture_output=True, text=True, timeout=1800) if r.returncode: raise GateError("G0", r.stderr[-2000:]) return tag

def run(tag, solution: pathlib.Path, tests_dir: pathlib.Path, deselect=()): """Run solution, then tests, in one fresh container. Returns (passed, junit_text).""" with tempfile.TemporaryDirectory() as tmp: out = pathlib.Path(tmp) args = " ".join(f"--deselect /tests/test_outputs.py::{t}" for t in deselect) script = (f"bash /solution.sh >/out/solution.log 2>&1; " f"pytest -q -p no:cacheprovider {args} --junitxml=/out/junit.xml /tests " f">/out/pytest.log 2>&1") r = subprocess.run(DOCKER_RUN + [ "-v", f"{solution.resolve()}:/solution.sh:ro", "-v", f"{tests_dir.resolve()}:/tests:ro", "-v", f"{out}:/out", tag, "bash", "-c", script], capture_output=True, text=True, timeout=1200) junit = (out / "junit.xml").read_text() if (out / "junit.xml").exists() else "" return r.returncode == 0, junit

class GateError(Exception): def __init__(self, gate, detail): super().__init__(f"{gate}: {detail}"); self.gate = gate

def markers(tests_py: str): """Map test function name -> set of requirement ids from @pytest.mark.req(...).""" tree, found = ast.parse(tests_py), {} for node in ast.walk(tree): if isinstance(node, ast.FunctionDef) and node.name.startswith("test_"): ids = set() for d in node.decorator_list: if (isinstance(d, ast.Call) and getattr(d.func, "attr", "") == "req"): ids |= {a.value for a in d.args if isinstance(a, ast.Constant)} found[node.name] = ids return found

def validate(child: pathlib.Path, parent: pathlib.Path | None) -> dict: meta = json.loads((child / "task.json").read_text()) tests_py = (child / "tests" / "test_outputs.py").read_text() instr = (child / "instruction.md").read_text() report = {"task": meta["id"], "gates": {}}

# G5 alignment (cheap, run first) req = set(meta["requirements"]) tmap = markers(tests_py) in_instr = set(re.findall(r"\[(R\d+)\]", instr)) tagged = set().union(*tmap.values()) if tmap else set() problems = [] if req - in_instr: problems.append(f"not in instruction: {sorted(req - in_instr)}") if req - tagged: problems.append(f"no test: {sorted(req - tagged)}") if tagged - req: problems.append(f"unknown ids in tests: {sorted(tagged - req)}") if [t for t, ids in tmap.items() if not ids]: problems.append("untagged tests") if problems: raise GateError("G5", "; ".join(problems)) report["gates"]["G5"] = "pass"

# G6 leakage, part 1: long literals from tests must not appear in the instruction lits = {n.value for n in ast.walk(ast.parse(tests_py)) if isinstance(n, ast.Constant) and isinstance(n.value, str) and len(n.value) >= 12} leaked = [l for l in lits if l in instr and not l.startswith("/")] if leaked: raise GateError("G6", f"instruction contains test literals: {leaked[:3]}")

tag = build(child); report["gates"] `` ["G0"] = "pass"; report["image"] = tag # G6 泄漏,第二部分:镜像内没有名为测试的文件 r = subprocess.run(DOCKER_RUN + [tag, "bash", "-c", "find / -xdev -name 'test_outputs.py' -o -xdev -name 'solution.sh' 2>/dev/null"], capture_output=True, text=True, timeout=300) if r.stdout.strip(): raise GateError("G6", f"found in image: {r.stdout.strip()[:200]}") report["gates"]["G6"] = "pass" tests = child / "tests" # G2 可解,3次全新运行 for i in range(3): ok, junit = run(tag, child / "solution.sh", tests) if not ok: raise GateError("G2", f"reference failed on run {i+1}") report["gates"]["G2"] = "pass" # G4 非平凡 with tempfile.NamedTemporaryFile("w", suffix=".sh", delete=False) as f: f.write("#!/usr/bin/env bash\ntrue\n"); noop = pathlib.Path(f.name) ok, _ = run(tag, noop, tests) if ok: raise GateError("G4", "no-op solution passes") report["gates"]["G4"] = "pass" if parent is not None: # G1 新颖性:父级解决方案必须无法通过子级测试 ok, _ = run(tag, parent / "solution.sh", tests) if ok: raise GateError("G1", "parent solution already passes child tests") report["gates"]["G1"] = "pass" # G3 回归:使用子级解决方案时,父级测试(减去被取代的)应通过 ok, _ = run(tag, child / "solution.sh", parent / "tests", deselect=meta.get("superseded_tests", [])) if not ok: raise GateError("G3", "child solution breaks inherited tests") report["gates"]["G3"] = "pass" return report if __name__ == "__main__": child = pathlib.Path(sys.argv[1]) parent = pathlib.Path(sys.argv[2]) if len(sys.argv) > 2 else None try: print(json.dumps(validate(child, parent), indent=2)) except GateError as e: print(json.dumps({"task": child.name, "rejected": e.gate, "detail": str(e)})) sys.exit(1) 关于此代码需要了解的事项: - G3 在子镜像中运行父级的测试。当子级更改夹具(fixtures)时,某些具有冻结值的父级测试会变为真正错误。这就是 superseded_tests 的用途,但请自行阅读每个被取代的条目,因为模型也可能利用它来丢弃那些仅仅不方便的测试。第 10 节建议设置一个上限。 - 对字面量的泄漏检查是一种启发式方法。它会遗漏数字和短字符串,并且可能会标记合法的文件路径,这就是排除路径的原因。将其视为触发器,而非确凿证据。 - “3-of-3”规则仅能发现明显的随机性(flakiness)。对于涉及计时、并发或随机性的任务,请提高计数阈值。 8. 递归增长循环 每个被接受的子级将成为下一次尝试的父级。被拒绝的子级会被记录并重试,每层最多重试固定次数。如果某层的尝试次数用尽,谱系便在此停止。你不会跳过未验证的任务直接前进。 grow.py ``python

import json, pathlib, shutil, subprocess, sys, time from extend import extend from validate import validate, GateError

def grow(seed: pathlib.Path, max_depth=3, attempts=4, log="runs/grow.jsonl"): pathlib.Path(log).parent.mkdir(exist_ok=True) parent = seed for depth in range(1, max_depth + 1): accepted = None for a in range(attempts): child = seed.parent / f"{seed.name.rsplit('-d', 1)[0]}-d{depth}-a{a}" shutil.rmtree(child, ignore_errors=True) rec = {"ts": time.time(), "parent": parent.name, "child": child.name, "depth": depth, "attempt": a} try: extend(parent, child) rec |= validate(child, parent); rec["status"] = "accepted" accepted = child except GateError as e: rec |= {"status": "rejected", "gate": e.gate, "detail": str(e)[:500]} except Exception as e: # malformed JSON, timeouts, path errors rec |= {"status": "error", "detail": repr(e)[:500]} with open(log, "a") as fh: fh.write(json.dumps(rec) + "\n") if accepted: break if not accepted: print(f"lineage stopped at depth {depth}"); return parent = accepted

if __name__ == "__main__": grow(pathlib.Path(sys.argv[1])) `` 针对多个种子运行,而不是只针对一个。单一的谱系只能告诉你关于模型决策链的信息。要对一般的“递归扩展”做出任何论断,你需要多个独立的谱系,每个谱系都有自己的种子。 ``shell

export EXTENDER_CMD="your-model-cli --max-tokens 16000" python validate.py tasks/logreport-d0 # 种子必须通过其自身的关卡 for s in tasks/*-d0; do python grow.py "$s"; done jq -r 'select(.status!="accepted") | [.depth,.gate // "error"] | @tsv' runs/grow.jsonl \ | sort | uniq -c `` 我们的种子的谱系可能看起来像什么(示例) 以下是模型可能提出的那种链条。我们亲自编写了它作为示例。它不是来自运行的输出: - d1:旋转日志 access.log.1.gz,access.log.2.gz 也必须被计数 [R3]。夹具增加了 gzip 文件,而父级解决方案计数不足,因此按预期触发了 G1。 - d2:来自 /app/config/healthcheck_ips.txt 列表中 IP 的请求(每行一个,允许 # 注释)将被排除 [R4]。父级测试 test_counts_match_fixture 被取代,因为其冻结的计数发生了变化。 - d3:还写入 /app/report_hourly.csv,包含表头 hour_utc,status,count,时间戳从混合偏移量归一化为 UTC,行已排序 [R5]。正确的解决方案需要同时处理时区、CSV 格式以及之前的过滤器。 注意 d2 处发生的情况。被取代的测试是合法的,但替换后的测试必须使用新的冻结值再次覆盖 R1 和 R2。否则 G5 仍然会通过(R1 可能被其他测试覆盖),并且部分基础行为不再受到检查。请将取代列表与新测试集一起审查。 9. 测量成功率下降 代理合约 $AGENT_CMD 接收镜像标签和指令路径。它必须从该镜像启动一个容器(网络策略由你决定,但请记录下来),让代理执行工作,然后将最终的容器文件系统保存为一个新的镜像,供测试运行。一种简单且诚实的做法是使用 docker commit: ``shell

#!/usr/bin/env bash # agent_episode.sh IMAGE INSTRUCTION OUT_TAG set -euo pipefail cid=$(docker run -d --network none "$1" sleep infinity) your-agent --container "$cid" --instruction "$2" --max-steps 60 --timeout 1800 || true docker commit "$cid" "$3" >/dev/null docker rm -f "$cid" >/dev/null `` 然后针对 OUT_TAG 使用空操作解决方案运行测试。代理的工作即为解决方案。保持各深度下的步骤预算、超时时间和代理模型完全一致。如果在不同深度之间更改其中任何一项,你测量的将是该变更的影响,而非任务本身的难度。 measure.py ``python

import json, math, pathlib, subprocess, sys, collections from validate import run, image_tag

NOOP = pathlib.Path("noop.sh"); NOOP.write_text("#!/usr/bin/env bash\ntrue\n")

def wilson(k, n, z=1.96): if n == 0: return (float("nan"),) * 3 p = k / n; d = 1 + z*z/n c = (p + z*z/(2*n)) / d h = z * math.sqrt(p*(1-p)/n + z*z/(4*n*n)) / d return p, max(0, c - h), min(1, c + h)

def episode(task, i): tag = image_tag(task); out = f"{tag}-ep{i}".replace(":", "-") subprocess.run(["bash", "agent_episode.sh", tag, str(task / "instruction.md"), out], check=True, timeout=3600) ok, _ = run(out, NOOP, task / "tests") subprocess.run(["docker", "rmi", "-f", out], capture_output=True) return ok

def main(tasks_glob="tasks/*", n=20, log="runs/measure.jsonl"): res = collections.defaultdict(list) for task in sorted(pathlib.Path().glob(tasks_glob)): meta = json.loads((task / "task.json").read_text()) if meta.get("status") == "rejected": continue for i in range(n): ok = episode(task, i) res[(meta["id"], meta["depth"])].append(ok) with open(log, "a") as fh: fh.write(json.dumps({"task": meta["id"], "depth": meta["depth"], "episode": i, "pass": ok}) + "\n") by_depth = collections.defaultdict(lambda: [0, 0]) for (_, d), oks in res.items(): by_depth[d][0] += sum(oks); by_depth[d][1] += len(oks) for d in sorted(by_depth): k, m = by_depth[d]; p, lo, hi = wilson(k, m) print(f"depth {d}: {k}/{m} = {p:.2f} 95% CI [{lo:.2f}, {hi:.2f}]")

if __name__ == "__main__": main(*sys.argv[1:2]) `` measure.py 仅扫描被接受的目录作为任务。在实际操作中,当 grow.py 完成后,应将被拒绝的尝试移出 tasks/ 目录(或对其进行标记),以确保它们不会进入测量范围。 如何解读数据 - 按深度的合并通过率及威尔逊区间,如上所示打印。单个任务内的实验是相关的,因此如果每个深度的任务数量较少,区间估计会显得过于乐观。在谱系数量较少的情况下,更倾向于参考下方的按谱系视图。 - 按谱系的配对下降幅度:对于每个谱系,计算 rate(d0) − rate(dk)。由于配对共享相同的种子,这可以将“因扩展而变难”与“该种子本身就很困难”区分开来。报告中位数及完整列表。如果谱系数量少于大约十个,不要报告单一平均值作为结论。 - 门控产出率:每个深度的接受数/尝试数,以及每个门控的拒绝计数。这是工厂的成本侧。如果产出率随深度急剧下降,说明扩展器已达到其一致构建能力的极限,这也揭示了关于更深层次任务的信息。 - 通过率是在固定设置下 N 次实验的成功率。这实际上是从 N 个样本中估计的 pass@1,而非 pass@N。 结果模板(根据运行结果填写) 深度 接受的任务数 尝试次数 各门控拒绝数 实验次数 通过率 [95% CI] ``plaintext

0————— 1————— 2————— 3————— `` 同时记录:扩展模型及版本、智能体模型及版本、基础镜像摘要、步骤预算、超时设置、网络策略、N 值,以及你的工厂代码的提交哈希。缺少这些信息,任何人都无法复现你的表格,包括三个月后的你自己。 10. 验证工厂本身 闸门守护着任务。那么,谁来守护这些闸门呢?在信任任何测量结果之前,请执行以下检查: - 植入缺陷。选取一个被接受的子任务,并以已知的方式破坏它:清空测试用例以断言 True(G4 必须触发),将 Dockerfile 中的 COPY tests /tests 修改为其他内容(G0 必须触发),从指令中移除一个 [R3] 标签(G5 必须触发),原样复制父任务的测试用例(G1 必须触发),在断言中插入 random.random() < 0.5(G2 应大部分时间触发)。将这些情况编写为一个小型测试套件用于 validate.py,并在每次更改时运行它。 - 人工审计样本。对于每个深度,从头到尾阅读至少几个被接受的任务:指令是否足以在不猜测的情况下通过测试?记录答案为“否”的比例。闸门检查的是 consistency(一致性/合规性),而非公平性。一个任务可能通过所有七个闸门,但仍依赖于未声明的输出格式。 - 覆盖测试上限。如果子任务覆盖了超过一定比例(例如一半)的继承测试,则自动拒绝该子任务。由你自行设定阈值并记录下来。如果一个子任务丢弃了大部分继承内容,它是一个新任务而非扩展任务,从而跳过了整个方法所依赖的低成本差异验证。 - 从头重新运行。删除所有已构建的镜像,并对每个被接受的任务重新运行 validate.py。任何现在失败的任务都是由于缓存或时序问题而被错误接受的,必须将其移除。 11. 你应该预期的故障案例 - 测试复制了解决方案的逻辑。模型编写的测试使用与解决方案相同的解析器来计算预期值,因此无论解析器正确与否,两者都会达成一致。G4 和 G1 可能无法察觉此问题。缓解措施:提示词要求使用固定值,并由审查者检查测试中是否导入了解决方案的内容或对 fixtures 进行了重新解析。 - 未声明的格式。测试要求存在尾随换行符、特定的键顺序或浮点数精度,而指令中从未提及这些要求。闸门会放行,但智能体会因与技能无关的原因失败。这会导致测得的性能下降幅度虚高。只有人工审计能可靠地捕捉到此类问题。 - 晦涩难懂导致的难度增加。扩展通过隐藏信息(如未记录的配置路径)使任务变得“更难”,而不是要求更多的工作量。成功率会下降,但你测量的却是猜测能力。优先选择增加能力的扩展,而非增加歧义的扩展。 - Fixtures 重写。模型静默地更改了父任务的 fixtures,导致 G3 失败或覆盖列表增长。仅允许从父任务复制 fixtures 并添加新内容,只能消除大部分此类问题。 - 网络依赖。解决方案在运行时执行 pip install。在 --network none 模式下,它会失败并通过 G2 检查,这是正确的行为。应将安装步骤移至 Dockerfile 中,并锁定版本。 - 资源限制。某个 d3 任务仅在 8 GB 内存下才能通过,但在 2 GB 限制下会失败。需提前确定资源限制,并在提示词中告知扩展模型。 - 在测量数据上训练。如果用于训练的任务与用于测量成功率下降的任务相同,那么成功率的下降并非源于能力提升,而是其他原因。保留一组从未进入训练过程的独立谱系数据。这是一种数据泄露形式。 12. 局限性 - 门控机制验证了内部一致性:参考任务通过,父任务和空操作失败,需求已映射。但这并不能证明该任务是优质的、现实的,或对仅能看到指令的智能体公平。 - 以成功率下降程度衡量的难度取决于智能体。对某一智能体而言困难的任务,对另一智能体可能轻而易举。报告中需附带曲线及对应的智能体信息。 - 样本量较小。仅有少量谱系且每个任务20次试验时,置信区间较宽,相邻深度间的差异可能无法区分。在撰写报告时应如实说明,而非基于点估计对深度进行排名。 - 对于来自自有扩展器的任务,使用 --network none` 和资源限制进行 Docker 隔离是合理的。但这并非针对不可信代码的强安全边界。如果扩展器输出或智能体行为可能存在恶意,请使用更强的沙箱环境(如独立虚拟机、gVisor 等),并切勿将主机凭据或 Docker 套接字挂载到任务容器中。 - 该方法继承了种子任务的领域特性。对日志处理任务进行递归扩展,得到的是更难的日志处理任务。若要覆盖更广的范围,需要更多样化的种子任务,而非更深的谱系。 13. 简短检查清单 - 种子任务包含需求 ID、冻结的预期值、固定的基础镜像,并通过 validate.py 验证。 - 扩展器提示词要求严格的 JSON 输出;夹具从父任务复制,仅添加新增部分。 - 在无网络的独立容器中进行七道门控测试;为验证器植入缺陷套件。 - 多个独立谱系,每层固定尝试次数,每次拒绝均记录其触发的门控。 - 跨深度的智能体设置固定,每任务 N 次试验,使用威尔逊区间,按谱系配对计算下降幅度及门控通过率。 - 每层抽取人工审计样本,设置被替代测试上限,保留未参与训练的谱系用于评估。 - 在每个结果表旁记录模型、版本、摘要、预算及代码提交信息。 更多实操资料:所有指南和术语表。 我们发布对我们有效的方案——并为您的业务实施相同的解决方案。我们设计面向真实流程的 AI 自动化、Telegram 机器人、聊天应用和 AI 智能体。讨论您的项目 → © 2026 Agent Lab Journal · Guides · Glossary · RSS 原文:https://agentlabjournal.online/en/recursive-terminal-task-synthesis.html?utm_source=devto&utm_medium=referral&utm_campaign=agentlabjournal-en-global-all&utm_content=article&utm_term=recursive-terminal-task-synthesis

译文已达到本站中文翻译的字数上限,剩余内容请查看原文。

阅读原文