AI 辅助的字段抽取与数据质量闸门
一个抓来的教师岗位页面不是干净的数据。标题里带着装饰性垃圾字符,薪资有十几种写法,雇主名字是伪装成学校的中介,城市则以拼音出现、或裹在中文括号里。解析器的答案是一套两段式抽取——先把页面结构能交出来的东西取走,再把其余的交给一个语言模型——之后跟着一叠闸门,它们默认模型在撒谎,直到每个字段自己证明并非如此。本节讲的就是这条管线,以及烤进其中的那份不信任。
先结构,后模型
对每一条帖子,解析器都从 DOM 开始。它用 Cheerio 读取标题、位置与雇主那一行、描述段落,以及任何能用正则匹配到的薪资——并在来源已知时盖上一个已知默认值(一个只面向中国的岗位板,在其余一切运行之前先被标为 China)。只有当结构化这一趟不够用时,模型才登场,而它登场的方式有两种截然不同。
当 DOM 解析既产出了标题也产出了描述、但其余字段为空时,模型被要求从描述文本里补齐这些缺口。当 DOM 解析彻底失败——没有标题或没有描述——时,另一条回退路径要求模型直接读被剥净的页面文本,甚至连标题都靠推断得出。无论哪种方式,合并时都是 DOM 得来的值胜出:模型只填洞,不覆盖页面已明白写出的东西。而当再次抓取的描述与已存的相比没有变化时,模型调用被整个跳过——没有新东西可抽,于是解析器把那趟便宜的结构化结果送过最后的闸门就停下。
一种提示形状,各平台各自的内容
提示词不是每个站点手写的。一个小小的构造器接收一份各平台的配置——一句 intro、一组字段(每个带一句大白话描述和一个可选的类型提示)、一组关键规则、以及若干额外指示——把它们拼成单条提示:intro、(做了长度上限的)岗位描述、字段清单、规则,以及末尾一句「只返回一个 JSON 对象」的要求。类型提示会被翻译成具体指示——数字字段要求只给数字,数组字段要求给一个字符串的 JSON 数组、没有就省略。规则与指示正是平台领域知识的所在:把非英文内容翻译成英文同时保持事实含义,缺失的字段应省略而非编造,school 字段只返回学校名、若雇主是中介或代理则留空。
模型本身是一次 DeepSeek chat-completions 调用,以温度零驱动,并要求返回 JSON 对象,使输出确定、可解析。它的 API key 从环境配置里读取,调用被包在一个有上限的、指数退避的重试里,于是一次瞬时失败会先重试几次,解析器才放弃、返回结构化那趟找到的东西。
不信任那个答案
模型输出径直进入一个规范化器,而它的全部职责就是不信这份输出。一组共享的占位字符串——「not specified」「n/a」「none」「unknown」及其同类——被当作「缺失」:任何等于其中之一的字段都被丢弃,不予存储。文本字段被裁剪,并经过一步去重,抓出模型把一个短语前后连着回显两遍的情况。列表字段(requirements、benefits)的项目符号标点被剥掉,占位条目被移除,过长的条目被截断。货币代码统一大写,RMB 折叠为 CNY;周期转小写;薪资数字被缩减为纯数字。
这里最锋利的闸门,是拿薪资去和源文本做的合理性核对。模型返回的一个薪资数字,只有当那串数字确实出现在描述的某处时才被保留——既按纯数字查,也按带千位逗号的变体查。若最小值与最大值都在源文本里找不到,整块薪资就作为幻觉被丢弃。原则很直白:一个模型产出的、却不在它所读文本任何地方的数字,不是数据,是编造。
是学校还是中介
雇主身份有它自己的一套启发式,因为这个区分很要紧、而来源又极少明说。一个候选学校名会被拒,如果它含有任何中介/代理词汇(recruitment、agency、consulting、placement、staffing、公司后缀等等),如果它太长,如果它读起来像一句话或一段营销文案而不像一个名字,或者如果它带有中文标点或非 ASCII 字符。它只有在同时含有一个真正的机构关键词——school、academy、college、university、kindergarten、campus、institute——时才被接受。通过的名字随后被清洗:重复短语被合并,括注被裁剪,像「international school」或「training center」这样的通用标签因太笼统、当不成一个真名字而被拒。模型自己抽出的公司名会被并进 recruiter 字段然后丢弃,这样一个雇主字符串不会伪装成两条事实。
最后的闸门
在 DOM 与模型数据合并之后,一切在离开解析器之前还要过最后一个校验器。城市名被剥掉中文括注和垃圾后缀,从拼音写法(wu han shi → Wuhan)映射到它们的英文形式,当文本是另一种文字时折叠到一个代表性城市,当它指的是一个模糊的多城市区域时干脆被拒。标题被清掉装饰性垃圾字符——方块元素、箭头、星星、心形、emoji——以及残存的 HTML 实体,然后做长度上限。国家名对着一份 ISO 名称映射来解析,认不出时被丢弃,而不是原样放行。
薪资在这一阶段有第二次、独立的合理性核对:非正或非数值的边界被清除,最小值大于最大值时被交换,货币与周期被重新规范化,一个大到不可能是月薪的月度 CNY 数字被重新解读为年薪。收尾的一趟扫描丢掉任何仍持有占位值的字段。抵达调用方的东西,已经过模型一遍、过闸门好几遍。
底下的立场
有一个姿态贯穿始终:页面结构可信,一份已知词汇表可信,但模型的自由文本答案只在能对着源文本或一份固定清单核对时才可信——而在无法核对之处,字段被丢弃而非被猜测。 薪资数字必须出现在产生它的文本里。学校名必须通过中介过滤、并含有一个机构词。城市、国家、货币、标题必须能被归约成一个已知形式。模型说的没有一样是凭信任存下的;每个字段都靠熬过一道闸门来挣得自己的位置。