18 KiB
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 能力结论
- 现行 unicode61 中文全 0 命中(难过/下雨/雨声/杭州/我 → 0;仅英文命中)。表在≠中文可用。
- SQLite 3.37.2 自带 trigram 分词器:≥3 字中文子串命中正常(「杭州的雨」命中;跨句 OR 三元组成功召回前日话题;BM25 rank 正常;英文默认大小写不敏感)。服务器 Python 侧 SQLite 版本需在施工卡开工时同样实测一次(本沙箱 3.37.2 已含)。
- trigram 表上 <3 字符查询(MATCH 和 LIKE 都)静默 0 行——1–2 字中文词(雨声/难过/杭州)必须回退基表
mem_episodicLIKE(实测命中)。禁止对 trigram 表用短 LIKE。 - 安全查询:用户原文直传 MATCH 会抛 OperationalError(引号/连字符/NEAR 等)。唯一安全构造 = 服务端自行提词(剥离双引号/控制符/标点后按 ≥3 字片段或三元组),每个词元双引号包裹,词元间只写固定
OR;任何用户原文不得出现在引号外(实测'…' OR b-c'仍报错,证明必须整表达式由服务端生成)。
2.2 降级与停止条件(沿用获批 R3 停止条件并具体化)
- 0 命中 → 不注入记忆段,纯近轮上下文(正常降级,不算故障)。
- 噪声判定:仅命中 1 个词元且无整短语命中视为噪声 → 同样不注入。
- 真实体验中人设漂移/答非所问持续出现 →
life_control键memory_search=off一键关闭检索(保留写入),日志留证并回报总管——对应获批「检索噪声导致人设漂移 → 关检索开关回报」。
3. AM-001 对 R3 的硬边界(重点)
- R3 episodic 只写聊天文本(user/companion 双方 'sent' 消息),绝不写 life_event.content——未分享生活从构造上就进不了记忆层,压缩/成长层无从绕过披露门禁。生活记忆仍留待 told 门禁之后的后续批次。
- 模型内部上下文 ≠ 对用户公开返回:检索命中的记忆注入的是模型 system 上下文(内部);她是否讲述某条记忆属输出层,已由 R2 sharing judge + 世界红线约束,R3 不新增任何披露路径。
/api/memories*全部走PublicView同款服务端裁剪序列化器:lifeIds 用telling_log+PublicView.references过滤(同public.py:26-31);messageIds 只回 'sent';未知/未分享不出现,404 统一中性文案(同app.py:396-404模式)。前端过滤不是权限控制,响应前裁剪。- 记忆作业不触碰
life_event.told、telling_log、sharing_attempt、bazi_core、persona_*、growth_*;不调用任何等价「确认接收」接口;回执确认只由前端既有路径发起。 - 旧链接/搜索:本批不新增记忆搜索端点;未来任何搜索端点必须先过同一 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-51life_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/healthcapabilities.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 · 只读核对+文档交付;未写产品代码、未部署、未真实调用模型。