开始前,会这些就够了
- 能运行 Python 文件,读懂字典、列表与 assert
- 理解进程输出文字和进程退出码是不同信息
- 知道 JSON 文件需要解析后才能检查里面的字段
助手让一个工具导出两条已选便条,屏幕出现“Report ready.”。这句话可能来自正常程序,也可能在文件写入前就已经打印。即使退出码是零,工具仍可能根本没有生成文件,或者导出了另一个任务的结果。把日志原样转述给用户,不能完成产物验收。
本篇运行七次真实的本地子进程。它们都打印同一句成功提示,父流程却只接纳其中一次。学习产物是一份 JSON 验收报告和一张 CSV 对照表;所有便条都是脚本内的虚构数据,只运行随文附带的固定工作代码,不调用语言模型或任意生成代码。
把例子带到自己的电脑上
下载完整练习包
需要 Python 3.10 或更新版本,无第三方依赖;制作时实测 Python 3.14.0。使用标准库,不需要安装第三方包。下载 ZIP 后选择“全部提取”,进入能看到 README.md 的文件夹。
下载 ZIP · 5 个文件包里有什么
- artifact_acceptance.py:完整示例
- README.md:环境、范围与独立练习
- observed.stdout.txt:制作时的实际输出
- verification.json:源码与产物验证记录
- SHA256SUMS.txt:文件内容摘要
在这个文件夹打开 PowerShell,逐行运行
python --version
python -B artifact_acceptance.py解压到独立练习文件夹,在含源文件的目录打开终端运行。程序会在当前目录写入教学报告;先预测结果,再查看当次输出。真实启动七个受控子进程,按任务身份及文件内容验收;不执行任意外部程序,也不构成代码沙箱。
找不到命令或文件时,先按 README 检查环境和所在目录。文中标出的阅读时间只估算文章篇幅,安装环境和动手练习可按自己的节奏进行。
三张检查单,分别回答三个问题
第一张检查进程有没有按约定退出;第二张检查本次约定位置是否有文件、能否解析;第三张检查文件是否属于这项任务,内容是否与约定一致。任何一张不通过,都不能使用最终的 deliverable 状态。stdout 只留下诊断文字,不参与成功判定。
Python subprocess 文档分别提供 returncode、stdout 与 stderr。它说明零通常代表进程运行成功,却没有替你的应用定义“导出任务已经满足”。本例主动组合退出码与内容检查,规则写在父流程,避免让工作程序一句自报成功决定全部结果。
先写任务要求,再看产物说了什么
任务明确要求导出 project-alpha-selected-notes 范围里的 N1、N2 两条便条,编号和原文都在 request.json 中。产物要包含 task_id、scope 和 records。验收器从原请求建立预期映射,再逐条核对结果的编号与文字;它没有采用工作程序自己声称的数量作为验收依据。
这是一份准确复制已选记录的任务,因此可以做逐值检查。若任务改为摘要、翻译或图片生成,不能照搬“文字必须完全相等”;需要先定义对应的内容质量标准,并说明仍无法自动判定的部分。本篇刻意选择边界小、结果可明确核对的任务,把执行证据链讲清楚。
每次从空目录开始,给本次任务单独编号
每次 run_case 都创建新的临时目录与任务编号,写入本次请求后才启动子进程。运行前还断言 result.json 不存在。这样 zero_no_file 不会误捡上次成功运行留下的同名文件。文件名固定只方便约定位置,不能承担任务身份。
TemporaryDirectory 文档说明上下文结束后会清理临时目录。本例在清理前读取结果,把解析后的快照放进最终报告;七个原始临时结果不会继续留在磁盘上。最终 JSON 与 CSV 是持久交付的验收记录,不能把已清理的临时路径发给用户当下载地址。
即使目录隔离了,wrong_task 仍故意写入旧任务编号。验收器会把它拒绝,展示“文件在这次目录里”与“文件声明属于这次任务”是两项不同检查。编号本身不是身份认证,本题只是防止受控流程中的串单和旧结果混入,没有声称抵御恶意程序伪造。
有文件但退出失败,与没文件却退出成功
nonzero_with_file 先写出内容合格的文件,再以七退出。内容检查返回 accepted,最终仍是 rejected,因为本题交付政策要求进程成功且内容合格。zero_no_file 恰好相反:退出码零,日志也说准备好了,但结果文件不存在,因此同样被拒绝。
内容合格不等于整个生产步骤完成。非零退出可能意味着后续整理步骤失败,也可能只是某个工具采用另一套退出码约定;真实接入应先核对它的合同。本题不尝试抢救失败进程留下的文件,也不把这种保守政策描述为所有命令行程序都必须遵守的普遍规则。
能打开文件之后,还要核对它装了什么
bad_json 只写出半截对象,解析失败;wrong_scope 写入另一项目范围;wrong_rows 只保留一条便条。三种错误分别得到 invalid_json、scope_mismatch 和 records_mismatch,不能都归成一个含糊的“工具失败”。报告仍保留退出码和输出文字,方便定位到底哪层没有满足要求。
Python json 文档说明解析错误与 object_pairs_hook,也提醒默认解析器会接受重复成员并保留最后一个。本例用钩子拒绝重复 JSON 成员,再检查记录编号是否重复、字段是否为文字。结构解析、任务归属和内容一致性依次检查,各自承担不同责任。
当前任务不要求便条列表保持输入顺序,所以验收先转成编号到文字的映射,但转之前必须拒绝重复编号。否则两条同编号记录会在字典里互相覆盖,错误可能被隐藏。若后续产品要求顺序一致,就需要增加顺序条件,不能凭本题通过便声称所有展示要求都已满足。
运行完整脚本,七次子进程只接纳一次
保存为 artifact_acceptance.py,在单独练习目录执行。主进程使用当前解释器启动同一文件的 --worker 分支,只接受预设的七种情形。参数以列表传入,不经过 shell;每次等待上限为十秒。没有下载程序,也没有执行模型产出的代码。
脚本生成 artifact-acceptance-report.json 和 artifact-acceptance.csv。七次工作进程都会打印 Report ready.,只有 valid 得到 deliverable。断言还覆盖重复 JSON 成员、重复便条编号和错误字段类型;随机任务编号每次不同,验收状态和终端显示顺序保持可对照。
展开收起代码 · 小实验
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
"""Run only this bundled worker; demonstrate output acceptance, NOT a sandbox."""
import csv
import json
from pathlib import Path
import subprocess
import sys
import tempfile
from uuid import uuid4
SCRIPT = Path(__file__).resolve()
MODES = ("nonzero_with_file", "zero_no_file", "bad_json", "wrong_task", "wrong_scope", "wrong_rows", "valid")
def worker(mode, task_id):
# The fixed worker reads only the synthetic request in its working directory.
task = json.loads(Path("request.json").read_text(encoding="utf-8"))
assert task["task_id"] == task_id
artifact = {"task_id": task_id, "scope": task["scope"], "records": task["records"]}
if mode == "wrong_task":
artifact["task_id"] = "a-prior-task"
if mode == "wrong_scope":
artifact["scope"] = "another-project"
if mode == "wrong_rows":
artifact["records"] = artifact["records"][:1]
if mode == "bad_json":
Path("result.json").write_text('{"records":', encoding="utf-8")
elif mode != "zero_no_file":
Path("result.json").write_text(json.dumps(artifact), encoding="utf-8")
print("Report ready.") # Every mode says the same thing.
return 7 if mode == "nonzero_with_file" else 0
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError("duplicate_json_member")
result[key] = value
return result
def validate(artifact, task):
if not isinstance(artifact, dict) or set(artifact) != {"task_id", "scope", "records"}:
return "wrong_shape"
if artifact["task_id"] != task["task_id"]:
return "task_mismatch"
if artifact["scope"] != task["scope"]:
return "scope_mismatch"
rows = artifact["records"]
if not isinstance(rows, list) or any(not isinstance(r, dict) or
set(r) != {"id", "text"} or not all(isinstance(v, str) for v in r.values()) for r in rows):
return "invalid_records"
ids = [r["id"] for r in rows]
if len(ids) != len(set(ids)):
return "duplicate_record_id"
actual = {r["id"]: r["text"] for r in rows}
expected = {r["id"]: r["text"] for r in task["records"]}
if actual != expected:
return "records_mismatch"
return "accepted"
def inspect_artifact(path, task):
if not path.is_file():
return "missing_file", None
try:
artifact = json.loads(path.read_text(encoding="utf-8"), object_pairs_hook=unique_object)
except (ValueError, UnicodeError):
return "invalid_json", None
return validate(artifact, task), artifact
def run_case(mode):
task = {"task_id": uuid4().hex, "scope": "project-alpha-selected-notes",
"records": [{"id": "N1", "text": "Read the input."}, {"id": "N2", "text": "Check the output."}]}
# Fresh directory and task ID for EACH invocation prevent reuse of an old result.
with tempfile.TemporaryDirectory(prefix="artifact-case-", dir=Path.cwd()) as temp:
folder = Path(temp)
path = folder / "result.json"
assert not path.exists()
(folder / "request.json").write_text(json.dumps(task), encoding="utf-8")
result = subprocess.run([sys.executable, str(SCRIPT), "--worker", mode, task["task_id"]],
cwd=folder, capture_output=True, text=True, encoding="utf-8",
timeout=10, check=False)
artifact_status, artifact = inspect_artifact(path, task)
decision = "deliverable" if result.returncode == 0 and artifact_status == "accepted" else "rejected"
return {"mode": mode, "task_id": task["task_id"], "returncode": result.returncode,
"stdout": result.stdout.strip(), "stderr": result.stderr.strip(),
"artifact_exists": path.is_file(), "artifact_status": artifact_status,
"decision": decision, "artifact_snapshot": artifact}
def main():
cases = [run_case(mode) for mode in MODES]
assert len({r["task_id"] for r in cases}) == len(cases)
assert all(r["stdout"] == "Report ready." for r in cases)
assert [r["decision"] for r in cases] == ["rejected"] * 6 + ["deliverable"]
assert [r["artifact_status"] for r in cases] == [
"accepted", "missing_file", "invalid_json", "task_mismatch", "scope_mismatch", "records_mismatch", "accepted"]
assert cases[0]["returncode"] == 7 and cases[0]["artifact_exists"]
assert cases[1]["returncode"] == 0 and not cases[1]["artifact_exists"]
assert all(r["stderr"] == "" for r in cases)
try:
json.loads('{"x":1,"x":2}', object_pairs_hook=unique_object)
except ValueError:
pass
else:
raise AssertionError("duplicate member was accepted")
probe = {"task_id": "probe", "scope": "s", "records": [{"id": "1", "text": "x"}]}
assert validate({**probe, "records": probe["records"] * 2}, probe) == "duplicate_record_id"
assert validate({**probe, "records": [{"id": "1", "text": False}]}, probe) == "invalid_records"
report = {"scope": "fixed bundled subprocess; exact export of two selected synthetic notes",
"policy": "zero exit AND accepted artifact; stdout is diagnostic only",
"cases": cases, "assertions": "passed"}
Path("artifact-acceptance-report.json").write_text(
json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
columns = ["mode", "task_id", "returncode", "artifact_exists", "artifact_status", "decision"]
with Path("artifact-acceptance.csv").open("w", encoding="utf-8", newline="") as handle:
writer = csv.DictWriter(handle, fieldnames=columns, extrasaction="ignore")
writer.writeheader()
writer.writerows(cases)
for row in cases:
print(f"{row['mode']}: exit={row['returncode']}, artifact={row['artifact_status']}, {row['decision']}")
print("PASS: 7 real child runs; one deliverable; independent directories and task IDs")
if __name__ == "__main__":
if len(sys.argv) == 4 and sys.argv[1] == "--worker" and sys.argv[2] in MODES:
raise SystemExit(worker(sys.argv[2], sys.argv[3]))
main()
怎样运行:Python 3.10 或更新版本,在独立练习目录执行:python artifact_acceptance.py。仅标准库。 2026-10-15 已用 Python 3.14.0 执行这份代码并通过自带检查;下方是当次输出。真实启动七个受控子进程,按任务身份及文件内容验收;不执行任意外部程序,也不构成代码沙箱。
运行后对照结果
nonzero_with_file: exit=7, artifact=accepted, rejected
zero_no_file: exit=0, artifact=missing_file, rejected
bad_json: exit=0, artifact=invalid_json, rejected
wrong_task: exit=0, artifact=task_mismatch, rejected
wrong_scope: exit=0, artifact=scope_mismatch, rejected
wrong_rows: exit=0, artifact=records_mismatch, rejected
valid: exit=0, artifact=accepted, deliverable
PASS: 7 real child runs; one deliverable; independent directories and task IDs交付时写出通过了什么,失败停在哪里
CSV 适合并排看退出码、文件存在性、内容状态与最后判定;JSON 则保留任务编号、stdout、stderr 和解析后的产物快照。读者可以先找到非零但有文件那一行,再和零退出无文件那一行比较,确认报告没有把任何单一信号当作完成凭据。
程序没有生成自然语言总结,而是把可检查事实交给后续展示层。助手可以据此说“本次导出的两条指定便条已核对”,不能扩写为“这个项目所有便条都完整导出了”。这里的范围来自明确输入的两条记录,既没有搜索真实账号,也没有验证上游资料是否已经列全。
不要把 rejected 理解成“这次什么都没有发生”。第一组已经产生文件,只是按本题政策暂不交付;其他真实工具还可能留下更重要的副作用。验收结论用于决定这份产物能否按当前任务交付,不能替代对外部变化的调查或撤销。
真实执行了子进程,仍要守住验证边界
2026-10-15 已实际执行完整脚本,七个固定工作进程均结束,得到六次拒绝、一次可交付,全部内置断言通过。文章代码由执行文件直接嵌入;报告里的快照来自本次临时产物读取,没有用手填的预期结果代替工作进程输出。
临时目录、无 shell 和等待上限都不是运行任意代码的安全沙箱。工作程序仍以当前用户权限运行,本题只能运行随附的固定代码。没有验证恶意程序隔离、子孙进程清理、磁盘写满或操作系统崩溃;超时与启动错误会让实验抛错停止,也没有伪装成已完成的验收报告。
记住这三点
- 日志、退出码、文件存在与内容验收是不同证据,需要按任务组合。
- 每次任务使用独立目录与编号,产物还要与原请求核对,避免旧文件或串单伪通过。
- 交付结论必须保留实际验收范围,准确复制两条输入不能证明真实项目已全部导出。
别把例子用过头
- 把工作程序打印的成功文字直接当成最终完成状态。
- 复用输出目录而不检查任务身份,误把上次文件当作本次产物。
- 解析成功后只看声明的数量,不核对记录内容;把子进程运行方式称为安全沙箱。
自己试一小步
如果导出任务要求保留便条顺序并附带一个新增字段,你会怎样扩展验收器?
- 另建三条虚构记录,明确字段、顺序和允许的输出形式;先写验收清单,再改工作程序。
- 构造至少五个失败产物,包含顺序错误、重复记录、错任务、非零退出留文件与格式损坏。
- 设计一份用户能读懂的交付摘要,把进程结果、产物检查与尚未证明的范围分别交代。
- 交付独立临时目录中的实际运行记录、JSON 与 CSV;继续只运行自带固定代码。
做到这些,就算完成
- 预期是否来自原任务,而不是产物自己声明的成功或数量?
- 旧文件、错任务和重复编号是否均无法得到可交付状态?
- 是否明确限制内容质量、上游完整性与代码隔离方面的结论?
查原始资料
想确认一个细节,先看这份资料对应的部分,不必从头读完所有文档。
- Python 3.10:subprocess 运行与返回结果 (新标签页)↗
核读 run、
CompletedProcess、参数列表与sys.executable说明;支持取得退出码、标准输出、错误输出及设置工作目录与等待上限。 - Python 3.10:TemporaryDirectory (新标签页)↗
核读
TemporaryDirectory创建、上下文清理及 dir 参数;支持为每次固定工作进程创建独立目录。 - Python 3.10:JSON 解析与重复成员 (新标签页)↗
核读 loads、
JSONDecodeError、object_pairs_hook与 Repeated Names Within anObject;支持解析错误识别和自定义重复成员拒绝。