c670b9e454
- 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解 - 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护) - 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理 - 专业图标体系替换全部emoji、看板美化 - 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC - 444单测+204前端测试+51 e2e
98 lines
3.5 KiB
TypeScript
98 lines
3.5 KiB
TypeScript
/**
|
|
* 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<T extends string>(label: T, confidence: number): ScoredCandidate<T>[] {
|
|
return [{ label, confidence: toConfidence(confidence) }].slice(0, MAX_CANDIDATES);
|
|
}
|
|
|
|
/**
|
|
* 用 LLM 识别业务类型与行业;失败或输出非法时返回 null(交由规则分类器兜底)。
|
|
*/
|
|
export async function llmClassify(
|
|
description: string,
|
|
): Promise<ClassificationResult | null> {
|
|
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<string, unknown>;
|
|
|
|
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,
|
|
};
|
|
}
|