跳转至

Quick Parse 两阶段解析与流式方案

  • 版本:v1.0
  • 日期:2026-04-11
  • 作者:Codex
  • 适用项目:Tender_Documents

1. 背景

当前 parse-events 曾尝试直接把全文按 chunk 切开后,让模型逐段输出 JSON,再把原始 token 流直接返回前端。

这种方式有两个明显问题:

  1. 前端直接拼接多个 chunk 的原始 JSON 文本,天然不是一个稳定、完整的 JSON 结构。
  2. overview / scoring / requirements 混在同一条全文流里,准确性和展示体验都较差。

从中国标书的结构特征看,overviewscoring 通常具备较强的章节稳定性,适合先做范围锁定,再分别解析。


2. 目标

2.1 目标

  1. 将解析链路拆为“范围锁定”与“目标解析”两阶段。
  2. confirm 阶段只做轻量操作,不堆重模型调用。
  3. requirements 放到 worker 中优先落库。
  4. parse-events 只负责 overview + scoring 两路并行 AI 流式。
  5. 前端不再拼多个全文 raw JSON 对象,而是按目标区域分别展示。

2.2 非目标

  1. 本期不要求 requirements 也做流式展示。
  2. 本期不要求彻底重写 worker 架构,只需按现有任务链路平滑接入。
  3. 本期不要求对所有非标准企业标书做到 100% 规则命中,允许 fallback。

3. 总体结构

3.1 最终结构

confirm

输出并缓存:

  • source_markdown
  • overview_range
  • scoring_range
  • requirements_range

worker

负责:

  • requirements_range -> requirements
  • 落库到 TenderRequirementTenderContent JSONB

parse-events

负责:

  • overview_range -> overview
  • scoring_range -> scoring
  • 两路并行 SSE

3.2 一句话结论

可以拆成两步,而且这是更好的方向:

  • confirm:只做全文 Markdown 提取和范围锁定
  • requirements:放 worker 先做并落库
  • parse-events:只并行做 overview + scoring 的 AI 流式

这样既轻量,又符合“重模型调用不要堆在 API confirm 上”的设计。


4. 为什么这样拆

4.1 为什么 confirm 不直接做重 AI

confirm 已经能拿到全文 Markdown,但它仍然属于 API 主链路。

如果在这里直接跑多个 AI 任务,会带来:

  1. API 请求时间被拉长。
  2. 多用户并发时容易把 API 线程/协程资源拖满。
  3. 和现有“worker 承担重解析任务”的设计初衷冲突。

因此,confirm 更适合做:

  1. Markdown 提取
  2. 章节范围识别
  3. 结果缓存

4.2 为什么 parse-events 只做 overview + scoring

前端最关注的首屏信息通常是:

  1. 项目名称、预算、工期、资格条件等概况
  2. 评分项与分值

requirements 往往:

  1. 文本更长
  2. 更分散
  3. 对首屏体验不如 overview / scoring 重要

所以把 requirements 后置到 worker,更适合当前体验目标。


5. 范围锁定设计

5.1 规则优先,模型兜底

优先基于全文 Markdown 做本地范围识别:

  1. 章节标题
  2. 页码标记(如 -第X页
  3. 目录关键词
  4. 表格/评分特征词

只有规则无法锁定时,才考虑模型兜底。

5.2 overview_range

优先锁定这些章节:

  1. 第一章 投标邀请
  2. 招标公告
  3. 项目概况
  4. 投标人须知
  5. 投标人须知前附表

目标范围通常在文档前部。

建议输出:

{
  "section": "overview",
  "page_start": 2,
  "page_end": 6,
  "text": "..."
}

5.3 scoring_range

优先锁定这些章节:

  1. 评标
  2. 评标办法
  3. 评审标准
  4. 综合评分法
  5. 详细评审
  6. 含明显“分值/评分项/评审因素/得分”表格的章节

建议输出:

{
  "section": "scoring",
  "page_start": 28,
  "page_end": 66,
  "text": "..."
}

5.4 requirements_range

优先锁定这些章节:

  1. 招标内容与技术要求
  2. 采购需求
  3. 主要商务要求
  4. 技术标准与要求
  5. 合同与验收

注意:

  1. requirements 可能跨多个章节。
  2. 允许识别为多个范围片段,后续 worker 合并处理。

6. 缓存与数据输出

6.1 confirm 阶段建议缓存内容

建议缓存到 TenderContent 或专用缓存结构中的字段:

  • source_markdown
  • overview_range
  • scoring_range
  • requirements_range
  • range_locator_version

推荐形态:

{
  "source_markdown": "...",
  "range_map": {
    "overview": { "page_start": 2, "page_end": 6, "anchors": ["第一章 投标邀请", "投标人须知前附表"] },
    "scoring": { "page_start": 28, "page_end": 66, "anchors": ["第五章 评标", "详细评审"] },
    "requirements": { "page_start": 16, "page_end": 27, "anchors": ["第三章 招标内容与技术要求"] }
  }
}

6.2 为什么要缓存范围而不是每次重算

  1. parse-events 首连更快。
  2. worker 和 API 可以复用同一份定位结果。
  3. 便于排查错位问题。
  4. 同一文件多次重连 SSE 时无需重复定位。

7. Worker 设计

7.1 Worker 负责内容

worker 读取:

  • source_markdown
  • requirements_range

执行:

  1. 提取 requirements 所需文本
  2. 可按要求范围继续 chunk
  3. 调用 AI 提取 project_requirements
  4. 落库

7.2 落库建议

当前项目已有 TenderRequirement 表,字段设计支持条目化结构:

  • category
  • content
  • sort_order
  • meta_data

因此当前阶段优先建议:

  • requirements -> TenderRequirement

如果后续发现:

  1. 读取需求条目的场景很少
  2. 更偏向“整体展示而非条目级操作”

则可以考虑改为先落 TenderContent 的 JSONB 字段。


8. parse-events 设计

8.1 输入

parse-events 不再直接吃全文,而是优先吃缓存好的:

  • overview_range.text
  • scoring_range.text

8.2 执行方式

建议并行:

await asyncio.gather(
    stream_overview(),
    stream_scoring(),
)

但实际 SSE 输出要通过统一事件队列合流,避免两个协程直接同时 yield 导致时序混乱。

推荐内部结构:

  1. 两个子任务分别解析
  2. 子任务把事件推入 asyncio.Queue
  3. SSE 主协程统一从队列取出并 yield

8.3 SSE 字段设计

建议前端不再接一个全文 raw JSON 串,而是按目标接收。

推荐事件:

pending

{
  "type": "pending",
  "stage": "ready",
  "message": "范围定位完成,开始解析 overview / scoring"
}

section_started

{
  "type": "section_started",
  "target": "overview",
  "page_range": [2, 6]
}

section_delta

{
  "type": "section_delta",
  "target": "overview",
  "text": "..."
}

section_result

{
  "type": "section_result",
  "target": "scoring",
  "data": [...]
}

done

{
  "type": "done",
  "targets": ["overview", "scoring"]
}

9. 前端展示建议

前端维护两个独立状态区:

  • overviewState
  • scoringState

处理方式:

  1. section_delta(target=overview) 只更新概况区域
  2. section_delta(target=scoring) 只更新评分区域
  3. section_result 用于最终校准

这样前端不再需要:

  1. 拼多个全文 JSON 对象
  2. 推断 chunk 边界
  3. 从混合流中再拆结构

10. fallback 设计

范围锁定不是 100% 成功,所以需要兜底。

10.1 overview_range 兜底

如果规则未命中:

  1. 取前 N
  2. 或取前部固定字符数
  3. 再做概况提取

10.2 scoring_range 兜底

如果规则未命中:

  1. 优先搜索“分值/评分/评审/综合评分法”等关键词密集区
  2. 若仍失败,再退回全文较后部范围

10.3 requirements_range 兜底

如果规则未命中:

  1. 退回第三章附近范围
  2. 若仍失败,则 worker 走全文 chunk 提取

11. 推荐执行顺序

建议按以下顺序实施:

阶段 1:范围锁定基础能力

  1. 新增范围定位函数
  2. 基于全文 Markdown 输出 overview/scoring/requirements 范围
  3. 将范围缓存到可复用位置

阶段 2:worker 接管 requirements

  1. worker 读取 requirements_range
  2. 提取 requirements
  3. 落库到 TenderRequirement

阶段 3:parse-events 双路流式

  1. overview 独立提示词与解析函数
  2. scoring 独立提示词与解析函数
  3. 并行执行并统一 SSE 合流输出

阶段 4:前端联调

  1. target 分区展示
  2. 加入 section_result 最终校准
  3. 不再拼全文 raw JSON 串

12. 当前实现与目标实现的差异

当前 parse-events 更接近:

  1. 全文切 chunk
  2. 单一路模型流
  3. 原始 JSON token 直出前端

目标实现应调整为:

  1. confirm 产出 source_markdown + range_map
  2. worker 处理 requirements
  3. parse-events 仅处理 overview + scoring
  4. 前端按目标区分展示

13. 最终建议

这是当前阶段最推荐的结构:

  1. confirm:只做 Markdown 提取与范围锁定
  2. worker:先落 requirements
  3. parse-events:并行流式 overview + scoring
  4. 前端:按 target 分区域展示,不再拼全文 raw JSON

这套方案兼顾了:

  1. API 轻量
  2. worker 承担重活
  3. SSE 更适合前端展示
  4. 中国标书相对稳定格式下的高命中率规则定位