本节摘要:抽取与摘要是两种性格迥异的任务:抽取要的是逐字段的确定性与可解析性,摘要要的是覆盖度与忠实度的平衡。本节分别给出两者的建模方法:类型化签名的抽取管道、字段级指标的设计、摘要任务的双裁判评估,以及抽取任务特有的批量处理经济学。
抽取任务的验收标准天然严格:字段名错一个、日期格式偏一点、金额多个"约"字,下游系统就解析失败。这种严格性反而使它成为 DSPy 的舒适区——封闭的输出空间让规则指标可以直接发力。管道的骨架是类型化签名:
import dspy from typing import List, Optional class ExtractContract(dspy.Signature): """从合同文本中抽取结构化字段;文本中不存在的信息一律返回 null,禁止推测。""" contract_text = dspy.InputField(desc="合同全文") parties: List[str] = dspy.OutputField(desc="全部签约方的法定全称") effective_date: Optional[str] = dspy.OutputField(desc="生效日期 YYYY-MM-DD,未写明为 null") amount: Optional[float] = dspy.OutputField(desc="合同总金额,纯数字,未写明为 null") termination_clause: str = dspy.OutputField(desc="解约条款原文摘录,最多两句") class ContractExtractor(dspy.Module): def __init__(self): super().__init__() self.extract = dspy.Predict(ExtractContract) def forward(self, contract_text): return self.extract(contract_text=contract_text)
四个设计决定值得逐一说明。类型注解是解析保障:Optional[str] 让"未写明"有了合法表达,模型不再被迫编造日期来填表;List[str] 触发列表解析,多签约方不会挤在一个字符串里。"禁止推测"写进契约:抽取任务的黄金纪律是宁缺毋错——下游宁接受 null,接受不了错误值。摘录字段要求原文:termination_clause 抽原文而不是转述,转述引入语义漂移,原文可以回溯比对。单预测器起步:抽取任务链路宜短,多步抽取(先分类合同类型再抽字段)只在文本形态差异极大时才值得引入。
抽取的指标必须做字段级分解,因为总分掩盖不了字段特性的差异:
def contract_metric(pred, example, trace=None): score = 0.0 # 主体:集合语义,无序比对 if set(p.strip() for p in pred.parties) == set(example.parties): score += 0.4 # 日期与金额:字面语义,严格比对(null 对 null 也算对) if pred.effective_date == example.effective_date: score += 0.2 if pred.amount == example.amount: score += 0.2 # 摘录:包含语义,原文主体命中即可 if example.termination_clause[:30] in pred.termination_clause: score += 0.2 return score
权重设计反映业务代价:签约方错误最致命(权利义务主体错乱)占四成;日期与金额是结构化字段、错误即事故,但形态封闭、模型表现普遍较好,各占两成;摘录字段宽容度最高占两成。评估报告按字段分项输出——5.1 节的分项切片原则在抽取任务上落到字段粒度,哪类字段失分一眼可见。编译时这个指标同样供自举过滤用,字段级的严格性会传导进示范池:只有全字段合格的轨迹才配当示范,模型从第一天就学着"宁缺毋错"。
批量处理的经济学是抽取任务的特有话题。抽取通常面对整批文档,单篇成本 = 输入 token(全文)加输出 token(字段)加可能的解析重试。三个降本旋钮按效果排序:输入截断(合同的关键条款集中在前部与签署页,按版式定位截取,输入量常能砍半);模型分层(简单票据用小模型、复杂合同用大模型,路由器可以就是一个 Predict);解析失败重试的封顶(类型注解带来的自动重试很方便,但必须封顶并监控重试率——重试率持续偏高说明签名或输入有问题,不是运气问题)。
摘要与抽取性格相反:输出是自由文本,"正确"没有唯一标准。它的指标设计采用双裁判结构——忠实度裁判(摘要是否与原文一致,有无原文没有的断言)与覆盖度裁判(原文的要点是否被覆盖):
class FaithfulnessJudge(dspy.Signature): """判断摘要中的每个断言是否都能从原文推出。""" source_text = dspy.InputField() summary = dspy.InputField() faithful = dspy.OutputField(desc="true 或 false") class CoverageJudge(dspy.Signature): """判断原文要点被摘要覆盖的比例。""" source_text = dspy.InputField() summary = dspy.InputField() coverage = dspy.OutputField(desc="0 到 1 的小数") faith_judge = dspy.Predict(FaithfulnessJudge) cover_judge = dspy.Predict(CoverageJudge) def summary_metric(pred, example, trace=None): faithful = "true" in faith_judge(source_text=example.source_text, summary=pred.summary).faithful cover = float(cover_judge(source_text=example.source_text, summary=pred.summary).coverage) return (1.0 if faithful else 0.0) * (0.6 + 0.4 * cover)
乘法结构体现业务铁律:不忠实的摘要一票否决(乘以零),忠实前提下覆盖度才有意义——宁可摘要保守,不可无中生有。摘要编译的一个经验观察:与抽取不同,摘要从自举示范中获益显著(好的摘要范式可以模仿),从指令搜索中获益相对有限——评估主观性让指令的边际改善难以稳定测量。所以摘要任务的编译预算更多投在示范端与裁判质量端,裁判的人机对齐抽检(5.5 节)频次要高于封闭任务。