Files
ThearchyHelios 4559c96fdd 出具校验认得中文数字了:"增长三成、客户两千五百家"不再能盖 formal 章
校验器只抽阿拉伯数字。strict 契约下"本周营收增长三成、客户两千五百家"一个数
都抽不出来,判 formal;同样的意思写成 2500 却会被 withheld。叙述层换一种写法
就绕过了"不许现编数字"这条地基,而"出错自动改一次"的复核回路会把模型往这条
路上推——被拦一次,它学会的是写汉字。review.check_numbers 复用同一套抽取,
同样的洞。

中文数字难在两头,规则按"这是不是在报一个量"来定(文件头有完整说明):
- 多字且解析得通的一律抽:十二、两千五百、两千五(省略写法)、一万二、三点五万;
- 单字只在后面跟计数单位时抽:两家、五天。"一"加量词多半是冠词(一个问题),
  不抽——放过一个值为 1 的现编,代价远小于满篇误杀;
- 成、倍、百分之、分之、一半单字也抽,按口语精度给容差(三成 = 30% ± 半成);
- 余 / 多 / 来按区间回指(两千余 = [2000, 3000]),不跳过——跳过就是新的绕法;
- 不抽:序数、星期、月日、季度名(三季度 = Q3)、"从三个方面看"这种结构性列举、
  几十 / 数百 / 上千这种虚指、成语(五花八门、千方百计、万一、十分重要)。

顺带修了阿拉伯数字的两处:
- 万 / 亿后缀的容差按书写精度算。3.5万 写到千位,应是 ±500,以前按 3.5 的一位
  小数给 ±0.05,只认 35000 整——README 承诺的"按书写精度"对带后缀的数不成立。
- 补上千 / 百万 / 千万后缀:"2千家"以前抽出来是 2,回指的是一个错的值。

出具校验以前没有一条直接测试(只经 check_numbers 间接覆盖)。新增 test_issuance.py
39 例:解析、该抽的、不该抽的两头都守,外加当初那条绕过(旧代码实测:抽不到、判
formal)。374 → 413 passed。
2026-09-25 15:30:32 +08:00

102 lines
5.0 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""出具校验的数字回指:叙述里写出来的每个量都得在指标集里找得到。
以前只认阿拉伯数字——"增长三成、客户两千五百家"在 strict 契约下判 formal,
同样的意思写成 2500 却会被 withheld。叙述层换一种写法就绕过了整套"不许现编
数字"的机制,而"出错自动改一次"的复核回路甚至会把模型往这条路上推。
中文数字难在两头:漏抽就是绕法,误抽就是满篇降档(一个问题、十分重要、从三个
方面看都不是数据)。所以这组用例两头都守。
"""
from __future__ import annotations
import pytest
from app.engine import review as rv
from app.engine.issuance import cn_to_number, decide_tier, extract_numbers, trace_numbers
def values(text: str) -> list[tuple[str, float]]:
return [(t.raw, round(t.value, 2)) for t in extract_numbers(text)]
@pytest.mark.parametrize("text, expected", [
("十二", 12), ("二十三", 23), ("一百零五", 105), ("两千五百", 2500),
("两千五", 2500), # 省略写法:千后面省掉了"百"
("一万二", 12000), ("三万零五百", 30500), ("一亿两千万", 1.2e8),
("三点五", 3.5), ("三点五万", 35000), ("三万亿", 3e12), ("万", 1e4),
("五一", None), ("三三两两", None), ("一一", None), ("十十", None),
("万一", None), # 不是一万一千
("二〇二六", None), # 年份的逐位写法不是一个数
])
def test_cn_to_number(text, expected):
assert cn_to_number(text) == (None if expected is None else float(expected))
@pytest.mark.parametrize("text, expected", [
("本周营收增长三成", [("三成", 30.0)]),
("客户两千五百家", [("两千五百", 2500.0)]),
("其中一半来自华东", [("一半", 50.0)]),
("翻了两倍", [("两倍", 2.0)]),
("约三分之一的门店", [("三分之一", 33.33)]),
("达到百分之三十", [("百分之三十", 30.0)]),
("关闭了两家门店,用时五天", [("两", 2.0), ("五", 5.0)]),
("新增两千余家", [("两千余", 2500.0)]), # 按区间 [2000, 3000] 回指
("营收 3.5万,客户 2千家", [("3.5万", 35000.0), ("2千", 2000.0)]),
])
def test_quantities_written_in_chinese_are_extracted(text, expected):
assert values(text) == expected
@pytest.mark.parametrize("text", [
"这是一个积极的信号,一些门店一起统一调价,十分重要",
"万一出问题千万不要慌,五花八门、千方百计、独一无二",
"第三季度和三季度营收,周三开会",
"十二月十五日,二〇二六年九月",
"从三个方面看,主要原因之一",
"几十家、数百家、上千家、十几家",
"十一假期期间,下午三点给出三点建议",
])
def test_figurative_numerals_are_not_extracted(text):
assert values(text) == [], "误抽了"
def test_the_bypass_that_used_to_pass_as_formal():
"""strict 契约下,指标集里没有的中文数字要让它 withheld,而不是盖 formal 章。"""
narrative = "本周营收增长三成、客户两千五百家。"
report = trace_numbers(narrative, [{"id": "revenue", "value": 1_234_567}])
assert [u["token"] for u in report.unmatched] == ["三成", "两千五百"]
tier = decide_tier(missing_required=[], missing_expected=[], unmatched=report.unmatched,
strict=True)
assert tier == "withheld"
def test_chinese_numerals_that_trace_back_pass():
"""换成中文写法本身不是问题——回指得上就照常 formal。"""
narrative = "本周营收增长三成、客户两千五百家,近一半来自华东。"
metrics = [
{"id": "growth", "value": 0.2987}, # 比率:三成 = 30% ± 半成
{"id": "customers", "value": 2500},
{"id": "east_share", "value": 48.6}, # 百分数:一半 = 50% ± 5
]
report = trace_numbers(narrative, metrics)
assert report.ok, report.unmatched
assert decide_tier(missing_required=[], missing_expected=[], unmatched=[], strict=True) == "formal"
def test_wan_suffix_tolerance_follows_written_precision():
"""3.5万写到千位,按 ±500 回指——以前按 3.5 的一位小数给 ±0.05,只认 35000 整。"""
assert trace_numbers("营收 3.5万", [{"id": "r", "value": 35123}]).ok
assert not trace_numbers("营收 3.5万", [{"id": "r", "value": 36000}]).ok
assert trace_numbers("营收 35万", [{"id": "r", "value": 351234}]).ok
def test_ranges_trace_within_their_span():
assert trace_numbers("新增两千余家", [{"id": "n", "value": 2150}]).ok
assert not trace_numbers("新增两千余家", [{"id": "n", "value": 3150}]).ok
def test_review_may_rewrite_numbers_in_chinese_but_not_invent_them():
"""复核重写时把 6 写成"六"不算新数字;多出来一个"五家"才算。"""
assert rv.check_numbers("管理员有 6 个。", "管理员有六个。") == []
assert rv.check_numbers("管理员有 6 个。", "管理员有六个,商家有五家。") == ["五"]