- Published on
数据过期后,Agent 应该怎样刷新,而不是反复重试?
- Authors

- Name
- hpoenixf
数据过期后,Agent 应该怎样刷新,而不是反复重试?
本文回看一次受控刷新:如何在保留来源拒绝和只读边界的前提下,生成、去重并原子发布一份新证据。
本文复述的是 2026-09-04 的受控刷新窗口,属于特定跨 root/app 方案的工程记录;不代表当前所有 Research Runtime 路径都采用同一刷新机制。
一、来源被拒后,普通重试不会造出新证据
来源拒绝后,问题不在于怎样把不可用文案说得更委婉,而在于怎样取得一份新的、经过批准的证据。对同一个未批准来源重复调用,不会改变来源身份;沿用旧快照,也不能把旧事实变成当前事实。
这次机制补上的正是安全刷新路径:使用固定操作、限制数据 scope、按 scope/fingerprint/时间窗去重,并且只有验证通过后才原子发布。来源仍被拒绝或刷新失败时,系统继续保留不可用状态。
在一次受控验证窗口内观察到:产物、工具、流式、持久化、重放、回读六个环节中,离线分组通过四比四,在线分组通过七比七,六类 fail-closed 行为全部按预期关闭,而非放行。
该结论仅描述该窗口的观测结果,不做普遍保证。换窗口、换环境、换负载后需重新采集,不可直接外推。
这些数字只对应该次受控验证窗口,不能外推成当前生产能力;细节见文末的历史记录。
二、两个核心概念
2.1 来源可信度:答案从哪里来
来源可信度回答的是:这个数字或结论,到底来自哪条可信通道。不是“模型觉得合理”,而是“哪份受信数据、在什么时间、以什么口径给出”。
可信通道通常同时满足三点。第一,职责明确,只读的行情与持仓通道不混入推测。第二,时效明确,新鲜或过期都有标记,过期数据不冒充现状。第三,可回查,每条关键断言能关联到具体证据,而不是孤立文本。
在这个模型里,模型文本默认是建议性的、不可信的。只有经过 allowlist 的受信工具输出,才能为数值与状态背书。文本再流畅,也不能替代这条链路。
这意味着评审重点不在措辞,而在链路。先问来源是否在名单,再问口径是否一致,最后才看表达是否清晰。顺序一反,质量观就歪了。
口径一致同样关键。同一指标可能有不同统计口径,不注明口径的数字即使来源受信,也无法直接用于决策。来源、时间、口径三者缺一不可。
2.2 不可用:刷新流程的起点
不可用表示当前证据不能支撑目标结论。它应保留目标和缺口,但不自动触发任意检索;只有通过明确授权的刷新入口,才可能生成新证据。
三、主流程:拒绝、不可用、受控刷新
下面是简化后的主流程。它有三条关键分支:来源拒绝、状态降级为不可用、受控刷新后恢复。另有一条显式未采集分支,见图例说明。
3.1 先定义五个机制词:它们在图中何处生效
| 机制词 | 定义 | 在主流程中的挂载点 |
|---|---|---|
| 候选限定 | 仅接受根层候选,不接受旁路写入 | 受控刷新入口:非候选直接拒绝 |
| 固定操作 | 变更动作类型固定且受限,不支持任意写入 | 刷新执行中:动作越界即失败 |
| 规范原子发布 | 要么整体可见,要么整体不可见,不存在半可见 | 刷新收尾:发布失败则整体回退 |
| 去重 | 按范围、指纹、小时窗口去重 | 发布前检查:命中即复用,不重复做 |
| 同会话精确一次 | 同一会话内连续处理精确一次,不多做不少做 | 去重通过后:保证会话内单次生效 |
graph TD
RootCandidate --> FixedOperations
FixedOperations --> AtomicPublish
AtomicPublish --> Deduplicate
Deduplicate --> ExactlyOnce
主图中的受控刷新节点,即展开为上面这条子链。任何一环不满足,刷新即按失败处理,回到不可用并记审计。
graph TD
UserQuestion --> ParseEvidenceNeed
ParseEvidenceNeed --> CheckSourceAllowlist
CheckSourceAllowlist --> RejectUntrustedSource
RejectUntrustedSource --> ReportUnavailable
CheckSourceAllowlist --> CheckEvidenceFreshness
CheckEvidenceFreshness --> ReportUnavailable
CheckEvidenceFreshness --> PublishCitedAnswer
ReportUnavailable --> CheckRefreshAllowed
CheckRefreshAllowed --> RemainUnavailable
CheckRefreshAllowed --> ControlledRefresh
ControlledRefresh --> CheckRefreshResult
CheckRefreshResult --> PublishCitedAnswer
CheckRefreshResult --> RemainUnavailable
ControlledRefresh --> MarkNotCollected
MarkNotCollected --> RemainUnavailable
关系或判断条件:
- CheckSourceAllowlist → RejectUntrustedSource:否:未命中。
- CheckSourceAllowlist → CheckEvidenceFreshness:是:命中。
- CheckEvidenceFreshness → ReportUnavailable:过期或缺失。
- CheckEvidenceFreshness → PublishCitedAnswer:新鲜且完整。
- CheckRefreshAllowed → RemainUnavailable:否:普通问答不隐式触发。
- CheckRefreshAllowed → ControlledRefresh:是:显式重试或定时采集且小时窗未超限。
- CheckRefreshResult → PublishCitedAnswer:成功。
- CheckRefreshResult → RemainUnavailable:失败。
- ControlledRefresh → MarkNotCollected:需真实环境或超出受控窗。
这个流程的关键在于分叉点都很显式。拒绝发生在证据进入之前,不可用发生在回答生成之前,刷新发生在独立受控通道中。
任何一条分支都不会悄悄绕过。不可信标签进不来,过期数据顶不上去,刷新动作也不可混入普通问答路径。
图例补充:虚线分支为显式未采集出口。当刷新需要真实库、多进程或外部网关质量等受控窗外条件时,不硬判成功或失败,而是标记未采集并保持不可用。
触发规则只有两类合法来源:用户显式重试,以及定时采集任务。两者共享同一小时窗口限频。普通问答永不隐式触发刷新。
四、一次来源拒绝的教训
在早期联调中,出现过一个看似无害的内部来源标签,例如某类内部便签标签。它来自系统内部注释,本意是方便调试,却被误当成可引用的证据来源。
用结构化字段表达就是:source_label 为该内部标签,allowlisted 为假,因此 trusted_hit 按定义为假。allowlist 机制直接拒绝了它,相关状态声明统一降级为不可用。
这个拒绝看起来“过于严格”,却避免了一类系统性风险:内部便签、调试备注、模型转述,披着来源的外衣进入答案。宁可当时多几次不可用,也不能开这个口子。
事后看,严格反而加快了进度。因为边界清晰,联调时不用争论个案该不该放行,只需核对是否命中名单。争议从主观判断变成了客观检查。
与之对应的提案从零开始推进。到评审时累计形成二十五个任务项,逐项落实来源边界、降级文案、审计字段,而不是先上线再补。
任务拆细还有一个好处:每个任务都有可验收的证据。来源、降级、刷新、审计各自独立验证,避免把所有风险堆到最后一次集成时爆发。
五、从输入到结构化输出
为了让不可用可被机器处理,输入与输出都做了结构化约束。下面是教学级示意,不含任何真实数据。
{
"input": {
"question": "当前组合状态如何?",
"need_numbers": true,
"need_advice": false
},
"evidence": {
"source_label": "internal-note",
"allowlisted": false,
"trusted_hit": false,
"freshness": "unknown",
"source_id": null
},
"output": {
"status": "unavailable",
"numbers": [],
"disclosure": "当前无可用受信证据,未做状态断言。",
"next": "可触发一次受控刷新后重试",
"audit": {
"action_tendency": "none",
"execution_allowed": false
}
}
}
其中 trusted_hit 的定义是:当且仅当 source_label 非空且 allowlisted 为真,trusted_hit 才为真。它是派生字段,不可手填。need_numbers 和 need_advice 是这份历史示意中的类型化输入,不表示 Runtime 可以用规则识别自由文本意图。
伪代码只有四步,优先级固定不变:
trusted_hit = allowlisted and source_label in allowlist
if not trusted_hit: return unavailable
if 证据缺失或过期: return unavailable
if 数值无受信根:数字留空 + 固定披露
else: 生成带引用回答 + 审计落盘
上例中 source_label 为内部便签类标签,allowlisted 为假,故 trusted_hit 为假,直接走来源拒绝分支。
输入侧只关心问题是否需要数值与操作倾向。输出侧永远带状态、披露、下一步与审计。动作倾向在不可用路径下恒为无,不产生任何执行暗示。
这种约束让前端很好处理:有数字就展示数字与引用,无数字就展示披露与刷新入口,不存在半吊子文本需要猜测。
对排查同样友好。线上问题只需看结构化字段:来源是否命中、新鲜度为何值、动作为何清零。几秒即可定位是证据问题还是生成问题。
对审计更有价值。每次回答都留下相同的字段结构,事后可以批量统计不可用比例、刷新成功率、降级原因分布。质量从感觉变成了数字。
六、运行规则:输入、优先级、失败与安全
下图是运行期的操作规则摘要。阅读顺序即执行优先级,越靠前越先判断,不可跳步。
| 维度 | 规则摘要 |
|---|---|
| 输入一:问题类型 | 是否涉及数值与状态;涉及则强制走证据门 |
| 输入二:证据命中 | 仅名单内通道算命中,其余视为未命中 |
| 输入三:新鲜度 | 新鲜可用,过期未知缺失按不可用处理 |
| 优先级一:来源门 | 来源拒绝优先于生成逻辑,先拦后答 |
| 优先级二:降级门 | 门未过则生成固定披露,不自由发挥 |
| 优先级三:刷新门 | 仅显式重试或定时采集可触发,小时窗限频,普通问答永不隐式触发 |
前六条解决“何时停”。输入决定是否进门,优先级决定停在哪一步。任何一步不满足,后续步骤不再执行,直接走向固定披露。
| 维度 | 规则摘要 |
|---|---|
| 失败一:网关异常 | 依赖失败直接降级,不猜测不顶旧数 |
| 失败二:部分证据 | 缺关键字段即整体不可用,不拼接结论 |
| 降级一:文案固定 | 不可用文案模板化,避免误解 |
| 降级二:动作清零 | 动作倾向为无,可执行标志恒为假 |
| 安全一:只读边界 | 问答路径无执行无变更能力 |
| 安全二:审计必记 | 记录原因判定与下一步建议 |
后六条解决“停后怎么办”。失败侧保证不硬撑,降级侧保证表达一致,安全侧保证无副作用且可追溯。两组规则合在一起,才是完整闭环。
补充四条原子性规则。第一,候选证据只接受根层候选,不接受旁路写入。第二,变更操作固定且受限,不支持任意写入。第三,发布采用规范原子发布,要么整体可见,要么整体不可见。第四,去重按范围、指纹、小时窗口执行,同一会话内保证精确一次的连续处理。
受控刷新在受控窗口内观测到:根层示例六比六通过,应用层示例三十一比三十一通过。该计数是示例范围,不做普遍保证。它没有复用执行级的每日作业流,而是走独立受控通道。
触发规则重申一次:只有用户显式重试与定时采集任务能触发刷新,共享小时窗口限频。普通问答无论证据多缺,都永不隐式触发。
隔离设计值得单独强调。问答需要高频、低延迟、可重试,而资金链路需要低频、强校验、重审计。两者混用只会互相拖累,分开反而各自更稳。
去重策略也是为稳服务的。按范围限定影响面,按指纹避免重复劳动,按小时窗口防止频繁刷新。同一会话内的连续处理保证精确一次,不多做也不少做。
七、三个对比:什么时候该说不知道
案例一:过期快照 vs 坦白不可用
假设受信通道短暂不可达,手头只有一份过期快照。一种做法是拿旧数顶上,加一句“仅供参考”。另一种做法是直接返回不可用,说明缺少新鲜证据。
前者看似体贴,实则危险。用户很可能忽略后缀,把旧数当成现状。后者虽然多了一步刷新操作,却守住了时效语义。过期数据只能解释过去,不能断言现在。
过期场景有明确的禁用说法。禁止写“根据最新数据,当前状态为……”,因为此时并无新鲜证据。应写“暂无新鲜受信证据,未做当前状态断言,可显式重试刷新”。
案例二:内部便签 vs 正式拒绝
假设某个内部标签写着看似正确的结论,引用它只需一行代码。一种做法是为了通过率先放行,事后再补 allowlist。另一种做法是当场拒绝,保持不可用直到合法来源就绪。
前者打开了先例:今天放一个便签,明天就会放更多。后者用一次短期的不可用,换来了长期的来源纯洁性。这正是前文拒绝事件的泛化教训。
案例三:猜测补全 vs 等待刷新
假设证据缺了一个关键字段,但其余字段齐全。一种做法是按常识补全缺口,让回答看起来完整。另一种做法是承认缺口,触发受控刷新后再答。
前者把模型推测混入了受信断言,审计也无法区分哪句来自证据、哪句来自猜测。后者保留了可恢复性,刷新成功即可解除,失败则继续保持诚实状态。
八、已验证与未采集:一张诚实的清单
设计评审与运行验证分开记录。前者只说明方案是否评审通过,后者只说明受控窗口内观测到什么。
| 设计评审项 | 状态 |
|---|---|
| 来源边界与降级文案 | 已评审:提案从零起步,累计二十五个任务项 |
| 候选、操作、发布、去重、会话语义 | 已评审:机制定义落实到流程与规则 |
| 运行验证项 | 受控窗口观测 |
|---|---|
| 产物到回读六环节端到端 | 离线四比四,在线七比七 |
| 六类关闭式失败行为 | 全部按预期拒绝或降级 |
| 不可信来源标签拒绝 | 名单拦截并降级为不可用 |
| 受控刷新示例分层 | 根层示例六比六,应用层示例三十一比三十一 |
其中根层与应用层是示例架构分层标签,用来区分底层证据候选与上层问答装配。六与三十一是该次示例范围的用例计数,不是通用规模声明。
| 未采集项 | 状态 |
|---|---|
| 真实数据库与多进程行为 | 未采集:未做真实库压测与并发验证 |
| 外部服务质量与长尾延迟 | 未采集:真实网关质量指标缺失 |
| 生产级长时间运行表现 | 未采集:仅覆盖受控验证窗口 |
这张图特意保留了未采集部分。因为验证窗口之外的结论,不应被当成已证明。不可用的精神同样适用于复盘写作:没测过,就写没测过。
这种写法看似示弱,实则增强了可信度。读者能清楚区分已证事实与待补短板,不会高估系统边界。后续补测也有明确清单。
未采集项主要集中在真实运行环境。受控验证能证明逻辑正确,却证明不了高并发、长延迟、真实网关抖动下的表现。这部分必须留白。
特别说明两点。第一,受控刷新未复用执行级每日作业流,两者隔离是刻意设计。第二,会话内精确一次连续处理仅覆盖同一会话的受控范围,不扩展为全局恰好一次承诺。
九、结论:刷新本身也要有边界
受控刷新解决的是“怎样合法地产生并发布新证据”,不是“如何把所有不可用都自动修好”。触发来源、scope、fingerprint、时间窗、原子发布和失败审计都必须保持显式;超出该验证窗口或缺少真实环境证据的能力继续记为未采集。
证据边界与材料来源
- 受控证据刷新复盘:记录只读入口、candidate 校验、原子发布、scope/hour 去重和受控验证结果。
- 本文数字仅描述该历史窗口的测试样本;真实 PostgreSQL、多进程、provider 质量和生产长时间运行仍未采集。