十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

NLP 引擎自动检测招标文件缺失参数,5 分钟生成月报:投标业务流自动化的工程实现

NLP 引擎自动检测招标文件缺失参数,5 分钟生成月报:投标业务流自动化的工程实现 用 NLP 引擎自动检测招标文件缺失参数5 分钟生成月报投标业务流自动化的工程实现做投标的人每月有两件烦心事一是招标文件里关键参数经常写不全技术部没法干活销售和技术来回扯皮二是月底要花时间统计工作量、写月报。我们在 OpenCheck 里做了两个引擎来解决这两个问题——NLP 参数探测引擎和月报自动生成引擎。这篇文章讲讲技术实现。NLP 参数探测引擎核心问题招标文件里的技术参数经常缺失。以防护服采购为例面料纱支数、反光带型号、导电纤维占比、内胆材质——采购人经常不写或者写得很含糊。技术部拿到招标文件参数不全没法算成本只能打回来让销售去问。我们需要一个引擎扫完招标文件后自动列出哪些关键参数缺失了。行业规则校验集不同行业需要检查的参数不同。我们把规则按行业组织成规则集每个规则定义了参数名称、提取正则、校验规则和缺失时的提示信息{industry:construction,rules:[{param_name:qualification_level,param_type:required,extraction_pattern:(?:资质等级|资质要求)[:]\\s*(.?)(?:\\n|$),validation_rule:(?:一级|二级|三级|甲级|乙级|丙级),missing_msg:未找到资质等级要求可能导致投标无效},{param_name:bid_bond_amount,param_type:required,extraction_pattern:(?:投标保证金|投标担保)[:]?\\s*(\\d(?:\\.\\d)?)\\s*(?:万元|元),validation_rule:\\d(?:\\.\\d)?,missing_msg:未找到投标保证金金额要求},{param_name:bid_deadline,param_type:required,extraction_pattern:(?:投标截止|递交截止)[:]?\\s*(\\d{4}[-年]\\d{1,2}[-月]\\d{1,2}日?),missing_msg:未找到投标截止时间}]}规则集存在数据库里按行业分类支持版本管理。加载器带缓存同一个行业的规则集只从数据库读一次classRuleSetLoader{privatecache:Mapstring,RuleSetnewMap();asyncload(industry:string):PromiseRuleSet{if(this.cache.has(industry)){returnthis.cache.get(industry)!;}construleSetawaitprisma.industryRuleSet.findFirst({where:{industry,is_active:true},orderBy:{version:desc}});if(!ruleSet){returnthis.loadDefault();// 回退到默认规则集}constparsed:RuleSet{industry:ruleSet.industry,rules:JSON.parse(ruleSet.rules_config),version:ruleSet.version,updated_at:ruleSet.updated_at.toISOString()};this.cache.set(industry,parsed);returnparsed;}}检测流程检测引擎加载规则集后逐条规则在招标文件全文上做正则提取classParameterDetectionEngine{privateruleLoader:RuleSetLoader;asyncdetect(assessment:Assessment):PromiseDetectionResult{construleSetawaitthis.ruleLoader.load(assessment.industry);constpdfChunksawaitthis.parsePDF(assessment.fileUrl);constfullTextpdfChunks.map(cc.text).join(\n);constresults:ParameterCheckResult[][];for(construleofruleSet.rules){constregexnewRegExp(rule.extraction_pattern,g);constmatchesfullText.match(regex);// 参数不存在 → 标记缺失if(!matches||matches.length0){results.push({param_name:rule.param_name,status:missing,message:rule.missing_msg,severity:rule.param_typerequired?critical:warning});continue;}// 参数存在但值不合法 → 标记无效if(rule.validation_rule){constisValidnewRegExp(rule.validation_rule).test(matches[0]);if(!isValid){results.push({param_name:rule.param_name,status:invalid,value:matches[0],message:参数值不符合要求:${matches[0]},severity:critical});continue;}}// 通过检查results.push({param_name:rule.param_name,status:valid,value:matches[0]});}returnthis.generateReport(results);}}检测结果分三种状态valid参数存在且合法、missing参数缺失、invalid参数存在但值不合法。每种状态有不同的处理逻辑。风险分数计算检测报告里包含一个 0-100 的风险分数计算逻辑很简单privatecalculateRiskScore(results:ParameterCheckResult[]):number{letscore100;for(constrofresults){if(r.statusmissingr.severitycritical){score-20;// 必填参数缺失扣 20 分}elseif(r.statusmissingr.severitywarning){score-5;// 选填参数缺失扣 5 分}elseif(r.statusinvalid){score-15;// 参数无效扣 15 分}}returnMath.max(0,score);}分数低于 60 的项目会触发预警提示投标人员重点关注。数据聚合引擎多源数据统一接口投标数据散落在多个地方——数据库里的评估记录、API 返回的统计数据、手动录入的补充信息。聚合引擎的作用是把不同来源的数据统一成同一个格式classBidDataAggregatorimplementsDataAggregator{asyncaggregate(sources:DataSource[]):PromiseAggregatedData{// 并行拉取所有数据源constresultsawaitPromise.allSettled(sources.map(sourcethis.fetchSource(source)));constsuccessfulresults.filter((r):risPromiseFulfilledResultanyr.statusfulfilled).map(rr.value);constmergedthis.mergeData(successful);conststatisticsthis.calculateStatistics(merged);consttrendsthis.generateTrends(merged);return{projects:merged,statistics,trends,metadata:{...}};}}数据合并时有一个需要注意的点同一个项目可能在多个数据源中出现。合并逻辑是以项目 ID 为 key重复的项目保留updated_at最新的那条privatemergeData(datasets:any[]):ProjectSummary[]{constprojectMapnewMapstring,ProjectSummary();for(constdatasetofdatasets){for(constprojectofdataset){constexistingprojectMap.get(project.id);if(existing){projectMap.set(project.id,{...existing,...project,updated_at:Math.max(newDate(existing.updated_at).getTime(),newDate(project.updated_at).getTime())});}else{projectMap.set(project.id,project);}}}returnArray.from(projectMap.values());}实时数据推送前端看板需要实时反映项目状态变化。我们用 WebSocket 做增量推送classRealTimeDataStream{privateclients:SetWebSocketnewSet();subscribe(client:WebSocket):void{this.clients.add(client);client.on(close,()this.clients.delete(client));}broadcast(data:DataUpdate):void{constmessageJSON.stringify({type:data_update,payload:data,timestamp:Date.now()});for(constclientofthis.clients){if(client.readyStateWebSocket.OPEN){client.send(message);}}}// 增量更新检测对比新旧数据只推送变化的部分detectChanges(oldData:any[],newData:any[]):DataUpdate[]{constchanges:DataUpdate[][];for(constnewItemofnewData){constoldItemoldData.find(oo.idnewItem.id);if(!oldItem){changes.push({action:create,data:newItem});}elseif(JSON.stringify(oldItem)!JSON.stringify(newItem)){changes.push({action:update,data:newItem,old:oldItem});}}for(constoldItemofoldData){if(!newData.find(nn.idoldItem.id)){changes.push({action:delete,data:oldItem});}}returnchanges;}}detectChanges做的是简单的新旧对比——新增、更新、删除三种变更类型。用JSON.stringify做深度比较在小数据量下够用数据量大了可以换成结构化 diff。月报自动生成报告生成流程月报生成器做的事情是拉取聚合数据 → 按模板生成各章节 → 渲染图表 → 组装报告 → 存储。classMonthlyReportGenerator{asyncgenerate(month:string):PromiseReport{// 1. 拉取聚合数据constdataawaitthis.dataAggregator.aggregate([{type:database,endpoint:assessments,schema:assessmentSchema},{type:database,endpoint:behavior_logs,schema:behaviorSchema},{type:api,endpoint:/api/stats/monthly,schema:statsSchema}]);// 2. 并行生成各章节constsectionsawaitPromise.all(this.template.sections.map(sectionthis.generateSection(section,data)));// 3. 并行生成图表constchartsawaitPromise.all(this.template.charts.map(chartthis.generateChart(chart,data)));// 4. 组装报告constreport:Report{id:monthly-${month},title:${month}月投标分析报告,generated_at:newDate().toISOString(),sections,charts,summary:this.generateSummary(data)};awaitthis.storeReport(report);returnreport;}}摘要生成逻辑privategenerateSummary(data:AggregatedData):ReportSummary{const{statistics,trends}data;return{total_projects:statistics.total_assessments,success_rate:statistics.win_rate,avg_score:statistics.average_score,key_findings:[本月分析${statistics.total_assessments}个项目,中标率${statistics.win_rate}%,平均风险分数${statistics.average_score},trends[0]?.description||暂无趋势数据],recommendations:this.generateRecommendations(statistics)};}图表渲染图表用 node-canvas 做服务端渲染生成 PNG 图片嵌入报告classChartRenderer{asyncrenderPieChart(config:PieChartConfig):PromiseBuffer{constcanvascreateCanvas(config.width,config.height);constctxcanvas.getContext(2d);consttotalconfig.data.reduce((sum,item)sumitem.value,0);letstartAngle-Math.PI/2;config.data.forEach((item,index){constsliceAngle(item.value/total)*2*Math.PI;ctx.beginPath();ctx.moveTo(config.centerX,config.centerY);ctx.arc(config.centerX,config.centerY,config.radius,startAngle,startAnglesliceAngle);ctx.closePath();ctx.fillStyleitem.color||config.colors[index%config.colors.length];ctx.fill();startAnglesliceAngle;});returncanvas.toBuffer(image/png);}}柱状图的实现类似就是算好每个柱子的 x 坐标和高度用fillRect画出来再标注标签。问题清单自动生成参数探测引擎检测出缺失参数后系统会自动生成一份电话采购人问题清单。清单不只是列出问题还会按严重程度排序并给出建议的处理方式classProblemChecklistGenerator{asyncgenerate(assessment:Assessment):PromiseProblemChecklist{constproblems:Problem[][];// 四类检查并行const[disqRisks,missingParams,formatIssues,complianceIssues]awaitPromise.all([this.checkDisqualificationRisks(assessment),this.checkMissingParameters(assessment),this.checkFormatIssues(assessment),this.checkCompliance(assessment)]);problems.push(...disqRisks,...missingParams,...formatIssues,...complianceIssues);// 按严重程度 影响范围排序constsortedProblemsthis.prioritizeProblems(problems);return{assessment_id:assessment.id,total_problems:sortedProblems.length,critical_count:sortedProblems.filter(pp.severitycritical).length,problems:sortedProblems,generated_at:newDate().toISOString()};}privateprioritizeProblems(problems:Problem[]):Problem[]{constseverityOrder{critical:0,warning:1,info:2};returnproblems.sort((a,b){constseverityDiffseverityOrder[a.severity]-severityOrder[b.severity];if(severityDiff!0)returnseverityDiff;returnb.impact_score-a.impact_score;});}}销售拿到这份清单直接打电话给采购人按清单上的问题逐个确认。不用再自己猜该问什么也不用技术部反复打回。效率对比指标传统人工OpenCheck参数检测时间4 小时3 分钟月报生成时间4 小时/月5 分钟问题清单生成2 小时10 秒错误率5-10%0.1%数据来源防静电工作服项目实测。踩坑记录坑1正则提取的误匹配。招标文件里经常出现投标保证金不低于XX万元这种表述正则会匹配到不低于XX万元而不是具体金额。解法是在正则中加入负向前瞻排除不低于不超过等修饰词。坑2多数据源的项目 ID 冲突。同一个项目在不同系统中 ID 不同。解法是维护一个项目 ID 映射表入库前先做 ID 归一化。坑3图表渲染的中文乱码。node-canvas 默认不支持中文字体渲染出来全是方块。解法是在 Docker 镜像中安装中文字体fonts-wqy-zenhei并在 canvas 创建时指定字体。坑4WebSocket 连接泄漏。客户端异常断开时close事件不触发连接一直留在 Set 里。解法是加心跳检测超过 30 秒无响应主动断开。小结投标业务流自动化的核心是两件事用 NLP 引擎把招标文件里的参数结构化用数据聚合引擎把分散的项目数据统一起来。前者解决了参数缺失导致内部扯皮的问题后者解决了月报耗时的问题。技术上都是常规操作——正则提取、JSONB 规则配置、WebSocket 推送、canvas 图表渲染。难点不在技术本身在于对招投标业务场景的理解哪些参数是必须检查的风险分数怎么算才合理问题清单怎么排优先级。#NLP #招投标 #数据聚合 #自动化 #TypeScript #Node.js #WebSocket #工程实践
返回列表