一个研究 Agent 先后查了组合、检索了资料,模型终于写出了回答。服务端已经拿到 Markdown,似乎只要把字符串转发给浏览器就可以了。
但到了渐进回答、来源撤销和断线恢复阶段,问题变得不一样:正文已经出现在页面上,数据库却可能还没保存;一段正文已经提交,下一次模型调用仍可能失败;新预览和旧回执甚至可能带着同一个版本号,却包含不同的来源提示。此时再用"最后一次模型调用成功了吗"决定整篇答案是否存在,会丢失已经发生的事实。
2026 年 10 月的一次实际修复就暴露了这种错位:相同修订的实时预览,在重复内容比较时遮住了真正已提交回执的来源状态。另一次认证应用与 PostgreSQL 重启测试则发现,Publication 已提交正文而运行日志尚未写入时,单靠运行日志重建历史会丢掉这段回答。
这篇文章只讨论一个组件:Publication(回答发布组件)为什么不能只是发消息,以及它怎样管理候选正文、校验、预览、持久化提交、修订和恢复。 这是工程发布机制,不是一个自动判断投资分析是否正确的裁判。
一、模型有正文,不等于系统已经发布了正文
早期的问答接口容易把三个事件压成一次操作:模型返回文本、服务端保存消息、客户端展示文本。只要回答只能在末尾整体返回,这种简化在一段时间内可以工作。
但研究型 Agent 经常需要先交付有根据的部分,然后继续阅读资料,再修订已经发布的段落。这里至少出现五种不同事实:
| 事实 | 谁能证明 | 不能推出什么 |
|---|---|---|
| 模型提出候选正文 | 已封存的模型回合 | 不证明正文可公开 |
| 候选通过发布准备 | Publication 的当前校验 | 不证明已保存 |
| 临时预览发出 | 服务端的预览事件 | 不证明客户端收到或数据库已提交 |
| 版本化发布已提交 | 持久化 Publication 回执 | 不证明浏览器已经渲染,也不证明文章正确 |
| 客户端收到并展示 | 对应版本的交付、DOM 观测 | 不证明用户阅读或认可 |
最重要的变化不是增加了多少状态,而是不再让后一个阶段冒充前一个阶段的权威。
如果删去独立 Publication,让模型回合或流式传输各自处理发布,结构契约、引用绑定、修订身份、保存顺序和恢复判断就会分散到多个调用方。Publication 所集中的是这些共性责任,而不是替模型判断哪个观点更有说服力。
二、两个正文入口,最终进入同一个发布归属方
这套系统经历过一次很实际的接口演进。
最初我们用显式的发布动作表达回答,后来把渐进回答工具收敛为 send_answer_part:模型只需提交完整的 Markdown,必要时给出一个由 Host 发放、仍然有效的 replaces 目标。它不再被要求填写来源版本、数字通道、文本偏移或运行状态。
再后来,我们发现要求"模型必须调用发布工具才允许出现正文"也不合理。经过网关明确归一化的普通 Assistant Text,同样可以成为候选答案;含工具调用的同一模型回合也可以带普通正文。两种入口有不同的候选身份,但共用后续 Publication 校验与交付机制。
例如,下列内容只是教学示意,不是真实持仓、来源编号或现行底层 wire payload:
text
入口 A:模型普通正文
"本轮先确认了组合配置,以下是可独立回答的部分......"
入口 B:send_answer_part
markdown: "该基金的现有占比需要结合报告日期解释。[[cite:完整 Host 引用]]"
replaces: <可选,Host 已发放的当前 publication ID>
Host:
为每个候选建立运行内身份 → 校验 → 准备 → 发布
原始协议帧、隐藏推理、Entry 路由结果、未闭合的工具参数,都不能因为"包含一段像答案的文字"而进入发布链。对于真正不同的正文,系统也不靠语义相似度删除一条入口的内容;只有满足精确重复规则才复用已有身份。
这一步的取舍是:让模型负责写什么,让 Host 负责这段文字是否有资格成为当前可展示、可持久化的答案。
三、Publication 的主流程:准备、预览和提交不是一个动作
可以把当前关键机制缩成一张图。图中临时预览是可选的提前交付路径;不是所有 Publication 调用都必须先发预览。
在当前实现里,发布适配层先把当前模型候选和本次实际允许的证据视图结合,得到可展示正文和依赖绑定。它可以在写库之前完成准备,并允许 UI 提前看到 answer-preview;真正的 answer-publication 则必须有已提交的版本化事实。
持久化路径不仅保存正文,还在原事务中处理块版本、来源及依赖关系、交付事件和运行约束。正常提交、修订、恢复都复用这个归属方。不能因为临时 preview 已经展示,就把它伪装成成功写入数据库。
为了降低首段等待时间,当前路径允许预览先交付、保存随后结算。这样做增加了一个必须正视的代价:用户可能看到尚未耐久保存的内容。 保存失败时要保留失败事实,不能在恢复后把临时预览当成已提交正文;某些已经发生的可见修改也不能因为随后保存失败就假装没有消耗过修改额度。
四、校验分两层:机械合格,不代表文字论证成立
Publication 使用统一的 AnswerBlock 结构契约,供运行时、发布、恢复与对话投影共同解析。内部块可以有文本或表格、角色及受控绑定;但普通模型写作入口不需要亲自维护这些结构事实。
第一层是机械校验:身份与结构是否合法、内容是否越界、修订目标是否存在、是否与当前运行和作用域匹配。同一结构值在不同环节不应一处接受、一处又用另一套私有规则拒绝。过长内容在准备阶段必须显式处理,不能在已提交之后暗中裁剪成另一份正文。
第二层是 Publication 的发布授权。它比"JSON 能通过 schema"更严格,但也更有边界。
引用需要原始资格。 模型写下 [[cite:...]],并不自动得到引用权。它必须引用 Host 实际发放的完整身份,由当前可用的证据视图解析;搜索命中、模型猜出的来源名、截短的编号,都不能冒充已经阅读和获准的材料。能够生成合法引用,也只说明绑定机制成立,不证明该资料足以支持整段推论。
受控数值需要原始绑定。 对组合等需要确定性来源的数值,Host 发放受控数字句柄,再按当前版本、作用域和来源将值渲染到回答里。无效句柄不会被随意猜成另一个值。另一方面,普通字面数字或模型计算出的派生比率不会因为没有受控句柄就一律禁止发表;它们可以按普通文字处理,但也不会自动获得逐值正确性的认证。
来源提示跟着具体发布块走。 没有有效行内引用,或者只有部分引用被确认时,系统可以给这一块 sources_unverified 或 sources_partial 提示。它不等于全篇不可靠,更不能仅凭来源提示把一次执行标记为 interrupted。已经提交的来源提示绑定到该块的修订和正文身份;旧格式或绑定不匹配时保留 unknown,而不是默认"无风险"。
换句话说,Publication 可以回答"这段内容能否按当前合同发布",不能替代研究者或独立评测回答"这段投资分析是否正确"。
五、一个块有三个关键坐标:身份、修订与展示位置
渐进回答不是不断向页面 append 字符串。它更像一组可修订、可恢复的回答块:
| 坐标 | 作用 | 为什么不能省 |
|---|---|---|
publicationId / 块身份 |
确定"修改的是哪一段" | 避免修订被当作新段追加 |
revision |
确定该段的内容版本 | 防止旧事件覆盖新内容 |
deliveryOrdinal |
确定该段在整篇中的位置 | 修订原段时维持原始顺序 |
下面是概念示例,不是实际 SSE 字段或数据库记录:
text
首次提交:A,revision=1,deliveryOrdinal=2
修订同段:A,revision=2,deliveryOrdinal=2
新增一段:B,revision=1,deliveryOrdinal=3
把上面的"修订 A"落成一个结构化事实,可以得到下列合成的内部发布事件。它不是模型输入,也不是外层 SSE 的完整传输格式:
json
{
"type": "answer_block_published",
"id": "synthetic-section-A",
"revision": 2,
"deliveryOrdinal": 2,
"content": {
"type": "text",
"text": "该比较仅说明示例资料中的差异,仍需核对报告日期。"
}
}
数据库为修订保存版本历史,当前交付投影替换同一位置的正文,而不是把 A 的两个版本同时显示成两段结论。客户端收到同一发布身份、同一修订但正文不同的事件时,也必须视为协议冲突,不能默默以"最后到达者"为准。
为了防止模型反复发表同义内容,系统还有一个刻意狭窄的精确重复规则:正文完全相同、保护来源与依赖绑定及提示未变,并且当前授权再次通过时,才允许返回 unchanged,复用原回执,不新增修订、交付事件或修改额度。仅仅文字相似不算重复,Markdown 空格改变也不算;来源撤权后,旧内容即使逐字相同也不能绕过重新检查。
当前默认修改额度是单块 2 次、一次 Research 共 8 次;它限制修改已有块,不是禁止新增回答段。超限拒绝该次修改,不应抹去此前合法内容,或自动宣告整个研究失败。
六、真实问题一:同修订预览曾遮住已提交回执
2026 年 10 月 11 日的回归案例尤其能说明为什么"正文一样"还不够。
当时运行时维护了实时候选和已提交候选。重复检测会查找当前可见的同一段正文,但早期比较中,实时映射可能盖住同一修订号的持久回执。于是即使文字一致,真正已提交的来源状态也可能丢失。
问题不是模型突然写错了文章,而是我们把两个不同可信度的记录放到同一个候选池里,却没有定义清楚相同修订的优先级。
修正后的规则可以简化为:
text
较新的合法预览 > 较旧的已提交版本(仅用于当前临时显示)
同一修订的已提交回执 > 同一修订的预览元数据
当前来源拒绝 > 旧预览和旧回执的可见性
第二行尤为关键:已提交回执携带的是数据库确认的来源和版本事实,准备阶段的预览不能反过来给它补一个"看上去干净"的状态。第三行则提醒我们,历史提交真实发生过,不等于被撤权的来源今天仍可展示。
本次修复保留原候选与回执归属,没有另造一套发布状态或权限系统。定向检查记录 45/45 通过;后续正式 provider-free 关闭在独立工程候选上完成。此前的失败、并行超时和真实模型质量缺口没有因此消失。
七、真实问题二:数据库提交了,Runtime 日志却还没写完
另一个更棘手的时间窗口发生在持久化提交与运行时日志之间。
设想同一段回答 A 的 Publication 事务已经提交,包含正文、来源绑定与交付回执;紧接着进程退出,Runtime journal 还没有记录到对应状态。重启后如果只重建 journal,就可能得出"没有 A"的错误结果。
正确的恢复不是让模型重新写 A,也不是把原先的预览强行标记为已保存,而是:
- 按原运行、块身份和已提交回执,从 Publication 原归属方读取受保护正文。
- 核对当前账户、来源和块之间的继承依赖;B 引用了 A 的来源,不能只检查 B 表面上的直接引用。
- 能继续展示的部分走已有历史快照和交付通道;无法确认权限或已经撤销的部分保持受保护状态。
- Runtime 的原执行终态、Publication 的提交事实和客户端是否展示,继续独立记录。
这里还有一个细节:旧回执没有当前来源状态的资格字段,不等于已确认来源安全;但确实已被验证为空的来源集合,也不应和完全未知混为一谈。
2026 年 10 月 11 日的认证应用、隔离 PostgreSQL 与重启路径实际暴露并验证了这一类恢复缺口。修复包括读取原始受保护回执、重新检查继承来源,以及保留旧来源提示的 unknown 语义。它支持这一段机制,不能推导整套异步交付已经完成验收。
八、为什么"后来的失败"不能抹掉"之前的提交"
现在再看开头的问题,答案就清楚了。
假设 A 已提交,随后模型继续研究 B 时供应商失败。系统可以同时记录:
text
A:已提交 revision=1,当前授权下可读
B:未提交,原因需要继续核查
Run:failed 或 partial,按原执行终态记录
客户端:是否实际收到 A,依对应交付证据记录
这四行不是互相矛盾的"成功"和"失败"。A 的历史提交事实是真的,B 的失败也是真的;用户是否收到了 A,还需要交付链证明。后续失败不能把 A 的事务撤销成"从没发生过",但同样不能把 A 的存在包装成"整个用户任务已经完成"。
具体来说,send_answer_part 的发布效果与模型工具回执是两个独立结算点。发布已经提交、回执写入却失败时,系统不能简单把这一轮改写为"已知发布拒绝",再盲目重试原副作用。恢复时应沿原身份确认已结算效果,未知的执行继续保留 unknown。
反过来,来源撤权也不是"后来的普通失败"。它改变的是当前展示资格:原提交记录仍可审计,但受到撤权影响的正文不能因为曾经成功发布就继续向新客户端披露。
最后还要坚持一条容易被忽略的界限:服务端发出了 answer-publication,只能证明服务端交付动作;浏览器接收、DOM 提交、历史快照与用户实际阅读各有自己的观察位置。任何一个时钟或回执都不能替所有阶段签字。
九、这套机制已经证明了什么,还没证明什么
当前的技术材料足以支持以下有限结论:
| 范围 | 已有证据 | 不应外推 |
|---|---|---|
| 结构与发布归属 | 统一 AnswerBlock 契约、发布适配层及数据库版本/回执实现与相关本地回归 | schema 通过不代表语义正确 |
| 相同修订的回执优先级 | 2026-10-11 定向回归及独立工程候选 provider-free 正式关闭 | 不代表完整生产部署或真实模型质量验收 |
| 已提交正文的重启恢复 | 认证应用、原 Factory 与隔离 PostgreSQL 的历史恢复、来源撤销等有界验证 | 不代表整项异步交付 Spec 已关闭 |
| 渐进回答的阅读体验 | 已有 Web/CLI、修订、警告及引用显示的机械检查 | 同题同源的真实模型可读性与质量增益尚未得到严格因果证明 |
这套机制也有成本:多出准备与提交两个阶段,需要维护修订身份、持久化事件、来源关系和预览/回执的合并规则。它不适合被理解为"所有聊天应用都必须照搬一个重型发布服务"。
可迁移的判断标准反而简单:如果内容允许提前显示、后来修订、跨进程恢复,并且依赖可撤销的资料,那么"写出答案"和"发布事实"就值得分离;至于实现可以是一个深模块,也可以是独立服务。
结语:回答是模型写的,发布事实必须由系统持有
回看这段演进,我越来越不愿意把 Agent 的输出当成一条不断增长的字符串。
模型负责组织解释,Publication 负责把可公开的候选转成有身份、有修订、有来源绑定、可恢复的提交事实;交付层负责传输和展示这些事实,运行时负责本轮任务怎样结束。它们应该协作,但不能相互冒充。
一句话总结:可以先让用户看到有用内容,但不能让"看到了""存好了""全部成功了"变成同一件事。