同一份提示词既让模型抽字段又让它写摘要,通常两头都做不利索:字段要求穷尽、命名固定、不许改写,摘要要求取舍、排序、控制篇幅,两个目标在解码时会互相拉扯。更稳的做法是拆成两条独立调用——一条只按字段清单取值,一条只按原文顺序整理要点;长文档先切分再逐段处理,最后在程序侧合并与比对。下面是可替换的提示词骨架,以及解析失败时的兜底代码骨架;字段名和切分阈值需要结合你的文档再改。
拆分的前提是输出要被程序消费:字段抽取走 JSON 约束加解析校验,摘要走要点顺序加段落编号引用。做法是把长文档切成带编号的块,让两类提示词各自处理同一块,程序侧再合并字段、拼接要点。验证方式是用同一批文档分别跑合并版和拆分版,逐条比对字段缺失位置和摘要要点漏项。边界在于:文档结构很乱或表格密集时,切分本身会丢信息,需要先人工确认切块结果。
把两类任务的提示词拆开写
拆分的第一条规矩是词汇隔离:字段清单型提示词里不出现「总结」「概括」「提炼」,要点顺序型提示词里不出现字段名和 JSON 结构。两者混写时,模型常把字段当成摘要的一部分,或者在摘要里塞进半截 JSON,解析和阅读都会出问题。
字段清单型(只做抽取)
你是字段抽取器,只输出 JSON,不输出任何解释性文字。
输入:一段带段落编号的文档片段。
输出字段(缺一不可,缺失时填空字符串或空数组,不要省略键):
- doc_title:string,原文标题
- parties:string[],文档涉及的主体名称
- effective_date:string,保持原文写法,不做格式转换
- amount:string,保留原币种与单位
- source_span:string,支撑上述字段的原文片段,不超过 80 字
规则:
1. 只依据原文,不推断、不补全、不做单位换算。
2. 每个字段都要能在 source_span 中找到依据,找不到就留空。
3. source_span 里带上段落编号,格式 [P012]。
要点顺序型(只做摘要)
你是摘要器,只输出要点列表,不输出 JSON,不输出字段名。
输入:一段带段落编号的文档片段。
输出:5-8 条要点,每条一行,格式 - [P012] 一句话要点。
规则:
1. 按原文段落出现的先后排列,不按重要性重排。
2. 每条要点带来源段落编号,一条只讲一个意思。
3. 原文没有的结论不写,也不要把多段压成一句泛泛判断。
4. 单条不超过 40 字。
| 维度 | 字段清单型 | 要点顺序型 |
|---|---|---|
| 必须包含 | 字段名与类型、空值写法、原文定位、禁止换算 | 条数区间、排列顺序、段落编号、单条字数上限 |
| 必须禁止 | 概括、推断、解释性段落、新增键 | 字段名、JSON 结构、按重要性重排、无来源结论 |
| 产出形态 | 单个 JSON 对象 | 纯文本行列表 |
| 失败信号 | 解析报错、必填键缺失、类型不符 | 缺段落编号、条数越界、顺序倒置 |
长文档先切分再逐段处理
超出单次可处理长度时,不要指望模型自己压缩后再抽取,压缩过程本身就丢字段。先切分,切分后每块都保留原文的段落编号,下游才能定位字段和要点来自哪一段。
- 按标题切:优先按文档一级、二级标题切,语义最完整;块长不均,超长小节再二次切。
- 按段落累加:逐段累加到字数阈值,不切断句子;阈值给模型上下文留出余量,具体数值需要结合模型实际可处理长度确认。
- 按固定长度切:只在无标题、段落也极长时使用,要留重叠窗口,重叠部分靠段落编号去重。
编号保留方式是切分前先打标:给原文每个段落加 [P001] 前缀,编号按全文递增,切块时不动编号,不因分块重新计数。跨段字段合并的规则建议分开定:标量字段取第一个非空值并记录来源编号;列表字段做并集去重;同一字段出现多个不同值时不要覆盖,保留候选并标 needs_review,交给人工或二次调用判定。
import re
def number_paragraphs(text, start=1):
paras = [p.strip() for p in re.split(r'\n\s*\n', text) if p.strip()]
return [f'[P{i:03d}] {p}' for i, p in enumerate(paras, start=start)]
def chunk_by_paragraphs(paras, max_chars=6000, overlap=1):
chunks, buf, size = [], [], 0
for p in paras:
if size + len(p) > max_chars and buf:
chunks.append('\n'.join(buf))
buf = buf[-overlap:] if overlap else []
size = sum(len(x) for x in buf)
buf.append(p)
size += len(p)
if buf:
chunks.append('\n'.join(buf))
return chunks
def merge_fields(parts):
merged = {}
for data, span in parts:
for k, v in data.items():
if v in ('', [], None):
continue
cur = merged.get(k)
if cur is None:
merged[k] = {'value': v, 'src': [span]}
elif cur['value'] == v:
cur['src'].append(span)
else:
merged[k] = {'value': [cur['value'], v],
'src': cur['src'] + [span],
'needs_review': True}
return merged
结构化字段的输出约束与解析
字段抽取能不能自动化,取决于模型返回的文本能否被稳定解析。除了上面那份字段清单,建议在提示词尾部固定一段输出约束,把格式要求写死在同一位置,便于替换和复用。
输出格式:只输出一个 JSON 对象,以左花括号开头、右花括号结尾。
不要输出 Markdown 代码围栏,不要输出「以下是结果」之类的前后缀。
键名严格使用上面给出的英文名,不改大小写,不新增键。
解析函数只做四件事:剥掉可能的代码围栏、反序列化、校验必填键、校验类型。校验不过就重试一次,把上一次的错误原因回填给模型;第二次仍失败就写入待处理文件并发出告警,不要静默丢弃,否则字段缺失会被误认为原文没有。
import json, re
REQUIRED = {'doc_title': str, 'parties': list, 'effective_date': str,
'amount': str, 'source_span': str}
def parse_fields(raw):
text = re.sub(r'^```(json)?|```$', '', raw.strip(), flags=re.M).strip()
try:
data = json.loads(text)
except json.JSONDecodeError as e:
return None, f'json_decode_error: {e}'
missing = [k for k in REQUIRED if k not in data]
if missing:
return None, 'missing_fields: ' + ','.join(missing)
for k, t in REQUIRED.items():
if not isinstance(data[k], t):
return None, f'type_error: {k}'
return data, None
def extract_with_retry(call_model, base_prompt, chunk, max_attempt=2):
prompt, err = base_prompt, None
for i in range(max_attempt):
raw = call_model(prompt, chunk)
data, err = parse_fields(raw)
if data is not None:
return {'ok': True, 'data': data, 'attempt': i + 1}
prompt = base_prompt + f'\n上次输出无法解析:{err}。请只输出一个 JSON 对象,不要加解释或代码围栏。'
save_dead_letter(chunk, err)
alert('field_extract_failed', err)
return {'ok': False, 'error': err}
两条路径的产出放在一起比对
拆分是否真的有用,只能拿同一批文档跑两条路径来比。固定样本、固定字段清单和要点条数,只改提示词组织方式,其余变量保持不动,结果才有可比性。
| 文档 | 路径 | 字段缺失位置 | 摘要要点漏项 | 解析失败次数 | 备注 |
|---|---|---|---|---|---|
| doc-01 | 合并版 | 如 amount,落在 P013 | 漏小节「违约责任」 | 2 | 重试后成功 |
| doc-01 | 拆分版 | 无 | 无 | 0 | 切块 3 段 |
| doc-02 | 合并版 | 如 parties,落在切分边界 | 要点顺序倒置 | 1 | — |
| doc-02 | 拆分版 | 如 parties 第 2 个主体 | 无 | 0 | 需看重叠窗口 |
抽查项按四条走:字段级逐条核对 source_span 能否在原文命中,命中不了记为无依据字段;位置级看缺失字段落在哪一段,是否正好是切分边界或重叠窗口;摘要级拿原文小节标题做覆盖清单逐条打勾,漏项记下小节名;顺序级看要点编号是否单调递增,出现倒序说明模型做了重排。判定不用模型打分,用规则即可:字段缺失条数、要点覆盖小节数、解析失败条数。切分边界附近的段落最容易出问题,抽查时优先看这些位置。