本节摘要:本节把第 8 章的立场落成数据结构。先立论:留痕不是负担而是资产——出了争议时,自动化操作者的举证责任在自己,「做了什么、为什么做、依据什么」三问都要拿记录回答;没有留痕的系统在合规问询面前等于裸奔。然后给事件溯源式的设计:每个动作是一条只追加(append-only)的事件,字段含时间戳、任务标识、动作类型、意图(为什么做)、决策依据(模型当时的思考或规则触发的条件)、结果与证据指针(第 6.1 节三件套的路径)——事件流不可改写,回放即真相。第 2 章的执行历史、第 5 章的可视化报告、第 4 章的快照 trace,在这里统一汇入同一个事件结构。再讲脱敏与保留期:留痕本身也有隐私面(页面内容、用户数据进了日志),字段级脱敏规则加保留期制度让留痕可持续。最后是三种审计查询的形态:回放(单任务复盘)、统计(群体行为画像)、追责(时间线取证)。双语实现一个最小事件日志。
第 6.1 节的三件套解决了「失败时刻」的证据;审计留痕把覆盖面从失败时刻扩展到每一个动作。动力来自三问:
审计三问 没有留痕时的回答 有留痕时的回答 这个系统做了什么? 「大概……按设计应该……」 事件流逐条回放 它为什么这么做? 「模型决定的,我也想知道」 每条事件附决策依据 出问题的是哪一步? 「我们重新跑一遍看看?」 时间线定位到动作
举证责任的归属要明确:自动化操作者自证清白。站点方或监管方看到异常流量时,你拿不出「授权范围、操作清单、决策依据」的记录,默认解释权就不在你手里。第 2.2 节的事后审计形态、第 7.1 节的凭证台账,到这里汇成完整的自证体系。工程上它还有第三重价值:事件流是最高质量的回归素材与失败样本库,第 11 章的监控与错误分类直接消费它。
事件溯源(event sourcing)思想的核心:状态是事件的折叠结果,事件本身只追加、不修改、不删除。落到浏览器智能体:
| 字段 | 内容 | 示例 |
|---|---|---|
| ts | 时间戳(含时区) | 2026-10-16T10:30:01+08:00 |
| run_id | 任务标识 | task-0417 |
| action | 动作类型与目标 | click 添加按钮 |
| intent | 为什么做(任务步骤名) | 提交新待办 |
| basis | 决策依据(模型思考摘要或规则条件) | 模型判断表单已填完 |
| outcome | 结果状态 | 成功;页面出现新条目 |
| evidence | 证据指针(三件套路径) | runs/task-0417/step3.* |
append-only 的意义:记录一旦写入不可改——它保护的不是系统是信任。事后补记、悄悄删改在自己内部复盘里或许无伤,在争议场景里一旦被发现,整份记录的可信度清零。工程上用只追加文件或事件表实现,配合第 11 章的日志采集。
双语最小实现(JSONL 追加,字段即上表):
# audit_log.py —— 动作事件留痕(append-only,写法示意) import json import time from pathlib import Path LOG = Path("runs") / "audit.jsonl" SENSITIVE = {"password", "token", "cookie", "session"} # 脱敏字段集 def mask(payload: dict) -> dict: return {k: ("***" if k in SENSITIVE else v) for k, v in payload.items()} def append_event(action: str, intent: str, basis: str, outcome: str, evidence: str, run_id: str = "task-0000") -> None: event = { "ts": time.strftime("%Y-%m-%dT%H:%M:%S+08:00"), "run_id": run_id, "action": action, "intent": intent, "basis": basis, "outcome": outcome, "evidence": evidence, "extra": mask({"password": "never-write-real-value"}), } LOG.parent.mkdir(exist_ok=True) with LOG.open("a", encoding="utf-8") as fh: fh.write(json.dumps(event, ensure_ascii=False) + "\n") append_event( action="click(添加)", intent="提交新待办", basis="表单校验通过,模型选择主提交按钮", outcome="成功,列表出现条目", evidence="runs/task-0000/step3.*", )
// audit_log.mjs —— 动作事件留痕(append-only,写法示意) import { appendFileSync, mkdirSync } from "node:fs"; const LOG = "runs/audit.jsonl"; const SENSITIVE = new Set(["password", "token", "cookie", "session"]); // 脱敏字段集 /** 字段级脱敏 @param {Record<string, unknown>} payload 附加字段 */ function mask(payload) { return Object.fromEntries( Object.entries(payload).map(([k, v]) => [k, SENSITIVE.has(k) ? "***" : v]) ); } /** * 追加一条动作事件(只追加,不修改) * @param {string} action 动作 @param {string} intent 意图 @param {string} basis 决策依据 * @param {string} outcome 结果 @param {string} evidence 证据指针 @param {string} runId 任务标识 */ function appendEvent(action, intent, basis, outcome, evidence, runId = "task-0000") { mkdirSync("runs", { recursive: true }); const event = { ts: new Date().toISOString(), run_id: runId, action, intent, basis, outcome, evidence, extra: mask({ password: "never-write-real-value" }), }; appendFileSync(LOG, JSON.stringify(event) + "\n", "utf8"); } appendEvent( "click(添加)", "提交新待办", "表单校验通过,模型选择主提交按钮", "成功,列表出现条目", "runs/task-0000/step3.*" );
事件结构的价值在统一。前半本各章已经在各自产出留痕材料,只是形态分散:
| 来源 | 已有材料(出处) | 汇入方式 |
|---|---|---|
| 循环执行历史 | browser-use 的动作结果序列(第 2.1 节) | 每个动作映射一条事件,basis 取模型思考 |
| 快照 trace | MCP 会话的快照加 ref 记录(第 4.2 节) | 快照文件作为 evidence 指针 |
| 视觉中间产物 | Midscene 报告的截图加坐标(第 5.2 节) | 报告路径作为 evidence 指针 |
汇总原则:框架的原始产物保持原样存放,事件流只存指针与摘要。这样事件流轻(可长期保存、可全文索引),证据重(按需调取、占空间但有保留期)。决策依据(basis)字段的采摘要克制:模型的长思考全文不必进事件,取「选择了什么、排除了什么、为什么」三要素的摘要即可——审计要的是可解释,不是可复读。
留痕系统本身在制造新的合规面:页面内容、用户数据、会话状态都进了日志。两条制度兜住:
| 制度 | 规则(示例) | 说明 |
|---|---|---|
| 字段级脱敏 | 敏感字段集(凭证、令牌、身份证件号)写入即掩码 | 上文代码的 mask;第 2.2 节占位符是上游防线 |
| 保留期分级 | 摘要事件流长期(一年以上),完整证据短期(两周至三个月) | 证据含页面快照,隐私浓度高 |
| 访问控制 | 事件流只读、按角色开放查询 | 审计材料不能成为新的泄露源 |
保留期的两头都要定:太短,问询时拿不出证据;太长,存量日志变成隐私债务。分级的逻辑是「摘要不含敏感内容可长存、证据含敏感内容须短存」——与第 7.1 节状态文件的生命周期管理同一套思路。
留痕的价值最终由查询兑现:
| 查询 | 问题 | 形态 |
|---|---|---|
| 回放 | 这一个任务当时怎么走的? | 按 run_id 过滤事件流,按 ts 排序,配合证据逐条查看 |
| 统计 | 这类任务的群体行为画像? | 聚合动作分布、成功率、失败类别(喂给第 11.2 节看板) |
| 追责 | 某时刻系统对某账号做了什么? | 时间线切片,account 加时间窗过滤,输出动作与依据 |
三种查询共享同一份事件流,不需要三套日志。工程上事件流落 JSONL 或事件表皆可,量级大了再谈时序存储——起步阶段的瓶颈从来不是存储引擎,是字段纪律:漏记 intent 或 basis 的事件,存储再先进也回答不了审计三问。
| 坑 | 现象 | 排查与处理 |
|---|---|---|
| 只记失败不记成功 | 成功路径无据可查 | 每动作一条事件,失败只是一种 outcome |
| basis 记结论不记依据 | 「模型点击了按钮」无解释 | 记选择与排除的理由摘要 |
| 日志明文进敏感字段 | 快照里带 Cookie 值 | 字段级脱敏加上游占位符双保险 |
| 保留期一刀切长存 | 隐私债务逐年累积 | 摘要与证据分级保留 |
| 事后补记 | 记录时间戳与真实时间对不上 | 只追加、不可改,写入即定格 |
至此,验证、基建、合规三块地基全部就位。带着这套约束重新审视效率问题:哪些任务值得从「每步花 token」的路径搬到「零边际成本」的路径?下一章讲固化决策——Agent 探索在先、测试固化在后的完整管道。