跳到正文
曾亮懿 ZengLiangYi Blog

AI 编程对话复盘:分母、质量抽检与可复用证据

以合成样本核对来源与长度分母、错收漏收和空样本,并区分当前经验筛选、模型摘要与真实复用结果。

曾亮懿

曾亮懿

阅读约 5 分钟
目录 展开 收起

把很多 AI 编程对话归一化后,能更系统地复盘来源、任务、排查过程、决策与复用结果。数量本身不能证明效率提升。原稿的 5247 条对话、3811 条笔记、标签排名和个人八个月故事没有可审计数据集,本文不把它们当作事实,而给出可执行的统计和质量抽检方法。

先定义统计分母与缺失范围

server/src/routes/status.ts 的 stats 分别对 conversations、notes 和 tags 全表计数,包含库中的 demo 数据。排除 demo 的 realConvCount 只用于判断 isSeeded,会话关联查询只用于 recentNotes;不能把这些条件套到总计数上。这是应用当前库的快照,不是所有工具中的原始历史。CLI 的 conversations 和 notes list 分页返回 items/total,search 则返回数组。第一页数量、搜索命中数或标签使用次数不能互相替代。采集时保存目标实例、固定版本、日期、过滤条件、分页范围以及导入错误和未支持的来源;需要排除 demo 时,在自己的纳入规则里明确执行并记录。

问题 合适的单位/分母 容易混淆
来源与项目分布 同一纳入范围的归一化会话 原始文件数、重复导入与缺失来源
长度分布 每篇归一化消息数,区间不重叠 消息总数与会话数,解析遗漏
标签分布 指定笔记集合上的使用次数 多标签累计不必等于笔记数
摘要覆盖 纳入会话的笔记/筛选结果分别计 filtered 不等于摘要失败或已有笔记
复用效果 同任务可追溯的验收结果 命中标签或生成摘要不等于解决问题

原始副本、统一消息、模型摘要和可读导出是不同层,见对话管理方案。缺失思考、工具记录或构造时间会影响后续统计,不能把归一化库当无损的原始行为记录。

对四类模式提出可证伪的问题

短问答是否低价值,应看是否有明确问题、处理过程、结果和复用条件;短的完整修复也可能有用。调试笔记要保留被排除的假设与验证,不只抄最终答案。架构讨论应记录备选项、约束、取舍和实际验证,可另写 ADR。看起来重复的提问要核对项目、版本、根因和结果,语义相似不等于同一问题,更不能直接推算节约了几次工时。

当前 server/src/services/experience/gate.ts 已有信号预筛、模型维度与合并规则,不是未来才计划“按少于五条消息且无代码过滤”。server/src/services/summarize.ts 对未接受的会话记录 filtered 及原因,已有笔记和用户拒绝还有不同分支。摘要与嵌入/关系结果分别检查,避免把队列完成率当知识质量。

用合成标签演示分母和错判清单

保存为 retrospective.mjs,用 Node 24 执行 node retrospective.mjs。六条都是手写合成样本;expected 是示例人工标签,selected 是预先写好的判断,不调用当前 gate、模型、CLI、数据库或真实导出。程序核对来源分母、互斥长度区间、混淆计数和空分母,并保留错收与漏收的 ID。结果只属于这六条,不是用户数据上的精确率或召回率。

import assert from 'node:assert/strict';

// Hand-labelled synthetic cohort, not exported conversations or model judgments.
const cohort = [
  {
    source: 'fixture-a',
    id: 'short-fix',
    messages: 4,
    project: 'one',
    expected: true,
    selected: true,
  },
  {
    source: 'fixture-a',
    id: 'long-chat',
    messages: 100,
    project: 'one',
    expected: false,
    selected: true,
  },
  {
    source: 'fixture-b',
    id: 'quick-info',
    messages: 2,
    project: 'two',
    expected: false,
    selected: false,
  },
  {
    source: 'fixture-b',
    id: 'missed-fix',
    messages: 24,
    project: 'two',
    expected: true,
    selected: false,
  },
  {
    source: 'fixture-c',
    id: 'decision',
    messages: 45,
    project: 'three',
    expected: true,
    selected: true,
  },
  {
    source: 'fixture-c',
    id: 'unresolved',
    messages: 80,
    project: 'three',
    expected: false,
    selected: false,
  },
];
function audit(items) {
  const seen = new Set();
  const sources = {};
  const bins = [0, 0, 0, 0]; // <10, 10–29, 30–79, >=80
  let tp = 0,
    fp = 0,
    fn = 0,
    tn = 0;
  const falseAccepts = [],
    falseRejects = [];
  for (const item of items) {
    const key = `${item.source}:${item.id}`;
    assert.equal(seen.has(key), false);
    seen.add(key);
    assert.ok(Number.isInteger(item.messages) && item.messages >= 0);
    sources[item.source] = (sources[item.source] ?? 0) + 1;
    bins[item.messages < 10 ? 0 : item.messages < 30 ? 1 : item.messages < 80 ? 2 : 3]++;
    if (item.selected && item.expected) tp++;
    else if (item.selected) {
      fp++;
      falseAccepts.push(key);
    } else if (item.expected) {
      fn++;
      falseRejects.push(key);
    } else tn++;
  }
  const ratio = (a, b) => (b === 0 ? null : a / b);
  assert.equal(
    Object.values(sources).reduce((a, b) => a + b, 0),
    items.length
  );
  assert.equal(
    bins.reduce((a, b) => a + b, 0),
    items.length
  );
  assert.equal(tp + fp + fn + tn, items.length);
  return {
    total: items.length,
    sources,
    bins,
    tp,
    fp,
    fn,
    tn,
    precision: ratio(tp, tp + fp),
    recall: ratio(tp, tp + fn),
    falseAccepts,
    falseRejects,
  };
}
const result = audit(cohort);
assert.deepEqual(result.bins, [2, 1, 1, 2]);
assert.equal(result.precision, 2 / 3);
assert.equal(result.recall, 2 / 3);
assert.deepEqual(result.falseAccepts, ['fixture-a:long-chat']);
assert.deepEqual(result.falseRejects, ['fixture-b:missed-fix']);
assert.equal(audit([]).precision, null);
assert.throws(() => audit([...cohort, cohort[0]]));
const lengthOnly = cohort.map((item) => ({ ...item, selected: item.messages >= 5 }));
assert.ok(audit(lengthOnly).falseRejects.includes('fixture-a:short-fix'));
console.log(JSON.stringify({ scope: 'six synthetic labelled fixtures only', result }, null, 2));

让错误口径被检查捕获

去掉重复 source:id 的拒绝断言,重复样本的 assert.throws 应失败;把空分母 ratio 改为 0,空样本 precision=null 的断言应失败。恢复代码再运行。长度门槛漏掉 short-fix 只是这里的反例,不给某模型或当前产品排名。

真实抽检先保留全部纳入/排除 ID,按来源、任务、长度和决策状态抽样,对同一原文独立标记遗漏、虚构、错误归因和复用条件;保留分歧及裁决。训练/规则调整和留出验收分开,报告具体错收、漏收及分母,不只报告一项总通过率。server/src/services/experience/eval-samples.json 的固定样本明确 provenance 为 synthetic_calibration_cases、contains_real_user_data=false;其规则校准结果不能外推为个人库或真实模型质量。

从问答习惯到可追溯复用

开始任务前按实际症状和项目查历史;处理中将命中的原始证据、版本和适用条件提供给助手;结束后检查问题、过程、结果与来源是否闭合。需要更正时保留修改理由和验收记录,现有 server/src/services/experience/reviews.ts 的删除反馈会记录质量原因,不意味着所有编辑已具备完整版本史。

标签、摘要和关系服务不同用途;“标签比摘要更重要”“80 分乘大量覆盖更好”都需要同任务对照数据,不能当普遍结论。数量也不能证明自己测试不足或编码速度翻倍。先固定任务成功标准,对比检索是否找到可用证据、执行是否成功、是否需要人工纠正,再讨论收益。

更完整的决策记录、代码依赖验证、成员权限或跨库检索可以是后续设计目标,必须与当前已有过滤、写回 receipt 和远端导入分开。沿图谱辅助重构把关系线索转成故障检查,沿团队共享边界保留审批和身份责任;系列的收官是可复核的使用方法,不是未经验证的增长预测。

分享:

相关文章

团队知识共享:令牌、审批与实例边界

区分当前云令牌与拟议成员权限,以合成网关规则验证只读操作和整批审批,并明确共享与备份责任。

曾亮懿 曾亮懿
阅读约 5 分钟
#ChatCrystal #Knowledge Management #Security

AI 对话格式差异:事件、快照与跨库消息定位

对照五个固定适配器,区分 JSONL 容器、事件语义与 SQLite 键布局,用两个内存数据库观察消息索引、孤立数据和未知版本。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #Node.js #SQLite

AI 对话管理方案:四种存储路径与迁移验收

比较 Claude Code、Cursor、Copilot、Trae 的固定适配器输入,区分原始副本、可读导出与知识库,并用合成清单验证身份和信息保留。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #Node.js #SQLite

从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录,欢迎交流实现细节。