Skip to content

[Bug] 蒸馏从不采集工具结果与子会话交付:tool/result 与 tool/code-dispatch 读了不存在的 data.output,且 switch 未覆盖 agent/inbox/spliced #211

Closed

Description

@lxl1782586616-alt

环境

项 值
DSH 0.1.6-alpha.1(macOS,Web profile)
@modusensus/dsh-mneme 0.8.2(npm 最新 0.8.2;main 分支同为该问题存在)

症状

autoSummarize 送进模型的蒸馏转录里,所有工具结果都是空壳:只有 [工具结果] 成功 这一行,看不到任何输出内容。顺带,失败的工具也被标成「成功」。

更严重的是:子会话(subagent)的交付结果完全进不了蒸馏。用 subagent 工具派发任务后,子代理的最终报告在蒸馏输入里一个字都没有。

这不是"信息少一点"的问题——它意味着:凡是靠子代理做的调研/实现,其结论都不会被沉淀成记忆。

实测数据

对两个真实长会话(macOS,~/.dsh/sessions/**/session.v3.jsonl.zstd)按 collectMessages 的规则逐条重放统计:

会话 行数 mneme 采集到的转录(截断前) 助手正文 用户消息 工具结果文本(当前恒为空)
session-aafea559-… 22,427 1,939,923 字符 822,286 10,348 3,984,954 字符(3,553 条)
session-72a2d2f1-… 17,495 1,289,277 字符 256,964 13,918 3,666,103 字符(3,364 条)

被丢弃的工具结果按工具分布(会话 aafea559):

bash                3,162,456 字符 / 2587 条 / 均 1,222
read                  453,750 字符 /  174 条 / 均 2,607
skill                 112,548 字符 /   19 条 / 均 5,923
web_fetch              81,780 字符 /   13 条 / 均 6,290
edit                   63,014 字符 /  364 条 / 均   173
job_output             45,682 字符 /   51 条 / 均   895

即:被丢弃的工具结果是「用户 + 助手正文」的 4.8 倍(会话 aafea559)/ 13.5 倍(会话 72a2d2f1)。

根因(三处独立缺陷)

1. tool/result / tool/code-dispatch 读了不存在的字段

src/summarize.js:

      case "tool/result": {
        const out = typeof data.output === "string" ? data.output : data.output ? JSON.stringify(data.output) : "";
        const status = data.ok === false ? STR.statusFail[language] : STR.statusOk[language];
        lines.push(STR.transcriptToolResult[language](status, trim(out, 500)));
        break;
      }
      case "tool/code-dispatch": {
        const out = typeof data.output === "string" ? data.output : data.output ? JSON.stringify(data.output) : "";
        const status = data.ok === false ? STR.statusFail[language] : STR.statusOk[language];

但 DSH 的 tool/result 事件 payload 既没有 output,也没有 ok。产出点 @deepseek-ai/dsh-agent-loop/lib/index.js:697:

function appendToolResult(session, turn, step, block, result, callSeq) {
	const message = createToolResultMessage({
		callId: block.id,
		content: result.content,
		isError: result.isError
	});
	session.append("tool/result", {
		turn,
		step,
		message,
		...result.error?.info ? { error: result.error.info } : {},
		...result.meta !== void 0 ? { meta: result.meta } : {}
	}, { ... });
}

会话格式的规范形状(@deepseek-ai/dsh-session-format-v0-to-v1/lib/index.js:217):

	"tool/result": disposition([
		"turn",
		"step",
		"message"
	], ["error", "meta"], ["meta"]),

所以 data.output 恒为 undefined → out 恒为 "";data.ok 也恒为 undefined → status 恒为「成功」。失败的工具被记成成功。

正文与错误标记的真实位置是:

data.message.content[0]            // { type: "tool-result", toolCallId, content: [...], isError }
data.message.content[0].content    // [{ type: "text", text: "…" }, …]
data.message.content[0].isError    // 真正的失败判据

(createToolResultMessage 见 @deepseek-ai/dsh-llm/lib/index.js:94,它经 createUserMessage 构造,所以全部内容都在 data.message 下。)

2. switch 未覆盖 agent/inbox/spliced —— 子会话交付结果全丢

collectMessages 的 switch 只处理 user/message / assistant/message / tool/call / tool/result / tool/code-dispatch,其余走 default: break(src/summarize.js:143)。全库 grep agent/inbox 命中数 0。

而子会话的结果不走 tool/result。subagent 工具的返回只是一个句柄,实测每条都是 53 字符:

"started subagent d85a8ddc-e50c-47a0-b171-64f571281074"

真正的交付报告走 agent/inbox/spliced 事件(data.inserted[]):

Background subagent 69a39984-d35d-4d2b-9246-ca7c15c87810 finished and will do no further work unless you send it more.
Its closing message:
已交付完成,结果也已发给父 agent。## 文件清单(绝对路径,全部新建)…

实测规模(会话 aafea559):

项 值
agent/inbox/spliced 条数 1,146
其中文本合计 679,055 字符
按 source.kind 拆 agent-message 293,065 / goal 182,274 / subagent-settled 181,654 / user 10,594 / agent-instructions 5,879 / plugin 5,589

其中 subagent-settled + agent-message(合计 47.5 万字符)就是子会话的交付结果,当前 100% 被丢弃。

3. user/message 的 kind 过滤把注入类消息一并挡掉

src/summarize.js:110:

	case "user/message": {
		if (kind !== undefined && kind !== "user") break;

该过滤本身是合理的(避免把工具结果当作对话正文重复采集),但副作用是注入类消息(goal / agent-message / subagent-settled / agent-instructions / skill-catalog)也以 user 角色出现,于是被一并跳过。实测被跳过 569 条 / 1,092,323 字符。

⚠️ 实现时请注意去重:这些注入消息与 agent/inbox/spliced 的 data.inserted[] 是同一批内容的两种视图(实测按前 80 字比对,569 条中 528 条重合),不能简单叠加采集,否则内容会被重复计入。

为什么长期没被发现

与 #129 / #136 同类:字段路径漂移 + 症状静默。

  1. trim(out, 500) 说明作者本意是要采集工具结果的,只是路径不对 → 收了个空壳,而空壳在转录里看起来"格式正常"
  2. 短会话里"取会话开头"的取样方向问题会掩盖它(与本项目 [Bug] 蒸馏取样方向反了:maxChars 上限用 slice(0, n) 取【会话开头】,长会话/复制来的会话会每轮反复提炼同一段 #210 同源现象)
  3. 一旦失败被标成「成功」,连失败线索也没了

建议修复方向

不建议简单放开——aafea559 里 bash 输出占 3,162,456 / 3,984,954 ≈ 79%,原样采集会让蒸馏上下文被命令输出淹没,信噪比反而下降。建议分档限量:

来源 建议
子会话交付(agent/inbox/spliced 的 subagent-settled / agent-message) 高价值,可放宽(如 2,000–4,000 字符),这是当前损失最大的部分
普通工具结果(bash / read / grep) 保留少量首尾(如 300–500 字符)作为线索即可
失败工具 至少输出 status=fail + error,别静默标成功
采集 agent/inbox/spliced 必须与 user/message 去重(见上文 ⚠️)

字段取值建议改为:

const tr = data.message?.content?.[0];
const out = (tr?.content ?? []).filter(b => b?.type === "text").map(b => b.text).join("\n");
const status = tr?.isError === true ? STR.statusFail[language] : STR.statusOk[language];

复现方法

  1. 在一个会话里用 subagent 派发一个会产生长报告的任务,等它 settled
  2. 触发一次 autoSummarize
  3. 检查送进模型的蒸馏文本:所有工具结果行为 [工具结果] 成功(无正文),且子会话的 closing message 完全不出现
  4. 也可离线复现:解压 ~/.dsh/sessions/**/session.v3.jsonl.zstd,按 collectMessages 规则重放,统计 data.output 的取值(恒为 undefined)与 agent/inbox/spliced 的文本量

相关

Activity

  1. lxl1782586616-alt commented on Sep 16, 2026

    @lxl1782586616-alt
    Author

    补一条与 #78 的关联观察,供定位参考。

    #78(v0.7.14,修「私有 reasoning 不入蒸馏」)为该行为补了回归测试,但没有覆盖工具结果——而本 issue 的根因 data.output / data.ok 就在同一段 collectMessages 里。同一份转录采集逻辑,一处有测试、一处没有。

    这也是它能长期隐身的原因之一:测试覆盖缺口 + 症状静默([工具结果] 成功 这一行格式完全正常,看不出正文丢了)叠加,让「工具结果会被采集」成了一个看起来成立、实际从未成立的假设。

    同理可作为回归测试的锚点:trim(out, 500) 明确表达了「要采集」的意图,测试只要断言「工具结果文本非空」即可守住这条路径。

  2. added
    bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)
    on Sep 16, 2026
  3. modusensus commented on Sep 17, 2026

    @modusensus
    Member

    已对照源码核实,三处缺陷都属实:

    • tool/result / tool/code-dispatch 读的 data.output / data.ok 在宿主事件 payload 中不存在,真实内容在 data.message.content、失败判据是 isError——当前所有工具结果都是空壳,且失败恒被标成「成功」;
    • switch 未覆盖 agent/inbox/spliced,子会话交付结果当前完全进不了蒸馏;
    • user/message 的 kind 过滤把注入类消息一并挡掉的副作用也确认存在。

    你补充的 #78 关联观察同样成立:同一段采集逻辑,一处有回归测试、一处没有,正是覆盖缺口叠加症状静默的典型。分档限量表也合理——尤其是 bash 类输出只留首尾线索,不让命令输出淹没蒸馏上下文。

    欢迎提 PR,几点期望:

    1. 正文里的 ⚠️ 必须落实:注入消息与 agent/inbox/spliced 的 data.inserted[] 是同一批内容的两种视图,采集必须去重,这条请有专门用例;
    2. 失败工具显式标失败(isError),别再静默标成功——一并补回归测试;
    3. 「工具结果正文非空」「子会话 closing message 出现在蒸馏输入中」各补用例;
    4. 本 issue 与 [Bug] 蒸馏取样方向反了:maxChars 上限用 slice(0, n) 取【会话开头】,长会话/复制来的会话会每轮反复提炼同一段 #210 改的是同一段 collectMessages,建议 [Bug] 蒸馏取样方向反了:maxChars 上限用 slice(0, n) 取【会话开头】,长会话/复制来的会话会每轮反复提炼同一段 #210(取样方向 / 增量提炼)先合,本 PR 基于其上做;
    5. 提交署名请遵循 CONTRIBUTING 的要求(commit 须可归属到你本人账号)。

    验收观察点:蒸馏转录中工具结果带正文且失败可见;子代理交付进入蒸馏且限量合理;同内容不重复计入。

  4. z2Ace0107 commented on Sep 18, 2026

    @z2Ace0107
    Contributor

    感谢合并 #226。我想继续负责并认领 #211,按 Issue 描述推进工具结果、代码派发结果和子会话消息的蒸馏补全,请确认这个安排是否合适。

    另外,基于 #226 的 review,我发现一项独立的边界补漏:非空 JSON 数组在过滤后全部无效时,当前会被视为成功并推进 seq 游标。我已在本地准备最小修复和回归测试:空数组仍视为合法成功;全无效数组保留窗口并重试;混合数组继续保存有效条目。这个补漏应并入 #211,还是单独开一个 PR?

    你在 #226 中提到的另一个小尾巴,即 lastDistilledSeq 目前是进程内存态、重启后会重新蒸馏历史窗口,是否建议另开一个 Issue/PR 单独处理?我倾向于不把游标持久化混入 #211,除非你建议合并处理。

  5. modusensus commented on Sep 18, 2026

    @modusensus
    Member

    先道谢——#226 的增量蒸馏质量很高,review 里那几处边界你也跟得很紧。认领确认:#211 归你,范围按 issue 描述推进(tool/result 与 tool/code-dispatch 读取真实字段、失败状态透传、agent/inbox/spliced 三类子会话交付的采集),照惯例 PR 见。

    两个拆分问题,我们的意见:

    1. 数组过滤边界补漏——建议并入 [Bug] 蒸馏从不采集工具结果与子会话交付:tool/result 与 tool/code-dispatch 读了不存在的 data.output,且 switch 未覆盖 agent/inbox/spliced #211 同一个 PR。它与主缺陷同在蒸馏采集的「窗口推进正确性」域,验收线都是「不丢、不重、不假成功」,拆开反而要两套上下文。但请在 PR 描述里单列一节并配独立回归测试,方便 review 对账。三条口径都同意:空数组=合法成功;全无效=保留窗口重试;混合=保存有效条目。

    2. lastDistilledSeq 持久化——单开 issue/PR,不混入 [Bug] 蒸馏从不采集工具结果与子会话交付:tool/result 与 tool/code-dispatch 读了不存在的 data.output,且 switch 未覆盖 agent/inbox/spliced #211。同意你的判断:游标持久化是 schema 变更,存量库要配幂等迁移,防御等级不一样,混进来会放大 review 面。我们随后开跟踪 issue,把重启重蒸馏的触发条件与游标语义写清楚,届时优先由你认领。

    节奏不赶:#226 刚合入 main,建议动手前先把 collectMessages 的现状对一遍,避免和刚落地的改动打架。

  6. moved this from 📋 Backlog to 🔨 In Progress in dsh-mneme 任务看板on Sep 18, 2026
  7. moved this from 🔨 In Progress to ✅ Done in dsh-mneme 任务看板on Sep 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bug功能异常、崩溃、数据错误(agent 诊断 → 人工确认)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions