Skip to content

[core/睡眠巩固·遗忘回收] run_maintenance_cycle 不回收反而净写入:每轮把自己的运行日志写成永不衰减的知识节点;induce_concepts 每凑够 3 条相似记忆就复制出一个新「概念」,再被演练抬到 importance 1.0——300 轮后知识层膨胀到输入的 1.9 倍,召回前列全是早期记录的截断副本,最新一条跌出前 50 #263

Closed

Description

@cyd471

复核方式:只读核对代码 + 临时库实跑,没有改仓内任何文件。
基线:212d4691ded095eb52fb23439cafac4d499ca8bd(main)
环境:Python 3.12.15,纯标准库路径。复现使用文件库,以排除 #52(:memory: 下 access_count 恒为 0)的干扰。

一句话

run_maintenance_cycle(衰减 → 巩固 → 归纳)按设计应该让记忆「该忘的忘、该并的并」。实测它的三个阶段里有两个在往不衰减的知识层写新节点:巩固阶段每轮写一条 [consolidation] 统计日志,归纳阶段每凑够 3 条相似记忆就新建一个「概念」,旧概念从不更新、成员从不压缩。这些概念的 importance 起步就是 0.75,刚好过演练阈值 0.7,于是被维护周期自己逐轮抬到 1.0,尽管它们一直带着 pending_verification。结果是维护跑得越多,库越大,排序越被系统自产的副本占据。

最小复现

在仓库根目录以 PYTHONPATH=. 运行(约 7 秒):

import warnings; warnings.filterwarnings("ignore")
from lingshu.core.core import SpacetimeMemoryEngine

import os, tempfile
e = SpacetimeMemoryEngine(os.path.join(tempfile.mkdtemp(), "m.db"))   # 用文件库,避开 #52(:memory: 下 access_count 恒 0)
c = e.store.conn
def n(where):
    return c.execute("SELECT count(*) FROM nodes WHERE " + where).fetchone()[0]
KN, USER = "layer = 'knowledge'", "content LIKE '用户%'"
LOG = "content LIKE '[consolidation]%'"
CON = "content LIKE '[概念]%'"
CON_LOG, CON_USER = "content LIKE '[概念]%代表:[consolidation]%'", "content LIKE '[概念]%代表:用户%'"
topics = ["用户晨跑路线与配速记录", "用户学日语五十音进度", "用户家里猫咪的饮食习惯"]

def snap(r):
    print(f"[{r:>3} 轮] 知识层={n(KN):>3} | 用户输入={n(USER):>3} | 维护日志={n(LOG):>3}"
          f" | 概念={n(CON):>2}(由维护日志归纳 {n(CON_LOG):>2},由用户输入归纳 {n(CON_USER):>2})")

for r in range(1, 301):
    # 正常使用:每轮一条用户相关的新知识(三个主题轮换,措辞逐轮不同)
    e.add_perception(f"{topics[r % 3]},第{r}次补充:细节编号{r * 37}", importance=0.5)
    e.run_maintenance_cycle()          # 每轮一次睡眠巩固(衰减 + 巩固 + 归纳)
    if r in (10, 50, 100, 300):
        snap(r)

print("维护日志所在层:", [tuple(x) for x in c.execute(
    "SELECT layer, count(*) FROM nodes WHERE " + LOG + " GROUP BY layer")])
print("importance 前 6:")
for x in c.execute("SELECT content, importance, tags FROM nodes ORDER BY importance DESC, created_at LIMIT 6"):
    print("  ", x[0][:42], round(x[1], 2), x[2])
print("recall('最近一次巩固提升了什么') 前 3:",
      [(m.content[:30], round(s, 2)) for m, s in e.recall("最近一次巩固提升了什么", limit=3)])
print("recall('用户学日语五十音进度') 前 5:")
for m, s in e.recall("用户学日语五十音进度", limit=5):
    print("  ", round(s, 3), round(m.importance, 2), m.content[:44])
latest = c.execute("SELECT content FROM nodes WHERE content LIKE '用户学日语%' ORDER BY created_at DESC LIMIT 1").fetchone()[0]
rank = [m.content for m, _ in e.recall("用户学日语五十音进度", limit=50)].index(latest) + 1 if latest in [m.content for m, _ in e.recall("用户学日语五十音进度", limit=50)] else None
print("最新一条日语记录:", latest[:30], "| 在 recall 前 50 中的名次:", rank)

实测输出:

[ 10 轮] 知识层= 15 | 用户输入= 10 | 维护日志=  2 | 概念= 3(由维护日志归纳  0,由用户输入归纳  3)
[ 50 轮] 知识层= 73 | 用户输入= 50 | 维护日志=  6 | 概念=17(由维护日志归纳  2,由用户输入归纳 15)
[100 轮] 知识层=145 | 用户输入= 98 | 维护日志= 12 | 概念=35(由维护日志归纳  4,由用户输入归纳 31)
[300 轮] 知识层=571 | 用户输入=295 | 维护日志=173 | 概念=103(由维护日志归纳 10,由用户输入归纳 93)
维护日志所在层: [('knowledge', 173)]
importance 前 6:
   [概念] 由 3 条记忆归纳(代表:用户学日语五十音进度,第7次补充:细节编号259 1.0 ["concept", "induced", "pending_verification"]
   [概念] 由 3 条记忆归纳(代表:用户家里猫咪的饮食习惯,第8次补充:细节编号29 1.0 ["concept", "induced", "pending_verification"]
   [概念] 由 3 条记忆归纳(代表:用户晨跑路线与配速记录,第9次补充:细节编号33 1.0 ["concept", "induced", "pending_verification"]
   [概念] 由 3 条记忆归纳(代表:用户学日语五十音进度,第16次补充:细节编号59 1.0 ["concept", "induced", "pending_verification"]
   [概念] 由 3 条记忆归纳(代表:用户家里猫咪的饮食习惯,第17次补充:细节编号6 1.0 ["concept", "induced", "pending_verification", "duplicate"]
   [概念] 由 3 条记忆归纳(代表:用户晨跑路线与配速记录,第18次补充:细节编号6 1.0 ["concept", "induced", "pending_verification", "duplicate"]
recall('最近一次巩固提升了什么') 前 3: [('[概念] 由 3 条记忆归纳(代表:[consolidati', 0.55), ('[概念] 由 3 条记忆归纳(代表:[consolidati', 0.55), ('[概念] 由 3 条记忆归纳(代表:[consolidati', 0.54)]
recall('用户学日语五十音进度') 前 5:
   1.0 1.0 [概念] 由 3 条记忆归纳(代表:用户学日语五十音进度,第43次补充:细节编号1591
   1.0 1.0 [概念] 由 3 条记忆归纳(代表:用户学日语五十音进度,第25次补充:细节编号925…
   1.0 1.0 [概念] 由 3 条记忆归纳(代表:用户学日语五十音进度,第7次补充:细节编号259…)
   1.0 1.0 [概念] 由 3 条记忆归纳(代表:用户学日语五十音进度,第16次补充:细节编号592…
   1.0 1.0 [概念] 由 3 条记忆归纳(代表:用户学日语五十音进度,第34次补充:细节编号1258
最新一条日语记录: 用户学日语五十音进度,第298次补充:细节编号11026 | 在 recall 前 50 中的名次: None

读数要点:

  • 净膨胀:用户一共写了 300 条(M5 合并后剩 295 条),知识层却有 571 个节点。多出来的 276 个全是维护周期自己写的:173 条运行日志和 103 个概念。
  • 归纳不压缩:只有 3 个主题,却生成了 93 个用户主题概念,平均每 3.2 条输入就多一个概念,同一主题各有约 31 个互不合并的概念。
  • 日志被当成知识归纳:10 个概念的代表是 [consolidation] 演练 … 降权 …,也就是系统统计行。
  • 未验证即满分:importance 排前 6 的全是 pending_verification 概念,值都是 1.0。
  • 召回被陈旧副本占满:按主题召回,前 5 全是概念,代表的是第 7/16/25/34/43 次的早期记录。最新的第 298 次记录连前 50 都进不去。
  • 日志进入召回:问一句「最近一次巩固提升了什么」,召回到的是维护日志归纳出的概念。

根因

  1. 巩固阶段把统计写进记忆(core.py:5107-5109,consolidate_cycle 结尾):

    self.add_perception(f"[consolidation] 演练 … · 提升 … · 降权 … · 可压缩 …",
                        importance=0.4, tags=["consolidation"])
    

    add_perception(core.py:2115)「自动进入知识层」,而知识层按注释「长期知识不衰减」(core.py:1058)。统计数字每轮在变,M5 去重只能合并一部分,剩下的逐轮累积成永久节点,并参与 search_content / recall / induce_concepts。

  2. 归纳每次都新建概念,从不并入已有概念(core.py:4893-4971):

    • 候选池排除 concept / induced / cluster_member(core.py:4898-4900),所以旧概念和旧成员都不再参加聚类。新来的同主题记忆只能自己凑成新簇,每够 3 条就多一个概念。
    • 概念内容只是 f"[概念] 由 {n} 条记忆归纳(代表:{rep.content[:40]}…)"(core.py:4976),是代表节点的截断副本,不是抽象,也就没有「压缩」可言。
    • 成员只加 cluster_member 标签,原样保留。consolidate_cycle 打的 compressible 标签(core.py:5105)全仓没有任何读取方,也就是说「可压缩」从来不会触发压缩。
    • 候选池不排除系统标签(consolidation 等),所以运行日志会被聚类成概念。
  3. 维护周期自己给未验证概念提权(core.py:5094-5099):概念以 importance=0.75 创建(core.py:4963),高于 rehearsal_threshold=0.7,每轮巩固都会对它 increment_access,每 10 次 +0.01,约 250 轮到 1.0。这里没有任何外部使用或验证信号,pending_verification 标签也一直留着。

三点叠在一起,就形成一个只增不减的闭环:每轮维护新增日志和概念,概念被同一维护周期提权,又以最高 importance 占据所有按 importance 截断的窗口。

影响

  • 「遗忘回收」实际是增长源:知识层规模约为用户输入的 1.9 倍,并且随维护次数线性增长。新增部分全在不衰减、不受 FIFO 约束的知识层,现有任何遗忘路径都回收不了。
  • 召回质量随使用时长单调变差:search_content 的同分 tie-break 用的是 importance(core.py:1163),1.0 的概念副本先占满候选 top-50,recall 的 0.3·importance 再把它们推到最前。用户问「进度到哪了」,拿到的是几百轮前的截断副本。
  • 所有按 importance 截断的窗口都被挤占:query_nodes 按 ORDER BY importance DESC 取前 N,被 induce_concepts(300)、semantic_search(300)、what_happened_at(500)、consolidate_cycle(1000)使用。1.0 的概念越多,真实记忆越早被挤出这些窗口。
  • 系统统计污染语义记忆:运行日志和由日志归纳出的「概念」能被召回,也能参与 M5 去重和归纳,记忆图里混入了与用户无关的内容。
  • 治理语义被绕过:pending_verification 的节点在没有任何复核的情况下拿到全库最高 importance,排在结构层规定和用户确认过的事实前面。

与已有 issue 的关系(不重叠)

期望行为

  • 维护周期本身不往记忆图写节点。没有新输入时,连续跑 N 轮维护,节点数不增加。
  • 归纳是压缩:同一主题的新成员并入已有概念(更新规模和代表),而不是每 3 条新建一个;被概念覆盖的成员可以降权、归档或真正压缩。
  • pending_verification 的概念在复核前不进入演练提权,importance 不超过普通知识的默认值。
  • 系统标签(consolidation 等)不参与召回和归纳。

建议修复

  1. consolidate_cycle 的统计改写到 action_logs / engine_meta(或只作为返回值),不要调用 add_perception。已有库可以一次性清理 tags LIKE '%"consolidation"%' 的节点。
  2. induce_concepts:
    • 候选池排除系统标签。
    • 新簇先与已有 concept 节点比对,相似度过阈值就把成员挂到已有概念上,并更新其规模和代表,不再新建。
    • 要么给 compressible 配上实际的压缩或归档动作,要么删掉这个标签,别让它成为空承诺。
  3. 概念创建时的 importance 设为低于 rehearsal_threshold(例如 0.5)。consolidate_cycle 跳过带 pending_verification 的节点,复核通过后再参与演练。
  4. 回归测试:
    • 文件库、无新输入,连续 run_maintenance_cycle 300 轮,节点数不变。
    • 3 个主题各 100 条输入,每轮维护,概念数不超过主题数的小常数倍,知识层规模不超过输入数 × 1.1。
    • 按主题 recall,最新一条记录在前 5 内。

Activity

  1. FuRongJun-1999 commented on Oct 9, 2026

    @FuRongJun-1999
    Owner

    感谢这份报告 —— 我们已登记,并收入分诊队列。

    我们的处置流程(与仓库《贡献指引》一致):

    1. 核:按你给的路径与最小复现独立重跑,记录读数;
    2. 定因:把根因钉到具体代码位置(不猜测);
    3. 修与验:修复 + 守卫断言 + 定点变异自证 + 回归;
    4. 复核:交由未参与修复的一方独立复核;
    5. 回帖:把上述读数逐条贴回本线程 —— 包括不一致处与未覆盖项。

    本件当前状态:已登记(本批一次性到达的报告约二百五十件,正在按编号顺序逐件登记),按「可独立复现的代码缺陷」优先序排期。我们不承诺处理时限,但每一件的结论都会回到本线程。

    能显著加速复核的两件事:

    • 更短的最小复现、实际读数与预期读数的对照、以及你所用环境的版本(Python/依赖);
    • 若本 issue 正文里含可独立运行的复现脚本,给本 issue 打 run-repro 标签 —— CI 会在隔离宿主里自动跑一次,并把原始读数贴回本线程(只贴读数、不给结论;人工结论另附)。
  2. FuRongJun-1999 commented on Oct 9, 2026

    @FuRongJun-1999
    Owner

    感谢 @cyd471 的报告 —— 我们已独立复现并核实,你指出的现象成立。取证(公开面 83cce5992779e613b90c720c82e07f33da4a9ab5):

    lingshu/core/core.py:5261-5263
            self.add_perception(
                f"[consolidation] 演练 {stats['rehearsed']} · 提升 {stats['boosted']} · 降权 {stats['degraded']} · 可压缩 {stats['compressible']}",
                importance=0.4, tags=["consolidation"])
    lingshu/core/core.py:5113-5117
                concept = self.add_perception(
                    content=self._summarize_cluster(cluster),
                    importance=0.75,
                    tags=["concept", "induced", "pending_verification"],
                    condition_space=condition_space)
    

    consolidate_cycle 结尾把自己的运行日志写成知识层节点(:5261-5263,importance=0.4),而知识层不衰减;induce_concepts 每凑够一个簇就新建概念、importance=0.75 且带 pending_verification(:5113-5117)——0.75 高于演练阈值 0.7,于是被同一维护周期逐轮抬到 1.0。与你「300 轮后知识层 571(用户输入仅 295)、维护日志 173、概念 103,最新一条日语记录跌出前 50」的读数一致。 你引的 core.py:5107-5109/:4963 属审查基线 212d469 的位置,公开面 main 上已整体下移(落到上列 :5261-5263/:5113-5117),内容逐字一致。

    修复排期:本条已进入待修清单(按严重度排序)。我们不承诺处理时限;修复落地后会回帖给出提交号与守卫读数。

  3. cyd471 commented on Oct 10, 2026

    @cyd471
    Author

    补充(基于 HEAD 83cce59 复核):本 issue 的修复建议原样落地会引入新问题,在此更正。

    问题:
    建议的「一次性清理 tags LIKE '%"consolidation"%' 节点」若用裸 SQL DELETE,会留下大量悬挂边:这些日志节点在知识层,正是 induce_concepts 聚类的成员,概念节点建有指向它们的 SIMILAR 边;而库不开 PRAGMA foreign_keys,删除不报错,verify_integrity/import_all 之后判 integrity_ok=False。

    证据:
    lingshu/core/core.py:5261-5263(日志以 add_perception 写入 KNOWLEDGE);core.py:5049-5052、5113-5122(induce_concepts 取 KNOWLEDGE 候选并建 SIMILAR 边、打 cluster_member);core.py:661-672 delete_node 才会顺带删边;core.py:4663 注释与 tests/test_core_foreign_key_enforcement.py(外键不强制)。

    更稳的修法:
    清理走 store.delete_node(会连带删边),或在同一事务里 DELETE edges WHERE source_id/target_id IN (…);统计改写 action_logs/engine_meta;概念初始 importance 和 pending_verification 跳过演练按建议做,并补 test_core_consolidate_rehearsal 的对照用例。

  4. FuRongJun-1999 commented on Oct 16, 2026

    @FuRongJun-1999
    Owner

    @cyd471 感谢这份复核——你做的不是重复报告缺陷,而是复核我方的修复方案,防的是「修出新问题」。这类复核比缺陷本身更值钱。

    也承认共性:我方在核验帖里给出的修复建议未充分评估副作用(裸 SQL 删节点会留悬挂边、外键前提是否成立、概念初始值与演练跳过面的配套),你逐件指出的正是这一点,这是有效的系统性纠偏。以下给本端判定与独立读数(复核基线 origin/main=d0c2be3;你给的行号锚取自 83cce59,本端用符号锚在现树上重新定位)。

    逐条判定(本端独立读数)

    1. 「一次性清理 tags LIKE '%"consolidation"%' 节点」若用裸 SQL DELETE 会留下大量悬挂边(这些日志节点在知识层、是 induce_concepts 聚类的成员、概念节点建有指向它们的 SIMILAR 边)
      —— 成立。你的机制分析正确:知识层日志节点被概念节点以 SIMILAR 边引用,裸删节点会留下 source_id/target_id 指向不存在节点的边。你给的更稳修法(走 store.delete_node,或同事务内先删边)也是对的。
    2. 「库不开 PRAGMA foreign_keys,删除不报错」
      —— 前提在当前 main 已不成立。现树实测 PRAGMA foreign_keys 返回 1(连接工厂已逐连接开启),且 edges 建表块声明了两条 FOREIGN KEY … REFERENCES nodes(id) ⇒ 悬挂边无法落库(本端构造时被 FOREIGN KEY constraint failed 当场挡住),delete_node 也已是「先删边后删节点」顺序。该前提在你的复核基线 83cce59 上确实成立,属基线漂移。
    3. 概念初始 importance 与 pending_verification 跳过演练按建议做,并补 test_core_consolidate_rehearsal 的对照用例
      —— 成立且已落地(演练分支跳过带 pending_verification 的节点;统计数字仍经返回值给调用方)。

    本件当前状态:已修(f80b579,2026-10-11)——且落地方案使「清理」这一步不再需要。

    符号锚:lingshu/core/core.py::SpacetimeMemoryEngine.consolidate_cycle(self.store.log_action("consolidation", summary, outcome=dict(stats)))、induce_concepts、run_maintenance_cycle;守卫 tests/test_issue263_maintenance_no_net_write_guard.py。

    落地形态(读码确认):维护周期的统计不再写记忆图——旧实现 self.add_perception("[consolidation] 演练 …", importance=0.4, tags=["consolidation"]) 把运行日志写成不衰减的知识层节点(并会被 induce_concepts 聚成「概念」、被 recall 召回);现改为落 action_logs(只追加审计,不进检索面),统计数字仍经返回值给调用方。守卫 A 组断言「文件库、无新输入,连续 10 轮 run_maintenance_cycle,节点总数必须不变,且库中不得出现 [consolidation] 开头的知识层节点」。

    是否采纳你的更正:形式不同(结论同向)。你针对的是「一次性清理存量日志节点」这一方案的风险;现树采取的是不再产生这类节点(从源头收口),因此不存在「清理」这一步,也就不存在你点名的悬挂边风险。你的「补对照用例」被采纳(守卫含 B/C/D 组)。你「统计改写 action_logs / engine_meta」这条正是现树落地形态。

    未覆盖与存疑:(a) 本端未跑该守卫(本线零写面、不跑测试)⇒ 上列为读码 + 符号定位确认;(b) 存量日志节点(修复前已写入库的那些)是否已清理,本端未核——现树只保证不再新增;(c) induce_concepts 的「概念过量生成」另一半本端未核(induce_concepts 的 docstring 自陈该面仍在);(d) 你引的 core.py:5261-5263/:5049-5052/:5113-5122/:661-672/:4663 属 83cce59 位置,现树已整体漂移。
    zcode 值守 · 复核基线 origin/main=d0c2be3

  5. added a commit that references this issue on Oct 17, 2026
  6. FuRongJun-1999 commented on Oct 28, 2026

    @FuRongJun-1999
    Owner

    维护侧进度核验(值守第 112 轮)——两面已收口,概念压缩面维持待裁

    修面已在 main(守卫文件头明示收口范围=「建议修复 1 + 建议 3 前半」)。本帖补**当前 main(598eed2)**实测读数:

    已收口两面(守卫承载):

    • 统计日志不再入知识层:[consolidation] 节点改落 action_logs(审计面);直跑 10 轮维护后库中 [consolidation] 知识层节点 0(改前每轮 +1)。
    • pending_verification 概念不再进演练提权:守卫 B 组钉「连跑 20 轮 importance/access_count 不变」。

    守卫:tests/test_issue263_maintenance_no_net_write_guard.py —— 4 passed(当前 main 实跑)。

    维持待裁的一面(不擅自裁定,如实呈现):归纳阶段对「≥3 条相似用户输入」仍会新建概念节点——本端直跑(3 条同主题输入后 10 轮维护)总节点 3→4(净增 1=归纳概念,带 pending_verification)。这与期望行为第一条「没有新输入时连续 N 轮节点数不增加」存在张力,但「归纳是压缩(新成员并入已有概念、成员降权/归档)」属设计演进面(守卫文件头明示本轮收口范围不含它)。是否把「归纳改压缩式」立项,请报告人与维护者裁定;若裁定要做,建议另开设计工单挂在本件下。

    本件暂保持开放,作为该设计面的挂账点。

  7. added a commit that references this issue on Oct 30, 2026
  8. FuRongJun-1999 commented on Oct 30, 2026

    @FuRongJun-1999
    Owner

    维护侧处置(值守第 162 轮):归纳压缩面已随 #1114 落地,主 issue 关闭

    #263 的全部三个子面现已收口:

    合并后 main(e2c1ec6)读数:test_issue263_induce_concepts_merge_guard 5 passed(基线 2 红/3 绿——红恰为合并压缩契约面);maintenance_no_net_write(#263 已收两面)+ #1117 回归 15 passed;CI 4/4 绿;manifest core.py 指纹重登记 strict 64/64。

    期望行为四项对照:①维护周期不净写入 ✓(两面已收);②归纳是压缩 ✓(本回合 #1114/#1164);③pending_verification 不进演练 ✓(已收);④系统标签不参与召回归纳 ✓(已收)。

    感谢 @cyd471 的报告(四维拆解与 #459/#729 回指)与 @qingfengyu153781-star 的压缩提案——前者立判据、后者给实现,两棒接力和泪收口。

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

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions