/** * LLM 业务类型/行业识别。 * * 用 LLM 替代关键词匹配做自由文本理解,但输出经严格 schema 校验后才采用: * - businessType 必须是五类合法业务类型之一; * - 置信度夹取到 [0,1] 两位小数; * - 按与规则分类器一致的阈值(0.6)推导 needsConfirm 标志与候选列表。 * * 任意失败(未配置 / 调用异常 / 输出非法)返回 null,调用方据此回退到规则 `classify`。 */ import { isBusinessType } from '../classifier/index.js'; import { CONFIRMATION_CONFIDENCE_THRESHOLD, MAX_CANDIDATES, toConfidence, type ClassificationResult, type ScoredCandidate, } from '../classifier/index.js'; import { BUSINESS_TYPE_VALUES, INDUSTRY_UNRECOGNIZED, type BusinessType, type Industry, } from '../domain/common.js'; import { chatJSON } from './client.js'; const SYSTEM_PROMPT = [ '你是外包项目风险评估系统的业务分类器。', '根据项目描述判断「业务类型」与「所属行业」,并给出 0-1 的置信度。', `业务类型必须从以下五类中选择其一:${BUSINESS_TYPE_VALUES.join('、')}。`, '行业用简短中文词(如 制造业、信息技术、金融业、物流业、零售业、客服服务 等);无法判断时填 "未识别"。', '只输出 JSON,结构为:', '{"businessType":"<五类之一>","businessTypeConfidence":0.0,"industry":"<行业或未识别>","industryConfidence":0.0}', ].join('\n'); /** 从 LLM 原始输出构造候选列表(仅含被选中项,保证至少一项且 ≤ MAX_CANDIDATES)。 */ function singleCandidate(label: T, confidence: number): ScoredCandidate[] { return [{ label, confidence: toConfidence(confidence) }].slice(0, MAX_CANDIDATES); } /** * 用 LLM 识别业务类型与行业;失败或输出非法时返回 null(交由规则分类器兜底)。 */ export async function llmClassify( description: string, ): Promise { let raw: unknown; try { raw = await chatJSON([ { role: 'system', content: SYSTEM_PROMPT }, { role: 'user', content: `项目描述:\n${description}` }, ]); } catch { return null; } if (typeof raw !== 'object' || raw === null) { return null; } const obj = raw as Record; const businessTypeRaw = obj.businessType; if (typeof businessTypeRaw !== 'string' || !isBusinessType(businessTypeRaw)) { return null; } const businessType: BusinessType = businessTypeRaw; const businessTypeConfidence = toConfidence(Number(obj.businessTypeConfidence ?? 0)); const industryRaw = typeof obj.industry === 'string' ? obj.industry.trim() : ''; const industryDeterminable = industryRaw !== '' && industryRaw !== INDUSTRY_UNRECOGNIZED; const industry: Industry = industryDeterminable ? industryRaw : INDUSTRY_UNRECOGNIZED; const industryConfidence = industryDeterminable ? toConfidence(Number(obj.industryConfidence ?? 0)) : 0; const needsBusinessTypeConfirm = businessTypeConfidence < CONFIRMATION_CONFIDENCE_THRESHOLD; const needsIndustryConfirm = industryDeterminable && industryConfidence < CONFIRMATION_CONFIDENCE_THRESHOLD; return { businessType, businessTypeConfidence, businessTypeCandidates: needsBusinessTypeConfirm ? singleCandidate(businessType, businessTypeConfidence) : [], needsBusinessTypeConfirm, industry, industryConfidence, industryCandidates: needsIndustryConfirm ? singleCandidate(industry, industryConfidence) : [], needsIndustryConfirm, }; }