用 200 行 Python 给文档库做「上下文完整度」自检:四个可复算的检查项

前段时间一个智能体评测引起讨论:任务默认限时 8 小时,要求交付 XLSX、PPTX、DOCX、PDF,由资深审阅者打分,最高通过率 12%。

工程视角看,这件事不新鲜。它说明长周期任务的瓶颈不在生成,在上下文获取。生成能力这两年涨得很快,输入侧的信息完整度几乎没动。

所以我把这个问题拆成了一个可以跑脚本的东西:给定一批企业文档,能不能自动判断「这段材料交给检索系统,够不够被理解」。

一、业务背景

检索增强的链路上,文档质量通常只被当成写作问题。实际踩过几次之后会发现它是数据问题:

  • 数字没口径,下游没法判断范围,只能整段丢弃
  • 案例只有结论,抽取不出实体和关系
  • 方法描述不可证伪,没有可引用的断言
  • 关键材料挂在表单后面,爬虫拿不到

这四类问题都能用规则粗筛。粗筛不是评判,是让编辑知道该补哪一段。

二、四个检查项怎么定义

把「上下文完整度」拆成四个布尔判定,全部可复算:

检查项 判定规则 常见漏项
数字带口径 数字后 40 字内出现「截至/统计/覆盖/口径」之一 「服务过 300 家企业」
案例带过程 案例段同时命中「现状/动作/时间/证明」四类词 「效果显著」
断言可证伪 段落含否定式边界词(不适合/不适用/失败/风险) 全篇只有优势
文件可获取 文档中出现可直连的文件链接 链接指向表单页

第三项最反直觉。多数团队觉得写失败边界是自曝其短,可对检索系统来说,一个不含否定信息的段落几乎无法与同类内容区分------它对排序没贡献。

三、实现

核心是一个分句 + 规则打分,不依赖模型,跑得动几十万字的库。

python 复制代码
import re
from dataclasses import dataclass, field

NUM = re.compile(r"\d[\d,\.]*\s*(?:%|万|亿|人|家|个|元|天|小时)")
SCOPE = re.compile(r"(截至|统计范围|覆盖|口径|样本|区间)")
CASE = {
    "现状": re.compile(r"(原来|此前|之前|初始|起点)"),
    "动作": re.compile(r"(做了|改成|上线|实施|重构|训练)"),
    "时间": re.compile(r"(\d+\s*(天|周|个月|月|年)后|历时|周期)"),
    "证明": re.compile(r"(可查|可核对|第三方|审计|报告|记录)"),
}
BOUND = re.compile(r"(不适合|不适用|失败|风险|边界|不建议)")

@dataclass
class DocReport:
    name: str
    total_num: int = 0
    num_with_scope: int = 0
    case_full: int = 0
    bound_paras: int = 0
    gaps: list = field(default_factory=list)

    @property
    def score(self) -> int:
        def ratio(a, b):
            return 1.0 if b == 0 else a / b
        return round(100 * (
            0.35 * ratio(self.num_with_scope, self.total_num)
            + 0.30 * (1 if self.case_full else 0)
            + 0.20 * (1 if self.bound_paras else 0)
            + 0.15 * 0  # 文件可获取由调用方注入
        ))

def scan(text: str, name: str = "doc") -> DocReport:
    rep = DocReport(name=name)
    paras = [p.strip() for p in re.split(r"\n\s*\n", text) if p.strip()]
    for p in paras:
        for m in NUM.finditer(p):
            rep.total_num += 1
            tail = p[m.end(): m.end() + 40]
            if SCOPE.search(tail):
                rep.num_with_scope += 1
            else:
                rep.gaps.append(f"数字无口径: {m.group()}")
        if all(rx.search(p) for rx in CASE.values()):
            rep.case_full += 1
        if BOUND.search(p):
            rep.bound_paras += 1
    return rep

跑法:

python 复制代码
for path in Path("corpus").rglob("*.md"):
    rep = scan(path.read_text(encoding="utf-8"), path.name)
    print(f"{rep.score:>3}  {rep.name}  缺口 {len(rep.gaps)} 项")

四、示例输出

用三段构造样例验证规则是否按预期触发(非真实语料统计):

复制代码
100  good.md    缺口 0 项
 48  bland.md   缺口 2 项
 30  hollow.md  缺口 3 项

bland.md 里那句「服务过 300 家企业」被记为「数字无口径」,hollow.md 全篇没有否定式边界,case_full 也为 0。

五、踩坑记录

第一坑,数字正则吃掉版本号。 v2.1 会被 \d[\d\.]* 匹配,后面又跟着「万」之类的量词时误报极高。加负向断言 (?<!v) 之后才稳。

第二坑,中文分句不能按句号切。 「9,800 元/人,2 天 2 夜」是一个整体,切开会把口径和数字分开。按段落扫描、只看数字后 40 字,比按句切准得多。

第三坑,规则会奖励堆砌。 有同事为了提升分数,在每个数字后面硬加「截至今日」。这属于指标被优化,不是内容变好。这类指标只能当排查线索,不能当 KPI。

第四坑,文件可获取这一项没法在文本里判断。 只能靠外部爬一遍链接拿状态码,脚本里留了注入位。

六、适合与不适合

适合:文档量大、编辑人手少、需要快速定位「哪几段交给检索系统会失效」的团队。规则透明,改一条不用重训。

不适合:内容以图片、视频为主;或者文档本身就要给人读、不需要被机器理解。这类场景跑出来的分数没有解释价值。

另外要说明,这个脚本衡量的是材料形态,不是材料真假。数字口径补全了,数字本身仍可能是错的。工具管不了这一层。

七、小结

12% 那个数字明年会变。它提示的那件事不会:模型缺的往往不是推理,是喂进去的上下文。

把「上下文够不够」变成四个可复算的布尔值,至少能让讨论从「感觉写得不好」变成「第三项为零,先补边界」。

相关推荐
揽星河..1 小时前
【C语言】扫雷游戏
c语言·算法·游戏
丘山录1 小时前
本地部署 LongCat-2.5,要 7 台机器我只有 4 台
人工智能·算法
Allen_LVyingbo2 小时前
NVIDIA CUDA Tile 在医疗计算中的算子形式化、算法重构与内核实现
大数据·python·算法·重构·知识图谱·健康医疗·数据库架构
冻柠檬飞冰走茶3 小时前
PTA刷题集之Airline Routes(C语言实现)
c语言·开发语言·数据结构·算法
宗旨飞翔3 小时前
不同的二叉搜索树
算法
736c3 小时前
固件开发岗算法
数据结构·算法
青少儿编程课堂3 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:倍增法求最近公共祖先LCA解析
c++·python·算法·bfs·信息学竞赛
冻柠檬飞冰走茶3 小时前
PTA刷题集之Battle Over Cities - Hard Version(C语言实现)
c语言·开发语言·数据结构·算法
吴长建先生重名了3 小时前
电商订单系统在Docker和Kubernetes中部署的高
算法