外包风险评估系统:领域引擎+前端+服务端持久化与生产部署
- 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解 - 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护) - 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理 - 专业图标体系替换全部emoji、看板美化 - 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC - 444单测+204前端测试+51 e2e
This commit is contained in:
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user