免费POC, 零成本试错
AI知识库

53AI知识库

学习大模型的前沿技术与行业应用场景


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

发布日期:2025-09-19 18:24:36 浏览次数: 1517
作者:算力领跑者老王

微信搜一搜,关注“算力领跑者老王”

推荐语

AI助力标书制作,告别繁琐格式调整,让投标人重获自由!

核心内容:
1. 标书制作的痛点:格式调整与图片插入的繁琐流程
2. AI解决方案:智能解析招标文件,自动提取并精准插入附件
3. 实践案例:知识库管理与企业资质图片的自动化处理

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

📖 前言:当算力遇上格式强迫症

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

📥 输入输出样例

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

🎬 产品演示:眼见为实

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

🤖 Agent搭建全解析

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

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

我们采用"分治策略"管理企业资质:

  1. 将营业执照、纳税证明等图片存入docx
  2. 按材料类型分段存储,确保检索时能返回完整图片集
  3. dify平台通过URL分发图片资源
知识库:这里存放着价值百万的"数字房产证"🏠

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

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

🛠️ 前处理三剑客

  1. DOCX标题提取器 - 专业的"目录生成师"
import requestsimport tempfileimport osimport refrom collections.abc import Generatorfrom typing import AnyDictListOptionalfrom io import BytesIOfrom docx import Documentfrom dify_plugin import Toolfrom dify_plugin.entities.tool import ToolInvokeMessageclass DocxTitleTool(Tool):    def _invoke(self, tool_parameters: dict[strAny]) -> Generator[ToolInvokeMessage]:        """        提取DOCX文件中的标题
        Args:            tool_parameters: 包含docx文件和title_level参数        """        # 获取参数        uploaded_file = tool_parameters.get('docx')        title_level = tool_parameters.get('title_level'1)
        if not uploaded_file:            yield self.create_text_message("请上传DOCX文件")            return
        try:            title_level = int(title_level)            if title_level < 1 or title_level > 9:                yield self.create_text_message("标题级别必须在1-9之间")                return        except (ValueError, TypeError):            yield self.create_text_message("标题级别必须是有效的数字")            return
        try:            # 下载文件            file_url = "http://api:5001" + uploaded_file.url            response = requests.get(file_url)
            if response.status_code != 200:                yield self.create_text_message(f"文件下载失败,状态码: {response.status_code}")                return
            # 创建临时文件保存DOCX内容            with tempfile.NamedTemporaryFile(suffix='.docx', delete=Falseas temp_file:                temp_file.write(response.content)                temp_file_path = temp_file.name
            try:                # 提取标题                headings_result = self._extract_headings_by_level(temp_file_path, title_level)
                if not headings_result:                    yield self.create_text_message("未在文档中找到指定级别的标题")                    return
                # 返回结果                yield self.create_json_message({                    "result": headings_result,                    "summary"f"成功提取了{title_level}级及以上的标题"                })
            finally:                # 清理临时文件                if os.path.exists(temp_file_path):                    os.unlink(temp_file_path)
        except Exception as e:            yield self.create_text_message(f"处理文件时发生错误: {str(e)}")
    def _get_heading_level(self, paragraph) -> Optional[int]:        """        获取段落的标题级别
        Args:            paragraph: 文档段落对象
        Returns:            int: 标题级别 (1-9),如果不是标题则返回None        """        # 方法1: 通过样式名称判断        style_name = paragraph.style.name.lower()
        # 检查标准的标题样式        if 'heading' in style_name:            # 提取数字,如 'Heading 1' -> 1            match = re.search(r'heading\s*(\d+)', style_name)            if match:                return int(match.group(1))
        # 检查中文标题样式        if '标题' in style_name:            match = re.search(r'标题\s*(\d+)', style_name)            if match:                return int(match.group(1))
        # 方法2: 通过outline_level属性判断        if hasattr(paragraph, '_p'and hasattr(paragraph._p, 'pPr'):            pPr = paragraph._p.pPr            if pPr is not None and hasattr(pPr, 'outlineLvl_val'):                outline_level = pPr.outlineLvl_val                if outline_level is not None:                    return outline_level + 1  # outline_level是0-based,转换为1-based
        # 方法3: 通过样式的outline_level属性        try:            if hasattr(paragraph.style, '_element'and hasattr(paragraph.style._element, 'pPr'):                pPr = paragraph.style._element.pPr                if pPr is not None:                    outline_lvl = pPr.find('.//{http://schemas.openxmlformats.org/wordprocessingml/2006/main}outlineLvl')                    if outline_lvl is not None:                        val = outline_lvl.get('{http://schemas.openxmlformats.org/wordprocessingml/2006/main}val')                        if val is not None:                            return int(val) + 1        except:            pass
        return None
    def _extract_headings_by_level(self, file_path: str, max_level: int) -> List[Dict[strList[str]]]:        """        提取指定级别的标题
        Args:            file_path: DOCX文件路径            max_level: 最大标题级别
        Returns:            List[Dict[str, List[str]]]: 按用户要求格式返回的标题列表        """        try:            # 加载文档            doc = Document(file_path)
            # 按级别收集标题            headings_by_level = {}            for level in range(1, max_level + 1):                headings_by_level[level] = []
            # 遍历所有段落            for paragraph in doc.paragraphs:                # 跳过空段落                if not paragraph.text.strip():                    continue
                # 获取标题级别                heading_level = self._get_heading_level(paragraph)
                if heading_level is not None and heading_level <= max_level:                    headings_by_level[heading_level].append(paragraph.text.strip())
            # 转换为用户要求的格式            result = []            level_names = {                1"一级标题",                2"二级标题"                3"三级标题",                4"四级标题",                5"五级标题",                6"六级标题",                7"七级标题",                8"八级标题",                9"九级标题"            }
            for level in range(1, max_level + 1):                if headings_by_level[level]:  # 只添加有内容的级别                    level_name = level_names.get(level, f"{level}级标题")                    result.append({level_name: headings_by_level[level]})
            return result
        except Exception as e:            raise Exception(f"提取标题时发生错误: {str(e)}")
  1. 招标文件分析 - LLM的"格式侦探"
你是一个招标文件分析助手,在招投标领域,乙方投标文件,必须按照甲方招标文件中规定的格式进行撰写#输入是招标文件的章节标题,请判断对于投标文件的格式要求,所在的章节,然后返回开始章节标题,和结束章节标题#符合条件的章节,一般包含有"附件"字样,即该章,附带了许多格式化的表格,投标人照此写标书#如果没有结束章节标题,则置为空参考输出格式:{"开始标题":”XXX“,”结束标题“:"XXX"}{{#投标文件的章节名称.result#}}/no_think
经过LLM后,去掉输出结果的think标签
import re,jsonfrom typing import Listimport jsondef main(arg1):    tmp = re.sub(r"<think>[\s\S]*?</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    }
  1. 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{    "element_type": "paragraph",    "xml_content": "<w:p>7-2纳税证明</w:p>",    "text_content": "7-2纳税证明",    "index": 642}
智能匹配流程:a.文字:"7-2纳税证明"发出相亲请求b.知识库:匹配到纳税证明图片集c.LLM媒人:生成"结婚证"XML数据)d.输出:文字+图片的完美组合
大模型将图片转换为xml的prompt:
你是一个json数据处理专家,请将数据A,按照参考格式,进行构造,然后返回一个新的json数据#如果待处理数据只有文字,则生成element_type为paragraph。如果待处理数据还有图像,则生成element_type为picture。#index字段,与B保持一致,举例B为100,则输出的所有index都是100数据A:{{检索到的图片url.result#}}数据B:{{#当前数据索引#}}如果只有文字,输出格式参考 {    "element_type""paragraph",    "xml_content""xxxx",    "text_content""xxx",    "index": xxx,    "section_format": null  }如果是图像,输出格式参考{    "element_type""picture",   "image_path""XXX,   "index": XXX}

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

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

使用xml标签,确保了生成标书的格式的正确性,而且自动插入图片,节省了投标人员一部分时间。



💫 技术价值:拯救周末,从我做起

通过XML标签实现:

  1. 格式100%准确 → 告别招标方的格式警告
  2. 图片自动插入 → 省下买咖啡的钱
  3. 效率提升90% → 多出来的时间能追完《三体》

最终效果:投标人员终于可以笑着交标书,而不是熬夜改格式 😄

如果这篇技术分享对您的AI落地实践有所启发,欢迎点赞、转发

有任何技术问题或合作意向,欢迎在评论区留言交流 ~~~

53AI,企业落地大模型首选服务商

产品:场景落地咨询+大模型应用平台+行业解决方案

承诺:免费POC验证,效果达标后再合作。零风险落地应用大模型,已交付160+中大型企业

联系我们

售前咨询
186 6662 7370
预约演示
185 8882 0121

微信扫码

添加专属顾问

回到顶部

加载中...

扫码咨询