'use strict'; const DIMENSION_IDS = Object.freeze([ 'direct_answer', 'judgment_value', 'continuity', 'next_step', 'naturalness', 'non_repetition', ]); const FILLER_PATTERN = /^(?:好的|收到|了解|明白|行|可以)[,,。\s]*(?:我看看|我来处理|这边处理|稍后回复|正在处理|帮您关注)?[。!!\s]*$/; const INTERNAL_PATTERN = /qualityScore|qualityChecks|requiresHuman|fallbackReason|系统提示|内部审核|机器评分/i; const JUDGMENT_PATTERN = /因为|所以|取决于|主要看|关键是|区别在|相比|更适合|优先|不建议|建议|重点|要结合|需要结合|不能只看|暂时不能|目前不确定|还不能确定|不能直接下结论/; const NEXT_STEP_PATTERN = /(?:您|你)(?:先)?(?:发|说|告诉|确认|选)|可以先|先看|先按|先把|接下来|下一步|再比较|再判断|再定|不用急|不着急|有需要再|方便时再|[??]/; const PRESSURE_PATTERN = /马上定|现在就定|赶紧|尽快交|直接签|先交定金|今天必须|错过就没/; const MECHANICAL_PATTERN = /(?:建议由人工(?:专员|顾问).{0,10}(?:复核|核验)){2,}|(?:需要核验|待核验).{0,12}(?:需要核验|待核验)|^(?:根据您提供的信息|感谢您的咨询)/; function normalize(value = '') { return String(value || '').toLowerCase().replace(/[\s,。!?!?,;;::'"“”‘’()()【】\[\]-]+/g, ''); } function roleOf(message = {}) { return message.role || (message.direction === 'outbound' ? 'assistant' : 'user'); } function contentOf(message = {}) { return String(message.content || message.text || '').trim(); } function questionFields(text = '') { const value = String(text || ''); return [ ['budget', /预算|总价|首付|月供/], ['region', /区域|片区|地段|小区|哪里|哪边/], ['roomCount', /几房|几室|户型|房间数/], ['area', /面积|多大|多少平/], ['school', /学校|学区|入学|户籍|划片/], ['schedule', /哪天|什么时候|几点|上午|下午|时段/], ].filter(([, pattern]) => pattern.test(value)).map(([id]) => id); } function makeDimension(score, detail) { return { score: Math.max(0, Math.min(2, Number(score) || 0)), detail: String(detail || '') }; } function scoreDirectAnswer(reply = '') { const content = String(reply || '').trim(); if (!content || FILLER_PATTERN.test(content)) return makeDimension(0, '空回复或空泛承接'); const first = content.replace(/^(?:好的|收到|了解|明白)[,,::\s]*/, '').split(/[。!?!?\n]/)[0]; if (!first || /^(?:我看看|我来处理|这边处理|稍后回复|正在处理)/.test(first)) return makeDimension(0, '首句是处理过程'); if (/[??]$/.test(first) && !/还不能|不确定|要看|取决于/.test(first)) return makeDimension(1, '首句直接进入澄清,但没有先给方向回答'); return makeDimension(2, '首句给出实质回应或诚实边界'); } function scoreJudgmentValue(reply = '') { const content = String(reply || '').trim(); if (!content || FILLER_PATTERN.test(content)) return makeDimension(0, '没有判断信息'); if (JUDGMENT_PATTERN.test(content)) return makeDimension(2, '包含判断、取舍依据或明确边界'); if (/^[^。!?!?]*[??]$/.test(content)) return makeDimension(0, '只有追问,没有判断'); return makeDimension(1, '有回应,但判断依据较弱'); } function scoreContinuity(reply = '', messages = [], inbound = '') { const userHistory = messages.filter(message => roleOf(message) === 'user').map(contentOf).filter(Boolean); if (!userHistory.length) return makeDimension(2, '当前没有需要承接的历史条件'); const knownText = [...userHistory, inbound].join('\n'); const asked = questionFields(reply); const alreadyKnown = questionFields(knownText); if (asked.some(field => alreadyKnown.includes(field)) && /[??]|请问|告诉我|说下/.test(reply)) { return makeDimension(0, '再次采集历史中已有字段'); } const knownNumbers = knownText.match(/\d+(?:\.\d+)?/g) || []; if (knownNumbers.length && knownNumbers.some(number => String(reply).includes(number))) return makeDimension(2, '复用了历史中的明确条件'); if (/按您|您刚|您说|之前|继续保留|先按|记下/.test(reply)) return makeDimension(2, '显式承接上一轮条件'); return makeDimension(1, '未冲突,但对历史条件承接不明显'); } function scoreNextStep(reply = '', plan = {}) { const content = String(reply || '').trim(); if (PRESSURE_PATTERN.test(content)) return makeDimension(0, '下一步对当前阶段施压过强'); const lowPressure = plan.generationStrategy?.signals?.lowPressure; if (lowPressure && /什么时候|哪天|几点/.test(content) && !/不急|方便时|定下来再/.test(content)) { return makeDimension(0, '客户低压力时仍追问推进时间'); } if (NEXT_STEP_PATTERN.test(content)) return makeDimension(2, '下一步明确且与当前阶段兼容'); return makeDimension(1, '完成回答,但下一步不够清晰'); } function scoreNaturalness(reply = '') { const content = String(reply || '').trim(); if (!content || INTERNAL_PATTERN.test(content) || FILLER_PATTERN.test(content)) return makeDimension(0, '存在内部状态、空正文或空泛承接'); if (MECHANICAL_PATTERN.test(content) || content.length > 240) return makeDimension(1, '表达偏机械或过长'); return makeDimension(2, '表达简洁、客户可读'); } function scoreNonRepetition(reply = '', messages = []) { const current = normalize(reply); const previous = messages.filter(message => roleOf(message) === 'assistant').map(message => normalize(contentOf(message))).filter(Boolean); if (previous.some(item => item === current && current.length >= 6)) return makeDimension(0, '逐字复读历史回复'); if (previous.some(item => current.length >= 12 && item.length >= 12 && (item.includes(current) || current.includes(item)))) { return makeDimension(1, '与历史回复存在明显复用'); } return makeDimension(2, '未发现明显复读'); } function scoreProfessionalResponse(input = {}) { const reply = String(input.reply ?? input.content ?? '').trim(); const messages = Array.isArray(input.messages) ? input.messages : []; const plan = input.plan || {}; const dimensions = { direct_answer: scoreDirectAnswer(reply), judgment_value: scoreJudgmentValue(reply), continuity: scoreContinuity(reply, messages, String(input.inboundContent || input.inbound || '')), next_step: scoreNextStep(reply, plan), naturalness: scoreNaturalness(reply), non_repetition: scoreNonRepetition(reply, messages), }; const total = DIMENSION_IDS.reduce((sum, id) => sum + dimensions[id].score, 0); return { version: '1.0', total, max: DIMENSION_IDS.length * 2, dimensions, }; } module.exports = { DIMENSION_IDS, scoreProfessionalResponse, };