#!/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();