ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

两次LLM调用反而更省钱?信息提取的两阶段设计实战

两次LLM调用反而更省钱?信息提取的两阶段设计实战 在做 LLM 信息抽取类应用时一个很现实的问题摆在面前想让准确率更高往往要堆更大的模型、更长的上下文、更多的 few-shot 示例结果就是成本水涨船高反过来想省钱模型又频繁漏字段、输出幻觉内容。最近看到一个很有意思的实验结论Two LLM calls beat one意思是“两次 LLM 调用反而比一次调用更好”。在结构化字段提取场景里这个方案比单次调用便宜约 67%提取准确率还能从 72% 提升到 100%。乍一听有点反直觉但拆开来看其实是任务拆分和模型选择的问题。这篇文章会围绕这个主题展开先解释为什么一次调用会“既贵又不准”再拆解两次调用的三种设计模式然后给出一套完整的 Python 实现包括代码、成本估算和常见坑点。适合正在做 LLM 信息提取、文档解析、简历解析、发票识别等应用的同学阅读。1. 背景一次 LLM 调用为什么不够好1.1 信息提取任务的真实痛点信息提取是 LLM 应用中最常见的任务之一比如从 PDF 中提取合同关键条款、从简历中解析候选人信息、从发票中识别金额和税号。这类任务看起来简单实际落地时却会遇到几个共性问题。第一个问题是上下文过长。一份真实合同可能几十页虽然 LLM 的上下文窗口不断变大但塞入超长文本后模型对文本中间部分的信息提取能力会明显下降出现“中间信息遗忘”现象。为了覆盖完整信息又不得不截断或分块结果可能切碎跨段落的语义反而增加了提取难度。第二个问题是字段冲突与信息噪声。长文本里往往同时存在大量与目标字段无关的内容比如合同里既有“违约金”也有“违约金上限”既有“总金额”也有“预算金额”。如果直接把所有文本倒给模型模型很容易被无关信息带偏输出一个看似合理但不是目标字段的值。第三个问题是幻觉与格式漂移。当你要求模型输出 JSON 时模型虽然能看懂指令但在长上下文压力下它可能漏掉某个字段或者把字段名从delivery_date改成deliveryDate造成下游解析失败。每次解析失败都意味着需要重试或人工修正成本和延迟都会上升。单次调用看似节省了“第二次请求”的额外延迟实际上把所有压力都压到了同一个模型上。既要理解全文又要排除噪声还要严格按格式输出任务太多准确率和成本自然都很难兼顾。1.2 “两次调用”的实验结论“Two LLM calls beat one” 的核心不是简单地把同一个任务问两遍而是把“提取”拆成两个独立子任务每个子任务只做一件事。这样每个模型的职责变简单了模型就更容易把一件事做好。在结构化字段提取实验里对比结果大致如下方案提取准确率相对成本单次调用72%100%两次调用候选定位 精炼提取100%33%这里的“提取准确率”是指测试样本中模型输出 JSON 与人工标注完全一致的样本占比“相对成本”则按输入和输出 token 总和折算。当然这不是一个适用于所有任务的绝对结论而是一个特定场景下的实验数据。它说明的是在信息提取类任务里“先定位、再精炼”的两阶段方式有潜力做到既省钱又更准。为什么会出现这种反直觉结果关键在于两个数字一是第一次调用使用的模型更小、更便宜二是第二次调用的输入不再是整篇长文本而是第一次调用筛选出的少量候选段落。真正喂给模型的 token 总数反而比单次调用少得多。这也提醒我们LLM 应用的成本和准确率并不取决于“调用次数”而取决于“喂给模型的 token 总量”和“单次任务的复杂度”。2. 核心设计两次调用的三种拆分模式两个 LLM 调用的具体配合方式并不唯一。下面列举三种最常见的模式实际项目里可以互相组合使用。2.1 模式一候选定位 精炼提取这种模式也可以理解为“先粗后细”。第一次调用使用一个便宜的小模型从长文本中快速筛出与目标字段相关的候选片段第二次调用再把候选片段作为上下文使用更强或同等规格的模型输出结构化结果。适用场景是全文过长、目标字段只出现在少数段落、一次要提取多个字段比如合同要素提取、发票解析、简历解析。它解决的核心问题是避免大模型阅读整篇文本。第二次调用只需要阅读候选片段上下文大幅缩短既能降低成本也能减少噪声干扰。这种模式也是本文实战部分要实现的方案。它在工程上最容易理解对成本结构的改善也最明显。2.2 模式二置信度级联置信度级联适合同时处理大量简单样本和少量困难样本的场景。第一次先用小模型提取结果并让模型同时输出一个置信度分数。如果置信度高于阈值直接采用如果置信度低再调用大模型重新提取。适用场景包括客服工单分类、资料初审、大量短文档的批量信息抽取。它解决的核心问题是让大模型只处理小模型没把握的少数样本。简单样本走便宜路线困难样本走高质量路线整体成本接近“全用小模型”准确率则接近“全用大模型”。实现时需要注意置信度分数由模型自己输出并不一定可靠。一般要在测试集上观察置信度与真实准确率的关系找到合理的截断阈值而不是拍脑袋定一个 0.8。2.3 模式三生成 校验第一次调用生成一个初始结果第二次调用换一个模型或换一个视角对结果做校验并修正。这种模式适合“不允许出错”但“结果可验证”的场景比如数据入库前的字段清洗、代码生成后的语法检查。它解决的核心问题是降低幻觉。单个模型在生成时可能“自信地犯错”但第二个模型在只看结果、不看原文时更容易发现矛盾字段比如日期格式错误、金额范围异常。不过需要提醒的是模式三会额外增加一次调用成本不一定下降。它更适合把“准确率红线”放在“成本优化”之前的场景或者用于最终结果的人工抽检前自动初筛。3. 成本拆解为什么两次调用反而便宜成本是很多人对这个方案最大的疑问两次调用怎么可能比一次调用便宜下面把成本公式拆开看。3.1 LLM 调用成本公式目前多数商用 LLM 的计费方式是按 token 计费大致可以写成单次调用成本 输入 token 数 × 输入单价 输出 token 数 × 输出单价输出 token 单价通常比输入 token 更贵。每多做一次调用确实会增加一次固定请求开销但如果每次调用的 token 总数远小于单次调用总成本仍然可能下降。3.2 两阶段方案的成本构成假设原始文档是 10000 个 token需要提取 3 个字段单次调用需要把 10000 token 全部输入模型可能还要输出 300 token成本按 10300 token 计算。两阶段第一步用小模型扫描全文输入 10000 token输出 300 token 候选片段。但这部分小模型单价可能是主力大模型的 1/5 甚至更低。两阶段第二步把第一步筛选出的约 1000 token 候选片段输入大模型再输出 300 token 结构化结果。大模型只处理 1000 token 输入而不是 10000 token。可以看到虽然做了两次网络请求但大模型的输入 token 从 10000 降到了 1000小模型的输入虽然接近 10000但单价极低。综合算下来总成本就能降下来。3.3 一个示例成本对比表下面是一个示意性的估算表单价按社区常见的价格量级设置真实价格请以你的 API 服务商为准计费项单次调用方案两阶段方案大模型输入 token100001000大模型输出 token300300小模型输入 token010000小模型输出 token0300估算成本约 10.3 单位约 3.4 单位按这个模型两阶段方案的成本是单次调用方案的 33% 左右正好节省约 67%。如果小模型的单价做到大模型的 1/5节省比例还会更高。当然这个计算成立的前提是候选定位环节能够把大模型的输入压缩到足够小。如果候选片段仍然覆盖了全文的 80%那两阶段方案的收益就会变得不明显。4. 实战两阶段 LLM 信息提取完整实现理论知识讲完下面用一个可运行的案例把方案落地。我们采用“候选定位 精炼提取”模式。4.1 场景与需求假设我们需要从一份项目需求文档中提取三个字段deliverables交付物清单deadline交付截止日期owner最终负责人文档比较长包含背景介绍、技术方案、人员安排、风险分析等多个段落目标字段分散在不同位置。4.2 环境准备本文示例使用 Python 3.10 以上版本需要安装 OpenAI Python 库pip install openai如果你想在本地估算 token 数量可以再安装一个可选库pip install tiktoken如果你使用的是其他 LLM 服务只要兼容 OpenAI Chat Completions 格式也可以直接复用代码。如果 SDK 版本不同请求参数可能需要微调。4.3 第一次调用候选定位第一次调用的目标是找出文档中与目标字段相关的候选段落。为了减少后续解析成本这里直接让模型输出 JSON 数组数组元素是原文中的段落编号。# 文件路径extract.py import json from openai import OpenAI client OpenAI(api_keysk-xxxx) def build_candidate_prompt(field_names: list[str], document: str) - str: fields_text 、.join(field_names) return f 你是一个文档检索助手。请阅读下面这段项目文档找出所有与以下字段相关的段落 {fields_text} 要求 1. 只输出段落编号组成的JSON数组例如 [0, 3, 7]。 2. 如果你认为某个字段在文档中没有对应内容不要硬找。 3. 不要修改或复述原文只输出JSON。 文档如下 {document} JSON: 这个提示词有几个设计要点要求输出段落编号而不是直接输出候选文本是为了减少第一次调用的输出 token要求只输出 JSON是为了让后续解析更稳定。4.4 第二次调用精炼提取第二次调用接收第一次筛选出的段落编号从原文中取出对应段落作为精炼提取的上下文。def build_extract_prompt(segments: list[str]) - str: segment_text \n\n.join( f[段落 {i}]\n{seg} for i, seg in enumerate(segments) ) return f 请从下面的候选段落中提取项目信息返回一个JSON对象。 字段说明 - deliverables: 交付物清单数组格式 - deadline: 交付截止日期格式为YYYY-MM-DD - owner: 最终负责人名称字符串 候选段落 {segment_text} 要求 1. 只输出JSON对象不要输出其他内容。 2. 如果某个字段无法从候选段落中提取使用null。 3. 字段名严格使用deliverables、deadline、owner。 JSON: 第二次调用的提示词比第一次更严格因为这一步直接决定最终输出质量。通过字段说明和格式要求模型被约束在很小的输出空间里犯错的概率会明显下降。4.5 完整调用与结果解析接下来把两次调用串起来。核心逻辑分四步分割段落、候选定位、拼接候选段落、精炼提取。def extract_fields(document: str, client: OpenAI) - dict: # 1. 分割文档为段落记录段落编号 segments [seg.strip() for seg in document.split(\n\n) if seg.strip()] # 2. 第一次调用候选定位 candidate_prompt build_candidate_prompt( [交付物, 交付截止日期, 负责人], document ) candidate_resp client.chat.completions.create( modelgpt-4o-mini, messages[{role: user, content: candidate_prompt}], temperature0, ) candidate_idx json.loads(candidate_resp.choices[0].message.content) # 3. 根据段落编号取回候选段落 candidate_segments [segments[i] for i in candidate_idx if i len(segments)] # 4. 第二次调用精炼提取 extract_prompt build_extract_prompt(candidate_segments) extract_resp client.chat.completions.create( modelgpt-4o, messages[{role: user, content: extract_prompt}], temperature0, response_format{type: json_object}, ) result json.loads(extract_resp.choices[0].message.content) return result这段代码有几个关键点第一次调用使用gpt-4o-mini这种便宜的小模型第二次才使用gpt-4o。如果实际环境里没有这两个模型可以替换成对应服务商提供的轻量模型和主力模型。两次都设置temperature0保证输出尽可能稳定。第二次调用使用response_format要求 JSON 输出可以大幅减少格式解析错误。如果你的模型不支持该参数需要在提示词里更明确地强调“只输出 JSON”。4.6 运行与验证用一段模拟文档测试if __name__ __main__: doc 项目背景本项目旨在建设一个企业级的统一日志平台用于解决日志采集、存储、检索和分析问题。 技术方案平台采用微服务架构组件包括日志采集器、消息队列、存储引擎和可视化控制台。 交付物包括统一日志平台系统、部署文档、运维手册以及测试报告。 时间计划项目建设周期为6个月预计在2026年6月30日前完成交付。 负责人项目由张伟担任技术负责人李娜担任产品负责人王强负责系统运维。 result extract_fields(doc, client) print(json.dumps(result, ensure_asciiFalse, indent2))预期输出{ deliverables: [ 统一日志平台系统, 部署文档, 运维手册, 测试报告 ], deadline: 2026-06-30, owner: 张伟 }这里的示例文档很短实际使用时可以把document替换成更长的真实文档。建议在你的 API 账号上运行这段代码观察第一次调用筛选出来的段落是否准确以及第二次调用的输出是否满足字段要求。5. 常见问题与排查思路把两次调用方案引入生产项目后大概率会遇到下面几个问题。5.1 第一次调用漏掉关键段落怎么办如果小模型在候选定位阶段漏掉了包含目标字段的段落第二次调用无论多强也无法找回最终结果就会出现null字段或者提取到错误信息。问题现象常见原因解决思路输出字段为 null候选定位没有召回目标段落调大第一次调用的输出范围允许输出更多候选段落编号字段来自错误段落候选范围过宽噪声段落混入在提示词中增加相关性要求或让模型先输出“相关原因”再输出编号更稳妥的做法是第一次调用的提示词里不要只列字段名要补充字段出现的上下文特征。比如“交付物”常以“交付物”“交付内容包括”等形式出现把这些线索写进提示词召回率会明显提升。5.2 第二次调用仍然输出幻觉字段即使候选段落正确大模型也可能在提取时“脑补”出一个原文里没有的值。这通常是因为模型被训练时见过的类似文本带偏了。解决思路有两个方向一是约束输出格式使用 JSON Schema 或response_format限制字段名和类型二是在结果解析层增加校验逻辑比如日期格式校验、枚举值白名单校验。校验失败时再触发一次修正调用或者把该样本标记为需要人工确认。5.3 成本没有下降反而上升两阶段方案的成本优势依赖“候选定位后输入大幅缩小”。如果第一次调用后候选段落仍然很多第二次调用的输入就不会减少总成本甚至可能超过单次调用。可以尝试的优化是在第一次调用结束时让模型输出两组候选一组是“高置信段落”一组是“低置信段落”。第二次只处理高置信段落低置信段落再单独走一轮小模型复查。这样可以在控制成本的同时保留对困难样本的召回能力。5.4 如何验证准确率是否真的提升不要只看一两条样例就下结论。建议准备一批有标注的测试集分别运行单次调用和两阶段方案统计字段级正确率、JSON 解析成功率和整体成本。跑一轮之后再决定是否切换方案。简单来说验证流程是准备测试集 - 跑单次调用 - 跑两阶段方案 - 对比准确率和成本 - 分析失败样本。只有把失败样本摊开看才能知道问题出在候选定位还是精炼提取。6. 工程最佳实践6.1 任务拆分原则两阶段方案的本质是把“读懂全文”和“结构化输出”拆成两步。拆分时要注意第一个模型做“定位类”任务第二个模型做“生成类”任务。不要把一个复杂任务拆成两个简单任务后提示词里还是要求第一个模型顺带完成第二个模型的工作。如果第一次调用既要做候选定位又要输出结构化字段那其实又回到了单次调用的老路任务复杂度并没有下降。6.2 模型选型建议第一次调用追求“便宜、快、召回率高”优先考虑小参数模型比如gpt-4o-mini或同类轻量模型。第二次调用追求“指令跟随能力强、格式化稳定”使用主力模型即可但由于输入已经缩小成本压力不大。这里要注意小模型不是随便选的。建议把候选定位的召回率作为模型评估指标而不是只盯着单价。如果一个便宜模型经常漏召回后续修正成本可能超过省下来的费用。6.3 结构化输出与校验结构化输出是降低解析成本的利器。建议在第二次调用的提示词中给出完整 JSON 示例并要求模型只输出 JSON不要输出解释文字。下游解析时可以使用jsonschema库或者自己写一个小校验器。字段缺失时保留null并记录日志不要直接抛异常中断整个任务。这样批量处理时即使遇到个别坏样本也能先把整体流程跑完。6.4 缓存与可观测性在成本敏感的批处理场景可以对相同输入的候选定位结果做缓存避免重复调用。很多信息提取任务会反复处理同一批文档比如每天跑一次增量更新缓存能显著降低成本。同时也建议记录每次调用的输入 token、输出 token 和耗时统一汇总到日志系统。没有成本观测的优化很难判断是不是真的省了 67%。建议用一张表记录每次实验的模型、token 数、耗时和准确率方便后续比较。6.5 生产环境注意事项所有 API 密钥放在环境变量或密钥管理服务中不要硬编码在代码里。批量执行前先用小样本验证确认准确率和成本符合预期。对模型返回结果做异常捕获一次解析失败不应当让整个任务崩溃。如果涉及用户敏感数据注意脱敏和权限控制不要让模型能力成为数据泄露的入口。7. 总结与下一步回到标题“Two LLM calls beat one”。这个方案并不是让所有人盲目地把一次调用改成两次而是提供一个思路LLM 应用的成本和准确率更多取决于任务拆分是否合理以及每个子任务是否匹配了恰当的模型规格。在信息提取场景里“候选定位 精炼提取”的组合既能减少大模型需要阅读的 token 量又能降低噪声干扰最终实现更低的成本和更高的准确率。如果你正在被单次大模型调用的成本和幻觉问题困扰可以先在自己的数据集上跑一组对比实验记录成本、准确率和失败样本再决定是否把两阶段方案应用到生产环境。下一步可以继续学习的方向包括LLM Agent 中的多步骤工具调用、RAG 场景下检索与生成的配合、结构化输出约束的工程化实践。这些都是“让 LLM 在真实业务里更可控”的核心能力和本文的两阶段思路是一脉相承的。如果本文对你有帮助可以收藏备用也欢迎在评论区聊聊你遇到过哪些“单次调用不划算”的场景。
返回列表