From 1a6b50cba720833ded74b0ab75763758b61d8800 Mon Sep 17 00:00:00 2001 From: ninghongbin <2409766686@qq.com> Date: Thu, 4 Dec 2025 10:11:52 +0800 Subject: [PATCH] vllm --- config.py | 5 +- 并发_vllm.py | 290 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 并发_vllm.py diff --git a/config.py b/config.py index 3e08d28..0fea372 100644 --- a/config.py +++ b/config.py @@ -67,4 +67,7 @@ class DetectionResponse(BaseModel): # 智能体相关 OLLAMA_MODEL = "alibayram/Qwen3-30B-A3B-Instruct-2507:latest" # 当前最强本地模型 -OLLAMA_BASE_URL = "http://192.168.110.5:11434" \ No newline at end of file +OLLAMA_BASE_URL = "http://192.168.110.5:11434" + +VLLM_BASE_URL = "http://192.168.110.5:8000/v1" +VLLM_MODEL_NAME = "/home/zrway/vllm_models/LLM/new" \ No newline at end of file diff --git a/并发_vllm.py b/并发_vllm.py new file mode 100644 index 0000000..15ff559 --- /dev/null +++ b/并发_vllm.py @@ -0,0 +1,290 @@ +# -*- coding: utf-8 -*- +import os +import re +import time +from pathlib import Path +from typing import List, Dict +from docx import Document +from openai import AsyncOpenAI +import asyncio +from datetime import datetime + +from config import VLLM_BASE_URL, VLLM_MODEL_NAME + +# ==================== 配置区 ==================== +INPUT_WORD = r"C:\Users\YC\Desktop\1.docx" +OUTPUT_WORD = r"C:\Users\YC\Desktop\投标文件-最终版.docx" + + +# 统一中间文件目录(强烈建议不要改) +PROCESS_DIR = "12并发过程文件" +# =============================================== + +# ==================== 12并发 =================== +client = AsyncOpenAI(base_url=VLLM_BASE_URL, api_key="EMPTY", timeout=1800) +SEM = asyncio.Semaphore(12) + +# ==================== 进度条 ==================== +class ProgressTracker: + def __init__(self, total: int, name: str): + self.total = total + self.name = name + self.completed = 0 + self.start = time.time() + + def update(self): + self.completed += 1 + elapsed = time.time() - self.start + avg = elapsed / self.completed + eta = (self.total - self.completed) * avg + bar = "█" * (30 * self.completed // self.total) + "░" * (30 - 30 * self.completed // self.total) + print(f"\r{self.name} |{bar}| {self.completed}/{self.total} " + f"[{100*self.completed/self.total:5.1f}%] " + f"已用 {elapsed/60:4.1f}分 预计剩余 {eta/60:4.1f}分", end="") + + def finish(self): + total_time = time.time() - self.start + print(f"\n√ {self.name} 完成!总耗时 {total_time/60:.2f} 分钟") + +TOTAL_START = time.time() +print("="*90) +print("12并发生成投标文件 · 字典+回调法(终极优化版)") +print(f"启动时间:{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") +print("="*90) + +# ==================== 异步调用 ==================== +async def call_llm_async(messages: List[Dict], temperature=0.3, max_tokens=56000): + async with SEM: + for attempt in range(6): + try: + start = time.time() + resp = await client.chat.completions.create( + model=VLLM_MODEL_NAME, + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + top_p=0.95, + presence_penalty=1.08, + ) + content = resp.choices[0].message.content.strip() + took = time.time() - start + print(f" √ 成功 | ≈{len(content)//4}千字 | 用时 {took:.1f}s") + return content + except Exception as e: + print(f" × 重试 {attempt+1}/6:{e}") + if attempt < 5: + await asyncio.sleep(8) + else: + return "【请求失败,已保底】" + +# ==================== 1. Word → Markdown ==================== +def word_to_md(word_path: str) -> str: + print("\n开始 1. Word → Markdown 转换...") + start = time.time() + md_path = os.path.splitext(word_path)[0] + "_tender.md" + doc = Document(word_path) + text = "\n\n".join(p.text for p in doc.paragraphs if p.text.strip()) + Path(md_path).write_text(text, encoding="utf-8") + print(f"1. 转换完成!用时 {time.time()-start:.1f}s → {md_path}") + return md_path + +# ==================== 2. 并发四级目录 ==================== +async def generate_full_outline_async(tender_md: str) -> str: + print("\n开始 2. 生成超级四级目录...") + tender_text = Path(tender_md).read_text(encoding="utf-8") + + # 生成三级目录骨架(prompt1 优化版) + prompt1 = f"""你现在是投标文件合规总负责人,唯一目标是让投标文件100%通过形式审查和符合性审查,不被废标。 + + 任务:输出一个【完全符合招标文件规定】的投标文件总目录(最多到四级)。 + + 绝对铁律(任何违反都会导致废标): + 1. 目录的章节顺序、层级符号(一、 二、 1. 1.1 1.1.1等)、标题文字(包括标点、空格)必须与招标文件《投标文件格式》《投标文件组成》《投标文件编制要求》或其附件一字不差。禁止任何改动、增删、合并、同义词替换。 + 2. 如果招标文件明确提供了目录模板或提纲,必须逐字逐句照抄,不允许任何改动。 + 3. 只允许在招标文件明确要求提交特定表格/清单的章节下添加对应的四级表格标题,且表格标题文字必须100%来自招标文件或其附件原文。 + 4. 表格编号必须与招标文件一致(如表1-1、表3.2-1),不得自编。 + 5. 页码一律留空或写“(页码)”,严禁填写数字。 + 6. 如有正副本、分册要求,必须在最前面或最后明确标明(如“正本”“副本”“共X册 第X册”)。 + 7. 严禁出现任何招标文件未明确要求的章节或表格。 + + 招标文件原文(必须严格遵守的全部目录及表格要求): + {tender_text[:60000]} + + 请直接输出完整的投标文件目录(包括必要的四级表格标题),不要任何解释、说明、引号、markdown标记。""" + + outline_skeleton = await call_llm_async([{"role": "user", "content": prompt1}], temperature=0.01, max_tokens=10000) + + level3_titles = [line.strip() for line in outline_skeleton.splitlines() + if re.match(r'^\d+\.\d+[、 ]', line.strip())] + + print(f" 检测到 {len(level3_titles)} 个三级标题,开始并发展开四级...") + + tracker = ProgressTracker((len(level3_titles) + 7) // 8, "2. 四级目录展开进度") + results = [""] * ((len(level3_titles) + 7) // 8) + task_to_idx = {} + + async def on_done(fut, idx): + results[idx] = await fut + tracker.update() + + for i in range(0, len(level3_titles), 8): + batch = level3_titles[i:i+8] + batch_text = "\n".join(batch) + ref_text = tender_text[:50000] + + # 四级表格展开批处理提示词(prompt2 优化版,更安全) + prompt2 = f"""根据招标文件要求,只在明确写明“投标人应提交XX表”“填写附件X格式”“提供XX清单”的章节下,插入对应的四级表格标题。 + + 绝对禁止: + - 自创新表格或标题 + - 修改招标文件原文的任何文字 + - 在未要求的地方添加表格 + + 允许的操作: + 仅当招标文件原文出现类似“投标人应提交……表(格式见附件X)”“……一览表”“……清单”等语句时,才添加对应表格标题作为四级目录。 + + 输出要求: + - 每行格式:原章节号 + 空格 + 招标文件原文中的完整表格标题(含表号,如表3-1) + - 用空行分隔不同章节的表格组 + - 如果某个批次的三级章节下没有必须的表格,则输出空字符串即可 + + 本批次需要处理的章节标题(保持原顺序): + {batch_text} + + 招标文件相关原文摘录(包含所有表格要求): + {ref_text} + + 直接输出四级表格标题,不要任何解释。""" + + task = asyncio.create_task(call_llm_async([{"role": "user", "content": prompt2}], temperature=0.2, max_tokens=20000)) + task_to_idx[task] = i // 8 + task.add_done_callback(lambda t, idx=i//8: asyncio.create_task(on_done(t, idx))) + + while [t for t in task_to_idx if not t.done()]: + await asyncio.sleep(0.1) + + # 关键修复:只追加四级内容,不重复三级标题 + full_outline = outline_skeleton + "\n\n" + "\n".join(filter(None, results)).strip() + "\n" + tracker.finish() + + outline_path = f"{PROCESS_DIR}/四级目录.md" + Path(outline_path).write_text(full_outline, encoding="utf-8") + print(f"四级目录已保存 → {outline_path}") + return full_outline + +# ==================== 3. 并发正文生成 ==================== +async def batch_fill_content_async(outline: str, tender_text: str) -> str: + print("\n开始 3. 分批生成正文内容...") + level4_titles = [line.strip() for line in outline.splitlines() + if re.match(r'^\d+\.\d+\.\d+[、 ]', line.strip())] + + batch_size = 8 # 表格减少后,8个一批更稳定 + total_batches = (len(level4_titles) + batch_size - 1) // batch_size + print(f" 共 {len(level4_titles)} 个四级标题,分 {total_batches} 批处理") + + tracker = ProgressTracker(total_batches, "3. 正文内容生成进度") + results = [""] * total_batches + task_to_idx = {} + + async def on_done(fut, idx): + results[idx] = await fut + tracker.update() + + for i in range(0, len(level4_titles), batch_size): + batch = level4_titles[i:i+batch_size] + titles_str = "\n".join(batch) + ref_text = tender_text[:60000] + + prompt = f"""你现在是拥有25年投标经验的合规总师,目标:让投标文件100%通过初步评审(形式审查+符合性审查+资格审查),绝不废标。 + + 请为以下章节撰写极简、极干、绝对安全的正文内容,每节总字数严格控制在80-400字。 + + 废标红线(任何一条违反都会废标,严禁触碰): + 1. 只能响应招标文件明确要求的内容,多一个字、一个承诺、一个“亮点”都不允许。 + 2. 严禁出现“优于招标要求”“优选方案”“创新”“国内领先”“独家”等任何可能被认定为正偏离或多余承诺的字眼。 + 3. 所有表格必须100%使用招标文件或附件提供的原表格格式(表头、单位、列数、行数、备注文字一字不改),只允许填写数据。 + 4. 正文文字只能是“本公司完全响应招标文件要求”“详见后附证明材料复印件”“见下表”等最安全的套话。 + 5. 只能使用以下9个占位符(其他一律删除,不得出现具体公司名、金额等): + 【投标单位全称】【法定代表人姓名】【委托代理人姓名】【项目名称】【投标总价(大写)】【投标总价(小写)】【总服务期(天)】【投标保证金金额】【投标文件签署日期】 + 6. 严禁出现任何图片、流程图、彩色文字、组织机构图、超过两段的文字说明。 + + 不同类型章节的唯一允许写法(必须严格选择其一): + • 投标函及附录、投标一览表类:只填招标文件提供的表格 + 一句“以上内容真实有效,完全响应招标文件所有实质性要求和条件,无任何偏差。” + • 法定代表人身份证明、授权委托书:严格按招标文件提供的格式原文填写(含“身份证号码”等栏),一字不改。 + • 资质、资格证明文件类:只写“详见本节后附证明材料复印件(加盖公章)”。 + • 业绩表、人员表、设备表等所有表格类:只输出完整表格,不加任何文字说明。 + • 承诺书类:严格按招标文件提供的格式或范文填写,最多加一句“本公司完全响应招标文件要求”。 + + 当前必须严格按顺序撰写的章节标题: + {titles_str} + + 招标文件强制要求的内容及所有表格格式原文: + {ref_text} + + 直接输出每个章节的正文内容,用 + + --- + + 分隔,不要任何章节标题、解释、编号。""" + + task = asyncio.create_task(call_llm_async([{"role": "user", "content": prompt}], temperature=0.35, max_tokens=56000)) + task_to_idx[task] = i // batch_size + task.add_done_callback(lambda t, idx=i//batch_size: asyncio.create_task(on_done(t, idx))) + + while [t for t in task_to_idx if not t.done()]: + await asyncio.sleep(0.1) + + tracker.finish() + # 关键修复:去掉无用标题,直接拼接 + final_content = "\n\n---\n\n".join(results) + content_path = f"{PROCESS_DIR}/正文内容.md" + Path(content_path).write_text(final_content, encoding="utf-8") + print(f"正文内容已保存 → {content_path}") + return final_content + +# ==================== 4. Markdown → Word(简单占坑) ==================== +def md_to_word(md_path: str, output_word: str): + print("\n开始 4. Markdown → Word 转换...") + print("提示:请使用 Pandoc + 模板转换为精美 Word") + print(f"推荐命令:pandoc \"{md_path}\" -o \"{output_word}\" --reference-doc=模板.docx") + print("暂未自动执行,需手动运行以上命令或自行补全转换逻辑") + # 如需自动,可取消下方注释(需先安装 pandoc) + # import subprocess + # subprocess.run(['pandoc', md_path, '-o', output_word, '--reference-doc=模板.docx']) + +# ==================== 主流程 ==================== +async def main_async(): + # 统一创建目录,永绝路径错误 + os.makedirs(PROCESS_DIR, exist_ok=True) + os.makedirs("output", exist_ok=True) + + tender_md = word_to_md(INPUT_WORD) + tender_text = Path(tender_md).read_text(encoding="utf-8") + + outline = await generate_full_outline_async(tender_md) + content = await batch_fill_content_async(outline, tender_text) + + final_md = f"""# 【投标单位全称】 + +## {Path(INPUT_WORD).stem} - 投标文件 + +{outline} + +{content}""" + + final_md_path = f"{PROCESS_DIR}/最终投标文件.md" + Path(final_md_path).write_text(final_md, encoding="utf-8") + print(f"\n最终整合完成 → {final_md_path}") + + md_to_word(final_md_path, OUTPUT_WORD) + + total_seconds = time.time() - TOTAL_START + print("\n" + "="*90) + print("大功告成!顶级投标文件已生成!") + print(f"总耗时:{total_seconds/60:.2f} 分钟") + print(f"最终 Word 文件路径:{OUTPUT_WORD}") + print(f"所有过程文件保存在:{PROCESS_DIR}/") + print("="*90) + +if __name__ == "__main__": + asyncio.run(main_async()) \ No newline at end of file