feat(api)!: memory single chain — async MemoryService, strict eval closure
Route all memory ingest/retrieve/enrichment/compaction through async MemoryService. Remove legacy sync memory implementations (ingest/retrieve/compaction); Celery and memoir Phase2 call asyncio.run into MemoryService-backed helpers. Memoir Phase1 batch ingest uses MemoryService.ingest_transcripts_batch; drop chapters. evidence_bundle_json mirror (Alembic 0015). Evaluation uses snapshot/link-only bundles; raise EvidenceClosureMissing instead of partial/fallback lineage tiers. Split memoir state into NarrativeCoverageState and InterviewControlState; delete the _interview_meta_store adapter layer. Remove rolling-query and recent-fact fallback settings from config and evidence assembly. Update judges, docs, tests, and PlaygroundPage alignment. Made-with: Cursor
This commit is contained in:
@@ -49,7 +49,6 @@ Regenerate: `uv run python api/scripts/ai_touchpoints_scan.py --markdown api/doc
|
||||
| `api/app/features/conversation/ws/profile_collector.py` | `agents_layer` |
|
||||
| `api/app/features/conversation/ws/router.py` | `agents_layer` |
|
||||
| `api/app/features/evaluation/judge_service.py` | `json_llm_helpers`, `llm_call_module` |
|
||||
| `api/app/features/memoir/_interview_meta_store.py` | `agents_layer` |
|
||||
| `api/app/features/memoir/deps.py` | `memory_ai` |
|
||||
| `api/app/features/memoir/memoir_images/prompting.py` | `agents_layer`, `json_llm_helpers`, `langchain` |
|
||||
| `api/app/features/memoir/service.py` | `memory_ai` |
|
||||
|
||||
@@ -131,18 +131,17 @@ Playground 的结构化摘要里,后端会给出一份 `gate`:
|
||||
|
||||
## 回忆录评审:可追溯证据闭包(lineage)
|
||||
|
||||
**产品与 tier 口径(strict / partial / fallback)、synthetic vs library 分表、PM 对齐规则、backlog** 见同目录 **[traceable-memoir-lineage.md](./traceable-memoir-lineage.md)**。
|
||||
**严格闭包口径、synthetic vs library 分表** 见同目录 **[traceable-memoir-lineage.md](./traceable-memoir-lineage.md)**。
|
||||
|
||||
手动 `/judge/memoir-chapters` 与历史自动化 run 的 `judge_bundle_json` 已按 **artifact 绑定证据** 组 prompt,而不再默认拼接「最近 N 个会话全文」:
|
||||
|
||||
- **`lineage_tier`**:`strict` / `partial` / `fallback`(章节:**有可解析 transcript 链 + 结构化记忆为 strict**;**仅有结构化记忆、无绑定 segment/transcript = partial**,与标注口径一致)。故事侧以 `StoryEvidenceLink` 与章节推导为主;`fallback` = 显式降级最近会话 transcript,避免静默当 strict。
|
||||
- **`evidence_trace`**:bundle 完整 JSON(segment / conversation / chunk / fact / timeline / summary、`notes` 等)。内审计一般够用;若需按类型深链 UI 再排期。
|
||||
- **`format_meta`**:`truncated`、`dropped_sections`、`included_token_estimate` 等,区分「prompt 裁掉」与「库中无 lineage」。
|
||||
- **生产侧**:叙事流水线在每次 Story 写入后覆盖 `story_evidence_links`,并在当前 `story_versions.prompt_meta.memoir_retrieval` 写入本轮检索到的稳定 id(见 `story_pipeline_sync._persist_story_lineage_sync`)。
|
||||
- **章节快照 Phase C**:`chapter_evidence_snapshots` + `chapter_evidence_links`,`chapters.current_evidence_snapshot_id` 指向当前版本;`evidence_bundle_json` 仍为镜像。评测读取顺序:表快照 → JSON → 现场 `source_segments`(不一致时 `notes` 提示)。刷新见 `memoir/chapter_evidence_snapshot.py`。历史库可选 `uv run python scripts/backfill_chapter_evidence_snapshots.py`(旧数据不强制)。
|
||||
- **章节快照 Phase C**:`chapter_evidence_snapshots` + `chapter_evidence_links`,`chapters.current_evidence_snapshot_id` 指向当前版本;评测只读当前快照。刷新见 `memoir/chapter_evidence_snapshot.py`。
|
||||
- **对话 memory trace(Phase 八)**:访谈路由下,`conversation_messages.memory_retrieval_trace_json` 在配对 **AI** 消息上写入本轮 `HybridRetriever` 命中的 chunk/fact/timeline/summary/story 等 id(见 `memory/retrieval_trace.py`)。
|
||||
|
||||
历史数据可无 link:评测仍可用 partial/fallback 跑通;可选离线 backfill 须在 job 中显式打标,不冒充 strict。
|
||||
历史数据缺 link / snapshot 时不可评测,需先通过正式流水线重新物化。
|
||||
|
||||
## Fixture 详情扩展
|
||||
|
||||
@@ -150,4 +149,3 @@ Playground 的结构化摘要里,后端会给出一份 `gate`:
|
||||
|
||||
- `source_user_id`:导出抬头中的 User ID
|
||||
- `memoir_sections`:`## 回忆录章节(生成正文)` 下按标题切分的基线正文(已去掉 `{{IMAGE:...}}` 占位)
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ This document summarizes production-oriented behavior for the memoir narrative p
|
||||
| `memoir_fidelity_fail_open_on_parse_error` | `False` | When `True`, fidelity JSON/LLM failures pass the gate even for new stories (rollback only via ops need). |
|
||||
| `memoir_narrative_evidence_overlap_min_chars` | `14` | Deterministic overlap check between body and evidence plain text. |
|
||||
| `memoir_title_slots_require_body_or_oral_match` | `True` | Narrows title-generation slot inputs to body/oral overlap. |
|
||||
| `memory_fact_search_use_recent_fallback` | `False` | When `False`, fact ILIKE misses do **not** fall back to “recent confirmed facts” (reduces contradictory/unrelated facts in prompts). |
|
||||
| `memory_compaction_enabled` | `True` | Near-duplicate chunk soft-exclude; requires Celery worker + **Beat** for periodic `memory_compaction_sweep`. |
|
||||
| `memoir_recompose_retry_on_lock_contention` | `True` | Chapter recompose retries with backoff when the chapter pipeline lock is held. |
|
||||
| `memoir_phase2_singleflight_immediate` | `True` | Immediate Phase 2 `send_task` uses a stable `task_id` per user/category to reduce duplicate queue entries. |
|
||||
|
||||
@@ -1,54 +1,21 @@
|
||||
# 记忆检索:异步 API 与 Celery 同步路径
|
||||
# 记忆检索:async 单链路
|
||||
|
||||
## 两条路径
|
||||
Memory 运行链路只有一个入口:`MemoryService`。
|
||||
|
||||
| 路径 | 入口 | 检索能力 |
|
||||
|------|------|----------|
|
||||
| **异步(HTTP / MemoirService)** | `MemoryService.retrieve` → `HybridRetriever` → `evidence.retrieve_evidence_bundle_async` | **向量(pgvector)** chunks;facts / timeline 按 **query ILIKE**,无命中则 **fallback** 最近条;rolling + ILIKE **摘要**;**stories**(标题/摘要匹配) |
|
||||
| **同步(Celery)** | `retrieve_evidence_sync`(注入 `get_embedding_provider()` → `evidence.retrieve_evidence_bundle_sync`) | **向量** chunks + 同上元数据;与异步路径对齐 |
|
||||
| 能力 | 入口 | 行为 |
|
||||
| --- | --- | --- |
|
||||
| ingest | `MemoryService.ingest_transcript` / `ingest_transcripts_batch` | 写入 `memory_sources`、`memory_chunks`、embedding;commit 后投递 enrichment |
|
||||
| retrieve | `MemoryService.retrieve` | 非空 query 做向量 chunk 检索,并合并 query 命中的 facts / timeline / session summaries / stories |
|
||||
| enrichment | `MemoryService.enrich_source` | 单次 LLM 生成 session summary 与 confirmed facts |
|
||||
| compaction | `MemoryService.compact_user` | 近重复 chunk 软排除并 stale 相关 facts |
|
||||
|
||||
证据组装在 `app/features/memory/evidence.py`;`memory/repo` 提供原子查询(chunk 向量、facts/timeline 搜索、摘要列表等),story 合并在 evidence 层完成。
|
||||
## 检索语义
|
||||
|
||||
## 依赖 embedding
|
||||
- 空 query 固定返回空 evidence bundle。
|
||||
- facts / timeline / summaries 只按 query 命中返回;不回退最近事实、最近时间线或 rolling summary。
|
||||
- `MemorySummary.summary_type="session"` 可进入 evidence;rolling summary 不参与 prompt evidence。
|
||||
- Celery task 只是同步入口包装 async service,不再维护 sync memory 业务链路。
|
||||
|
||||
- 未配置 `ZHIPU_API_KEY`(或 provider `_client` 为空)时,chunk 检索为空列表,仍会返回 facts/timeline/summaries/stories(按 query ILIKE)。
|
||||
- 日志:`HybridRetriever` / `retrieve_evidence_bundle_sync` 在无 provider 或空向量时会打 warning。
|
||||
## 回忆录流水线
|
||||
|
||||
## 行为矩阵(async / sync 契约)
|
||||
|
||||
以下行为应对齐;变更 `evidence.py` 时须同时检视 `HybridRetriever` + `retrieve_evidence_bundle_sync`,并跑 `tests/test_memory_evidence.py` 中的双路径用例。
|
||||
|
||||
| 条件 | 同步 `retrieve_evidence_bundle_sync` | 异步 `retrieve_evidence_bundle_async` |
|
||||
|------|--------------------------------------|---------------------------------------|
|
||||
| query 空白 | `memory_evidence_empty_query_include_rolling=false` → 与 `EMPTY_EVIDENCE_BUNDLE` 同键、全空列表 | 同上 |
|
||||
| query 空白 | `memory_evidence_empty_query_include_rolling=true` → 无 chunks;rolling 摘要(若有)+ 最近 facts / timeline;`relevant_stories` 为空 | 同上(`_empty_query_bundle_*` 对称实现) |
|
||||
| query 非空 | 本函数内 `embedding_provider.embed_text_sync` → `search_chunks_vector_sync`;再并行拉取元数据 | chunks 由调用方预计算(`HybridRetriever` 中 `search_chunks_vector`);本函数只 `fetch_evidence_metadata_async` 合并 |
|
||||
| 无 embedding | warning;chunks 为空;元数据仍按 ILIKE 等返回 | async 路径若上游无向量则 `merged_chunk_dicts=[]`;元数据仍返回 |
|
||||
| 输出形状 | `{"relevant_chunks", "relevant_summaries", "relevant_facts", "timeline_hints", "relevant_stories"}` chunk 项为 `id, content, chunk_index`(不含 distance) | 非空 query 下 `relevant_chunks` 等于传入的 `merged_chunk_dicts`(已由检索层剥掉 distance) |
|
||||
|
||||
Facts 状态过滤(如 `confirmed` / 排除 `stale`)与 ILIKE fallback 由 `repo` 查询实现;两条路径共用同一套 sync/async repo 函数族,语义以 `evidence.py` 调用为准。
|
||||
|
||||
## 空 query
|
||||
|
||||
- 默认:`relevant_*` 均为空(与历史行为一致)。
|
||||
- 若设置 `memory_evidence_empty_query_include_rolling=true`:返回**无 chunk**,但含 **rolling 摘要**、最近 facts / timeline(用于「浏览」模式)。
|
||||
|
||||
## 富化(ingest 后 LLM)
|
||||
|
||||
- `memory_enrichment_enabled`(默认 `true`):ingest 成功并 **commit** 后,通过 `schedule_memory_enrichment` 将任务投递到 **`CELERY_MEMORY_ENRICHMENT_QUEUE`**(默认 `memory_idle`),在 worker 上 **单次 LLM 调用**产出 **会话摘要(`MemorySummary` session)+ 结构化事实(`MemoryFact`)**;`false` 时不投递。
|
||||
- ingest 路径 **不再**维护滚动摘要(rolling)与 **时间线表**(`timeline_events`)的物化;检索中的 `timeline_hints` 依赖既有数据(若有)或为空;空 query 下「浏览」模式若开启 `memory_evidence_empty_query_include_rolling`,仅当库内仍有历史 rolling 行时才会出现。
|
||||
- 异步 `MemoryService.ingest` 与同步 `ingest_transcript_sync` 均 **不**在请求/任务热路径内内联 LLM 富化;回忆录 Phase1 在 DB commit 后调用 `schedule_enrichment_for_sources`(与 `memoir_correlation_id` 观测一致)。
|
||||
- Worker 须消费该队列(例如 `-Q celery,memory_idle`),否则任务会堆积。
|
||||
- `memory_enrichment_max_chars`:截断送入 LLM 的文本长度。
|
||||
- Ingest 写入 **embedding**(best-effort);历史 FTS 列 `content_tsv` 已由迁移 `0007_drop_chunk_content_tsv` 删除。
|
||||
- 叙事阶段 `retrieve_evidence_sync` **不等待**富化完成;证据随富化渐进变丰富。
|
||||
|
||||
## Celery 任务中的顺序
|
||||
|
||||
`process_memoir_segments`(`app/tasks/memoir_tasks.py`)在**同一任务**内先执行批量 ingest(`ingest_transcripts_batch_sync` 并 `commit`),再富化入队与 `MemoirOrchestrator`、派发 Phase2。Phase2 内 `retrieve_evidence_sync` 能看到**本批刚写入**的 memory chunks(无竞态),前提是 embedding API 已成功写入向量;富化 Summary/Facts 可能稍后才就绪。
|
||||
|
||||
章节分类上,若模型返回 **none** 或命中零散档案启发式,Story 侧会统一落入 **`summary` 章节**并继续叙事落库,与「本批 transcript 已进 memory」一致,避免误以为内容被丢弃。
|
||||
|
||||
## Evidence 与叙事 Prompt
|
||||
|
||||
`format_evidence_chunks_for_prompt` 拼接 chunks、**摘要(若有)**、facts、timeline、**故事摘要(若有)**;模型应把摘录视为参考材料,非本段口述。
|
||||
`process_memoir_phase1` 通过 `MemoryService.ingest_transcripts_batch` 批量写入记忆;`process_memoir_phase2` 先通过 `MemoryService.retrieve` 取 evidence,再把 evidence 传给叙事流水线。叙事代码只消费 evidence,不直接调用 memory repo。
|
||||
|
||||
@@ -1,41 +1,20 @@
|
||||
# 回忆录可追溯证据(产品与内评口径)
|
||||
# 回忆录可追溯证据
|
||||
|
||||
本文与 PM、标注、工程共用:**旧库数据不要求为评测专门 backfill**;新写入走统一闭包与快照表。口径不清会导致反复对齐成本,变更 tier 规则时请同步改 `EvalTraceService._chapter_closure_tier` / Story 侧等价逻辑与本文。
|
||||
Library artifact 评测只接受严格证据闭包。
|
||||
|
||||
## lineage_tier:strict / partial / fallback
|
||||
## 严格闭包
|
||||
|
||||
| 档位 | 含义(章节) | 含义(故事,概要) |
|
||||
|------|----------------|-------------------|
|
||||
| **strict** | 既有可解析的访谈 **segment**(可绑定 transcript),又有从对应会话闭包得到的 **结构化记忆**(chunk / fact / timeline / summary 等任一非空)。 | Story 上以 `StoryEvidenceLink` 等为主链解析出 segment + 结构化记忆均存在。 |
|
||||
| **partial** | 有可解析的 **segment / transcript 链**,但只有结构化记忆为空,或仅有结构化记忆而 **无** 可绑定的 segment(**与 PM/标注对齐:仅有结构化、无 transcript = partial**)。 | 能从章节 `source_segments` 等推导出一侧证据但闭包不完整。 |
|
||||
| **fallback** | 无法从 artifact 构建足够闭包(例如无 segment 且无法走库内链路),评测侧 **显式** 降级为「最近若干会话」等粗粒度 transcript;须在结果 `notes` / `evidence_trace` 中可见,避免静默当 strict 用。 |
|
||||
- Chapter:必须有 `chapters.current_evidence_snapshot_id` 指向的 `chapter_evidence_snapshots` 行,且同时包含可绑定 transcript 的 `segment_ids` 与至少一类 memory evidence id。
|
||||
- Story:必须有 `StoryEvidenceLink`,且能绑定到 transcript segments。
|
||||
- 缺闭包时,`EvalTraceService` 抛 `EvidenceClosureMissing`;不再拼接最近会话全文或从旧 JSON 镜像补齐。
|
||||
|
||||
**说明**:`partial` 不是「质量差」,而是「血缘不完整仍可评审」;`fallback` 是「链路断裂时的保守降级」,评审 prompt 与 gate 解读需区别对待。
|
||||
## Phase C 表
|
||||
|
||||
## 自动化评测:synthetic memoir vs library artifact(分表心理模型)
|
||||
- `chapter_evidence_snapshots`:一行对应一次物化闭包,`version_no` 递增。
|
||||
- `chapter_evidence_links`:按当前快照整批替换章节侧结构化 evidence id。
|
||||
- `chapters.current_evidence_snapshot_id` 是章节评测唯一入口。
|
||||
|
||||
同一次 `eval_run` 里可能同时存在两类「回忆录」分数,语义不同,**勿混为一谈**:
|
||||
## Synthetic vs Library
|
||||
|
||||
- **Synthetic(replay 合成短文)**:由 case 的 replay 对话现场拼出的短 markdown,证据闭包仅为 **重放 transcript**,不绑定用户库里的 memory chunk / fact / timeline / summary。`judge_meta.synthetic_memoir_lineage_tier` 等为 `replay_transcript_only` 一类标记。
|
||||
- **Library(库内章节 / 故事)**:真实 `Chapter` / `Story` artifact,使用 `EvalTraceService` 组装的 evidence bundle(含 `lineage_tier`、`evidence_trace`)。
|
||||
|
||||
聚合规则见 `judge_bundle_json.judge_meta.memoir_aggregate_rule`(例如合成与 library 均有分时的加权方式)。对 PM 汇报时请分项展示,避免只报一个「回忆录分」。
|
||||
|
||||
## 内评 JSON:`evidence_trace` 是否够用?
|
||||
|
||||
当前 `evidence_trace` 为 `ChapterEvidenceBundle` / `StoryEvidenceBundle` 的 **完整序列化**:含 `segment_ids`、`conversation_ids`、各类 memory id、`lineage_tier`、`notes`、`augmented_with_chapter_context` 等。**一般内审计够**:可按 id 去 DB 或日志反查。
|
||||
|
||||
若需 **按 artifact 类型展开为可点击深链 / 批量导出**,属于体验增强,可单独排期(eval-web 已支持章节级折叠展示 id 列表)。
|
||||
|
||||
## Phase C:`chapter_evidence_snapshots` 与 `chapter_evidence_links`
|
||||
|
||||
- **快照表**:一行对应一次物化闭包(`version_no` 递增);`chapters.current_evidence_snapshot_id` 指向当前生效行。
|
||||
- **链接表**:与 `StoryEvidenceLink` 对称,按快照刷新时 **整批替换** 章节侧结构化记忆 id,便于审计与扩展。
|
||||
- **评测消费顺序**:`current_evidence_snapshot`(表)→ `evidence_bundle_json`(JSON 镜像,兼容)→ 现场用 `source_segments` 计算(与 live 不一致时 `notes` 提示)。
|
||||
- **旧数据**:可不迁;新流水线写入会同时更新表与 JSON 镜像。
|
||||
|
||||
## 技术债(backlog,不阻塞发版)
|
||||
|
||||
1. **统一闭包计算**:生产快照与 `EvalTraceService` 已共用 `build_chapter_evidence_closure_payload_sync`;Story / 其它路径若仍有重复推导,应收敛到同一入口,避免双份实现漂移。
|
||||
2. **扩展 memory trace**:除当前访谈检索外,其它入口若向模型喂 memory,评估是否同样写入 `memory_retrieval_trace_json`(或等价 trace),以便 partial / strict 判定与事后审计一致。
|
||||
3. **canonical 与 `source_segments` union 冲突**:若线上冲突案例增多,再评估独立快照表外的「版本级 link」或更强约束;当前 Phase C 已降低仅依赖单列 JSON 的风险。
|
||||
- Synthetic replay 只评估重放 transcript,不绑定用户库 memory。
|
||||
- Library chapter/story 使用严格闭包;缺闭包即不可评测。
|
||||
|
||||
Reference in New Issue
Block a user