Files
AI_agent_detect/并发_vllm.py

290 lines
14 KiB
Python
Raw Normal View History

2025-12-04 10:11:52 +08:00
# -*- 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())