日期抽取
用象信读出日期的各个部分,再在代码里把它们拼成 date。
本篇要做的函数是 extract_date(document, role, today):给它一段文字、一句说明要找哪个日期的话(比如「春节假期的最后一天」),再给它发文当天的日期,它返回一个 date 和一个置信度。日期可以是写全的「2026年12月1日」,可以是省略了年份的「9月16日」、承接前文月份的「至23日」,也可以是相对发文日的「昨天」「明天」「周一」。文中根本没提这个日期时,它返回空,并把这一条交给人工。
分工很明确:象信在一次请求里用几道 Choice 读出日期是怎么写的、写了哪几个部分;代码负责所有的日历运算——补年份、从发文日往前往后数天、算「周一」落在哪天。象信一号不擅长数值和日期推理,所以它只读字面,不做计算。
一句话结论(2026-09-25 真实运行):在 29 篇真实文本、62 个日期上,57 个完全正确(文中写了的 53 个对 49 个,文中没有的 9 个对 8 个)。5 个错误的置信度全部 ≤ 0.26。按 TypeSafe 原文用的 0.60 复核线,只有 17 个日期自动通过(全部正确),其余 45 个送人工——这个比例偏保守,下文会讨论。62 次调用共 60,576 个输入 token,约合 ¥0.0025。
环境准备
pip install xiangxin-sdk
export XIANGXIN_API_KEY="sk-xx-..."模型为 xiangxin-latest(本次运行全部返回 xiangxin-1.0.0)。
数据
三类真实公开文本,2026-09-25 从官网抓取:
| 来源 | 篇数 | 日期数 | 特点 |
|---|---|---|---|
| 国务院办公厅关于2026年部分节假日安排的通知(国办发明电〔2025〕7号,2025-11-04 发布) | 1 | 15 | 日期不带年份,「至23日」承接前文月份,调休上班日散落各处 |
| 外交部发言人例行记者会实录(2026-09-15 至 09-24,共 8 场) | 24 段 | 38 | 大量「昨天」「今天」「明天」「周一」,以及不带年份的「9月16日至17日」 |
| 中国政府网:国务院令第 846、847 号,两条新华社政策新闻 | 4 | 9 | 「自2026年12月1日起施行」、常务会议日期、签署日期 |
每段记者会实录只取一问一答或一条发布内容作为一篇文档(文本不改一字),发文日期就是那场记者会的日期;相对日期都以它为 today。62 个日期里有 9 个是文中没有的,例如「东博会闭幕的日期」「元宵节的放假日期」「《市场监督管理所条例》废止的日期」,正确答案是空。期望值由本文作者对照原文人工标注,文档与标注见 data/documents.json、data/examples.json。
代码里没有任何节假日规则:节假日通知只是被当作一篇普通文档来读,答案都来自通知原文。
问题设计
一次请求最多 7 道 Choice。form 说明日期是怎么写的:写了几月几日(具体日期)、相对发文日的某一天(前天 到 后天)、用星期几表示(星期几),或者文中没有(none)。其余几道分别读出月、日、年、星期几、哪一周,以及这件事相对发文日是过去还是将来。代码只读 form 需要的那几道。
年份没有像 TypeSafe 原文那样列出 1900–2050 共 151 个选项,而是先用正则取出文中出现过的年份,只把它们作为选项;文中一个年份都没有时,干脆不问,交给代码推断。
import re
from xiangxin import Choice
MONTHS = {f"{m}月": m for m in range(1, 13)}
DAYS = {f"{d}日": d for d in range(1, 32)}
WEEKDAYS = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]
ANCHORS = {"前天": -2, "昨天": -1, "今天": 0, "明天": 1, "后天": 2}
YEAR_RE = re.compile(r"(?<!\d)((?:19|20)\d\d)年")
def year_options(document: str) -> list[str]:
"""年份不列 1900–2050 全表:先用正则取出文中出现过的年份,只把它们作为选项。"""
return sorted({f"{y}年" for y in YEAR_RE.findall(document)})
def date_questions(role: str, document: str) -> dict[str, Choice]:
"""最多七个 Choice,一次请求:只读出日期的"写法"和各个部分,不做任何日历运算。"""
questions = {
"form": Choice(
instructions=f"文中是怎样给出「{role}」的?",
criteria={
"具体日期": "写的是几月几日(可能带年份,也可能是「至23日」这样承接前文月份的写法)",
**{a: None for a in ANCHORS},
"星期几": "写的是周一、下周三之类",
"none": "文中没有给出这个日期",
},
),
"month": Choice(
instructions=f"「{role}」在几月?",
criteria={m: None for m in MONTHS} | {"none": "文中没有写出这个日期的月份"},
),
"day": Choice(
instructions=f"「{role}」是几号?",
criteria={d: None for d in DAYS} | {"none": "文中没有写出这个日期的几号"},
),
"year": Choice(
instructions=f"「{role}」是哪一年?",
criteria={y: None for y in year_options(document)}
| {"none": "文中没有写出这个日期的年份"},
),
"weekday": Choice(
instructions=f"「{role}」是星期几?",
criteria={w: None for w in WEEKDAYS} | {"none": "文中没有用星期几来表示这个日期"},
),
"week_offset": Choice(
instructions=f"文中说「{role}」是哪一周的星期几?",
criteria={
"上周": "上周三、上个星期三",
"本周": "本周三、这周三",
"下周": "下周三、下个星期三",
"没有限定": "只说「周三」,没有上/本/下",
},
),
"direction": Choice(
instructions=f"相对于发文当天,「{role}」是已经过去的日子,还是还没到的日子?",
criteria={"过去": "已经发生、已经过去", "将来": "还没有到来、计划或预告中的"},
),
}
if not year_options(document): # 文中一个年份都没有:不必问,年份交给代码推断
del questions["year"]
return questionsdirection 是为中文新闻加的。英文里 "next Thursday" 多半指将来,但记者会上「贝森特周一警告称」说的是刚过去的那个周一。只说星期几、没有「上/本/下」时,代码用 direction 决定往前找还是往后找。
在代码里拼日期
assemble 把答案拼成 date:没写年份时取发文日所在年,若这个日期已经比发文日早一个多月,就改到下一年(2025 年 11 月发布的节假日通知里的「1月1日」因此落在 2026 年);「昨天」「明天」从发文日加减;星期几按下面的约定换算。置信度取实际用到的各部分中最低的那个,任何一部分读得没把握,整个日期就送复核。
from datetime import date, timedelta
REVIEW_BELOW = 0.60
def resolve_weekday(today: date, weekday: str, week_offset: str, direction: str) -> date:
"""约定:上周/本周/下周按自然周(周一为一周第一天);
没有限定词时,过去的事取今天及以前最近的一个,将来的事取今天及以后最近的一个。"""
w = WEEKDAYS.index(weekday)
this_monday = today - timedelta(days=today.weekday())
if week_offset == "上周":
return this_monday + timedelta(days=w - 7)
if week_offset == "本周":
return this_monday + timedelta(days=w)
if week_offset == "下周":
return this_monday + timedelta(days=w + 7)
if direction == "过去":
return today - timedelta(days=(today.weekday() - w) % 7)
return today + timedelta(days=(w - today.weekday()) % 7)
def assemble(parts: dict, today: date) -> dict:
"""把象信读出的部分拼成日期。置信度取实际用到的各部分中最低的那个。"""
form = parts["form"]["choice"]
confs = [parts["form"]["confidence"]]
def result(resolved: date | None, note: str) -> dict:
confidence = min(confs)
return {
"date": resolved.isoformat() if resolved else None,
"confidence": confidence,
"needs_review": resolved is None or confidence < REVIEW_BELOW,
"note": note,
}
if form == "none":
return result(None, "文中没有这个日期")
if form == "具体日期":
parts = {"year": {"choice": "none", "confidence": 1.0}} | parts
month, day, year = (parts[p]["choice"] for p in ("month", "day", "year"))
confs += [parts[p]["confidence"] for p in ("month", "day", "year")]
if month not in MONTHS or day not in DAYS:
return result(None, "月或日没有读出")
m, d = MONTHS[month], DAYS[day]
if year == "none": # 没写年份:取今年;如果已经过去一个多月,改为明年
try:
resolved = date(today.year, m, d)
except ValueError:
return result(None, f"不存在的日期:{month}{day}")
if resolved < today - timedelta(days=31):
resolved = date(today.year + 1, m, d)
return result(resolved, "年份由代码推断")
try:
return result(date(int(year[:4]), m, d), "")
except ValueError:
return result(None, f"不存在的日期:{year}{month}{day}")
if form in ANCHORS:
return result(today + timedelta(days=ANCHORS[form]), "")
if form == "星期几":
weekday, offset = parts["weekday"]["choice"], parts["week_offset"]["choice"]
confs += [parts["weekday"]["confidence"], parts["week_offset"]["confidence"]]
if offset == "没有限定": # 只有这时才需要"过去 / 将来"
confs.append(parts["direction"]["confidence"])
if weekday not in WEEKDAYS:
return result(None, "没有读出星期几")
return result(resolve_weekday(today, weekday, offset, parts["direction"]["choice"]), "")
return result(None, f"未知的写法:{form}")
def extract_date(document: str, role: str, today: date) -> dict:
answers = client.system_one(
state=document, questions=date_questions(role, document), model="xiangxin-latest"
).answers
parts = {k: {"choice": a.choice, "confidence": a.confidence} for k, a in answers.items()}
return assemble(parts, today)运行
62 个日期逐个调用 extract_date,每个一次请求。下面是真实输出(OK 表示与人工标注一致):
文档 要找的日期 期望 结果 置信 标记
--------------------------------------------------------------------------------------------------------------
OK holiday 元旦假期的第一天 2026-01-01 2026-01-01 0.46 <== 人工复核
OK holiday 元旦假期结束后调休上班的那一天 2026-01-04 2026-01-04 0.39 <== 人工复核
OK holiday 春节假期的第一天 2026-02-15 2026-02-15 0.47 <== 人工复核
OK holiday 春节假期的最后一天 2026-02-23 2026-02-23 0.31 <== 人工复核
XX holiday 春节假期之后调休上班的那一天 2026-02-28 none 0.18 <== 人工复核 (没有读出星期几)
OK holiday 清明节假期的第一天 2026-04-04 2026-04-04 0.55 <== 人工复核
OK holiday 劳动节假期的最后一天 2026-05-05 2026-05-05 0.36 <== 人工复核
OK holiday 劳动节假期之后调休上班的那一天 2026-05-09 2026-05-09 0.47 <== 人工复核
OK holiday 端午节假期的第一天 2026-06-19 2026-06-19 0.58 <== 人工复核
OK holiday 中秋节假期的第一天 2026-09-25 2026-09-25 0.59 <== 人工复核
OK holiday 国庆节假期的最后一天 2026-10-07 2026-10-07 0.49 <== 人工复核
OK holiday 国庆节假期之前调休上班的那一天 2026-09-20 2026-09-20 0.20 <== 人工复核
OK holiday 这份通知的落款日期 2025-11-04 2025-11-04 0.41 <== 人工复核
OK holiday 元宵节的放假日期 none none 0.81 <== 人工复核 (文中没有这个日期)
OK holiday 除夕的日期 none none 0.51 <== 人工复核 (文中没有这个日期)
OK mfa0915_amorim 阿莫林访华的第一天 2026-09-16 2026-09-16 0.55 <== 人工复核 (年份由代码推断)
OK mfa0915_amorim 阿莫林访华的最后一天 2026-09-17 2026-09-17 0.57 <== 人工复核 (年份由代码推断)
OK mfa0915_amorim 阿莫林抵达北京的航班起飞日期 none none 0.43 <== 人工复核 (文中没有这个日期)
OK mfa0915_jcfl 刘彬部长助理会见日中友好议员联盟代表团的日期 2026-09-14 2026-09-14 0.44 <== 人工复核
OK mfa0915_space 美国太空军宣称已在太空部署武器的日期 2026-09-14 2026-09-14 0.77
OK mfa0915_ai 《人工智能安全治理框架3.0》发布的日期 2026-09-14 2026-09-14 0.94
OK mfa0916_caexpo 丁薛祥出席东博会开幕式的日期 2026-09-17 2026-09-17 0.68 (年份由代码推断)
OK mfa0916_caexpo 国务院新闻办介绍东博会情况的新闻发布会的日期 2026-09-01 2026-09-01 0.44 <== 人工复核 (年份由代码推断)
OK mfa0916_caexpo 东博会闭幕的日期 none none 0.42 <== 人工复核 (文中没有这个日期)
OK mfa0916_lawsuit 二战中国受害劳工后代提交起诉书的日期 2026-09-16 2026-09-16 0.78
OK mfa0916_eu Connect Europe发布公开信的日期 2026-09-15 2026-09-15 0.52 <== 人工复核 (年份由代码推断)
OK mfa0916_eu 欧盟委员会出台《网络安全法案》修订案的日期 none none 0.24 <== 人工复核 (月或日没有读出)
OK mfa0917_gfci 第40期《全球金融中心指数》报告发布的日期 2026-09-16 2026-09-16 0.31 <== 人工复核 (年份由代码推断)
OK mfa0917_letter 阿莫林与王毅外长会见的日期 2026-09-16 2026-09-16 0.67
OK mfa0921_unga 第81届联大一般性辩论开始的日期 2026-09-22 2026-09-22 0.87
OK mfa0921_unga 全球发展倡议5周年高级别对话会举办的日期 2026-09-25 2026-09-25 0.79 (年份由代码推断)
OK mfa0921_unga 第81届联大一般性辩论结束的日期 none none 0.44 <== 人工复核 (文中没有这个日期)
OK mfa0921_duma 俄罗斯国家杜马选举开始的日期 2026-09-18 2026-09-18 0.42 <== 人工复核 (年份由代码推断)
OK mfa0921_duma 俄罗斯国家杜马选举结束的日期 2026-09-20 2026-09-20 0.35 <== 人工复核 (年份由代码推断)
OK mfa0921_rareearth 暂停实施稀土出口管制措施的截止日期 2026-11-10 2026-11-10 0.54 <== 人工复核 (年份由代码推断)
OK mfa0921_attack 巴基斯坦开普省警察营地遭恐怖袭击的日期 2026-09-18 2026-09-18 0.42 <== 人工复核 (年份由代码推断)
OK mfa0922_hanzheng 韩正赴纽约出席联大一般性辩论的第一天 2026-09-24 2026-09-24 0.39 <== 人工复核 (年份由代码推断)
OK mfa0922_hanzheng 韩正结束纽约之行的日期 2026-09-26 2026-09-26 0.35 <== 人工复核 (年份由代码推断)
OK mfa0922_expo 第48届世界技能大赛开始的日期 2026-09-22 2026-09-22 0.70 (年份由代码推断)
OK mfa0922_expo 第五届全球数字贸易博览会开始的日期 2026-09-23 2026-09-23 0.72 (年份由代码推断)
OK mfa0922_expo 第五届全球数字贸易博览会结束的日期 2026-09-27 2026-09-27 0.74 (年份由代码推断)
XX mfa0922_iran 贝森特发出这一警告的日期 2026-09-21 2026-09-28 0.15 <== 人工复核
OK mfa0922_iran 伊朗航司开始在全球被停飞的日期 2026-09-23 2026-09-23 0.68
OK mfa0922_caexpo 第23届中国—东盟博览会闭幕的日期 2026-09-21 2026-09-21 0.70
OK mfa0922_caexpo 平陆运河正式通航的日期 none none 0.25 <== 人工复核 (文中没有这个日期)
OK mfa0922_video 外交部发言人办公室发布“建设性战略稳定”视频的日 2026-09-22 2026-09-22 0.69
OK mfa0923_skills 世界技能大会开幕式召开的日期 2026-09-23 2026-09-23 0.69
OK mfa0923_skills 第48届世界技能大赛结束的日期 2026-09-27 2026-09-27 0.81 (年份由代码推断)
OK mfa0923_visit 习近平主席抵达美国的日期 none none 0.22 <== 人工复核 (没有读出星期几)
OK mfa0923_takaichi 高市早苗在联合国大会发言的日期 2026-09-22 2026-09-22 0.29 <== 人工复核 (年份由代码推断)
OK mfa0924_dte 第五届全球数字贸易博览会开幕的日期 2026-09-24 2026-09-24 0.35 <== 人工复核
XX mfa0924_truce 中美贸易休战延长后的截止日期 2027-01-10 none 0.19 <== 人工复核 (月或日没有读出)
OK mfa0924_recess 外交部例行记者会休会的日期 2026-09-25 2026-09-25 0.59 <== 人工复核
OK mfa0924_recess 外交部例行记者会恢复的日期 2026-09-28 2026-09-28 0.31 <== 人工复核
OK decree847 国务院常务会议修订通过《审计法实施条例》的日期 2026-08-31 2026-08-31 0.53 <== 人工复核
OK decree847 《审计法实施条例》开始施行的日期 2026-12-01 2026-12-01 0.25 <== 人工复核
OK decree847 国务院令第847号的签署日期 2026-09-19 2026-09-19 0.54 <== 人工复核
XX decree846 《市场监督管理所条例》开始施行的日期 2026-11-01 2026-11-11 0.26 <== 人工复核
OK decree846 国务院常务会议通过《市场监督管理所条例》的日期 2026-08-21 2026-08-21 0.50 <== 人工复核
XX decree846 《市场监督管理所条例》废止的日期 none 2026-11-01 0.15 <== 人工复核
OK xinhua0918 部署医疗康复护理扩容提升工程的国务院常务会议召开 2026-09-18 2026-09-18 0.61 (年份由代码推断)
OK gov0923 介绍“十五五”民政事业情况的国新办新闻发布会的日 2026-09-23 2026-09-23 0.76 (年份由代码推断)几个值得看的行:
- 节假日通知:15 个日期对了 14 个。「春节假期的最后一天」原文是「2月15日……至23日」,象信读出了 2 月和 23 日;「国庆节假期之前调休上班的那一天」是写在国庆条目末尾的「9月20日(周日)」,也读对了。错的一个是「春节假期之后调休上班的那一天」(2月28日):
form被读成了「星期几」(原文写着「2月28日(周六)上班」),weekday又没读出来,结果为空,置信度 0.18。 - 相对日期:「第81届联大将于明天开始举行一般性辩论」(9 月 21 日记者会)→ 2026-09-22,置信度 0.87;「就在昨天,中国相关部门发布《人工智能安全治理框架3.0》」→ 2026-09-14,置信度 0.94。
- 星期几:「贝森特周一警告称」出自 9 月 22 日(周二)的记者会,应为 9 月 21 日。象信读对了「周一」(0.77),但
direction答成了「将来」(0.32),代码于是往后找到 9 月 28 日。这一行的置信度只有 0.15,被送去复核。 - 文中没有的日期:9 个里 8 个正确地返回空。唯一的错误是「《市场监督管理所条例》废止的日期」——文中只有施行日期,象信把它当成了答案,置信度 0.15。
- 20 个日期的年份由代码推断(文中只写了「9月16日」之类),全部正确。
用置信度分流
与 TypeSafe 原文一样,置信度低于 REVIEW_BELOW = 0.60 的日期、以及拼不出日期的,都交给人工:
accepted = [r for r in results if not r["needs_review"]]
review = [r for r in results if r["needs_review"]]| 数量 | 其中正确 | |
|---|---|---|
| 自动通过(置信度 ≥ 0.60) | 17 | 17 |
| 送人工复核 | 45 | 40 |
5 个错误全部在复核队列里,自动通过的 17 个没有一个出错。代价是复核队列太长:45 个里有 40 个其实是对的。换一个阈值看(只统计拼出了日期的结果):
| 阈值 | 自动通过 | 其中正确 |
|---|---|---|
| 0.3 | 46 | 46 |
| 0.4 | 37 | 37 |
| 0.5 | 28 | 28 |
| 0.6 | 17 | 17 |
| 0.7 | 11 | 11 |
| 0.8 | 3 | 3 |
在这批数据上,所有错误的置信度都 ≤ 0.26,所以阈值降到 0.3 时自动通过 46 个,仍然全部正确。但这是在同一批 62 个样本上挑出来的阈值,拿到新数据上未必成立;实际使用时应当留出一批标注数据来定阈值。
置信度偏低的主要原因是「取最小值」:一个日期要用到 3–4 道题,任何一道拿不准都会拉低整体。62 个日期里,有 46 个的最低分来自 form 这道题——象信能分清「具体日期」和「昨天」,但概率常常分给几个相邻的写法(form 的置信度中位数为 0.55)。如果你的业务里日期写法比较单一(例如只处理公文里的「X年X月X日」),把 form 的选项收窄,置信度会明显提高。
问法的影响:第一版为什么差
本文的第一版问题照搬了 TypeSafe 原文的结构:mode(absolute / relative / none)、day_anchor 各一道,其余每道题都写成「如果「……」是写明月日的公历日期,它是这个月的几号?」这样的条件句,年份列出 1900–2050 全部 151 个选项。同样 62 个日期、同样的代码逻辑,结果是(问题定义见 v1_questions.py,原始输出见 results/v1_rows.json):
| 第一版 | 现行版 | |
|---|---|---|
| 正确 | 51/62 | 57/62 |
| 置信度 ≥ 0.60 自动通过 | 2 | 17 |
| 输入 token | 179,103 | 60,576 |
逐题看,第一版里「几号」这道题的置信度中位数只有 0.17。我们在同一篇文档上对比了两种问法(probe_phrasing.py,输出见 results/probe_phrasing.json):
如果「……报告发布的日期」是写明月日的公历日期,它是这个月的几号? -> 16日 置信 0.16
「……报告发布的日期」是几号? -> 16日 置信 0.91答案都对,但条件句让模型把大量概率分给了 none 和相邻的日子。改成直截了当的问法、把年份选项缩到文中实际出现的那几个之后,准确率和置信度都上去了,token 也少了三分之二(年份的 151 个选项占了大头)。经验是:一道题只问一件事,前提条件交给代码去判断,不要写进问题里。
局限
- 只处理公历。「农历腊月二十八」「正月初七」这类写法不会被读成日期;需要时先在代码里用农历库换算,或者只取同一句里的公历日期。
- 「上周」「上个月」「月底」「节后第一个工作日」这类说法,本文的选项不覆盖。可以按同样的思路加选项,但凡是需要日历知识的(哪天是工作日、哪天调休),一律放在代码里做。
- 一句话里有多个日期时(「9月24日至26日」),要找哪一个完全靠
role说清楚。role写得含糊,结果就不稳。
完整代码
https://github.com/xiangxinai/xiangxin-cookbooks/tree/main/date_extraction_cookbook

