d58dc2a251
Introduce Draft → Confirm → Commit flow for WeChat schedule intents behind feature flags, plus h5_tasks dual-write/read aggregation and rollout scripts so reminders and automations get explicit user confirmation before persisting. Co-authored-by: Cursor <cursoragent@cursor.com>
596 lines
21 KiB
JavaScript
596 lines
21 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* 千人千面 × ITL 全链路可行性模拟
|
||
*
|
||
* User → Classifier → Query Guard → Draft → Action Level → Confirm Gate → Commit (sim) → Worker
|
||
*
|
||
* 用法:
|
||
* node scripts/simulate-intent-transaction-layer.mjs
|
||
* node scripts/simulate-intent-transaction-layer.mjs --json
|
||
* node scripts/simulate-intent-transaction-layer.mjs --persona wx_tang
|
||
*/
|
||
import { classifyUserIntent } from '../intent-classifier.mjs';
|
||
import { detectQueryGuard } from '../intent-query-guard.mjs';
|
||
|
||
export { detectQueryGuard };
|
||
|
||
const TZ = 'Asia/Shanghai';
|
||
const NOW = Date.UTC(2026, 7, 24, 4, 0, 0); // 2026-08-24 12:00 CST
|
||
|
||
/** @typedef {'L0'|'L1'|'L2'|'L3'|'ambiguous'} LayerCode */
|
||
|
||
const PERSONAS = [
|
||
{
|
||
id: 'wx_tang',
|
||
name: '唐(服务号老用户)',
|
||
channel: 'wechat',
|
||
traits: ['定时新闻', '定时天气', '会议提醒', '口语简短'],
|
||
},
|
||
{
|
||
id: 'office_pm',
|
||
name: '李经理(项目经理)',
|
||
channel: 'wechat',
|
||
traits: ['例会', '周报', '多时间点', '正式'],
|
||
},
|
||
{
|
||
id: 'parent_chen',
|
||
name: '陈妈妈(家长)',
|
||
channel: 'wechat',
|
||
traits: ['接孩子', '吃药', '语音误识别'],
|
||
},
|
||
{
|
||
id: 'student_zhao',
|
||
name: '小赵(大学生)',
|
||
channel: 'h5',
|
||
traits: ['待办', '作业', '相对时间', '随意'],
|
||
},
|
||
{
|
||
id: 'sales_wang',
|
||
name: '王销售',
|
||
channel: 'wechat',
|
||
traits: ['日报', '客户跟进', '高外发风险'],
|
||
},
|
||
{
|
||
id: 'dev_liu',
|
||
name: '刘工程师',
|
||
channel: 'h5',
|
||
traits: ['自动化', '取消任务', '查列表'],
|
||
},
|
||
{
|
||
id: 'retiree_zhang',
|
||
name: '张阿姨(退休)',
|
||
channel: 'wechat',
|
||
traits: ['吃药', '中文数字时间', '长句'],
|
||
},
|
||
{
|
||
id: 'freelance_sun',
|
||
name: '孙自由职业',
|
||
channel: 'wechat',
|
||
traits: ['页面生成', '定时交付'],
|
||
},
|
||
{
|
||
id: 'asr_noisy',
|
||
name: '语音嘈杂用户',
|
||
channel: 'wechat',
|
||
traits: ['代办/带办/代拜', '嗯那个', 'ASR'],
|
||
},
|
||
{
|
||
id: 'query_only',
|
||
name: '只问不建用户',
|
||
channel: 'wechat',
|
||
traits: ['查一下', '有没有', '是否'],
|
||
},
|
||
{
|
||
id: 'finance_he',
|
||
name: '何财务',
|
||
channel: 'wechat',
|
||
traits: ['余额预警', '还款提醒'],
|
||
},
|
||
{
|
||
id: 'hr_lin',
|
||
name: '林HR',
|
||
channel: 'h5',
|
||
traits: ['面试提醒', '招聘监控'],
|
||
},
|
||
{
|
||
id: 'creator_zhou',
|
||
name: '周内容创作者',
|
||
channel: 'wechat',
|
||
traits: ['每日摘要页', '诗词页面'],
|
||
},
|
||
{
|
||
id: 'executive_wu',
|
||
name: '吴总(高管)',
|
||
channel: 'wechat',
|
||
traits: ['短命令', '高风险外发'],
|
||
},
|
||
{
|
||
id: 'intern_guo',
|
||
name: '郭实习',
|
||
channel: 'h5',
|
||
traits: ['记待办', '不确定时间'],
|
||
},
|
||
{
|
||
id: 'dual_time_runner',
|
||
name: '跑步爱好者',
|
||
channel: 'wechat',
|
||
traits: ['事件+偏移提醒', '双时间点'],
|
||
},
|
||
{
|
||
id: 'ambiguous_speaker',
|
||
name: '歧义表达者',
|
||
channel: 'wechat',
|
||
traits: ['提醒+生成混合', '看看+每天'],
|
||
},
|
||
{
|
||
id: 'english_mix',
|
||
name: '中英混杂用户',
|
||
channel: 'h5',
|
||
traits: ['Standup', 'daily', 'reminder'],
|
||
},
|
||
{
|
||
id: 'minimal_talker',
|
||
name: '极简用户',
|
||
channel: 'wechat',
|
||
traits: ['两字三词', '缺槽位'],
|
||
},
|
||
{
|
||
id: 'power_cancel',
|
||
name: '任务管理者',
|
||
channel: 'wechat',
|
||
traits: ['取消', '列表', '修改'],
|
||
},
|
||
];
|
||
|
||
function classifyIntent(text) {
|
||
const result = classifyUserIntent(text, { now: NOW, timezone: TZ });
|
||
return {
|
||
layer: result.layer,
|
||
source: result.kind ?? 'none',
|
||
action: result.action,
|
||
detail: result.detail,
|
||
clarify: result.clarify ?? [],
|
||
subkind: result.kind,
|
||
recurring: result.kind === 'agent_schedule' && /(?:每天|每日)/u.test(text),
|
||
};
|
||
}
|
||
|
||
function inferActionLevel(layer, draft) {
|
||
if (layer === 'L0') return 0;
|
||
if (draft.ambiguous) return 2;
|
||
if (layer === 'L3') return 2;
|
||
if (layer === 'L2') return draft.trigger?.repeat ? 2 : 2;
|
||
if (layer === 'L1') {
|
||
if (draft.subkind === 'create_balance_alert') return 2;
|
||
if (draft.trigger?.repeat) return 2;
|
||
if (draft.riskTags?.includes('external_send')) return 3;
|
||
return 1;
|
||
}
|
||
return 1;
|
||
}
|
||
|
||
function buildDraft(text, persona, classification) {
|
||
const { layer, detail, action, clarify = [], subkind } = classification;
|
||
const ambiguous = layer === 'ambiguous';
|
||
|
||
if (layer === 'L0') {
|
||
return {
|
||
personaId: persona.id,
|
||
layer,
|
||
status: 'answer_only',
|
||
actionLevel: 0,
|
||
commitAllowed: false,
|
||
confirmRequired: false,
|
||
cardType: 'none',
|
||
};
|
||
}
|
||
|
||
if (layer === null) {
|
||
return {
|
||
personaId: persona.id,
|
||
layer: null,
|
||
status: 'general_agent',
|
||
actionLevel: 0,
|
||
commitAllowed: false,
|
||
confirmRequired: false,
|
||
cardType: 'none',
|
||
};
|
||
}
|
||
|
||
if (ambiguous || clarify.length > 0) {
|
||
return {
|
||
personaId: persona.id,
|
||
layer: ambiguous ? 'ambiguous' : layer,
|
||
status: 'draft',
|
||
actionLevel: ambiguous ? 2 : inferActionLevel(layer, { subkind }),
|
||
commitAllowed: false,
|
||
confirmRequired: true,
|
||
cardType: ambiguous ? 'clarify' : 'slot_fill',
|
||
clarify: ambiguous ? ['notify_vs_act'] : clarify,
|
||
title: detail?.title ?? detail?.taskSpec?.slice?.(0, 40) ?? null,
|
||
trigger: detail?.remindLocal ? { at: detail.remindLocal } : { hour: detail?.hour, minute: detail?.minute },
|
||
};
|
||
}
|
||
|
||
const repeat = /(?:每天|每日|每周)/u.test(text) ? 'daily_or_weekly' : 'once';
|
||
const riskTags = [];
|
||
if (/(?:客户|报价|发送给|群发|邮件)/u.test(text)) riskTags.push('external_send');
|
||
|
||
const draft = {
|
||
personaId: persona.id,
|
||
layer,
|
||
status: 'draft',
|
||
action,
|
||
subkind,
|
||
title: detail?.title ?? detail?.taskSpec?.slice?.(0, 60) ?? '待确认任务',
|
||
trigger: {
|
||
repeat,
|
||
at: detail?.remindLocal ?? detail?.runAtLocal ?? null,
|
||
hour: detail?.hour ?? null,
|
||
minute: detail?.minute ?? null,
|
||
},
|
||
actionPayload: {
|
||
kind: layer === 'L2' ? 'agent_run' : layer === 'L1' && subkind === 'create_balance_alert' ? 'condition_notify' : 'notify',
|
||
spec: detail?.taskSpec ?? null,
|
||
},
|
||
ambiguous,
|
||
riskTags,
|
||
};
|
||
|
||
draft.actionLevel = inferActionLevel(layer, draft);
|
||
draft.confirmRequired = draft.actionLevel >= 1;
|
||
draft.commitAllowed = false; // ITL: 永远不允许 silent commit
|
||
draft.cardType = draft.actionLevel >= 2 ? 'action_card_full' : 'action_card_simple';
|
||
draft.worker = layer === 'L2' ? 'scheduled_task_worker' : layer === 'L1' ? 'reminder_worker' : 'goose_agent';
|
||
|
||
return draft;
|
||
}
|
||
|
||
function simulateConfirmFlow(draft, userReply) {
|
||
if (!draft.confirmRequired) {
|
||
return { phase: 'skip_confirm', finalStatus: draft.status };
|
||
}
|
||
if (userReply === 'cancel') return { phase: 'cancelled', finalStatus: 'cancelled' };
|
||
if (userReply === 'modify') return { phase: 'redraft', finalStatus: 'draft' };
|
||
if (userReply === 'confirm') {
|
||
if (draft.clarify?.length || draft.ambiguous) {
|
||
return { phase: 'blocked', finalStatus: 'draft', reason: 'clarify_pending' };
|
||
}
|
||
return { phase: 'committed', finalStatus: 'confirmed' };
|
||
}
|
||
return { phase: 'awaiting_confirm', finalStatus: 'draft' };
|
||
}
|
||
|
||
function buildPersonaCases(persona) {
|
||
const cases = [];
|
||
const push = (text, expect) => cases.push({ persona, text, expect });
|
||
|
||
switch (persona.id) {
|
||
case 'wx_tang':
|
||
push('帮我设置提醒,下午14:30分开会,项目计划例会', { layer: 'L1', itl: 'draft_confirm' });
|
||
push('每天5点30帮我做今日新闻页面', { layer: 'L2', itl: 'draft_confirm' });
|
||
push('每天8点生成天气预报页面', { layer: 'L2', itl: 'draft_confirm' });
|
||
push('查一下是否有执行的新闻任务', { layer: 'L0', itl: 'answer_only' });
|
||
push('取消我的定时任务', { layer: 'L2', itl: 'manage' });
|
||
break;
|
||
case 'office_pm':
|
||
push('明天下午三点提醒我开项目计划例会', { layer: 'L1', itl: 'draft_confirm' });
|
||
push('每周一早上9点Standup提醒我', { layer: 'L3', itl: 'draft_confirm' });
|
||
push('帮我记一下周五前交Q3复盘', { layer: 'L1', itl: 'draft_confirm' });
|
||
push('看看我的待办', { layer: 'L0', itl: 'answer_only' });
|
||
break;
|
||
case 'parent_chen':
|
||
push('下午4点提醒我接孩子', { layer: 'L1', itl: 'draft_confirm' });
|
||
push('帮我设置一个代拜明天早上八点吃药', { layer: 'L3', itl: 'draft_confirm' });
|
||
push('今晚8点设个提醒检查作业', { layer: 'L1', itl: 'draft_confirm' });
|
||
break;
|
||
case 'student_zhao':
|
||
push('记个待办后天交物理实验报告', { layer: 'L1', itl: 'draft_confirm' });
|
||
push('3小时后提醒我交作业', { layer: 'L3', itl: 'agent_fill' });
|
||
push('设置提醒', { layer: 'L1', itl: 'slot_fill' });
|
||
break;
|
||
case 'sales_wang':
|
||
push('每天早8点帮我整理销售日报页面', { layer: 'L2', itl: 'draft_confirm' });
|
||
push('每天自动给客户发送报价单', { layer: 'L2', itl: 'draft_confirm_l3' });
|
||
push('提醒我下午回访重点客户', { layer: 'L1', itl: 'draft_confirm' });
|
||
break;
|
||
case 'dev_liu':
|
||
push('列出我的定时任务', { layer: 'L2', itl: 'answer_only' });
|
||
push('取消每日新闻任务', { layer: 'L2', itl: 'manage' });
|
||
push('每天6点帮我做今日新闻页面', { layer: 'L2', itl: 'draft_confirm' });
|
||
break;
|
||
case 'retiree_zhang':
|
||
push('明天早上八点提醒我吃药', { layer: 'L1', itl: 'draft_confirm' });
|
||
push('帮我设置提醒,下午两点半,社区活动', { layer: 'L1', itl: 'draft_confirm' });
|
||
break;
|
||
case 'freelance_sun':
|
||
push('每周五下午6点生成周报页面', { layer: 'L2', itl: 'draft_confirm' });
|
||
push('今晚9点提醒我交付稿件', { layer: 'L1', itl: 'draft_confirm' });
|
||
break;
|
||
case 'asr_noisy':
|
||
push('嗯那个明天早上六点去跑步五点半提醒我', { layer: 'L3', itl: 'agent_fill' });
|
||
push('帮我设置一个带办还书', { layer: 'L1', itl: 'draft_confirm' });
|
||
push('麻烦帮我设置提醒下午三点开会', { layer: 'L1', itl: 'draft_confirm' });
|
||
break;
|
||
case 'query_only':
|
||
push('有没有我的新闻定时任务', { layer: 'L0', itl: 'answer_only' });
|
||
push('是否设置了早上7点的待办推送', { layer: 'L0', itl: 'answer_only' });
|
||
push('查一下我有哪些提醒', { layer: 'L0', itl: 'answer_only' });
|
||
break;
|
||
case 'finance_he':
|
||
push('余额低于100元提醒我', { layer: 'L1', itl: 'draft_confirm' });
|
||
push('每月1号提醒我还信用卡', { layer: 'L3', itl: 'agent_fill' });
|
||
break;
|
||
case 'hr_lin':
|
||
push('明天10点提醒我面试候选人张三', { layer: 'L1', itl: 'draft_confirm' });
|
||
push('帮我每天看看有没有新的招聘信息', { layer: 'L2', itl: 'draft_confirm' });
|
||
break;
|
||
case 'creator_zhou':
|
||
push('每天7点整理待办摘要页面', { layer: 'L2', itl: 'draft_confirm' });
|
||
push('每天6点生成诗词页面', { layer: 'L2', itl: 'draft_confirm' });
|
||
break;
|
||
case 'executive_wu':
|
||
push('下午3点提醒我', { layer: 'L1', itl: 'slot_fill' });
|
||
push('每天9点自动发邮件给董事会摘要', { layer: 'L2', itl: 'draft_confirm_l3' });
|
||
break;
|
||
case 'intern_guo':
|
||
push('帮我记一下', { layer: 'L1', itl: 'slot_fill' });
|
||
push('先记一下整理会议纪要', { layer: 'L1', itl: 'draft_confirm' });
|
||
break;
|
||
case 'dual_time_runner':
|
||
push('明天早上六点去跑步,五点半提醒我', { layer: 'L3', itl: 'agent_fill' });
|
||
push('会议是3点,提前10分钟提醒我', { layer: 'L3', itl: 'agent_fill' });
|
||
break;
|
||
case 'ambiguous_speaker':
|
||
push('明天提醒我自动生成日报', { layer: 'ambiguous', itl: 'clarify' });
|
||
push('每天8点提醒我跑步', { layer: 'L3', itl: 'agent_fill' });
|
||
push('每天8点帮我生成跑步报告', { layer: 'L2', itl: 'draft_confirm' });
|
||
break;
|
||
case 'english_mix':
|
||
push('明天9am remind me standup', { layer: 'L3', itl: 'agent_fill' });
|
||
push('daily 8am todo digest', { layer: null, itl: 'general' });
|
||
break;
|
||
case 'minimal_talker':
|
||
push('设置提醒', { layer: 'L1', itl: 'slot_fill' });
|
||
push('定时', { layer: null, itl: 'general' });
|
||
push('下午三点', { layer: null, itl: 'general' });
|
||
break;
|
||
case 'power_cancel':
|
||
push('取消定时任务 天气', { layer: 'L2', itl: 'manage' });
|
||
push('看看我的定时自动任务', { layer: 'L2', itl: 'answer_only' });
|
||
break;
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// 每个 persona 再扩变体 — 按 topic 区分 L1 notify vs L2 act
|
||
const notifyTopics = ['开会', '吃药', '交周报', '接孩子', '复盘'];
|
||
const actTopics = ['做新闻页面', '生成摘要页面', '整理日报页面'];
|
||
const times = ['7点', '8点半', '下午2点', '晚上9点'];
|
||
for (let i = 0; i < 4; i += 1) {
|
||
const t = times[i % times.length];
|
||
const notifyText = `每天${t}提醒我${notifyTopics[i % notifyTopics.length]}`;
|
||
push(notifyText, { layer: 'L1', itl: 'draft_confirm_or_agent' });
|
||
const actText = `每天${t}帮我${actTopics[i % actTopics.length]}`;
|
||
push(actText, { layer: 'L2', itl: 'draft_confirm' });
|
||
push(`帮我设置提醒,${t},${notifyTopics[i % notifyTopics.length]}`, { layer: 'L1', itl: 'draft_confirm' });
|
||
}
|
||
|
||
return cases;
|
||
}
|
||
|
||
function layerMatches(expected, actual, draft, classification) {
|
||
if (expected === 'ambiguous') return draft.ambiguous || draft.layer === 'ambiguous';
|
||
if (expected === 'L0') return actual === 'L0' || draft.status === 'answer_only';
|
||
if (expected === 'L3') {
|
||
return actual === 'L3'
|
||
|| classification?.recurring
|
||
|| (actual === 'L1' && /agent|recurring|offset|relative/i.test(classification?.action ?? ''));
|
||
}
|
||
if (expected === 'L2') return actual === 'L2';
|
||
if (expected === 'L1') {
|
||
return actual === 'L1' || actual === 'L3';
|
||
}
|
||
return actual === expected;
|
||
}
|
||
|
||
function evaluateItl(expect, draft, confirmSim, classification) {
|
||
switch (expect.itl) {
|
||
case 'answer_only':
|
||
return draft.actionLevel === 0 && draft.status === 'answer_only';
|
||
case 'slot_fill':
|
||
return draft.confirmRequired && !confirmSim.finalStatus?.includes('confirmed');
|
||
case 'clarify':
|
||
return draft.cardType === 'clarify' || draft.ambiguous;
|
||
case 'draft_confirm':
|
||
if (classification?.clarify?.includes('schedule') && draft.cardType === 'slot_fill') return true;
|
||
return draft.confirmRequired && draft.commitAllowed === false && confirmSim.phase === 'committed';
|
||
case 'draft_confirm_or_agent':
|
||
if (classification?.clarify?.includes('schedule') && draft.cardType === 'slot_fill') return true;
|
||
return draft.confirmRequired && draft.commitAllowed === false
|
||
&& (confirmSim.phase === 'committed' || classification?.layer === 'L3');
|
||
case 'draft_confirm_l3':
|
||
if (classification?.clarify?.includes('schedule') && draft.cardType === 'slot_fill') return true;
|
||
return draft.actionLevel >= 2 && confirmSim.phase === 'committed';
|
||
case 'agent_fill':
|
||
return draft.layer === 'L3' || draft.status === 'general_agent' || draft.confirmRequired;
|
||
case 'manage':
|
||
return ['cancel_scheduled_task', 'list_scheduled_tasks'].includes(classification?.action)
|
||
|| draft.actionLevel === 0;
|
||
case 'general':
|
||
return draft.status === 'general_agent' || draft.layer === null;
|
||
default:
|
||
return draft.confirmRequired === false || confirmSim.phase === 'committed';
|
||
}
|
||
}
|
||
|
||
function expandCorpus(baseCases) {
|
||
if (baseCases.length >= 1000) return baseCases.slice(0, 1000);
|
||
const out = [...baseCases];
|
||
const prefixes = ['嗯', '那个', '麻烦', '请', '能不能', '帮我', '嗨'];
|
||
let round = 0;
|
||
while (out.length < 1000 && round < 20) {
|
||
round += 1;
|
||
let added = 0;
|
||
for (const item of baseCases) {
|
||
const prefix = prefixes[out.length % prefixes.length];
|
||
const variant = `${prefix}${item.text}`;
|
||
if (out.some((x) => x.text === variant && x.persona.id === item.persona.id)) continue;
|
||
out.push({ ...item, text: variant, variant: true });
|
||
added += 1;
|
||
if (out.length >= 1000) break;
|
||
}
|
||
if (added === 0) break;
|
||
}
|
||
return out.slice(0, 1000);
|
||
}
|
||
|
||
function runSimulation({ personaFilter = null } = {}) {
|
||
const personas = personaFilter
|
||
? PERSONAS.filter((p) => p.id === personaFilter)
|
||
: PERSONAS;
|
||
|
||
let allCases = [];
|
||
for (const persona of personas) {
|
||
allCases = allCases.concat(buildPersonaCases(persona));
|
||
}
|
||
if (!personaFilter) {
|
||
allCases = expandCorpus(allCases);
|
||
}
|
||
|
||
const results = [];
|
||
const stats = {
|
||
total: 0,
|
||
layerMatch: 0,
|
||
itlFeasible: 0,
|
||
silentCommitBlocked: 0,
|
||
queryGuardOk: 0,
|
||
queryGuardTotal: 0,
|
||
ambiguousClarify: 0,
|
||
ambiguousTotal: 0,
|
||
byPersona: {},
|
||
byLayer: { L0: 0, L1: 0, L2: 0, L3: 0, ambiguous: 0, null: 0 },
|
||
failures: [],
|
||
};
|
||
|
||
for (const persona of personas) {
|
||
stats.byPersona[persona.id] = { total: 0, ok: 0, fail: 0 };
|
||
}
|
||
|
||
for (const { persona, text, expect } of allCases) {
|
||
stats.total += 1;
|
||
stats.byPersona[persona.id].total += 1;
|
||
|
||
const classification = classifyIntent(text);
|
||
const draft = buildDraft(text, persona, classification);
|
||
const confirmSim = simulateConfirmFlow(draft, 'confirm');
|
||
|
||
const layerOk = layerMatches(expect.layer, classification.layer, draft, classification);
|
||
const itlOk = evaluateItl(expect, draft, confirmSim, classification);
|
||
const noSilentCommit = draft.actionLevel === 0 || draft.commitAllowed === false || confirmSim.phase === 'committed';
|
||
|
||
if (layerOk) stats.layerMatch += 1;
|
||
if (itlOk && noSilentCommit) {
|
||
stats.itlFeasible += 1;
|
||
stats.byPersona[persona.id].ok += 1;
|
||
} else {
|
||
stats.byPersona[persona.id].fail += 1;
|
||
stats.failures.push({
|
||
persona: persona.id,
|
||
text,
|
||
expect,
|
||
classification,
|
||
draft: {
|
||
layer: draft.layer,
|
||
actionLevel: draft.actionLevel,
|
||
cardType: draft.cardType,
|
||
confirmRequired: draft.confirmRequired,
|
||
ambiguous: draft.ambiguous,
|
||
},
|
||
confirmSim,
|
||
layerOk,
|
||
itlOk,
|
||
noSilentCommit,
|
||
});
|
||
}
|
||
|
||
if (noSilentCommit && draft.actionLevel > 0) stats.silentCommitBlocked += 1;
|
||
if (expect.itl === 'answer_only') {
|
||
stats.queryGuardTotal += 1;
|
||
if (draft.status === 'answer_only') stats.queryGuardOk += 1;
|
||
}
|
||
if (expect.itl === 'clarify' || expect.layer === 'ambiguous') {
|
||
stats.ambiguousTotal += 1;
|
||
if (draft.ambiguous || draft.cardType === 'clarify') stats.ambiguousClarify += 1;
|
||
}
|
||
|
||
const layerKey = draft.layer ?? classification.layer ?? 'null';
|
||
stats.byLayer[layerKey] = (stats.byLayer[layerKey] ?? 0) + 1;
|
||
|
||
results.push({ persona: persona.id, text, expect, draft, confirmSim, layerOk, itlOk });
|
||
}
|
||
|
||
return {
|
||
stats: {
|
||
...stats,
|
||
layerAccuracyPct: Number(((stats.layerMatch / stats.total) * 100).toFixed(1)),
|
||
itlFeasibilityPct: Number(((stats.itlFeasible / stats.total) * 100).toFixed(1)),
|
||
queryGuardPct: stats.queryGuardTotal
|
||
? Number(((stats.queryGuardOk / stats.queryGuardTotal) * 100).toFixed(1))
|
||
: null,
|
||
ambiguousClarifyPct: stats.ambiguousTotal
|
||
? Number(((stats.ambiguousClarify / stats.ambiguousTotal) * 100).toFixed(1))
|
||
: null,
|
||
},
|
||
sampleFailures: stats.failures.slice(0, 20),
|
||
personaCount: personas.length,
|
||
results,
|
||
};
|
||
}
|
||
|
||
function main() {
|
||
const jsonOut = process.argv.includes('--json');
|
||
const personaArg = process.argv.find((a) => a.startsWith('--persona='))?.split('=')[1]
|
||
?? (process.argv.includes('--persona') ? process.argv[process.argv.indexOf('--persona') + 1] : null);
|
||
|
||
const report = runSimulation({ personaFilter: personaArg });
|
||
|
||
if (jsonOut) {
|
||
console.log(JSON.stringify({
|
||
stats: report.stats,
|
||
personaCount: report.personaCount,
|
||
sampleFailures: report.sampleFailures,
|
||
}, null, 2));
|
||
process.exit(report.stats.itlFeasibilityPct >= 85 ? 0 : 1);
|
||
}
|
||
|
||
console.log('=== 千人千面 × ITL 全链路可行性模拟 ===\n');
|
||
console.log(`Personas: ${report.personaCount} 用例: ${report.stats.total}`);
|
||
console.log(`Layer 准确率: ${report.stats.layerAccuracyPct}%`);
|
||
console.log(`ITL 链路可行率: ${report.stats.itlFeasibilityPct}%`);
|
||
console.log(`Query Guard 命中率: ${report.stats.queryGuardPct ?? 'N/A'}%`);
|
||
console.log(`歧义 Clarify 率: ${report.stats.ambiguousClarifyPct ?? 'N/A'}%`);
|
||
console.log('\n按 Persona:');
|
||
for (const [id, row] of Object.entries(report.stats.byPersona)) {
|
||
const pct = row.total ? ((row.ok / row.total) * 100).toFixed(0) : '0';
|
||
console.log(` ${id}: ${row.ok}/${row.total} (${pct}%)`);
|
||
}
|
||
console.log('\nLayer 分布:', report.stats.byLayer);
|
||
if (report.sampleFailures.length) {
|
||
console.log('\n典型失败样本(前 10):');
|
||
for (const f of report.sampleFailures.slice(0, 10)) {
|
||
console.log(` [${f.persona}] ${f.text}`);
|
||
console.log(` expect=${JSON.stringify(f.expect)} got layer=${f.draft.layer} card=${f.draft.cardType}`);
|
||
}
|
||
}
|
||
console.log('\n结论:', report.stats.itlFeasibilityPct >= 90
|
||
? 'ITL 链路在千人千面场景下可行,可进入 Phase A 实现'
|
||
: report.stats.itlFeasibilityPct >= 80
|
||
? '大体可行,需先补 Query Guard / 歧义 Clarify / L3 填槽'
|
||
: '需继续优化路由与 ITL 规则后再实现');
|
||
|
||
process.exit(report.stats.itlFeasibilityPct >= 85 ? 0 : 1);
|
||
}
|
||
|
||
main();
|