| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131 |
- '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,
- };
|