DeepSeek Harness 的消息反馈通道:模型跑着的时候你插一句 /feedback 会怎样
先说清楚这篇要回答的那个具体问题。
你在跟 agent 干活,模型正在一轮里连着调工具,你眼看着它往一个错方向走,顺手在输入框里敲了一句反馈。这时候会发生三件事里的哪几件:这一轮会不会被打断?这句话会不会被塞进模型的上下文,从而顶掉本来能复用的前缀?以及,这句话最后落到哪里去了、还能不能撤回?
DeepSeek Harness 仓库里管这块的是 packages/feedback/ 这个包组,它下面只有两个包:command-feedback/ 和 message-feedback/。两个包解决的是完全不同的两件事,名字又都叫 feedback,第一次翻很容易串。先把它们各自的路径走一遍。
需要先摆在前面的限定:该仓库 README 的 Developer preview 一节自述项目处于开发者预览阶段,并且用整句大写明写会有破坏兼容性的变更(README.md 第 9 至 11 行)。下面提到的命令、字段名、默认值随时可能变,请以仓库最新内容为准。本文对应的是版本 0.1.0-rc.5。
第一条通道:/feedback,一条写进日志但模型看不见的记录
打开 packages/feedback/command-feedback/src/index.ts,整个包的公开写入口是一个五行的函数(第 72 至 76 行):
export function recordFeedback(session: Session, text: string): void {
const normalized = text.trim()
if (normalized.length === 0) throw new TypeError('feedback text must not be empty')
session.append('feedback/record', { text: normalized })
}
它做的事就是把首尾空白去掉,然后往 Session 日志里 append 一条 feedback/record 事件,完。没有任何一步去启动模型工作。 斜杠命令 /feedback 的 handler executeFeedbackCommand 也只是调用它,然后拼一段确认文本回去。
所以开头那个问题的答案,在同一个包的 README 的 Model Experience 一节里写得很直白:模型什么也看不到。feedback/record 是 log-only 事件、不带 surfaceOp,因此不进 ordered surface、不进 deriveMessages()、不进 system prompt;README 还专门写了一句「在一轮进行中记录反馈,不改变这一轮剩下的请求」,以及 token 影响为零、对 KV Cache 的影响是「与模型请求路径无关」。
这里有个细节值得单独拎出来。命令注册时给了 recordInput: false:
ctx.commands.register({
name: 'feedback',
description: 'record feedback about this session',
input: { hint: '<text>' },
recordInput: false,
handler: invocation => executeFeedbackCommand(invocation, ctx),
})
dsh-commands 照例会追加它通用的 command/run / command/done 一对事件,但因为这个开关,command/run 不带 args。README 自述的理由是:反馈可能从 /feedback 之外的触发点进来,事件才是权威,把文本留在 command/run 里会变成两条记录承载同一段文字。这是文档自述,不是我们的推断。
别把「确认文本出现了」当成「已经落盘了」。 README 的已知限制里明写:确认发生在 append 之后,不是 flush 之后,所以紧接着崩溃的话,这条记录会跟着其它未 flush 的尾巴一起丢;需要落盘保证的消费方得自己去 await ctx.sessions.flush(session)。文档里给的原话是「反馈不值得为它强制一次同步磁盘写」。
确认文本本身由三段拼成:接收会话的 id、一个匿名用户 id、以及一句会话共享披露。第三段是从已挂载的遥测服务读出来的,四选一,源码里 sharingSentence 的 switch 就是这四支:
| 披露到的状态 | 确认文本里那一句 |
|---|---|
full | Session sharing is enabled. |
feedback-only | Session sharing is feedback-gated; recording feedback releases the session prefix for sharing. |
disabled | Session sharing is disabled. |
| 没挂遥测服务 | Session sharing is not configured. |
这张表要这么读:它说的是「你这个部署当前的共享策略」,不是「你这条反馈已经送到了谁那里」。README 对此写得很谨慎——full 与 feedback-only 下记录会交给后端的非阻塞入队,批量、重试、丢失策略归 SDK 管,所以这句话不承诺任何东西真的到达了收集端。另外 README 提到,某个 harness home 第一次接受反馈时,可能会创建 $DSH_HOME/.anonymous-user-id。
一处文档与代码不一致,照实记。 packages/feedback/command-feedback/README.md 第 18 行写的是通过 ctx.get('telemetry') 读这个服务,而 packages/feedback/command-feedback/src/index.ts 第 92 行里实际写的是 ctx.get('sessionTelemetry')。中文版 README 同一行同样写作 telemetry。两处不一致,以我们实读的源码为准。就到这里,不往下延伸。
这条通道的边界,README 的已知限制列得比正文还有用:没有检索面(挂载了 OTel 插件也只是拿这个事件当共享触发器)、没有结构化字段(一条就是一段自由文本,没有分类、没有严重级、没有关联事件)、没有修改或撤回(日志只追加,这个包不加墓碑,写错了只能再写一条)、只在 Web 端有(headless、ACP 自动化、JSON-RPC 都不提供命令适配器)。还有一个坑很具体:全新的空会话里执行 /feedback,事件会记下,但 Web 记录区要等会话激活后才渲染命令行,所以你看不到确认。
第二条通道:给某一条助手消息打分,走的是完全另一条路
packages/feedback/message-feedback/ 干的是另一件事:对某一条已定稿的助手消息给 positive / negative,可以带一段备注,而且可编辑、可删除。它不进 Session 日志、不是投影、不做遥测交接,docs/subsystems/feedback.md 的原话是它只待在 storage-domain 的伴随记录里。
顺着 src/index.ts 里 put 的路径走一遍,会看到几个顺序上很讲究的点:
- 先校验备注,再查会话。
resolveNote在入队之前就跑(第 207 行),空白备注直接note-blank,超长直接note-too-large。README 明写了这个顺序的后果:会话根本不存在时,你也可能先拿到备注类错误,因为这一步压根没碰持久化。 - 按 Session 串行入队。
enqueue用一个operationTails的 Map 把同一 Session 的操作串成链(第 361 行起)。检查、读取、冲突判断、整行写入都在队列里,因此这套保证只覆盖单个 Host 进程内的并发。 - 目标必须是「非空的、append-origin 的
assistant/message」。hasFeedbackTarget逐条过事件,replacement-origin 的、只带 usage 的空记录、非助手记录,一律target-not-found。 - 写伴随记录之前先立耐久屏障。
ensureTargetDurable对身份匹配的 live 会话先走ctx.sessions.flush,没有耐久监听器参与就直接抛错;随后无论 live 还是 cold,都用SessionPersistence.readFrom(id, 0)从序列零物理复读一遍,再校验一次 header 身份和目标是否还在。文档给的结论是:目标日志的持久提交,永远早于它的伴随记录。 - 严格乐观并发。
ifVersion必须与当前条目版本完全相等,哪怕这次请求根本不改值。版本是randomUUID()生成的不透明 token(第 132 至 134 行),只能做相等比较,调用方不能排序也不能自己合成。冲突时返回的version-conflict里带着权威当前条目(不存在时是null),所以调用方不用再读一次就能对账。
值得记住的两个数值锚点:存储域名字是 message_feedback、version: 0,声明在 src/spec.ts 末尾的 defineDomain;maxNoteBytes 是必填的正 safe integer,Web bundle 里配的是 8192——这个值在 packages/bundle/web-app/cordis.patch.yml 的 message-feedback 那一段(第 64 至 67 行)能直接翻到。maxNoteBytes 按 UTF-8 字节算(Buffer.byteLength(note, 'utf8')),所以中文备注能写多少字,跟这个数字不是一回事。
还有一条容易被忽略的语义:省略 note 意味着「期望值是没有备注」。也就是说一次版本匹配的实质 put 如果不带 note,会把原来那条备注清掉,而不是保持不变。
这些边界什么时候会咬到你
docs/subsystems/feedback.md 的 Boundaries 一节把话说得比较硬,挑几条落地的:
- 队列是进程内的,storage-domain 没有跨进程条件写,多个 Host 进程写同一个存储根目录时不提供 compare-and-swap,也不保证不丢更新。
- persistence 没有持久删除接口,服务不把
session/disposed当删除,所以带外删掉日志之后,孤儿伴随记录可能还留着。 - cold 请求会扫完整的 Session snapshot 目录,因为 persistence 没有按 id 读元数据的操作。
- 单个 Session 行没有条目数上限、也没有聚合字节上限,
maxNoteBytes只管每条备注。 - Host 约定不记录已认证的 actor 或审计身份,文档直说这里假设调用方边界可信。这条决定了这个 Host 端点该暴露在什么位置上,不要想当然。
- 只有
{createdAt, cwd}不同时,header 身份才能识别出复用的 Session id;保留了相同 header 身份的克隆日志,这套约定区分不了。
Web 侧还有两条口径宽窄不一致,是文档自己写明的:被中断冻结的部分输出不带 messageId,渲染点在字段缺失时直接跳过反馈控件;多步 turn 里,Host 接受每一条 append-origin 步骤消息作为目标,但 UI 只在收尾那条助手消息上渲染一次操作栏,所以UI 暴露的范围比 Host 约定允许的窄。另外 trajectory 与 waterfall 视图不渲染反馈条目,尽管它们的助手节点带着同样的 messageId;伴随记录也不发布实时帧,另一个标签页的评分要等重连或下一次冲突响应才可见。
第二处不一致,同样照实记。 packages/feedback/message-feedback/README.md 的已知限制第一条写的是「Client aggregate and UI are absent」——客户端聚合与 UI 消费方缺席、延后处理;packages/feedback/README.md 结尾也是同样口径。而 docs/subsystems/feedback.md 有完整的 Web surface 一节在描述 @deepseek-ai/dsh-client-ui-message-feedback,这个包在 packages/client/ui-message-feedback 确实存在,并且在 packages/bundle/web-app/cordis.patch.yml 第 246 至 247 行被挂载。三处口径不一致,我们只陈述差异,不推断哪个是当前状态。
回到开头那个问题
如果你的诉求是「我想留一句话给这次会话,不想影响正在跑的这一轮」,走的是 /feedback:模型看不到、零 token、不动请求前缀,但写下去就改不了,也没有地方能查回来。
如果你的诉求是「这条回答不行,我要标记出来并写清楚哪不行」,走的是消息反馈:可改可删,有版本对账,但它待在伴随记录里,不进日志、不进模型、也不触发遥测释放——指望模型看到你的差评然后改,是没有的事。
最后重复一遍那层限定:以上全部字段名、事件名与默认值都取自仓库当前快照,该项目自述处于开发者预览阶段并明确会有破坏兼容性的变更,读到这篇的时候请自己回仓库对一遍再动手。
本文依据 DeepSeek Harness 官方仓库(github.com/deepseek-ai/deepseek-harness)的 README、docs/ 下的
架构与子系统文档、以及 packages/ 下的源码整理,核对日 2026-08-17,对应仓库快照 47f9438(版本 0.1.0-rc.5)。
本文内容为仓库源码与文档口径,我们没有安装、也没有运行过这个项目,
因此不涉及界面外观、操作手感与运行速度的任何描述。
该仓库 README 自述处于开发者预览阶段并明确说明未来会有破坏兼容性的变更,
文中出现的命令、配置与默认值随时可能变动,请以仓库最新内容为准。