LLM信息抽取优化:两段式调用让成本降67%准确率达100%

LLM信息抽取优化:两段式调用让成本降67%准确率达100% 信息抽取是 LLM 应用开发里最常见的需求之一但很多项目一上来就用一次调用塞一堆字段定义结果准确率卡在七成左右prompt 越长成本越高。最近我看到的这个实验结论值得认真拆一下用两次 LLM 调用替代一次调用在相同抽取任务上成本降了 67%准确率从 72% 提升到 100%。这不是玄学也不是模型升级而是把“一次大而全的复杂抽取”拆成“宽召回 精校验”两段。对做文档结构化、数据清洗、批量字段抽取的开发者来说这个思路比纠结 fp16/fp32/bf16 这类数值精度问题更值得先看。下面按实际落地顺序把为什么有效、成本怎么算、步骤怎么拆、上线要盯什么完整讲一遍。1. 为什么“两次调用”反而比“一次调用”更好1.1 一次调用的问题提示词越长模型负担越重很多人默认一个规则任务越复杂就让一次提示词做更多事。信息抽取就是典型场景。比如要从一段合同文本里同时抽出甲方、乙方、金额、日期、联系方式、条款编号。一个调用里要写清楚字段定义、抽取规则、边界情况、输出格式还要面对文本里的同义词、特殊格式、缺失值。问题是模型在生成时要把所有这些约束都“记住”。要求越多越容易顾此失彼。实测里最常见的失败模式是字段 A 和字段 B 的定义相似模型把 A 的值填到 B 里。对模糊片段模型硬着头皮给一个“猜测值”而不是标记为不确定。输出格式漂移该是数组的地方输出字符串该是数字的地方输出带单位文本。源文本一长模型只抽到了前半部分后半段直接丢。这些并不一定是模型能力不够很可能是单次调用的任务粒度太粗。一个模型同时要做“理解文本”“识别候选”“做判断”“整理格式”四件事任何一环出错最终结果都会错。1.2 两次调用的分工先宽召回再精校验两段式把上述四件事拆成两个更聚焦的调用。第一段“宽召回”只负责定位。它把源文本里的候选片段先找出来例如所有“可能是姓名”的片段所有“可能是日期”的片段。这一段要求低门槛、高覆盖宁可多返回一些可疑项也不要漏掉真实目标。甚至可以让模型只返回片段和所在句子不做任何最终判断。第二段“精校验”只负责判断和格式化。它拿第一段的候选列表作为输入逐项判断“这个候选到底是不是目标字段的值”再把符合要求的候选整理成统一格式。两段各自的 prompt 都短任务边界都清晰。模型第一段不需要费劲输出最终结构第二段不需要在长文档里重新找一遍。错误率自然下降。这也是准确率能从 72% 提到 100% 的主要原因。1.3 成本账为什么两次短调用反而比一次长调用便宜这是很多人第一反应最不相信的地方多调用一次怎么会更便宜核心在于大模型按 token 计费成本由输入 token 和输出 token 共同决定。一次大而全的调用提示词里要写大量字段说明、格式要求、示例这些都要算输入 token。而且为了让模型理解边界往往还要给几条 few-shot 示例这部分开销很可观。两段式的第一段提示词很短输入就是源文本输出是候选列表第二段输入几乎只有候选列表和一句短指令输出是最终字段。源文本不会在第二段被重复发送除非校验需要局部上下文。用一个具体数量级来理解假设源文本 3000 token一次调用的提示词加 few-shot 约 1500 token输出约 800 token。两段式中第一段提示词约 300 token 加源文本 3000 token输出候选约 600 token第二段输入候选和短指令约 700 token输出约 200 token。一次调用总 token 约 5300两段式约 4800如果第二段再换一个便宜的模型费用差距会进一步拉大。注意标题里的 67% 是特定实验条件下的结果不同模型价格、不同文档长度会有差异。但数量级说明一个问题复杂任务用长 prompt 硬撑往往不是最便宜的方式。2. 什么样的抽取任务适合拆成两段2.1 适合两段式的任务特征不是所有抽取任务都要拆。根据实测经验以下特征同时出现越多两段式越划算待抽字段种类多字段之间存在语义重叠或近似比如“联系人”和“经办人”。源文本篇幅长比如合同、报告、公告、聊天记录、日志。目标字段对格式有严格要求比如手机号、身份证、日期、金额、统一社会信用代码。准确率要求高且希望错误能被定位到具体环节而不是最终结果一锅粥。抽取是离线批量任务对延迟没有硬性要求。2.2 不适合两段式的任务特征反过来这些情况先别拆字段就一两个短 prompt 一次就能稳定输出拆了反而增加延迟和复杂度。强实时交互场景比如用户在对话框里等结果两段串行调用会让等待时间翻倍。源文本极短且高度结构化本身不需要“找候选”直接做格式化判断即可。依赖全局上下文判断字段值候选片段脱离上下文无法判断。这种情况需要把相关上下文一起传给第二段成本优势会缩水。2.3 一个可以套用的判断标准我一般用三个问题判断要不要拆一次调用的 prompt 是否超过 1000 token是继续看下一条否大概率不用拆。目标字段类别是否超过 5 个是值得试否先跑一次调用看准确率。历史一次调用在测试集上的准确率是否低于 85%低于强烈建议试两段式高于说明任务简单拆了收益不大。这三条不需要全中两条命中就值得花半小时做一轮小样本对比。3. 落地步骤从单次调用改成两次调用3.1 第一段宽召回只找候选不判断第一段的 prompt 要刻意“放松”。目标是把可能相关的片段全部捞出来不要过早做判断。一个比较稳的写法是明确告诉模型“不要判断真伪不要输出最终字段”。让模型返回候选片段以及片段所在的原文句子。明确输出 JSON 结构方便后续解析。保留“所在句子”这一步很关键。因为第二段做判断时很多候选只有放在原句里才能确认比如“张三经理”“张三已离职”这种歧义。3.2 第二段精校验只判断候选不重新扫描第二段的输入不是源文本全文而是第一段的候选列表。这是两段式成本优势的关键。如果第二段又把整篇原文塞进去同时要求再抽一遍那就退化成两次完整调用成本翻倍准确率还不一定提升。第二段需要告诉模型三件事每个目标字段的精确定义。判断标准包括否定条件。输出 JSON 的确切字段名和格式要求。第二段 prompt 可以比第一段长一点但也要控制长度。判断标准写得越明确格式漂移的错误越少。3.3 用 JSON 锁住输入输出结构两段式抽取最容易翻车的地方是模型输出格式不稳定。所以两段的输出都要尽量用结构化输出能力或者至少强制 JSON 格式。第一段输出示例{ candidates: [ { fragment: 张三, sentence: 甲方张三于2024年签署本协议 }, { fragment: 张伟, sentence: 联系人张伟电话13800000000 } ] }第二段输出示例{ name: 张三, phone: 13800000000, date: 2024-03-15 }如果模型服务支持 response_format 或结构化输出一定要开。这能避免大部分 JSON 解析问题。3.4 最小可运行示例以下是一个兼容常见模型服务的 Python 示例只演示两段式结构具体接口参数以你使用的服务为准。import json from openai import OpenAI client OpenAI() # 按你的服务配置 base_url 和 api_key def recall_candidates(source_text: str) - list: system_prompt ( 你是信息标注助手。你的任务是从文本中找出所有可能是 “姓名”“电话”“日期”的片段。 不要判断真伪不要修改原文不要输出最终结果。 ) user_prompt ( 文本\n source_text \n\n 请返回 JSON格式为 {candidates: [{fragment: 原文片段, sentence: 所在句子}]} ) resp client.chat.completions.create( modelyour-model, messages[ {role: system, content: system_prompt}, {role: user, content: user_prompt}, ], response_format{type: json_object}, ) data json.loads(resp.choices[0].message.content) return data.get(candidates, []) def validate_and_format(candidates: list) - dict: system_prompt ( 你是信息抽取校验器。针对候选列表逐项判断它们是否 是目标字段的真实值并统一输出格式。 姓名保留原文格式电话只保留数字日期统一为 YYYY-MM-DD。 ) user_prompt ( 候选列表\n json.dumps(candidates, ensure_asciiFalse) \n\n 目标字段name姓名、phone电话、date日期。\n 请返回 JSON {name: ..., phone: ..., date: ...} ) resp client.chat.completions.create( modelyour-model, messages[ {role: system, content: system_prompt}, {role: user, content: user_prompt}, ], response_format{type: json_object}, ) return json.loads(resp.choices[0].message.content) source_text 联系人张伟电话13800000000签署日期为2024年3月15日。 candidates recall_candidates(source_text) result validate_and_format(candidates) print(result)注意这段代码里第二段并没有把源文本全文传进去而是传候选列表。这样成本才能在长文档场景下真正降下来。3.5 中间结果一定要留痕两段式比一次调用多了一个中间产物调试难度也随之上升。我的建议是第一段原始返回、第二段原始返回、最终结果、token 消耗、耗时全部记录到日志或数据库中。不要只在内存里跑。原因很简单最终结果错了你要能判断错在第二段的“判断”还是第一段的“召回”。如果没有中间日志就只能靠猜。4. 关键参数与实际部署要考虑的问题4.1 模型选择大模型做召回小模型做校验两段式给模型选择留了一个优化空间。第一段宽召回对语义理解要求高文本里可能有同义词、口语化表达、简称模型太小容易漏召回。所以第一段一般优先用能力更强、价格也更高的模型。第二段本质是“基于候选做判断 格式化”任务更轻小模型往往也能胜任。如果第二段用便宜的小模型整体成本会比两个大模型组合低不少。实际操作时我建议第一段固定用你已验证过的大模型第二段可以试两个候选模型做对比。判断标准是准确率下降不超过 1 到 2 个百分点而成本明显下降就可以选小的。4.2 超时、重试与并发两次串行调用意味着单文档等待时间约等于平时两倍。如果你的服务有超时限制一定要给第二次调用留足时间。通常我会把两段的超时都设置到比单次调用更长比如单次 30 秒两段各 45 秒。重试也要单独设计。第一段失败可以直接整体重试第二段失败如果不改第一段结果重试第二段即可。不要让重试把两段都重新跑一遍否则成本白白翻倍。不要把第一段和第二段的重试混在一起。两段各自维护独立的重试次数才能避免一次网络抖动造成整条链路重复计费。并发不要一上来就拉满。先确认模型服务的速率限制再按单文档耗时估算吞吐。批量任务更稳妥的做法是先单条跑通再开小并发看错误率和限流情况逐步上调。4.3 Token 统计和成本核算上线前必须做一次 token 对比。把基线方案一次调用和两段式在同样数据上的 token 消耗统计出来按输入和输出分别记录。项目一次调用两段式第一段两段式第二段两段式合计输入 token大源文本 短指令候选列表 短指令视文档长度而定输出 token较大候选列表最终字段一般小于一次调用失败重试成本整体重跑单独重跑单独重跑更低费用计算公式很简单费用 输入 token × 输入单价 输出 token × 输出单价不同模型输入和输出单价不一样有的差好几倍。所以不能只看 token 总数要看最终费用。如果两段用不同模型分别按各自单价计算。4.4 缓存和幂等抽取任务有两个常见问题同一文档重复处理以及网络重试导致同一条记录被抽两次。建议在任务入口做一层幂等控制用文档哈希或业务 ID 判断该文档是否已经处理过。处理成功就写入结果表处理失败才允许重跑。重试第二段时如果第一段结果没有变化直接复用第一段缓存。如果项目已经用了 LLM 编排框架或 Agent 流程这两段调用可以设计成两个独立节点中间缓存自然由流程引擎管理调试时也方便单独重跑某个节点。5. 效果验证不能只看最后一条准确率5.1 要看的指标两段式能不能推广要看四个维度字段级准确率每个字段最终值是否正确。召回率真实存在的目标值有没有被第一段捞出来。精确率捞出来的候选中真正是目标值的比例。格式正确率日期、电话、金额等是否满足统一格式要求。准确率只是一个总览真正定位问题要看召回率和精确率。指标定义关注点字段准确率正确字段数 / 总字段数最终质量召回率第一段找回的真实目标数 / 真实目标总数第一段是否漏精确率被判定为真的候选中真实为真的比例第二段是否误判格式正确率格式合法的字段数 / 输出字段数格式是否统一一次调用做对比时同样记录这四个指标。如果两段式的字段准确率更高但召回率没变说明提升主要来自第二段的判断和格式整理如果召回率也提升了说明任务拆分本身就让模型更容易发现候选。5.2 测试集怎么设计测试集不用大但要有代表性。我一般准备 50 到 100 条样本覆盖正常样本字段齐全、格式规范。缺失值样本某个字段完全不存在。多值样本同类型字段出现多个。歧义样本片段像目标字段但实际不是。长文本样本夹杂大量无关内容。每类至少 10 条。基线一次调用和两段式跑同一份测试集才能横向比。5.3 失败模式怎么归类拿到错误结果后先归到三类第一段漏召回真实字段没出现在候选里。第二段误判候选是错的但被确认为真。格式错误值对了但日期、电话、金额格式不统一。归类后分别处理。第一段漏召回优先放宽 prompt或把漏掉的类型补充到示例里第二段误判优先收紧判断标准增加否定条件格式错误优先在第二段追加格式规则或者用后处理正则做兜底。6. 常见坑点和排查顺序6.1 坑一第二段又变成“大而全”这是最典型的问题。写第二段时有人习惯把源文本全文又传给模型再加一句“按上次要求重新抽一遍”。这样做的结果是两段都在做全量抽取成本翻倍准确率并不会因为多跑一次而提升。正确做法是第二段的输入范围务必小于等于第一段的输出范围。如果确实需要更多上下文只补充候选片段所在的那一小段原文而不是全文。6.2 坑二候选片段脱离上下文第二段无法判断“张三”单独看很难判断是不是“甲方”。解决方法是第一段返回候选时带上所在句子第二段把句子一起传给模型。6.3 坑三JSON 解析失败即使开了结构化输出也存在个别模型版本不稳定的情况。建议在提示词里明确 JSON 结构和类型。开启 response_format 或结构化输出能力。解析失败时重试一次重试时提示词追加“必须返回合法 JSON”。仍然失败就记录原始内容进入人工兜底队列不要静默丢数据。6.4 坑四没有日志定位全靠猜两段式最忌讳的就是只打印最终结果。一旦批量任务出错没有中间结果排查时只能把整条链路重新跑一遍非常浪费时间。第一段和第二段的原始返回必须留档。6.5 排查顺序批量任务出问题时我按这个顺序查先看最终结果错的字段判断是值错、漏值还是格式错。再看第二段输入候选如果候选列表本身就缺这个字段问题在第一段。如果候选存在但最终输出错了问题在第二段的判断或格式化。看 token 统计如果两段式 token 比一次调用还多先检查第二段是不是把全文当输入了。看重试日志如果大量任务走了重试要区分是网络波动、超时还是模型返回格式问题。7. 边界什么时候保持一次调用什么时候考虑三段式7.1 不需要改的场景两段式并不是银弹。如果你的应用就抽一个字段源文本就几句话一次调用又快又准完全没有必要拆。两段式带来的延迟、日志、重试复杂度对简单任务来说是净负担。成本也不是越低越好。如果应用是高频交互用户等不了两段串行延迟那哪怕贵一点也应该保持一次调用或者考虑用缓存、并行等手段优化。7.2 超长文档可以进一步拆成三段源文本非常长比如几十页报告时两段式也可能不够。可以把流程扩展为三段分块召回把文档按段落或章节切块每块找出候选。跨块汇总把所有块候选合并去重排序。精校验对汇总后的候选判断和格式化。核心仍是同一个思路每段只做一件事。但每多一段失败点和延迟也会增加所以不要为了拆而拆。7.3 落地前做这三件事我的建议是准备切换之前先做三个动作用 50 到 100 条样本跑基线一次调用记录准确率和成本。用同样样本跑两段式记录准确率、成本、延迟、失败率。如果两段式准确率提升 5 个百分点以上或成本下降 30% 以上再正式切换。两个条件至少中一个再上。两个都不满足说明你的任务更适合原来的方案。信息抽取场景里真正拉低准确率的往往不是模型版本也不是 fp16/fp32/bf16 这类数值精度问题而是任务粒度太大让模型同时承担了太多职责。两段式的本质是把“查找”和“判断”分离让每次调用都专注一件事。这个思路在批量数据清洗、合同字段抽取、日志结构化、知识库构建等场景里都能复用。先把单任务跑稳再考虑批量和接口两段式的收益会看得更清楚。