Repository navigation
[core/睡眠巩固·遗忘回收] run_maintenance_cycle 不回收反而净写入:每轮把自己的运行日志写成永不衰减的知识节点;induce_concepts 每凑够 3 条相似记忆就复制出一个新「概念」,再被演练抬到 importance 1.0——300 轮后知识层膨胀到输入的 1.9 倍,召回前列全是早期记录的截断副本,最新一条跌出前 50 #263
Description
复核方式:只读核对代码 + 临时库实跑,没有改仓内任何文件。
基线: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 都进不去。
- 日志进入召回:问一句「最近一次巩固提升了什么」,召回到的是维护日志归纳出的概念。
根因
-
巩固阶段把统计写进记忆(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。 -
归纳每次都新建概念,从不并入已有概念(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等),所以运行日志会被聚类成概念。
- 候选池排除
-
维护周期自己给未验证概念提权(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 的关系(不重叠)
- [core/海马体·睡眠巩固] 情境层→长期层的巩固通路整条不存在:门控的「情境层」判定不落库、promote_context_memories 全仓零调用;睡眠巩固反把情境记忆压到 importance=0 后卡在删除线下永久留存(forget_advisor 归档值恰好等于删除线,归档即永生) #214:讲的是情境层到长期层的巩固通路缺失,以及情境节点被压到 0 后成为僵尸。本 issue 讲的是知识层被维护周期自产节点撑大,以及概念被提权,两者路径和后果都不同。
- [core/归纳] induce_concepts 倒排预筛丢掉池内 DF>半数的二元组:某主题占候选池过半时永远聚不成概念(同主题 10 条 Jaccard 0.8 + 无关 9 条 → 0 个) #166:讲的是
induce_concepts倒排预筛让大主题聚不成概念。本 issue 是反方向:常规输入下概念过量生成、从不合并。两者可以同时成立。 - [core] consolidate_cycle 用增量前的过期副本判 %10:高重要度记忆第 1 轮即被提权,第 10 次演练漏提、第 11 轮才补 #94 / fix(core): consolidate_cycle 提权判据改用增量后计数——首轮不再误提权、第 10 轮准点提权 #104:讲的是演练分支
%10判据早一轮或晚一轮的时序问题。本 issue 讲的是谁有资格进演练分支:未验证概念 0.75 起步、高于阈值,于是被维护周期自己抬到 1.0。修好 [core] consolidate_cycle 用增量前的过期副本判 %10:高重要度记忆第 1 轮即被提权,第 10 次演练漏提、第 11 轮才补 #94 不改变这个结论。 - core: increment_access 在 :memory:(引擎默认)后端静默失效——另开连接落进空库,access_count 恒 0,consolidate_cycle 分流失真 #52:
:memory:下 access_count 恒为 0。本复现用文件库,结论不依赖它。 - [core] 检索按插入序硬截断无 ORDER BY:600 条记忆只返回前 300,老记忆永久不可达 #35:检索按插入序截断。本复现中主题命中行远少于 300,不触发那个截断。
- [core/记忆] add_perception 的 M5 去重命中时丢弃新输入的 entities / tags / importance / modality,返回值与「写入成功」无法区分 #223 / [core/负记忆][M5×P1-3] M5 去重无视成败极性:同一操作序列的失败经验被并进「成功」节点并给它增信(5 次失败把成功节点置信度从 0.50 推到 0.60),失败节点从记忆图中消失;反过来,成功也会被并进失败节点 #225 / [core/记忆] M5 去重对否定词与数值不敏感:「不过敏」「7.5 毫克」「尚未修复」被当作重复吞掉,原命题反而增信;更正 25 次后错误事实置信度 1.0 #142(M5 去重语义)、[core] 「不可遗忘」保护在两条遗忘路径上完全失效:衰减周期与情境层上限均不检查保护标记 #36(保护失效)、[core/衰减] decay_cycle 情境层只处理 importance > min_confidence 的节点:importance ≤ 阈值的情境记忆永不衰减、永不删除,越不重要越长寿 #207 / [core/遗忘] forget_advisor 三处错误:importance=0.0 被
or 0.5当成 0.5 不归档;「降权」把低于 archived_imp 的节点提到 0.1;归档值正好落在衰减死区,归档节点永久驻留 #210(衰减与归档阈值)均无交集。
期望行为
- 维护周期本身不往记忆图写节点。没有新输入时,连续跑 N 轮维护,节点数不增加。
- 归纳是压缩:同一主题的新成员并入已有概念(更新规模和代表),而不是每 3 条新建一个;被概念覆盖的成员可以降权、归档或真正压缩。
pending_verification的概念在复核前不进入演练提权,importance 不超过普通知识的默认值。- 系统标签(
consolidation等)不参与召回和归纳。
建议修复
consolidate_cycle的统计改写到action_logs/engine_meta(或只作为返回值),不要调用add_perception。已有库可以一次性清理tags LIKE '%"consolidation"%'的节点。induce_concepts:- 候选池排除系统标签。
- 新簇先与已有
concept节点比对,相似度过阈值就把成员挂到已有概念上,并更新其规模和代表,不再新建。 - 要么给
compressible配上实际的压缩或归档动作,要么删掉这个标签,别让它成为空承诺。
- 概念创建时的 importance 设为低于
rehearsal_threshold(例如 0.5)。consolidate_cycle跳过带pending_verification的节点,复核通过后再参与演练。 - 回归测试:
- 文件库、无新输入,连续
run_maintenance_cycle300 轮,节点数不变。 - 3 个主题各 100 条输入,每轮维护,概念数不超过主题数的小常数倍,知识层规模不超过输入数 × 1.1。
- 按主题
recall,最新一条记录在前 5 内。
- 文件库、无新输入,连续
Activity
FuRongJun-1999 commentedon Oct 9, 2026
感谢这份报告 —— 我们已登记,并收入分诊队列。
我们的处置流程(与仓库《贡献指引》一致):
本件当前状态:已登记(本批一次性到达的报告约二百五十件,正在按编号顺序逐件登记),按「可独立复现的代码缺陷」优先序排期。我们不承诺处理时限,但每一件的结论都会回到本线程。
能显著加速复核的两件事:
run-repro标签 —— CI 会在隔离宿主里自动跑一次,并把原始读数贴回本线程(只贴读数、不给结论;人工结论另附)。FuRongJun-1999 commentedon Oct 9, 2026
感谢 @cyd471 的报告 —— 我们已独立复现并核实,你指出的现象成立。取证(公开面
83cce5992779e613b90c720c82e07f33da4a9ab5):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),内容逐字一致。修复排期:本条已进入待修清单(按严重度排序)。我们不承诺处理时限;修复落地后会回帖给出提交号与守卫读数。
cyd471 commentedon Oct 10, 2026
补充(基于 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 的对照用例。
FuRongJun-1999 commentedon Oct 16, 2026
@cyd471 感谢这份复核——你做的不是重复报告缺陷,而是复核我方的修复方案,防的是「修出新问题」。这类复核比缺陷本身更值钱。
也承认共性:我方在核验帖里给出的修复建议未充分评估副作用(裸 SQL 删节点会留悬挂边、外键前提是否成立、概念初始值与演练跳过面的配套),你逐件指出的正是这一点,这是有效的系统性纠偏。以下给本端判定与独立读数(复核基线
origin/main=d0c2be3;你给的行号锚取自83cce59,本端用符号锚在现树上重新定位)。逐条判定(本端独立读数)
tags LIKE '%"consolidation"%'节点」若用裸 SQLDELETE会留下大量悬挂边(这些日志节点在知识层、是induce_concepts聚类的成员、概念节点建有指向它们的 SIMILAR 边)—— 成立。你的机制分析正确:知识层日志节点被概念节点以 SIMILAR 边引用,裸删节点会留下
source_id/target_id指向不存在节点的边。你给的更稳修法(走store.delete_node,或同事务内先删边)也是对的。PRAGMA foreign_keys,删除不报错」—— 前提在当前 main 已不成立。现树实测
PRAGMA foreign_keys返回 1(连接工厂已逐连接开启),且edges建表块声明了两条FOREIGN KEY … REFERENCES nodes(id)⇒ 悬挂边无法落库(本端构造时被FOREIGN KEY constraint failed当场挡住),delete_node也已是「先删边后删节点」顺序。该前提在你的复核基线83cce59上确实成立,属基线漂移。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=
d0c2be3fix(#214): 路 A 落地——`run_maintenance_cycle` 接进 `promote_context_memori…
FuRongJun-1999 commentedon Oct 28, 2026
维护侧进度核验(值守第 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 轮节点数不增加」存在张力,但「归纳是压缩(新成员并入已有概念、成员降权/归档)」属设计演进面(守卫文件头明示本轮收口范围不含它)。是否把「归纳改压缩式」立项,请报告人与维护者裁定;若裁定要做,建议另开设计工单挂在本件下。本件暂保持开放,作为该设计面的挂账点。
Merge pull request #1164 from FuRongJun-1999/fix/induce-concepts-merg…
FuRongJun-1999 commentedon Oct 30, 2026
维护侧处置(值守第 162 轮):归纳压缩面已随 #1114 落地,主 issue 关闭
#263 的全部三个子面现已收口:
test_issue263_maintenance_no_net_write_guard承载。e2c1ec6)落地——induce_concepts命中既有概念时并成员/更新代表而非新建(300 轮/3 主题从 99 个副本收敛 +30/−1)。作者提案即该待裁面的裁定输入,审读通过即采纳。合并后 main(
e2c1ec6)读数:test_issue263_induce_concepts_merge_guard5 passed(基线 2 红/3 绿——红恰为合并压缩契约面);maintenance_no_net_write(#263 已收两面)+#1117回归 15 passed;CI 4/4 绿;manifestcore.py指纹重登记 strict 64/64。期望行为四项对照:①维护周期不净写入 ✓(两面已收);②归纳是压缩 ✓(本回合 #1114/#1164);③pending_verification 不进演练 ✓(已收);④系统标签不参与召回归纳 ✓(已收)。
感谢 @cyd471 的报告(四维拆解与 #459/#729 回指)与 @qingfengyu153781-star 的压缩提案——前者立判据、后者给实现,两棒接力和泪收口。