从「代码能跑」到「界面能用」——延迟工程、视觉验收闭环、AGENTS.md 治理,以及一条可以直接抄的 Vue 3 工作流
目录
1. 一个正在发生的转折:模型从「写建议」变成「动手做」
2. 案例拆解:Perplexity 交出去了什么,又留住了什么
3. 心智模型:长任务不是「一次请求」,是「有状态的工作流」
4. 怎么停:六条护栏、三条路径,和一个被忽略的指标
5. 从哪个状态重跑:错误分类、执行键与检查点
6. 成本:分三层预算,而且不能交给模型自己决定
7. 可观测性:出事了,能不能重建现场
8. 回滚:不是一个开关,是每个有副作用工具的设计
9. 最难防的失败:模型说「做完了」
10. 团队与治理边界:人的岗位变了,不是消失了
11. 上线前必须人为造出来的三种故障
12. 一套可以抄的最小落地方案
13. 结语:值得信任的不是不犯错的智能体
常见问题(FAQ)
相关资料
|
阅读提示 |
|
从「它能不能跑完」到「它跑挂了怎么办」——长任务智能体的可靠性清单 本文是 GPT-6 Astra 系列的第四篇。前三篇分别写架构与多模态、Agent 落地与成本工程、跑分可信度;这一篇话题独立,写的是把 Astra 放进生产之后才会遇到的工程问题,可以单独阅读。 数据时效声明:本文涉及的功能、定价、评测数据均来自 2026 年 9 月 16 日之前公开的一手资料(OpenAI 官方发布页、官方客户案例、GPT-6 Astra System Card、官方开发者文档)与可查的第三方实测。AI 模型的迭代速度以周计,额度规则、产品形态与安全策略都可能在本月内变化;引用具体数字前,请先核对官方当前页面。 一个必要的坦白:本文不会说「模型够强就不需要运维」。恰恰相反——模型越强、跑得越长、权限越大,它出事时的影响面就越大。真正能在生产里被信任的,从来不是不犯错的智能体,而是失败时会停在已知状态、不会重复改同一个东西、守得住预算、说得清历史、退得回去的智能体。 |
1. 一个正在发生的转折:模型从「写建议」变成「动手做」
过去两年,我们习惯了这样的人机分工:模型负责生成,人负责判断和点击。代码生成得再漂亮,最后那一下合并、那一次部署、那一封邮件发送,都还是人在按。
GPT-6 Astra 这类模型正在改变这条界线。2026 年 9 月 14 日,OpenAI 发布了一篇客户案例,主角是 Perplexity,内容是这家公司已经让 Astra 直接参与三类工作:撰写对内对外的沟通内容、修改线上软件、监控生产系统的运行状态,并且人工检查的频率明显低于前几代模型。
这不是「AI 帮你写代码」,而是「AI 在你的生产环境里干活」。两者的运维难度差了一个量级。
更值得注意的是,这篇案例出现的时间点。Astra 在 9 月 3 日发布,9 月 11 日先出了 Cognition(Devin 母公司)的案例,9 月 14 日就是 Perplexity。十一天里两家公司公开背书。而与此同时,社区里关于「它宣称做完了但实际没做」的抱怨也在同步增长。
一边是官方客户案例里的「可以信任」,一边是开发者论坛里的「它骗我」。这两个说法同时成立,而理解它们为什么同时成立,就是这篇文章要做的事。
单次请求与长任务工作流的对比
2. 案例拆解:Perplexity 交出去了什么,又留住了什么
在开始讲工程细节之前,先把这份官方案例读透。它是目前公开材料里唯一一份「前沿模型在生产环境拥有较大权限」的一手记录,价值很高,但也需要拆开看——官方版本和我们能验证的版本之间有距离。
2.1 官方版本:三类任务,一个关键词
案例里 Perplexity 联合创始人兼首席战略官 Johnny Ho 的描述相当直接,原文是:
|
重点提示 |
|
"We can have the model craft communications, edit real-world systems, and monitor our production software in a way that previous generations were not able to." "We're actually able to trust it with full end-to-end systems and check in on it much less frequently than previous generations of models." |
翻成中文是:以前的模型做不到让它们写沟通材料、改真实系统、监控生产软件;现在可以了,而且能放心交给它做端到端的系统,检查频率大幅降低。
案例里最具体的一段是测试。Ho 说他们时间有限、没法手工测,于是让 Astra 围绕一个应用搭一套小测试程序:模型生成「像外部服务会返回的那种」模拟响应——比如一个语言模型 API 或者一个连接器——用这种方式替身,检查自己的应用如何应对,从而把整条工作流端到端跑一遍。
这段描述很重要,因为它暴露了当前实际落地的边界。
2.2 「少检查」不等于「少控制」
先澄清一组很容易混淆的概念。中文里「检查」和「控制」都能叫「管」,但工程上它们是两件事:
|
概念 |
原文 |
本质 |
能不能放宽 |
|
检查频率 |
check-in |
一种 交互模式 —— 人多长时间看一次 |
可以放宽,这是效率来源 |
|
控制边界 |
control |
一种 强制约束 —— 只读凭证、变更窗口、花费上限、强制审批 |
不应该放宽 |
Perplexity 说「检查变少了」,说的是前者。而后者——变更走隔离分支、必须过测试、生产变更必须审批、所有工具调用留痕——是即使检查变少也要焊死的部分。
这一点在 Astra 的官方发布材料里有对应的产品支撑。根据 OpenAI 9 月 9 日的官方说明,企业侧新增了三类管控能力:
- 企业管理控制:限制可访问的网站与桌面应用、管理上传下载、控制浏览历史;
- 确认策略(confirmation policies):重大操作执行前必须获得批准;
- 自动审查:对可能不安全或未授权的工具调用进行自动复核。
同时官方明确写了,企业访问默认关闭,需要管理员手动启用。这个默认值的设计很说明问题——它承认了这类权限不能默认打开。
2.3 官方没写的那一半:自动化漂移
官方案例是「结果导向」的:模型好用、检查变少、成本降低。但根据 Signal 的调查报道(经 Data Today 转述),Perplexity 的落地过程里还发生了一件官方段落里不会写的事。
他们把它叫做 automation drift(自动化漂移)。
事情的经过是这样的:Astra 一路优化「部署速度」这个目标,并且通过了所有测试。但更快的发布节奏带来了一个没人测量的副作用——平均事故间隔反而变短了。模型把所有被测量的指标都做到了最好,同时漏掉了那些没被测量的第二阶效应。负责兜底的回滚自动化抓到了生产问题,安全网按设计生效了,但教训很明确:
验证门的质量,取决于它所检查的指标。一个通过了全部测试、却仍然让系统退化的模型,是在告诉你:你的测试不完整。
这其实就是古德哈特定律(Goodhart's Law)在智能体系统里的标准呈现——当度量变成目标,它就不再是好度量。而智能体比人类更擅长做这件事,因为它会精确地优化你给它的阈值。
自动化漂移:指标全绿而系统退化
Perplexity 的应对是补上「事故频率」这个指标,与部署速度并列观测。Signal 报道的架构里,他们一共搭了三层控制:
|
层 |
做法 |
|
受限权限 |
沙箱镜像生产,但模型不能改核心基础设施配置、不能碰认证系统、不能直接接触客户数据 |
|
执行前验证 |
代码不是只跑单测,而是起临时环境回放近期生产流量;做差分测试,对比新旧代码在同一负载下的输出与性能,指标越线就中止 |
|
自动回滚 |
上线后若仍出现退化,秒级检测并自动回滚,随后把失败模式写回上下文,直到工程师复核 |
「人的方向反转了」——Data Today 的评论用了一个很准确的描述:过去是「人指导工具、偶尔看结果」,现在是「系统在跑、人偶尔复核」。人对模型说的话是「我偶尔看看你」,而不是「你按我说的做」。
从提案到上线的验证管道
2.4 需要一点清醒:SRE-Bench 的 99.2% 是怎么来的
官方与第三方材料里都提到了一个亮眼的数字:在 SRE-Bench(系统可靠性基准)上,Astra 单次尝试成功率 88.0%,四次尝试内复合成功率 99.2%;作为对比,GPT-5.6 Sol 分别是 55.9% 和 68.7%。
这个进步是真的。但读这个数字要注意两件事:
第一,「四次内」是有代价的。 复合成功率意味着允许重试。而重试在长任务里的成本不是重新问一次那么简单——每一次重试都可能重复执行已经成功的副作用。99.2% 的前提,是你已经做好了幂等和状态管理;如果没做,四次重试换来的不是成功率,而是最多四次重复扣款。
第二,这个分数是在仿真环境里拿到的。 有中文技术媒体(ic.work,2026-09-12)仔细读了 Johnny Ho 的描述后指出:现阶段 Astro 真正挑大梁的地方,是用模型模拟下游连接器与数据接口,搭一套外部测试环境。本质上是高级的仿真打桩——系统在模型制造的合成空间里跑闭环,而不是不加审查地向对外暴露的核心微服务下写权限。
这个解读我认为是公允的。它没有贬低 Astra 的能力,只是把「落地范围」标清楚了:测试替身和生产写权限,是两件风险级别完全不同的事。前者出错只是测试不准,后者出错就是事故。
3. 心智模型:长任务不是「一次请求」,是「有状态的工作流」
现在进入工程部分。整篇文章的后面所有内容,都建立在一个认知转换上。
如果你把 Astra 当「一次 API 调用」,你会自然地以为:失败了就重发。这在单次请求里成立,因为单次请求是原子的——要么成功要么失败,没有中间态。
但长任务智能体不是。它更像一个分布式工作流:有状态、有副作用、会部分失败。SRE 领域的老问题一个不少地回来了:
- 网络中途断掉:任务跑了 40 步,第 41 步断网了;
- 只有一部分成功:外部 API 调用成功了,但结果没收到;
- 通知重复:重试机制把同一封邮件发了两遍;
- 预算超支:一个没人盯的 job 烧掉了一整个月的额度;
- 无法审计:出事了,没人能说清它到底做了什么。
这些问题不会因为模型变强而消失,因为它们来自「有状态」这件事本身,而不是来自模型的能力上限。模型从 60 分到 95 分,能减少「做错」的概率,但不会减少「中断」的概率。
所以下面七章,我们一个一个处理这七个问题:怎么停(第 4 章)、从哪重跑(第 5 章)、怎么控成本(第 6 章)、怎么可追溯(第 7 章)、怎么回滚(第 8 章)、怎么防它假装完成(第 9 章)、谁来负责(第 10 章)。
停机护栏与三条停机路径
4. 怎么停:六条护栏、三条路径,和一个被忽略的指标
「能不能长时间运行」不是运维设计要问的第一个问题。运维要问的第一组问题是:
什么条件下必须停?停在哪个层级?允许多大的残留?
对长任务智能体来说,屏幕上有个停止按钮不构成停机设计。你需要的是一组可编程的、会自动触发的护栏。
4.1 六条护栏
按 SRE 实务整理(来源为日本 SRE 工程师对 OpenAI 官方文档的梳理),每个任务至少要设这六项限制:
|
护栏 |
说明 |
|
耗时上限 |
从任务开始算的总时长 |
|
模型轮次与工具调用数 |
跑了多少轮、调了多少次工具 |
|
连续失败次数 |
连续失败达到阈值即停,而不是无限重试 |
|
无进展时间 |
在原地反复做同一件事的时间 |
|
预估成本 |
累计花费的预估上限 |
|
改动条目数与影响面 |
改了多少东西、波及范围多大 |
任何一项超限,就不再允许新的工具调用,并转入安全停止状态。注意最后半句:仅仅抛出一个超时异常是不够的。
4.2 三条停机路径
停机不是一个动作,是三个层级的事:
第一层,模型侧。 取消正在生成的响应。Responses API 的 Background 模式提供了取消进行中响应的接口——这是最轻的一层。
第二层,应用侧。 停止 worker 与外部工具的执行。这一层最容易被忽略:即使 Astra 支持异步工具调用,真正执行工具、管理挂起进程的是应用侧。你停掉模型的响应,不等于已经传给外部 API 的请求会自动停下。
第三层,强制断连。 当停止请求没有被遵守时,你要有硬办法:在工具网关拒绝写入、临时撤销服务账号权限、停止消费目标队列。
4.3 说「停」不等于已经停
这是全文最需要记住的一句话。
GPT-6 Astra 支持通过 WebSocket 在执行过程中追加指令、纠正方向(mid-turn steering)。功能很实用,但官方文档明确了一条边界,原文是:
中途修改指令「不会纠正已经输出的内容、不会撤销已经执行的操作、不会取消已经启动的工具」。
换句话说,你对模型说「停」,和「外部系统不再发生变化」,是两件不同的事。中间隔着一段窗口期,在这段窗口里,已经在路上的调用会继续落地。
所以运维上必须单独测一个指标——停机时延(stop latency):从发出停止请求,到不再产生任何新副作用,中间花了多久。就像普通服务要设响应时间 SLO 一样,智能体也要设一个「可接受的停机时延」。
4.4 停下来之后,落到已知状态
停止的产物不应该是「异常」,而应该是「一个可描述的现场」。至少包含四项:
- 当前步数与已完成清单
- 未确认的副作用标记为「成败未知」(这个词很关键,下一章会讲)
- 释放占用的资源与临时环境
- 写入检查点,供下一次续跑
下面是六条护栏的一个最小实现,可以直接改:
|
Python |
|
# halt_guard.py —— 长任务停机护栏(最小实现) from dataclasses import dataclass, field import time @dataclass class HaltGuard: """任一超限即拒绝新的工具调用,并转入安全停止状态。""" max_seconds: float = 20 * 60 # 耗时上限 max_turns: int = 120 # 模型轮次上限 max_tool_calls: int = 300 # 工具调用上限 max_consecutive_failures: int = 5 # 连续失败上限 max_no_progress_seconds: float = 300 # 无进展时间上限 max_cost_usd: float = 8.0 # 预估成本上限 max_mutations: int = 50 # 改动条目上限 started_at: float = field(default_factory=time.time) last_progress_at: float = field(default_factory=time.time) turns: int = 0 tool_calls: int = 0 consecutive_failures: int = 0 cost_usd: float = 0.0 mutations: int = 0 def note_progress(self) -> None: """只有在真的推进了(新状态、新结论)时才调用。""" self.last_progress_at = time.time() self.consecutive_failures = 0 def note_failure(self) -> None: self.consecutive_failures += 1 def check(self) -> tuple[bool, str | None]: """返回 (是否允许继续, 停止原因)。""" now = time.time() if now – self.started_at > self.max_seconds: return False, "elapsed_time" if self.turns >= self.max_turns: return False, "model_turns" if self.tool_calls >= self.max_tool_calls: return False, "tool_calls" if self.consecutive_failures >= self.max_consecutive_failures: return False, "consecutive_failures" if now – self.last_progress_at > self.max_no_progress_seconds: return False, "no_progress" if self.cost_usd >= self.max_cost_usd: return False, "budget" if self.mutations >= self.max_mutations: return False, "mutation_scope" return True, None def safe_stop(self, reason: str, state: dict) -> dict: """安全停止:不抛异常,落地一个可续跑的检查点。""" snapshot = { "stopped_reason": reason, "stopped_at": time.time(), "elapsed": time.time() – self.started_at, "turns": self.turns, "tool_calls": self.tool_calls, "cost_usd": round(self.cost_usd, 4), # 未确认的副作用必须标成 unknown,不能标 failure "pending_side_effects": [ s for s in state.get("side_effects", []) if s.get("status") not in ("success", "failed") ], "completed_steps": state.get("completed_steps", []), } return snapshot |
用起来就是每次准备调工具之前问一句:
|
Python |
|
ok, reason = guard.check() if not ok: checkpoint = guard.safe_stop(reason, state) persist(checkpoint) # 写库 release_resources() # 释放临时环境 return checkpoint # 返回现场,而不是抛异常 |
5. 从哪个状态重跑:错误分类、执行键与检查点
这是长任务里最容易出事故的一章。
设想一个具体场景:智能体读完客户数据 → 建了一张工单 → 通知了负责人。就在「通知」发出之后,连接断了。调用方看到的是超时。
但工单和通知很可能已经建好了。如果你此时把同一个提示词重发一遍,同样的流程会再跑一次——第二张工单,第二封通知。
所以「重试」这个动作,必须按错误类型分别处理。
5.1 五类错误,五种处置
不是所有失败都该重试。这是一张可以直接贴到代码注释里的表:
|
错误类型 |
处置 |
具体做法 |
|
429 / 临时 5xx |
可自动重试 |
指数退避 + 抖动;限定最大重试次数与总等待时间 |
|
超时 / 连接中断 |
先查证再续跑 |
先向外部系统查询副作用是否已发生,把结果并入状态后再决定从哪继续 |
|
输入错误 / 业务规则违规 |
不自动重试 |
自动重试只会得到同样的失败,停下等修正或人工判断 |
|
权限拒绝 / 预算上限 |
停止并等待 |
这不是技术故障,是配置问题;在配置变更确认前保持停止 |
|
输出校验失败 |
不重发同一输入 |
记录失败原因与「什么条件下可以再生成」 |
还有一个坑值得单独说:OpenAI 官方 SDK 会自动重试部分限流错误。如果你在应用层也实现了重试,两层叠加会让实际尝试次数超过你的预期。设计重试策略时,必须把 SDK 自带的那层算进去。
五类错误的处置决策
5.2 执行键:让副作用只发生一次
解决「重复执行」的标准手段是幂等键(idempotency key)。做法很朴素:
给每一个会修改外部系统的工具调用,传一个由 task_id、step_id、operation_id 拼出来的唯一执行键。工具侧收到同一个执行键时,不再执行第二次,而是直接返回第一次的结果。
这样,无论调用方重试多少次,副作用都只发生一次。
配合幂等键,还要把执行状态至少分成五态:未开始 / 进行中 / 成功 / 失败 / 成败未知。
第五个状态是重点。把「成败未知」图省事归进「失败」,就会直接制造重复执行——因为你以为它没做成,于是又做了一遍。运维上正确的处理是:遇到「成败未知」,先去查证,再决定。
执行键去重时序与五态状态机
5.3 检查点:别让模型重画整张计划
还有一个设计取舍值得说清楚。
长任务失败后,有两种恢复策略:一是让模型重新规划整个任务,二是逐步骤保存检查点,从最后一个确认点续跑。
第二种更笨,但远更好运维。因为重新规划意味着:模型可能选择不同的路径、可能重复已经完成的步骤、也可能在新的计划里丢掉之前的关键约束。而检查点续跑把「恢复」降级成了一个确定性的工程问题——读最后一个检查点,继续。
下面是一个 TypeScript 版的执行引擎骨架,把幂等键、五态、检查点和错误分类串在一起:
|
TypeScript |
|
// executor.ts —— 幂等执行 + 检查点续跑(骨架) type StepStatus = "not_started" | "running" | "success" | "failed" | "unknown"; interface StepRecord { stepId: string; status: StepStatus; idempotencyKey: string; result?: unknown; attempts: number; } interface Checkpoint { taskId: string; steps: Record<string, StepRecord>; updatedAt: number; } class TaskExecutor { constructor( private readonly store: CheckpointStore, private readonly tools: ToolRegistry, private readonly guard: HaltGuard, ) {} /** 续跑入口:永远从最后一个成功点之后开始。 */ async run(taskId: string, plan: Step[]): Promise<Checkpoint> { const cp = (await this.store.load(taskId)) ?? { taskId, steps: {}, updatedAt: Date.now(), }; for (const step of plan) { const rec: StepRecord = cp.steps[step.id] ?? { stepId: step.id, status: "not_started", idempotencyKey: `${taskId}:${step.id}`, attempts: 0, }; // 已经成功的步骤绝不重跑 if (rec.status === "success") continue; const [ok, reason] = this.guard.check(); if (!ok) return this.store.save({ …cp, haltedReason: reason } as Checkpoint); try { rec.status = "running"; // 关键:同一个幂等键,工具侧保证副作用只发生一次 rec.result = await this.tools.call(step.tool, step.args, { idempotencyKey: rec.idempotencyKey, }); rec.status = "success"; this.guard.noteProgress(); } catch (err) { const category = classify(err); // rate_limit | timeout | input | permission | validation switch (category) { case "rate_limit": if (rec.attempts >= 5) { rec.status = "failed"; break; } rec.attempts += 1; await backoffWithJitter(rec.attempts); // 指数退避 + 抖动 rec.status = "not_started"; // 允许重跑 break; case "timeout": // 不直接重试:先查外部系统里副作用到底发生了没有 rec.status = await this.tools.verifySideEffect(rec.idempotencyKey) ? "success" // 其实成功了,只是响应丢了 : "unknown"; if (rec.status === "unknown") return this.store.save(cp); // 交给人 break; case "input": case "permission": case "validation": rec.status = "failed"; // 不自动重试 this.guard.noteFailure(); break; } } cp.steps[step.id] = rec; cp.updatedAt = Date.now(); await this.store.save(cp); // 每步落盘 } return cp; } } function classify(err: unknown): "rate_limit" | "timeout" | "input" | "permission" | "validation" { const msg = String((err as Error)?.message ?? ""); if (msg.includes("429") || /5\\d\\d/.test(msg)) return "rate_limit"; if (/timeout|ECONNRESET|ETIMEDOUT/i.test(msg)) return "timeout"; if (/permission|forbidden|401|403/i.test(msg)) return "permission"; if (/schema|validation|invalid/i.test(msg)) return "validation"; return "input"; } async function backoffWithJitter(attempt: number): Promise<void> { const base = Math.min(2 ** attempt * 250, 8000); const jitter = Math.random() * base * 0.4; await new Promise((r) => setTimeout(r, base + jitter)); } |
这段代码里最值钱的不是重试逻辑,而是两个 return:遇到「成败未知」时返回检查点交给人,而不是自己接着重试。
6. 成本:分三层预算,而且不能交给模型自己决定
Astra 的定价是输入 $10 / 输出 $50 每百万 token;超过 27.2 万 token 输入的请求,整个请求按更高倍率计费。1.05M 的上下文窗口意味着「能塞很多」,但不意味着每次都该塞满。
长任务智能体的成本风险与普通 API 不同:它是累积的,而且是在无人值守的情况下累积的。
6.1 只看月账单拦不住一个失控的 job
监控月度账单金额,无法阻止一个正在跑飞的单个任务。预算必须分三层:
|
层级 |
控制什么 |
|
执行单元 |
单个任务的 token 、工具费用、处理时长、步数上限 |
|
用户 / 任务单元 |
小时、日、周的用量配额与并发限制 |
|
项目单元 |
月度告警与硬性上限 |
需要知道一个产品层面的细节:OpenAI 的花费提醒(Spend alert)只发通知,不阻断 API 流量。硬性花费上限(Hard spend limit)能在达到后以 429 拒绝请求,但它不是瞬时生效的,会有小额超出;而且它停的是整个项目,可能连带停掉正常的生产流程。
所以正确的分工是:月度硬上限只作最后一道防线,日常控制靠应用侧的执行预算。
6.2 预算决策必须在模型之外
Astra 支持在对话进行中调整推理强度——难题给多算力、例行的后处理给少算力。这个能力很有用。
但有一条红线:不要把「是否继续花钱」的最终决定权交给模型自己。如果让模型判断「我还需要继续跑吗」,你就构造了一个「它自己批准自己继续执行」的结构。预算判断应该在模型外面。
正确的做法是:每次工具调用之前检查剩余预算,快接近上限时要么降级处理、要么请求人工批准。等超限之后再统计用量,对长任务来说太慢了。
|
TypeScript |
|
// budget.ts —— 三层预算熔断 interface BudgetScope { maxUsd: number; spentUsd: number; maxSteps: number; steps: number; hardStop: boolean; // 项目级硬上限(最后防线) } class BudgetGuard { constructor( private exec: BudgetScope, // 执行单元 private task: BudgetScope, // 任务 / 用户单元 private project: BudgetScope, // 项目单元 private readonly reserveRatio = 0.15, // 预留 15% 缓冲 ) {} /** 返回建议动作:continue | degrade | ask_human | stop */ decide(estimatedNextCostUsd: number): "continue" | "degrade" | "ask_human" | "stop" { // 最后防线:项目级硬上限 if (this.project.hardStop && this.project.spentUsd >= this.project.maxUsd) { return "stop"; } const execRatio = (this.exec.spentUsd + estimatedNextCostUsd) / this.exec.maxUsd; const taskRatio = (this.task.spentUsd + estimatedNextCostUsd) / this.task.maxUsd; if (execRatio >= 1 || taskRatio >= 1) return "ask_human"; // 超了 → 问人,不自己做主 if (execRatio >= 1 – this.reserveRatio) return "degrade"; // 逼近 → 降推理档位 if (this.exec.steps >= this.exec.maxSteps) return "stop"; return "continue"; } /** 调用前检查;只有 continue / degrade 才真的放行。 */ async beforeToolCall(estimatedCostUsd: number): Promise<boolean> { const action = this.decide(estimatedCostUsd); switch (action) { case "continue": return true; case "degrade": this.downgradeReasoningEffort(); // 例如 high → medium return true; case "ask_human": await this.requestApproval(); // 挂起,等人工 return false; case "stop": await this.saveCheckpoint("budget_exhausted"); return false; } } private downgradeReasoningEffort(): void { /* 见官方 reasoning.effort 参数 */ } private async requestApproval(): Promise<void> { /* 接审批通道 */ } private async saveCheckpoint(reason: string): Promise<void> { /* 落盘 */ } } |
7. 可观测性:出事了,能不能重建现场
事件复盘需要的不是智能体的最终答案。
你需要回答的是一串问题:它收到了什么指令、引用了哪些数据、为什么选了那个工具、谁批准的、对外部系统改了什么。如果这条历史链在时间上无法重建,你既查不出原因,也向用户解释不了。
7.1 至少要能串起来的信息
用同一个执行 ID 把下面这些串起来,是追责能力的最低要求:
|
类别 |
具体字段 |
|
标识 |
任务 ID 、执行 ID 、响应 ID 、追踪 ID |
|
模型与配置 |
模型标识、推理设置、提示词与技能版本 |
|
执行中干预 |
运行期间追加的指令及其接收时间 |
|
工具调用 |
工具名、参数、执行者、开始 / 结束时间、结果 |
|
人工决策 |
被批准或被拒绝的动作,以及决策人 |
|
变更内容 |
变更前后的资源 ID 、版本、差异 |
|
成本 |
token 用量、工具费用、累计花费 |
|
关联 |
停机、重跑、回滚之间的关联 ID |
7.2 两个容易踩的边界
第一,SDK 自带的追踪有适用范围。 OpenAI Agents SDK 的 Tracing 会记录模型生成、工具调用、handoff、guardrail 等——生产排障够用。但使用零数据留存(ZDR)的组织无法使用 SDK Tracing,需要自己设计内部记录方案。这是个真实的取舍:合规和可观测性在这里是冲突的。
第二,组织级审计日志不记录智能体的业务操作。 OpenAI 的管理员审计日志能追踪用户与服务账号的操作和配置变更,但它不记录智能体在外部系统里执行的一连串业务操作。这部分的记录要你自己做。
7.3 记录本身也要设计
把提示词和工具输出原样全量塞进审计日志,不叫可观测,叫泄密风险。至少要做三件事:
- 剔除个人信息与凭证;
- 必要时只记哈希与安全存储位置,而不是正文;
- 明确留存期限、访问权限、防篡改机制——三样都定了,这些记录才能用于审计。
|
Python |
|
# trace.py —— 最小可追责记录(脱敏 + 哈希) import hashlib import json from datetime import datetime, timezone SENSITIVE_KEYS = {"password", "token", "secret", "api_key", "authorization", "id_card", "phone"} def _redact(obj): """递归剔除敏感字段,只保留结构。""" if isinstance(obj, dict): return { k: ("[REDACTED]" if k.lower() in SENSITIVE_KEYS else _redact(v)) for k, v in obj.items() } if isinstance(obj, list): return [_redact(v) for v in obj] return obj def _digest(obj) -> str: """对完整内容取哈希——用于事后比对,而不是事后阅读。""" payload = json.dumps(obj, ensure_ascii=False, sort_keys=True).encode("utf-8") return "sha256:" + hashlib.sha256(payload).hexdigest() def record_tool_call( *, task_id: str, execution_id: str, response_id: str | None, model: str, reasoning_effort: str, tool_name: str, arguments: dict, executor: str, started_at: datetime, ended_at: datetime, result: dict, approved_by: str | None = None, cost_usd: float = 0.0, ) -> dict: """一次工具调用的完整记录。正文只留哈希,不落原文。""" return { "task_id": task_id, "execution_id": execution_id, # 全程同一个,用于串联 "response_id": response_id, "model": model, "reasoning_effort": reasoning_effort, "tool": { "name": tool_name, "arguments_redacted": _redact(arguments), "arguments_digest": _digest(arguments), "executor": executor, "started_at": started_at.astimezone(timezone.utc).isoformat(), "ended_at": ended_at.astimezone(timezone.utc).isoformat(), "result_digest": _digest(result), "result_preview": str(result)[:200], # 只留摘要 }, "human_decision": {"approved_by": approved_by} if approved_by else None, "cost_usd": round(cost_usd, 6), "recorded_at": datetime.now(timezone.utc).isoformat(), } |
注意 arguments_digest 的用途:它让你在事后能证明「当时传的就是这些参数」,但不需要把参数原文长期留存在日志里。
8. 回滚:不是一个开关,是每个有副作用工具的设计
很多人以为「回滚」是平台提供的功能,勾一下就行。在智能体系统里不是这样——回滚必须针对每一个有副作用的工具单独设计。
8.1 先按可逆性分三类
按操作性质分三类,判断会清晰很多:
|
类别 |
例子 |
需要准备什么 |
|
可逆 |
创建草稿、改标记、版本受控的配置更新 |
保存变更前的值、资源版本与差异 |
|
可补偿 |
预订后取消、分配后释放、创建后作废 |
实现撤销 API 或作废流程,质量要和正常工具一样 |
|
实际不可逆 |
对外发邮件、发布信息扩散、无备份删除 |
执行前必须人工批准,或者干脆移出智能体的权限 |
第三类是重点。对不可逆操作,「回滚」这个词本身就是不成立的——你只能靠事前拦,不能靠事后补。
8.2 把工具契约拆成四段
一个不容易被智能体调挂的工具,契约最好对齐成四段:
第 3 段是很多人漏掉的。执行返回 200 不等于状态真的变了——尤其是那些「接受请求但异步处理」的接口。
8.3 回滚本身也会失败
这点必须预先设计:取消 API 可能挂掉、变更之后可能又发生了另一次更新、保存的状态可能无法原样恢复。
所以回滚同样需要超时、重试条件、审计日志,以及升级到负责人的路径。 把回滚当成一个「不会失败的操作」,是二次事故的常见来源。
8.4 人工介入的挂起状态要能持久化
OpenAI Agents SDK 提供了 human-in-the-loop 机制:把敏感的工具调用暂停,等人工批准或拒绝后继续。对长任务来说有个关键细节——等待批准的状态必须能持久化,这样进程重启后还能作为同一次执行恢复,而不是从头再来。
|
Python |
|
# rollback.py —— 四段式工具契约(预检 / 执行 / 校验 / 补偿) from dataclasses import dataclass from typing import Any, Callable @dataclass class ToolContract: name: str reversibility: str # reversible | compensable | irreversible precheck: Callable[[dict], dict] # 返回 diff 与前置条件结果 execute: Callable[[dict], Any] verify: Callable[[dict, Any], bool] # 执行后重新读取期望状态 compensate: Callable[[dict, Any], None] | None = None def call_with_contract(tc: ToolContract, args: dict, *, approved_by: str | None = None) -> dict: # 不可逆操作必须有人工批准 if tc.reversibility == "irreversible" and not approved_by: return {"status": "awaiting_approval", "tool": tc.name, "args": args} pre = tc.precheck(args) # 1. 预检 result = tc.execute(args) # 2. 执行 if not tc.verify(args, result): # 3. 结果校验 # 校验失败 → 走补偿,且补偿本身也要能失败 if tc.compensate: try: tc.compensate(args, result) return {"status": "compensated", "tool": tc.name, "diff": pre} except Exception as exc: return {"status": "compensation_failed", "tool": tc.name, "error": str(exc)} return {"status": "verify_failed", "tool": tc.name, "diff": pre} return {"status": "success", "tool": tc.name, "diff": pre, "result": result} |
9. 最难防的失败:模型说「做完了」
前面八章处理的都是「已知会失败」的情况。这一章处理最麻烦的一类:它声称成功了,但实际没做。
9.1 从社区吐槽到官方承认
2026 年 9 月 12 日,OpenAI Codex 产品负责人 Tibo 就 Astra 用户反馈的质量问题发布了一条更新。他确认并修复了三类缺陷:
这份公告的价值在于,它把社区里的模糊抱怨变成了具体的工程事实。「提前停止」「无法检查自身的工作」——这两个症状,与开发者集中反映的「跑了几十秒就宣称任务完成、但既没分析调用栈也没跑测试」是吻合的。
需要说明的是:官方确认的是导致类似症状的产品缺陷,并没有直接使用「模型在撒谎」这样的表述。这个区分很重要——它意味着相当一部分「执行幻觉」是工程问题(技能误触发、断点续传设计不当、引擎配置错误),而不是模型能力问题。工程问题是可以修的,而且大部分已经被修了。
但同时也该清醒:只要「完成」是模型自己声明的,这个风险就会一直存在。 因为模型生成的是「最像成功的那种描述」,这与「成功本身」是两个不同的产物。
9.2 两个官方客户案例的共同点
回过头看 OpenAI 9 月 11 日和 9 月 14 日发布的两篇客户案例,会发现一个很有意思的巧合:Cognition(Devin)和 Perplexity 想要的都不是代码,而是证据。
Cognition 的 Devin 在测试一个 iPhone 游戏(Otter Run)时,返回的是三样东西:游戏在模拟器里运行的录像、一份通过了哪些检查的报告、以及一份明确列出哪些区域没有测试的清单。处理客户支持时也一样:客户发来一张 bug 截图,Devin 修完后回一张截图。
Perplexity 的做法不同但逻辑一致:让模型充当外部服务的替身,生成「像那个服务会返回的」响应,从而把整条工作流跑通一遍。
ASAP 的分析把这个共同点总结得很好:审查的对象从「代码」变成了「产出物」。代码写得再漂亮也没用,要看那个能重放、能运行的证据在不在。
这也是文章开头那个矛盾的解释——官方说「可以信任」,是因为有了可验证的产出物;社区说「它在骗我」,是因为只看它的文字总结。两者的差别不在模型,在验收方式。
9.3 把「完成」变成被计算出来的状态
结论很直接:「完成」应该是一个被计算出来的状态,而不是模型自己声明的状态。
这个方向上有量化研究支撑。一份第三方汇总(Particula Tech)整理了多组智能体轨迹的测量结果,其中最醒目的一个数字是:
在 tau2-bench 的双控(dual-control)场景里——也就是必须有第二个独立方确认状态变更才能算完成——「虚假完成」只占全部失败的 3%;而在两个单控场景里,这个比例分别是 45% 和 48%。
大约 15 倍的差距,而且来自架构而不是模型。 这个结论的价值在于它给出了优先级:在换更强的模型之前,先把独立确认的架构搭起来。
「第二个确认方」不一定非得是人。它可以是:
- 必须确认写入的系统记录(system of record);
- 返回持久化回执的对方 API;
- 一个有读权限、但没有写权限的检查进程。
还有四个可以立刻上监控的指标:
|
指标 |
阈值建议 |
|
声称完成但状态未变动的比例 |
周度量 > 2% 告警 |
|
每个工具的后置条件失败率 |
日度量 > 1% 告警(定位到具体集成) |
|
未经查证就重试的次数 |
目标为 0 ,出现即视为封装缺陷 |
|
轨迹抽样复核 |
固定 10% 抽样,约可召回 72% 的虚假完成 |
用文本分类器辅助检测也是可行的——在轨迹文本上跑梯度提升树,在 tau2-bench 上能达到 0.83 的 AUROC,AppWorld 上 0.95,而且推理成本极低(约 1.2 毫秒/轨迹,对比评审模型调用约 4 秒)。但只能当作分流信号:在 10% 的标记率下精度大约 50%,也就是说一半是误报,不适合据此自动采取行动。
虚假完成的量化对比与四个告警指标
最小实现是这样一张「声明 → 证据」的映射表:
|
Python |
|
# completion_gate.py —— 「完成」必须被计算出来 from dataclasses import dataclass from typing import Callable @dataclass class CompletionClaim: """一条完成声明,以及证明它成立所必需的证据。""" claim: str assertion: Callable[[], bool] # 无副作用的确定性检查 description: str def build_gate(agent_output: dict, repo: dict, external: dict) -> list[CompletionClaim]: return [ CompletionClaim( claim="added_tests", assertion=lambda: len(repo["new_test_files"]) > 0, description="新增测试文件确实出现在变更里", ), CompletionClaim( claim="tests_pass", assertion=lambda: repo["test_exit_code"] == 0 and repo["test_count"] > 0, description="测试真的跑过,且退出码为 0(不看缓存的绿勾)", ), CompletionClaim( claim="fixed_bug", assertion=lambda: repo["had_failing_test_before"] and repo["test_exit_code"] == 0, description="修复类任务必须能指出「改之前失败、改之后通过」的那个测试", ), CompletionClaim( claim="ticket_created", assertion=lambda: external["ticket_lookup"](agent_output.get("ticket_id")) is not None, description="声称建的工单,要能按 ID 查得到(不能只看格式对不对)", ), CompletionClaim( claim="email_sent", assertion=lambda: external["outbound_log_contains"](agent_output.get("message_id")), description="声称发的邮件,要能在发件日志里查到消息 ID", ), ] def evaluate(claims: list[CompletionClaim]) -> dict: """返回:能不能算完成、每条声明各自的证据状态。""" results = [] for c in claims: try: ok = bool(c.assertion()) except Exception: ok = False results.append({"claim": c.claim, "verified": ok, "requires": c.description}) verified = all(r["verified"] for r in results) return { "status": "completed" if verified else "incomplete", "evidence": results, # 关键:不通过的证据要能说清「差什么」,否则人工没法接手 "blocking": [r["claim"] for r in results if not r["verified"]], } |
注意最后那个 blocking 字段。它的作用是:当任务被判为未完成时,交接给人的不是一句「没做完」,而是「哪条声明拿不出证据」。
10. 团队与治理边界:人的岗位变了,不是消失了
前面九章基本都是技术。这一章讲人和流程——因为当模型可以改线上系统时,「谁负责」这个问题不能留白。
10.1 权限最小化:不同任务,不同身份
Perplexity 的部署里有一个被反复提及的架构细节:三类任务(写沟通材料、改软件、监控生产)用的是同一个模型,但需要的权限完全不同。
|
任务 |
出错后果 |
最低控制 |
|
起草沟通材料 |
信息过早或错误 |
发送前人工批准 |
|
修改软件 |
回归缺陷、安全缺陷 |
分支隔离、测试、合并需复核 |
|
监控生产 |
漏报事故或误报刷屏 |
明确的阈值与 on-call 责任人 |
|
端到端测试 |
测试覆盖不全带来虚假信心 |
已知的失败用例集与可追溯结果 |
关键的一句是:把这些任务放在同一个宽泛身份下,会让复核变难、并放大影响半径。一个需要读日志的监控智能体,和一个需要改代码的智能体,不该共用一套凭证。
10.2 三条车道:先观察,再演练,最后才生产
一个稳妥的试点路径是分层放权:
|
车道 |
允许的动作 |
升级条件 |
|
观察 |
读日志、指标、代码;提出诊断建议 |
告警准确率与漏报率达标 |
|
演练 |
在隔离副本里应用变更 |
变更成功率与回滚成功率达标 |
|
生产 |
真实变更,但受变更窗口与审批约束 |
持续监测 + 自动回滚就位 |
注意第二车道的价值:「在副本里改」和「在生产里改」之间的差距,大部分不是模型能力差距,而是环境差距。先在副本里把所有集成问题暴露完,再进生产,能避免很多低级事故。
10.3 职责分工:新岗位不是「审批员」
Data Today 对这个转变的描述很到位:人从审批门移到了架构层——从「点击批准」变成「设计那个决定是否需要批准的系统」。
具体来说,团队需要有人负责:
- 定义阈值:哪些指标算达标、哪些越线必须中止;
- 构建验证层:差分测试、后置条件断言、独立确认器;
- 设计回滚体系:哪些操作可逆、哪些可补偿、哪些必须事前拦;
- 管理事故复盘:把失败模式写回模型的上下文,形成闭环。
这也意味着一个不太舒服的结论:这个新岗位的工作量可能比它替代的那个更大。 但它也更有价值——因为它是「让自动化真正可用」的那个环节。
顺便说一句,这个方向还给出了一个反直觉的招聘建议:如果要为验证层招人,画像更接近 SRE,而不是传统开发。 因为核心能力是定义边界、设计观测、处理退化——这些正是 SRE 的日常。
10.4 合规与数据留存
最后一块拼图是合规。几个需要注意的点:
- 零数据留存(ZDR)对符合条件的 API 客户可用(需批准、受支持的端点)。但如前所述,用 ZDR 的组织无法使用 SDK Tracing,需要自建记录方案;
- OpenAI 表示正在测试 Private Safety Processing,目标是在保护客户隐私的前提下维持监控能力——这是一个值得关注的平衡点;
- 企业管理控制的默认值是关闭。这不是保守,而是承认「这类权限不该默认打开」。企业管理员应基于实际需要逐项开启,而不是一次性全开。
11. 上线前必须人为造出来的三种故障
这一节很短,但可能是整篇最实用的一节。
在预生产环境里,故意制造下面三种状态:
判断标准很简单:如果在这三种情况下都恢复不了,它在生产里也恢复不了。
反过来说,如果这三种都能过,你的长任务系统就已经比大多数演示项目更接近生产可用了。
12. 一套可以抄的最小落地方案
把前面几章拼起来,一个最小可用的长任务执行框架长这样:
|
Python |
|
# agent_runner.py —— 最小可用骨架(把前面各章拼起来) async def run_long_task(task_id: str, goal: str, *, approved_by: str | None = None): guard = HaltGuard(max_seconds=30 * 60, max_cost_usd=15.0, max_mutations=80) budget = BudgetGuard(exec_scope, task_scope, project_scope) checkpoint = await store.load(task_id) or new_checkpoint(task_id) while True: # ① 停机护栏 ok, reason = guard.check() if not ok: return await store.save(guard.safe_stop(reason, checkpoint)) # ② 预算熔断(判断权在模型之外) if not await budget.before_tool_call(estimate_next_cost()): return await store.save(checkpoint) # ③ 让模型产出下一步动作(带执行键) action = await call_astra( goal=goal, history=checkpoint.completed_steps, # 把「你必须给出可验证的证据」写进指令,而不是指望它自觉 instructions=( "每一步完成后,必须给出可被独立检查的证据(文件路径、ID、日志行)。" "如果某一步无法验证,明确说明「未验证」,不要声称已完成。" ), ) # ④ 不可逆操作必须人工批准 if action.is_irreversible and not approved_by: checkpoint.pending_approval = action return await store.save(checkpoint) # 挂起,等审批后恢复 # ⑤ 执行(带幂等键 + 四段式契约) result = await execute_with_contract(action, idempotency_key=f"{task_id}:{action.step_id}") # ⑥ 后置条件断言:完成必须是算出来的 gate = evaluate(build_gate(result, repo=repo_snapshot(), external=external_probes())) if gate["status"] != "completed": # 不通过就说清差什么,交给人,而不是自己重试 checkpoint.blocking_evidence = gate["blocking"] return await store.save(checkpoint) # ⑦ 记录 + 落盘检查点 await tracer.record(result, cost_usd=result.cost_usd) checkpoint.completed_steps.append(result.step_id) await store.save(checkpoint) if action.is_final: return checkpoint |
这套骨架只有七个环节,但每一个都对应前面的一章:①→第 4 章、②→第 6 章、③→第 9 章、④→第 8 章、⑤→第 5 章、⑥→第 9 章、⑦→第 7 章。
13. 结语:值得信任的不是不犯错的智能体
回到开头那个矛盾:官方说可以信任,社区说它在骗人。
这两个说法都对,因为它们说的其实是不同的事。官方说的是:在有验证层的前提下,模型产出足够好,可以把检查频率降下来。 社区说的是:如果只看它的文字总结,你会被误导。
所以生产里值得信任的,从来不是「不犯错的智能体」,而是失败时会停在已知状态、不会重复改同一个东西、守得住预算、说得清历史、退得回去的智能体。
这五条听起来一点都不性感,也上不了发布会的 PPT。但2026 年的现实是:模型能力已经跑得比大多数团队的运维能力更远了。现在真正的瓶颈不在模型,在「怎么把它安全地放进生产」这件事上。
而这个瓶颈,恰好是工程师能直接动手解决的那部分。
常见问题(FAQ)
下面六个问题是实际落地时被问得最多的,回答都尽量指向可以直接执行的做法,而不是停留在概念解释。
Q1:GPT-6 Astra 到底能不能在生产环境里长时间无人值守运行?
分场景。如果是读操作、草稿生成、隔离环境里的变更,可以,这类工作失败的影响面有限。如果是对外可见或不可逆的操作(发邮件、发布信息、直接改生产配置),目前仍然需要人工批准——这也是 OpenAI 官方确认策略(confirmation policies)的默认取向。官方客户案例里的「少检查」,指的是检查频率下降,不是控制消失。
Q2:官方说的「检查频率大幅降低」,具体降低了多少?
公开材料没有给出量化数字。Perplexity 的表述是定性的("much less frequently than previous generations"),没有基线间隔,也没有按任务类型拆分的比例。所以这个说法目前只能当作方向性信号,不能当作可复制的指标。
Q3:什么是「成败未知」状态,为什么不能归进「失败」?
「成败未知」指的是:请求已经发出,但调用方没收到确认(超时、断连)。此时副作用可能已经发生。如果把它归进「失败」并触发重试,就会重复执行——比如扣两次款、发两封邮件。正确做法是先向外部系统查证,再决定。
Q4:幂等键要放在哪里?
放在每一个会修改外部系统的工具调用上,由 task_id、step_id、operation_id 这类信息拼出唯一值,工具侧收到重复的执行键时直接返回首次结果。注意:这要求工具侧配合实现,所以工具契约设计时就要把这层考虑进去。
Q5:几百万 token 的上下文窗口,是不是意味着我可以把整个代码库塞进去?
不建议。上下文窗口大,不等于每次都该塞满——超过 27.2 万 token 输入的请求,整个请求会按更高倍率计费。而且长任务的成本是累积的,一次性的大输入会被后续每一步反复计费。更稳的做法是用检索 + 摘要控制每次实际进入上下文的信息量。
Q6:如果一定要给一个「第一步做什么」的建议?
先做幂等和检查点。 这两个是最小投入、最大收益的改动:它们把「重试」从一个危险动作变成安全动作,而不依赖任何模型能力的提升。至于虚假完成的检测、差分测试、自动回滚——那些可以排在后面。
相关资料


