# Lindorm Chat History

> 按 UID 查用户在 Lindorm(LindormSearch/ES 兼容) 里的聊天原文。使用时：用户想查某个 UID(或反转 UID)在 chat history 索引里的聊天记录、会话原文、消息内容时触发。

- Skill: `igoingdown/lindorm-chat-history` (Agent Skill, multi-file: 8 files)
- Install (CLI): `npx skillmds@latest add igoingdown/lindorm-chat-history`
- Raw SKILL.md: https://api.skillmd.com/api/skills/igoingdown/lindorm-chat-history/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: igoingdown (https://skillmd.com/u/igoingdown)
- Updated: 2026-09-21
- Page: https://skillmd.com/skills/igoingdown/lindorm-chat-history

---


# Lindorm 用户聊天原文查询

按 UID 从 Lindorm 的聊天记录索引里拉用户聊天原文。索引名、字段名都通过环境变量注入，适配不同部署。

## 快速开始

```bash
# 按原始 UID 查（脚本自动反转后匹配用户字段）
bash query_chat_history.sh 100000000000000001

# 手上已经是反转后的值（或索引直接存原始 UID），加 --reversed 跳过反转
bash query_chat_history.sh 100000000000000009 --reversed

# 只数命中条数
bash query_chat_history.sh <uid> --count

# 只看某个会话
bash query_chat_history.sh <uid> --conversation <conversation_id>

# 覆盖索引名（否则取 env LINDORM_CHAT_INDEX）
bash query_chat_history.sh <uid> --index my_chat_index

# 导出该用户全部消息到 JSONL（search_after 翻页）
bash query_chat_history.sh <uid> --export /tmp/chat.jsonl
```

## 关键背景（务必先读，都是踩过的坑）

- **端点通常是 LindormSearch 搜索引擎**（ES 兼容，`proxy-search` 端口默认 30070），**不是 LindormTable SQL**。必须走 ES REST 接口；`lindorm-cli` 对这个地址无效，会报 `parse url failed`。
- **UID 可能是反转存储的**：有些部署把用户 ID 十进制字符串整体按字符倒序后再入库（打散分片、避免写热点），存进 `rev_user_id` 之类字段。脚本默认把传入 UID 反转后再匹配。
  - 若你手上已是反转值 → 加 `--reversed`。
  - 若索引直接存原始 UID → 设 `LINDORM_USER_FIELD` 指向原始字段并加 `--reversed`（等于不反转、按你给的值查）。
  - **反转后必须按有符号 int64 补码回绕**：存储侧存的是 `int64(ReverseUint64(uid))`，反转后的十进制超过 `INT64_MAX`（实测约 **7.7%** 的 UID）就会回绕成负 BIGINT。脚本用 `$(( 10#… ))` 复刻这个回绕（bash 是 64 位有符号运算），顺带去掉前导零。**只做十进制反转、不回绕，是曾经的真 bug**：反转值当正数发给 BIGINT 列会被存储引擎报 `Can't convert to BIGINT. Overflow`，而这个错常被外层吞成"查不到"，整批悄悄漏掉这批用户——排查"串台/查不到"时先看有没有踩这个。
- **有的索引 `_source` 被关闭**（`enabled:false`），默认命中不返回文档，必须用 `docvalue_fields` 显式点名字段。脚本已按 docvalue 取值。
- **若 `content` 是 `keyword` 且设了 `ignore_above`**：超过该长度的单条消息不会进该字段索引，可能取不到。
- **"这个字段不存在"必须先核 mapping 再下结论**：`_source` 关闭时，没在 `docvalue_fields` 里点名的字段一律不返回，看起来和"表里压根没这列"一模一样。判断字段有无只认 `GET <index>/_mapping`（再对照存储侧的建表定义），不认"查了一次没返回"。踩过：据此宣布索引缺 sender 字段，被用户当场纠正"这个表有该字段，你的结论明显是错的"。
- **缺发送方/角色字段时，禁止用序号奇偶推断角色**：会话内序号未必从固定一方起步，也可能有系统消息、重发、异步补写打乱奇偶——按奇偶分 user/assistant 的标注会成片错，且错得看不出来（用户第一反应就是"按奇数偶数分角色可能不准呀"）。正确做法是多源交叉补齐这一列：① 先核 mapping / 建表定义确认索引或存储里是否本就有该字段（多数情况是有的，只是没取）；② 存储侧按主键点查同一条消息，拿它的发送方枚举值；③ 仍缺就用日志侧的请求/落库原文按消息 ID 对齐反查。三条都不通时，标注只能标 unknown，不许用奇偶顶上。

## 角色归属：先找权威源，回贴要锚定

标注每条消息是 user 还是 AI（含整理取证文档时的 role 列），启发式有三种已翻过车的形态，一律不许当判据：**长度阈值**（长≈AI——边界差一个字整条翻转）；**序号奇偶交替**（见上）；**文体/人称**（重度角色扮演用户会用第一人称+动作描写写自己，也会整段代写 AI 角色的台词与心理活动——"这段读起来像 AI 写的"不构成改判依据，先当成两个假设分别验证：数据归属错了，还是用户真的在代写）。正确路径是按优先级找**权威源**，再拿权威源回贴：

- **权威源优先级**：① 服务端逐轮双标日志——同一条结构化日志里分别带"用户发送原文"与"AI 回复原文"字段，等价 sender 真值；限制是日志保留期与单条截断。② 落库/送模前的攒批原文——逐行带角色占位符标注，随数据长存，但每批只覆盖几轮。③ 结构判定的系统消息——奖励/事件类 JSON 既非 user 也非 AI，单列 SYSTEM，别当对话渲染。④ 低置信启发式兜底——只在强信号时用，必须标 ⚠️ 存疑。
- **回贴匹配必须单向、前缀锚定**：把权威源文本匹配回消息时，禁止"双向子串 + 首个命中即停"——短的 user 原文恰好是某条长 AI 回复的子串时，会把没被权威源覆盖的 AI 轮误标成"已核实"，制造伪权威（真实翻车：一条近三千字的纯 AI 独白被标成"user·已核实"，被用户当场抓出）。只认"权威源文本的归一化前缀 == 消息内容前缀"；同一前缀能映射到多个角色就判 ambiguous 不采信。
- **覆盖率如实报**：匹配严格化后"已核实"覆盖率会明显回落，那才是真实值；未覆盖的轮回落到启发式并标 ⚠️，不许为了覆盖率好看放宽匹配。

## 宣布"查不到"之前：先翻自己的持久记忆

这条索引查不到的东西，常常在别的通道里查得到，而那条通道很可能是**之前的会话已经打通并写进持久记忆的**。踩过的返工形态：宣布某字段/某数据拿不到，用户回一句「你的记忆里有一个结合多个数据源确认这个字段的方法，你试试」——同一句提示在不同会话里被重复了三次，说明每次都是先下结论、后被提醒。规则：

- 说"拿不到/查不到/没有这个能力"之前，先检索持久记忆里的现成通路（多源交叉的取数手法、代理/绕行方式、凭据位置），命中就直接按它跑，不要重新发明。
- 接手一个需要多源拼装的取证任务时，第一步就把记忆里已有的数据读取能力清点一遍并列给用户，让用户能补漏。用户的说法是「这些新的数据读取能力都在你的记忆里，有问题可以问我」。
- 用完新打通的通路，把它写回持久记忆（入口、凭据来源、限制），下一个会话才不用再被提醒一次。

## 给索引补字段：存量不会自动回填

查询要用的字段索引里没有时，`ALTER INDEX ... ADD COLUMNS(<字段>)` 只让**新写入**的数据带上它，**存量文档不会自动补**——加完立刻查存量仍是空，这不是没生效。判断方法：对同一索引分别数 `exists:<字段>` 的命中数和总数，比值就是覆盖率。要让存量可查只能重建索引，而重建在大索引上是重活，动手前按下面几条先评估，别直接发命令：

- **先在小索引上跑通流程**：同集群挑一个量级小的同类索引验证「重建后存量确实能查到该字段」，再谈大索引怎么排期。
- **算磁盘余量**：重建是「先建新分片再切换删旧」，期间会额外占用接近一份索引的空间。剩余空间不足一份索引大小就会撑爆并触发只读水位，先看各节点余量再决定。
- **确认重建期间的可用性**：向平台/DBA 确认该索引在重建期间是否降级或短暂不可用。若线上有查询依赖它，需要配合功能降级窗口；只用于离线排查、线上不依赖的索引风险低得多，先把「谁在用这个索引」查清楚再定级。
- **异步 + 限速 + 低峰**：同步重建会把连接挂到结束、大索引上几乎必然超时，用异步形态提交；能限速就限速（重建吃 IO/CPU/heap，压力会直接打到同集群的线上查询），并挑业务低峰执行。
- **进度可观测**：用上面那个 `exists:<字段>` 覆盖率比值当进度指标轮询，不要靠猜。
- **执行归属**：生产索引重建交用户/DBA 在约定窗口执行，本 skill 侧只负责把命令、风险、观测口径写清楚，不擅自触发。

### 重建报超时 ≠ 重建在跑：先测状态，别重试

`REBUILD` 这类语句在大索引上几乎必然把客户端等到超时（报文形如"等待 xx 毫秒后超时"），而**超时只说明客户端没等到应答，不说明服务端有没有真的开工**。两种可能后果完全相反：真开工了再重试一次就是并发重建、雪上加霜；根本没开工却以为在跑，就会干等几小时。所以收到超时后第一件事是**只读取证**，不是重试也不是换个写法再发一遍：

- **两次采样看覆盖率有没有涨**：隔十几分钟各数一次 `exists:<字段>` 命中数与总数，比值不动（只随新写入微涨）= 没有回填在跑。这是最直接的判据。
- 顺带核**分片/恢复状态**与索引体积、各节点磁盘余量：没有处于恢复态的分片、分片均匀且都正常在线，进一步佐证没有重建任务。
- 把"用户实际执行的语句"和"仓库里留档的语句"对齐一遍：**加字段（只改元数据，秒级返回）和回填存量（重扫全量，重活）是两条完全不同量级的操作**，报错常来自后者而用户以为在跑前者。先说清这一点，再谈下一步——否则会顺着错误的那条给建议。

另外，超时前先判断**这条命令该不该跑**：如果加字段那步已经成功（mapping 里字段在、新写入数据都带上了），而排查只需要新数据，那么为几亿文档做一次全量回填可能根本不必要。给出"要不要重建"的判断，别默认用户提的动作就是对的动作。

### 动手前先核清「这个端点能不能执行这条语句」和「凭据够不够权限」

同一套存储常同时暴露**两条协议完全不同的通道**：搜索侧（ES 兼容 REST，查文档/看 mapping/数覆盖率）和表侧（数据库协议，`ALTER INDEX` 之类的 DDL 只在这边有）。踩过：手上只有搜索侧地址，却被要求"帮我执行重建"——那条语句在搜索侧天生不存在对应命令，怎么试都不会成功；而用户报错里的地址其实来自另一个控制台通道。

- **先按端点性质分类，再答能不能执行**：地址里的用途片段与端口能区分通道；配置里那些"控制台网页链接"不是数据库连接串，别当端点用。
- **凭据的权限先看清**：只读账号（名字里常带 readonly / 临时字样）即使地址对了也做不了 DDL。缺权限时如实说"凭据不存在/权限不够，我做不了"，并说明需要什么样的账号，而不是把失败当成"再试一次可能就好了"。
- **网络能不能连通先自查——出口 IP 白名单**：直连 Lindorm 实例（任意协议：ES REST / MySQL 兼容 / HBase 兼容）时，本机**公网出口 IP 必须在实例白名单里**，否则连接被拒或超时。这个表象酷似网络抖动或凭据错误，实则两者都没问题——凭据齐、地址对却连不上时，先确认出口 IP 是否已加白，别在换凭据/重试上空耗。
- **直连与经网关（如 DMS/tool-bridge）是相互独立的故障域**：网关侧的租户失效、授权过期（如报 `TenantNotExist`）不代表实例本身挂了，反之亦然。一条通路断了先判断断在哪一层，另一条通路可能仍可用作绕行。
- 用户说"信息都在我的凭据文件里，你帮我执行"时，仍要先核出那份凭据对应哪条通道、什么权限，再回答做不做得了——直接开跑会在错误通道上白折腾几轮。

配套：这类 DDL / 索引变更语句要落到服务仓库存放建表与索引脚本的目录里留档，别只存在于一次会话的聊天记录里——下次有人重建环境时找不到就会漏掉这一列。

## tool-bridge 后端（可选）

除 lindorm 直连（默认）外，可经团队 **tool-bridge** 网关查询：`--backend tool-bridge`。
定位：TB 是**可选的接入层**，不是替代——lindorm 直连仍是默认，TB 挂了不影响查询（故障域不收敛）。

**实测结论（逐条跑通，含随机 UID 端到端验证）**：TB 的聊天数据源有**两套引擎，别混**：

- **搜索侧计数/聚合工具（ES）**：`count`/`aggregate` 只出数字/聚合、**禁正文**，且搜索索引 mapping **没有 `sender_type` 字段**（只有 content/character_id/rev_user_id/sequence 等少数几个）。
- **宽表侧 SQL 查询工具**：**可以 `SELECT content` 拿正文 + `sender_type` 真值（1/2，不用猜角色）**。这才是拿原文的正路。

所以 `--backend tool-bridge` 三种模式都实现了：

- `--count` → 宽表 `SELECT COUNT(*) WHERE rev_user_id=<REV>`（分区点查，毫秒级）。
- `--size N`（page）/ `--export <file>` → 宽表 SQL 查询分页取原文，落**本机** JSONL（含 character_id/sequence/sender_type/timestamp/content）。
- `--index` 在 TB 后端表示**宽表名**（默认取 env `LINDORM_TB_DEFAULT_TABLE`），不是 ES 索引全名。
  表名带 `rich` 特征的用 `user_id` 原值，其余用 `rev_user_id`（脚本自动反转；负数值须加 `--reversed`），脚本按表名切换。

**批量多 UID**：`tb_chat_batch.sh --uids "u1,u2,..." --size 1000`（严格串行、UID 间限速，护共享 devbox）。

### 实现里必须守的坑（都踩过，代码已应对）

1. **单 MCP 容器会偶发串台**（返回别的请求结果）——每页必须核对返回回显的 SQL==发出的 SQL，不符即重试（`_parse_tb_lindorm.py` 做校验）。
2. **宽表 SQL 查询返回只展示前 50 行** → 分页 `LIMIT<=50 + OFFSET`，逐页拼。
3. **宽表 9 秒硬超时** → 只做带 `WHERE rev_user_id=` 的分区点查；**禁全表 `GROUP BY`/`DISTINCT`/无过滤 ORDER BY**（实测 `GROUP BY rev_user_id` 被存储引擎按 `groupby.keys.limit=1000` 熔断）。
4. **`export_result` 落网关容器侧、拿不回本机** → 不用它，直接解析返回表格落本机。
5. **rev_user_id 是有符号 int64、可为负** → 反转后超 `INT64_MAX` 要按补码回绕成负 BIGINT（脚本 `$(( 10#… ))` 已做，约 7.7% UID 命中）；不回绕会 `Overflow` 被吞成 `n=-1` 空结果。UID 校验也放行负号：负值本身就是 rev 值，直接传须加 `--reversed`。

### 其它拿原文的路（对照）

- **lindorm 直连（默认后端）**：ES `docvalue_fields` 取 `content`，`--export` 走 search_after 翻页落本机。**但 ES 索引无 `sender_type`**，要角色得回宽表补——所以要 sender_type 时优先用 TB 宽表后端。
- **只读查库（如经 bytebase 类 MCP）**：原文若在后端主库 MySQL，只读 `query` 可 SELECT。库定位务必带 instance（prod/test 实例名与库名放本地配置，不进仓库）。

**接入步骤见 `TUTORIAL.md`**。配套：`tb_call.sh`（渐进发现）、`tb_probe.sh`（自证）、`_parse_tb_lindorm.py`（解析防串台）、`tb_chat_batch.sh`（批量串行）。
TB 凭据 `TB_BASE_URL`/`TB_SK`、工具节点路径 `LINDORM_TB_*` 全放 secrets.sh（用 tb_probe.sh 实测后填）。

### 网关其它能力的可用状态（按你自己网关实测填）

本 skill 只用到「计数」和「取原文」两块能力。网关上其它源的可用性因你的 token 作用域和各源授权状态而异，用 `tb_call.sh tree` + `tb_probe.sh` 实测你自己网关后记在本地笔记里：

tool-bridge 网关上除「计数」「取原文」外通常还挂着别的源（日志/trace、部署、代码平台、只读查库、
飞书文档、项目管理、App 构建平台等）。可用性因 token 作用域和各源授权/绑定状态而异，几条与具体
网关无关的通用经验：

- **读写混排的源**：靠工具名 `get_/list_/search_` 前缀判读写，写操作调用前必看 `~help` 并让用户确认。
- **飞书文档**：网关侧走应用身份(tenant_access_token)受 ACL 限；**用本机 `lark-cli`(用户身份)替代**更省事。
  读 docx：`lark-cli docs +fetch --doc <id> --doc-format markdown`。
- **需绑定操作人身份的源**：有的源要管理员把你的 SK 绑到操作人身份才能用；未绑时连只读都可能 `permission_denied`。
  替代通道可能用**固定挂载身份**——开箱即用，但写操作署名不是你，落地前想清楚署名归属。
- **需 OAuth / 特殊 scope 授权的源**：普通 token 常无「注册授权挂载点」的 scope，得管理员用 admin token 授权。
- **CLI 前置**：网关 CLI 可能要较新 Node（用前先 `nvm use` 切到受支持版本）。

详见 `TUTORIAL.md` 附录。

## 配置（索引名/字段名放 env，真实内部命名不进仓库）

| 变量 | 必填 | 默认 | 说明 |
|------|------|------|------|
| `LINDORM_ENDPOINT_URL` | 是 | — | LindormSearch 接入地址，如 `http://ld-xxx-proxy-search-public.lindorm.rds.aliyuncs.com:30070` |
| `LINDORM_USER` | 是 | — | 用户名 |
| `LINDORM_PASSWORD` | 是 | — | 密码 |
| `LINDORM_CHAT_INDEX` | 是 | — | 聊天记录索引名（也可用 `--index` 传） |
| `LINDORM_USER_FIELD` | 否 | `rev_user_id` | 用户 ID 字段（term 匹配用） |
| `LINDORM_CONV_FIELD` | 否 | `conversation_id` | 会话 ID 字段（排序/过滤用） |
| `LINDORM_SEQ_FIELD` | 否 | `sequence` | 会话内序号字段（排序用） |
| `LINDORM_CONTENT_FIELD` | 否 | `content` | 消息原文字段 |
| `LINDORM_EXTRA_FIELDS` | 否 | — | 额外要取的字段，逗号分隔 |

凭据/配置统一放 `~/github/my_dot_files/secrets.sh`（可用 `SECRETS_FILE` 或 `--secrets` 改路径），脚本启动时若 env 未设置会自动 `source`。完整示例见 `secrets.example.sh`。密码通过 curl `-K` 从 stdin 传入，不会进 `ps` 或命令历史。

结果按会话字段再按序号字段排序，即每段对话的自然顺序。

## 拉完之后：怎么组织成可 review 的取证材料

聊天原文常常只是链路的一环。要复盘"用户这一轮到底经历了什么"，需要把原文和日志侧的中间产物按消息 ID / 时间对齐拼起来，而不是只贴聊天记录。踩过的返工点：

- **先在本地整理成表格（CSV / JSONL）跑通一遍，再同步到协作文档**。直接边查边往云文档里写，字段缺漏和顺序错乱要到用户 review 时才暴露，返工一次成本很高。本地那一版就是自查用的：每行一轮，列齐"输入 / 中间产物 / 输出"，自己从头到尾读一遍再上传。
- **组织维度是"每个用户一章 → 章内按对话轮次顺序"**，不是按数据源或按字段分块。用户要看的是时间线，不是"这是 A 源的数据、那是 B 源的数据"。
- **非中文内容逐条给中文译文，一条都不能漏**。用户明确要求原文 + 译文对照，包括摘要类、要点类等衍生字段——只译对话正文、漏译衍生字段会被打回。
- 单个用户几十上百轮时原文体量很大：一律先落盘（`--export`）再用脚本统计，上下文里只留结论与少量示例，别把整段原文读进对话。

## 拿聊天原文喂模型（摘要/抽取/评测）之前：先把场景筛干净

聊天索引里混着多种链路的消息：单角色聊天、多角色、剧场/影院式功能、附属小工具、系统事件。把它们不加区分地拼成上下文喂给模型，产出的摘要会带上不属于目标场景的情节，事后看是"幻觉"，实际是**输入就脏**。用户对此的要求很硬：「我只要单角色聊天的上下文」「除了这几种，是否还有其他场景或链路？请结合代码分析清楚……这个工作量比较大，但必须前置做好，否则后面一旦出现问题，返工会非常困难」。规则：

- **场景枚举先做到不重不漏，再动手跑数据**：回代码把写入这张索引的**全部**链路列一遍（每条链路的入口、写入时带什么标记、能否与目标场景区分），逐条给"保留 / 剔除 / 无法区分"三态。凭印象列三四种就开跑，是返工的主要来源。
- **无法区分的链路要显式挂账**：某场景在索引里没有可判别字段时，写清判别缺口与临时替代判据（如按会话维度的其他特征），并标为已知偏差，不要悄悄按目标场景处理。
- **筛选逻辑要落到具体字段与表**：需要联多张表才能筛出目标数据时，写清"从哪张表按哪个条件取哪些列"，而不是"取单角色聊天的内容"这种口头口径。
- **产出里出现来源不明的具体事实，必须溯源到输入，不能只改 prompt**：摘要里冒出一条原文里没有的具体属性（年龄、身份、地点之类）时，第一动作是**回溯它来自哪一条输入**——同批混进来的其他场景内容、模型对角色设定的补全、还是上一轮摘要滚进来的旧错误。定位到来源再谈修法；直接加一条"禁止编造"的 prompt 约束是掩盖，不是修复。
- **多轮滚动更新会放大既有错误**：摘要在旧摘要基础上迭代时，一次污染会一路带下去且越来越像事实。评估时专门测这一条：连续多轮更新后，早期注入的错误是否还在、有没有被放大。
- **验收 = 机械核验 + 人工盲审两道**：机械核验查可判定项（是否出现禁止的原话、长度是否超限并被截断、字段是否齐全、场景是否越界）；人工盲审查语义质量，且要盲（不告诉评审者哪份来自哪个方案）。只跑其中一道不算验收。
- **一次喂不完时按"完整一轮"切，不做定长截断**：一轮对话是 user + 角色回复的整体，切在中间会让上下文失去意义。做法是读出多轮后按轮次累加到预算上限，超了就**回退一轮**；仅当单轮本身就超长时才不得已截断，并把截断计数打进观测——预期为零的截断一旦发生必须能被发现。
- **并发拉取要评估对线上的压力**：为了跑得快而并发读消息列表会直接压到线上存储。先算并发度 × 单次读取量，给出限速方案，别默认拉满。
- **产出质量存疑时，回到真实原文对齐，别只盯产出本身**：摘要/抽取的结果看着不对（信息稀薄、抽错重点、疑似退化）时，按 UID + 角色 + 会话三元组把**那一条**用户真实原文拉出来对照，先判定"是产出逻辑坏了，还是这条输入本就没料"。取原文的顺序沿用本 skill：先走聊天索引，查不到再从存储侧按主键点查——这一步是把"幻觉"和"输入稀薄"区分开的唯一实锤依据。
- **短会话是已知的退化触发点，要单列成一类**：只聊了一两轮、原文很短的用户，摘要天然抽不出东西，产出稀薄不等于逻辑有 bug。评测时把"原文轮数/字数"作为一个维度先分桶，短会话的差产出单独归因，别混进"模型退化"的结论里冤枉模型。
- **评一个"记忆/摘要注入"类特性时，抽样必须落在该特性真正生效的人群上**：这类特性只在"逐字窗口装不下、发生了截断"的会话里才起作用。按流量随机抽 UID，绝大多数会话根本没截断，摘要没有用武之地，会把信号稀释到看不出差异——此时"两组无差异"说明不了特性无用，只说明根本没测到它该起作用的场景。截断必须用**运行时权威信号**判定（如落库/送模日志里的截断标记、`截断后条数<原始条数`），不能用"消息数超某阈值"这类静态阈值近似。同一个抽样错误在信息增益、遗忘抱怨两轮评估里各犯过一次，代价是整轮结论作废重跑。
- **低频信号用"率 + 分母 + 置信区间/显著性检验"下结论，不用绝对数**：漏记、遗忘抱怨、复述抱怨这类事件天然稀疏。"实验组 4 个 vs 对照组 1 个"这种绝对个数完全落在噪声里，且极易被单个评委的一次判定波动带偏；必须除以各组"真正发生截断的会话数"得到率，再给 CI 和显著性检验。两组 CI 大幅重叠 = 差异不显著，就不能宣称"确认有增益/有回归"，哪怕点估计方向很诱人。还要看这个"率差"是不是主要由某一个评委贡献的——若某信号几乎全来自单一评委、双评委共识很弱，结论要标为不稳。
- **"摘要独有的信息增益"必须先排除该信息已在同批注入的逐字窗口里**：判断"摘要补的早期信息 AI 有没有用上"时，容易把"摘要和逐字历史里同时存在的信息"错记成摘要的独有增益——其实模型从窗口内的历史就能看到，摘要给不给都一样。所以每一条"增益"case 都要回查用户最近的上下文窗口里有没有同样的信息，共存的一律不算独有增益。这一步必须交给判官按语义判，别用"抽实体/关键词匹配"来近似——实体粒度太粗，会把改写、指代当成不同信息。
- **评"生成质量"时，评测自身的重新生成链路两臂都不能注入被测变量**：评测有两种不同口径要分清——「信息增益」拿的是已经发生的线上真实产出来判；「生成质量」往往要把原始对话重新喂模型、现场生成一份新回复再打分。后者最容易踩的坑是：为了"对齐两组"，在重跑时给实验组和对照组都注入了被测变量（如摘要），等于把想测的那个处理提前灌进了基线，两臂不再只差这一个变量，质量分根本不可比。规则：评测自身的重新生成一律保持中性、两臂都不注入被测处理；处理的真实效果只通过线上真实产出或受控实验设计来体现，绝不在判官/重跑链路里二次注入被测变量。这条对任何"注入类处理"的离线质量评测都通用，不限于摘要——用户当场纠偏并要求沉淀成后续通用纪律。
- **给人/AI 复核的材料必须完整，截断会静默污染判断**：凡是要喂给人或判官 review 的对话原文、摘要，一律完整展示，不做定长截断。尤其警惕**日志侧截断留下的省略号**——它和"内容本来就短"长得一样，会让 review 误判信息缺失或增益有无。产出文档里出现省略号时先分清是自己主动省的还是被日志截断的，被截断的必须回原始源补全再评。用户为此反复强调过：「凡是要给 AI 和人 review 的内容都要完整显示，不然信息会有遗漏」「summary 无论如何都完整不截，有问题就在文档里记录下来」。
- **验数据质量用两个不同模型交叉判，不自证**：让产出摘要的同一个模型再来评判自己抽得好不好，是自证。换两个不同来源/家族的模型分别对同一批样本打质量分，结论一致才可信，分歧的样本恰好是要人工细看的。
- **评委模型一经选定就跨轮固定**：多轮评估之间换评委，前后分数失去可比性，等于每轮都在重开基线。评委对（两个不同家族的模型）在第一轮就和用户敲定并记录进评估档案，此后每轮重跑都用同一对；确需更换视为新基线，全量重评。用户为此当场叫停过一次：「评委务必用这两个模型再跑一遍，这是纪律」。
- **prompt 一改，评估必须全量重跑（纪律）**：生成侧 prompt 有任何改动后，对同一套历史样本**完整重新生成 + 重新打分 + 重新过 rubric**，不许只跑新样本、只复评分歧样本、或拿旧分数对比新产出——口径变了分数就不可比。评估成本是已接受的代价，用户原话「每次改了 prompt 都要做，花钱就花了，不要偷懒」。结论存疑时先扩大样本量再重跑，不在小样本上硬下结论。
- **专门抽小语种样本验「输出语言 = 输入语言」**：摘要/抽取类 prompt 若没有硬性要求「输出语言跟随输入语言」，在多语言语料下极易出现语言漂移——中英文样本看着正常，非中非英的对话产出却被写成了英文或中文。评测时**按对话语言分桶，专门捞非英、非中的样本**逐条比对产出与原文语言是否一致；用户的追问形态是「prompt 里好像没强调输入输出语言一致，多语言情况下会不会有问题？帮我捞非英非中的对话看看 summary 语言对不对」。发现漂移先定位是 prompt 缺约束还是模型能力问题，再谈修法，不要只在中英样本上验就下「语言正常」的结论。

## 依赖

`curl`（必需）、`jq`（美化/导出模式需要，缺失时降级为原始 JSON 输出）。

