空圈容错 · 鸿蒙 Agent 长任务链熔断与降级实战
一句话定位:本文是《空圈容错视角 — HarmonyOS 7 架构体检》 (母篇最新 V3.7,原基准 V3.4)的下钻篇。母篇看的是「整机架构真稳还是假兜底」, 本篇只钻一处——一条跨越多个 Skill 的 Agent 长任务链,断在半路时,系统到底有没有真冗余。
- 作者:空圈容错理论(Liaiyang66 + AI 协作)
- 版本:V2.0(终审定稿 · 全量吸收外部审稿两轮共六刀;V2.0 为校订版:编号自洽、§ 引用校正、代码围栏复位、特殊字符清理、内部计数校平,不改判据与结论)
- 理论框架:空圈 · 容错整合理论
(空圈五公理 / C·M·P 三层拓扑 / R1–R9 检测框架 / ∅_safe 安全空集 / 元公式 / 砍三刀)
📌 框架口径与跨篇映射说明
| 项 | 本篇口径 |
|---|---|
| R 编号 | 沿用母篇 V3.7 领域版:R1 前置缺失 / R4 范式绑架 / R6 假兜底 / R7 无异构通道 / R8 自指闭锁 / R9 跨层完美同一(本体论死锁)。本次列示 R4 / R8 / R6 / R7 / R9;R2 由 §3.2 的 R8 统一承载(并刀判据见 §3.2);R1 / R3 / R5 不在本次扫描列示 |
| CMP 三层(操作级重定义) | C = 主干底座(Agent 框架 + 分布式底座);M = 收容机制(小艺规划与调度);P = 冗余通道(熔断 / 重试 / 降级 / 端侧兜底) |
| 标记体系 | R 扫描用「强命中 / 半命中 / 弱命中 / 未命中」;CMP 操作级仅用「稳 / 漂移 / 弱」。§3.7 对照表「母篇(整机)」一列按母篇原档位「极稳 / 半稳(漂移)/ 弱」标注,仅作跨维对照,不参与本篇判定 |
| 颗粒度对齐 | 与系列《数据管道脚本实战》《轴承磨削工序实战》同档——对象都是「单次作业级的闭环」。母篇给管理层看,本篇给一线开发者看 |
| 口径迁移提示 | CMP 定义会随分析维度变化。跨系列阅读、或单独转发本篇时,请留意开篇这段口径说明 |
⚠️ 本篇的 CMP 是「单次作业 · 操作级」重定义,与母篇「整机维度」的档位名称相同、含义不同。 混用是本次扫描里最容易被误读的一处,请先记住这一句。
声明(边界)
本报告不提供代码级漏洞挖掘,不评价产品的商业成败,不代表华为官方结论。 文中的工程量级、阈值、参数均为方向级建议值,不是官方 SLA、也不是标准值,请按自己项目的实测分布调整。
本文讨论的是「应用侧如何在鸿蒙 Agent 框架之上自建一层兜底」: 不改动系统框架,也不假设系统框架已经提供这些能力。(这一条,请和 §2.2.1 的「能力落点矩阵」连起来读——五动作里,只有一部分是应用侧今晚就能自己上的。)
〇 定位:为什么只钻「长任务链」这一处
母篇对 R4(端云协同范式绑架)判了强命中,判词是: 「整个系统的智能表现,被端侧感知 + 云侧推理这条范式焊死。」
这句话在整机架构图上是一句判词;落到具体场景里,它有一个 最直接、最可验证、也最容易出真金白银损失的暴露面—— 一条跨越多个 Skill 的 Agent 长任务链。
理由有三:
- 它是端云绑架的放大器:短任务(问一句天气)云侧抖一下,用户重问一遍就行; 长任务(订票 + 订房 + 约车)链上有 N 个写操作,云侧抖一下, 代价可能是真金白银的重复扣款。
- 它是系统侧补不了的那一角:母篇给开发者的五条目标中, 「熔断与降级」排在第一位(针对 R4)——因为系统侧补的是进程级崩溃, 业务级半完成,只能应用侧自己兜。
- 它是可交付性最强的一处:一条链的容错机制,能被完整地写成 「裸奔版 vs 加固版」两段代码、一张成本账、一份六问清单——今晚就能拿自己的链试一遍。
所以本篇只做一件事: 把「这条链的容错」从一句架构判词,做成一份能落地的工程动作。
一、共同体验的那个瞬间:进度条 70%,还是绿的
这个剧本,是加固版上线之前会发生的事。下文每一刀,都是为了让它不再发生。
下午两点四十,你对着手机说: 「下周三我要去上海开会,帮我订一张上午到虹桥的机票,订一间会场附近的酒店,再约一辆接机车。」
小艺把这件事拆成了五步 —— 查航班 → 订票 → 查酒店 → 订房 → 约车。前三步跑得很顺,机票已经出票,酒店锁定。
第五步,你的手机从写字楼 Wi-Fi 走进了地下车库,切进弱网。 于是你看到:任务卡片停在「正在为您预约接机……」,进度条 70%,转圈,不动了。
你等了一分钟,不耐烦,点了重试。 晚上落地上海,你打开订单:
- 机票——两张;
- 酒店——两间;
- 接机车——一辆(这个倒是对了)。
这不是段子,这是「无幂等 + 无超时上限」在任何 Agent 系统上的必然结果。 母篇讲的「端云协同焊死」,在用户侧就长这样。
还有一种更隐蔽、也更坏的形态: 链其实在第四步就静默断了(云侧限流,子 Agent 返回了空结果但没报错), 可是界面最后弹了一个「任务已完成」。 因为你到机场排队办登机时才被告知——没有这张票。
「进度条全绿、任务显示完成、用户实际什么都没办成」——这就是本篇要拆的那一盏绿灯。
和《数据管道脚本实战》里的「跑批全绿、老板拿不到对的数据」、 和《轴承磨削工序实战》里的「套圈全绿、装机三个月却剥落」,是同一盏灯。
二、两条链摆一起:裸奔版 vs 加固版
2.1 裸奔版:线性直连链
```typescript
// ❌ 裸奔版:无锚
// TripIntent / Task / Step 为本篇业务伪类型,业务项目需自行补全类型定义
async function bookTrip(intent: TripIntent) {
const plan = await xiaoyi.plan(intent); // 云侧规划
for (const step of plan.steps) {
const res = await Agent.startAgent({ // 无显式超时上限
agentName: step.skill,
intent: step.args,
});
if (res.code !== 0) { // 只看返回码
toast('任务失败,请重试'); // 一把梭兜底
return; // 前 N 步副作用已经发生
}
}
toast('任务已完成'); // ← 绿灯幻觉的产地
这段代码里有五处空圈,全都不显眼:
| 位置 | 空圈 | 后果 |
|---|---|---|
startAgent 无超时 | 云侧限流时一直挂起 | 用户以为卡死,手动重试 |
| 无幂等键 | 重试即重发 | 重复下单、重复扣款 |
| 无检查点 | 断了就断在原地 | 只能从头再来,重复烧 token、重复扣款 |
| 循环走完就报「完成」 | 「200 / 没抛异常」就算成功 | 报「完成」,实际业务没成 |
| 资金 / 授权无硬闸门 | 模型判断即放行 | 触发即灾难级(R9) |
2.2 加固版:三道锚(V1.9 · 检查点为唯一事实源)
第一段:主循环代码
// ✅ 加固版:三道锚 + 检查点驱动状态机
// TripIntent / Task / Step / Checkpoint 均为本篇业务伪类型,需自行补全定义
const TIMEOUT_MS = 8000;
const BREAKER = new SlidingWindowBreaker({ window: 20, errorRate: 0.3 });
async function bookTripHardened(task: Task) {
// ★ 契约:链恢复时以检查点为唯一事实源,task 只是它的投影(见 §2.2.5 resume)
const restored = resume(task);
if (restored.stop) return restored.value;
task = restored.task;
for (const step of task.pending) { // ← 每轮都读「当前」task.pending
// ── 锚一:资金 / 授权硬闸门(只拦「未确认」,不拿版本号误杀)
assertPrecondition(step);
// ── 锚三:熔断(闭闸期零网络,半开探测才是真请求,见 §四)
if (BREAKER.isOpen(step.skill)) {
return degrade(task, step, '该能力当前不可用,已为您保留已完成部分');
}
// ── 锚二:本轮意图版本「有 intent 就复用,没有才生新」
// key 点:超时重试全程用同一把 idemKey,服务端去重才兜得住 ×2
const intentCp = checkpoint.latestIntent(task.id, step.id);
const thisVer = intentCp?.idemKey ?? bumpVersion(task.confirmVersion);
if (!step.confirmed) throw new ConfirmationRequired(step.id);
const idemKey = hash(task.id, step.id, thisVer);
let res;
try {
res = await withTimeout(
Agent.startAgent({
agentName: step.skill,
intent: { ...step.args, idemKey },
}),
TIMEOUT_MS
);
} catch (e) {
if (isTimeoutError(e)) {
BREAKER.record(step.skill, false); // 超时也是失败样本,喂给熔断
await checkpoint.save(makeIntentCp(task, step, thisVer)); // 先固化意图版本
return degrade(task, step, '云侧超时,操作可能已发出,请先确认');
// ↑ 绝不能说「已失败 / 已回滚」:远端可能票已出
}
throw e; // 非超时异常,上抛
}
// ── 锚一:调用后校验业务事实,而不是看返回码
const fact = await verifyFact(step, idemKey);
if (!fact.ok) {
BREAKER.record(step.skill, false);
await checkpoint.save(makeIntentCp(task, step, thisVer));
return degrade(task, step, '操作结果未确认,请先核对订单');
}
BREAKER.record(step.skill, true);
// ── 锚二 + 两阶段检查点:先落「意向态」→ 回读命中才升「已确认态」
// ★ 派生快照,立刻回写到 task,下一轮拿到的就是最新值(§2.2.5)
const done = [...task.done, step.id];
const pending = task.pending.slice(1);
await checkpoint.save({
taskId: task.id,
stepId: step.id,
state: 'confirmed', // 回读命中 → 才可续跑
done, pending,
idemKey: thisVer, // 把本轮键落盘,重试 / 续跑都读它
lastObs: fact.summary,
retry: 0,
confirmVersion: thisVer,
});
task.done = done; // ← V1.9 关键:推进投影
task.pending = pending; // ← 否则下一轮的 done/pending 用的是 stale 解构值
task.confirmVersion = thisVer;
}
await reconcile(task); // 独立对账,不靠自己汇报
toast('任务已完成');
}
// ──────────────────────────────────────────────
// 辅助:意图态检查点(不可续跑,只用于防重 + 待确认)
function makeIntentCp(task: Task, step: Step, thisVer: string): Checkpoint {
return {
taskId: task.id,
stepId: step.id,
state: 'intent', // 意向态:明确不可续跑
done: task.done,
pending: task.pending,
idemKey: thisVer,
lastObs: null,
retry: (task.retry ?? 0) + 1,
confirmVersion: thisVer,
};
}
第二段:闸门校验代码(放在 §2.2.1 里)
// ── 锚一:资金 / 授权硬闸门
function assertPrecondition(step: Step) {
if (step.sideEffect !== 'money' && step.sideEffect !== 'auth') return;
// 唯一硬拦:用户没确认
if (!step.confirmed) throw new ConfirmationRequired(step.id);
// 续跑一致性:检查点有上轮记录时,才要求「确认版本 === 上轮版本」
const last = checkpoint.lastConfirmed?.(step.id) ?? null;
if (last !== null && step.confirmVersion !== last) {
throw new ConfirmationRequired(step.id);
}
// ★ 注意:不在这里写 step.confirmVersion = thisVer
// 用户「确认版本」与本轮「意图版本」是两个东西,压平会吞掉确认语义(见 §2.2.1)
}
双轨表
| 版本 | 谁生成 | 谁消费 | 推进时机 | 一句话 |
|---|---|---|---|---|
用户确认版本 step.confirmVersion | 用户点确认时绑定 | 闸门(续跑一致性校验) | 用户重新确认时才换 | 管「这一刀敢不敢放」 |
本轮意图版本 thisVer | bumpVersion(首跑)/从 intent 检查点读回(重试) | 幂等键、检查点落盘、对账 | 回读命中后才写进 task.confirmVersion | 管「这把键落在哪、重试还认不认」 |
三条铁律
- 闸门只看「有没有确认 + 上轮对不对」,绝不因为版本号对不上就误杀首次发写。
idemKey = hash(taskId, stepId, thisVer)全程恒定:首跑生一次,超时重试从 intent 检查点读回同一个,不许retry一次bump一次——否则键漂移,服务端去重失效,照样 ×2。thisVer只在回读命中后才推进成task.confirmVersion;推进前它就是「待确认态的键」。
反例(不要这么做):
const thisVer = bumpVersion(task.confirmVersion);
step.confirmVersion = thisVer; // ❌ 把用户确认版悄悄换成系统刚生的号后果:断点续跑时
lastConfirmed是旧号、比对必炸,闸门误杀;幂等键与服务端订单的确认语义拧开,×2 更难查。
2.2.2 两阶段检查点:为什么「先落盘,再发写」在资金这一步要打折(V1.9 新增)
补一句边界,免得「先落检查点,再发起写操作」被照字面误用。
对可逆 / 可本地化的副作用,检查点可以「先落已确认态、再发写」,断点续跑直接跳过已做步骤。
但资金 / 授权是不可逆写操作,物理上做不到「先落一个已确认的检查点、再出票」—— 票还没出、座位还没锁,那个「已确认」是假的,断网重启后会误以为票已出、再发一次,正好造 ×2。
所以资金这一步的检查点必须是两阶段:
意向态 intent ──发写──▶ 回读命中 ──▶ 已确认态 confirmed(可续跑,推进 thisVer)
↑ 只防重放 / 存键 ↑ 此刻才知道「到底成没成」
不可续跑 下一轮才敢跳过这步
- 能「先落」的只有意向态:状态=准备中,明确不可续跑,只用来固化
idemKey、挡重复提交; - 可续跑的已确认态:必须等
verifyFact命中后才落; - 把两阶段混成一个
save,要么续跑造 ×2,要么不敢续——这是 R8 最阴的接缝,别省那一行状态。
2.2.3 为什么「超时不能报失败」:超时 ≠ 远端没执行
withTimeout 只代表调用方放弃等待,不代表云侧把操作撤销了。 网络断在「已发出、回包丢了」那一瞬,票可能早就出了。
所以超时只有一个合法终点:degrade 的待确认态。
// ✅ 合法文案
degrade(task, step, '云侧超时,操作可能已发出,请先确认订单');
// ❌ 绝对不能写
degrade(task, step, '操作失败,已为您回滚'); // 你在撒谎,且会造 R9
// confirmWhat 让降档可解释到「确认什么」这一层(不是一句黑话):
function degrade(task: Task, step: Step, reason: string) {
const local = runLocalFallback(step); // 无副作用的端侧兜底:缓存查询 / 本地比价 / 待办草稿
return notifyUser({
status: 'DEGRADED',
done: task.done, // 哪些成了
blocked: step.id, // 卡在哪
reason, // 为什么
// ★ 降档可解释的最低标准:不是「需要你确认」,而是「确认什么」
confirmWhat: step.sideEffect === 'money' || step.sideEffect === 'auth'
? `请确认是否${step.label}` // 例:请确认是否出票·上海→虹桥 08:15
: undefined, // 非敏感降档不必弹确认
local, // 端侧能先给你的
needConfirm: step.sideEffect === 'money' || step.sideEffect === 'auth',
});
}
2.2.4 能力落点矩阵(★今晚能不能「上手」,看这张表)
声明里说过「不假设框架已经提供这些能力」。 那
reconcile、checkpoint、idemKey到底谁给?这是全稿最容易被拆的一问,摊开回答。
| 加固能力 | 应用侧自己能上的 | 必须靠框架 / 合作方给的口 | 拿不到时的退路 |
|---|---|---|---|
| 幂等去重 | 客户端生成 idemKey + 本地去重表 + 键恒定(§2.2.1) | 服务端须认这个键并做唯一约束 | 出账前回读订单唯一性,命中重复即拒绝 |
| 检查点 | 本地 KV / Preferences 落盘(两阶段,§2.2.2) | 跨端续传要云同步 | 意向态只防重、不续跑;丢一步标『待确认』,不报完成 |
| 熔断 | 进程内滑动窗口计数,闭闸期零网络、零依赖 ✅ | — | 五动作里闭闸期零依赖的两把之一;半开探测仍是真请求 |
| 调用后回读 | 端侧发起只读查询 | 要有可读的服务端查询口 | 查不到 → 走降级,不当成成功 |
独立对账 reconcile | 换一条路径 / 换一套凭据再问一次 | 需另一个只读视图或审计账 | 没有第二双眼睛 → 最多给『已提交,待确认』,永远别说『已完成』 |
| 端侧降档 | 端侧本地算:缓存查询、本地比价 ✅(无副作用) | 不扣钱、不发写、不产生副作用 | 断网也能把「我替你记着」交出去 |
| 硬闸门 | 端侧 UI 确认组件,零依赖 ✅ | 确认事件须进检查点 | 没确认 → 连发写都不许进 |
结论(咬死): 严格零依赖(连网络都不要)的有两把——熔断、硬闸门;断网仍可交付的还有一类——端侧降档(缓存查询、本地比价、待办草稿,不花钱、不发写)。 其余幂等、回读、对账都卡在「服务端 / 框架给不给口」上。
把这事说清楚,不是削弱本文,恰恰是它最诚实的地方—— 因为 R8 的解药从来不是「写得更漂亮」,是「第二个观测者到底是谁」。 不说清,reconcile 就会退化成跟 toast('任务已完成') 一样的自指:自己证明自己办成了。
2.2.5 续跑契约:检查点是事实源,task 是投影(V1.9 新增)
// 进程重启 / 链恢复:以检查点为唯一事实源,task 只是它的投影
// ★ 谁先写谁说了算;绝不能「检查点一份、闭包变量一份」各记各的
function resume(task: Task): { stop: true; value: any } | { stop: false; task: Task } {
const cp = checkpoint.latest(task.id);
if (!cp) return { stop: false, task }; // 首跑
if (cp.state === 'confirmed') { // 已确认:投影可追平,正常续跑
return {
stop: false,
task: {
...task,
done: cp.done,
pending: cp.pending,
confirmVersion: cp.confirmVersion
}
};
}
// intent 态:远端状态未知 → 绝不自动续跑发写,交给用户确认
return {
stop: true,
value: degrade(
{ ...task, done: cp.done, pending: cp.pending },
{ id: cp.stepId } as Step,
'上一次操作可能已发出,请先确认后再继续'
)
};
}
点题:检查点落盘之后,
task.done / task.pending必须立刻从同一份派生快照回写(§2.2 主循环那两行)。 漏这一步,下一轮for (const step of task.pending)拿到的是 stale 解构值, 检查点里done=[A]、循环里还当 A 没做 → 续跑重发,×2 照旧发生。这就是 V1.9 补上的最后一毫米。
三、探针扫描与因果链
3.1 R4 范式绑架 —— 强命中
操作级判据:这条链有没有一步是「云侧不可用时也能走完」的?没有。
链的「业务写操作」,没有一步是云侧不可用还能推进的——出票、扣款、锁库存全得等云侧。 唯一例外是熔断这一刀:它不救活写操作,只在闭闸期给链一个 「不再傻等、不再重发」的边界,把无限挂起砍成有终点的降档。
这说明 R4 的解药不在「让写操作离线成」(做不到), 而在「给失败一个可控的形状」——正对应 §5.1 第三刀「换一条路」。
3.2 R8 自指闭锁 —— 整机半命中,操作级强命中
裸奔版最后那句 toast('任务已完成'),凭据是「循环正常退出了」; 而循环正常退出的前提,是每一步都「没抛异常」——异常早被兜底分支吞掉了。 用执行方自己的状态证明任务完成,闭环里没有第二个观测者。
▸ R2 → R8 的并刀判据(本篇仅此一处合并)R2「独立异构通道缺失」与本篇 R8「自指闭锁」在操作级坍缩成同一条接缝: 校验与转换同源的唯一暴露面,就是「完成度靠执行方自己报」。 故 R2 不单列,归 R8 承载。这是颗粒度裁断,不是改名: 一旦出现「异构对账通道客观存在、却仍读同一份快照」的场景,须把 R2 拆回单列,不可继续借 R8 的壳。
3.3 R6 假兜底 —— 整机弱命中,操作级强命中
整条链只有一个兜底——最后弹一句「任务失败」。这不是兜底,是把问题推回给用户。 兜底是分级还是一把梭?一把梭 → 强命中。 链越长,这把梭越钝。
3.4 R7 无异构通道 —— 半命中
云段 / 端段怎么切,只有一条判据——这一步的副作用是否「可回滚、可本地化」:
- 可落端侧:查(缓存的航班列表)、比(本地比价)、提醒、生成待办、填表单草稿——断网也值钱;
- 必须留云侧:出票、扣款、锁库存、改订单——没网就别装成完成,进『待确认』;
- 切错了才是事故:把「扣款」降级到端侧假装成功,那是造 R8,不是容错。
degrade() 里的 runLocalFallback 就是这条线的入口,它兜的只能是「无副作用、能本地化」的那一半。
3.5 R9 跨层完美同一 —— 操作级弱命中(现状)
资金 / 授权动作,若指望「模型足够聪明就不会错放」,等于用有限模型覆盖无限异常。 加固版的解药,正是把这一步换成可信 UI 硬闸门(§2.2 的 assertPrecondition):
- 闸门立住 + 回读命中 = 真隔离;
- 卡在等确认 = 『待确认』态,绝不标『已完成』(六问第 5 问的通过条件)。
一旦触发,就是灾难级。所以本篇的验收第 5 问不设「凑合过」,只认这两档。
3.6 因果链:这盏绿灯是怎么亮起来的
云侧受限 / 网络抖动
↓ 无超时上限 → 傻等
用户手动重试 / 系统自动重试
↓ 无幂等键 → 写操作重复生效
订单 ×2、扣款 ×2
↓ 无检查点 → 断点无法续
只能从头重跑(再烧一遍 token、再扣一次款)
↓ 异常被兜底分支吞掉
返回码为 0、循环正常退出
↓ 无独立对账 → 报「任务已完成」
用户到现场才发现:什么都没有
↑ 最阴的一刀在这条线之外:
[出票/扣款若没过资金闸门,前面四把刀全装上也会 ×2]
→ 资金类副作用必须单拎:先确认、后发写、确认版本进检查点(§2.2.1)
↑ V1.9 补上的三跳(都在代码里,不是注释):
[超时未捕获 → 崩出链外] 没 catch 时 degrade 根本走不到,§2.2.3
[检查点非事实源 → 续跑重发] task 没回写时 done 丢步,§2.2.5
[idemKey 漂移 → 去重失效] retry 重新 bump 时键变了,§2.2.1 铁律 2
3.7 CMP 三层在操作级的健康度
| 层 | 母篇(整机)【整机维度档位】 | 本篇(单条链) | 说明 |
|---|---|---|---|
| C 主干底座 | 极稳 | 稳 | Agent 框架、分布式底座不背这个锅 |
| M 收容机制 | 半稳(漂移) | 漂移 | 规划与调度全依赖云侧与场景分布 |
| P 冗余通道 | 弱 | 弱 | 单条链上几乎没有真冗余,只有最后一句报错;补 P 靠 §2.2.4 的能力归属 |
结论:链断,断不在底座,断在「没人给这条链修冗余」; 而修冗余的第一问不是「写什么代码」,是「第二个观测者谁给」。
四、灵魂拷问:加这些动作,慢了多少、贵了多少
这是最容易被打回的一问。裸奔版唯一的优势就是快——多一层锚是不是就把体验拖垮了?
| 加固动作 | 额外开销 | 量级 | 换来什么 | 弱网下的真实代价 |
|---|---|---|---|---|
| 幂等键 | 一次散列 + 入参 + 服务端去重查一次 | 链路正常 < 10ms | 不重复下单;键恒定才兜得住超时重试 | 服务端不可达 → 退化『待确认』,不扣 |
| 检查点 | 每步结构化写(两阶段) | 单步 < 50ms | 断点能续、且续的是真·已完成 | 本地写稳、云同步可迟;先落意向、回读后才确认 |
| 熔断滑动窗口 | 内存计数,闭闸期零网络(严格零依赖) | 忽略 | 不傻等到超时 | ⚠️ 半开探测那一下是真请求——别把「零依赖」宣传成「零开销」 |
| 调用后回读校验 | 一次只读查询 | 正常 < 100ms | 抓到「200 但业务没成」 | 弱网下 3–8s 甚至超时;它治的是「别扣第二次」,不是提速 |
| 超时捕获 | 一次 try/catch + 一次意向检查点 | 可忽略 | 崩出链外 → 转待确认 | 没有这行,前面所有兜底在超时那一刻集体失效 |
| 合计(10 步链) | — | 正常约 0.5–1 秒 | 避免一次重复支付 / 断链重跑 | 弱网里最慢的是回读,价值是上限可控 |
量级判断:一条 10 步 Agent 链的总耗时通常在数秒到数十秒量级 (含多次云侧推理与外部写操作)。链路正常时,0.5–1 秒的加固成本占比通常不到几个百分点。
而一次重复支付的代价:退款流程的时间成本 + 一次真实的资损 + 一次对产品信任的消耗。 这笔账的分子分母,不在一个量级上。
一句话把账说正:上表是链路正常时的增量开销;弱网才是这套动作的战场。 那时回读与半开探测本身就是最长路径。 所以别拿它当「性能优化」卖——它换的不是延迟更低,是别再悄悄地扣第二次,是上限可控。谁拿「弱网更慢了」来杠,杠不动这一句。
五、砍三刀 + 四阶段落地
5.1 砍三刀
第一刀(砍成功率数字): 「复杂任务成功率超 90%」是全局平均。分母里的失败若有一次是支付错账或授权外溢,这个 9 分就不算 9 分。
第二刀(砍「系统会自动兜底」): 系统兜的是进程级崩溃(杀后台、回收内存、重启);它兜不了业务级半完成(钱扣了、单没出)。 把「进程还活着」当成「任务没丢」,是典型的跨层幻觉。
第三刀(砍「重试就是容错」): 没有幂等的重试,是同通道空转——越重试越错。 重试是「在原来的路上再走一次」,容错是「换一条路,或者明确降档」。 真换路,先问 §2.2.4 那张表——路在谁手里。 而换路的第一条纪律是 §2.2.1 铁律 2:重试不许换键。
5.2 四阶段落地(时间窗 / 强依赖 / 诚实补丁)
| 阶段 | 时间窗 | 核心动作 | 强依赖 | 诚实补丁 |
|---|---|---|---|---|
| 一 · 观测 | 0–1 个月 | 给每步打点:stepId、耗时、结果、是否重试、是否走 degrade | — | 打点有开销与隐私面,只记结构化元数据,不记用户内容 |
| 二 · 断点 | 1–3 个月 | 幂等键(键恒定) + 两阶段检查点 | 服务端须支持幂等去重 | 幂等接口常在合作方手里;对方没有,退而用「操作账本 + 出账前回读」。本地检查点会丢最近一步,须「先落意向、回读后才确认」 |
| 三 · 熔断 | 3–6 个月 | 超时上限 + 滑动窗口 + 半开探测 | 阶段一的耗时 / 失败分布 | 阈值太松没用、太紧误伤正常链,初版宁晚熔,按数据再调;半开探测有真实请求代价 |
| 四 · 降档 | 6 个月起 | 把 ∅_safe 从「崩溃兜底」升级成「优雅降档的产品承诺」 | 前三阶段 + 端侧降档产品化 | 降档必须能解释「哪步成了、哪步没成、需要你确认什么」(§2.2.3 的 confirmWhat);没有独立对账就只给『待确认』 |
四阶段强依赖,不能跳: 没有阶段一的分布,阶段三的阈值就是拍脑袋; 没有阶段二的检查点(且 task 跟着回写,§2.2.5),阶段四的降档就没有「已完成清单」可交付; 而阶段二的命脉,是服务端给不给那个幂等口(§2.2.4)——给不了,就别吹「已兜底」。
六、可证伪验收清单(六问)
这六问,今晚就能拿自己的链挨个问一遍。 任何一问答「会」或「不知道」,那条链就还亮着绿灯幻觉。
- 幂等:某个写操作超时后重试,会不会产生第二笔真实订单? → 必须答「不会」:idemKey 从 intent 检查点读回同一个,服务端去重生效。(§2.2.1 铁律 2)
- 独立观测:你的「任务完成」是执行方自己汇报的, 还是有一个独立对账维度? → 再追一句:那双眼睛谁给? 没有 → 最多『已提交,待确认』,别说『已完成』。(§2.2.4)
- 熔断:云侧限流时,这条链是傻等到超时,还是有明确的熔断阈值? → 且超时路径必须进
degrade,不能靠catch漏掉崩出去。(§2.2.3) - 断点:链断在第 5 步,恢复后是从第 5 步续,还是「从头再说一遍」? → 以检查点为事实源(
resume,§2.2.5);若停在 intent 态,宁可拦住问用户,也不自动续发写。 - 硬闸门(三态,不是过 / 不过):
- 模型判断放行 → 没过;
- 用户已确认 + 回读命中 → 真隔离;
- 卡在等确认 → 『待确认』态,不算过。 → 闸门只拦「没确认」,别拿版本号误杀首次。(§2.2.1)
- 降档可解释:降级发生时,你告诉用户的是「失败了」, 还是「哪几步成了、哪步没成、需要你确认什么」? → 必须有
confirmWhat字段,不能只给needConfirm: true。(§2.2.3)
七、诚实清单与边界重申(本篇自己的空圈)
- 本文阈值、超时、窗口参数都是建议初值,不是标准值,也不是华为官方口径;
- ∅_safe 是本框架内的定义(安全降级状态),不是行业通用术语——对外沟通须先解释;
- 「合计 0.5–1 秒」「占比不到几个百分点」是链路正常时的工程量级判断, 不是实测数据;弱网下回读本身即可能成为最长路径(§四),请以自己链上的打点分布为准;
- 严格零依赖(连网络都不要)的是「熔断 + 硬闸门」; 断网仍可交付的是「端侧降档(无副作用)」; 幂等、回读、对账仍依赖服务端 / 框架给口,拿不到就按 §2.2.4 的退路走, 别把『待确认』包装成『已完成』;
- 本文只给「应用侧如何在框架之上自建兜底」,不涉及框架内部实现, 也不假设系统已经提供这些能力;
- 本文写作时点为 2026-11,数据以 HDC 2026 及最新公开信源为准;
- 尚未运行时验证的项(本篇自己的空圈,不藏):
- 版本双轨流转(
thisVer首跑生、重试读回、回读后推进)尚未在鸿蒙运行时跑通验证; - 超时 →
degrade分支(含resume对 intent 态的拦停)同上; - 服务端是否真正认
idemKey并做唯一约束,是整条链能否防 ×2 的地基——本文无法替合作方签字。
- 版本双轨流转(
边界:不提供代码级漏洞挖掘,不评价产品商业成败,不代表华为官方结论。 母篇的判词「收容做得很漂亮,容错才是深水区」在这里依然成立—— 本篇是把「深水区」里的一处,写成能下手的施工图。
八、本篇的相对位置(增量归属)
现象层(行业已有):熔断、幂等键、检查点、退避、半开探测、 outbox/inbox 事件溯源的输出箱 / 输入箱模式——这些是成熟工程实践; 母篇点出的「端云绑架」也是公开讨论。本篇不抢这些功劳。
本篇的框架层增量:
- 把母篇的架构判词下沉成一条链上的可检查动作—— 五处空圈(超时 / 幂等 / 检查点 / 完成判定 / 资金闸门) 逐一定位,并标清能力归属(§2.2.4);
- R8 的「层级敏感」发现:同一个 R8 自指闭锁, 在整机层面判「半命中」,下沉到单条链的操作层面升格为强命中—— 因为链上没有任何外部校验点,而那个「外部点」往往不在应用侧手里;
- 版本双轨(V1.9):把「用户确认版本」与「本轮意图版本」拆成两条线, 并给出
idemKey跨超时重试恒定的铁律——这是把「防重放」从口号变成可验证契约; - 检查点即事实源(V1.9):
task只是投影,续跑从检查点重建, intent 态绝不可自动续发写——堵死「续跑重发」这条最阴的缝; - 可证伪六问:把「这条链有没有真冗余」变成六个能当场自测的问题, 第 2 问追「第二双眼睛谁给」,第 5 问走三态,第 6 问落到
confirmWhat字段。
仍未验证:这六问尚未在真实第三方团队上做过独立复现; 落地效果也没有真实企业的硬指标数据。 它证明了这套动作「可以用」,还没证明它「一定管用」—— 尤其是 §2.2.4 里那四个「等口」的动作,能不能落地,取决于生态给不给路。
附录 A · 母篇 → 本篇的映射
| 母篇要素 | 本篇对应 |
|---|---|
| R4 端云协同范式绑架(强命中) | §3.1 链的可靠性 = 云侧可用性 × 网络稳定性;解药是「给失败可控形状」 |
| R8 自指闭锁(整机:半命中) | §3.2 操作级升为强命中(链上无外部校验点);含 R2 并刀判据 |
| R6 假兜底(整机:弱命中) | §3.3 操作级升为强命中(一把梭兜底) |
| R7 无异构通道(整机:弱命中) | §3.4 无「云段 + 端段」拆分设计,附切分判据 |
| R9 本体论死锁(整机:强命中 | 操作级:弱命中·现状) | §3.5 资金 / 授权硬闸门缺失;闸门立住 + 回读命中即转「已隔离」 |
| 开发者目标一:长任务链熔断与降级 | §2.2 三道锚 + §2.2.4 能力落点 + §5.2 四阶段 |
| ∅_safe 安全空集 | §2.2.3 degrade() 优雅降档(含 confirmWhat) |
| 元公式 D_max 判词 | §5.1 第一刀:平均值代替尾部 |
| 砍三刀 | §5.1 |
| —— V1.9 新增 —— | §2.2.1 版本双轨 | §2.2.2 两阶段检查点 | §2.2.5 检查点即事实源 |
注:R9 一行按「整机 / 操作级」双档标注,避免跨篇读者忽略层级差异。
附录 B · 事实与数据来源
- HarmonyOS 7 与 Agent 架构:华为开发者大会 HDC 2026(2026-06-12,东莞松山湖), 余承东发布 HarmonyOS 7,三大核心升级为 Agent 亲和系统架构、 鸿蒙智能体框架 2.0、系统智能体小艺 (百度百科「HarmonyOS 7」「Agent 亲和系统架构」等公开条目)。
- 复杂任务成功率超 90%:鸿蒙智能体框架 2.0 公开口径(同上)。
- 小艺 2100+ 系统能力、500+ 伙伴精选 Skill、2000+ 鸿蒙智能体: HDC 2026 现场报道(雷科技等公开报道)。
- SDK 26:100+ Kit、8 万 + API:华为开发者官网《HarmonyOS 7 (API 26) 新能力一览》。
- Agent 开发范式(@Agent / AgentSession / A2A): 公开技术教程(CSDN,2026-08),含鸿蒙 AgentSession 用法及 A2A 等业界协议对照, 本文仅作范式参照,不以此给鸿蒙能力背书。
- 超丝滑方舟引擎(性能大模型,启动 +22%、跳转 +25%):HDC 2026 公开数据。
- 星河互联架构 250MB/s、分布式软总线 8ms 级连接:公开资料与母篇。
- 星盾安全(信通院增强级端云协同 AI 安全认证;AI 风险识别「卓越级」):公开资料。
- Agent 长任务链工程实践(状态管理 / 幂等 / 检查点 / 熔断降级):公开技术文章(2026-09 至 2026-11,多篇工程复盘)。 本文的工程参数建议取自其共性做法,数值为本文建议初值,非其原始值。
以上数据对应的原始链接与最新口径,请以华为官方、信通院最新发布为准。 本篇未对鸿蒙系统的内部兜底实现做任何断言。
附录 C · 审稿记录(本篇不藏刀)
| 轮次 | 来源 | 发现 | 处理 | 落点 |
|---|---|---|---|---|
| 外部审稿 · 第一刀 | 同行挑刺 | assertPrecondition 首轮 currentVer=null 误杀(死循环) | 采纳,改写为「唯一硬拦 = 未确认;续跑才比上轮版本」 | §2.2.1 |
| 外部审稿 · 第二刀 | 同行挑刺 | withTimeout 无 try/catch,超时崩出链外,degrade 走不到 | 采纳,补 catch + 喂熔断 + 先落 intent 检查点 | §2.2.3 |
| 外部审稿 · 第三刀 | 同行挑刺 | 指「幂等键与闸门版本不同步」 | 诊断采纳,修复驳回:压平成一个数会吞确认语义、复活首轮误杀;改为版本双轨 | §2.2.1 |
| V1.9 · 第一刀 | 自审 | task.done / task.pending 未从检查点快照回写,续跑丢步重发 | 采纳,派生即回写 + resume() 以检查点为事实源 | §2.2.5 |
| V1.9 · 第二刀 | 自审 | 超时重试 bumpVersion 导致 idemKey 漂移,服务端去重失效 | 采纳,thisVer 从 intent 检查点读回、仅回读后推进 | §2.2.1 铁律 2 |
| V1.9 · 第三刀 | 自审 | §2.2.2 引言重复两遍 | 已清理 | §2.2.2 |
原则:审稿人也得被审稿。第三刀的方向是对的(接缝真实存在),但它的修复会把第一刀刚修好的东西拆回去—— 所以本篇记录的是「哪处是真洞、哪处是误判、为什么拒」,而不是「谁挑的都照单全收」。 这本身,就是空圈自反在这一稿里的落点。
────────────────────────────────────────
更多推荐



所有评论(0)