AI赋能招投标:标书生成,告别”复制粘贴”地狱!

"投标人员的时间守恒定律:格式调整耗时 = 插入图片数 × 标书页数/10" ⏰在企业的日常经营中,经过投标判断助手确定标底后,就来到了让无数投标人闻风丧胆的标书撰写阶段。以算力领域为例,一份标书通常包含:
📑 10+个章节
📝 5万字+文本
🖼️ 100+张资质图片
😫 1位濒临崩溃的投标人员

📖 前言:当算力遇上格式强迫症
格式的执念:虽然算力招标对技术参数允许合理波动,但对标书格式却有着"像素级"要求。不同招标单位的格式模板差异,足以让最淡定的投标人抓狂。
图片的迷宫:如何在保证技术准确性的同时,把企业资质图片精准插入到标书指定位置?这不仅是技术问题,更是艺术!

📥 输入输出样例

graph LR    A[招标文件] –> B[智能解析]    B –> C[自动提取附件]    C –> D[精准插入图片]    D –> E[完美格式标书]
* *

🎬 产品演示:眼见为实

温馨提示:观看时请勿羡慕到流泪 😭
* *

🤖 Agent搭建全解析

🎯知识库:AI的"记忆宫殿"

数据是LLM的血液,没有数据的Agent就像没有汽油的超跑 —— 只能看不能开

我们采用"分治策略"管理企业资质:
将营业执照、纳税证明等图片存入docx
按材料类型分段存储,确保检索时能返回完整图片集
在dify平台通过URL分发图片资源

AI赋能招投标:标书生成,告别"复制粘贴"地狱!

知识库:这里存放着价值百万的"数字房产证"🏠

💡工作流:标书生成的"流水线"

AI赋能招投标:标书生成,告别"复制粘贴"地狱!

温馨提示:本流程已通过ISO-9001"防加班"认证 🕒
* *

🛠️ 前处理三剑客
DOCX标题提取器 – 专业的"目录生成师"

import requestsimport tempfileimport osimport refrom collections.abc import Generatorfrom typing import Any, Dict, List, Optionalfrom io import BytesIOfrom docx import Documentfrom difyplugin import Toolfrom difyplugin.entities.tool import ToolInvokeMessageclass DocxTitleTool(Tool):def invoke(self, toolparameters: dict[str, Any]) -> Generator[ToolInvokeMessage]:"""提取DOCX文件中的标题Args:toolparameters: 包含docx文件和titlelevel参数"""# 获取参数uploadedfile = toolparameters.get('docx')titlelevel = toolparameters.get('titlelevel', 1)if not uploadedfile:yield self.createtextmessage("请上传DOCX文件")returntry:titlelevel = int(titlelevel)if titlelevel < 1 or titlelevel > 9:yield self.createtextmessage("标题级别必须在1-9之间")returnexcept (ValueError, TypeError):yield self.createtextmessage("标题级别必须是有效的数字")returntry:# 下载文件fileurl = "http://api:5001" + uploadedfile.urlresponse = requests.get(fileurl)if response.statuscode != 200:yield self.createtextmessage(f"文件下载失败,状态码: {response.statuscode}")return# 创建临时文件保存DOCX内容with tempfile.NamedTemporaryFile(suffix='.docx', delete=False) as tempfile:tempfile.write(response.content)tempfilepath = tempfile.nametry:# 提取标题headingsresult = self.extractheadingsbylevel(tempfilepath, titlelevel)if not headingsresult:yield self.createtextmessage("未在文档中找到指定级别的标题")return# 返回结果yield self.createjsonmessage({"result": headingsresult,"summary": f"成功提取了{titlelevel}级及以上的标题"})finally:# 清理临时文件if os.path.exists(tempfilepath):os.unlink(tempfilepath)except Exception as e:yield self.createtextmessage(f"处理文件时发生错误: {str(e)}")def getheadinglevel(self, paragraph) -> Optional[int]:"""获取段落的标题级别Args:paragraph: 文档段落对象Returns:int: 标题级别 (1-9),如果不是标题则返回None"""# 方法1: 通过样式名称判断stylename = paragraph.style.name.lower()# 检查标准的标题样式if 'heading' in stylename:# 提取数字,如 'Heading 1' -> 1match = re.search(r'headings*(d+)', stylename)if match:return int(match.group(1))# 检查中文标题样式if '标题' in stylename:match = re.search(r'标题s*(d+)', stylename)if match:return int(match.group(1))# 方法2: 通过outlinelevel属性判断if hasattr(paragraph, 'p') and hasattr(paragraph.p, 'pPr'):pPr = paragraph.p.pPrif pPr is not None and hasattr(pPr, 'outlineLvlval'):outlinelevel = pPr.outlineLvlvalif outlinelevel is not None:return outlinelevel + 1  # outlinelevel是0-based,转换为1-based# 方法3: 通过样式的outlinelevel属性try:if hasattr(paragraph.style, 'element') and hasattr(paragraph.style.element, 'pPr'):pPr = paragraph.style.element.pPrif pPr is not None:outlinelvl = pPr.find('.//{http://schemas.openxmlformats.org/wordprocessingml/2006/main}outlineLvl')if outlinelvl is not None:val = outlinelvl.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val')if val is not None:return int(val) + 1except:passreturn Nonedef extractheadingsbylevel(self, filepath: str, maxlevel: int) -> List[Dict[str, List[str]]]:"""提取指定级别的标题Args:filepath: DOCX文件路径maxlevel: 最大标题级别Returns:List[Dict[str, List[str]]]: 按用户要求格式返回的标题列表"""try:# 加载文档doc = Document(filepath)# 按级别收集标题headingsbylevel = {}for level in range(1, maxlevel + 1):headingsbylevel[level] = []# 遍历所有段落for paragraph in doc.paragraphs:# 跳过空段落if not paragraph.text.strip():continue# 获取标题级别headinglevel = self.getheadinglevel(paragraph)if headinglevel is not None and headinglevel <= maxlevel:headingsbylevel[headinglevel].append(paragraph.text.strip())# 转换为用户要求的格式result = []levelnames = {1: "一级标题",2: "二级标题",3: "三级标题",4: "四级标题",5: "五级标题",6: "六级标题",7: "七级标题",8: "八级标题",9: "九级标题"}for level in range(1, maxlevel + 1):if headingsbylevel[level]:  # 只添加有内容的级别levelname = levelnames.get(level, f"{level}级标题")result.append({levelname: headingsby_level[level]})return resultexcept Exception as e:raise Exception(f"提取标题时发生错误: {str(e)}")
招标文件分析 – LLM的"格式侦探"

你是一个招标文件分析助手,在招投标领域,乙方投标文件,必须按照甲方招标文件中规定的格式进行撰写#输入是招标文件的章节标题,请判断对于投标文件的格式要求,所在的章节,然后返回开始章节标题,和结束章节标题#符合条件的章节,一般包含有"附件"字样,即该章,附带了许多格式化的表格,投标人照此写标书#如果没有结束章节标题,则置为空参考输出格式:{"开始标题":”XXX“,”结束标题“:"XXX"}{{#投标文件的章节名称.result#}}/no_think经过LLM后,去掉输出结果的think标签import re,jsonfrom typing import Listimport jsondef main(arg1):    tmp = re.sub(r"<think>[sS]*?</think>", "", arg1, flags=re.DOTALL)    tmp = re.sub(r'^s*“(?:json)?s*n?', '', tmp,flags=re.IGNORECASE)    tmp = re.sub(r'n?`s*$', '', tmp)    tmp = tmp.replace(r'n', 'n').replace(r'"', '"').strip()    result=tmp    return {        'result':result    }
DOCX章节提取器 – 精准的"文档外科医生",代码逻辑如下:

===== 数据结构定义 =====定义 ElementInfo:  元素类型: 'paragraph' 或 'table'  XML内容: 字符串  文本内容: 字符串 (用于预览)  索引: 整数  分区格式: 字典 (可选)定义 SectionInfo:  标题: 字符串  元素列表: [ElementInfo]定义 PageFormatInfo:  页面宽度, 高度, 方向, 四周边距: 整数/字符串定义 DocumentExtractResult:  章节列表: [SectionInfo]  页面格式: PageFormatInfo  源文件路径: 字符串  元素总数: 整数  抽取信息: 字典  分区格式列表: [字典] (可选)  样式XML: 字符串 (可选)  编号XML: 字符串 (可选)# ===== 文档解析器类 =====类 DocxExtractor:  初始化(文档路径):    加载DOCX文档  获取页面格式(章节索引=0):    尝试从文档获取页面设置    失败则返回默认A4格式  查找单章节(开始标题, 标题级别=1, 结束标题=None):    预处理标题(移除空格)    收集所有文档元素(段落+表格)    遍历元素:      – 发现匹配的开始标题 → 创建新章节      – 继续收集元素直到:          a) 遇到结束标题          b) 遇到同级标题(无结束标题时)    返回找到的章节列表  抽取单章节(开始标题, 标题级别=1, 结束标题=None):    调用查找单章节    收集分区格式信息    确定最终页面格式:       优先使用提取的分区格式       否则调用获取页面格式    构建DocumentExtractResult对象    返回结果# ===== 插件工具类 =====类 DocxExtractorTool (继承Tool):  执行入口(参数):    验证必要参数(文档文件, 开始标题)    下载DOCX文件 → 保存为临时文件    创建DocxExtractor实例    调用抽取单章节方法    处理结果:       添加成功元数据       转换为JSON格式       生成输出文件    异常处理:      值错误/通用错误 → 生成错误JSON文件    返回处理结果(文件blob或错误信息)

🔄 迭代环节:图片与文字的"相亲大会"

遍历所有xml数据,查找对应的图片

// XML数据结构:标书的DNA{    "elementtype": "paragraph",    "xmlcontent": "<w:p>7-2纳税证明</w:p>",    "textcontent": "7-2纳税证明",    "index": 642}智能匹配流程:a.文字:"7-2纳税证明"发出相亲请求b.知识库:匹配到纳税证明图片集c.LLM媒人:生成"结婚证"(XML数据)d.输出:文字+图片的完美组合 大模型将图片转换为xml的prompt:你是一个json数据处理专家,请将数据A,按照参考格式,进行构造,然后返回一个新的json数据#如果待处理数据只有文字,则生成elementtype为paragraph。如果待处理数据还有图像,则生成elementtype为picture。#index字段,与B保持一致,举例B为100,则输出的所有index都是100数据A:{{检索到的图片url.result#}}数据B:{{#当前数据索引#}}如果只有文字,输出格式参考 {    "elementtype": "paragraph",    "xmlcontent": "xxxx",    "textcontent": "xxx",    "index": xxx,    "sectionformat": null  }如果是图像,输出格式参考{    "elementtype": "picture",   "image_path": "XXX,   "index": XXX}

🎉 后处理:标书诞生的"高光时刻"

函数 assembledocument(输入: 抽取结果, 输出路径, 是否清理分区属性):`尝试:创建临时目录和DOCX基础结构初始化XML内容列表添加DOCX文档头部到XML列表收集所有元素并按原始索引排序遍历每个元素:如果元素类型是图片:从Docker获取图片数据生成图片XML并保存到媒体目录将图片XML添加到内容列表如果元素类型是段落或表格:获取元素XML内容根据标志决定是否清理分区属性将处理后的XML添加到内容列表添加DOCX文档尾部到XML列表将完整XML写入document.xml文件创建辅助文件:[ContentTypes].xml主关系文件(.rels)文档关系文件(document.xml.rels)样式文件(styles.xml)编号文件(numbering.xml)将临时目录打包为DOCX文件输出处理统计信息异常处理:抛出错误信息最终:清理临时目录
使用xml标签,确保了生成标书的格式的正确性,而且自动插入图片,节省了投标人员一部分时间。

AI知识库企业落地新闻资讯

Harness Engineering 实践:LLM Wiki 什么时候、怎么引入?

2026-7-4 4:11:12

个人提效企业落地新闻资讯

与 AI 一起做产品的六条原则

2026-7-4 4:40:03

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
购物车
优惠劵
搜索