
凌晨三点半监控屏上滚进来一条公告标题“XX科技关于收到立案告知书的公告”。如果这条信息等到九点开盘再处理影响的不只是夜班盯盘的人而是几十个交易策略的开关。我在这个系统上前后磨了三个版本体感最深的一句话是自然语言处理在金融实时事件监测和财务快讯生成这个场景里拼的从来不是单个模型在榜单上的分数而是整条链路能不能在几十秒内把“发生了什么”这件事做对、做快、做省。这套链路不神秘但细节很多适合正在做金融NLP、投研数据中台、快讯或预警系统的同行参考。1. 金融文本流的真实体感NLP要处理的到底是个什么东西1.1 金融文本不只是新闻更像是一堆“半结构化的变化记录”进到系统里的数据源通常不是单一维度的“新闻”。我把日常要处理的东西分成了四类监管公告业绩预告、业绩快报、并购重组、回购、减持、质押、诉讼、停牌、澄清公告等。这类文本最大的特点是“半结构化”标题通常有固定前缀正文却经常把事情写得很绕。比如一份增持公告里面可能同时包含“增持主体”“拟增持金额”“实施期限”“资金来源”“是否存在不确定性”等条件真正的结论在最后一两段。财经新闻通讯社和媒体发的快讯。标题通常已经把结论放在最前面但正文里会补大量背景、行业对比、分析师观点。处理这类文本的关键是抓“增量信息”而不是把整篇新闻塞给用户。分析师研报包含目标价、盈利预测、评级调整等。研报的特点是预测性语言多“我们预计”“大概率”“存在下行风险”这类文本不能与事实性公告混在一起做事件判定。投资者关系活动记录表也就是常说的调研纪要、互动易问答。这里信息密度高但往往是碎片化的口语表达数字也经常省略单位。这四类文本的格式差异之大决定了你不可能指望一个通用模型通吃所有任务。最直观的例子是公告里的表格很多PDF公告会内嵌Excel表格转文本时行列会乱掉字段和数值错位而财经新闻几乎没有这种问题但新闻会为了可读性把最重要的结论做成小标题。第一批版本我跑得很难看不是模型选错而是对不同文本源的预处理方式没有分开设计。1.2 三大难点表达变体、数字量级、事件与状态把金融文本真正跑通之后我发现核心难点集中在三个地方这三件事也决定了后面所有模型和规则的设计第一个是表达变体。同一个公司可能叫“中行”“中国银行”“Bank of China”证券代码是601988同一个股东可能在不同公告里写法完全不同“控股股东”“大股东”“实际控制人张某某”“张某某及其一致行动人”。对机器来说这些都是不同的字符串但在金融事件里它们通常是同一个主体。所以第一步不是训练模型而是先把“实体归一”做扎实。没有这一步后面做金额汇总、关联交易识别、股东增减持判断都会乱七八糟。第二个是数字量级。金融文本里最常出现的数字是金额和比例而它们的写法几乎没有任何规范。“8.5亿元”“850,000,000元”“850 million RMB”“人民币八亿五千万”看着是四个写法实际是同一个数。更麻烦的是“万元”和“亿元”混用一家公司增持了“250万元”另一家减持了“0.35亿股”如果系统不做单位归一直接拿原始字符串去做数值比较后面所有排序和阈值判断全是错的。第三个是事件与状态的区别。公告里写“拟减持”和“减持实施完毕”是两回事写“回购进展”和“回购方案”也不是同一类事件更不用说“不构成重组”“可能构成重组”“不排除构成重组”这三种表述语义方向完全不同。这些词都出现在同一句话、同一类公告里如果只做关键词命中系统会源源不断地产出错误快讯。我最终的处理思路是先识别“候选事件”再判断“当前状态是方案、进展、实施、完成还是中止”最后才决定要不要作为新事件推出去。2. 分层选型同一套NLP里装着三种不同难度的任务2.1 第一层基础信息抽取规则和模型都要给金融NLP的第一层任务很基础但很重要从文本里抽出公司名、人名、证券代码、金额、日期、股权比例。这一层我用的是轻量方案主要靠词典加正则配合少量模型实体识别。为什么要这样选因为金融领域的实体高频且规律性强公司简称、证券代码、公告类型都有很强的模式用规则能覆盖大部分情况速度也快跑一条公告只需要几十毫秒。但只有规则不够金融实体还有大量变体。比如“XX环保”与“XX环保科技股份有限公司”指的是同一家公司光靠词典枚举枚举条目会无限膨胀。我的做法是做一个“公司别名表”以证券代码为主键把代码、全称、简称、曾用名、英文名、集团名全部挂在一个实体ID下面。每次识别到某个名称就归一到同一个实体ID。这张表会随着系统运行不断补充新出现的简称可以半自动加入。对于“控股子公司”“参股公司”“第二大股东”这类角色词再叠加依存关系或简单的位置规则去判断主体关系。2.2 第二层事件理解重点在触发词上下文和幅度判定基础实体抽清楚之后真正决定系统价值的是事件理解。一个金融事件通常由“事件类型主体对象方向金额时间状态”组成。比如“控股股东拟减持不超过公司总股本2%的股份”事件类型是“减持”主体是“控股股东”方向是“减少”比例是“2%”状态是“拟/计划中”。我第二层的实现方式是“规则触发词小模型分类”的混合方案。先用触发词库把候选句子捞出来再用一个小型文本分类模型判断事件类型和置信度。触发词库覆盖常用动作词比如“收购”“增持”“减持”“回购”“质押”“解除质押”“中标”“签署合同”“立案”“诉讼”“停牌”“复牌”等。触发词的好处是召回快缺点是精度不行因为“不构成重大资产重组”里也包含“重组”。这时候分类模型再上场看全句语义来判断是真事件还是否定表达。下表是我在实际项目里用到的分工方式任务推荐方案理由命名实体识别词典正则轻量NER金融实体高频规则召回快模型负责泛化变体公司名归一以证券代码为主键的别名表解决简称、曾用名、英文名等多个写法的归属问题事件类型判定触发词候选小模型分类触发词保证召回模型负责区分否定、可能、假设等语义增减持方向与幅度方向词典幅度词库精确控制“增持/减持”“拟/已/完成”等状态快讯生成模板字段填充生成式模型润色模板保证五要素齐全生成式模型只做语言组织这一层最重要的经验是不要指望一个BERT分类模型覆盖几十种事件类型。事件类型之间边界模糊样本不均衡训练一个多标签模型反而很难用。我后来把事件类型拆成两级一级是宏观类型并购、融资、增减持、回购、纠纷、业绩、经营二级是细分动作要约收购、协议转让、竞价增持、大宗减持。一级类型用粗粒度分类二级动作再用规则校正这样系统行为更可控。2.3 第三层内容生成生成式模型只做润色不做发明快讯生成是难度最高的一层也是最容易被误会的层。很多人以为快讯就是“用大模型把公告压缩成两三句话”但实际跑下来会发现生成式模型最擅长的是调语序和改表达最不擅长的是保证金融数字、主体、条件不被篡改。模型舒服地写出一句“XX股份将以12亿元现金收购YY公司全部股权”但原文可能是“不超过12亿元、分期支付、需经股东大会审议通过”。这种偏差在人工复核时会一次性拉低整个系统的可信度。所以我的分层是把快讯拆成“事实槽位文本润色”。事实槽位由前两层抽取出来作为结构化字段传给生成模块生成模块的任务是把这些字段组织成一句通顺、专业、可读的话而不是自己从原文里重新理解和创造。生成式模型在这里承担的是修辞功能不是信息源。这一点在后面讲快讯生成时会展开。3. 实时事件监测主链路从公告接入到事件指纹与置信分级3.1 数据接入与预处理先做干净再做智能实时监测的链路很长第一步反而是最容易被轻视的预处理。公告源经常会给你一份PDF、一个HTML链接、一段从Wind或同类终端导出的XML不同数据源的字段命名和编码都不一样。我最初遇到的真实问题是公告标题里的时间戳有两套一套是公告发布日期一套是董事会决议日期两者相差一天。如果直接用发布时间作为事件时间很多快讯会在第二天被另一条来源误判成“新事件”。预处理阶段我固定做四件事格式统一PDF转文本时保留段落结构表格区域单独标记避免行列串位时间对齐区分公告发布时间、事件发生时间、公告落款时间快讯只认“公告发布时间”作为时效起点单位归一把“万元”“亿元”“人民币/美元”统一转换成基础单位数值行情信息关联给事件打好证券代码标签后顺带关联当时的市值、涨跌幅、行业分类用于后面的影响度打分。单位归一这一步看似简单但坑非常多。我写过一个很小的解析函数专门处理“不超过人民币8亿元”“比去年同期的1.2亿元增加30%”这类带修饰成分的金额描述。只有把金额和比例都解析成标准数值后续的阈值判断才有意义。import re def normalize_amount(text): # 仅示例处理 不超过8亿元 8.5万元 这类常见写法 pattern re.compile(r([\d,](?:\.\d)?)\s*(亿元|万元|元)) m pattern.search(text) if not m: return None val float(m.group(1).replace(,, )) unit m.group(2) if unit 亿元: val * 1e8 elif unit 万元: val * 1e4 return val3.2 事件指纹与增量状态机快讯是“变化”不是“重复内容”做完预处理就进入核心环节判断“这是一条值得快讯的新事件还是旧事件的重复报道”。金融数据源有个很讨厌的特点同一个事件公告先发一遍财经媒体再写两三篇解读互动平台再有人提问一遍。如果每个来源都推一次用户一天会收到几十条内容几乎相同的“新快讯”。我们用的方案是“事件指纹状态机”。每一条解析后的结构化事件会生成一个指纹指纹由几个关键字段拼起来再取哈希公司ID、事件类型、事件状态、时间维度、金额档位。比如同一笔减持计划新闻稿换着措辞报道只要公司、动作类型、对象、金额区间一致指纹就相同系统就知道这不是新事件而是同一事件的另一篇报道。对于需要跟踪进展的事件则用状态机来管理一个事件从“董事会预案”到“股东大会通过”到“实施完成”每次状态变更都算一次增量。增量事件用于推送重复内容直接进缓存。这个思路很像做订阅系统的“事件流”而不是“文本流”。我最开始的版本用标题MD5去重结果同一公司公告标题变化一下就被当成新公告后来改成多字段指纹才基本解决重复推送问题。3.3 置信分级与推送策略不是每条都要立刻打断用户事件识别出来了接下来是推不推、怎么推。如果所有事件都第一时间全量推送系统会在财报季变成刷屏机器用户很快会关闭通知。我把事件分成三档高优先级立案调查、退市风险、重大资产重组、业绩预告由盈转亏、停牌复牌。这类事件要求秒级处理推送到手机端强提醒中优先级大额增减持、回购方案、中标重大合同、股权质押比例异常。这类事件进入快讯流但不做强打扰低优先级常规股权变动、日常关联交易、例行公告。这类事件只写入事件库备查不推送。优先级分值是“事件类型权重金额量级主体市值方向异常度”的加权结果。所谓方向异常度是指与市场预期相反的事件比如业绩预告“由盈转亏”“由亏转盈”触发词命中“转”这个关键状态时分值会显著上调。这样设计的目的很简单让系统在用户最需要快速行动的少数场景里做到极速推送在大量公告场景里做到安静沉淀。4. 财务快讯自动生成五要素填充、模板骨架与防幻觉校验4.1 一条能直接被用的快讯需要先回答五个问题做快讯生成之前我反复问自己一个问题交易员拿到一条快讯到底想看什么答案是主体、事件、对象、金额和时间。一条合格的财务快讯不是“把公告压缩一下”而是把公告里最关键的五个要素提取出来再用最短的句子说清楚。我把这五个要素固定下来要素要回答的问题来源校验方式主体谁发生了这件事公司实体识别结果必须有证券代码事件发生了什么事件分类结果必须命中事件类型枚举对象涉及什么资产/股权/人物宾语抽取结果不能为空否则标注待确认金额/比例涉及多少钱、多少股份数字归一模块必须转成标准数值保留原文本锚点时间上下文何时公告、何时生效日期解析模块区分公告日与事件日看起来简单但实际操作里最难的是“对象”这个要素。一个公告可能是“公司拟收购A公司70%股权、B公司30%股权、同时向C公司增资”涉及多个对象多个金额如果只提取最大金额快讯内容就会失真。我的处理是每个对象生成一个独立的“事件单元”快讯可以多条并列但绝不合并成一个模糊说法。4.2 模板骨架加字段填充加生成式润色具体实现上我采用“三段式”流程。第一步把结构化事件字段填入固定模板骨架。模板负责保证句子顺序和格式比如“【{公司简称}{代码}{事件动作}{对象}】{公告日期}{公司全称}公告拟{动作}{对象}交易对价{金额}本次交易{关联关系}{特别安排}。”这个骨架就算没有生成式模型也能直接输出一条可读的快讯。第二步用生成式模型对模板套出来的句子做润色。润色有两个目的一是让句子更像人写的二是把模板里可能重复的表述合并掉。但润色时必须给模型“约束”只能改写给定字段不能增删数字不能改变动作方向。我在提示词里会让模型把数字和专有名词当作不可更改的占位符任何新增内容都视为违规。第三步对这个快讯打置信度标签。高置信度直接进入快讯库展示低置信度会带上“待核验”标记人工审核时只需要点开原文锚点核对即可。我举个实际操作里的例子。某份公告的核心句子是XX股份600123公告公司拟通过支付现金方式收购YY公司55%股权交易对价不超过8亿元。本次交易不构成关联交易不构成重大资产重组。公司股票自6月18日起停牌。系统解析后生成的字段大致是{ company_code: 600123, event_type: 收购, action: 拟收购, target: YY公司55%股权, amount_min_cny: 0, amount_max_cny: 800000000, relation: 不构成关联交易, restructure: 不构成重大资产重组, stop_trading_date: 6月18日 }套模板加润色后快讯输出为【XX股份拟现金收购YY公司55%股权】XX股份6月17日晚间公告拟以现金收购YY公司55%股权交易对价不超过8亿元。本次交易不构成关联交易亦不构成重大资产重组股票6月18日起停牌。这条快讯里每一个数字都能在原文里找到出处模型没有凭空生成任何新增事实。这是我最看重的地方。4.3 防幻觉数字必须有原文锚点模型只负责连词成句生成式模型最大的风险是“把句子写顺了但把意思改了”。金融快讯场景里这种改变的代价很高。比如原文说“不超过8亿元”模型润色成“以8亿元”虽然只少了“不超过”三个字但语义从“上限”变成了“确定金额”对交易员是完全不同的信息。为了防止这类问题我做了一套约束规则所有金额和比例必须来自抽取模块并且带着原文中的上下文字段“不超过”“不少于”“约”“区间”模型不能自己决定“约等于”所有方向性词汇由事件判定模块输出模型不能从“拟收购”改成“已完成收购”如果生成模型被要求进行短语改写改写结果必须通过一个“数字一致性校验器”——把原文数字和快讯数字逐项比对任何新增或缺失都会让该条快讯回到人工队列。这套约束看起来死板但正是这种死板保证了快讯的可靠性。金融用户对快讯的要求不是“文笔好”而是“可以直接基于它做判断”。宁可句子朴素也不能有一处含糊。5. 生产环境踩坑复盘从漏报和误报端到端打磨系统5.1 案例一否定表达把“不构成重组”识别成了重大重组第一版事件分类器上线一周后我收到一条很典型的漏报反馈某公司公告标题是“关于重大资产重组进展暨公司股票停牌的提示性公告”正文里却写“本次交易预计不构成重大资产重组”。系统把“重大资产重组”这个触发词命中后直接把它归入高优先级事件推给了所有人。几小时后公司又发了一条更清晰的公告把“不构成”说得更直白。用户的不满在于这条快讯把不确定性当成了确定性。这个问题本质上不是实体识别问题而是语义否定问题。修复时做了两件事第一把事件判定的文本单位从单个句子扩大到“以标点切分的语义块”看触发词前后有没有“不”“未”“没有”“可能”“不排除”等修饰词第二针对“是否构成”“可能构成”“不构成”“构成”“不排除构成”这五种情形分别建立子类型前两者一律标记为“待确认”只有明确说“构成”或“不构成”时才进入确定事件流。修复之后类似的误报基本消失但也留下一个教训规则触发词在召回上做得越好在否定语境下的误报风险就越高模型必须和规则互相补位。5.2 案例二同一事件的三篇报道触发了三次快讯另一个印象深刻的是去重不彻底。某家上市公司披露一份重要合同后公告发了一条、财经客户端发了一条、第三方资讯平台又发了一条。因为经过不同的文本解析路径三条记录的标题、正文和发布时间都不完全一样但事件本质是同一件事。系统按标题去重没拦住一晚上推了三遍相似内容。后来我把去重逻辑全面改成事件指纹并且在校验时给“同一事件”设置了生命周期事件创建后在“未变更状态”前任何触达同一指纹的新文本都只追加到原事件的时间线里不再产生新的快讯。只有当事件状态变更比如从“预案”变成“实施完成”才生成新的快讯。这个改动之后重复推送问题大幅下降同时“进展类快讯”的增量更新也变得更清晰。5.3 上线后的评估指标不能只看模型F1很多刚接触这个方向的人喜欢用精确率、召回率、F1来衡量整个系统。但做生产系统的时间一长你会发现模型F1只是过程指标真正要看的是端到端指标。我自己在项目里固定看一张评估表指标测量方式参考目标端到端延迟P95从公告发布时间到快讯进入展示库的时间差小于60秒漏报率人工抽样一周内100条真实事件检查系统是否识别小于1%误报率每日推送事件中人工确认为误报的比例小于10%快讯字段修正率人工审核时修改字段的比例小于5%重复推送率同一件事被推送两次以上的次数占比趋近于0其中“快讯字段修正率”是最容易被忽略的。有一次我优化了生成式模型的润色效果人工给我的反馈反而是“句子更顺了但数字和条件需要改得更多”。这说明模型的文本流畅度和事实准确度确实是两个维度。后来我在每次迭代时都会拉出“人工修改记录”去做归因看看修改最多的是句子还是数字再决定优化方向。5.4 数据漂移与样本回流边用边喂不调参也涨点系统上线之后最大的风险不是模型本身而是数据漂移。金融市场里公告类型五花八门今年常见的“向特定对象发行股票审核问询函回复”明年可能变成“向不特定对象发行可转换公司债券”。新公告类型、新措辞习惯不断出现静态模型很快就会过时。我没有选择定期重训模型这种重方案而是做了一个轻量的样本回流闭环每周固定把“未触发事件”“低置信事件”“人工审核修改过字段的事件”汇总成一袋样本由分析师或运营人员快速标注一组只有几十条但足够让分类模型做增量训练或者让触发词库做增量补充。这样做的好处是系统会越来越适应当前市场的表达习惯而不是停留在上线那一刻的知识水平。投入成本很低但漏报率下降非常明显。每次模型迭代我最先看三个东西漏报清单、误报清单、人工修改记录。这三个比任何评分指标都诚实。当这三个清单在变薄的时候这个系统才算是真正好用了。