外包风险评估系统:领域引擎+前端+服务端持久化与生产部署

- 确定性领域引擎(分类/评分/分级/红线/费用/裁决)+LLM(通义千问)语言理解
- 6步评估向导、服务端草稿持久化(跨设备/编辑草稿保护)
- 工作流(草稿→风控→管理层)、RBAC、报告导出、校准、客户/费率/红线/最低工资管理
- 专业图标体系替换全部emoji、看板美化
- 生产化:API_BASE可配置(同源反代)、auth密钥惰性读取修复RBAC
- 444单测+204前端测试+51 e2e
This commit is contained in:
freedakgmail
2026-06-13 01:06:39 +08:00
commit c670b9e454
404 changed files with 61820 additions and 0 deletions
+97
View File
@@ -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,
};
}