结构化抽取三件套:日期、实体对齐、预解析值用 Jev 做
先说结论
- 官方三个 cookbook 面对的是同一类脏文本难题:把自然语言里零散的值变成可验证的结构化字段。日期抽取按部件读取后在代码里算日历,实体对齐用 Score 做三态判定,预解析值先由 regex 穷举候选再由模型选择。
- 共同模式很明确:把原文片段与候选值一起放进
state,用Choice在多个候选里选一个,或用Score给有序结果打分;关键是把可接受的值集合写进criteria。 - 官方三篇都没有公布准确率、召回率等性能数字。可确认的是实体对齐示例使用模型
jev-1.12(官方数据),实验日期是 2026-08-11(官方数据)。其余性能数据未能核实。 - 我的实践建议:抽取类任务不要一次到底,先召回候选、再对齐选择;金额、日期等关键字段必须在代码侧做二次校验,模型判断只作为其中一道闸。
- 不要把整篇长文档塞给模型。日期示例用
role短语定位要读的那笔日期;实体对齐逐对发单条 JSON;预解析示例的state是单条邮件或发票片段。
证据与过程
日期:让模型读部件,不让它算日历
Date extraction 的官方定位是:“Extracts absolute and relative dates by asking TypeSafe for the parts named in a document, then resolving and validating them in code with confidence-based review.”。
一言概括,就是模型负责从文本里读出日期是怎么写的、哪些部分被提到,真正的日历计算交给代码。官方原句写得很直白:“The model reads what the text says and never does the calendar math.”。这样设计有个好处:模型不会替你做日期的加减,也不会凭空补一个年份。
日期有绝对和相对两种写法:绝对日期需要 month、day、year;相对日期相对今天,需要 day_anchor,比如 today、tomorrow、the day after tomorrow,或者一个星期的名字。官方示例把“今天”固定为 date(2026, 7, 30)(官方数据),这样相对日期可以复现地解析。示例里还有一个低置信度闸门 REVIEW_BELOW = 0.60(官方数据),这是示例常量,不是官方推荐的普适阈值;我把它当作示例参数,不建议直接照搬到所有业务里。
该 cookbook 用七个 Choice question 在一次请求中完成读取:mode 判断日期是 absolute、relative 还是 none,其余六个读 month、day、year、day_anchor、weekday、week_offset。值得留意的是年份选项是 1900 到 2050 的列表(官方数据),另外带 none 和 out_of_range 两个出口:文本没写年份就由代码补,写超出范围就由代码标记,而不是让模型猜。这个设计我觉得很务实:模型只报告看到了什么,缺省值和范围异常由代码决定策略。
实体对齐:用 Score 做三态编辑判断
Knowledge graph entity alignment 处理的是知识图谱里的同实异名问题。它的输入不是原始文档,而是已经经过粗略第一遍筛选的候选对。官方示例使用两个啤酒目录,先筛出 450 个候选对(官方数据),每个实体带 name、brewery、style、abv 四个字段。
官方原句把任务说得很清楚:“Given potential duplicate pairs, a single TypeSafe Score decides whether each pair is a duplicate, or whether it deserves a closer look from a curator.”。
判断不是简单的“是/否”,而是三态:
different product:保持两个实体不链接;related, but possibly not the same:交给 curator 决定;same product:合并。
为什么用 Score 而不是 Choice 或 Noul?官方解释是:“We use a Score question because we want to attach a semantic label, the score criteria, directly to each outcome, including the middle outcome. A Noul question could accomplish this indirectly through thresholding on its output instead, and a Choice question would lose the ordered relationship of the three outcomes.”。
这段话把三种 primitives 的边界讲得很透:Choice 丢掉有序关系,Noul 要自己设阈值,而 Score 能把语义标签直接贴到每个层级。实体对齐还伴生 Noul 问题,在同一个请求里判断各字段是否 match,给 curator 提供更多信息。这些是官方示例的做法。
另外,官方特别强调合并错误比漏匹配更贵:“Merging two entities inappropriately is the more expensive mistake, since every fact about either entity now describes the merged one”。我理解这是中间态存在的现实原因:既不能安全合并,也不能安全丢弃,所以需要人工或代码进一步处理。
官方还给出一个成本模型:“One request goes out per pair, so what you spend follows the number of pairs you were handed rather than the size of either source.”。也就是说,请求数跟着候选对走,不跟着两个源的数据总量走。这点很关键:上游第一遍如果能筛掉大量不可能的组合,下游成本就低很多。
预解析值:regex 先找,Jev 只做选择
Pre-parsed value extraction 最像工业抽取管线的标准解法。官方概述说:“A regex finds the candidate values, TypeSafe picks the one the question asks for, and code copies it verbatim.”。
它分三步:规则先找候选;模型再选哪个是问题要的值;代码最后原样拷贝并规范化。处理对象包括邮箱、电话、金额三类。官方示例里的三个场景是:收据应发到哪个邮箱;电话号码 +14155550177;发票总额 1315.50 USD flagged as a charge(官方数据)。
我最喜欢这篇里的一句话:“Because TypeSafe only ever chooses among the spans the regex found, the value you get back is one of those spans, copied unchanged. It cannot invent a value or transpose a digit.”。
这句话解释了为什么这套模式适合金额、电话这类关键字段:模型没法发明值,也没法把数字抄乱,因为它只能从 regex 找到的 span 里选一个原样返回。官方代码注释还写着:“The options ARE the candidate spans, so choice is a verbatim copy of one of them (or the none hatch) – the model chooses, code owns the string.”。
这里的候选集就是 regex 找到的 span,criteria 里放这些 span 再加一个 none 出口。官方指导 regex 要“Tune it to over-find.”,也就是规则端宁可多召回,也不要漏掉正确值。这是官方对规则端的方向性建议,不是推荐某个具体正则。
共同模式:state + criteria + 恰一个判断
三篇 cookbook 放在一起看,共同骨架非常清晰。Primitives 官方文档写明:“TypeSafe’s primitives are the small, typed building blocks you compose in code. They come in pairs: a question defines one judgment for a System One model to make about a state, and its answer is the typed value that comes back.”。
三种问题类型返回形状不同:
Choice:choice、probabilities、confidenceScore:score、legend、probabilities、confidenceNoul:noul(0 到 1)
这是官方给出的返回字段(官方数据)。其中 Choice 适合无顺序关系的选项集,Score 适合有顺序的层级,Noul 适合是非判断。
把三篇结合起来,共同模式是:先把原文片段和候选值放进 state;再用 Choice 在多候选里选一个,或用 Score 给有序层级打分;最后将模型输出交给代码去解析、规范化或二次校验。候选值在三篇中各有来源:日期来自枚举选项,实体对来自上游粗筛,预解析值来自 regex。相同点在于,可接受的值集合都写进了 criteria。
三者差异对照表
| 维度 | 日期抽取 | 实体对齐 | 预解析值抽取 |
|---|---|---|---|
| 输入形态 | 短文档 + role 名称 |
实体对 JSON,每个实体四个字段(官方示例) | 原始文本片段,如邮件或发票上下文 |
| 是否需要候选集 | 内部枚举月份、星期、年份窗口等 | 需要外部候选对,先粗筛 | 需要代码先 regex 或规则找候选 span |
| 输出形态 | 结构化日期 + confidence | Score 等级 + 伴随 Noul 字段差异 | 原样 span + 后续 normalize + 属性标签 |
| 典型失败 case | 部件不全拼不成日期;低置信度;文档未提日期但 mode 判为 none |
既不能安全合并也不能安全丢弃,落到 curator 中间态 | regex 漏召回,候选集没有正确值,落到 none 供人工检查 |
| 问题类型 | Choice | Score + Noul | Choice + Choice/Noul |
表中“典型失败 case”是基于官方文本的推断(自己推算),不是官方 cookbook 给出的指标或统计数据。例如日期篇官方提到“flags a low-confidence read, and one whose parts do not add up to a date at all, including a date the document never states.”,对应表中日期例的两条失败形态。
最小请求骨架
下面这段代码基于官方示例中的字段和调用方式写成,不是官方完整 cookbook:
import os
import re
from typesafe_sdk import Choice, TypeSafeClient
# 实践中先截取与用户问题相关的两段,而不是整篇长文
relevant_span = "Invoice total is $1,315.50. Charge to card ending 4321."
# 代码先粗找候选值(此处以金额为例)
MONEY_RE = re.compile(r"$?s?d[d,]*(?:.d{2})?")
candidates = [m.strip() for m in MONEY_RE.findall(relevant_span)]
ts = TypeSafeClient(
api_key=os.environ.get("TYPESAFE_API_KEY", "cache-only"),
timeout=30.0,
)
resp = ts.system_one(
state=relevant_span,
questions={
"pick": Choice(
instructions="Which amount is the invoice total?",
criteria={c: None for c in candidates}
| {"none": "None of these is the requested value."},
)
},
model="jev-1.12",
)
answer = resp.answers["pick"]
print(answer.choice, answer.confidence)
state、questions、Choice、criteria、model 这些名称来自官方示例;MONEY_RE、候选集构建和“先截取相关两段”的策略是本文工程做法。criteria 里混入 none 出口的写法,也来自预解析官方示例(来源3)。
关于 state 放什么:日期任务把 role 写进 instructions,state 放包含该日期指代的短文档;实体对齐放候选对的 JSON;预解析放单条文本。如果原文很长,工程上应当先切成若干相关段,再分别放进 state,不要让一个庞大文档拖慢判断,也不要把噪声全倒给模型。
我的实践清单
以下是我自己的判断,不归因到官方文档:
- 先找候选,后做选择。不要一上来让模型自由生成结构化值。regex、规则、检索或上一轮模型输出,都可以先生成冗余候选集,再让 Jev 在受控集合里选。预解析篇就是一个很好的起点。
- 关键字段必须有代码侧二次校验。日期用
date/datetime试构造,金额转Decimal并校验币种与正负号,电话/邮箱跑格式校验。模型输出只当作候选置信度,不作为最终事实源。 - 把可接受的值集合写进
criteria。选项列表不要过长。日期示例用 1900–2050 年份窗口可以,但业务上也可以先抽数字再放少量候选。若候选集可能覆盖不到正确值,显式加none或out_of_range。 - 中间态比二分类更有用。实体对齐里的中间态能让 curator 只处理少数可能合并的候选,而不是全量扫描。若你的任务也有“可能错了但不确定”的情况,应模拟三态。
- 请求粒度要控制成本。实体对齐示例逐对发请求,上游先筛候选对可以有效控制请求量。预解析按文档调用,也可以在上游限制文档长度。不要一上来就把全量数据整体发给模型。
这次没核实的
- 三篇 cookbook 的准确率、召回率、F1、ROC 等性能指标,在给定来源正文中未出现。具体数值未能核实,本文没有填入任何猜测值。
- 日期 cookbook 中的
REVIEW_BELOW = 0.60是官方示例常量,不是官方推荐的普适阈值;我把它当示例参数,不当作最佳实践。 - “每个候选项各发一个评分问题”、“并行评估”等性能优化做法,在来源中没有明确官方建议。若采用,需要自己在业务里验证成本与延迟。
- 官方文档后续是否有其他模型版本更新,以及
jev-1.12是否为当前推荐版本,未能从给定片段核实。
参考来源
评论区
登录后可评论。