Skip to content

autoSummarize 每个 turn/end 都触发且无产出上限——建议新增节流、条数上限与同会话主题抑制(实测单会话日铸 198 条) #127

Closed

Description

@483218131

现象

src/summarize.js:356-365:监听 session/event,event.type !== "turn/end" 直接返回,否则执行蒸馏;只有同一会话的 inFlight 互斥,没有频率下限(对照 autoDream 有 dreamMinIntervalMinutes)。

产出条数没有上限:parseSummaryJson 之后逐条落库(summarize.js:318-330),条数完全由模型输出决定。

本机实测(0.7.31):

  • 09-11 当天 autoSummarize 成功 84 次(llm_audit_logs,trigger_source='autoSummarize');
  • 单个会话 session-39c42c2b-… 一天落库 198 条;
  • 同一事实「Win7 PowerShell 2.0 下 OpenSSH 官方脚本失效」在 6 小时内被铸出 28+ 条(先后含 36 条,后由 dream 归档)。

为什么写入端去重拦不住

service.js:903-905 的去重条件是「同类型 + 标题 trim() 后完全相等 + 该类型 importance 排序前 100 条」。而蒸馏每次都会重写标题措辞——本次审计中,cos≥0.85 的重复簇里「标题归一化(去空格与标点)后完全相同」的簇为 0 个。也就是说这类重述在写入端必然穿透。

影响

09-11 当天新增 409 条、receipt_chain 合并 59 条;活跃记忆 3 天内从 276 涨到 855。清理速度追不上写入速度,重复与冲突队列(见 #126)持续累积。

建议

  1. 新增 summarizeMinIntervalMinutes(语义对齐 dreamMinIntervalMinutes);
  2. 单次蒸馏的产出条数上限(可在 prompt 与落库两侧同时约束);
  3. 同一会话内的主题抑制:同一事实在 N 小时内不重复落库(可用标题归一化 + 向量相似度近似实现);
  4. 或让 summarize 落库前走一次向量查重(与"写入端去重改为向量"是同一处实现,可合并落地)。

复现方式

按 llm_audit_logs.trigger_source='autoSummarize' 统计调用次数;按 memories.source 分组并对标题做归一化聚类,即可看到同一会话的重述率。


相关 issue

环境与现场

  • dsh-mneme v0.7.31(npm latest)· DSH 0.1.1-rc.2 · Windows · Node v22.22.2
  • embedding:OpenAI 兼容(nvidia/llama-nemotron-embed-vl-1b-v2:free,2048 维,/semantic 返回 ready:true)
  • 库况快照(2026-09-11):总 2804 / 活跃 818 / 归档 1986 / conflict_pending 挂起 121(已解决 0)
  • 生效配置(GET /api/dsh-mneme/features,优先于 cordis.patch.yml):autoDream=true、sleepModeEnabled=true、heatEnabled=true、conflictFreezeEnabled=true、allowCrossTypeMerge=false、dreamSkipInvalid=true、dreamMinIntervalMinutes=3、dreamMaxSnapshotSize=100、memoryQualityFilter.enabled=true
  • 审计方法(可复现):用 node:sqlite 只读打开 memory.db,对活跃记忆读取 embedding(JSON 数组,2048 维)归一化后做余弦聚类;窗口口径与 src/dream.js:600-612 一致(活跃非 summary,按 updated_at 倒序取前 dreamMaxSnapshotSize)

Activity

  1. self-assigned this
    on Sep 11, 2026
  2. Anans-Amarre commented on Sep 11, 2026

    @Anans-Amarre
    Collaborator

    感谢实测数据(单会话日铸 198 条、同一事实 6 小时铸 28+ 条)。节流 + 条数上限 + 同会话主题抑制三个方向我们都认同,对照 autoDream 已有 dreamMinIntervalMinutes,autoSummarize 确实该补上。

    这个属于 v0.8 的增强方向,欢迎直接提 PR,也可以先在 issue 里把节流参数设计讨论清楚再动手。

  3. 483218131 commented on Sep 12, 2026

    @483218131
    ContributorAuthor

    以下为设计提案(尚未实现)。代码引用基于本机安装的 v0.7.31。


    一、现状事实

    事实 位置
    触发 = session/event 且 event.type === "turn/end",每轮都跑;仅同一会话 inFlight 互斥 src/summarize.js:356-365、199-205
    无频率下限(对照 autoDream 已有 dreamMinIntervalMinutes) —
    产出条数无上限:parseSummaryJson 后逐条 saveWithDedupe,条数由模型输出决定 src/summarize.js:318-330
    去重拦不住重述:saveWithDedupe = 同类型 + 标题 trim() 后全等 + 该类型 importance 前 100 条 src/service.js:903-905
    输入侧唯一可调项 = distillMaxChars(默认 24000,截断语义) src/summarize.js:223

    实测代价(本机,0.7.31):09-11 单日 autoSummarize 成功 84 次;单会话 session-39c42c2b… 一天落库 198 条;同一事实(Win7 PowerShell 2.0 下 OpenSSH 官方脚本失效)6 小时内被铸出 28+ 条。当天新增 409 条、receipt_chain 合并 59 条 → 活跃记忆 3 天从 276 涨到 855。

    二、三个参数(默认值全部 = 零行为变化)

    键 类型/范围 默认 语义
    summarizeMinIntervalMinutes int 0–10080 0 同一会话两次蒸馏之间的最小间隔;0 = 现状(不限)。与 dreamMinIntervalMinutes 语义对齐:失败也占用间隔,防止失败连发
    summarizeMaxEntriesPerRun int 0–50 0 单次蒸馏最多落库条数;0 = 不限
    summarizeDedupeMode off | title | vector off 落库前的同会话去重策略(见 §3.3);off = 现状
    summarizeDedupeMinSim 0.5–0.99 0.92 vector 模式的并入阈值(title 模式不使用)
    summarizeDedupeWindowHours int 0–168 24 去重回看窗口:只看该会话最近 N 小时内落库的记忆

    五项都进 feature_flags 白名单(src/settings.js:74-83 的 FEATURE_FLAG_INT_RANGES + FEATURE_FLAG_ENUMS),使面板可即时开关、也是线上回滚开关——沿用仓库既有风格。

    三、实现要点

    3.1 间隔门(per-session)

    createSummarizer 内部已有 per-session inFlight Map(summarize.js:199),旁边加一个同样以 session.id 为键的 lastRunAt Map:

    const gapMs = (config.summarizeMinIntervalMinutes ?? 0) * 60000;
    const last = lastRunAt.get(session.id) ?? 0;
    if (gapMs > 0 && Date.now() - last < gapMs) return;   // 静默跳过
    • 进入函数即打点(不在成功后才打点),保证失败也占间隔——与 dreamMinIntervalMinutes 的既有语义一致(config.js:52 注释:失败/degraded 的 run 也占用间隔)。
    • aborted(AbortError)不写点,避免会话关闭误伤下一次。
    • Map 随会话 dispose 清理(现有 inFlight.clear() 处一并清)。

    3.2 条数上限

    parseSummaryJson 之后、落库循环之前截断:

    const cap = config.summarizeMaxEntriesPerRun ?? 0;
    const entries = cap > 0 ? parsed.slice(0, cap) : parsed;

    被截断的条数写审计(见 §4),便于用户调整上限。

    3.3 落库前去重(summarizeDedupeMode)

    先纠正一个我起初写偏的地方:本条最初写成"纯确定性标题归一化",理由是"确定性逻辑不用 LLM"。那是把仓库「确定性流程禁用 LLM」的规则过度套用了——那条规则针对的是路由 / 重试 / 状态码这类规则性流程;而"两条记忆是不是同一件事"是语义判断,本来就该由向量或 LLM 来做。而且本设计引用的实测数据自己就否证了纯标题方案:cos≥0.85 的重复簇里"归一化标题全同"的簇为 0 个。

    因此改为分层,默认不带 LLM,但把语义能力放在正确的位置:

    层 手段 成本 职责
    快路径 标题归一化(toLowerCase + 去空白/标点) ≈0 命中即并入;只拦完全同名的高频重述,不承担主要职责
    主路径 向量近邻(复用已有 embedding 列 + store 的余弦比对) 一次比对,无 LLM 拦"同一事实换措辞"——这是重述的主体
    语义细粒度 LLM 每次调用 不放在写入端:留给 dream / sleep 的批量裁决(一次调用成组处理,成本被摊薄),#126 的动作集扩展正是为它补齐 merge / differentiate 出口

    vector 模式流程(写入端,无 LLM):

    1. 取该会话 summarizeDedupeWindowHours 内落库的记忆(store.list({ source: \session:${session.id}` })后按created_at 过滤,source` 字段已带会话 id);
    2. 对每条新 entry 计算与这些既有条目的余弦相似度(同类型内比较,向量缺失则跳过该条);
    3. sim ≥ summarizeDedupeMinSim → 并入既有条目(service.update 追加内容,沿用 appendContent 的 \n\n---\n[ts] 形式,content_history 留痕);
    4. 低于阈值 → 正常 saveWithDedupe(跨会话的同类事实仍交给 autoDream / sleep 的合并路径)。

    为什么不把这个交给 LLM:写入端每次蒸馏都要判定,调用量与 84 次/天同量级,成本与延迟都会显著抬升;而 dream / sleep 已有批量裁决通道与 receipt 审计,语义判断放那里更划算——这也是本设计不去抢 #126 职责的原因。

    title 模式仅作可选的极低成本档(默认 off),保留它是因为"完全同名"虽少但确实存在(例如同一会话里反复出现的同名结论),且它零成本、零风险。

    3.4 与 distillMaxChars 的关系

    distillMaxChars 限制输入(转录截断),本设计限制输出与频率。两者互补,互不替代;文档里建议并列说明。

    四、可观测性

    llm_audit_logs.status 已有 skipped 取值(schema 注释明确列了 success | error | skipped),但当前只写 success/error。建议:

    • 被间隔门跳过 → 写一行 status='skipped'、error_message='min-interval';
    • 被条数上限截断 → 正常写 success,并在 metadata 记 {parsed: N, capped: M};
    • 被去重并入既有条目 → metadata 记 {deduped: K, mode: 'vector'|'title', maxSim: 0.94}。

    这样升级后可以用一条 SQL 直接验证节流是否生效(现在完全不可观测,也是本 issue 难以被用户自查的原因)。

    五、向后兼容

    • 三个默认值(0 / 0 / 0)逐一等价于现状:无间隔门、不截断、不抑制。
    • 不改变任何既有落库路径(saveWithDedupe 语义不动);主题抑制开启后才走 update 分支。
    • 面板新增三个数字输入(FEATURE_FLAG_INT_RANGES 驱动,前端已有同类控件)。

    六、测试清单(建议)

    1. summarizeMinIntervalMinutes=0 → 与 v0.7.31 行为一致(连续两次 turn/end 都触发)。
    2. =30 → 第二次 turn/end 在 30 分钟内不触发;第一次失败也一样占间隔。
    3. summarizeMaxEntriesPerRun=5、模型返回 12 条 → 只落 5 条,审计 metadata 记 {parsed:12, capped:7}。
    4. summarizeDedupeMode='vector'、MinSim=0.92:构造同一事实的两种措辞(本机实测余弦 0.94 量级)连落三次 → 只落 1 条,后两次并入(content_history 有记录),不新增记忆行;同时断言 sim 低于阈值的一组确实新建。
      4b. summarizeDedupeMode='title':仅完全同名被并入,措辞不同的正常新建(用于确认该档的边界,避免误以为它能替代向量档)。
      4c. 向量缺失(embedding IS NULL)的既有条目不参与比对,不抛错。
    5. 抑制只作用于本会话:另一会话的相同标题正常落库。
    6. 会话 dispose 后 Map 清理,无内存泄漏(既有测试模式)。
    7. 三个参数都在 feature_flags 白名单内(设置→读取 round-trip 通过校验)。

    七、与 #47 的关系(顺带回答)

    #47「如何做到临时关闭记忆模式?」目前只有 autoSummarize: false 这种全局极端开关。本设计的 summarizeMinIntervalMinutes 提供了一个中间档(例如设 1440 = 一天最多蒸馏一次),可以在不彻底关闭记忆的前提下把产量压下来;文档里可以把它列为 #47 的一个可选答案。

    八、待确认的几个点

    1. 三个参数命名是否与既有风格一致(对照 dreamMinIntervalMinutes / distillMaxChars)?是否需要把 summarizeMaxEntriesPerRun 与 distillMaxChars 合并成一个"预算"概念?
    2. 默认值是否同意全部取 0(零行为变化)?还是希望 summarizeMinIntervalMinutes 直接给一个非零默认(会改变现有用户行为)?
    3. 去重档位:默认 off 可以接受吗?如果希望开箱即用,是否把 vector 作为默认(阈值 0.92),还是担心小 embedding 模型误折叠而保持 opt-in?命中时的动作:并入既有条目(本文档建议,content_history 兜底)还是直接跳过丢弃?
    4. 去重与「写入端向量去重」的关系:本设计的 vector 档是写入端语义去重的实现之一(作用域 = 同会话 + 时间窗)。若维护者更希望它做成全局的写入端去重(不限会话),请指定;我会把参数与作用域改成对应形态——两者共用同一段比对逻辑,不重复实现。
    5. 审计埋点是否同意用 status='skipped' + metadata(现有表结构即可,无需 schema 变更)?
    6. codingRetrospect 路径:已核实它只切换 prompt(src/summarize.js:243),入口、触发与落库循环完全共用同一个 summarize() → 本设计的三项参数天然同时覆盖两条路径,无需单独处理。若维护者希望两者分开节流,才需要额外设计。
  4. modusensus commented on Sep 12, 2026

    @modusensus
    Member

    设计帖照准,五个参数与实现要点全部按你的拟案实施 👍 逐条回应「待维护者确认」:

    1. 命名 ✓:与 dreamMinIntervalMinutes / distillMaxChars 风格一致;summarizeMaxEntriesPerRun 不与 distillMaxChars 合并——一个限输入(截断转录)、一个限输出(落库条数),单位与语义都不同,按你 §3.4 的说法文档并列说明即可。
    2. 默认全 0 ✓:零行为变化,用户自己拧阀门。这也让回滚变成"把键清空"。
    3. 去重档位:默认 off 可接受(仓库 opt-in 惯例,searchSemanticDedup 同款);命中动作取并入既有条目(content_history 兜底可回溯),不做直接丢弃。作用域先按「同会话 + 时间窗」——全局写入端语义去重是另一个更大的话题(且与 searchSemanticDedup 的检索端去重容易混淆),不并进这个 PR,将来若要做会单独开 issue 并复用你这段比对逻辑。
    4. 审计埋点 ✓:status='skipped' + metadata,零 schema 变更,这个设计让节流效果一条 SQL 可验证——比参数本身更有价值。
    5. codingRetrospect ✓:共用入口已被你核实,三项参数天然覆盖,不拆分。

    可观测性那节(§4)和「把节流档位列为 #47 的中间档答案」(§7)都是加分项,尤其后者正好补上"全局开关之外没有中间态"的空缺。

    落地顺序:建议本条最先做(改动面最小、完全独立于另外两条),PR 直接对 main。测试按你 §六 的清单走,额外补一条「间隔门在会话 dispose 后重置」(新会话不受旧会话 lastRunAt 影响——如果你已经在 §3.1 的 Map 清理里覆盖了就忽略这条)。

  5. added 2 commits that reference this issue on Sep 12, 2026
  6. added a commit that references this issue on Sep 12, 2026
  7. modusensus commented on Sep 12, 2026

    @modusensus
    Member

    #127 已在 v0.7.32 落地(PR #143):summarize 节流 + 每会话上限 + 同会话去重;PR #144 补 fail-safe 边界。

  8. moved this from 📋 Backlog to ✅ Done in dsh-mneme 任务看板on Sep 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions