公共采购文档里的控告性语言,最近在不少合规与风险团队里变成了一个绕不开的问题。所谓控告性语言,不是普通差评,也不是情绪化抱怨,而是供应商在质疑招标文件、评标过程或中标结果时使用的正式表述。这类句子往往出现在质疑函、投诉书、澄清函和评审意见中,数量不大,但影响不小。一个人一天能精读几十份投诉材料,却很难在上千份采购文档中把每一句“话里有话”都找出来。
如果只是把文本丢给通用情感分析模型,效果通常不理想。控告性语言的情绪不一定激烈,甚至可能用最克制的措辞表达最严重的指控。要识别它,需要回到语义和语境层面。过去一段时间,我比较多地在关注这类“职责界定型”文本的自动化处理,一个验证下来比较有效的做法,是构建一条级联的无监督-有监督 NLP pipeline:先用无监督方法从大量未标注文本里召回疑似控告片段,再用有监督模型做精确判定。这个设计不是炫技,而是对数据现状的一种务实妥协和优化。
1. 先搞清楚我们要检测的“控告性语言”是什么
1.1 公共采购文本里的控告性语言长什么样
公共采购领域的质疑与投诉,往往不像日常评论那样直白。它有一套相对固定的句式,常见于“质疑函”“投诉书”“澄清说明”以及投标人对评标结果的意见回复。控告性语言通常包含三个要素:对象、行为和诉求。对象是招标文件、评分标准、评审专家、中标人;行为是倾向、不公、串通、弄虚作假;诉求是重新评审、废标或暂停采购。这三个要素不一定同时出现,但至少要有一个明确的“负面指向”。
举例来说,下面这些表述在真实材料中很常见:
- “该项目评分标准明显倾向特定品牌,涉嫌限制潜在投标人参与竞争。”
- “评审专家在答辩环节的打分缺少客观依据,我公司对中标结果提出严重质疑。”
- “中标供应商在投标文件中提供的业绩存在明显夸大,请求核实并重新评审。”
对照之下,普通业务句子是这样:
- “我方已按照招标文件要求提交全部资质材料,完全响应所有技术条款。”
- “本项目开标过程正常,现场秩序良好,未收到有效质疑。”
这两种句子在词表层面会有交集,但语义指向完全不同。控告性语言的本质是“责任归咎”,不是“情绪表达”。它可能不带一个感叹号,也没有辱骂词,但把对象指向一个具体的失当行为。这就是为什么只靠情感极性判断行不通。
1.2 为什么通用情感分析模型在这里会失灵
通用情感分析模型学的是“正面/负面/中性”这个维度。采购文件中的控告句多数是负面,但负面不一定是控告;而真正有效的控告句,措辞往往非常正式,甚至可以被情感模型归为中性。
从工程视角看,这属于典型的“小正样本、高标注成本、强长尾分布”问题。每批采购项目里真正有控告性质的句子可能只占百分之几,而且不同的投诉角度会带来完全不同的异义表达。一个全监督模型如果只靠几百条标注数据,几乎不可能覆盖“串通投标”“指定倾向”“评审不公”“业绩造假”等不同子类型。
所以,最稳妥的处理方式不是一上来就训练一个端到端分类器,而是先回答一个更朴素的问题:在几千份文档里,哪些句子最值得人工细看?这恰好是级联流水线的切入点。
关键认识:控告性语言检测,本质是一个“从大量无标注文书里筛选高风险片段”的召回问题,而不是一个简单的文本分类问题。
2. 级联流水线的核心逻辑:先召回,再精判
2.1 单模型方案为什么不适合这个场景
很多人会想:既然要做文本分类,直接用预训练模型微调不就行了。但在公共采购场景里,这条路会遇到三重阻力。
第一,标注数据太少。大部分招标文件、投标文件、质疑回复都没有现成的“是否控告”标签。合规团队可能能抽出几百条,但不足以训练一个稳定的深度学习分类器。
第二,类别极度不平衡。真正包含控告性语言的句子占全部句子的比例极低。模型在这样的分布下训练,很可能学会“全部判负”,准确率还非常高,却没有任何业务价值。
第三,长尾表达太多。控告可以有无数种绕开关键词的写法。如果只靠有监督模型直接分类,没有足够多样本覆盖,很容易在真实数据上遇到从未见过的句式。
这不是说单模型不可行,而是说在训练样本有限、业务风险又高的时候,单模型的效果不稳定,且难解释。级联方案多了一个无监督召回层,天然给人工审核留出一个“候选集”,降低了漏报风险。
2.2 无监督阶段做什么,有监督阶段做什么
可以把级联理解成一个漏斗。第一层是“筛子”,目标是不漏掉任何可能的控告句,哪怕捞出来的噪声大也没关系。第二层是“放大镜”,在候选集上做精细判断,把不相关的句子剔掉。
无监督阶段可以使用规则、关键词、句子嵌入、聚类、异常检测等方法,运行在全部未标注文本上。它不要求精确判断某句话是不是控告,只要求“找出来让模型觉得可疑的句子”。输出是候选句列表,以及对应的原始文档编号和上下文段落。
有监督阶段则使用人工标注的高质量样本,在无监督召回后的候选集上训练一个分类器。因为候选集已经比全量语料小很多,模型面对的类别分布也相对更均衡。分类器输出一个“控告概率”,再结合阈值决定是交给人工复核,还是直接抽出来生成报告。
两个阶段的关系不是替代,而是互补。无监督阶段贡献覆盖度,有监督阶段贡献精度。这也是级联这个名称真正的含义:先用资源消耗低的方法粗筛,再用资源消耗高的方法细判。
顺带说明一下,这里的 pipeline 不是 CI/CD 里的构建流水线,也不是 Redis 或 Storm 里那种数据管道,而是从原始文本到高风险候选集的完整 NLP 处理链路。很多人一看到 pipeline 会往工程方向联想,但在文本风险筛查这个场景,重点不在于数据怎么流,而在于每一层过滤掉什么、保留什么。
2.3 级联的评估方式和主要指标
级联流水线和单模型不同,不能只看最终的准确率。需要分阶段评估:
- 无监督召回层的目标:在全部人工标注的正样本里,能覆盖多少。这个指标可以叫“候选覆盖率”,因为候选集里不一定全部正确。
- 有监督层的目标:对候选集内部,判断的精确率、召回率和 F1,是否达到可接受水平。
- 整体流水线的目标:最终交付给人工的“高风险队列”里,真正控告句子的比例有多高,同时漏掉的比例有多低。
实际项目里,我更建议把无监督层的“候选覆盖率”卡在高位,比如要求至少覆盖九成以上的人工正样本,然后让有监督层去控制精确率。如果第一层漏了,第二层再准也没用。这个优先级如果搞反,做出来的系统会显得“精准”,但实际漏掉了大量真正的控告。
3. 无监督召回阶段的落地实现
3.1 数据准备与文本切片
无监督召回层的第一步不是建模,而是把文档切成适合判断的语义单元。公共采购文本有大量表格、条款编号和固定格式。如果直接把整篇文档丢给模型,计算量大,噪声也大。
建议按文档结构做分句。常见做法是:先提取正文中的段落,再按句号、分号、换行切分为句子。对表格里的关键字段,比如“质疑内容”“投诉事实”“采购需求”等,可以单独抽出来作为碎片。因为这些字段本身就是语义密度高的区域。
切片之后,每一条记录需要保留三个字段:原始文档 ID、章节位置、句子内容。这一步看起来简单,但会直接影响后面所有环节的稳定性。我遇到过很多项目,模型调了半天,最后发现是分句时把“(一)”和“(二)”两段拼成了一句话,导致后续聚类全部被干扰。
3.2 用规则和关键词建立第一道召回
关键词规则在公共采购场景里依然非常有效。不要因为叫“无监督”就拒绝规则,规则本质上是先验知识。
可以准备两组词表。一组是“行为类提示词”:串通、倾向、不公、虚假、夸大、暗箱、质疑、投诉、违法、违规、排除、限制、没有依据、缺少证据。另一组是“对象类提示词”:招标文件、评分标准、评审专家、中标供应商、评标委员会、采购人。当句子里同时出现行为类词和对象类词时,就进入候选集。
这组规则的召回率通常不错,但精确率很低。比如“为防范评委不公,本项目采用双盲评审”这句话可能同时包含“评委”和“不公”,但并不是控告性语言。所以规则只能作为第一道粗筛,不能直接输出最终结果。
3.3 用句子嵌入和聚类找出“相似但未标注”的控告簇
光靠关键词还不够。很多控告性语言会绕开关键词,比如用“再次期待贵方重新考虑”这种暗示性表达。为了覆盖长尾,可以引入句子嵌入和聚类。
比较稳妥的流程是:
- 对全量句子用句子嵌入模型生成向量。
- 在嵌入向量上用 KMeans 或 HDBSCAN 做聚类,簇的数量可以先放宽松一些。
- 对每个簇取中心向量附近的样本,做一次人工抽样查看。
- 如果一个簇里出现多个疑似控告句,就把整个簇标记为候选召回。
这个流程的价值不是让模型自动判断,而是帮助人快速发现“规则没有定义到的可疑表达”。比如第一批聚类后,你可能会发现有一个簇全是“根据采购法相关条款提出投诉”的句子,这些都可以进候选集。代码层面,常见写法大概是这样:
from sentence_transformers import SentenceTransformer from sklearn.cluster import KMeans sentences = [...] # 分句后的文本列表 model = SentenceTransformer("paraphrase-multilingual-MiniLM-L12-v2") embeddings = model.encode(sentences, show_progress_bar=True) kmeans = KMeans(n_clusters=50, random_state=42) labels = kmeans.fit_predict(embeddings) # 对每个簇抽样查看,决定是否进候选集这里有两个经验要强调:一是嵌入模型最好选择支持中文的多语种模型,不要一上来就加载超大模型拖慢整个无监督阶段;二是聚类簇数不是越大越好。可以先从 50 到 200 之间试,目的不是把句子精确分类,而是把人眼可以浏览的候选规模控制在合理范围内。
3.4 输出候选集和可解释依据
无监督层输出不能只是一个句子列表,至少要包含“为什么召回”的可解释信息。我建议输出四列:句子、句子所属文档、命中的规则词、所属聚类簇编号。后面人工复核或训练有监督模型时,这些信息都很有用。
如果某一批数据特别干净,规则召回加聚类召回可以覆盖绝大多数控告句。但不要高枕无忧。规则和聚类都容易漏掉那些极其隐晦的指涉,比如“该条款的表述方式令人难以理解”可能是在暗示条款设置有倾向。这种边界样本,只有有监督模型加人工复核才能兜住。
4. 有监督精判阶段的工程实现
4.1 标注策略:不要一上来标注全量
有监督阶段最容易犯的错,是让业务人员一次性标注几千条句子。结果是标注质量参差不齐,类别分布依然偏斜。
更好的做法是先把无监督召回后的候选集按聚类簇抽样,每簇抽几十条,加上一部分随机负样本,组成一个几百条的“标注种子集”。业务人员只需要判断“这句话是否构成明确的控告性语言”,不要做多分类,避免主观歧义。
标注时要给出判定标准。我一般建议按三个条件判断:是否有明确指向对象?是否包含负面归因?是否可能影响采购决定?三个条件都成立才算控告句。标注完成后,计算标注者之间的一致性,不一致的句子拿出来讨论并修正。
4.2 模型选择与训练示例
有监督层可以用两条路线。一条是轻量路线:用句子嵌入模型生成向量,再训练逻辑回归或随机森林。另一条是重型路线:直接微调一个预训练语言模型,比如中文的 BERT 类模型。
如果标注样本只有几百条,我更建议先走轻量路线。它对数据量更友好,可解释性也更好。实现也比较直接:
from sklearn.linear_model import LogisticRegression from sklearn.model_selection import cross_val_score # X_train 是句子嵌入矩阵,y_train 是标注标签 clf = LogisticRegression(max_iter=1000) scores = cross_val_score(clf, X_train, y_train, cv=5, scoring="f1") print(scores.mean())如果标注数据超过两千条,再考虑微调预训练模型。一个常见的做法是先加载中文文本分类模型,把标签数量改成二分类,用较小的学习率训练几个 epoch。但要注意,微调预训练模型很容易过拟合,因为数据量不一定够大。每个 epoch 之后都要看验证集的 F1,而不是只盯训练集准确率。
4.3 阈值、校准和人工复核回路
有监督模型输出的不是最终答案,只是概率。阈值的高低直接决定了最终送人工复核的队列大小。
如果业务对漏报非常敏感,比如“疑似串通投诉不能漏”,阈值要设得低一点,比如概率大于 0.3 就算命中。如果业务对误报敏感,比如“不能打扰太多正常供应商”,阈值要设得高一点,比如 0.7 以上。
我还建议给每个预测样本输出概率和模型依据。轻量模型可以输出特征权重较高的句子片段;预训练模型则可以用近似解释方法给出关键词高亮。人工复核时看到这些依据,能更快判断模型是否对。复核结果反馈回标注集,形成下一轮迭代。
注意:人工复核不是流水线的终点。每轮复核结果都应该回流到标注集里,用于重新训练或评估。没有回流闭环,系统用一段时间后就会失去校准。
5. 级联流水线容易踩的坑和排查路径
5.1 从模型加载到 pipeline 创建:依赖问题怎么查
级联流水线涉及多个模型和组件,最容易出现的问题不是算法不准,而是环境不匹配。常见现象包括:加载句子嵌入模型时提示缺少依赖、创建 pipeline 时抛出依赖错误、GPU 版本与库版本不一致等。
遇到这类问题不要急着重装环境。按顺序排查:
- 看报错信息里具体是哪个包被拒绝。
- 检查 Python 版本、pip 包版本和模型所需版本是否兼容。
- 看是否同时装了两套深度学习框架,导致符号冲突。
- 检查模型缓存目录是否有权限,默认下载路径是否被改过。
- 最后才考虑重装虚拟环境,而且建议用 requirements.txt 记录精确版本。
其中模型缓存目录是一个很隐蔽的坑。在服务器上跑 pipeline 时,经常因为当前用户对缓存目录没有写权限,导致模型下载失败,报出来的错误却像网络问题。先把环境变量指向一个有权限的目录,能避开大部分坑。
5.2 数据层面:类别不平衡和长尾表达
有监督模型训练时,如果正负样本比例悬殊,模型很容易退化成“全部判负”。要注意几个信号:训练集准确率很高,但正样本的召回率非常低;或者模型预测正样本时,只有极少数固定句式能触发。
处理办法包括:对负样本做下采样,让正负比例接近 1:2 或 1:3;给正样本在损失函数里增加权重;在候选集内部选择标注样本,利用无监督层的召回信息平衡分布。
还有一类问题很难在训练时发现,就是长尾控告表达。比如“相关条款的设置方式值得商榷”这种句子,在训练集里可能只有一两条,模型学不到。为了减少这类漏判,可以在有监督模型上线后,对预测为低概率但规则命中的句子做定期复盘。复盘出的错漏样本进入下一轮标注。
5.3 结果不稳定:先检查输入、阈值、资源
同一套流水线,在批处理时结果突然变差,通常不是模型坏了,而是输入变了。
常见的排查顺序是:
- 先看输入文本格式:有没有乱码、全半角不统一、拆分错误。
- 再看切片逻辑:句子是否被截断,表格内容是否被漏掉。
- 再看阈值:批量处理时是否误用了另一个环境的阈值。
- 再看资源:GPU 显存不足会导致 pipeline 退到 CPU 上,推理速度和结果都可能变化。
- 最后看模型版本:是不是有人更新了嵌入模型,但没有更新缓存。
这个顺序基本覆盖了绝大多数“看起来像模型问题”的工程问题。我在生产环境里见过太多次,最后都是输入切片或环境变量的问题。
6. 从研究原型到业务可用的长期维护
6.1 批次处理和接口化的平衡
公共采购文档的检测往往不是实时的,更适合批次处理。每天或每周把新增文档跑一遍,输出候选交由人工复核。这种方式可以避开接口化的高并发复杂度,也更容易追溯结果。
如果后续需要嵌入到在线审批流里,再考虑提供一个简单接口:输入文档 ID,输出高风险句列表。但不要一开始就做成在线服务,否则团队会花大量时间处理超时、排队和异常重试,而不是优化检测质量。
6.2 数据漂移与更新机制
公共采购文本的表达方式会随着政策、法规、模板变化而变化。半年前的模型,可能半年后就失效了。建议建立一套轻量更新机制:
- 每季度抽一批新文档,用当前流水线跑一遍,人工标记错误。
- 把新标记数据并入标注集,重新训练有监督模型。
- 定期重新聚类,观察是否出现新的“可疑簇”。
- 版本记录里保存模型版本、阈值、训练数据时间,方便回溯。
这个更新周期不需要很长,但一定要有。否则级联流水线会逐渐变成一套“看起来很智能,实际已经过时”的工具。
6.3 这个流水线究竟适合谁,不适合谁
适合的场景包括:采购监督部门、企业内控团队、公共资源交易中心做风险线索初筛;律所处理大量质疑与投诉文件;研究机构做政策文本分析。适合的团队至少要有一定 NLP 工程能力,并且愿意投入标注和复核人力。
不适合的场景包括:拿它做自动裁决、自动处罚依据、完全无人审核的高风险决策。控告性语言检测只能做到“提示”和“定位”,不能替代人的判断。还有,如果机构连几十条高可用的标注样本都不具备,直接上这套流水线会很吃力。不如先用规则做筛选,积累标注集后再引入无监督召回。
说到底,级联无监督-有监督流水线解决的不是“让机器看懂文字”,而是“让有限的人力可以处理海量文档”。它的价值不在更快,而在于把复杂的语言风险收敛成一条可追溯、可复核、可持续优化的链路。如果你正在处理公共采购中的质疑和投诉文本,不妨从一个小样本开始:先清洗分句,用规则做一个粗召回,再人工标注两百条,用嵌入加分类器跑一轮。跑通之后,你自然会知道下一步该往哪里加工程力。