Tender 标书端到端流程概览¶
本文档梳理 Tender 从 PDF 招标文件解析 → 信息提取与结构化存储 → 目录大纲生成 → 章节正文写作 → 整卷标书导出 的完整链路,以及各阶段使用到的模型和关键模块,方便后续排查问题和扩展能力。
一、整体阶段拆分¶
- 标书基础配置 & 外部资料绑定 (
routes/tender/config.py等) - 上传 PDF 并解析为结构化信息 (
routes/tender/parser.py+services/ingestion/*) - 生成标书目录大纲(Outline)与节点编写思路 (
routes/tender/generation/outline.py+services/generation_service.py) - 按目录生成章节正文(单章 / 批量) (
routes/tender/generation/section.py+services/generation_service.py) - 汇总大纲与正文,导出整卷 Word 标书 (
routes/tender/document.py+utils/document_renderer.py) - 解析任务与 SSE 进度追踪 (
app/routes/tasks.py挂载于/v1/tasks+services/task_service.py)
下面按时间顺序、结合核心模型说明每个阶段的输入/输出和关键代码位置。
二、标书基础配置 & 引用资料绑定¶
2.1 数据模型¶
app/models/tenders/core.py→Tender-
关键字段:
id: 标书 IDtitle/name: 标书标题与内部名称status: TenderStatus:PARSING / DRAFT / GENERATING / COMPLETED / FAILED,表示解析/正文任务状态。step: TenderStep:ANALYSIS / OUTLINE / CONTENT,表示用户已到达的最远业务步骤(标书解析/目录编制/正文编制)。meta_data: JSONB:存预算、项目名称、采购人等动态属性task_id: UUID:关联的解析任务IngestionTaskblind_rules, table_density, has_flowchart:写作/排版相关配置- 关联关系:
content、scoring_items、section_contents、knowledge_references、product_references、image_references
-
app/models/tenders/content.py→TenderContent overview_data: JSONB:解析得到的项目概况原始结构-
outline: JSONB:AI 生成的目录树(OutlineNode列表,结构见schemas/generation.py) -
app/models/tenders/scoring_item.py→TenderScoring -
评分项结构化结果:
category / sub_category / name / max_score / review_content / review_criteria / scoring_rule等 -
app/models/tenders/references.py→ 三类引用关系 TenderKnowledgeReference:标书 ↔ 知识库文件TenderProductReference:标书 ↔ 产品库条目TenderImageReference:标书 ↔ 图片资源
2.2 列表状态映射¶
历史记录列表的四个展示状态由 Tender.status 映射,不使用 Tender.step:
| 列表展示 | API 筛选参数 | 写入时机 |
|---|---|---|
| 未完成 | ?status=DRAFT |
解析完成,或正文操作结束但仍有叶子章节未写 |
| 已完成 | ?status=COMPLETED |
当前目录的全部叶子章节都有非空正文 |
| 处理中 | ?status=PARSING&status=GENERATING |
quick-parse 正在解析,或正文生成任务正在执行 |
| 处理失败 | ?status=FAILED |
解析/正文生成失败、取消,或正文异步任务入队失败 |
GET /v1/tenders 的 status 参数支持多选,因此筛选“处理中”必须同时传入 PARSING 与 GENERATING。
2.3 配置接口¶
app/routes/tender/config.pyPATCH /tenders/{tender_id}→update_tender_config- 请求体:
schemas/tender.UpdateTenderConfigRequest- 更新字段:
title, meta_data, page_count, layout_style, blind_rules - 以及:
knowledge_reference_ids, product_reference_ids, image_reference_ids
- 更新字段:
- 内部逻辑:
- 对普通字段直接
setattr(tender, key, value) - 三类引用 ID 通过清空原有
Tender*Reference再重建,保证幂等
- 对普通字段直接
2.4 外部资料与图库¶
- 知识库:
models/knowledge.py+services/knowledge_* - 产品库:
models/product_library.py+services/product_library/+app/routes/v1/product_library/(REST 前缀/v1/product-libraries,与知识库一致:GET/POST ""、GET/PATCH/DELETE /{library_id};列表分页;子资源/{library_id}/products、/{library_id}/parse/...) - 图库:
models/image_gallery.py+services/image/gallery_service.py
这些引用信息后续会在 章节写作上下文 和 整卷导出 时统一注入。
三、上传 PDF 并解析为结构化标书信息¶
3.1 上传与任务初始化¶
- 入口:
app/routes/tender/parser.py POST /tenders/{tender_id}/documents/parse→parse_document-
核心步骤:
- 校验文件类型(仅支持
application/pdf)。 - 生成
task_id = uuid4(),将上传的 PDF 以流式方式写入/tmp/tender_uploads/...,限制最大 50MB。 - 读取文件字节
file_bytes,并通过OSSService.upload_bytes上传到对象存储(OBJECT_STORAGE_*配置)。 - 调用
task_service.init(...)创建IngestionTask记录: - 保存文件名、大小、对象存储 URL、用户 ID 等。
- 将
Tender.task_id = task_id,Tender.status = PARSING,并确保存在关联的TenderContent记录。 - 使用 FastAPI
BackgroundTasks调用: ingestion_service.process_document(task_id, file_bytes, original_filename)
- 校验文件类型(仅支持
-
解析历史:
GET /tenders/documents/parse/history&GET /v1/tasks结合TaskService.summarize_channels查看每次解析的概况/评分结果。
3.2 文档解析主流程¶
- 入口:
app/services/ingestion/index.py→DocumentIngestionService.process_document - 创建
IngestionTaskRunner,核心参数:document_extractor: DocumentExtractortask_id, file_bytes, filename, total_pages
- 使用
fitz.open(stream=file_bytes, filetype="pdf")打开文档,遍历所有页:await self.runner.run_all_pages(document)
- 所有页跑完后,等待向量化任务
vector_tasks最长 60 秒。 - 最终调用:
tender_service.save_tender_results(task_id, final_overview, final_scoring, raw_overview_markdown)将解析结果落库task_service.finish(task_id, final_result=...)标记任务完成- 异常则
task_service.fail(...)并将Tender.status设置为FAILED。
3.3 单页处理与模型链路¶
app/services/ingestion/runner.py → IngestionTaskRunner
- 页面内容提取
DocumentExtractor.extract_markdown(document, page_index)(core/document_extractor.py)- 首选:
pymupdf4llm.to_markdown(...)把指定页转换为 Markdown - 失败兜底:
page.get_text("text")
- 首选:
-
依赖:
PyMuPDF (fitz)pymupdf4llm[ocr,layout]
-
Markdown 处理
-
当前直接使用
DocumentExtractor输出的 Markdown,不再经过额外的 LLM 清洗。 -
信号检测 & LLM 信息提取
-
信号引擎:
core/universal_nlp_base.py→universal_nlp_base- 负责判断该页是否可能包含:
- 概况信息(项目名称、预算、编号等)→
has_overview_signal(text, page_index) - 需求/技术要求 →
has_requirement_signal(text) - 评分标准 →
has_scoring_signal(text) - 使用统一词表:
lexicons/common/overview_terms.txtlexicons/common/requirement_terms.txtlexicons/common/scoring_terms.txt
-
结构化提取:
core/extraction/extractor.py→Extractor- 客户端:
AsyncOpenAI(api_key=PARSER_EXTRACT_API_KEY, base_url=PARSER_EXTRACT_BASE_URL, model=PARSER_EXTRACT_MODEL) - 共有三个子任务,互相独立并发执行:
-
extract_overview(markdown)- system prompt:
overview_prompt.md - 返回字典(项目名称、编号、预算、开标时间、地点等)。
- system prompt:
-
extract_scoring(markdown)- system prompt:
scoring_prompt.md - 返回评分项列表(或带
items的 JSON),最终解析为统一 list 结构。
- system prompt:
-
extract_requirements(markdown)- system prompt:
analysis_indicators.md - 返回多个需求列表字段,统一通过
parse_requirements_payload清洗为: - 人员要求、设备要求、技术要求、服务方案要求、服务标准等。
- system prompt:
- 内置重试机制:
max_retries / retry_delays,对503/429/timeout等错误进行指数退避重试。
- 客户端:
-
聚合结果 & 进度推送(SSE)
aggregate_and_push(...)中将当前页的overview/requirements/scoring合并到 Runner 的全局状态:- 使用
ingestion/utils.py的merge_payload等方法进行去重与合并。
- 使用
- 调用
task_service.update_progress(...)持久化到IngestionTask.result_data:- 每一页的清洗后内容/来源(regex or llm)、以及分类(
overview_update / scoring_update / mixed_update / page_error)。
- 每一页的清洗后内容/来源(regex or llm)、以及分类(
-
前端通过
app/routes/tasks.py(前缀/v1/tasks)的 SSE 接口/v1/tasks/{task_id}/events实时订阅解析进度。 -
向量化与 RAG 索引
- 知识库摄入通过
services/knowledge/ingestion_service.py调用共享vector_store。 services/vector_store/factory.py根据VECTOR_STORE_PROVIDER创建向量存储;当前生产配置使用 Qdrant。- Embedding 请求由
EMBEDDING_API_KEY、EMBEDDING_BASE_URL、EMBEDDING_MODEL配置。 -
后续检索由
services/knowledge/search_service.py调用同一向量存储。 -
解析结果落库
-
services/tender_service.py→TenderService.save_tender_results - 根据
task_id找到对应Tender和TenderContent:Tender.meta_data中填充:project_name, project_code, record_code, budget_amount, deadline, opening_time, location, service_period, purchaser, agency, project_nature, procurement_scope等。TenderContent.overview_data保存 LLM 原始概况结果。
- 需求部分写入
Tender.meta_data的tech_requirements, scheme_requirements, service_standards等字段。 - 评分项 list 经过去重后写入
TenderScoring表。 - quick-parse 入队后将
Tender.status置为PARSING;结果落库成功后置为DRAFT;解析失败或取消时置为FAILED。
四、生成标书目录大纲(Outline)¶
4.1 目录生成接口¶
- 一二级目录入口:
POST /tenders/{tender_id}/outline/skeleton - 实际委托:
generation_service.generate_skeleton(...) -
模型配置:
OUTLINE_SKELETON_MODEL / OUTLINE_SKELETON_API_KEY / OUTLINE_SKELETON_BASE_URL -
三四级目录入口:
POST /tenders/{tender_id}/outline/children与.../stream - 实际委托:
suboutline_service.generate_outline_children_playground_raw(...) - 模型配置:
OUTLINE_CHILDREN_MODEL / OUTLINE_CHILDREN_API_KEY / OUTLINE_CHILDREN_BASE_URL
4.2 Prompt 构建与模型¶
- 一二级目录通过后台 Prompt 场景
outline_l1_l2渲染,注入评分标准、页数等结构化上下文。 - 三四级目录复用后台
writing_hintPrompt,按当前二级节、需求条目和已有一二级目录收窄上下文。 - 旧的
GENERATION_*结构化完整目录生成链路已下线。
4.3 Outline 结果与持久化¶
- 一二级目录生成后写入
TenderContent.outline / outline_flat / outline_markdown。 - 三四级目录生成后按二级节合并回
TenderContent.outline,并重算outline_flat / outline_markdown。 - 篇幅估算使用
OUTLINE_LEAF_BASE_WORDS / OUTLINE_WORDS_PER_SCORING_ITEM / OUTLINE_WORDS_PER_PAGE。
4.4 Outline 的读取与编辑¶
GET /tenders/{id}/outline:返回整棵目录树给前端。GET /tenders/{id}/outline/prompts:抽取每个节点的content_hint,方便独立展示和编辑。PUT /v1/tenders/{id}/outline/node/content:修改某个节点的title/content_hint并写回 JSON(仅 v1;旧/tenders/...已移除)。PATCH /tenders/{id}/outline/node:调整estimated_pages/estimated_words。
五、按目录生成章节正文(单章/批量)¶
5.1 多维上下文组装(不调用模型)¶
- 模块:
services/context_assembler.py→ContextAssembler - 入口方法:
assemble(session, tender, section_id, outline) - 输出:
SectionContext(数据类),包含: section_id, title, content_hint, breadcrumbscoring_text:通过TenderScoring+ 节点的related_scoring_items匹配出的评分说明。rag_materials:通过vector_service.search(task_id, query)检索到的招标文件原文片段。reference_text:标书绑定的知识库文件 & 产品库条目汇总文案。image_urls:标书绑定的图片资源(格式为“图片资源:(备注)”)。
sibling_titles:同级节点标题,用于避免内容重复。
这一层只负责聚合上下文,不直接调用任何 LLM,方便后期替换模型或添加更多维度(如历史项目案例)。
5.2 单章节生成(SSE 流式)¶
- 路由:
routes/tender/generation/section.py POST /tenders/{id}/sections/{section_id}/generate-
调用:
generation_service.generate_section(session, tender_id, section_id) -
GenerationService.generate_section主要逻辑: - 校验
Tender和TenderContent.outline是否存在。 - 调用
ContextAssembler.assemble构造SectionContext。 - 通过
classify_section(title, content_hint)使用UniversalNLPBase中的technical_terms / business_terms判断:- 技术章节 →
DeepSeek-V3(run_technical_writing_stream) - 商务/服务章节 →
Qwen(run_writing_stream)
- 技术章节 →
- 通过
section_content场景绑定读取数据库 Prompt,并使用SectionContext渲染模板变量。 - 调用对应模型的 流式接口,逐块
yieldSSE 事件:step = writing/generating_content,前端边收边渲染。
-
生成完全文本后调用
upsert_section_content(...)写入TenderSectionContent表,并更新Tender.updated_at。 -
GET /tenders/{id}/sections/{section_id}:查询单章节详情。
六、整卷标书导出(Docx)¶
6.1 Docx 导出¶
- 路由:
POST /tenders/{id}/document/download/docx -
请求体:
DocumentStyleConfig,可指定页边距和各级标题格式(字号、字体、对齐方式等)。 -
内部流程由
generate_document_docx_stream(...)使用python-docx生成.docx二进制流: - 顶级标题 → 一级 Heading
- 各级目录节点映射到不同 Heading 级别
- 叶子节点无正文则插入“此章节内容尚未生成”引用样式段落。
七、涉及的主要模型与第三方依赖¶
7.1 LLM 模型¶
所有模型的具体名称都来自 .env / core/config.Settings:
- 解析提取模型(Extractor)
PARSER_EXTRACT_MODEL-
用途:从 Markdown 中抽取:项目概况、需求条目、评分标准,统一写入
TenderContent/TenderScoring等。 -
一二级目录模型(Outline Skeleton)
OUTLINE_SKELETON_MODEL-
用途:根据评分项和标书上下文生成一二级目录骨架。
-
三四级目录模型(Outline Children)
OUTLINE_CHILDREN_MODEL-
用途:按二级节生成三四级目录和编写提示。
-
正文写作模型(Writing)
- 商务/服务类章节:
WRITING_MODEL(当前配置:Qwen/Qwen2.5-72B-Instruct) - 技术类章节:
DEEPSEEK_WRITING_MODEL(当前配置:deepseek-ai/DeepSeek-V3) - 均通过
LLMFactory提供:run_writing_stream(Qwen 流式)run_technical_writing_stream(DeepSeek 流式)
7.2 解析与向量化相关依赖¶
- PDF 解析:
PyMuPDF (fitz)、pymupdf4llm - 文本清洗:正则 + 自定义词库
- 结构化提取与写作:
openaiSDK + 兼容 SiliconFlow/OpenAI 接口 - 向量化与检索:
- Embedding:
EMBEDDING_MODEL(默认BAAI/bge-m3) - 存储与查询:Cloudflare Vectorize(HTTP API)
7.3 存储与对象存储¶
- 主库:PostgreSQL +
sqlmodel/alembic - OSS:
OBJECT_STORAGE_*配置(S3 兼容端点,如阿里云 OSS;使用aioboto3) - 对象:
- 源 PDF:
IngestionTask.file_url与对象 key - 知识库文件 / 图片:各自有 URL 并可通过引用关系挂到标书上。
八、端到端时序总结¶
- 用户创建标书(未在本文详述)并配置基础信息 / 绑定知识库、产品库、图片等。
- 调用
POST /tenders/{id}/documents/parse: - 文件上传 → 对象存储 → 创建
IngestionTask→ 标记Tender.status = PARSING→ 启动后台process_document。 IngestionTaskRunner对每一页做:- PDF → Markdown → 轻量/LLM 清洗 → 信号检测 → LLM 提取(概况/需求/评分)→ 聚合 & SSE 进度 → 向量化入库。
- 全部页面跑完:
TenderService.save_tender_results将聚合结果写入Tender / TenderContent / TenderScoring。Tender.status = DRAFT;解析失败或取消则为FAILED。- 用户调用
POST /tenders/{id}/outline/skeleton: - 评分项 + Prompt 场景 →
OUTLINE_SKELETON_MODEL输出一二级目录 → 保存到TenderContent.outline。 - 页面从解析进入目录时调用
POST /tenders/{id}/workflow/enter-outline,推进Tender.step = OUTLINE。 - 若前端漏调用步骤接口,目录生成保存成功也会兜底将
Tender.step推进到OUTLINE。 - 用户可以:
- 调整
content_hint / estimated_pages / estimated_words; - 生成编写思路与三四级目录;
- 查看历史记录等。
- 用户调用章节写作接口:
- 从目录页点击“编写正文”时调用
POST /tenders/{id}/workflow/enter-content,推进Tender.step = CONTENT。 - 单章:
POST /tenders/{id}/sections/{section_id}/generate - 全文:
POST /tenders/{id}/generation-runs/full - 过程:正文生成接口兜底推进
Tender.step = CONTENT,并将Tender.status设置为GENERATING→ 装配多维上下文 → 流式生成 Markdown 正文 → 写入TenderSectionContent。 - 仅当当前目录全部叶子章节都有非空正文时设置
Tender.status = COMPLETED;正文操作结束但尚未齐全时恢复为DRAFT;批量或异步全文任务失败时设置为FAILED。 - 最后导出整卷标书:
- Docx:
POST /tenders/{id}/document/download/docx - 组合目录树 + 各章节正文 + 引用资料 / 图片列表,生成最终交付文档。