本内容改编自小山学堂《学 AI 产品,从入门到精通》,为二次演绎配音版
模块:T4 解剖 DeepSeek Harness:一切皆插件的 Agent 底座
来源:xueai.miyang.cn(小山学堂 · 洛小山)
本集为两节合辑(素材 8335 字),已做取舍。
统一主线:两节都在回答同一个问题 —— 新进来的东西,什么时候算数。
| 节 | 问题 | 答案 |
|---|---|---|
| 不变量 | 一条内容进模型之前 | 必须先成为一条事件 |
| 双队列 | 一条消息进循环之前 | 必须先决定它属于哪一班 |
模型看到的一切都要能从日志重建,发请求前还要现场验一遍,验不过直接崩。Agent 正在干活时你想说句话,消息该排哪条队、什么时候被处理 —— 三个 API 共用一个 send(),只差两个参数。
| 概念 | 含义 |
|---|---|
| 会话日志 | 只追加的事件流,是唯一真相 |
| 消息历史 | 从日志派生,"从不单独存储" |
| 出站比对 | 每次请求前现场重派生 + 全量字符串比对 |
| 失败策略 | 抛异常,请求当场作废,发不出去 |
不变量原文(AGENTS.md L107):
*Model-visible ⟺ logged: anything that reaches a model request must be reconstructable from the session log; a new model-visible input requires a session event.*
const expected = session.deriveMessages()
if (JSON.stringify(options.messages) !== JSON.stringify(expected)) {
fail(`llm request for session "${String(session.id)}" diverges from the dispatch-time durable derivation (log-reconstruction desync)`)
}
出处:packages/core/agent-loop/src/invariant.ts L39–42。
比对范围:消息数组 + 系统提示词 + 模型名 + 采样参数 + 工具清单,全部与日志里的请求头快照逐字段对齐。出处:invariant.ts L22–52;请求头重建 packages/core/session/src/request-header.ts L65–71(七行纯函数)。
| API | target | wakeup | 进哪条队 | 何时被接走 |
|---|---|---|---|---|
| followup | next-turn | 唤醒 | 等下一班 | 当前 Turn 跑完,一班只接一条 |
| steer | next-step | 唤醒 | 插当前这班 | 当前 Step 跑完,下一步开头 |
| inject | next-step | 不唤醒 | 插当前这班 | 下一个自然 Step,不催开工 |
三个 API 全是 send() 的参数预设,各自只有三行。出处:packages/core/agent-loop/src/agent.ts L113–132。
大多数聊天程序都有两份对话:内存里一份数组,磁盘上一份存档,各写各的。进程崩了从存档恢复,恢复出的历史比模型当时实际看到的少了一条工具结果 —— 模型接下来的回答全对不上号,还查不出原因,因为两份状态谁也证明不了谁。
只要真相有两份,它们迟早漂移。
光写日志做不到第二点 —— 写日志是被动的,谁都可以绕过它直接改请求。所以必须有出站站岗。
检查用的派生函数,和恢复、回放用的是同一套公开函数。
如果检查用一套私有实现,两套实现迟早也会漂移 —— 等于给自己造了第二个真相。
请求头重建只是一个七行的纯函数:扫一遍事件、取最后一个快照。
告警意味着违规请求已经发给了模型:某个插件绕过日志偷偷改了消息,模型看到的和日志记的从这一刻起是两回事。日志接着记,记的全是错账。三天后拿这份日志回放排查,怎么都复现不出线上的怪行为。
静默偏差比崩溃可怕 —— 它把排查成本悄悄转嫁给了未来。
比对不过 → 抛异常 → 这次请求当场作废,根本发不出去。
设计笔记明确否决了温和方案,对「比较连续请求,发散时告警」这条备选的判词是:「因违规必须在接口层面不可表达而否决」。
出处:.agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md「曾考虑的替代方案」一节。
| 机关 | 作用 | 出处 |
|---|---|---|
| 检查器插在事件监听队列队头 | 防止别的监听器提前短路、把检查静默跳过 | invariant.ts L20–21、L54 |
| 请求对象与消息数组深度冻结 | 堵住「先过检、再改内容」的后门 | invariant.ts L22–29 |
为什么长期成立:这就是 fail-fast。崩溃把损失锁在零 —— 日志里没有被污染的轮次,修好 bug 重跑即可。带病运行的成本是复利,跑得越久坏数据越多,最后连哪天开始坏的都查不出来。
| 能力 | 原理 |
|---|---|
| 恢复 | 进程崩了,从日志重新派生一遍,接着聊 |
| 分叉 | 从任意一条事件岔出去,开一条平行会话 |
| 回放 | 把日志再派生一遍就是当年的请求,连 API Key 都不用 |
| 审计 | 界面上看到的轨迹就是模型看到的内容,有运行时断言背书 |
| 压缩 | 摘要同样以事件形式写进日志,压缩后的请求照样过出站比对 |
学名 event sourcing(事件溯源):银行和会计系统用了很多年 —— 不存余额,只存流水,余额永远从流水算出来。
| 方向 | 持久层是否承担正确性职责 | |
|---|---|---|
| DSH | 日志 = 真相,出站反向比对 | 是 |
| Grok Build | 内存是主、落盘是从(单行道) | 否 |
| Claude Code | 事后记录型(基于公开证据) | 未知 |
Grok Build 每来一条消息把内存条目拷一份丢进落盘通道,发送结果直接丢弃,落盘失败也不打断对话;压缩时允许把整份落盘历史一次性替换掉。出处:crates/codegen/xai-grok-shell/src/session/chat_persistence.rs L30–38。
差别不在有没有日志,而在方向 —— 前两家把持久化当恢复手段,DSH 把日志升格为需要运行时证明的第一性原理。
只有一条消息队列时,用户说话只有两种命运:打断,或者排队。
Agent 正在按计划改十个文件,改到第三个你发现方向偏了。打断,前两个文件的活白干;排队,只能眼睁睁看它把十个文件全改错。你想做的只是补一句话 —— 这两个选项都要你拿当前进度去换。
| API | 例句 |
|---|---|
| followup | 「这个改完之后,帮我再把测试补上。」 |
| steer | 「等等,配置文件用 YAML 写,别用 JSON。」 |
| inject | 「顺便说一句,用户刚把分支切到 main 了。」 |
为什么长期成立:这是中断的分类学,与实现语言无关。任何 agent 系统重写一遍,还是要回答同样两个问题。把分类做成 API,用户补一句话就有了第三种命运。
多处代码都能从同一条队列读消息时,两种事故迟早发生:
claim,原子地取走 next-step 队列的全部消息;claim 先于裁决发生。拒绝时不开新 Step,轮次直接以 blocked 收场 —— 被拒批次不回队。
出处:packages/core/agent/src/inbox.ts L71–78;agent.ts L229、L266–269。
为什么长期成立:领取制是消息队列几十年的老共识 —— 数据库里叫 SELECT FOR UPDATE,SQS 里叫可见性超时,本质都是把读取和占有合成一个原子动作。
按 Esc 中止当前活动,紧接着发一条 steer。steer 的语义是插进当前 Turn 的下一个 Step,但这个 Turn 正在死掉,它的下一个 Step 永远不会到来。
照原样入队只有两种坏结局:消息永远躺在队列里没人接(会话卡死);或强行插进一个正在收尾的回合(行为无法预测)。
send() 在入队前先看一眼现场:这条消息要求唤醒,而当前活动已被中止?
inject 不要求唤醒,不受降级影响 —— 照常排进 next-step,等新一班车的第一站被顺路接走。
出处:agent.ts L113–120(判断与目标改写)、L164–193(wakeDriver 上闩与重放);另见 L299(next-step 非空时 Turn 不关闭)、L324–329(队列空则收工,有存货则换 AbortController 继续)。
为什么长期成立:这是并发系统的通用命题 —— 事件到达时,它的目标正在死亡。答案也是通用的:别追一个正在退出的执行体,把事件重新排到下一个稳定边界。操作系统给正在退出的进程递信号、Actor 系统给正在停机的 Actor 发消息,套路都一样。
inject()。推演:这条消息最早在哪个时刻被领取?如果 Step 2 本来是本轮最后一步,它会被丢掉,还是把 Turn 续命一步?如果 Agent 已经空闲,它要等到什么时候才被消费?quizFiles)一律不进入口播稿,仅作为集页下方的文字自测卡渲染。DSH 的会话日志是对话的唯一真相,恢复、分叉、回放、审计共用同一份事件流。每次请求出站前从日志现场重建并逐字节比对,对不上当场抛异常,请求发不出去。崩溃优于告警 —— 因为请求发出去的那一刻,日志就再也解释不了模型的行为。
DSH 用 next-turn / next-step 两条持久队列把说话的时机编码成数据,三个 API 只是 send() 的参数预设。claim 是原子交接:消息要么在队列,要么归属某个 Turn,被拒不回队。中断后的唤醒输入一律改排 next-turn,因为死掉的 Turn 不再有下一个 Step。
*来源:xueai.miyang.cn(小山学堂 · 洛小山《学 AI 产品,从入门到精通》)*