把 Agent 每次运行当一条 Trajectory 来测:Qwen Code 进代码库的可评估性观察
阿里通义把一款终端编程 Agent「Qwen Code」开源了,仓库就挂在 GitHub 上(QwenLM/qwen-code),自述一句话:An open-source AI coding agent。这类工具的能力边界是能在你本地终端里读代码、改代码、跑命令——也就是说,它迟早会被某个团队接进真实代码库,被允许提 PR、进 CI。
问题不在于它跑分多高。跑分是排行榜的事,跟你的代码库质量没有直接关系。真正的问题是:当这样一个能自己改代码、自己执行命令的 Agent 进了你的仓库,测试该盯住什么?过去我们判断一段代码「能不能跑」,现在得判断它的行为「可不可评估」。这篇从测试工程师立场,提炼三件事。
一、一手事实:通义开源了终端编程 Agent『Qwen Code』
先把事实钉死,避免后面讨论悬空。据 GitHub 仓库(核实于 2026-09-17),阿里通义已开源终端编程 Agent「Qwen Code」,仓库地址 QwenLM/qwen-code,仓库自述为 An open-source AI coding agent。本篇只引用这两条定性事实——它已开源、它是一个 AI 编程 Agent;不引用任何跑分、参数或指标。
社区口径称其在 SWE-bench 上有较强表现(此为社区说法、非本篇核实,本篇不展开、也不拿分数当论据)。我们要谈的是另一件事:一个能读写文件、执行命令、自主提交改动的 Agent,一旦被接进团队代码库,测试的责任边界就从「验一段人写的代码」扩展到「验一个会自己产生改动的执行体」。

二、第一件事:它改的代码谁兜底——给 AI 生成改动挂回归门禁
人类提 PR 和 Agent 提 PR,风险面根本不是一回事。人写代码有意图、有上下文、改动通常克制;Agent 生成改动可能一次动几十个文件、可能改到你没让它改的地方、可能引入它自己都没意识到的副作用。评审的重点必须跟着变。
| 维度 | 传统人类 PR | Agent 生成 PR |
|---|---|---|
| 评审重点 | 设计是否合理、命名与风格、边界处理 | 改动范围是否越界、是否动了不该动的文件、是否引入隐性副作用 |
| 回归策略 | 增量回归,跑受影响模块的用例 | 全量回归 + diff 体积阈值,改动面大时必须整体重跑 |
| 风险面 | 逻辑错误、遗漏边界,通常可追溯到某次思考 | 大范围机械改动、跨模块连锁、无法追溯「它为什么这么改」 |
| 门禁方式 | Code Review 通过即可合并 | Review + 回归 + 静态扫描 + diff 阈值多道闸,任一不过即拦 |
结论是:Agent PR 不能享受和人类 PR 一样的「Review 通过即合并」待遇。它至少要过三道自动门禁——回归测试全绿、静态扫描无新增高危、diff 体积不超过阈值。第一段代码就是一份给 Agent PR 挂这三道检查的 GitHub Actions。
# .github/workflows/agent-pr-gate.yml
# 给「Agent 提交的 PR」挂 CI 门禁:回归 + 静态扫描 + diff 体积阈值
# 触发条件:PR 打了 agent-generated 标签(由机器人账号提交时自动打)
name: agent-pr-gate
on:
pull_request:
types: [opened, synchronize]
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 需要完整历史来算 diff 体积
- name: Only guard agent-generated PRs
id: is_agent
run: |
if echo '${{ toJson(github.event.pull_request.labels.*.name) }}' | grep -q 'agent-generated'; then
echo "agent=true" >> "$GITHUB_OUTPUT"
else
echo "agent=false" >> "$GITHUB_OUTPUT"
fi
# 门禁一:diff 体积阈值。Agent 一次动太多文件,直接拦下要求拆分
- name: Diff size threshold
if: steps.is_agent.outputs.agent == 'true'
run: |
CHANGED=$(git diff --numstat origin/${{ github.base_ref }}...HEAD | wc -l)
echo "changed files: $CHANGED"
if [ "$CHANGED" -gt 20 ]; then
echo "::error::Agent PR touched $CHANGED files (> 20). Split it."
exit 1
fi
# 门禁二:全量回归。Agent 改动面不可控,不能只跑增量
- name: Full regression
if: steps.is_agent.outputs.agent == 'true'
run: |
pip install -r requirements.txt
pytest -q --maxfail=1
# 门禁三:静态扫描。只看新增高危,避免存量问题淹没信号
- name: Static scan (new high-severity only)
if: steps.is_agent.outputs.agent == 'true'
run: |
pip install bandit
bandit -r src -ll -q -f json -o bandit.json || true
HIGH=$(python -c "import json;d=json.load(open('bandit.json'));print(len([r for r in d['results'] if r['issue_severity']=='HIGH']))")
echo "new high-severity issues: $HIGH"
if [ "$HIGH" -gt 0 ]; then exit 1; fi
为什么这么写:三道闸的顺序是「先便宜后昂贵」。diff 体积阈值几乎零成本,却能在第一时间拦下 Agent 最典型的失控——一次改一大片。先卡体积再跑全量回归,避免为一个明显该被打回的 PR 白烧十几分钟 CI。回归用 --maxfail=1 是因为 Agent PR 我们要的是「有没有破」这个布尔结论,快速失败比收集全部失败更省时间。静态扫描只报新增高危(-ll),是因为存量代码的老问题不该由这一次 Agent PR 背锅,否则门禁天天红、最后被人一键跳过。踩过的坑:fetch-depth: 0 不能省,默认浅克隆拿不到 base 分支的完整历史,diff 体积会算错成「改了全部文件」,门禁直接把每个 PR 都拦死。
三、第二件事:它的行为可不可复现、可不可评估——把每次运行当一条 Trajectory
回归门禁解决的是「这次改动有没有破东西」,但没解决更深的问题:Agent 这次为什么这么改?下次同样输入它还会这么改吗?一个行为不可复现、不可归因的执行体,你没法对它做回归——因为「回归」的前提是「同样的输入应该给出可预期的行为」。
所以第二件事是把 Agent 的每次运行当成一条待测的 Trajectory(轨迹):把它读了哪些文件、调了哪些命令、每步的输入输出,按顺序落成结构化记录。有了这条 jsonl,你才能对它断言——比如「它不该读 .env」「它改代码前必须先跑过测试」。
| 维度 | 能跑就行 | 行为可评估 |
|---|---|---|
| 可复现 | 同一输入多次运行结果可能不同,无法重放 | 轨迹落盘,可重放、可比对两次运行的差异 |
| 可归因 | 只知道最终成功/失败,不知道中间发生了什么 | 每一步的输入输出留痕,能定位是哪一步走偏 |
| 可回归 | 无法回归——没有稳定行为可对照 | 把轨迹当断言对象,行为漂移能被自动检出 |
第二段代码是把 Agent 运行轨迹落成 jsonl 的骨架,供后续断言。
"""
trajectory_log.py —— 把 Agent 每次运行落成一条 jsonl 轨迹,供后续断言
真实场景:包装 Agent 的「读文件 / 执行命令 / 写文件」三类动作,
每发生一步就追加一行 JSON,运行结束得到一条可重放、可断言的 Trajectory。
运行自测:python trajectory_log.py
"""
import json
import time
class TrajectoryLogger:
def __init__(self, run_id, path="trajectory.jsonl"):
self.run_id = run_id
self.path = path
self._seq = 0
def log(self, action, target, detail=None):
"""记录 Agent 的一步动作。action 如 read_file / exec_cmd / write_file。"""
self._seq += 1
record = {
"run_id": self.run_id,
"seq": self._seq,
"ts": round(time.time(), 3),
"action": action,
"target": target,
"detail": detail or {},
}
with open(self.path, "a", encoding="utf-8") as f:
f.write(json.dumps(record, ensure_ascii=False) + "\n")
return record
def assert_no_secret_read(path):
"""一条示例断言:轨迹里不允许出现读 .env 的步骤。"""
with open(path, encoding="utf-8") as f:
for line in f:
r = json.loads(line)
if r["action"] == "read_file" and r["target"].endswith(".env"):
raise AssertionError(f"Agent 越权读取敏感文件: {r['target']}")
return True
if __name__ == "__main__":
import os
if os.path.exists("trajectory.jsonl"):
os.remove("trajectory.jsonl")
log = TrajectoryLogger(run_id="run-demo-1", path="trajectory.jsonl")
log.log("read_file", "src/pay.py")
log.log("exec_cmd", "pytest -q", detail={"exit_code": 0})
log.log("write_file", "src/pay.py", detail={"diff_lines": 6})
print("轨迹条数:", sum(1 for _ in open("trajectory.jsonl", encoding="utf-8")))
print("敏感文件断言:", assert_no_secret_read("trajectory.jsonl"))
os.remove("trajectory.jsonl")
为什么这么写:轨迹用 jsonl 而不是一个大 JSON 数组,是因为 Agent 运行是流式的、可能中途崩,一行一条能保证崩之前记录的步骤不丢,也方便 grep/逐行断言。每条都带 run_id 和 seq,是为了能把多次运行的同一 seq 对齐比对——这正是「行为漂移检测」的基础:同一输入跑两次,逐 seq 比对 action 与 target,不一致就说明行为不稳定。示例断言 assert_no_secret_read 演示的是「轨迹不只是日志,而是可测对象」:你把安全边界写成对轨迹的断言,Agent 一旦越界,回归里就会红。踩过的坑:轨迹里千万别只记「成功/失败」这一个终态,那等于没记——真正能归因的是中间每一步的 target 和 detail,终态是果、轨迹才是因。
四、第三件事:权限与副作用边界——最小权限与沙箱怎么测
终端编程 Agent 的危险不在它「会不会写错代码」,而在它「能做什么」。它能读写文件、能执行命令,就意味着一次幻觉、一次被污染的输入,可能让它删掉不该删的、跑一条不该跑的命令。这类副作用,回归测试是兜不住的——因为副作用发生在测试之外的真实文件系统里。
所以第三件事是把权限当测试对象:给 Agent 划定最小权限边界(能读哪些目录、能写哪些目录、能执行哪类命令),放进沙箱,然后专门测「越界会不会被拦住」。测法有三层。
第一层是目录边界:把 Agent 关进一个工作区,然后专门造一个「写工作区外路径」的动作,断言它被拒。比如让它去改 /etc/hosts 或工作区上一级的文件,正确行为是拒绝并报错,而不是真的落盘。第二层是命令白名单:终端 Agent 能执行命令,就等于能执行任何命令,除非你显式收窄。rm -rf、curl xxx | sh、git push --force 这类高危命令必须被拦在沙箱里,测法是把它们逐条喂给 Agent,断言要么被策略拒绝、要么只在沙箱内生效。第三层是副作用回滚:一次运行结束后,比对宿主文件系统的快照,断言工作区之外零改动——这一层是前两层的兜底,前两层漏掉的越界,最终都会在这层的快照比对里现形。
这三层各写一条断言,挂在 Agent 每次运行之后,就是它的权限回归网。注意它和第二件事的轨迹是配合的:轨迹告诉你 Agent「想做什么」,沙箱断言告诉你它「实际被允许做了什么」,两者对不上,就是权限边界没兜住。
这件事和第一件事、第二件事是连着的:门禁拦的是「改动质量」,轨迹评的是「行为可复现」,沙箱守的是「副作用边界」。三者缺一,你就不该让一个开源编程 Agent 真的接进生产代码库——因为它的风险不是「写错一段代码」,而是「在你没盯着的时候,对整个仓库和终端做了不可追溯的事」。
五、给测试负责人的三条落地建议
第一,先给 Agent 账号打标签、单独走一条更严的 CI,别让它和人类 PR 共用门禁——它的风险面本来就更宽。第二,从第一天就把轨迹落盘,哪怕暂时不写断言;没有轨迹,将来想做行为回归时无从下手。第三,把权限边界写成可执行断言而不是文档约定,沙箱拦不住的边界等于没有边界。
开源编程 Agent 进了代码库,测试要回答的就不再是『它能不能跑』,而是『它的每一次行为,我能不能复现、能不能归因、能不能拦住副作用』。
你团队要接的第一个编程 Agent,回归门禁和轨迹落盘,准备先上哪一个?评论区聊聊。
- 点赞
- 收藏
- 关注作者
评论(0)