Files
memind/scripts/simulate-intent-transaction-layer.mjs
T
john d58dc2a251 feat(wechat): add Intent Transaction Layer with unified task schema
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>
2026-08-24 15:59:17 +08:00

596 lines
21 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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();