Files
life-echo/api/docs/internal-eval.md
Kevin 71fbd39e32 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
2026-04-30 14:11:50 +08:00

11 KiB
Raw Blame History

内部回归评测平台

与主 APIapp/main.py)隔离进程部署,避免评测候选链路透出给消费者 App。

启动

推荐一条命令internal-eval.sh 实际调用 development.sh,在同一进程树里启动主站 main:app8000)、一份 Celery、内部评测 internal_app(默认 8001)以及 app-eval-web(默认 5174)。不需要再并行执行两份启动脚本。

单一命令 ./internal-eval.sh
HTTP 主站 8000 + internal 8001
Celery 一个 worker与主站共用队列
前端 默认启动 app-eval-webSTART_EVAL_WEB=0 可关)

主站 + Celery 已在其他终端./development.sh 跑起来了,只在同一台机器上多开评测 HTTP 与前端、且 不再起第二份 Worker

cd api
# 确保 .env.development / .env 含 INTERNAL_EVAL_API_KEY:8000 已被主站监听
SKIP_INFRA=1 SKIP_INSTALL=1 EVAL_ATTACH_ONLY=1 ./internal-eval.sh

兼容旧写法:SKIP_CELERY=1 会映射为 EVAL_ATTACH_ONLY=1(仍要求 8000 已在监听)。

仅主业务、不要评测台时照旧:./development.sh(不设置 LIFE_ECHO_WITH_INTERNAL_EVAL)。

若你只需要 8001、刻意不启主站 8000,请用下文「手动 uvicorn」配合既有 Celery不要用 ./internal-eval.sh(一键脚本会顺带拉起主站)。

默认会起 app-eval-web,并用 Vite --open 尝试打开浏览器http://127.0.0.1:5174/)。不要前端时设 START_EVAL_WEB=0;只要前端但不要弹窗时设 OPEN_EVAL_WEB=0

数据库与主服务共用;需配置环境变量后启动专用进程:

cd api
export INTERNAL_EVAL_API_KEY='your-long-random-secret'
export INTERNAL_EVAL_ENABLE_DOCS=1   # 可选,开 /docs
# 评测评审Playground / Memoir 手动的对话与成稿打分)
# 智谱:默认 EVAL_JUDGE_API_KEY否则回退 ZHIPU_API_KEY
export EVAL_JUDGE_API_KEY='...'        # 可选
export EVAL_JUDGE_MODEL='glm-5'
# DeepSeekAPI 模型名 deepseek-reasoner 即 R1与访谈主链路密钥一致独立默认模型名
export DEEPSEEK_API_KEY='...'           # 选用 DeepSeek 评审时必填(或回退 LLM_API_KEY
export EVAL_JUDGE_DEEPSEEK_MODEL='deepseek-reasoner'   # 可选
export EVAL_JUDGE_DEEPSEEK_CONTEXT_WINDOW_TOKENS='64000' # 可选;用于 transcript 截断,避免按 GLM 200K 估长

uv run uvicorn app.internal_main:internal_app --host 0.0.0.0 --port 8001

Celery worker 与主站共用(celery_appinclude 回忆录等任务;不再包含已下线的 evaluation_tasks 实验批量跑批)。需 Phase1 / 叙事推进时请启动 worker

uv run celery -A app.tasks.celery_app worker -l info -Q celery,memory_idle

前端(app-eval-web

cd app-eval-web
npm install
VITE_EVAL_API_BASE=http://127.0.0.1:8001 VITE_EVAL_API_KEY=与上同 npm run dev

或使用仓库根目录 npm run eval-web(需本地已 npm installapp-eval-web)。

部署约定app-eval-web 不提供生产/预发 Docker 镜像,也 纳入 Staging/Production 的 api/docker-compose.yml 部署。评测 UI 仅在开发阶段于本机通过 Vite npm run dev(或 ./internal-eval.sh 拉起)使用;不要在预发/生产环境编译 dist/ 或挂载独立容器对外提供服务。

流式评审

POST /internal/api/evaluation/judge/conversation-stream 使用 fetch 读取 SSEchunk请求头携带 X-Internal-Eval-Key 即可;不要求浏览器 EventSource。Body 可选 judge_providerzhipu(默认)| deepseek,以及 judge_model(空则用该供应商环境默认)。首轮 meta 事件会回显 judge_provider / judge_model

新增事件:

  • compare_summary:结构化 A/B 对比摘要,包含 group_deltas、关键回落维度、是否出现重复盘问风险,以及 transcript 截断提示。
  • compare_delta:原有自由文案流,适合人读;不替代结构化结论。

评测 Webapp-eval-web

  • Playground · 分步测评:选用户导出 MD 为基线 → eval-sandbox + 逐轮 replay/conversationskip_memoir: true 时只做对话)→ memoir-submit 再可选轮询 memoir-phase1-ready → 跳转 Memoir / Stories 看成稿;支持 智谱 / DeepSeek R1 对话流式评分(工具栏「评审模型」)。
  • Memoir:按 user_id 拉库中章节快照与基线对照评审。
  • Stories:故事列表与评审。
  • 实机联调(侧栏「实机联调」,哈希路由 #live):用与 消费者主站相同的 REST / WebSocket 测核心聊天与回忆录。需主站 main:app 已启动(默认 :8000),并在主站环境启用 Mock 登录(见下)。开发时 app-eval-web/api/ws 代理到主站(VITE_MAIN_API_PROXY_TARGET,默认 http://127.0.0.1:8000);也可设 VITE_MAIN_API_BASE 直连完整主站 URL。

Mock 登录(仅非 production

在主站 .env / .env.development 中设置 MOCK_SMS_LOGIN_ENABLED=1(或 true)。APP_ENV=productionPOST /api/auth/mock/sms-login 始终返回 404。请求体:phone11 位)、agreed_to_terms: true,可选 nickname(新用户);响应与正式短信登录相同(access_token + refresh_token)。切勿在生产环境开启。

真实链路透传回放(与 App 一致)

方法 路径 说明
POST /internal/api/evaluation/sessions/eval-sandbox 无 body新建临时用户eval_ 伪手机号)+ 空白 conversation_id
POST /internal/api/evaluation/sessions/replay-bootstrap body{ "user_id" },在已有用户下返回新 conversation_id
POST /internal/api/evaluation/replay/conversation bodyconversation_idfixture_filename user_utterances;可选 skip_memoir(默认 false为 true 时不 queue_message、且不会仅因 flush_memoir_afterflush_pending)、flush_memoir_after(默认 trueskip_tts(默认 true。响应含 segment_ids(本批创建的用户 segment
POST /internal/api/evaluation/sessions/{conversation_id}/memoir-submit 无 body收集本会话内 topic_category IS NULLprocessed 为 false 的 segment调用 flush_pending(user_id, extra_segment_ids=…);返回 segment_idscelery_task_id
GET /internal/api/evaluation/sessions/{conversation_id}/memoir-phase1-ready querysegment_ids 可重复。所列 segment 均已写入 topic_categoryready: true

默认(skip_memoir: false:每轮仍相当于主站路径:create_user_segmentprocess_user_messagebackground_runner.queue_message;末尾可 flush_pending

Playground 分步(skip_memoir: true + flush_memoir_after: false:只做 create_user_segmentprocess_user_message入回忆录队列;对话结束后再调 memoir-submit 统一 flush。

  • TTS:回放默认 skip_tts: true
  • CeleryPhase1 / 叙事仍依赖 worker仅起 HTTP 未起 worker 时,memoir-submit 后任务会堆积。
  • Playground:第 2 步可选轮询 memoir-phase1-ready(前端默认最长约 10 分钟VITE_MEMOIR_PHASE1_WAIT_MAX_MS 可覆盖)。中断时本地草稿可「继续未完成重放」接续同一 conversation_id(仅对话进度;旧版「每轮等待 Phase1」草稿会被跳过并提示改走 memoir-submit)。

A/B 发布口径(追平 A / 超过 A

Playground 的结构化摘要里,后端会给出一份 gate

  • regressed:仍明显落后 Acontext_memory / emotion_carry 等关键项明显回落,或再次出现“重复盘问 / 忽略已答信息”。
  • parity:总分基本追平 A且关键维度未明显退步。
  • surpass:总分显著高于 A同时 context_memory、人物建模等关键项不退步,且未出现重复盘问风险。

建议发布前不要只看单个 case

  1. 先固定一组 黄金样本 fixture(覆盖童年、求学、职业、家庭、价值观,以及长对话样本)。
  2. 每次 prompt / state / anti-repeat 改动后,用同一组 fixture 全量重放。
  3. 要求整组样本里:
    • 不得出现 regressed 的受保护样本;
    • 大多数样本至少达到 parity
    • 目标样本才以 surpass 作为升级完成标志。

如果 compare_summary.truncation.*_truncated_for_compare = true,说明 A/B 对比所用 transcript 仍超过合计预算(compare_cap_total_chars)后做了裁切;单侧较短时会先占满「合计字符池」再裁较长一侧尾部。若仍截断,可略调高 EVAL_JUDGE_CONTEXT_WINDOW_TOKENS / 降低 EVAL_JUDGE_APPROX_TOKENS_PER_CHAR,或见 EVAL_JUDGE_MAX_COMPARE_TRANSCRIPT_CHARS_EACH。结论仍应结合逐轮评分与关键样本人工复核。

手动 GLM-5不写 eval_runs 表)

方法 路径 说明
POST /internal/api/evaluation/judge/conversation body{ "conversation_id" },返回轮次分 + 全文对话分
POST /internal/api/evaluation/judge/memoir-chapters body{ "user_id", "baseline_sections"? }Chapter/Story 分项
GET /internal/api/evaluation/users/{user_id}/memoir-snapshot 只读章节与故事正文快照

回忆录评审可追溯证据闭包lineage

严格闭包口径、synthetic vs library 分表 见同目录 traceable-memoir-lineage.md

手动 /judge/memoir-chapters 与历史自动化 run 的 judge_bundle_json 已按 artifact 绑定证据 组 prompt而不再默认拼接「最近 N 个会话全文」:

  • evidence_tracebundle 完整 JSONsegment / conversation / chunk / fact / timeline / summary、notes 等)。内审计一般够用;若需按类型深链 UI 再排期。
  • format_metatruncateddropped_sectionsincluded_token_estimate区分「prompt 裁掉」与「库中无 lineage」。
  • 生产侧:叙事流水线在每次 Story 写入后覆盖 story_evidence_links,并在当前 story_versions.prompt_meta.memoir_retrieval 写入本轮检索到的稳定 idstory_pipeline_sync._persist_story_lineage_sync)。
  • 章节快照 Phase Cchapter_evidence_snapshots + chapter_evidence_linkschapters.current_evidence_snapshot_id 指向当前版本;评测只读当前快照。刷新见 memoir/chapter_evidence_snapshot.py
  • 对话 memory tracePhase 八):访谈路由下,conversation_messages.memory_retrieval_trace_json 在配对 AI 消息上写入本轮 HybridRetriever 命中的 chunk/fact/timeline/summary/story 等 idmemory/retrieval_trace.py)。

历史数据缺 link / snapshot 时不可评测,需先通过正式流水线重新物化。

Fixture 详情扩展

GET /internal/api/evaluation/fixtures/user-exports/{filename} 在原有 turns 外增加:

  • source_user_id:导出抬头中的 User ID
  • memoir_sections## 回忆录章节(生成正文) 下按标题切分的基线正文(已去掉 {{IMAGE:...}} 占位)