news 2026/4/17 3:49:16

招投标文件结构化:为什么不要全文直抽?先切块再按模块定义输入输出(附GitHub项目地址)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
招投标文件结构化:为什么不要全文直抽?先切块再按模块定义输入输出(附GitHub项目地址)

项目介绍:这是一个面向投标/评标场景的结构化抽取工具。支持上传PDF、Word或Excel格式的招标文件,自动提取项目基础信息、投标资格、技术与商务要求、评标办法等关键条款,并还原目录层级与跨页表格。输出结构化JSON/Excel,适用于招标文件智能生成、AI辅助评标及招投标知识库建设。

GitHub项目地址:https://github.com/intsig-textin/xparse-sample-projects

接下来我们主要讨论一件事:如果目标是从一份很长的招标文件里稳定产出结构化结果,系统应该怎么搭。重点不是场景背景,而是中间层怎么定义、任务怎么拆、Prompt 为什么这样写。

一、先把目标定义清楚

如果只是让大模型“总结一份招标文件”,实现并不难;难的是把一份上百页的长文档稳定拆成可展示、可复用、可继续治理的结构化结果。

这类工具真正要完成的是下面这条链路:

  • 上传 PDF 招标文件

  • 调用 TextIn 把原始文件转成markdown + pages

  • 按标题把长文档切成多个语义片段

  • 把片段路由到基础信息、资格要求、评审办法、投标递交、无效标风险、附件格式 6 个模块

  • 每个模块单独调用大模型,输出固定 JSON

  • 前端直接按模块渲染,后续也可以继续导出或复用

所以这里的核心不是“抽字段”三个字,而是先把长文档抽取问题拆成多个边界明确的小任务。

二、架构应该怎么拆

如果目标是长文档结构化,推荐把链路拆成四层:

这四层分别解决不同问题:

  • 解析层:把 PDF 变成后续可计算的中间结构

  • 编排层:把长文档拆成多个上下文更小的任务

  • 抽取层:让每个模块只输出自己负责的 schema

  • 展示层:按模块 JSON 渲染,而不是再做一次自由解析

三、先把解析层的输入输出定义对

这里最容易写错。真正调用的不是form-data接口,而是 TextIn 的二进制流解析接口:

POST https://api.textin.com/ai/service/v1/pdf_to_markdown

请求头和请求体在代码里是这样组织的:

headers = { "x-ti-app-id": TEXTIN_APP_ID, "x-ti-secret-code": TEXTIN_SECRET_CODE, "Content-Type": "application/octet-stream", } params = { "parse_mode": "auto", "page_count": 200, "dpi": 144, "table_flavor": "html", "apply_document_tree": 1, "markdown_details": 1, "page_details": 1, "apply_merge": 1, "paratext_mode": "none", } resp = await client.post( "https://api.textin.com/ai/service/v1/pdf_to_markdown", headers=headers, params=params, content=file_bytes, )

这里的关键点只有两个:

  • 返回值里最重要的是result.markdown

可以把它理解成下面这个输入输出契约:

输入:

Headers: - x-ti-app-id - x-ti-secret-code - Content-Type: application/octet-stream Query: - parse_mode=auto - page_count=200 - dpi=144 - table_flavor=html - apply_document_tree=1 - markdown_details=1 - page_details=1 - apply_merge=1 - paratext_mode=none Body: - PDF 文件的原始字节流

输出:

{ "code": 200, "result": { "markdown": "# 第一章 招标公告\n...", "pages": [] } }

如果你做的是 Web 工具,通常会在本地后端再包一层/api/parse方便浏览器上传和鉴权隔离;但那只是工程封装,不是上游解析接口本身的协议。

四、为什么中间层必须是`markdown+pages`

这一步决定了后面能不能把长文档拆稳。

markdown的价值在于:

  • 保留标题层级

  • 保留段落结构

  • 表格可以以 HTML 或 Markdown 形式继续消费

  • 便于按标题、按章节做切块

pages的价值在于:

  • 保留分页语义

  • 为页面预览、页码回跳、证据定位留接口

  • 后续如果要做高亮溯源,不需要重新回到 PDF 二进制层

也就是说,解析完成之后,整个系统处理的对象就不再是 PDF,而是markdown+pages这个统一中间层。

五、长文档不要全文直抽,先按标题切块

招标文件最难的地方不是字段多,而是篇幅长、章节多、不同章节关心的问题完全不同。如果直接把全文塞给一个总 Prompt,结果很难稳。

更合理的做法是先按标题切块。代码里的切块入口就是:

function parseMarkdownToChunks(md: string): Chunk[] { const lines = md.split('\n'); const headerMatch = line.match(/^#{1,2}\s+(.*)/); }

这一步做的不是“抽取”,而是把文档转成一批更短、更聚焦的 chunk。

切完之后,再按关键词做模块路由,例如:

const MODULE_KEYWORDS = { basic: ['招标公告', '项目概况', '联系方式'], qualification: ['资格', '资质', '财务', '联合体'], evaluation: ['评标', '评审', '评分', '分值'], submission: ['投标文件', '递交', '开标', '保证金'], invalid_risk: ['无效标', '否决', '废标条款'], annex: ['附件', '格式', '表单', '清单'], };

这样设计有三个直接收益:

  • 每次发给模型的上下文更短,稳定性更高

  • 每个模块只关注自己的问题,不互相干扰

  • 后续新增模块时,只需要新增路由和 Prompt,不需要推翻整套架构

六、Prompt 不是“让模型抽字段”,而是定义模块契约

这里最值得学的不是“用了大模型”,而是 Prompt 把输入输出边界写得很死。

basic_prompt.txt为例,开头先把约束写清楚:

你是一个“招投标文件基础信息(basic)抽取器”。 你的任务:仅抽取【基础信息 basic】模块,并输出严格合法的 JSON。 【核心硬性原则:禁止捏造】 1) 你只能从输入的 Markdown 原文中抽取信息。 2) 如果原文没有明确出现某字段:该字段 value 必须为 "" 或 null。 6) 【溯源原子性原则——最高优先级】 - 每个 value 必须来自原文中一处连续段落/句子的逐字摘录。

接着把输出骨架固定下来:

{ "module_key": "basic", "module_name": "基础信息", "sections": { "bidder_agency": { "title": "招标人/代理信息", "blocks": [] }, "project_info": { "title": "项目信息", "blocks": [] }, "key_time_content": { "title": "关键时间/内容", "blocks": [] }, "bid_bond_related": { "title": "保证金相关", "blocks": [] }, "other_info": { "title": "其他信息", "blocks": [] }, "procurement_requirements": { "title": "采购要求", "blocks": [] } }, "missing_fields": [], "warnings": [] }

为什么要这么写,而不是只写一句“请抽取基础信息”?

原因很实际:

  • module_key固定,前端才能知道这是哪个模块

  • sections固定,页面才能直接按 section 渲染

  • missing_fields固定,后续才能做缺失项提示

  • warnings固定,后续才能挂冲突说明或风险提醒

Prompt 里还进一步把block限定为table / kv / list / text四种。这个设计很关键,因为招标文件天然是半结构化文档,不同信息的最佳表达形式并不一样。

例如:

  • 联系方式更像table

  • 项目编号、预算金额更像kv

  • 公告媒介、平台地址更像list

  • 开标说明、答疑说明更像text

这比把所有字段强行压成同一种平铺结构更稳。

七、输入、Prompt、输出必须一一对应

要让这套架构可维护,至少要把三件事先对齐:

1. 模块输入是什么

输入不是全文,而是某个模块命中的 Markdown 片段。前端会把命中的 chunk 重新拼成模块输入:

moduleData[key].markdown += `\n\n### ${c.title_path}\n` + c.content;

所以传给qualification模块的,并不是整份标书,而是“资格要求相关片段”。

2. Prompt 定义什么结构

以资格要求模块为例,Prompt 直接固定了三个 section:

{ "module_key": "qualification", "sections": { "applicant_requirements": { "title": "申请人资格要求", "blocks": [] }, "eligibility_review": { "title": "资格性审查", "blocks": [] }, "compliance_review": { "title": "符合性审查", "blocks": [] } } }

这意味着这个模块的职责只有三件事,不会在一次抽取里又去掺杂评标办法或附件格式。

3. 前端按什么结构展示

前端模块配置也会定义同样的 section key。这样一来,页面渲染不需要再猜字段,只要按约定好的 key 读取结果即可。

也就是说,这里不是“先让模型自由返回,再想办法接结果”,而是先把输入范围、Prompt schema、页面结构统一好,再让模型往固定壳子里填内容。

八、和传统做法相比,差别在哪里

如果目标是做工程化工具,而不是做一次性演示,这套方案和传统方式有两个本质区别。

1. 不是 OCR 文本加正则硬提

纯文本加正则在字段非常固定时还可以用,但招标文件章节名称、段落顺序、表格表达方式都经常变化,规则一旦堆多,维护成本会非常高。

2. 不是全文加一个总 Prompt

全文单 Prompt 很适合快速做一个效果展示,但它很难同时解决下面几个问题:

  • 模块边界不清

  • 输出结构不稳定

  • 某个模块要扩字段时会牵一发动全身

  • 很难做稳定展示和后续治理

更稳定的方式是:

  • 先解析成结构化中间层

  • 再切块

  • 再按模块分别抽取

  • 最后按模块 JSON 聚合

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 3:48:11

2010-2025年上市公司国地税改革DID数据

本数据以张浩天和卢盛峰(2025)《国地税机构合并与政府补助策略性调整》研究框架为参考,构建上司公司国地税改革DID虚拟变量。国地税合并的核心目标之一是提升税收治理效能,降低征纳成本,优化营商环境。然而&#xff0c…

作者头像 李华
网站建设 2026/4/17 3:46:16

树莓派上更换镜像源的方法

在树莓派上更换镜像源(如改为清华源、阿里云源等)可以显著提升软件安装和更新速度。以下是详细步骤,包含两种修改方式(直接替换文件或使用 sed 命令),并附常见问题解决方案:‌方法一&#xff1a…

作者头像 李华
网站建设 2026/4/17 3:42:11

数据中心机房工程建设方案

延伸阅读 | 数据中心机房规划、建设及运维管理,值得收藏学习!建成不是终点!数据中心A级标准满载测试,筑牢数字底座安全防线AI算力狂飙,800V高压直流绿电直连或成下一代数据中心标配

作者头像 李华
网站建设 2026/4/17 3:36:22

开源工具链全景图:2026年最值得关注的AI Agent开源项目汇总

开源工具链全景图:2026年最值得关注的AI Agent开源项目汇总 关键词 AI Agent开源工具链、LLM驱动智能体、Multi-Agent协作框架、Agent构建低代码/无代码、Agent推理增强技术、Agent内存系统、Agent评估基准 摘要 当GPT-4o Mini这样的“轻量且全能”的LLM成为2025-…

作者头像 李华