十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

LLM信息抽取:两阶段粗定位+精提取,成本降67%准确率提升至100%

LLM信息抽取:两阶段粗定位+精提取,成本降67%准确率提升至100% 这次我们来看一个很反直觉的结论LLM 信息抽取不一定是“一次调用搞定”最好。一份实验结果给出的是把一次全量提取拆成两次分层调用成本反而降低了约 67%提取准确率从 72% 提升到 100%。这不是某个需要装机部署的开源软件而是一种 LLM 推理链路设计模式先定位候选区域再针对局部上下文精确提取。它可以直接用在合同关键字段抽取、发票/票据 OCR 后结构化、长文档信息抽取、RAG 预处理、知识库构建等场景。门槛取决于你接的是 API 还是本地模型接“OpenAI 兼容接口”不需要显卡本地部署则按模型规模准备显存。本文会把这个模式背后的成本逻辑、精度来源、代码实现和工程落地注意事项全部拆开。1. 核心能力速览能力项说明方法论类型两阶段 LLM 抽取链路粗定位 精提取核心收益相比单次全量提取成本下降约 67%准确率从 72% 提升到 100%实验场景适用任务长文档关键字段提取、票据信息结构化、合同要素抽取、RAG 文档预处理硬件需求不固定接 API 无显卡要求本地部署按模型规模要求 GPU 显存启动方式无需单独项目启动作为推理链路集成进业务代码接口方式调用 OpenAI 兼容 /chat/completions 接口或本地推理服务批量任务支持按文件循环 日志 失败重试即可适合读者做文档解析、ES/RAG 索引前置处理、业务系统信息抽取的工程师这里先强调一个适用边界这个模式的核心收益来自“长文档 少数关键字段”。如果文档只有几百字、字段又密集出现在同一段两次调用反而可能更慢、更贵。遇到短文本直接单次提取即可。2. 适用场景与使用边界最适合这种“两步调用”模式的场景有四个第一合同、标书、法律文书的关键字段抽取。这类文档往往十几页到几十页但你需要提取的只有合同编号、签订日期、甲方、乙方、金额等少量字段。单次全量输入模型要在几万 token 里抓住几个关键词既容易漏输入成本又高。第二OCR 结果的结构化。扫描件切出来的文本质量参差不齐直接全量大模型抽取噪声会被放大。先定位到包含目标字段的页面或段落再在局部做精提取正确率会稳定很多。第三RAG 前的文档预处理。你要从一批 PDF 里抽标题、作者、发布时间、摘要写入数据库供后续向量检索使用。这个场景对准确率要求高但字段数量少非常适合两次调用。第四知识库实体抽取。例如从调研报告里抽公司名、人名、金额、时间等实体按字段窗口局部抽取比全量输出更可控。不适合的场景也要说清楚短文本分类、情感分析、单段落问答。一次调用已经足够拆两次只会增加延迟。需要模型理解全文脉络的开放性问题。这类任务必须全量上下文局部窗口反而损失信息。对实时性要求极高的接口。两步调用天然多一次网络往返如果每一路必须 500ms 内返回需要谨慎设计。合规方面必须提醒处理合同、票据、个人隐私信息前确保你对该文档有处理授权抽取结果进入业务系统前要加人工复核环节。不要用未授权数据测试模型也不要拿接口去做超出正当业务范围的事情。3. 单次调用为什么会又贵又不准先看单次调用为什么准确率上不去。核心问题可以总结为三个上下文过长导致注意力稀释、无关文本引入噪声、输出格式不稳定。当一篇文章有几万 token 时模型在生成阶段会“顾此失彼”。业界常说的 lost in the middle 现象指的是LLM 对长上下文中间位置的信息记忆最差。而你要抽取的字段往往正好分布在文中不同位置前面有、中间有、后面也有。单次提取时模型要么漏掉靠后的字段要么把开头出现过的相似字段误填到别处。第二个问题是噪声干扰。全文里会有大量与目标字段无关的表述比如合同中会出现“如未按期付款每逾期一日按合同金额的 0.05% 支付违约金”这里的“合同金额”是一个计算引用不是你要抽取的签约金额字段。模型在全量上下文中很难区分“这个金额到底是不是字段值”。局部窗口可以极大减少这种歧义。第三个问题是成本。很多人只计算 prompt 的 token 数忽略了生成 token 同样计费。单次提取时如果要求模型“请阅读全文并提取所有字段”模型为了保证输出完整性往往会在字段之外附加解释、单位、补充说明输出 token 一下子膨胀。而两步法里第二步只针对小窗口输出一个字段值输出 token 极少。从材料看单次调用对少量关键字段的提取准确率只有 72%意味着每 10 个字段中约有 3 个会出错或缺失。这就是这个模式要解决的直接痛点。4. 两步调用设计粗定位 精提取两步调用的思路非常直接可以分为两个阶段。第一步候选区域定位。目标是回答“目标字段大概在文档的哪个位置”。这一步不一定要用 LLM通常用更便宜、更可控的手段正则表达式直接匹配“合同编号”“签订日期”“甲方”等字段名。关键词命中 行号定位按行扫描命中关键词后取该行前后 N 行作为候选窗口。小参数模型做文本分类判断某段落是否属于“合同主体信息”区域。如果文档已经按页切分甚至可以只定位到“第几页”。第二步局部精提取。把第一步筛出来的候选区域拼成小段文本交给 LLM 抽取字段。因为窗口小、噪声少、任务单一模型可以专注输出结构化结果。两种常见变体单字段独立窗口每个字段单独定位、单独抽取互不干扰。适合字段分散在不同章节的文档。多字段合并窗口如果多个字段落在同一段落比如发票上的发票号、开票日期、金额都在票面上可以一次性提取多个字段减少调用次数。设计原则就一条让 LLM 看到的上下文尽量短同时仍然覆盖目标字段的上下文信息。窗口不是越小越好需要保留字段名附近两个句子的语义否则可能出现“只有数值没有单位”的情况。5. Token 成本测算67% 的降本是怎么来的要理解成本下降的逻辑需要先建立一个直觉单次全量提取的 token 成本等于整篇文档长度两次提取的 token 成本约等于“字段候选区域长度之和”。以一份 5 万 token 的合同为例做测算方案输入 Token 数输出 Token 数相对成本单次全量提取 10 个字段50000约 1500含解释51.5 单位两步法定位阶段0正则/关键词00 单位两步法精提取 10 个字段每字段窗口约 2000 token20000约 1000纯字段值21 单位合计20000100021 单位42%这是上面例子里两步法相对单次调用的成本比例。实际场景中字段窗口通常可以更小500 到 1000 token而且多数文档不是每个字段都需要一上来就全文档扫描。如果单次全量输入是 10 万 token两步法合计只有 3 万 token节约就达到 70% 左右。标题中的 67%落在这一量级上。另外一个容易被忽略的降本点是输出长度。单次调用模型为了求稳经常输出“字段值说明……”式结构输出 token 翻倍。两步法因为 prompt 里明确要求“只输出值不输出解释”输出压缩到最低。所以 67% 的降本来源可以拆成两部分输入 token 减少占大头输出 token 压缩占小头。实际比例跟文档长度、字段数量、窗口大小直接相关。测算公式可以抽象为单次成本 文档总长度 × 单价 输出长度1 × 单价 两步成本 候选窗口总长度 × 单价 输出长度2 × 单价 节约比例 1 - 两步成本 / 单次成本字段越少、文档越长这个模式收益越大。6. 精度提升机制从 72% 到 100% 的关键准确率提升不是玄学可以归因到四个具体机制。第一上下文窗口变短后注意力更集中。模型不再需要在一整篇文档中分辨哪个“合同金额”是字段值只需要在小窗口里判断“这里唯一的金额单位是什么”。干扰项减少了歧义自然下降。第二schema 约束输出。在第二步的 prompt 里可以明确要求输出类型、单位、默认值规则。比如“如果字段不存在返回 null”“金额只保留数字和小数点”“日期格式统一为 YYYY-MM-DD”。这比单次提取时让模型自由发挥可靠得多。第三可以加入确定性校验。两步法天然带一个校验机会如果字段 A 在候选区域里没找到可以扩展窗口重试一次。而单次调用一旦漏掉你很难知道模型是“真没看到”还是“错误理解”。第四可解释性更好。因为每次提取都能回溯到“用的是哪个窗口、哪几行”出了问题可以直接检查原始区域是否正确而不是翻遍全文猜模型为什么错。从材料看该方案在测试集上达到了精确率 100%对比单次调用的 72%。但这里必须说明100% 是特定测试集上的数字生产环境的准确率取决于文档质量、字段复杂度和模型能力。你应该建立自己的验证集对比单次和两步法在你业务数据上的表现再决定是否切换。7. 代码实现示例一个可落地的提取管线下面给出一套通用实现模板。假设你的文档已经被切分成文本目标是提取合同编号、签订日期、甲方名称、乙方名称、合同金额五个字段。7.1 定义字段与关键词FIELDS [ 合同编号, 签订日期, 甲方名称, 乙方名称, 合同金额, ] FIELD_KEYWORDS { 合同编号: [合同编号, 合同号, 编号], 签订日期: [签订日期, 签署日期, 签约日期], 甲方名称: [甲方, 甲方名称, 甲方盖章], 乙方名称: [乙方, 乙方名称, 乙方盖章], 合同金额: [合同金额, 合同总价, 金额为, 人民币], }7.2 第一步候选区域定位from typing import List, Tuple def locate_windows(text: str, field: str) - List[Tuple[int, int]]: 用关键词命中 行号定位候选区域。 返回行号区间列表每个区间是包含目标字段的局部窗口。 lines text.split(\n) windows: List[Tuple[int, int]] [] keywords FIELD_KEYWORDS.get(field, []) for idx, line in enumerate(lines): if any(kw in line for kw in keywords): start max(0, idx - 2) end min(len(lines), idx 3) windows.append((start, end)) return windows这一步完全不调用 LLM成本几乎为零。如果你的文档已经按页切分可以改成返回页码再根据页码取对应页文本。7.3 第二步局部精提取import json import requests API_URL http://127.0.0.1:8000/v1/chat/completions # 替换成你的服务地址 MODEL_NAME your-model-name def extract_field_with_llm(window_text: str, field: str) - str: prompt f以下是文档局部内容 {window_text} 请从以上内容中提取“{field}”字段的值。 规则 1. 如果字段不存在返回 null 2. 金额只保留数字和人民币单位 3. 日期格式统一为 YYYY-MM-DD 4. 只输出字段值本身不要输出解释。 输出 payload { model: MODEL_NAME, messages: [ {role: user, content: prompt}, ], temperature: 0, } resp requests.post(API_URL, jsonpayload, timeout120) resp.raise_for_status() content resp.json()[choices][0][message][content].strip() if content.lower() null: return None return content这里有几个细节值得注意温度设置为 0让抽取结果尽量确定。prompt 里明确规则尤其是“不存在返回 null”避免模型强行编造。输出层只取message.content不取reasoning_content避免把思维链内容落入字段值。7.4 主流程封装def extract_all_fields(text: str) - dict: lines text.split(\n) result {} for field in FIELDS: windows locate_windows(text, field) if not windows: result[field] None continue window_text \n.join( \n.join(lines[start:end]) for start, end in windows ) try: result[field] extract_field_with_llm(window_text, field) except Exception as e: result[field] {error: str(e)} return result如果多个字段落在同一窗口可以把它们合并为一次调用减少网络请求量。例如先按行号把所有字段窗口汇总成若干个整体窗口再对每个整体窗口批量提取多个字段。7.5 批量任务与日志import os import json from pathlib import Path def batch_extract(input_dir: str, output_dir: str): input_path Path(input_dir) output_path Path(output_dir) output_path.mkdir(parentsTrue, exist_okTrue) for file in input_path.glob(*.txt): text file.read_text(encodingutf-8) result extract_all_fields(text) out_file output_path / f{file.stem}.json with out_file.open(w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2) print(f[完成] {file.name} - {out_file.name})批量任务建议“一文件一结果文件”处理完一个就写盘一个。这样中途失败时已经处理完的部分不会丢失重跑时也可以只处理未生成结果的文件。8. 接口 API 与批量任务接入示例代码里使用的API_URL是标准 OpenAI 兼容格式兼容大多数本地推理服务。如果你用的是本地 vLLM、Ollama 或其他兼容服务按以下方式接入curl http://127.0.0.1:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: your-model-name, messages: [{role: user, content: 请提取字段}], temperature: 0 }返回结构统一取choices[0].message.content。如果你的服务在https域名后面把API_URL改成对应地址并在请求头里带Authorization: Bearer token。批量任务工程化建议每个文件失败重试 2 到 3 次用指数退避不要一失败就抛异常。给每个提取请求加超时时间长文档的 LLM 推理可能超过 60 秒超时设 120 秒比较稳妥。结果文件里除了字段值建议同时记录“使用的候选窗口行号”方便回溯。接入消息队列Redis/RabbitMQ可以做到多消费者并行处理但对绝大多数内部工具场景单进程顺序处理已经足够。9. 资源占用与性能观察性能观察主要看三点延迟、输入 token 总量、输出 token 总量。单次全量提取 5 万 token在接口侧需要先对 5 万 token 做预填充耗时通常是秒级到十几秒两步法精提取只处理 2 万 token预填充时间更短。虽然网络请求多了一次但总等待时间通常更低。如果是本地部署开源模型显存占用取决于模型参数量而不是提取方法本身。7B 模型部署推理大约需要 14G 到 20G 显存取决于量化方式和上下文长度13B 到 70B 依次递增。这里不写死某个模型的显存值实际上取决于你选的模型、量化位数和输入 token 长度按本机环境实测为准。降低延迟和占用的手段把精提取温度设为 0减少随机性还可以开启max_tokens限制输出长度。合理设置候选窗口大小不要为了保险把窗口开得过大。500 token 左右的窗口通常足够遇到特殊情况再扩展。如果文档已经按页切分第一步直接返回页码第二步只把命中页码的文本送给模型token 量进一步缩小。对重复出现的文档模板可以把正则定位结果缓存同一模板后续只做一次定位。建议在日志里记录每次调用的 token 用量接口返回里通常带有usage字段累计统计后可以精确核算成本节约比例。10. 常见问题与排查方法问题现象可能原因排查方式解决方案定位阶段找不到候选窗口关键词不匹配文档实际表述打印文档文本查看字段实际写法扩充 FIELD_KEYWORDS加入同义词精提取返回 null窗口位置不对字段不在窗口内查看窗口对应原文扩大窗口的上下文行数字段值带多余说明prompt 约束不够明确查看模型原始输出强调“只输出值”关闭流式输出金额格式不统一文档中大小写混合检查 prompt 规则增加金额格式化规则或用正则后处理批处理中某个文件失败网络超时或单文件过长查看日志堆栈增加重试拆分超大文件接口返回 429并发请求过多查看服务端限流配置降低并发增加退避准确率还是不达标模型能力不足或窗口选择不合适抽样人工标注对比换成更强模型或把窗口扩展到所在章节本地部署显存不足模型参数量过大或上下文过长用nvidia-smi观察显存减小窗口使用量化模型或改用 API最值得优先排查的是“定位阶段输出”。两步法里第一步是基础第一步候选区域错了后面怎么调都救不回来。定位阶段最好先跑一个抽样脚本把每个字段命中的行号单独打印人工确认覆盖率。11. 最佳实践与使用建议第一从最小窗口开始。先给 2 行上下文测试发现提取不准再逐步扩展。窗口越大噪声越多精度反而可能下降。第二统一输出 schema。合同金额字段不要只提取“1000000”要明确单位和币种日期字段统一格式。把这类规则写进 prompt不要等模型自由发挥。第三建立验证集。拿 20 到 50 份真实文档做基准分别跑单次全量提取和两步提取统计字段级准确率和成本。没有验证集就不知道 67% 和 100% 在你的数据上是否成立。第四保留完整链路日志。每次提取记录文档名、字段名、候选窗口行号、模型输出。出问题的时候能直接回放是哪一步出错。第五缓存值得做。同一个模板的文档定位阶段结果相似可以把文档类型相关的关键词规则沉淀成配置减少重复开发。第六合规红线处理合同、票据、个人数据时确认数据来源授权输出结果进入业务库前设置人工复核环节。任何自动化抽取项目复核都是最后一道安全阀。12. 总结与下一步这个模式的核心价值在于把“信息抽取”从一次重负载任务拆成了两次轻量任务先用廉价方式缩小搜索空间再让 LLM 聚焦在局部上下文上做精确提取。结果是成本下降、精度上升而且实现难度很低不需要训练模型不需要更换基础设施只需要改推理链路。如果你想试建议先做两件事找一份真实长文档手动挑出 5 个字段先用单次调用提一次再按本文的两步法提一次对比字段值和 token 消耗。这个对比做完你就能判断这个方案在你的业务场景下值不值得切。最容易踩的坑是第一阶段的候选区域定位。如果关键词规则写得太死漏掉文档里的实际写法后面的精提取再准也没用。建议花时间把每个字段的常见表达方式收集完整再上批量任务。下一步可以扩展的方向把窗口定位从“关键词 正则”升级为小模型分类排序把多个相关字段合并进同一窗口减少调用次数在精提取 prompt 里加入文档模板类型让模型知道自己在读什么类型的文档。整个模式的灵活度很高值得在你的文档处理管线里试一轮。
返回列表