Files
CyberGril/docs/r3/REPORT-HEL-832.md
T

18 KiB
Raw Blame History

R3 真实记忆接入核对报告(HEL-832)

性质:只读核对 + 施工收敛方案。基准 v1.10(59 项);读取提交 c285a0fd07e9d20e4fd4984942f3ae2fb8cbbd0c(分支 agent/agent/a67b70ddbb63;产品目标 origin/agent/agent/de8874a3803b = db571ba,是 c285a0f 的祖先,落后 12 个提交)。本轮未写产品代码、未启动服务、未发任何真实模型请求、未触碰 8300/8310/8320/8340 与任何用户数据。中文 FTS 结论来自隔离临时库实测(证据:docs/r3/fts-probe-HEL-832.txt)。 需求依据:R1–R7 顺序获批原话 01a1629c-732f-7559-8417-6f2ea05c7151「同意按这个顺序开工」;R3 范围 = sources/integration-plan-v1.md §3 R3(成功对话写 mem_episodic:raw + low 档 gist 压缩 + affect 粗估;chat 上下文加 FTS top-k;reinforce/decay 定期跑批;回忆页 episodic 层接真;不做向量/语义/关系层整合)。AM-001 原话 01a15f42-2781-700e-b75a-3ae4b023f46c「整套认可,1选等她讲过才显示…」。CG-013/014/015 底色/诠释/成长分离依据 01a15772+01a15774「同意你的推荐」。本报告的工程取舍均为建议,不构成用户新决定。

1. 现状核对(c285a0f,已有能力 vs 占位)

1.1 存储与 schema

事实 证据 判定
mem_episodic(raw/gist/affect/salience/accessibility/recall_count/consolidated/withheld/source)、mem_semantic、mem_relational 表已建 src/cybergril/memory/schema.sql:92-137 表可用,但全库无写入方(见 1.2)
Store.add_episodic/decay_accessibility/reinforce/search_fts 已实现(同步 FTS 维护、指数衰减 floor=0.03、间隔效应) src/cybergril/memory/store.py:102-171 API 可复用;search_fts 直传用户原文给 MATCH,不安全(§2.2)
FTS 索引 mem_fts 用 tokenize='unicode61 remove_diacritics 2',外部内容表挂 mem_episodic schema.sql:176-180 对中文不可用(§2.1);结构可照搬换 trigram
向量表 mem_vec 仅注释 schema.sql:182-186 与「不上向量」一致,保持
聊天表 chat_message(id,ts,role,text,turn_id,status),UNIQUE(turn_id,role),status ∈ pending/sent/failed/cancelled schema.sql:193-201 R3 幂等键可挂 turn_id(建议加列,§4.1)
R2 作业表模式:growth_job(source,ref_id,state,owner,lease_until)、sharing_attempt、life_run 租约恢复 src/cybergril/memory/r2.sql:2-49 R3 记忆作业照此模式新建,不动旧表

结论:真实记忆未实现。 grep 全 src/:add_episodic/search_fts/decay_accessibility/reinforce 除 memory/store.py 自身定义外零调用方;tests/test_store.py:41-53 只测插入,从不测中文检索。近 20 轮上下文(api/app.py:468-471)只是聊天记录,系统提示自己声明「长期记忆…仍未接通。近20轮不是长期记忆」(app.py:478)。不能把已有表或近 20 轮上下文当成真实记忆已实现。

1.2 聊天成功/取消/断连/接收确认(R3 写入的触发面,全部已验证)

路径 行为 证据
成功 同一事务:INSERT companion 'sent' + UPDATE user 'sent' + sharing.prepare 回执 api/app.py:553-565
取消(新轮次/主动取消) active["cancel"].set();generate 提前 return 不写;finally 把 user 置 cancelled app.py:439-443, 549-550, 583-588
断连 await request.is_disconnected() 即返回,不落 sent app.py:532, 549
失败 异常 → status='failed',user 行置 failed app.py:566-570, 583-588
重启 启动时 pending→cancelled,epoch+1 持久 app.py:196-201
重放 同 turn_id 同文重发 → 直接回放已存 sent 文本,不再调模型 app.py:444-456
接收确认 POST /api/chat/received → Sharing.acknowledge:status='sent' + token/text_hash 常量时间比对 + 有效期内 app.py:406-410, life/sharing.py:53-70

R3 写入点即「成功事务」app.py:553-560:取消/失败/断连天然不触发,重放被 UNIQUE 挡住。

1.3 life/told 三条件(AM-001 现行执行)

披露 = 成功轮次 AND 完整回执 AND 事后判定(life/sharing.py:1-4 文档串;提交条件 sharing.py:72-91:m.status='sent' AND a.ack_at IS NOT NULL AND a.state='judged' 才置 told=1 并写 telling_log)。PublicView.allows(told=1 才放行,life/public.py:9-12);/api/life 只回 told=1(app.py:386);详情 404 中性文案(app.py:394-404);public.message 的 lifeIds 服务端裁剪(public.py:26-31);chat_source_allowed:回复若以任何未公开候选为条件,不得作为公开成长来源(public.py:33-42)。R3 必须原样继承,不改 told、不自行确认接收(§3)。

1.4 预算与路由

  • Scene.MEMORY_GIST 已定义且路由 low(think/scenes.py:58-59、config/models.yaml:58-59),「量最大,必须低档 + 批处理」;structured()(life/model.py:50-94)走既有路由 + 预算预留(PersistentBudget 持久 token_usage,model.py:13-47),失败不落模板(LifeRuntime 同款约定 runtime.py:180)。R3 的 gist 作业直接复用,无需改 models.yaml。
  • 主对话上下文预算按档位裁剪(HIGH=4000,scenes.py:119-124;裁剪逻辑 app.py:507-520)——记忆注入块必须计入该预算。
  • 检索本身(SQLite)零模型调用,不占预算。

1.5 growth(成长层,R3 不得触碰)

Persona.grow 从 told=1 生活事件或 chat_source_allowed 聊天消息提炼 growth_event(life/persona.py:136-173;调度 runtime.py:131-143),出生底色经 fingerprint 校验且只在 persona_interp 层(persona.py:72-99)。R3 的 mem 表与 growth_event 互不引用、互不写;底色/人格/初始习惯/关系零改写。

1.6 回忆页现状

真实模式 memories: [](web/src/adapters/real.ts:102);回忆 tab 硬编码占位「暂无可核实的回忆 / 长期记忆尚未接通;文字聊天记录独立保留。」(web/src/life/LifePanel.tsx:153-157,real 视图 LifePanel.tsx:32)。域名模型 Memory {id,title,text,group,source,lifeIds} 已存在(web/src/domain/model.ts:32-39),演示模式四分组 MEMORY_GROUPS(LifePanel.tsx:11)。接线合同见 §5,实际界面由视觉总监负责。

2. 中文 FTS 实际能力(隔离实测,docs/r3/fts-probe-HEL-832.txt)

2.1 能力结论

  1. 现行 unicode61 中文全 0 命中(难过/下雨/雨声/杭州/我 → 0;仅英文命中)。表在≠中文可用。
  2. SQLite 3.37.2 自带 trigram 分词器:≥3 字中文子串命中正常(「杭州的雨」命中;跨句 OR 三元组成功召回前日话题;BM25 rank 正常;英文默认大小写不敏感)。服务器 Python 侧 SQLite 版本需在施工卡开工时同样实测一次(本沙箱 3.37.2 已含)。
  3. trigram 表上 <3 字符查询(MATCH 和 LIKE 都)静默 0 行——1–2 字中文词(雨声/难过/杭州)必须回退基表 mem_episodic LIKE(实测命中)。禁止对 trigram 表用短 LIKE。
  4. 安全查询:用户原文直传 MATCH 会抛 OperationalError(引号/连字符/NEAR 等)。唯一安全构造 = 服务端自行提词(剥离双引号/控制符/标点后按 ≥3 字片段或三元组),每个词元双引号包裹,词元间只写固定 OR;任何用户原文不得出现在引号外(实测 '…' OR b-c' 仍报错,证明必须整表达式由服务端生成)。

2.2 降级与停止条件(沿用获批 R3 停止条件并具体化)

  • 0 命中 → 不注入记忆段,纯近轮上下文(正常降级,不算故障)。
  • 噪声判定:仅命中 1 个词元且无整短语命中视为噪声 → 同样不注入。
  • 真实体验中人设漂移/答非所问持续出现 → life_control 键 memory_search=off 一键关闭检索(保留写入),日志留证并回报总管——对应获批「检索噪声导致人设漂移 → 关检索开关回报」。

3. AM-001 对 R3 的硬边界(重点)

  1. R3 episodic 只写聊天文本(user/companion 双方 'sent' 消息),绝不写 life_event.content——未分享生活从构造上就进不了记忆层,压缩/成长层无从绕过披露门禁。生活记忆仍留待 told 门禁之后的后续批次。
  2. 模型内部上下文 ≠ 对用户公开返回:检索命中的记忆注入的是模型 system 上下文(内部);她是否讲述某条记忆属输出层,已由 R2 sharing judge + 世界红线约束,R3 不新增任何披露路径。
  3. /api/memories* 全部走 PublicView 同款服务端裁剪序列化器:lifeIds 用 telling_log+PublicView.references 过滤(同 public.py:26-31);messageIds 只回 'sent';未知/未分享不出现,404 统一中性文案(同 app.py:396-404 模式)。前端过滤不是权限控制,响应前裁剪。
  4. 记忆作业不触碰 life_event.told、telling_log、sharing_attempt、bazi_core、persona_*、growth_*;不调用任何等价「确认接收」接口;回执确认只由前端既有路径发起。
  5. 旧链接/搜索:本批不新增记忆搜索端点;未来任何搜索端点必须先过同一 allows 门禁(写入本报告作为合同,防后续绕过)。

4. R3 后台施工范围(可直接执行;工程建议,非用户新决定)

4.1 写入(成功对话 → 可靠落库)

  • 触发时点:app.py:553-560 成功事务内,同步写两条 mem_episodic(role='user'/'companion',raw=原文,gist=NULL 待压缩,salience 默认 0.5 数值占位,affect=NULL 待粗估,source='chat'),并同事务登记压缩作业 mem_job(source='chat_message', ref_id=message_id, …)。同步写仅本地 SQLite 微秒级,不阻塞主对话;取消/失败/断连不写(不走该事务);重放幂等:新增守卫列 turn_id TEXT(guarded ALTER,照 store.py:44-51 life_event 先例)+ 部分唯一索引 UNIQUE(turn_id, role) WHERE turn_id IS NOT NULL。
  • gist/affect 粗估作业:后台循环(挂在现有 LifeRuntime.tick 的一个有界小节,照 growth_job「每次一条」模式 runtime.py:131-143)用 structured() + Scene.MEMORY_GIST(low 档)产出 gist(≤120 字要点)与 affect 粗估 {v,a,d};模型失败→作业 state='failed' 可重试(上限 3 次后留 failed+reason),gist 保持 NULL,绝不填模板(照 runtime.py:180 约定)。重启恢复照 runtime.recover()(runtime.py:61-68)租约过期模式。
  • affect/salience 数值启发式(命中关键词词典给 v/a 符号,缺省 NULL/0.5):数值字段、非人格文本,不涉 CG-014 模板红线;真实粗估由 gist 作业一并提供。

4.2 检索(FTS top-k 接后续对话)

  • 索引迁移(增量):新建 mem_fts_zh USING fts5(raw, gist, content='mem_episodic', content_rowid='id', tokenize='trigram')(r3.sql,CREATE … IF NOT EXISTS);一次性 INSERT INTO mem_fts_zh(rowid,raw,gist) SELECT id,raw,gist FROM mem_episodic(事务内、幂等);旧 mem_fts 保留不删(回滚即旧路)。add_episodic 改为双写两个索引直至切换验收后清理。
  • 查询构造(§2 实测结论的落地):用户新消息 → 剥离控制符/双引号/标点取连续段;段 ≥3 字 → 整段双引号短语;全无 ≥3 字段 → 基表 LIKE 回退;短语零命中 → 第二遍按段采样三元组 OR(上限 64 词元);词元只由服务端生成,固定 OR 连接。
  • 注入合同:命中 top-k(k≤4,accessibility≥0.05,排除本轮及近 20 轮已含 turn_id;rank 过滤噪声规则 §2.2)以独立 system 小节「她确实记得的更早的事(自然使用,不逐条复述,不确定就不用)」注入,块长 ≤600 字并计入 decision.context_budget 裁剪(app.py:507-520);命中即 reinforce(id) 并打一行 JSON 日志 {"turn_id","hits":[{"id","rank"}],"mode":"phrase|trigram|like|none"}(可追溯记忆 ID 与来源)。
  • 诚实文案:app.py:478 系统提示改为如实反映「长期记忆已接通、主动消息/语音/视觉/天气仍未接通」;/api/health capabilities.memory(app.py:289-294)仅在写入+检索都真实生效后置 true。

4.3 衰减/强化批次

  • decay_accessibility 每小时一批(life_control 键 last_mem_decay 记 dt,clock 参数注入可测);reinforce 检索命中即时调用(既有实现零改动)。

4.4 明确不做(本批边界)

向量/embedding(providers/embedding 闲置保留)、mem_semantic/mem_relational 整合、identity promotion、记忆搜索端点、记忆编辑/删除、生活事件入记忆、R4 主动提及记忆、任何模型/温度/路由/预算/运行时/认证改动、任何新依赖(trigram 为 SQLite 自带)。

5. 接口/DTO/分页/可见性合同(视觉总监接线依据)

GET /api/memories?limit=30&before=<id>   (游标分页,同 /api/life 模式)
→ {"memories":[MemoryDTO…], "next_before": <id>|null, "epoch": n}
GET /api/memories/{id} → {"memory": MemoryDTO, "epoch": n}
  未知/未分享/伪造 id 一律 404 中性文案(同 app.py:397「这一条现在打不开。」)

MemoryDTO(映射既有 Memory 域模型 model.ts:32-39):

字段 生成规则 可见性
id mem_episodic.id(字符串化) —
title gist(未压缩完成前不用该条)截断 ≤40 字 服务端生成
text gist 全文 ≤200 字 服务端生成
group episodic 固定「我们一起的」;「她的成长」仍来自 /api/persona(两源并存,是否合流由视觉总监定) —
source 「来自 M月D日 HH:MM 的聊天」(事件 ts + 档案时区) —
lifeIds telling_log × PublicView.references 服务端裁剪,未知/未分享不出现 AM-001
messageIds 仅 'sent' 消息(同 public.message 白名单思路) AM-001
ts 毫秒时间戳 —

前端规则:真实模式只渲染服务端已裁剪数据(照 LifePanel 现有 real 约定 LifePanel.tsx:31-32 注释);校验器照 serverLife 模式(real.ts:35-38)写 serverMemory,不合格行丢弃不修补;占位文案在数据到位前保持诚实(UX-006);演示路径零改动。

6. 隔离验收矩阵(施工卡必测;全部隔离库+注入时钟,不改系统时钟,零真实模型调用)

# 项 断言要点
T1 中文 FTS 证据套件 probe 全部用例转 pytest:unicode61 中文 0 命中(防回归声明)、trigram 短语/三元组命中、<3 字 LIKE 回退命中、trigram 表短 LIKE 静默 0(负例)、非法输入(引号/NEAR/OR/-/*)构造后不抛错且不越权
T2 成功写入幂等 成功轮→2 行 mem_episodic(turn_id 正确);取消/失败/断连→0 行;重放→不新增;重启→不重写
T3 作业持久与恢复 mem_job pending 经租约恢复;模型失败→failed 可重试、gist 保持 NULL 无模板;成功→gist/affect 落库且 FTS 双索引可见
T4 检索接线 旧记忆(近 20 轮窗口外+不直接复述答案)经新消息触发后出现在 ControlledLLM 收到的上下文;0 命中/噪声→无记忆段;reinforce+1 且日志含记忆 ID;记忆块计入预算裁剪
T5 回忆页合同 /api/memories 分页/before/epoch;DTO 逐字段;未分享 lifeId 不出现(种子 told=0 引用);详情 404 中性
T6 隐私全路径 /api/life、/api/messages、/api/persona 响应与 c285a0f 基线逐字节等价(除声明变更);told/telling_log/bazi_core/persona_/growth_ 零变化(fingerprint 断言底色不变)
T7 模拟时间 clock 注入驱动 decay 批次与租约过期(不碰系统时钟)
T8 回归 pytest 全绿(≥3.11 环境)、web build/lint/test 全绿、前端本卡零改动

隔天真实体验证据(真实模型+真实档案)不在施工卡:由总工在独立部署卡按授权上限执行(§7),模拟时间单测与真实证据分开记录。

7. 部署/回滚/真实体验授权上限(交后续卡)

  • 部署:独立实例(新端口、独立数据目录、全新隔离档案;不复制用户库、不动 8300/8310/8320/8340),模式照 /opt/cybergril-*/releases/<ts> + current 已验证路径。正式并入 8310 真实档案须白栖知另行确认。
  • 回滚:代码 Git 回退 + 符号链接切回上一 release + start;schema 全增量(新列/新表/新索引),数据不删;mem_fts 未动,老代码回退即忽略全部 R3 表。
  • 真实 smoke 上限(供总管在部署卡明确授权):gist 压缩验证 ≤8 次模型调用;真实对话验证 ≤6 轮(含 1 次隔天记忆取回);超限或触发 §2.2 停止条件即停并回报。日志留存请求/响应证据(照 HEL-830 model-smoke 记录格式)。

8. 关联要求与结论

关联:CG-006(同一份记忆跨设备)、CG-008(后台连续性)、CG-013/014/015(底色只读/诠释自由/成长分离——R3 零触碰)、CG-016(关系不改写过去)、CG-018(同一份记忆)、CG-021(后台任务小模型)、AM-001(§3 全文)、UX-006(诚实接线)、GOV-001–005、DEL-001(R1–R7 顺序)。需求变更:无。

未完待续:R3 后台施工、视觉总监回忆页接线、总工统一审核+独立部署均未开始(卡文见 CONSTRUCTION-CARDS-HEL-832.md,由总管建卡);R2 三天观察仍由 HEL-831 承接;R4–R7 仍主卡 HEL-797 追踪。阶段通过≠产品完成。

HEL-832 · 总工 · 2026-10-24 · 只读核对+文档交付;未写产品代码、未部署、未真实调用模型。