Lindorm 用户聊天原文查询
按 UID 从 Lindorm 的聊天记录索引里拉用户聊天原文。索引名、字段名都通过环境变量注入,适配不同部署。
快速开始
# 按原始 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 后端表示宽表名(默认取 envLINDORM_TB_DEFAULT_TABLE),不是 ES 索引全名。 表名带rich特征的用user_id原值,其余用rev_user_id(脚本自动反转;负数值须加--reversed),脚本按表名切换。
批量多 UID:tb_chat_batch.sh --uids "u1,u2,..." --size 1000(严格串行、UID 间限速,护共享 devbox)。
实现里必须守的坑(都踩过,代码已应对)
- 单 MCP 容器会偶发串台(返回别的请求结果)——每页必须核对返回回显的 SQL==发出的 SQL,不符即重试(
_parse_tb_lindorm.py做校验)。 - 宽表 SQL 查询返回只展示前 50 行 → 分页
LIMIT<=50 + OFFSET,逐页拼。 - 宽表 9 秒硬超时 → 只做带
WHERE rev_user_id=的分区点查;禁全表GROUP BY/DISTINCT/无过滤 ORDER BY(实测GROUP BY rev_user_id被存储引擎按groupby.keys.limit=1000熔断)。 export_result落网关容器侧、拿不回本机 → 不用它,直接解析返回表格落本机。- 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 输出)。