* add staging ios app build script * feat(api): add OpenTelemetry LGTM stack for local observability Wire OTel traces, metrics, and logs through a collector to Tempo, Prometheus, and Loki, with custom LLM instrumentation, dev compose overlay, Grafana provisioning, env templates, and development.sh auto-start. * feat: expand observability, harden dev tooling, and fix expo staging UX Add business and LLM Prometheus metrics with Grafana dashboards, alerting, and a metrics verification script. Wire telemetry through adapters and core LLM paths, and document the local LGTM workflow. Fix development.sh for macOS bash 3.2, open Grafana and eval-web in Chrome, and repair eval-web auto-open (unbound EVAL_WEB_BROWSER_SCHEDULED). Merge internal-eval into the main dev script with improved compose handling. Require EXPO_PUBLIC_* at build time, improve iOS HTTP ATS for staging IPs, show memoir empty state instead of load errors when no chapters exist, and add jest env setup plus chapter list response normalization. * chore: enable Grafana Assistant Cursor plugin * fix: memoir empty state and repair withdrawn 0020_chapters_book_id stamp Show empty memoir UI when the chapter list succeeds with no items; treat auth/404 as non-fatal. Extend alembic revision repair so local dev DBs stamped with the removed 0020_chapters_book_id migration can roll back and upgrade to 0019. --------- Co-authored-by: Kevin <kevin@brighteng.org> Co-authored-by: Cursor <cursoragent@cursor.com>
153 lines
11 KiB
Markdown
153 lines
11 KiB
Markdown
# 内部回归评测平台
|
||
|
||
与主 API(`app/main.py`)隔离进程部署,避免评测候选链路透出给消费者 App。
|
||
|
||
## 启动
|
||
|
||
**推荐一条命令**:`./development.sh` 默认启动主站(**8000**)、Celery、内部评测 API(默认 **7999**)、评测 Web(**5174**);`.env` 中 `OTEL_ENABLED=true` 时并起 Grafana 且自动打开浏览器。`./internal-eval.sh` 仅为兼容转发。
|
||
|
||
| | `./development.sh`(默认) |
|
||
|---|-------------------------------|
|
||
| HTTP | 主站 **8000** + internal **7999** |
|
||
| Celery | 仅 **一个** worker |
|
||
| 评测 UI | `open` → http://127.0.0.1:5174/(`OPEN_EVAL_WEB=0` 可关) |
|
||
| 可观测性 | Grafana :48300(`OPEN_OBSERVABILITY_UI=0` 可关) |
|
||
|
||
若 **主站 + Celery 已在其他终端** 由 `./development.sh` 跑起来了,只在同一台机器上多开评测 HTTP 与前端、且 **不再起第二份 Worker**:
|
||
|
||
```bash
|
||
cd api
|
||
# 确保 .env.development / .env 含 INTERNAL_EVAL_API_KEY;:8000 已被主站监听
|
||
SKIP_INFRA=1 SKIP_INSTALL=1 EVAL_ATTACH_ONLY=1 ./development.sh
|
||
```
|
||
|
||
兼容旧写法:`SKIP_CELERY=1` 会映射为 `EVAL_ATTACH_ONLY=1`(仍要求 **8000 已在监听**)。
|
||
|
||
仅主业务、不要评测台:`LIFE_ECHO_WITH_INTERNAL_EVAL=0 ./development.sh`。
|
||
|
||
若只需 **7999**、不启主站 **8000**,见下文「手动 uvicorn」;不要用一键脚本。
|
||
|
||
**默认会起 `app-eval-web`,并用系统浏览器打开评测台**(`http://127.0.0.1:5174/`,与 Grafana 同为 `open`)。不要前端时设 `START_EVAL_WEB=0`;只要前端但不要弹窗时设 `OPEN_EVAL_WEB=0`。
|
||
|
||
数据库与主服务共用;需配置环境变量后启动专用进程:
|
||
|
||
```bash
|
||
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'
|
||
# DeepSeek(API 模型名 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_app` 已 `include` 回忆录等任务;**不再**包含已下线的 `evaluation_tasks` 实验批量跑批)。需 Phase1 / 叙事推进时请启动 worker:
|
||
|
||
```bash
|
||
uv run celery -A app.tasks.celery_app worker -l info -Q celery,memory_idle
|
||
```
|
||
|
||
## 前端(`app-eval-web`)
|
||
|
||
```bash
|
||
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 install` 在 `app-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 读取 SSE**(chunk),请求头携带 `X-Internal-Eval-Key` 即可;不要求浏览器 `EventSource`。Body 可选 **`judge_provider`**:`zhipu`(默认)| `deepseek`,以及 **`judge_model`**(空则用该供应商环境默认)。首轮 `meta` 事件会回显 `judge_provider` / `judge_model`。
|
||
|
||
新增事件:
|
||
|
||
- `compare_summary`:结构化 A/B 对比摘要,包含 `group_deltas`、关键回落维度、是否出现重复盘问风险,以及 transcript 截断提示。
|
||
- `compare_delta`:原有自由文案流,适合人读;不替代结构化结论。
|
||
|
||
## 评测 Web(`app-eval-web`)
|
||
|
||
- **Playground · 分步测评**:选用户导出 MD 为基线 → `eval-sandbox` + 逐轮 `replay/conversation`(**`skip_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=production` 时 **`POST /api/auth/mock/sms-login` 始终返回 404**。请求体:`phone`(11 位)、`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` | body:`conversation_id`、`fixture_filename` **或** `user_utterances`;可选 **`skip_memoir`**(默认 false;为 true 时不 `queue_message`、且不会仅因 `flush_memoir_after` 而 `flush_pending`)、`flush_memoir_after`(默认 true)、`skip_tts`(默认 true)。响应含 `segment_ids`(本批创建的用户 segment) |
|
||
| `POST` | `/internal/api/evaluation/sessions/{conversation_id}/memoir-submit` | 无 body:收集本会话内 `topic_category IS NULL` 且 `processed` 为 false 的 segment,调用 `flush_pending(user_id, extra_segment_ids=…)`;返回 `segment_ids`、`celery_task_id` |
|
||
| `GET` | `/internal/api/evaluation/sessions/{conversation_id}/memoir-phase1-ready` | query:`segment_ids` 可重复。所列 segment 均已写入 `topic_category` 时 `ready: true` |
|
||
|
||
**默认(`skip_memoir: false`)**:每轮仍相当于主站路径:`create_user_segment` → `process_user_message` → `background_runner.queue_message`;末尾可 `flush_pending`。
|
||
|
||
**Playground 分步(`skip_memoir: true` + `flush_memoir_after: false`)**:只做 `create_user_segment` 与 `process_user_message`,**不**入回忆录队列;对话结束后再调 **`memoir-submit`** 统一 flush。
|
||
|
||
- **TTS**:回放默认 `skip_tts: true`。
|
||
- **Celery**:Phase1 / 叙事仍依赖 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`:仍明显落后 A,或 `context_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](./traceable-memoir-lineage.md)**。
|
||
|
||
手动 `/judge/memoir-chapters` 与历史自动化 run 的 `judge_bundle_json` 已按 **artifact 绑定证据** 组 prompt,而不再默认拼接「最近 N 个会话全文」:
|
||
|
||
- **`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` 指向当前版本;评测只读当前快照。刷新见 `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 / snapshot 时不可评测,需先通过正式流水线重新物化。
|
||
|
||
## Fixture 详情扩展
|
||
|
||
`GET /internal/api/evaluation/fixtures/user-exports/{filename}` 在原有 `turns` 外增加:
|
||
|
||
- `source_user_id`:导出抬头中的 User ID
|
||
- `memoir_sections`:`## 回忆录章节(生成正文)` 下按标题切分的基线正文(已去掉 `{{IMAGE:...}}` 占位)
|