跳转至

目录页数与字数分配规则

更新日期:2026-09-20。

本文汇总目前已约定并落地的目录篇幅规则,覆盖首次生成与导入、修改预设总页数、章内目录增删、整章删除、手动预算编辑及前端同步。保留原文件路径,作为本轮规则的统一说明。

注意:当前首次初始化按整数字数均分,修改预设及章内增删按整数页均分;手动预算编辑另有现存模式。下文分别说明,不将这些入口描述为完全相同的算法。

1. 名词与数据口径

名称 含义
预设总页数 Tender.page_count,用户设置的全文目标;特定删除或章内容量不足场景会回写
一级章节 第一章、第二章等;先获得章预算,再分配给本章末级目录
二级目录 第一节、第二节等;没有三级子目录时,自身作为一个分配节点
三级目录 二级目录下的正文编写节点,参与本章预算分配
末级目录 本章全部三级目录,以及没有三级子目录的二级目录
预计页数 节点的 estimated_pages,为预算,不是导出文件实际排版页数
预计字数 节点的 estimated_words,用于后续生成或重新编写的目标篇幅
已分配总量 汇总当前目录预算得到的全文页数、字数,可能与预设总页数不同
实际字数 已有正文的实际内容统计,与预计字数独立

章内均分以整个一级章的末级目录集合为范围,按目录树从上到下的顺序计数。二级目录有三级子目录时只汇总子目录预算,不再独立占一份,也不先给各二级节均分后再给三级分配。

例如一章中,二级 A 没有三级,二级 B 下有 B1、B2、B3:参与均分的是 A、B1、B2、B3,共四份。

只有一级章而没有任何二级目录时,一级章自身不承接正文预算,当前按 0 汇总。新生成目录仍要求一级章下有二级目录。

2. 配置与常量

配置或常量 默认值 用途
OUTLINE_WORDS_PER_PAGE 700 预算页数与字数的换算基准
OUTLINE_EXPANSION_PAGES_PER_SECTION 12 扩充判断中的单个三级目录预估容量;同时用于自动分配及页数编辑上限
OUTLINE_MAX_PAGES_PER_SECTION 12 末级目录页数硬上限,章内重分配和页数编辑不得突破
OUTLINE_EXPANSION_RATIO_THRESHOLD 0.6 扩充判断阈值,严格小于时命中
OUTLINE_MAX_WORDS_PER_SECTION 10,000 手动字数编辑的单个末级目录上限

前两个扩充配置集中在 app/core/config.py,其余篇幅常量在 outline_tree.py。12 是容量/上限,不是每次新增目录的默认分配页数。

章内整数页重分配和手动页数编辑的有效上限为:

L = min(OUTLINE_EXPANSION_PAGES_PER_SECTION, 12)

以下例子按默认配置,即每个末级目录最多 12 页、每页 700 字说明。

3. 各操作的行为总览

操作 重分配范围与方式 预设总页数如何处理
首次生成/完整目录导入 全文两层均分字数,末级按上下限截断 保留用户预设
修改预设总页数且值变化 全文两层均分整数页,末级封顶 保存用户新预设
保存时页数不变或未提交 不重分配,保留手动预算 不变
新增三级目录 本章全部新旧末级目录均分原章页数 正常不变;容量不足时回写全文实际页数
删除章内目录 本章剩余末级目录均分原章页数 正常不变;容量不足时回写全文实际页数
删除一级章 保留其他章预算,不重新均分全文 回写剩余章节页数之和
手动修改节点页数/字数 只改目标节点及其后代预算,汇总父级 当前不回写预设
读取/刷新/调整顺序 保留已有预算;需要时汇总父级 不因读取或排序重分配

预算变更只影响后续编写目标,不自动改写已有正文。删除节点本身仍会执行既有的正文软删除、关联编写思路及历史记录清理;保留节点的正文不会因重分配而改写。

4. 首次生成和完整目录导入

入口统一调用 OutlineBudgetAllocationService.initialize(),替代旧 Case1/Case2/Case3,不再按评分分支分配或追加篇幅。

设目标为 T 页、一级章数量为 M:

  1. 全文目标字数为 T × 700。
  2. 全文字数均分给 M 个一级章;整数字数的余数按章顺序逐个补齐。
  3. 各章字数再均分给本章全部末级目录;余数字数按目录顺序逐个补齐。
  4. 每个末级目录字数至少为 700,最多为 min(配置页数上限 × 700, 10,000);默认上限为 8,400 字。
  5. 根据末级字数计算预计页数,再逐级汇总。

一级章的理论页数不预先取整,避免丢失字数精度。触及上下限后不把溢出字数转分到其他目录或其他章,因此实际分配总量可能高于或低于目标。

示例:一章 10 页、三个末级目录,分配字数为 2,334、2,333、2,333,总计 7,000 字。各节点页数向上取整后都是 4 页,章预计页数为 12 页。

示例:目标 200 页,两个一级章分别有 5、10 个末级目录:

章节 理论章字数 实际每个末级字数 实际章页数/字数
A 70,000 8,400,达到上限 60 页/42,000 字
B 70,000 7,000 100 页/70,000 字

最终分配 160 页/112,000 字,预设仍为 200 页。A 的缺口不转给 B。

目标 1 页而有三个章、每章一个末级目录时,初始化受 700 字下限约束,实际分配为 3 页/2,100 字。

接入范围包括普通目录生成、自定义目录最终生成、旧生成入口的 initialize_outline_budget(),以及完整目录导入预览和应用。评分信息仍可用于生成内容,但不参与预算权重或预算初始化校验。

5. 整数页均分、余数和 12 页封顶

修改预设总页数、章内新增和删除共用整数页分配规则。设本章待分配页数为 P、末级目录数量为 N,有效单节点上限为 L:

容量 C = N × L
实际可分配页数 A = min(P, C)
基础页数 q = A // N
余页 r = A % N
前 r 个目录:q + 1 页
其余目录:q 页
每个目录预计字数 = 分配页数 × 700

N 为 0 时不做除法,章页数和字数归零。余数按当前树顺序从上到下分配,跨二级节保持这个顺序。

先限制可分配总量,再分余数,余页不会把已经 12 页的目录加到 13 页。

章待分配页数 P 末级数 N 最终分配 章实际页数
32 3 11、11、10 32
35 3 12、12、11 35
36 3 12、12、12 36
37 3 12、12、12 36
30 2 12、12 24
15 1 12 12

当前该算法不补 1 页下限:如果 P 小于 N,会有目录分到 0 页。例如 2 页分给三个目录为 1、1、0,避免凭空增加总量。这与首次初始化及手动编辑的下限不同,属于现有实现边界。

6. 修改预设总页数

保存接口为 PATCH /v1/tenders/{tender_id}。

  • 未提交 page_count,或者新旧值相同:不重分配,保留当前预算及手动调整。
  • 页数变化且已有目录:将新总页数按整数页均分到所有一级章,再按第 5 节规则分给各章全部末级目录;两层余数均按顺序补齐。
  • 没有目录:只保存总页数,等待首次生成时初始化。
  • 显式提交 0 或 null:清零已有目录预算,不补默认篇幅。
  • 本次新预设保留,即使部分章触及容量上限,也不按实际结果覆盖用户输入,不向其他章转移缺口。

例如新预设 31 页、两个章:A 获得 16 页,B 获得 15 页。A 有三个末级目录时为 6、5、5 页;B 有两个时为 8、7 页。

重分配覆盖原预算,包括原手动预算;目录结构、标题、编写思路、完成状态和已有正文保留,不调用目录或编写思路 LLM。

实现分工:

  • PATCH 路由只判断字段是否提交,并调用独立的 update_tender_page_budget()。
  • 页数更新 service 比较新旧值、复用目录修改锁、调用分配 service,并交给路由在同一事务提交页数与预算。
  • 正文生成期间沿用既有目录修改互斥规则。
  • 前端 usePatchTender 保存成功后刷新标书详情、目录、流程预算及正文统计缓存;等待已有流程状态缓存刷新后再跳转目录页。enter-outline 保留现有步骤流转职责。

7. 章内新增和删除目录

7.1 新增三级目录

在新增前汇总所属一级章的原页数 P,插入完成后重新统计本章全部末级目录 N,按第 5 节重新均分。

  • 所有新旧末级目录一起参与,不只分配新节点,也不只调整其所属二级节。
  • 新增节点不默认给 12 页或 5,600 字。
  • 前端新增弹窗已移除“每章字数设置”;旧客户端提交的合法预算值不参与新增三级目录的预算分配。
  • AI 批量新增先插入全部节点,再执行一次本章分配。连续多次请求各自按本次修改后的结构重分配。
  • 其他一级章预算保持原值。

例如原章 24 页、两个末级目录各 12 页,新增一个后为 8、8、8 页。

二级原来无子目录时自身占一份;新增三级后,二级退出独立分配,只汇总下方三级。例如原章 12 页、三个末级目录,其中一个是裸二级:该二级新增两个三级后,本章变为四个末级目录,全部重分配为 3 页,不是仅将原二级的 4 页拆成两份。

新增一级章、新增二级目录不属于本轮用户链路;既有接口保留,不扩展其业务约定。

7.2 删除章内目录

删除前保存所属章原页数 P,删除后将 P 均分给剩余末级目录,其他章不参与重分配。

  • 删除最后一个三级目录而保留二级父节点:二级恢复为末级目录,参与本章分配。
  • 删除一个二级目录:同时删除其子树,再统计本章剩余末级目录。
  • 删除章内最后一个二级目录:本章无分配节点,预算归零。

7.3 章内增删何时回写全文预设

  • P ≤ N × L:容量足够,保持原章页数和全文预设不变。
  • P > N × L:每个末级目录封顶,再以所有一级章实际页数之和更新 Tender.page_count。

例如 A 原 30 页,删除后只剩两个末级目录,A 变为 24 页;若其他章合计 40 页,全文预设回写为 64 页。

这里的回写规则适用于章内增删,与第 6 节“用户主动修改预设时保留新输入值”区分。

8. 删除整个一级章

删除一级章及其子树后,保留其他章预算,将剩余章页数相加写回全文预设,不再按旧总页数给剩余章重新均分。

删除前:总 60 页,A 10 页、B 20 页、C 30 页
删除 B 后:A 10 页、C 30 页,总页数更新为 40 页

删除最后一章后目录为空,预设和已分配总量均为 0。该规则替代此前讨论过的“删除整章后保持原总页数重新分配”方案。

9. 手动修改节点页数或字数

现存入口为 PATCH /v1/tenders/{tender_id}/outline/node,批量入口为 PATCH /v1/tenders/{tender_id}/outline/estimates/bulk。

编辑类型 单个末级范围 父节点含 N 个末级时 保存与回显
页数 1~L 页,默认 1~12 页 N~N×L 页 按分配页数 × 700 更新字数
字数 700~10,000 的整数字数 N×700~N×10,000 字 保留精确字数;页数向上折算,最多 12 页

手动请求越界时拒绝,不静默截断。仅编辑预算不改变全文预设,父级只汇总实际结果。

页数设置弹窗初次打开时显示已保存的实际字数,不用取整后的汇总页数反算原字数。只有页数与打开时不同,才按新页数 × 700 预览并保存;未修改或改回原页数时,显示原字数,点击确定只关闭弹窗、不发送预算更新请求。例如原预算 23,334 字、汇总 36 页,打开仍显示 23,334 字;改成 38 页时显示 26,600 字。

手动父节点编辑默认均分:前端不提交 redistribute_children,后端在未传或为 null 时均分给全部末级目录,不受原字数权重影响,余数从上到下补齐。分配仍保障各末级下限及上限,字数编辑保留精确总字数。

已有接口仍支持显式 equal 或 proportional;只有显式指定 proportional 才按原字数权重分配,当前前端页数设置入口不再选择该模式。比例分配达到上限的节点退出,余量继续分给未满节点;余数按小数部分大小补齐,同余数时按目录顺序。

手动设置 10,000 字时,字数仍为 10,000,预计页数为 12,并不会改成 8,400 字。默认自动分配的 8,400 字上限和手动字数编辑的 10,000 字上限是两种口径。

例如已有正文后把目标改成 3,000 字,当前正文原样保留;以后重新编写该节点时使用新目标。批量更新按请求顺序处理;节点不存在或最终预算超限则整批拒绝,不部分保存。

10. 汇总、读取与前端展示

对于需要从字数折算页数的末级目录:

预计页数 = min(12, ceil(预计字数 / 700))
父节点预计页数 = 子节点预计页数之和
父节点预计字数 = 子节点预计字数之和
全文已分配量 = 各一级章预算之和

12 页约束针对末级目录;一级章或有子目录的二级节点汇总后可以超过 12 页。

  • 缺少页数和字数时按 0 汇总,不再补默认 8 页/5,600 字。
  • 仅有页数时按页数 × 700 补字数;仅有字数时按上述公式补页数。
  • 已同时保存页数和字数时,读取保留原值并汇总,不借读取重分配或批量迁移历史数据。
  • 改标题、排序不主动重新均分预算;下一次发生重分配时使用修改后的树顺序分余数。
  • 整体替换子树的旧接口保留已填预算,缺失预算按 0 汇总;不能假定它等同于专用新增/删除链路。

page_budget 返回预设页数、已分配页数、已分配字数、三级节点数量、页数差值及提示。其中 level3_count 是实际三级数,不是所有末级节点数。

首次按字数均分的向上取整、最低篇幅、12 页封顶和手动编辑,都可能造成预设与已分配不同。现有超出预设提示保留;展示口径的进一步调整留待后续扩充流程讨论。

11. 0.6 扩充判断

由独立 OutlineExpansionService 判断:

R = 实际三级目录数量 × OUTLINE_EXPANSION_PAGES_PER_SECTION / 目标页数
R < OUTLINE_EXPANSION_RATIO_THRESHOLD:命中扩充条件
R ≥ OUTLINE_EXPANSION_RATIO_THRESHOLD:不命中
  • 默认参数为 12 和 0.6;等于 0.6 不命中。
  • 无有效正数目标页数时跳过判断。
  • 当前命中只记录 warning,正常放行,不调用扩充 LLM,也不阻断后续分配或生成。
  • 已接在目录生成的最终结果阶段;自定义目录中间规范化阶段跳过。PATCH 修改页数和章内增删未新增此判断调用。
  • 当前只统计实际三级节点;无三级的二级虽参与预算分配,但暂未纳入该判断口径。
  • 12 页封顶造成容量缺口,不保证命中 0.6;不额外增加扩充条件,留待后续细化。

12. 仍保留或待后续统一的边界

项目 当前处理
扩充 LLM 与扩充后的再次分配 尚未接入,现阶段 warning 后放行
无三级二级是否计入扩充容量 分配时计入,0.6 判断暂不计入
预设与已分配展示差异 保留现有展示和提示,后续讨论
首次字数分配与后续整数页分配 当前两种计算精度并存,见第 4、5 节
页数少于末级目录数量 整数页重分配可能有 0 页;首次初始化及手动编辑有下限
手动父节点编辑 前端不传分配方式,后端默认均分;接口仍保留显式比例分配能力
只有一级章而无二级目录 无末级预算;不自动补目录
历史数据 读取不批量重分配;专用增删可处理历史更深层末级节点,新建结构仍最多三级

这些边界是当前实现状态说明,不代表本轮新增兜底或已接入章节扩充。

13. 代码与验证索引

职责 位置
初始分配、整页重分配、手动编辑、预算汇总 outline_budget_allocation_service.py
修改预设总页数的编排 page_budget_service.py
标书 PATCH 接口 main.py
节点新增、删除及预算编辑接口 outline.py
0.6 判断及 warning outline_expansion_service.py
页字换算、预算汇总与读取 outline_tree.py
前端保存后缓存刷新 tender-app 仓库 services/tenders/index.ts 的 usePatchTender

相关测试用例:tests/test_outline_budget_initialization.py、tests/test_outline_add_budget.py、tests/test_outline_delete_budget.py、tests/test_outline_route_code_paths.py、tests/test_outline_word_limits.py、tests/test_outline_expansion_service.py、tests/test_tender_page_budget.py。

开发阶段已补充相关测试代码,检查范围为 Python 语法及差异检查;未运行测试、LLM 调用、前端构建或页面验证。本次文档整理不修改业务代码。