单次 LLM 调用做信息提取,准确率不够高还烧钱;改成两次调用后,成本反而降了 67%,准确率从 72% 拉到 100%。这不是玄学,是拆分任务、缩小模型输出范围之后带来的直接收益。这篇就拆解这套“两次调用替代一次调用”的 LLM 提取优化方案,讲清楚为什么更便宜、为什么更准,并给出一套可以直接改用的部署和测试流程。
如果你在处理合同解析、发票信息抽取、简历结构化、知识库入库这类场景,并且已经在用 LLM 做字段提取,这篇文章值得收藏。
1. 核心能力速览
| 能力项 | 说明 |
|---|---|
| 方案类型 | LLM 信息提取优化策略,两次调用协作完成提取 |
| 核心思路 | 第一次调用做粗提取,第二次调用做校验修正 |
| 成本效果 | 相比单次大模型全量提取,成本降低约 67% |
| 准确率效果 | 在材料对应测试中,从 72% 提升到 100% |
| 适用任务 | 结构化字段提取、实体抽取、文档解析、知识库入库 |
| 底层模型 | 不限定,可接 API,也可接本地部署的 LLM |
| 硬件要求 | 视模型而定;本地小模型可 CPU 推理,大模型建议 GPU |
| 是否支持 API | 支持,本质就是两次普通 LLM 接口调用 |
| 是否支持批量任务 | 支持,可写成批处理脚本或任务队列 |
| 适合读者 | 做 RAG、文档解析、数据清洗、自动化管线的开发者 |
这个方案的价值不在“模型”,而在“调用策略”。你不需要换更强的大模型,只需要把一次复杂提取任务拆成两次简单任务,让每次调用都专注于更小的范围,成本自然下降,准确率反而更容易控住。
2. 两次调用代替一次调用:为什么更便宜也更准
2.1 为什么“两次”反而比“一次”便宜
先说结论:两次调用的总成本低于一次调用,因为两次调用各自处理的 token 范围大幅缩小了。
单次调用的典型做法是这样的:把整篇文本塞给一个大模型,让它一次性输出所有目标字段。这个过程中,输入 token 可能占几百到几千,输出 token 因为要覆盖所有字段,也往往很长。更麻烦的是,模型为了“完整输出”,会把很多重话、补充说明、格式描述一起生成出来,这些都属于无效成本。
两次调用则把任务拆成两段。
第一次调用,用小模型或低成本的模型做“粗提取”。它只需要识别出字段所在的片段,输出一个较小范围的结构化结果,不需要输出长解释。这次调用输出 token 少,模型规模也可以小,成本很低。
第二次调用,把第一次的结果连同原文里对应的上下文片段交给一个校验模型,让它只做三件事:检查字段值是否合理、补缺漏字段、修正格式。这一次调用的输入是“原文局部片段 + 第一次提取结果”,输出只是修正后的差异部分,输出 token 同样远小于一次全量提取。
所以总 token 消耗比单次大模型全量提取少很多,模型单价也可能更低。从材料看,这套组合能把成本压到原来的三分之一左右,对应标题里“便宜 67%”。需要说明的是,这个比例和模型选择、文本长度、字段数量都有关,不能当固定结论用,但它下降的趋势是成立的。
2.2 为什么准确率反而更高
一次提取容易出错,是因为模型在长文本里同时要完成“理解全文 + 定位字段 + 生成结构化内容”三件事。任务复杂时,定位不准确、字段冲突、格式混乱都会发生,出错后又没有修复机制,最终准确率只能停在 72% 左右。
两次调用的设计天然解决这个问题:
- 第一次调用的目标非常窄:只做定位和初提取,不追求一次写对全部字段。
- 第二次调用的目标是修正:它能看到第一次的结果,能对着原文片段做针对性强校验。这就相当于给提取结果加了一层质检环节。
- 第二次输出的是差异修正,范围小,模型即使能力不强,也容易把任务做对。
一百个字段里挑出一个错误字段,比从零生成一百个正确字段简单得多。准确率从 72% 到 100%,本质上是把“一次生成正确”变成了“生成一次 + 修正一次”,后者的成功率自然高于前者。
2.3 这套思路的适用范围
从材料看,这个方案主要针对的是“从非结构化文本中提取结构化字段”的任务。典型特征是:输出字段固定、字段值来自原文、允许做核对验证。
如果你的任务符合这三条,它的收益最明显。如果任务是创意写作、开放问答、总结等没有固定答案的场景,就不能直接套用。
3. 适用场景与使用边界
3.1 适合谁用
这套方案适合以下人群:
- 正在做文档解析的开发者,需要从 PDF、Word、扫描件中提取合同字段、发票字段、简历字段。
- 做知识库入库的工程师,需要把非结构化文本批量转换成结构化条目。
- 使用 LLM 做数据清洗的数据工程师,需要从大量文本中抽实体、抽关系、抽属性。
- 在 RAG 链路里需要做召回后命中断言抽取的开发者。
3.2 能解决什么问题
第一个问题是成本。大模型按 token 计费,输出 token 往往比输入 token 更贵。减少输出 token 是控制成本最直接的手段。两次调用把每次输出都控制在小范围,整体输出量下降。
第二个问题是准确率。单次提取如果出错,往往只能加大模型、加提示词、再做多轮重试,每轮都是一次全量调用的成本。两次调用用一次低成本校验来解决,不依赖无限堆 prompt 技巧。
第三个问题是稳定性。第二次调用可以把输出格式强制规整到统一 schema,即使第一次输出乱,第二次也能修正。
4. 环境准备与前置条件
这套方案不是一个独立软件,而是调用 LLM 的一种方式,所以环境准备重点是模型访问和运行脚本。
4.1 模型选择
你可以选择两种路线:
- API 路线:使用 OpenAI、Anthropic、国内大模型厂商的 API,只需要申请 key,不需要显卡。
- 本地部署路线:使用 Qwen、Llama、DeepSeek 等开源模型,用 Ollama、vLLM 或 Transformers 加载,需要 Python 环境和 CUDA 环境。
如果你已经有可用的 LLM 服务,不管 API 还是本地,都可以直接跑通方案。建议第一次用小参数模型,减少资源压力。
4.2 通用环境清单
| 项目 | 要求 |
|---|---|
| 操作系统 | Windows / Linux / macOS 均可 |
| Python | 3.9 及以上 |
| 依赖库 | requests 或 openai 等 SDK |
| LLM 服务 | API 或本地 Ollama/vLLM 服务 |
| 网络 | API 路线需可访问对应服务;本地模型无需 |
| 磁盘 | 本地模型按大小需要数 GB 到数十 GB |
| 显存 | 本地大模型建议 8G 以上,小模型可 CPU 运行 |
如果走本地路线,先把模型服务起起来。以 Ollama 为例:
# 安装 Ollama 后拉取一个小模型,示例模型名需要替换 ollama pull qwen2.5:7b ollama serve这里强调一下,模型名、服务端口、请求格式要以你实际使用的框架为准。下面代码里的地址和模型名都是占位符。
5. 两步提取方案设计与核心实现
我用一个通用 Python 示例来说明完整流程。案例目标:从一段简历文本中提取“姓名、工作年限、技能列表、最近一份工作公司”四个字段。
5.1 第一次调用:粗提取
第一次调用只做一件事:把可能包含目标字段的原文片段找出来,并给出初步值。
import requests LLM_URL = "http://127.0.0.1:11434/api/generate" MODEL_NAME = "qwen2.5:7b" def first_extract(text): prompt = f""" 你是一个信息提取助手。请从下面的文本中提取字段:姓名、工作年限、技能列表、最近一份工作公司。 只输出 JSON,不要解释。 文本: {text} """ payload = { "model": MODEL_NAME, "prompt": prompt, "stream": False, "format": "json" } resp = requests.post(LLM_URL, json=payload, timeout=120) return resp.json()["response"]这一步的要点是:只要求输出 JSON,不要求模型做任何额外解释。如果模型输出格式不对,可以配合正则提取 JSON 片段,或者在 prompt 中给一个示例格式。
5.2 第二次调用:校验修正
第二次调用输入第一次的结果,同时只返回修正后的差异部分。
def second_validate(text, first_result): prompt = f""" 请校验下面这个信息提取结果,找出错误或缺失的字段。 原文: {text} 第一次提取结果: {first_result} 要求: 1. 对照原文核对每个字段。 2. 如果有错误,给出正确值。 3. 如果有缺失字段,补上正确值。 4. 如果都正确,返回一个空的 JSON 对象:{} 5. 只输出 JSON,不要解释。 """ payload = { "model": MODEL_NAME, "prompt": prompt, "stream": False, "format": "json" } resp = requests.post(LLM_URL, json=payload, timeout=120) return resp.json()["response"]第二次调用的 prompt 做了几件关键事:
- 明确告诉模型要“对照原文核对”。
- 要求“错误则修正,缺失则补全,正确则输出空对象”。
- 限制输出为 JSON。
这样第二次调用的输出 token 会非常少。绝大多数情况下,输出就是一个空 JSON 或只包含少数修改字段,成本自然可控。
5.3 合并最终结果
拿到两次调用的结果后,在代码里合并,而不是让模型再合并:
import json def merge_result(first_result, second_result): first = json.loads(first_result) second = json.loads(second_result) # 第二次结果就是修正或补漏的内容,覆盖到第一次结果上 merged = {**first, **second} return merged full_text = "张三,5年Python开发经验,精通FastAPI、Docker,目前在某某科技有限公司担任后端工程师。" first = first_extract(full_text) second = second_validate(full_text, first) result = merge_result(first, second) print(result)这里要注意:合并动作放在代码里做,不要再让 LLM 做一次合并。合并是确定性逻辑,不该让模型参与;重复调用只会增加成本和延迟。
5.4 成本估算参考
假设文本 1000 token,四个字段固定输出:
- 一次大模型全量提取:输入 1000 token,输出 300 token,输出很多是解释和格式内容。
- 两次小模型提取:第一次输入 1000 token,输出 120 token;第二次输入 300 token,输出 30 token。总量和单价都下降明显。
这里只是一个估算方式。实际成本你需要按模型单价、token 长度、字段数量重新算,但“单次全量输出 > 粗提取输出 + 校验修正输出”的规律是稳定的。
6. 功能测试与效果验证
这个方案不是装完就跑,而是需要一个评测集来确认它是否在你的数据上真的有效。
6.1 测试目的
验证两点:
- 准确率是否比单次调用提升。
- 成本是否比单次调用下降。
6.2 构造评测集
从一个更小的样本开始。准备 20 到 30 条真实文本,人工标注出正确结果。注意:这组数据必须是你自己业务里的真实样本,不要用测试生成的无意义文本。
6.3 测试流程
对于每条样本,分别执行单次调用和两次调用两种策略,记录结果、token 消耗、耗时。
# 可以用一个简单的目录结构来组织测试 data/ input/ # 原始文本 labels/ # 人工标注结果 run_single/ # 单次调用输出 run_two/ # 两次调用输出 cost_logs/ # 每次调用的 token 统计6.4 判断准确率
准确率的标准要提前定义好。常见做法是字段级准确率:抽取结果与标注结果完全一致才算该字段正确。
def field_accuracy(pred, label, fields): correct = 0 for field in fields: value = pred.get(field) expected = label.get(field) if isinstance(value, str): value = value.strip() if isinstance(expected, str): expected = expected.strip() if value == expected: correct += 1 return correct / len(fields)字段级准确率的好处是可量化、可比较。材料中 72% 到 100% 的提升,应该是在类似字段级标准下得到的,你要在自己的数据上用同一标准验证。
6.5 对比维度
| 对比项 | 单次调用 | 两次调用 |
|---|---|---|
| 字段准确率 | 记录 | 记录 |
| 输入 token 总和 | 记录 | 记录 |
| 输出 token 总和 | 记录 | 记录 |
| 总耗时 | 记录 | 记录 |
| 失败率 | 记录 | 记录 |
如果两次调用的准确率没有提升,优先检查第一次提取的 prompt 是否把要求写清楚了,以及第二次校验是否真的拿到了原文上下文。
6.6 判断成功与否
- 如果准确率提升超过 5 个百分点,且成本没有上升,方案成功。
- 如果准确率提升但成本上升,说明第二次调用模型的输出太啰嗦,需要继续压缩 prompt。
- 如果准确率没有提升,说明任务本身不适合拆成两步,回到单次调用再优化提示词。
7. 接口 API 与批量任务
如果要把这个方案接到业务系统里,可以直接封装成 API,或者用批量脚本处理目录文件。
7.1 封装成 API 服务
用 FastAPI 做一个轻量示例:
# requirements.txt: fastapi uvicorn requests from fastapi import FastAPI, Request from pydantic import BaseModel app = FastAPI() class ExtractRequest(BaseModel): text: str @app.post("/extract") def extract(req: ExtractRequest): first = first_extract(req.text) second = second_validate(req.text, first) return {"result": merge_result(first, second)} # 启动命令: # uvicorn app:app --host 127.0.0.1 --port 8000把第五节的三个函数导入进来,接口就可以直接跑通。注意这边的目标不是工程脚手架,而是说明“两次调用”逻辑可以完全嵌在业务 API 后面,对调用方透明。
7.2 批量任务设计
批量处理时,把文件列表读进来逐条处理,并在每步做日志记录:
import time import json def run_batch(input_dir, output_dir, log_dir): import os files = os.listdir(input_dir) for f in files: with open(os.path.join(input_dir, f), "r", encoding="utf-8") as fr: text = fr.read() start = time.time() try: first = first_extract(text) second = second_validate(text, first) result = merge_result(first, second) with open(os.path.join(output_dir, f + ".json"), "w", encoding="utf-8") as fw: json.dump(result, fw, ensure_ascii=False, indent=2) except Exception as e: with open(os.path.join(log_dir, f + ".err"), "w", encoding="utf-8") as ferr: ferr.write(str(e)) cost_time = time.time() - start print(f"{f}: {cost_time:.2f}s")批量任务需要关注失败率。如果某个文件第二次调用输出不是合法 JSON,可以做最多三次重试,每次重试把第一次结果加上“上次输出不是合法 JSON”的错误信息重新发给校验模型。
7.3 接口调用示例
如果是 curl 调用上面封装好的 API:
curl -X POST http://127.0.0.1:8000/extract \ -H "Content-Type: application/json" \ -d '{"text": "张三,5年Python开发经验,精通FastAPI、Docker,目前在某某科技有限公司担任后端工程师。"}'返回结构大致如下,实际字段以你的 schema 为准:
{ "result": { "姓名": "张三", "工作年限": "5年", "技能列表": ["Python", "FastAPI", "Docker"], "最近一份工作公司": "某某科技有限公司" } }8. 资源占用与性能观察
8.1 显存占用
如果使用本地模型,显存占用取决于模型大小。一个 7B 量化模型通常需要 6G 到 8G 显存,CPU 推理也能跑但速度慢。如果在两台机器上分别部署第一次和第二次调用所需模型,可以让两者并行处理不同文件,吞吐量更高。
显存占用要按你实际部署的模型来看。7B、14B、70B 的差距很大,不要拿某个模型的数值直接套用到别的模型。
8.2 Token 消耗观察
观察 token 消耗是这套方案落地最关键的步骤。要在每次调用前后记录:
- 输入 token 数。
- 输出 token 数。
通过对比单次调用的 token 总量,可以快速算出降本幅度。建议把日志写到 CSV 文件里:
timestamp,strategy,input_tokens,output_tokens,cost_estimate,status 2025-01-01 10:00:00,two_call,1300,150,0.003,success后续可以用 pandas 或 Excel 直接统计。
8.3 影响性能和成本的因素
- 输入文本长度:第一次调用的主要成本来源。
- 字段数量越多,第一次输出 token 越长,成本越高。
- 第二次调用输入不要用全文,只保留第一次结果和定位到的原文片段,否则成本会被拉高。
为了让第二次调用更省 token,可以在第一次 prompt 里要求模型同时输出“字段对应的原文片段”,第二次就只用这个片段做校验,不用整篇文本。
8.4 如何降低延迟
延迟方面,两次调用意味着平均多出一倍的网络往返或推理时间。如果两次调用都使用同一服务,可以尝试:
- 第一次用小模型,第二次用小模型或同模型,减少单次推理时间。
- 如果 API 允许并发,可以多线程处理不同样本。
- 本地服务可以用 vLLM 提升吞吐。
9. 常见问题与排查方法
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 第二次调用输出为空 JSON,但字段仍有明显错误 | 校验 prompt 没有引导模型发现错误 | 查看第一次结果和人工标注对比 | 在第二次 prompt 中增加“逐字段对照原文”的明确要求 |
| 成本没有明显下降 | 第二次调用输入仍使用了整篇原文 | 检查第二次调用输入的 token 数 | 第二次只传入第一次定位到的原文片段 |
| 模型输出不是合法 JSON | 模型能力不足或 prompt 未给示例 | 查看原始输出内容 | 加 JSON 示例,或用代码做格式修复 |
| 批量任务中途卡住 | 某条文本太长,请求超时 | 查看日志中的 timeout 报错 | 增加 timeout 时间,或按长度分片处理 |
| 第一次提取结果为空 | 文本格式与 prompt 假设不符 | 打印第一次 prompt 和输入文本 | 检查文本是否可以正常读取,调整 prompt 描述 |
| 本地模型推理速度很慢 | 模型较大或 CPU 推理 | 观察 CPU/GPU 占用 | 换小模型、量化模型,或加 GPU |
| 准确率提升不明显 | 任务不适合两步拆分,或第二次校验未生效 | 对比两次结果与标注的差异分布 | 先诊断错误类型,再决定是否继续使用该策略 |
10. 最佳实践与使用建议
第一次先用小数据集跑通全流程,不要直接上全量数据。30 条样本足够判断方案是否有效,跑一次也就几分钟。
第二次调用的 prompt 要重点设计。它决定了整个方案的准确率上限。建议把第二次调用的 prompt 写成类似“代码 review 任务”的语气:正在解决问题的模型,不需要重写所有逻辑,只需要检查改动点。这个角色设定能有效减少模型乱发挥的情况。
字段个数要克制。一次提取 20 个字段以上时,第一次调用的输出 token 会显著膨胀。如果字段很多,考虑按业务域拆成多个独立提取任务,各自做两次调用。
合并逻辑必须放在代码里。让第三次 LLM 调用去合并结果,既增加成本,又引入新的不确定性。
成本估算不要只看模型单价,还要看输出 token 的实际长度。有些模型输出冗长解释,即使单价低,总 cost 也不低。通过 token 日志持续监控是唯一可靠的方式。
涉及人脸、声音、肖像、版权素材的数据,在进入任何 LLM 调用链路之前,必须先确认数据来源合法、处理授权清晰,尤其是批量解析合同、简历、发票这类包含个人敏感信息的场景。测试环境建议用脱敏数据,生产环境要控制数据访问范围。
11. 总结与下一步
这套“两次 LLM 调用代替一次调用”的方案,核心价值有两个:一是用更细的任务拆分降低输出 token,从而压成本;二是用一次低成本的校验环节提升准确率。它不依赖某个特定模型,API 路线和本地部署路线都能跑。
如果你现在面对的是长文本、固定字段、重复性高的提取任务,建议先做三件事:
- 准备 30 条带标注的真实样本。
- 分别跑单次调用和两次调用。
- 对比字段准确率和 token 消耗。
最容易踩的坑也提前说清楚:第二次调用千万不要把整篇原文又丢进去,那样成本不会有明显下降;字段一定要固定,输出格式一定要在代码里校验。
后续如果想继续优化,可以在这个框架上叠加更多工程手段,比如基于规则的预筛、多模型投票、结果缓存、字段置信度打分。先把“两次调用”这条基线跑通,后面的优化都会更可控。