第 8 章:ML Feature Extraction 与 Structured Data Extraction——把文本变特征与结构化数据
第 8 章:ML Feature Extraction 与 Structured Data Extraction——把文本变特征与结构化数据
十种形态的最后一组,处理的是"从非结构化到结构化"这件事。区别只在产出去向:特征喂给下游模型,结构化记录喂给人或程序。本章讲清特征怎么组、schema 怎么设计、缺失值怎么处理,以及抽取为什么常常要"先定位候选、再逐条分类"。
两者的区别:喂模型 vs 喂程序
这两种形态经常被混为一谈,因为它们的输入都是非结构化的文本、图像或文档,输出都是某种"结构"。分界线其实很清楚:
- ML Feature Extraction 的产物是"给下游预测模型吃的特征"。它不追求人类可读,只追求"对模型有用"。输出通常进一个特征向量,和一个传统的分类器、评分卡、推荐模型拼在一起。
- Structured Data Extraction 的产物是"给人或程序用的结构化记录"。它追求字段明确、可被下游程序直接消费——一张发票的字段、一份简历的经历、一个表格的列映射。
一句话:特征服务于模型,记录服务于业务。 特征允许冗余、允许数值化、允许组合;记录要求字段干净、命名稳定、语义准确。下面分别说。
flowchart TD
IN["非结构化输入
文本 / 图像 / 文档"] --> Q{"产出去向"}
Q -->|"喂给下游预测模型"| FE["ML Feature Extraction
Score / Noul 组合"]
FE --> V["特征向量
sentiment, urgency, …"]
V --> M["传统分类器 / 评分卡 / TabPFN"]
Q -->|"喂给人或程序"| SE["Structured Data Extraction
逐字段 Choice / Noul"]
SE --> R["结构化记录
发票字段 / 简历 / 列映射"]
R --> H["业务系统 / 人工复核"]ML Feature Extraction:把文本变成特征列
经典机器学习的特征工程一直有个痛点:数值字段好处理,文本字段难。 用户评论文本、客服对话、商品描述这类非结构化信号,以前要么靠人工抽关键词,要么干脆丢掉。Jev 补的正是这块——把一段文本变成几个有语义的特征值。
特征可以是两类:
- Score 特征(连续值):「这段评论正面程度」
score = 2.8。 - Noul 特征(布尔/概率):「评论里提到了物流问题吗」
P(true) = 0.83。
from typesafe import TypesafeClient
client = TypesafeClient()
def features(review: str) -> dict:
d = client.systemone(
state=review,
questions={
"sentiment": {"type": "score", "question": "整体情感倾向?",
"labels": ["很负面","负面","中性","正面","很正面"]},
"mentions_shipping": {"type": "noul", "question": "是否提到了物流/配送问题?"},
"mentions_refund": {"type": "noul", "question": "是否提及退款或退货?"},
"urgency": {"type": "score", "question": "文本透出的紧迫程度?",
"labels": ["低","中","高"]}
},
instructions="你是特征抽取器,只输出判断,不解释。"
)
return {
"sentiment": d["sentiment"].score, # 例如 1.8
"mentions_shipping": d["mentions_shipping"].value, # True/False
"shipping_p": d["mentions_shipping"].probability, # 0.83
"mentions_refund": d["mentions_refund"].value,
"urgency": d["urgency"].score, # 1.0–3.0
}得到的 {sentiment: 1.8, urgency: 2.4, ...} 就是一个可以直接拼进特征向量的字典。注意布尔特征同时保留了概率——对下游模型来说,0.83 比 True 信息量更大,尤其当这个特征本身有不确定性时。
特征可以是 Score / Noul 的组合
特征工程里"组合"往往比"单点"有用。Jev 的输出天然支持组合:
- 多个 Noul → 二值特征组:「提到物流」「提到退款」「情绪激烈」,把一条评论变成三个布尔位。
- 一个多维 Score → 有序特征:严重度、紧迫度、价值度,各自一个连续值。
- Noul 的概率 → 软特征:不做 0/1 量化,直接送概率,让下游模型自己决定怎么用这个不确定性。
这套做法的意义在于:Jev 成了传统 ML 与语言之间的适配层。 以前你要为文本字段专门训一个 embedding 模型或者做一堆规则,现在可以用几个类型化问题把语义"翻译"成模型认识的数值特征。而且——这一点很关键——这些特征是自解释的:特征名就是问题,出现问题时可回溯、可审计,不像 embedding 那样是黑盒。
tab-jev(edamame-labs/tab-jev)就是这个思路的完整形态。 它对每一行问一个关于目标的 Noul 加若干 Score 评分细则,把每个选项的概率都变成一个列,把列的文本特征和行本身的数值字段并排放好,然后让一个表格基础模型(比如 TabPFN)在上下文里从已标注的行学习。在 Kickstarter 众筹预测上,它用 256 个标注拿到 0.745 AUC,而单独用 Jev 做校准只有 0.682。
这个数字的解读是关键:Jev 不是取代表格模型,而是给表格模型补充语义特征。 单独用 Jev(把它的判断当结果)只到 0.682;但把 Jev 的输出当作特征喂给 TabPFN,再加上少量的表格上下文学习,就到了 0.745。这正是"特征"与"决策"的分野——同一次 Jev 调用,用结果就是 0.682,当特征就是 0.745。把 Jev 当特征源用,比把它当唯一决策者用,更能发挥经典 ML 的威力。
Structured Data Extraction:把文本变成记录
现在转到另一半。Structured Data Extraction 的产物是明确定义的字段记录——发票的金额与日期、简历的学历与经历、一张表到另一张表的列映射。
这类任务真正的难点不在"抽取动作",而在schema 设计和缺失值处理。先看一个字段抽取:
d = client.systemone(
state=invoice_text,
questions={
"invoice_no": {"type": "choice", "question": "发票号码是下列哪一个?",
"choices": ["INV-8821", "INV-8831", "INV-8841", "无法确定"]},
"currency": {"type": "choice", "question": "计价货币?",
"choices": ["CNY", "USD", "EUR", "其他", "未标明"]},
"has_tax_id": {"type": "noul", "question": "文本里是否出现了税号?"},
"total_amount":{"type": "score", "question": "总金额落在哪个量级?",
"labels": ["<100", "100-500", "500-2000", "2000-10000", ">10000"]}
},
instructions="从发票文本里抽取字段,信息缺失时如实选择『无法确定/未标明』。"
)schema 设计与缺失值处理
上面这段代码里有三条 schema 设计准则,值得单拎出来:
其一,允许"未知",别逼模型硬编。 每个枚举字段都要带一个"无法确定 / 未标明"选项。逼模型在信息缺失时也选一个具体值,就是在制造幻觉性字段。 一个诚实的"未知"比一个编出来的数字有价值得多——下游可以据此触发人工补录,而编造的值会静默污染数据。这和前面章节反复讲的"低置信转人工"是同一个思路,只是落在了字段层面。
其二,枚举字段用 Choice,逐字段抽取。 像货币、状态、类型这种取值有限的字段,用 Choice 把候选写死;数值、日期这类无界的字段,则用多个 Noul 逐条确认或让它从给定候选里选,避免开放生成。能用封闭选择的地方就不要用开放生成,这是贯穿全书的纪律。
其三,字段要一个一个问,不要一次要一大坨。 一次问"把发票所有字段抽出来",等于让模型生成一段 JSON 再祈祷它不出错——这正是第 1 章批评的老路。逐字段、类型化地抽,每个字段一个独立问题,缺失就是缺失,边界清晰。
jevextract(gabazureus/jevextract) 把"允许未知"做得更细:它让代码先用精确偏移量提出候选片段(span),然后 Jev 对每个片段问一个 Choice(属于某个 schema 类别,还是不属于任何类别),外加每个句子级类别一个 Noul,只保留超过每类阈值的答案,并把"接近边界"的标记出来给人复核。它的公开 benchmark 显示:在 Gemini 3.5 Flash 上,成本比 LangExtract 低 10–26×,但 F1 更低(双语 jx-bench 上 84.2 vs 88.5)。 这是一个诚实的数据——用更低的成本换来略低的抽取质量,取舍点在哪由业务决定。它同时说明:Choice 里那个"不属于任何类别"的选项,就是"允许未知"的具体形态。
抽取的通用模式:先定位候选、再逐条分类
jevextract 的做法揭示了一个跨任务通用的抽取模式:不要让模型"直接读懂整段并输出结果",而是让代码先圈出候选,再让决策层对每个候选做一次类型化判断。
JevSpan(lzq-0529/jev-span) 是这个模式的教科书案例。它是一个零样本命名实体识别(NER)方案,流程是三段式:
- 代码在标点处切分文本,对每一种实体类型、在每一个候选窗口上问一个
Choice(这段是不是该类型的实体); - 对每个被提名的候选用第二次
Choice验证(是该类型 / 不是 / 混合 / 部分); - 再用第三次判断敲定边界。
它在 12 个中英文 NER benchmark 上平均 73.7 strict F1,对比用 Qwen3.8-27B 直接抽取的 72.1。
这组对比信息量很大。一个 0.6B 级的类型化决策方案,在严格 F1 上超过了用 27B 生成模型直接抽取。原因不在于谁更聪明,而在于任务被拆对了:NER 是"判断这个片段属于哪一类"的判定任务,不是"写一段带标注的文本"的生成任务。把它拆成"候选提名 + 验证 + 定界"三个类型化判断,用一个小而专的决策层去填,效果比让一个大生成模型一次性输出标注更好。
同样的"定位候选 → 逐条判定"模式在视觉上也成立。GroundingJev(xyzzzh/GroundingJev) 是一个受 Jev 启发的 Qwen3.5-0.8B 模型,它把图像 + 指称表达式映射成四个边界框坐标,在一次前向里完成,报告相对其自回归基座模型有 8.61× 的推理加速。目标定位本质也是"判断这个位置/区域对不对"的判定问题,用一次并行决策替代逐 token 生成,速度自然上一个台阶。
两个"记录落地"的项目
typeful-triage(cephalization/jev-triage)——把结构化抽取接进协作流程。 这是一个多人协作的分诊看板,Jev 对每个 issue 回答一组固定的类型化问题——kind、severity、urgency、duplicate、next step——而每一次人工纠正都会被保留,并在后续运行里回显给模型。这个项目体现了结构化抽取在真实产品里的两个需求:一是字段固定(始终是那几类判断,便于下游消费);二是能学习人类纠正。抽取结果不是终点,它会被人修正,而修正本身又反过来改善后续抽取。
jev-resume-screening(nanami-0713/jev-resume-screening)——一次调用凑齐"证据 + 打分 + 路由"。 它用一次请求把一份简历对 JD 的评估拆成三类问题:五个 Noul 证据门、四个 Score 维度、一个 Choice 后台路由。它的判据从 v1 迭代到 v3,专门对着"诱饵简历"做加固——一份自述"AI 重度使用者"的华丽简历,评分从 0.95 掉到 0.49——任何低置信的答案都会升级到人工复核。这个项目展示了 Structured Data Extraction 的成熟形态:它不只是一次字段抽取,而是"证据门 + 维度分 + 路由"的组合,且判据可以随对抗样本迭代硬化。
其余项目一张表带过
| 项目 | 形态 | 关键设计 |
|---|---|---|
| jev-curate(AkashPriyadarshii/jev-curate) | Noul(特征/筛选) | 用 Jev Noul 检查与校准置信度筛选合成 JSONL / Parquet 行,把通过与拒绝的流直接写盘 |
| jev-table-import-mapper(DuvInc) | Choice/Noul(记录) | 先确定性名称相等,再对剩余(源列,目标列)逐个 Noul,外加每列守卫 Noul;23 列导出映射 10/10 命中,一次调用 253 问、915 ms、$0.0012 |
| jev-align(sutro-sh/jev-align) | Choice/Score/Boolean | 对 CSV/Parquet/JSONL 行做类型化判定,模糊样本与审计样本转人工,用接受的人工标签经 GEPA 优化已保存的定义 |
| jev-fit(jev-fit.com) | Choice/Noul(撮合) | 把"一个软件想法 + 固定打分表"一次送给 Jev,Choice 在纯代码 / Jev / 推理 LLM 三选一,背后一个 Noul 门控区分"是不是任务";低置信直接返回"不确定" |
注意 jev-align 与 jev-fit 里的两个细节:jev-align 用人工标签反过来优化定义(GEPA),这是"抽取结果可迭代"的高级形态;jev-fit 在低置信时直接返回"不确定",而不是硬选一个——又一次呼应"允许未知"这条贯穿全章的原则。
小结
- ML Feature Extraction 服务于模型,Structured Data Extraction 服务于业务——前者可以是 Score/Noul 的任意组合,后者要求字段干净、命名稳定。
- 特征的价值在于把语言"翻译"成模型认识的数值;同一次 Jev 调用,当结果用是 0.682 AUC,当特征喂给 TabPFN 是 0.745(tab-jev,256 标注)——把它当特征源比当唯一决策者更能发挥经典 ML。
- 结构化抽取的难点是 schema 设计与缺失值处理:每个枚举字段都要允许"未知",逼模型硬编等于制造幻觉性字段;枚举用 Choice、逐字段抽取,别一次要一大坨 JSON。
- 抽取的通用模式是**"代码先定位候选,决策层再逐条类型化判定"**——jevextract 的 span 级
Choice+Noul、JevSpan 的"提名 + 验证 + 定界"三段式,后者以 73.7 strict F1 超过 27B 生成模型直接抽取的 72.1。 - 该模式在视觉上同样成立:GroundingJev 用一次并行决策做目标定位,比自回归基座快 8.61×。
- 落地形态可以更丰富:typeful-triage 保留人工纠正回显、jev-resume-screening 把"证据门 + 维度分 + 路由"合进一次调用并对着诱饵简历硬化判据(0.95→0.49)。
到这里,十种决策形态全部走完。从下一章开始,进入第三部分,用七个行业用例看这些形态怎么在一套真实系统里协同工作。