360 Secure SkillHub
360 安全技能中心

保险计划书分析大师

以消费者视角分析一款或多款保险产品:解析 PDF/Excel 计划书,判定险种与总利益口径,计算 IRR、回本与杠杆率,做分红压力测试和多品横向对比,最终输出 HTML 报告及可选的 PDF/Word/Excel。

danielshisafe
danielshisafe
热度 24.84
最近更新 2026-10-11 11:34:18
分类:数据依赖:代码执行 / 读写文件 / 联网浏览搜索应用场景:财务 / 报告撰写 / 数据处理 / 研究 ...

安装技能

复制下面的话术发给你的 Agent,它会通过 secure-skills 检索并安装该技能。

执行命令安装此 skill: npx secure-skills-cli add danielshisafe/insurance
Metadata
name
insurance-product-analyzer
description
面向消费者/投保人的保险产品分析器。当用户上传/粘贴/描述一款或多款保险产品(储蓄理财类如增额寿、年金、分红、两全、万能;健康保障类如寿/重/医/意外;财产险如车险/家财),希望「分析这款保险值不值」「对比几款产品」「算算 IRR/回本/总利益」「看懂条款坑点」时使用。支持单品深析与多品横向对比,内置红利/万能计算规则与消费者避坑清单。注意:本技能只做产品分析,不自动出保险计划书。
version
1.5.3
agent_created
true

保险计划书分析大师

一、角色与定位

消费者视角的保险产品分析器(不是销售工具,也不自动出保险计划书)。

目标:帮投保人客观看懂一款/多款保险产品的真实利益、坑点与适配性。

二、核心原则

  1. 消费者中立:不替任何公司站台,不替用户做投保决策。
  2. 结论基于用户提供的信息:本技能无实时产品数据库,所有数字来自用户上传/粘贴/描述的计划书、条款或参数。禁止编造产品数据。
  3. 演示 ≠ 保证:分红、万能的演示利益不代表保证,必须显式区分「保证」与「演示」两口径。
  4. 每篇末尾固定:消费者避坑清单 + 免责声明(见 references/templates.md)。
  5. 先总利益、再分项:对比时先比「总利益」这个统一口径,再拆细节。

三、何时使用(触发)

  • 用户上传/粘贴/描述一款保险产品,说「帮我看看这款保险」「分析一下值不值」。
  • 用户想「对比这几款」「哪款更划算」「算 IRR / 回本 / 总利益」。
  • 用户问「这款有什么坑」「条款我看不懂」。

四、工作流程

输入路由 → 险种识别 → 字段抽取
  → 定总利益计法(classify_policy.py · §7.16 五道判断题)
  → 构造 policy → 计算(irr.py) → 分红压力测试(stress_test.py)
  → 按险种模板分析 → 输出(DOCX/PDF+表格)

储蓄理财类必须先跑 classify_policy.py 再构造 policy。跳过这一步等于靠印象拼公式, 实测会漏项或双算(最严重一次总利益虚高 73.8%、IRR 从 2.97% 虚高到 7.61%)。

五、输入三路径(全保留)

  1. 上传解析(PDF / DOCX / Excel(xls,xlsx))——本环境原生支持:本 WorkBuddy 自带带库 Python 解释器(已装 openpyxl / python-docx / pdfplumber,已实测可用),可直接抽取结构化表格与文本。调用:
    • python scripts/extract.py <文件> → 输出 JSON(含 sheets/tables/pages 与文本),加 --md 输出 Markdown 便于人读核对。脚本会自动定位带库解释器,无需手动指定路径。
    • 优先抓取「总利益 / 现金价值 / 红利」等表格列(严格遵循 7.1、7.11 口径规则,注意「保证现价」与「总现金价值(保证+红利)」两列区别)。
    • 解析结果为空 / 仅为图片:若 docx / pdf 里的利益演示表是内嵌图片(extract 返回 tables 为空、只有图),数字读不出 → 转路径②(配合 OCR 技能)或路径③(粘贴参数),不得编造。
    • 复杂表格 / 多层嵌套可能需用户校正列映射。
    • 降级:仅当运行环境确实没有解析库(非本 WorkBuddy 环境)时,才明确告知并改用对话追问 / 粘贴参数,绝不编造字段。
  2. 图片(截图 png/jpg/jpeg)——可经 OCR 转写后分析:本环境具备本地 OCR 能力(如 ocr-local 技能,基于 tesseract.js;或 ocr-pro-v2、tencentcloud-ocr 等)。流程:
    • 用 OCR 将图片转写为文字/表格 → 按路径①抽取关键列 → 跑 irr.py。
    • 质量红线(极重要):OCR 对密集数字表格极易漏位/错位(实测 tesseract.js 在利益演示表上大量乱码)。凡来自 OCR 的数字,必须让用户核对「保费 / 现金价值 / 累计红利 / 总利益」等关键列后再计算 IRR;或直接请用户以文本/Excel 提供关键列。绝不用未经核对的 OCR 数字直接算 IRR(本会重蹈 IRR 算错的覆辙)。
    • 若环境无 OCR 引擎,提示用户:配合 OCR 技能、或把图片导出为 PDF / Excel(路径①)、或粘贴参数(路径③)。
    • 注意:图片里的具体数字必须由 OCR 或用户提供,本技能不得凭空生成。
  3. 对话描述 + 主动追问:用户口述产品要点,技能主动追问补全缺失关键字段(保费、缴费期、现金价值、领取方式等)。
  4. 粘贴/填写参数:用户直接给关键参数(保费、现金价值表、年度领取、红利、万能账户值等),技能据此结构化分析。

三条路径最终汇入同一分析引擎。若上传解析与用户描述冲突,以用户明确确认的参数为准。

5.1 产品说明书 vs 保险计划书(识别路由)

用户上传的文档可能是产品说明书(描述性,无个性化保费/年龄,只含示例利益演示)而非保险计划书(含具体被保人现金价值/IRR 数字)。两者分析目标不同:

  • 经 scripts/extract.py 抽取后,先跑 scripts/recognize_spec.py <extract.json> 判定文档类别并结构化识别关键产品属性:
    • 输出含 文档类别(初判)(产品说明书 / 保险计划书)、产品名称、险种类型、投保年龄、保险期间、交费方式、养老年金专属(开始领取年龄/保证领取期间/领取方式)、红利方式、保障责任(摘录)、责任免除(摘录)、条款要点关键词、利益演示、IRR适用性说明。
    • 说明书正确判定后,显式标注「IRR 不可直接算」:须用户提供具体被保人年龄/性别/保费/交费期,或对应计划书/利益演示表,方可走 scripts/irr.py 测算。
  • 识别器要点(已用德华安顾两份真实说明书验证):先 dedup_cjk 去中文文本层字符重复伪影;产品名允许「号」前后空格并 collapse;条款段落以「在本合同/因下列」为锚点优先抓真实条款、截到下一标题,不误抓末尾投保提示套话;万能型仅认「万能型/万能保险」并排除「非万能型」否定词;年金专属抓「按 N 个保单年度」式保证领取。
  • 若 recognize_spec.py 判为计划书且含个性化数字,则按「七」走 IRR 测算;判为说明书则只做属性识别与消费者提示,不编造 IRR。
  • 关键:说明书也常含「示例利益演示表」(如"30岁某先生/女士,年交保费X,交费Y年")。储蓄理财类产品必须把该示例演示表抽出来喂 scripts/irr.py 实算总利益 + IRR(这是本技能对储蓄理财类的硬性输出要求,不得只做属性识别而省略收益对比)。做法:
    • 自动链路(首选):python scripts/build_policy.py <extract.json> --name <产品> --run 会自动定位示例演示表、按表头关键词映射列(生存总利益/现金价值/累积红利/养老年金/年交保费)、稠密化年金、构造 policy 并直接跑出 irr_<产品>.csv。脚本优先采用现成「生存总利益」列(§7.11 铁律①)。
    • 手动兜底:用 extract.py 表格定位「保单年度/现金价值/累积红利/养老年金」等列 → 构造 irr.py 输入(premiums 按年交期;cv_guaranteed=现金价值;dividend_cv_demo=累积红利中性;terminal_bonus_demo=特别红利中性;benefit=养老年金),annuity 表稀疏须稠密化 carry-forward → 跑 irr.py。
    • ⚠️ 合并表头列错位:德华安顾等 PDF 的示例演示表常为「多行合并表头」,pdfplumber 抽取后表头列与数据列可能错位 1~5 列,build_policy.py 自动映射会取空→触发 WARNING 并输出全零。此时按 §7.11 铁律①直接用说明书打印的「生存总利益/总利益」列数值手工构造 policy(参考 examples/ 与「储蓄理财收益对比报告」做法),切勿拿全零结果跑 irr.py。验证列映射:跑 build_policy.py 后看其打印的「保费/交费期/最大年度」是否合理、再核对首年总利益是否等于打印值。
    • 结果须标注「示例口径、演示≠保证、非个性化方案」。
    • ⚠️ 图片型演示表(数据表是图,不是文本层):不少计划书(尤其微信/邮箱转发的 PDF、docx)把利益演示表整页存成图片,extract.py 只能抽出标题、表格数为 0。按以下顺序处理,不要一上来就让用户补数据:
      1. 先验真伪——用 pypdfium2 把该页按 300~400 DPI 渲染成 PNG,统计版心区域的暗像素占比。若 ≈0%,说明源文件的演示表本来就是空白页,直接判定「数据缺失」并在报告中写明是源文件问题,不要反复折腾 OCR。
      2. PDF 走渲染——pip install pypdfium2,page.render(scale=dpi/72).to_pil()。⚠️ 不要用 pdfplumber 的 page.to_image(),遇到 4bit 图像会抛 Unsupported bitspercomponent: 4。
      3. docx 走内嵌媒体——解压 word/media/*,按像素面积排序,面积最大的几张通常是数据表页。
      4. OCR 优选 Windows 内置引擎(免下载模型、中文开箱可用):pip install winocr,然后
        img = Image.open(p).convert("RGB")
        res = asyncio.run(winocr.to_coroutine(winocr.recognize_pil(img, "zh-Hans-CN")))
        for line in res.lines:
            for w in line.words:
                b = w.bounding_rect   # 有 x/y/width/height,可还原表格行列
        
        ★ 两个坑:recognize_pil 返回的是 IAsyncOperation 不是协程,必须经 winocr.to_coroutine() 包装才能 asyncio.run;返回值不支持 res["lines"] 下标,要用属性 res.lines / line.words / w.bounding_rect.x。
      5. OCR 兜底:rapidocr-onnxruntime(纯 pip,自带模型)。⚠️ 在部分 Windows 机器上 onnxruntime 会报 DLL load failed ... 动态链接库(DLL)初始化例程失败,降级 numpy/onnxruntime 通常无效,此时直接改用第 4 步的 Windows 内置引擎,不要在这上面耗时间。
      6. ★ OCR 结果必须落回原文核对:识别出的数字按年度做单调性/量级校验(现金价值一般逐年递增、首年远小于年交保费、末年趋近保额)。数字对不上就在报告里说明「OCR 可能失真」,绝不用目测估值填数(§二原则 2)。
      7. 全部失败 → 按 §7.11 数据可靠性红线处理:报告中该产品的现价/IRR/回本留空,明确写「源文件无数据,已尝试渲染+OCR 三重验证」,并请用户提供数字。

六、险种识别

判断归属并路由到对应模板:

  • 储蓄理财类(重点做深):终身寿、年金、分红型、两全、万能等所有储蓄/理财型。组合型(如「年金(附加万能)」「年金分红(附加万能)」)先拆主险+附加险。
    • ⚠️ 投连险(投资连结)单列处理(见 §7.13):投连险不纳入 IRR / 总利益测算,也不与其他理财类产品做收益对比;改为「穿透底层资产」分析。多产品对比中若含投连险,投连险仅出底层资产穿透分析,排除在 IRR 与理财性质对比之外。
  • 健康保障类(基础版):寿险、重疾险、医疗险、意外险。
  • 财产险类(基础版):车险、家财险、责任险。

险种识别不确定时,向用户确认后再分析。

6.1 ★★ 名字带「终身寿」必须再做一次定额/增额判定(先看这里)

「终身寿险」是两种完全不同的产品共用的名字,不判定就开算必然出错:

定额终身寿(杠杆寿) 增额终身寿(储蓄型)
保额 终身恒定(如固定 100 万) 逐年复利递增
钱花在哪 主要买身故保障 主要进现金价值增值
现金价值 几十年才回本,长期远低于已交保费 通常 7~10 年回本,之后快速超越
IRR 长期 0~1%,算 IRR 意义不大 有意义,是核心指标
该怎么评 ★ 杠杆率(保额 ÷ 保费)= 性价比 总利益 / IRR / 回本

★ 为什么必须分开:把杠杆寿的 IRR 当成评价指标,会把「保障做得好的产品」误判成「收益差的产品」。 实测德华安顾鑫传家(男30 / 100万 / 20年交):IRR 只有 0.94%,看起来像"很差", 但它第 1 年就能用 2.65 万撬动 100 万身故保障(37.7 倍)——这 0.94% 恰恰说明保费都花在保障上了。 反过来,若拿杠杆率去评价增额寿,也会得出荒谬结论。

判据(按可靠性排序,已固化进 scripts/classify_life.py):

  1. 主身故/全残保险金列是否恒定(★ 决定性)—— 恒定 = 定额;逐年递增 = 增额。 增额寿的身故赔付 = max(现价, 保额),后期随现价递增,这条几乎不会错。
  2. 基本保额 / 有效保险金额列是否递增 —— 递增 = 增额。
  3. 产品名含「增额」—— 弱信号(不少增额寿产品名并不带「增额」二字)。
  4. 首年现价 ÷ 年交保费 —— 增额寿通常 20%~80%,定额寿常低于 15%。 ⚠️ 趸交不适用,见下。
  5. 现价回本速度 —— 增额寿通常 ≤15 年,定额寿常 ≥30 年。⚠️ 趸交不适用。

⚠️ 趸交陷阱:趸交(一次交清)时首年现价天然接近甚至超过已交保费、回本天然在第 1 年, 判据 4、5 会把趸交的定额传承型产品误判成增额寿。脚本已自动跳过这两条(缴费期 ≤1 年时)。

动作:

python scripts/classify_life.py --extract <抽取.json> [--sum-assured 1000000]
python scripts/classify_life.py --extract A.json B.json C.json --compare   # 多品横向比杠杆
  • 判为定额终身寿 → 按 §7.18 杠杆寿口径分析(主看杠杆率;现价有就顺带算,但不作为评价指标)。
  • 判为增额终身寿 → 按「七、储蓄理财类」做 总利益 / IRR / 回本 全套。
  • 无法判定(脚本会明说缺哪一列)→ 暂停自动分析,直接问用户「这款是定额还是增额」, 绝不猜。猜错会让整份报告的口径从头错到尾。
  • 置信度非「高」时,在报告里写明判定依据,别悄悄下结论。

七、储蓄理财类对比方法论(深化,核心)

7.1 统一「总利益」锚点

所有形态(增额寿、年金、分红、万能、两全及组合)最终折算成同一条指标:

总利益(N) = 现金价值(N) + 累计已领生存金/年金(N) + 红利价值(N) + 万能账户价值(N)

每款产品同时给**「保证总利益」和「演示总利益」**(演示取中档、标注低/高档区间)。异形态(年金定期领 vs 增额寿减保取现)之所以能同台比,正是靠这个统一口径——落到「第 N 年把保单整体兑现/累计拿到手的总额」是同一个概念。

⚠️ 这个公式是「并集」,没有任何一款产品会全部命中,不要照抄。 真正决定某一项该不该加的是这笔钱的去向:年金进了万能就不能再累加年金;红利现金领走了就不能按存量计。 算任何产品前先走 §7.16 的五道判断题(大形态 → 是否分红 → 哪几种红利 → 红利去向 → 哪些流进了万能), 走完每一项该如何计就唯一确定了。可自动化: python scripts/classify_policy.py --name "<产品名>" --columns "<逗号分隔列名>"。


★★★ 零保额铁律(与红利、万能无关,先记住这条)

生存总利益衡量的是「第 N 年把保单兑现能拿回多少钱」。保额(保险金额)不是钱,一分都不能进。

总利益的每一个组成项都必须是现金价值口径,没有任何一项是保额:

组件 要取的值 ⛔ 绝不能取
主险 现金价值 / 退保金 基本保额
现金红利(累积生息) 累积现金红利(含息) 红利保额
增额红利 / 交清增额 增额红利的现金价值 累积红利保额
终了红利 终了红利的现金价值 终了红利保额
特别红利 特别红利的现金价值 特别红利保额
万能账户 账户价值 / 账户现金价值 万能账户保额
年金 / 生存金 各年实际领取额 年金保额
  • 「保额」只用于识别红利种类,绝不用于取值。 §7.16 第③步靠「红利保额」这个列名认出它是增额红利, 但取的数必须来自「增额红利的现金价值」列(或已含在合计列里)。
  • 踩坑代价极大且很隐蔽:增额红利的保额量级常与基本保额相当(几十万), 若被当成现金价值加进总利益,实测演示口径总利益虚高 42.9%、IRR 从 4.56% 虚到 6.01%(+1.45pp)、 回本年限从 8 年提前到 6 年——数字看着更漂亮,所以特别容易被当成「算对了」。
  • 唯一例外:列名里同时写了「现金价值」(如「累积红利保额对应的现金价值」),那才是可取的口径。
  • scripts/irr.py 的字段表里没有任何保额字段——只有 cv_*、benefit_*、dividend_cv_*、 dividend_cash_*、universal_*、terminal_bonus_*。若你在构造 policy 时发现自己在填某个 带「保额」的列,立刻停下重读本节。

★ 总利益抓取铁律(防止 IRR 算错,最重要):

  1. 优先用现成的「总利益 / 生存总利益 / 账户价值合计」列,直接抓取,绝不自行用公式重算。这是最准、最不会出错的来源。
  2. 只有当文档未给总利益列时,才用组件求和公式(7.11)。
  3. 关键口径(权威分解式):计划书里的「现金价值」列大概率是不含红利的保证现金价值。因此总利益通常 = 现金价值 + 各红利现金价值 +(年金)万能账户价值,详见 7.11。但若表的「现金价值」列已≈总利益(明显已含红利),则红利不再另加——此时以「总利益」列为准。

一句话:有总利益列就用它;没有才按 7.11 公式求和;求和后用总利益列(若有)做校验,避免重复计入或漏计。

7.2 双口径

  • 保证总利益:只计保证部分(保证现价 + 保证红利,常≈0 + 万能保底利率)。
  • 演示总利益:含演示红利/结息(若分低/中/高档,取中档为基准并标注区间)。

对比时先比保证总利益,再附演示总利益;演示 ≠ 保证的提醒放在旁边。

7.3 组合型先拆主附险、分别算值再汇总

  • 年金(附加万能)= 生存金自动进万能账户复利 → 总利益 = 主险现价 + 万能账户值。
  • 年金分红(附加万能)= 年金(带分红)主险 + 万能账户 → 总利益 = 主险现价(含红利) + 万能账户值。
  • 增额寿分红 = 现价随分红推高保额而增长。
  • 增额寿(终身寿)= 纯现价增长。

做法:先识别组件、分别算各组件价值,再求和得该产品总利益;报告标注「含万能中档演示」等,防止糊涂账。

⚠️ 年金 IRR 评估窗口:年金险算 IRR 必须测至给付期末(见 7.10.1),禁止在领取首年/早年退保点算 IRR(会算出负数伪影)。组合型年金(年金+万能)的生存总利益 = 主险现价(含红利) + 万能账户值;万能中档演示时须核对「现金红利是否进入万能」避免重复计入。

7.4 对比轴 = 保单年度(保单年末)

  • 横轴统一用「第 N 保单年度」,里程碑取关键保单年度(回本、第 10/20/30 保单年度、满期保单年度)。
  • 年龄不作为对齐轴:被保人起始年龄不同(如 0 岁 vs 30 岁)时,直接看「持有第 N 年」谁总利益高,避免错配。
  • 仅当所有参与产品起始年龄相同时,才在保单年度旁附一行年龄标注(如「第 10 保单年度 ≈ 40 岁」)。

7.5 保费 / 缴费期不同 → 仅比 IRR

若参与产品保费基础不可比(年缴/总保费/缴费期不同),总利益无直接可比性 → 省略总利益对比表,仅展示 IRR 对比(保证/演示)并注明原因;仅当保费基础可比(同总保费/同年缴)时才给总利益对比表。IRR 始终展示(它本是费率归一化的)。

7.6 现金流形态维度(必附)

总利益只反映「总额」,不反映「何时能拿到」。对比必附「现金流形态」说明:

  • 增额寿:自主减保取现(时间自由,需主动操作)
  • 年金:约定年龄起定期定额自动领(收入流)
  • 两全:满期一次性领
  • 万能:部分领取(灵活,受限额/手续费)

让用户看清「同样总利益,一个给终身工资、一个给灵活备用金」。

7.7 保证 vs 演示摆动显式标注

万能/分红产品的演示总利益高度依赖演示利率。对比表中保证与演示分两行/两列,并标注利率假设(如万能中档 4.5% / 保底 1.5%;分红中档)。提醒:演示 ≠ 保证。

7.8 红利 / 领取方式假设披露

每款产品标注其假设(红利领取方式:累积生息 / 交清增额 / 进万能;年金领取频率与起始年龄)。不同假设导致总利益构成不同,必须透明。

7.9 满期 / 期限不对齐

各产品满期年可能不同(如 80 岁 vs 终身 vs 60 岁)。总利益按保单年度对齐(7.4);满期总利益仅在各自满期年出现并标注「产品 X 满期于第 N 年」,不强行对齐满期。

7.10 IRR 计算时点明确

IRR(N) = 假设持有至第 N 保单年度终止的内部收益率,用 XIRR(不规则现金流):

  • 增额寿:现金流 = −已缴保费(各缴费年) … + 第 N 年现金价值(减保则含已领)。
  • 年金:现金流 = −已缴保费 … + 各年领取(至 N) + 第 N 年末现价/满期金。
  • 多持有期都给(回本、第 10/20/30 年、满期)。

★ 保费截断铁律(易错,已修复并验证):算第 N 年末 IRR 时,只计入 t < N 的保费 (premiums[n] 发生在 t=n,n=0 为投保时)。第 N 年末退保/终止时,第 N 年初及以后的保费 尚未缴纳,绝不能计入现金流。否则当 N < 缴费期 时会把未缴保费算成流出, 使 NPV 在全区间同号 → XIRR 误返回 NA(或算出失真的极低值)。 实例:10 年交年金算「第 1 年末退保」若未截断,现金流里凭空多出 9 期 × 5 万保费, XIRR 返回 NA;截断后正确值为 −63.5%(= 18,250 / 50,000 − 1,可手算校验)。

具体计算交给 scripts/irr.py,不要由大模型心算(易错)。

★ 制图年度范围:make_chart.py 的右轴 IRR 自动适应数据范围。若把「第 1–5 年退保」 这类极负值(常见 −60% ~ −90%)并入,右轴会被拉到 −99%,各产品的 IRR 曲线挤成平线而 失去横向对比意义。做法:图表从第 8(或第 10)保单年度起绘制,完整年度数据放在 IRR 表格里,并在图旁注明图表起始年度与原因。

与 §7.10.2 的关系:缴费期年度在 CSV 里已是「缴费期」文本而非数值, make_chart.py 解析不出数字会跳过该点(不会当成 0 画到轴上), 所以右轴不会再被缴费期的极负值拉爆——但这只是兜底, 仍需按上面的规则从缴费期结束后起画,否则曲线左端会缺一截、读者会以为是数据缺失。

7.10.1 年金险 IRR 测算细则(最重要易错点,已用真实说明书验证)

年金 IRR 极易算成负数,根因是「评估期截断」而非产品缺陷。必须遵守:

① 年金「生存总利益」公式(用于构成 IRR 现金流)

年金(分红 + 万能)生存总利益(N) = 年金现金价值(N) + 现金红利处理值(N) + 增额红利现金价值(N) + 终了红利现金价值(N) + 万能账户价值(N)

其中现金红利处理值按领取方式:

  • 现金领取:逐年现金领取金额(已流出保单,在现金流中作为「现金红利现金流(正)」体现,不计入末年现价);
  • 累积生息:累积红利(含息)现值;
  • 进入万能:不计入(已并入万能账户价值)。

若无万能账户 + 现金红利不累积(现金领取 / 累积生息,本类最常见):第 N 年 IRR 现金流 =

保费(各缴费年, 负) + 年金现金流(第1..N年已领, 正) + 现金红利现金流(第1..N年已领/累积, 正) + 第 N 年当年的[年金现金价值 + 增额红利现金价值 + 终了红利现金价值]

(末年终端价值 = 保单现价,不含未来未领年金;已领年金与已发红利作为历年正现金流进入。)

② 评估期必须覆盖整个给付期(致命规则)

年金险 IRR 必须测到给付期终点(如至 105 周岁 / 第 75 保单年度),不得用「给付期内某年退保」的 XIRR 当产品 IRR。

原因:年金自约定年龄起定期给付,若在领取首年/早年退保,只收到 1~数期年金、且年金现金价值往往归零,XIRR 会被巨额已缴保费压制成负数或极低(实例:鑫福年年8号第30年演示 IRR 算得 -1.99%、保证 -9.64%,纯属「仅领1期 vs 缴50万」的截断伪影,非产品缺陷)。

正确做法:

  • 领取前(积累期,如 yr5–25):可报「领取前退保 IRR」——退保拿回 现金价值+累积红利+特别红利,是合法退保点,可作积累期对比。
  • 领取后:只报「持有至给付期末 IRR(yr75 / 至105岁)」作为产品 IRR 代表值;中途退保快照(yr40/50/60)仅作「积累总额」参考,必须标注「中途退保快照(舍去后续年金)」,不得当作产品 IRR。
  • 横向对比年金 → 以 yr75 全给付期 IRR 为对齐基准;积累期比 yr10–25(均为合法退保点)。

③ 稠密化(cashflow 构造必做):年金演示表常只列关键年(30/35/40…),须 carry-forward 把年金自首次给付年起逐年补齐(年金自起领年起恒为定值),否则 Σ养老年金少计 → IRR 假阴性(实例曾因稀疏表只计 10 期而非 46 期而算成负)。

④ 一句话自检:年金 IRR 若算出负数 → 先查「是不是在给付期内某年退保算的」与「年金表是否稠密化」,而非怀疑产品。年金产品 IRR 代表值 = 持有至给付期末那一档。

7.10.2 缴费期不展示 IRR 数值(★ 强制显示规则)

规则:保单年度 N 尚未过缴费期时,IRR 单元格只写「缴费期」三个字,不写任何数字。

判断口径 = 连续缴费前缀 P:从第 1 年起每年都有保费的年数。第 1~P 年全部写「缴费期」, 第 P+1 年起才显示正常 IRR。

缴费方式 写「缴费期」的年度 首个显示 IRR 的年度
趸交 第 1 年 第 2 年
年交 100 万 × 5 年 第 1~5 年 第 6 年
年交 × 10 年 第 1~10 年 第 11 年
年交 × 20 年 第 1~20 年 第 21 年

为什么:保费没交完时的 IRR 是「只交了一部分钱、账户才刚起步」的算术产物, 实测常见 −80%、−61%、−45% 这类极端负值(§7.10 已解释成因)。它不代表产品差, 但读者极易误读成「巨亏」,对决策毫无参考价值,所以干脆不展示。

四条落地要求:

  1. 缩略表同样适用。第 1 / 5 / 10 年这类关键年度摘要、IRR 摘要一行、压力测试表, 只要该年度落在缴费期内,一律写「缴费期」。
  2. 报告里必须配一句解读,不能让读者以为漏算了。可直接用:

    第 1~P 年为缴费期,保费尚未交完,此期间 IRR 会出现 −60% 之类的极端负值, 对决策没有参考价值,故不展示数值;保费现金流仍逐年完整计入, 第 P+1 年及以后的 IRR 已包含全部已缴保费。

  3. ★ 铁律:这是纯展示层规则,绝不能反向影响计算。 现金流里每一笔已缴保费必须照常计为流出。irr.py 的 build_flows 按 n < N 截断 (第 N 年末退保时尚未缴纳的保费不计)——这是正确的,不是漏算。 自检:第 P 年计入现金流的保费累计额,必须恰好等于 P 年的保费之和。 实测:10 年交产品第 10 年现金流含全部 10 期保费(原始 IRR −0.188%,被隐藏), 第 11 年仍是 10 期保费 → −0.159%;5 年交 100 万产品第 5 年含 500 万 → 隐藏后第 10 年 0.438%。
  4. 报告内不得自相矛盾:irr.py 与 stress_test.py 已内置同一口径 (payment_span() 连续前缀 + premium_base() 下标基准探测), 两处对同一个保单年度不会一个写「缴费期」、一个写数字。

★ 实现要点(改代码时别踩):

  • 用连续缴费前缀,不要用「最后一期保费所在的年度」。否则「3 年交 + 第 10 年追加」 这类保单会把第 4~9 年也误判成缴费期,白白遮掉 6 个有正常 IRR 的年度。
  • 必须做 premiums 下标基准探测。build_policy.py 产出 0 基 (premiums[n] 在 t=n,首期 t=0),但手工 JSON 常见 1 基(premiums[0] 是占位 0, 保费在 premiums[1..P])。若不做探测,连续前缀会在第 0 位就断掉、 payment_span() 返回 0 → 整个遮罩静默失效,表上又冒出 −30% 这种数。

7.11 红利 / 万能计算规则(组件求和法)

当计划书/说明书直接给出「总利益/生存总利益/账户价值合计」 → 直接抓取,不再逐项算(最准)。仅在缺失时走组件求和:

总利益(N) = 主险现金价值(N) + 附加险现金价值(N) + 红利价值(N) + 万能账户价值(N)

★ 总利益重建公式(无现成总利益列时按此求和):

增额寿(分红型)生存总利益 = 增额寿本身现金价值(保证,不含红利)   + 现金红利的现金价值(累积生息→累积现金红利(含息);进万能→暂不计入,已并入万能账户;现金领取→不计入,已流出)   + 增额红利的现金价值   + 终了红利的现金价值   + 特别红利的现金价值

年金(含分红/万能):在以上基础上 + 万能账户价值,合计为总利益。

简化:很多产品没有那么多类型的红利,没有的类别直接不参与求和。

★ 防错「三步自检」(求和前必做,分红型必做):

步骤 操作 判定与处理
① 看列 文档里有没有「总利益/生存总利益/账户价值合计」列? 有 → 直接用此列,跳③,不要求和。
② 重建 无总利益列时,按上面「总利益重建公式」求和。注意:「现金价值」通常是不含红利的保证值,需把各红利现金价值加上去;若某表的「现金价值」列已明显≈总利益(已含红利),则红利现金价值记 0,不重复加。 得到重建总利益(N)。
③ 核对 若文档另处有「总利益」列,比较 重建值 ≈ 该列? 偏差 > 1% → 停下,疑似重复计入/漏计/读错数字,回②重判或请用户以文本/Excel 提供数值。

★ 真实表格常见「两列现金价值」的识别(极易踩坑):

很多分红型演示表同时给出两列:保证现价(保证、不含任何红利)与 总现金价值(保证+红利) / 演示现金价值(= 保证现价 + 现金红利,红利已含在内)。正确映射:

  • 保证口径 → 用 保证现价。
  • 演示口径 → 用 总现金价值(保证+红利) 作为演示现金价值;不要再把现金红利单列加回去(否则重复计入、IRR 虚高)。
  • 若表另列 特别红利/终了红利/增额红利 等独立红利列,则:总利益 = 总现金价值(保证+红利) + 独立红利列。这正是「总利益」列本身的构成。
  • 一句话:凡列名已写「含红利/保证+红利」的,红利已在内;只有列名是「保证」的才需补加红利。

★ 保费现金流时点(错一位 IRR 就失真,已用真实数据验证):

scripts/irr.py 中 premiums[n] 发生在第 n 年初(t = n 年,按 365 天/年):

  • 趸交:保费在投保时 → premiums[0] = 趸交额,其后全为 0。
  • 期交(如 3/5/10 年交):premiums[0..k-1] 各填年保费,对应第 1…k 保单年度初;其后为 0。
  • 切勿把趸交/首期保费错放到 premiums[1](=第1年末/第2年初),否则持有期被算短一年 → IRR 明显虚高(实测:30万趸交第5年 IRR 从正确 1.65% 虚到 2.07%)。

红利价值(N) —— 核心是「红利的现金价值」而非「红利保额」(§7.1 零保额铁律):

  • 现金红利-累积生息:计入「累积现金红利(含息)」。
  • 现金红利-现金领取:不计入总利益(已流出保单),可单列提示「已领 X」。
  • 现金红利-进入万能:已并入万能账户价值 → 不单列。
  • 增额红利 / 交清增额:按表是否单列「增额红利现金价值」决定——单列则相加,已含进「现金价值」列则不另加(用③校验确认)。
  • 终了红利:国内产品多为每年确定给付 → 按年度计入总利益,参与逐年对比;归类按计划书标注归入保证或演示。
  • 特别红利:特定时点给付,按给付时点计入。

万能账户价值(N):抓取「万能账户价值/现金价值」(非保额);若红利/生存金流入万能,已含在内,不重复。仅年金/带万能的组合才加这一项。

★ 数据来源可靠性红线(防读错数字):

  • 若数据来自截图 / 图片 / PDF 内嵌图,且模型无法可靠读取图中数字(视觉估算、模糊、分辨率低、列错位)→ 禁止凭印象填数,必须请用户按 references/templates.md 的「数值录入模板」以纯文本 / Excel / CSV 形式提供关键列(保单年度、年保费、现金价值、累计红利、总利益)。
  • 任何数值若与文档明显矛盾(如总利益 < 累计保费且非早期、IRR 异常 > 6% 与实际演示利率不符),先质疑数据而非结论,回看原始表或请用户复核。

每份分析加一行「计算口径说明」,明说总利益含了哪些、没含哪些(如「现金价值不含红利,已加累积现金红利与终了红利,进万能部分计入万能账户」「现金价值已含红利,红利未重复计入」),让结论透明。

7.12 两全险 · 满期生存金按「年金分配」计入 IRR

两全险的「满期生存金 / 满期保险金」是一笔到期一次性大额给付,IRR 计算与年金同源:把它当作「一笔大额年金分配」,计入满期年的 benefit 流入,与年金险年金现金流同一套算法:

  • 现金流构造:保费(各缴费年, 负) + 各年领取(增额寿/年金成分, 正) + 第 N 年满期生存金(正) + 第 N 年末现金价值(正)。即 benefit[满期年] = 满期生存金,其余年 benefit 为 0(或含少量生存金)。
  • 评估期 = 持有至满期年(第 N 年):与年金同理,禁止在满期前退保点把 XIRR 当产品 IRR——满期前退保只拿现金价值、未领满期金,IRR 偏低甚至为负纯属「截断伪影」,非产品缺陷。满期前仅可报「积累期退保 IRR」作参考。
  • ★ 总利益(满期) = 满期生存金 与 现金价值(满期年) 取其一,不可相加(§7.16 坑 3): 满期即合同终止,保单现金价值归零,满期金本身就是这笔钱的对价。 因此 cv[满期年] 应填 0(或确认表中该列确实为 0),满期金全落在 benefit[满期年]。 若表在满期年仍列有非零现价,必须核实:那通常是「满期前身故/退保」口径列, 与满期金分属两种终止方式,相加即重复计入。红利按中档计入演示口径。
  • 自动链路:build_policy.py 的 locate_columns 已把列名含「满期保险金 / 满期生存金 / 生存保险金」的列识别为 benefit 列(满期年单列、其余年空),无需手工处理;手工构造时令 benefit[满期年] = 满期金 即可。

7.13 投连险 · 穿透底层资产(不纳入 IRR)

投连险保单价值 = 持有单位数 × 单位价格(净值),随底层资产波动、不保证,无法套用「总利益(保证/演示)」统一口径,故:

  • 投连险不纳入 IRR / 总利益测算,也不与其他理财类产品做收益对比(见 §六路由)。
  • 分析方式 = 穿透底层资产:列出各投资账户及其底层资产类别、比例、性质,重点说明:
    • 资产类别与比例(如权益类 / 固定收益类 / 货币类 / 另类);
    • 性质与风险(投向、风险等级、流动性、历史波动);
    • 费用结构(初始费用、买卖差价、账户管理费等,直接侵蚀收益);
    • 不保证收益、客户自担投资风险的提示。
  • 数据来源:用户提供的计划书/说明书「投资账户 / 底层资产」章节;本技能不自动抓取实时净值(无实时数据库,见 §二原则2),仅基于文档披露做结构说明。
  • 多产品对比含投连险:投连险仅出「底层资产穿透分析」块(§7.13 上述维度),排除在 IRR 表 / 总利益表 / 现金流形态收益对比之外;其余理财类产品正常比。报告中明示「投连险因价值随净值波动、不保证,未纳入收益对比」。

7.14 文本层 PDF · 利益演示表「坐标重建」(0 表格时的标准解法)

症状:计划书 PDF 有文字层(extract.py 能抽出 text),但 tables 为 0 个; 且文本里数字被打断成多行片段,例如 1,000,00 与 0 分属两行、4,390,58 与 7 分属两行。 此时 build_policy.py 必然失败,直接按文本正则抓数也必错。

根因:这类 PDF 用绝对定位绘制文本,没有表格线;同一单元格内的长数字因换行被拆成 多个文本片段,片段间距(约 4pt)小于真实行间距(约 18pt)。

解法(已用中信保诚三款计划书验证):用 scripts/parse_pdf_table.py 做坐标重建。

# 1) 先诊断:全文档扫描,看行数 / 年度连续性 / 列宽分布 / 数据页
python scripts/parse_pdf_table.py <计划书.pdf> --scan --out rows.json

# 2) 看表头,确认每一列的含义(必须先做,别猜列序)
python scripts/parse_pdf_table.py <计划书.pdf> --page 12 --header

# 3) 看数据行每格的 x0,与表头对齐确定列位
python scripts/parse_pdf_table.py <计划书.pdf> --page 12 --dump

关键参数:--row-gap(默认 10.0)。实测「行内断行片段」间距 ≈ 4.1pt、 「相邻逻辑行」间距 ≈ 18.7pt,阈值取 10 刚好把断行片段并回同一逻辑行。 若扫描结果有缺年或列宽不一致,优先调这个参数(可试 6~14)。

★ 解析后必做的三件事(防错铁律):

  1. 校验年度连续无缺失(脚本 --scan 会自动诊断)。有缺年 → 参数不对,重调。
  2. 用表头确认列序再建模,尤其先定位「总现金价值 / 生存总利益 / 账户价值合计」列 (§7.11 铁律①:有总利益列就用它,绝不自行重算)。分红型计划书的列常达 26 列, 保证组与演示组结构对称,务必靠 --header 的 x0 对齐来区分,不要凭经验猜。
  3. 用行内恒等式交叉校验,例如 「基本保额对应现价 + 累积红利保额对应现价 = 总现金价值」、 「保证现价 + 保证红利 = 保证总利益」。对不上就是列位认错了。

常见列结构参考(供对照,仍以实际表头为准):

产品类型 列数 关键列
增额终身寿(分红·增额红利) 26 通列 0 年度 / 1 年龄 / 2 年缴 / 3 累计;保证组 4–14,其中总现金价值(退保金)=10;演示组 15–25,总现金价值=21
终身年金(非分红) 9 生存金=4,累计已领=5,身故=6,现金价值(退保金)=7
定期年金(分红·现金红利) 14 生存金=4,累计已领=5,满期金=6,身故=7,现金价值=8,累积现金红利 保证=11 / 演示=12

红利利率反推:若计划书未标注红利储存生息利率 / 保额递增率,可用相邻两年的 「上年累积 + 当年红利 → 本年末累积」反推:r = (cur − cur_d) / prev − 1。 多算几年取均值(实测中信保诚、中英人寿的演示均落在 1.75%)。 反推结果必须在报告的「计算口径说明」里显式披露。

注意:反推出的利率是演示假设值,不是保底利率;且 §十二.6 规定演示利率直接采用文档标注值, 反推只在「文档未标注」时用于理解数据结构,不得用反推值去替代合同约定的保证利率。

7.15 分红实现率压力测试(理财类含分红产品 · 必附)

为什么必做:理财类报告如果只给「保证 IRR」和「演示 IRR」两个数,等于只给了区间的两个端点—— 一个默认红利为 0,一个默认红利 100% 兑现,而真实情况几乎总在中间。 「演示 ≠ 保证」光靠一句免责声明没有说服力;压力测试把这段区间摊开成可比较的数字。

核心公式(组件线性缩放):

压力总利益(N, X) = 保证总利益(N) + X × [ 演示总利益(N) − 保证总利益(N) ]

对每个可缩放组件(现金价值 / 累积红利 / 生存金 / 万能 / 终了红利)分别缩放后求和。 benefit(年金领取)按流处理(Σ benefit[1..N]),其余按存量处理(取第 N 年末值)。

  • X = 0 → 红利为零,结果应等于纯保证口径(必须与 irr.py 的保证 IRR 对得上,作为自检);
  • X = 1 → 完全兑现演示,结果应等于演示口径(同理与 irr.py 的演示 IRR 自检)。

★ 压力测试必须输出三项指标(缺一不可):

指标 含义 为什么重要
IRR(N)% 该实现率下持有至第 N 年末的 XIRR 收益率的区间,是主指标
翻倍年数 总利益首次达到「累计已缴保费 × K」的保单年度(K 默认 2) 把「多久能翻倍」这个消费者最直觉的问题变成数字;K=1 即回本年数
临界实现率 追平指定目标 IRR 所需的最低实现率 把「要不要赌红利」变成阈值判断,是全部表格里决策价值最高的一列

临界实现率的基准怎么选(二选一,报告中写明用的是哪个):

  1. 以同批对比中的无分红产品为基准(推荐):用 --benchmark 无分红产品.json, 脚本自动取它第 N 年的保证 IRR 作为目标。含义是「红利要兑现到几成,这款分红险才能追平 那款不用赌的产品的地板收益」——这个问法对消费者最有用。
  2. 直接指定 IRR 目标:--target-irr 2.0。含义是「想拿到 2% 的收益,需要红利兑现到几成」。

复利修正(reaccum):若红利是累积生息型(红利留存公司按某利率复利), 线性缩放「累积红利」会有偏差(复利是非线性的)。此时应改为按 当年红利 × X 重新累积:

acc[y] = acc[y−1] × (1 + r) + 当年红利[y] × X

做法:在 policy JSON 里补 dividend_annual_guaranteed / dividend_annual_demo(当年红利), 脚本 --mode auto(默认)会自动启用,并自动反推储存生息利率 r(从累积红利与当年红利递推, 取中位数;实测中信保诚 1.75%、中英人寿 1.75%)。反推出的 r 要在报告「计算口径说明」里披露。

档位与呈现:

  • 档位默认 0 / 25% / 50% / 75% / 100% 五档;0% 与 100% 两档不可省(用于与 irr.py 自检)。
  • 表格形态:实现率 × 产品 → (IRR% , 翻倍年数, 总利益),末行附「临界实现率」。
  • 无分红产品也要放进表里:它的每一行都相同,正好作为「不用赌红利」的对照基线, 并在临界实现率那行标注「无分红」。

调用:

python scripts/stress_test.py --input A.json B.json --names "A" "B" --years 75 \
       --benchmark C.json --times 2 --out stress.csv
# 常用变体:--rates 0,30,60,100  自定义档位
#           --times 1            看回本年数(而非翻倍)
#           --target-irr 2.0     直接给 IRR 目标
#           --mode simple        关闭复利修正

⚠️ 措辞红线(必须遵守):

  • 这是假设情景压力测试,不是对保险公司历史分红实现率的评价。 本技能无实时实现率数据库,不抓取、不对比历史实现率(见 §十二.7)。
  • 报告中必须写明「假设情景」,且X=0 不代表红利真会为零。
  • 若用户自行提供了各产品的历史实现率数据,可另做展示;但不得把压力测试的 X 与历史实现率混为一谈。

7.16 生存总利益 = 五道判断题(★ 不要背公式,走决策树)

★ 核心原则:生存总利益怎么算,不是背公式,是走一道自上而下的逻辑判断题。 从大框架到小细节,每步只回答一个问题,走完自然得到唯一正确的计法。

为什么要这样:§7.1 那个「现金价值 + 累计年金 + 红利 + 万能」是并集, 没有任何一款产品会全部命中。背它必然会漏项或双算——实测已酿成 73.8% 的虚高(见坑 1)。 改成判断题后,每一项「该不该加、按流还是按存量」在走完五步后是唯一确定的。

自动化:python scripts/classify_policy.py --name "<产品名>" --columns "<逗号分隔列名>" 会替你走完这五步,直接输出「建议 policy 字段映射 + 生存总利益公式 + 冲突检查」。 解析 PDF/Excel 拿到的第一批信息就是列名,所以拿到列名就先跑一次。


第 ① 步:大形态是什么?

判定依据 结论 总利益的骨架
名称含「投资连结/投连」 投连 不算 IRR/总利益,走底层资产穿透(§7.13),判定结束
名称含「两全」/ 有「满期金」列 两全 满期金按一笔大额年金 → benefit[满期年]
名称含「年金」/ 有「生存保险金/养老年金」列 年金 cv(N) + Σbenefit(1..N)
名称含「终身寿险/增额」 增额终身寿 价值都在 cv,没有流

② ⑤ 步之前先做一件更省事的事:看列名里有没有合计列 (生存总利益 / 总利益 / 总现金价值 / 账户价值合计,注意排除「身故/全残总利益」)。 有 → 按 §7.11 铁律① 直接抓该列,五步走完只用于校验,红利字段全部留空(红利已含在合计列里)。 没有才继续往下做组件求和。

第 ② 步:是不是分红型?

判定:名称含「分红型」,或列里出现任何含「红利」的列,或 policy 里有 dividend_* / terminal_bonus_*。 否 → 直接跳第 ⑤ 步。 是 → 进第 ③ 步。

第 ③ 步:分红有哪几种?(可多选,逐项勾选)

红利种类 列名特征 怎么计
现金红利 现金红利、当年红利、年度红利、累积红利 → 进第 ④ 步定去向
增额红利 / 交清增额 含「保额」:红利保额、累积红利保额、交清增额 ⛔ 取增额红利的现金价值列;保额本身不进总利益。看该现价列是否单列:单列则相加,已含进合计列则不另加
终了红利 终了红利 直接取这部分红利的现金价值(累积值,非当年值),不是保额
特别红利 特别红利 直接取这部分红利的现金价值(不是保额),按给付时点计入

⚠️ 判定顺序不可调换:先判「保额」再判「现金」。 累积红利**保额** = 增额红利,累积**现金**红利 = 现金红利,两者同名不同物。

⚠️ 「保额」在这里只是识别红利种类的线索,不是要取的数(§7.1 零保额铁律)。 认出它是增额红利之后,要取的是「增额红利的现金价值」。若表只给了保额、没给对应现价, 说明它已折算进「总现金价值」列——直接抓合计列,不要自己拿保额去乘任何系数估算。

第 ④ 步:现金红利的去向是什么?(决定按流还是按存量)

去向 判定线索 policy 字段 计法
累积生息 列名含 累积/累计/含息/储存生息 dividend_cv_* 存量 dividend_cv(N)
现金领取 列名含 现金领取/直接领取 dividend_cash_* 流 Σdividend_cash(1..N)
进入万能 列名含 进入万能/转入万能/红利转万能 不填任何 dividend_* 只计 universal(N)
未知 — — 停下来回计划书确认,去向不明不许算

增额红利、终了红利、特别红利没有这一步——它们都直接取各自的现金价值即可 (增额红利多一个「是否已含在 cv 里」的判断,用合计列或总利益列校验)。

第 ⑤ 步:有没有万能账户?若有,哪些现金流进了万能?

判定:列名含 万能、结算利率、保证利率,或 policy 有 universal_*。

无 → 结束。 有 → 只问一个问题:哪些流进了万能?

现金流 是否进万能 进了怎么办
年金 / 生存金 常见 benefit_in_universal = true,不再累加 Σbenefit
现金红利 常见 不填 dividend_*,只计 universal
增额红利 / 终了红利 / 特别红利 通常不进 保持原计法,照常加各自的现金价值

★ 全部重复计入都发生在这一步。 一笔钱一旦进了万能账户,它的价值就只存在于账户余额里; 在账户之外再加一遍,就是双算。年金和现金红利可以同时进万能(见案例 C), 而终了/特别红利留在主险里照常单算——这就是「只关注哪些流进了万能」的含义。

⚠️ 唯一一处列名判不准、必须翻计划书的地方:万能账户与现金红利并存时, 「累积红利」既可能是留在主险累积生息(要加 dividend_cv(N)),也可能是红利已转入万能 (一律不加,只计 universal(N))——计法完全相反,列名却一模一样。 判断办法:看计划书「红利领取方式」选的是哪一项;或看万能账户价值曲线是否已被红利明显抬高。 classify_policy.py 遇到这种组合会直接告警,看到就别蒙,回去确认。


三个真实案例走查(照着套即可)

A 诚耀人生(年金·分红) B 金耀未来(增额寿·分红) C 年金分红+万能(复合)
① 大形态 年金 增额终身寿 年金
合计列 无 有:总现金价值(退保金) 无
② 分红 是 是 是
③ 分红构成 现金红利 增额红利(红利保额) 现金红利 + 终了红利
④ 现金红利去向 累积生息 — 进入万能
⑤ 万能 无 无 有;年金+现金红利进万能,终了红利不进
总利益(N) cv + Σbenefit + dividend_cv 直接取「总现金价值」列 cv + universal + terminal_bonus

案例 C 正是「年金和现金红利都进万能、终了红利留主险」的典型—— Σbenefit 和 dividend_cv 都不出现,只有 cv + universal + terminal_bonus。


附表 1 · 给付去向互斥矩阵(√ 计入 / × 不计 / — 该形态无此组件)

(决策树走完后用它做一遍交叉核对)

给付去向 cv 现价(存量) benefit(流) dividend_cv(存量) dividend_cash(流) universal(存量)
年金/生存金 · 现金领取 √ √ — — —
年金/生存金 · 自动进万能 √ × 已入万能 — — √
红利 · 累积生息 √ — √ — —
红利 · 现金领取 √ — × √ —
红利 · 进万能 √ — × 已入万能 — √
红利 · 交清增额 √(红利已含在现价内) — × — —

附表 2 · 常见组合速查(决策树的快捷路径,仅用于交叉核对)

险种形态 生存总利益(N)
增额终身寿(非分红) cv(N)
增额终身寿(分红·增额红利) cv_demo(N) —— 红利已含在现价里,不另加
年金(非分红) cv(N) + Σbenefit(1..N)
年金(分红·现金红利累积生息) cv(N) + Σbenefit + dividend_cv(N)
年金(分红·现金红利现金领取) cv(N) + Σbenefit + Σdividend_cash(1..N)
年金 + 万能(生存金进万能) cv(N) + universal(N) —— 不另加 Σbenefit
两全(满期终止) 满期年:满期金 与 cv(满期年) 取其一(见坑 3)
投连 不适用,走底层资产穿透(§7.13),不进 IRR

附表 3 · 六个高频坑(均有实测数据支撑)

  1. 年金进万能重复计入(最严重):年金既算进 Σbenefit、又已复利进 universal,同一笔钱计两次。 实测某年金+万能示例第 30 年:错值 367,422 / 正确 211,422,虚高 73.8%;IRR 从 2.965% 虚到 7.609%。 → policy 里设 "benefit_in_universal": true。irr.py / stress_test.py 已内置启发式自检, 发现 benefit 与 universal 同时非零 ≥3 个年度会直接告警。
  2. 现金领取红利按存量计:dividend_cv 是存量,会把多年红利整笔压在第 N 年末,损失时间价值。 实测 10 年每年领 3,000:IRR 3.198% vs 正确 3.594%,低估约 0.4 个百分点。 → 现金领取的红利请填 dividend_cash_*(按流),不要填 dividend_cv_*。
  3. 满期金与满年末现金价值相加:满期即合同终止、现价归零,满期金就是这笔钱的对价。 若表在满期年仍列出现价,先确认它是不是「满期前身故/退保」列——不可与满期金相加。 (实测中信保诚诚耀人生满期年 cv(75)=0、满期金 5,078,895 全落在 benefit(75),口径正确。)
  4. 身故赔付混入生存总利益:生存总利益不含身故/全残保险金。抓列时排除「身故/全残总利益」列 (build_policy.py 已自动排除);手工构造时不要把这些列的值填进 cv。
  5. 把保额当现金价值算进总利益(§7.1 零保额铁律):累积红利保额、终了红利保额 这些是保险金额不是钱。实测误用后总利益虚高 42.9%、IRR 从 4.56% 虚到 6.01%(+1.45pp)、 回本年限从 8 年提前到 6 年。之所以危险,是因为结果看起来更漂亮,容易被当成算对了。 → 每项都取现金价值口径;build_policy.py 会拦下保额列并告警,classify_policy.py 会点名列出。
  6. 万能账户 + 现金红利并存时把红利也算进去:累积红利 列名在「留在主险累积生息」和 「红利已转入万能」两种情形下完全一样,但前者要加 dividend_cv(N)、后者一律不加(只计 universal)。 蒙错一次就把整笔红利双算。→ 回计划书看「红利领取方式」,或看万能账户价值曲线是否已被红利抬高; classify_policy.py 命中此组合必告警,有告警不许跳过(第 ⑤ 步已详述)。

附表 4 · 口径边界(易漏,须写进报告的「计算口径说明」)

  • 领取时点:benefit[n] 默认发生在第 n 年末。若条款约定「保单周年日/年初」领取, 真实 IRR 会略高于脚本值;差异随持有期收敛,但假设必须在口径说明中写明。
  • 万能账户取「现金价值」而非「账户价值」:前几年退保/领取有费用,账户价值 > 实际可领金额。 有「万能账户现金价值」列时优先用它。
  • 追加保费 / 趸交追加:流入万能的追加投入要计入 premiums(现金流出),否则 IRR 虚高。
  • 保费豁免:触发豁免后未缴的保费应从 premiums 剔除,否则流出被高估、IRR 偏低。
  • 减保 / 部分领取:按流计入 benefit,且 cv 必须取减保后的现价(同一份表内口径要一致)。
  • 终了红利 / 特别红利:填累积值(存量),不是当年值;按计划书标注归入保证或演示。

7.17 抽取层五大静默陷阱(★ 全量回归实测,均已写成代码守卫)

本轮对 12 份真实文档做全量回归时挖出的五个「不报错但算错」的坑。它们的共同特征是 退出码为 0、CSV 正常生成、数字看起来像那么回事,只有交叉核对才会暴露 —— 比崩溃危险得多。 现已在 build_policy.py / irr.py / recognize_spec.py 中全部加了守卫,此处记录以便人工排查时对照。

  1. 稀疏「保单年度」列 → 保费少算、IRR 成倍虚报 合并单元格会让首列只在第 5/10/15… 年有值,其余为空。 旧逻辑以「首列第一个纯数字行」当表头结束,把前几行真实数据当表头吞掉; 且年份为空的行被整行 continue。 实测某年金:实为年交 100 万 × 5 年 = 500 万,脚本只抓到 1 期 100 万, IRR 从真实的 -0.2% 虚报到 29.4%。 → 现改为「首行含 ≥2 个纯数字单元格即数据行」+ 年份空则按上一年度 +1 递推。

  2. 康熙部首字符 → 整表列定位失守 部分 PDF 的 ToUnicode 把汉字映射成康熙部首(U+2F00–U+2FDF,如 ⾦ U+2FA6 代 金、 ⽣ U+2F63 代 生)。「现⾦价值」匹配不到关键词「现金价值」,locate_columns 把 prem/cv/g/d/ann 全部判为 None,随后静默产出空结果。 → 现统一做 unicodedata.normalize("NFKC", ...) 还原。

  3. 逐字重复伪影 → 整表判为「未找到演示表」 部分 PDF 用重叠字重模拟加粗,文本层每个汉字出现两次(保保\n单单\n年年\n度度)。 recognize_spec.py 早有去重,build_policy.py 此前没有,导致关键词全部失配。 → 现两边都做 dedup_cjk。

  4. 多层合并表头 → 保证档/演示档认错列 三层表头「生存总利益 ‖ 低档/中性红利 ‖ 万能最低保证利率/中性演示利率」抽取后, 子列只剩最下层标签,丢了上层分组名,低档与中性会被认串。 → 现改为先行内右向填充、再按列纵向拼接(合并单元格的标准还原法)。

  5. 「身故/全残保险金」列被当成「生存总利益」→ 演示 IRR 严重高估 is_sa() 只认「保额」「保险金额」,认不出「身故/全残保险金」(少一个「额」字)。 宽表(实测 56 列)表头重建时,「生存总利益」与「对应的身故/全残保险金」会被拼进 同一个标签,如「总利益演示生存总利益对应的身故/全残保险金中性」。 此时该列数据是身故赔付量级,却被映射成演示生存总利益。 实测某分红终身寿:首年正确值 保证 1,970 / 演示 2,005,而身故总利益列首年是 16,000 (第 5 年 80,000 vs 正确 27,000)—— 脚本把 16,000 那列选作了演示口径。 → 新增 is_death() 守卫:标签含「身故/全残」且不含「现金价值」的列,一律不进任何价值字段。 → 同时把「总利益全为 0」从 WARNING 升级为 exit 1 硬失败(此前会照常落盘、继续跑 irr.py)。

通用教训:抽取层任何一步「匹配不上」都不能退化成跳过或取 0,必须显式报错。 宁可让 build_policy.py 返回 exit 1 逼人走手工路径,也不要给出一份看起来合理的错误数字。

★ 排查手法(比改代码更重要):列映射是否选对,不能只看标签,要拿数值交叉核对。 储蓄类产品在缴费期内「生存总利益 < 累计已交保费」几乎是铁律(首年交 1 万、生存总利益 2,005); 若某列首年值反而大于已交保费(如 16,000 > 10,000),它八成是身故/全残给付列,不是价值列。

7.17b 对比层静默陷阱:同产品两档现价「某年后完全重合」

分析了德华安顾鑫传家的两个档位(男30 / 100万 / 20年交,年交 26,500 vs 23,780)后发现:

  • 两档现金价值从第 41 保单年度(71 岁)起逐元完全相同,此前最大差额 41,160 元, 差额曲线先升后降、到 71 岁精确归零。
  • 原因:贵的那一档 = 便宜档 + 一份定期附加责任(18–70 岁的「身故/全残额外关爱金」50 万)。 附加责任在第 40 保单年度(70 岁)到期,其现价自然归零,之后只剩同一份主险,故两档重合。 (已核对计划书表格:客运意外/猝死/额外关爱金三列在第 41 年同时由 100 万/10 万/50 万变为 0。)

为什么这是陷阱: ① 若只看到「71 岁后两档现价一样」,容易误判「多交的钱后来都退回来了」——恰恰相反, 多交的 54,400 元一分不退,现价相同只是因为附加险已到期消失。 ② 若只看末年 IRR(0.94% vs 1.11%),会以为只是费率差异,看不出多交的钱买的是纯消费型定期保障。

★ 判定与处理手法(遇到同一产品多档位时必做):

  1. 逐年度算两档现价差额,找峰值年与归零年。
  2. 归零年若等于某个整数年龄界限(70 / 71 / 80 / 88 岁),几乎可以断定差额是定期附加责任。
  3. 报告中必须拆开算一笔账:多交保费总额 / 附加保额 = 每万元保额的纯消费成本, 并列出不同退保时点的「多交保费 − 多退现价 = 净成本」。
  4. 结论措辞要明确:附加险越晚退保越「贵」(净成本随时间上升,到期后等于全额保费), 它不是储蓄,是消费型保障。

7.18 杠杆寿(定额终身寿)分析口径

判为定额终身寿后走本节,不再按 §7 的收益口径组织报告。

7.18.1 主指标:杠杆率体系(这才是杠杆寿的"性价比")

指标 公式 怎么读
首年杠杆 身故保额 ÷ 首年保费 第 1 年撬动多少倍,最直观地体现"杠杆"
总保费杠杆 身故保额 ÷ 缴费期总保费 ★ 掏完所有钱之后还能撬动几倍,横向可比的核心指标
峰值赔付杠杆 责任期内最高身故赔付 ÷ 总保费 最有利单一情景(互斥不叠加)能到几倍;必须附「构成明细 + 极端性注解」(见 §7.18.3)
单位保障成本 总保费 ÷ 保额(万元) ★ 每买 1 万元身故保障要花多少,越低越划算;与总保费杠杆互为倒数
逐年静态杠杆 第 N 年身故赔付 ÷ 累计已交保费 随持有年限自然衰减的曲线
退保差额 第 N 年累计已交保费 − 第 N 年现金价值 正数 = 若当年退保白花多少(保障的净花费);负数 = 现价已超保费

单位保障成本与总保费杠杆是同一个事实的两种说法,报告里二选一主打即可,不要并列当两个结论讲。

7.18.2 现金价值与 IRR 怎么摆

  • 计划书给了现金价值就照算(用 irr.py),但放在「退保能拿回多少」小节, 不放进结论速览的第一行,也不作为产品优劣的评判依据。
  • 必须写的一句提醒(实测吃过亏,不加这句读者一定误读):

    IRR 长期在 0~1% 是杠杆寿的本来面目——保费主要被拿去买身故保障了, 剩下来的才进现金价值。这不是产品差。

  • 回本年度(现价 ≥ 累计保费)照写,但它衡量的是"退保划不划算",不是"产品好不好"。

7.18.3 多品对比

  • 只有形态一致(都是定额终身寿)才能放进同一张杠杆表直接比。 混入增额寿必须分开讲——两者评价标准完全不同(一个看杠杆,一个看 IRR)。
  • 保费行不打★(保额/责任不同时"便宜"可能是保障缩水); 只在已按保额归一化的指标上打★(首年杠杆、总保费杠杆、单位保障成本)。

★ 对比表必须单列「缴费期(年)」这一行(用户强要求)

因为 总保费 = 年交 × 缴费期,缴费期直接决定总保费,进而决定总保费杠杆:

指标 受缴费期影响吗 说明
首年杠杆(保额 ÷ 首年保费) ❌ 不受 只跟第 1 年交的钱有关
总保费杠杆(保额 ÷ 总保费) ✅ 受很大影响 交得越久,总保费越高,杠杆越低
单位保障成本(总保费 ÷ 保额万元) ✅ 受很大影响 同上,交得越久越"贵"
峰值杠杆(峰值赔付 ÷ 总保费) ✅ 受影响 分母是总保费

⚠️ 关键:同一款产品拉长缴费期,总保费杠杆必然下降、单位成本必然上升, 但这纯粹是数学必然,不代表产品变差——拉长缴费反而提高了首年杠杆、 降低了每年缴费压力,是"用时间换保障"的合理安排。

  • 缴费期相同(如都是 10 年交)→ 总保费杠杆 / 单位成本可以直接比大小、可以打★。
  • 缴费期不同 → 总保费杠杆不能直接比。此时:
    1. 重点看 首年杠杆(不受缴费期影响,反映"第一次交钱就撬动多少");
    2. 或按缴费期分组后再比总保费杠杆;
    3. 或在报告里明确说明"因缴费期不同,总保费杠杆不可直接比较"。
  • classify_life.py --compare 已自动加「缴费期(年)」行, 并在缴费期不一致时自动打印告警(说明为什么不能直接比、该看什么)。
  • 必出「出险情景 × 产品」赔付矩阵(classify_life.py --matrix): 一般身故/全残 · 猝死 · 其他意外 · 客运交通意外 · 航空意外 · 动车高铁意外 · 附加责任到期后。 因为同样是 100 万保额,附加责任不同,实际赔付可能差 3 倍(实测 100 万 ~ 315 万)。
  • ⚠️ 附加责任多有到期日(如「18–70 岁」),到期后赔付归零,必须单列一张「附加责任与到期年龄」表。

★ 峰值赔付的三条叠加铁律(算错就会让产品错误登顶):

  1. 互斥场景不叠加——航空意外与动车高铁意外不可能同时发生,不能相加。 实测:信美某产品误加后 425 万 / 8.36 倍,正确为 315 万 / 6.20 倍,虚高 35%。
  2. 上位概念要展开——「客运交通工具」通常含飞机、火车(含动车高铁)、轮船、营运客车, 应同时计入航空 / 动车 / 客运三个场景。只归到一格会严重低估 (实测德华安顾:250 万被少算成 150 万)。
  3. 「通用意外」类附加适用于所有意外场景(如「意外身故 +15 万」); 但猝死不认定为意外,不叠意外类附加。

★ 峰值赔付必须做「构成 + 极端性」注解(用户强要求,漏了等于误导):

  • 不能只给一个数字。classify_life.py 现在会在单品与对比里同时输出:
    • 构成明细——峰值 = 一般身故/全残保额 + 无条件附加 + 命中该情景的附加责任, 逐项列出(如 合计 315万 = 一般身故100万 + 关爱金100万 + 航空意外100万);
    • 极端性标签——情景分四类:常规 / 特定(非意外) / 交通意外 / 极端小概率 (航空、动车高铁、自然灾害属此类,发生概率极低)。
  • 报告里必须写清:峰值赔付是单一出险情景下的金额,互斥情景不可叠加—— 航空/动车/自驾同属交通意外,现实中只能发生其一;交通意外与猝死、一般身故亦互斥 (一次身故只赔一种情景),故峰值 ≠ 各项之和,不可当作可拿到的「总额」。
  • 极端情景的峰值杠杆高 ≠ 日常杠杆高:带「极端」标签的峰值,要在报告里提醒读者 「不可据此判断产品性价比,日常身故杠杆请以『总保费杠杆 / 一般身故赔付』为准」。
  • 矩阵脚注已内置上述注解;单品 show() 在杠杆率表后单列「峰值赔付构成」块; 对比表新增「峰值情景」行并逐款列构成。三者口径一致,照输出即可。

7.18.4 报告结构(定额终身寿)

1. 结论速览(★ 第一行放杠杆率,不是 IRR + 一句「为什么不看 IRR」)
2. 形态判定(写明依据与置信度;置信度非「高」要照实写)
3. 杠杆率对比(四个核心指标 + 算式 + 打★规则)
4. ★ 出险情景 × 产品 赔付矩阵(互斥/上位/通用三条铁律 + 峰值构成/极端性注解)
5. 峰值赔付构成注解(单品必列:由哪几部分组成 + 是否极端小概率情景)
6. ★ 杠杆率走势图(`leverage_chart.py`,横轴自动截断,见 §7.18.5)
7. 附加责任与到期年龄(★ 到期归零,最容易漏)
8. 逐年杠杆衰减与退保差额(若有现价)
9. 现金价值与回本(参考信息,附「IRR 低不等于产品差」的提醒)
10. 加档成本拆解(若同产品有多档位,见 §7.17b)
11. 怎么选(按**出险情景**匹配,不按 IRR)
12. 计算口径与免责声明

7.18.5 杠杆率走势图(scripts/leverage_chart.py)

杠杆寿必附一张走势图(零依赖纯 SVG;与理财类的 make_chart.py 是两个脚本,不要混用)。

坐标轴设置(★ 保额放左轴,不要跟杠杆率共右轴)

轴 内容 理由
横轴 保单年度 —
左轴(金额) 累计已交保费 · 一般身故赔付 · 基本保额(水平虚线参考) 保额本质是金额,与累计保费同量纲,放一起才能直接比「交的钱 vs 能赔的钱」
右轴(倍数) 杠杆率 = 一般身故赔付 ÷ 累计已交保费 独占右轴

⚠️ 常见错误:把「保额 + 杠杆率」一起放右轴。 保额是 100 万级、杠杆率是 1.9~78 倍,量级差 4 个数量级,共用一轴必有一条被压成贴底直线。 ⭐ 画「一般身故赔付」而不是只画「保额」:身故赔付含无条件附加(关爱金), 能看出附加责任到期时的断崖跳变(实测德华安顾第 41 年 150 万→100 万)。 保额只是恒定 100 万的水平线,单独画信息量很小,留作水平参考虚线即可。

★ 横轴必须截断(后期是死重量,占一半画布却没信息)

  • 自动规则:横轴画到「最后一个还在变化的年度 + 3 年」(--tail 3,可调), 轴末端画折断符号,并注明「第 N 年后各指标恒定,已截断(实际保障至第 M 年)」。 「最后一个变化年」= 累计保费与身故赔付都停止变化的年度(缴费结束 / 附加责任到期的较晚者)。
  • 实测:德华安顾 A 档第 41 年(71 岁)关爱金到期后恒定 → 截断到第 44 年(74 岁)。 多品对比取各款稳定年的最大值再留余量,保证每款的断崖都在图内。
  • 手动覆盖:--xmax 30。

★ 线性 vs 对数(--log) 杠杆率是 1/x 衰减(实测德华 A:第 1 年 56.6x → 第 2 年 28.3x → 第 5 年 11.3x → 第 20 年 2.83x → 第 41 年 1.89x 后恒定):

  • 线性(默认):如实呈现「前几年断崖式衰减」,适合给客户看; 但第 5 年之后几乎全挤在底部——缴费结束与附加责任到期这两个台阶在杠杆率线上 相差不到 10 像素,基本看不见(金额线上的断崖仍然清晰)。
  • 对数(--log):跨度压缩后分布均匀,后期台阶清晰可见(第 41 年断崖落差约 128 像素), 适合自己做分析或需要展示附加责任到期影响时。
  • 建议:给客户的报告用线性(配文字说明关键年度数值);分析用对数;也可两张都放。

调用

# 单品
python scripts/leverage_chart.py --extract A.json --out lev_A.svg
# 对数刻度
python scripts/leverage_chart.py --extract A.json --log --out lev_A_log.svg
# 多品对比(只画各款杠杆率曲线)
python scripts/leverage_chart.py --extract A.json B.json --policy p.json \
    --mode compare --out lev_cmp.svg
# 多款 + single:逐个出图,文件名自动加 _1 _2 …
python scripts/leverage_chart.py --extract A.json B.json --mode single --out lev.svg
# 手动控制截断:稳定后再留 6 年 / 直接截到第 30 年
python scripts/leverage_chart.py --extract A.json --out lev.svg --tail 6
python scripts/leverage_chart.py --extract A.json --out lev.svg --xmax 30

脚本会打印每款的「最后变化年(对应年龄)」,便于核对截断位置是否合理。

完整成品样例见 examples/sample_杠杆寿对比_4款.md (4 款真实计划书的完整对比:德华安顾鑫传家 ×2 档 + 信美传承美好 ×2 档)。

7.19 ★ 抽样年度演示表:别把「第几个数据点」当成「第几年」

实测踩到的坑,会影响 classify_life / leverage_chart / irr 全部脚本。

产品说明书的利益演示表常常不是逐年连续的,只给抽样年度。 实测德华安顾鑫传家 / 传世臻耀只给 1–15、25、35、45、55、65、70 共 21 行; 传家臻享给 1–10、15、20、30…75;信美传家有道给 1–10、20、30…65。

此时 「第 i 个数据点」≠「第 i 年」。若直接用索引当年份,会产出 「第 17 年、年龄 70 岁」这种自相矛盾的输出(实测鑫传家把第 35 年数据报成第 17 年, 回本年度与逐年表全错)。

处理规则:

  1. 绝不插值、不补点——缺失年度一律留白(与「数据缺失不要估算」同一原则)。
  2. policy.json 用 years 字段显式给出每行真实年度;脚本优先用 years[i], 取不到才回退 range(1, n+1)。
  3. 取「第 N 年」的值时,取 不超过 N 的最后一个实际数据点,不要用 索引 = N-1。
  4. 横轴上限 / 截断 / 「全期多少年」一律用 max(years),不是数据点数 n。
  5. irr.py 约定 cv[n] = 第 n 年末,假设数组连续,抽样年度表不能直接用; 改为按真实年度构造现金流自己解 XIRR(现金流 [(t, 金额)],流出为负)。 make_chart.py 横坐标按值映射,支持抽样年度,可直接喂真实年度的 CSV。

遇到「年龄和年度对不上」的输出,第一反应就该查是不是踩了这个坑。

八、缩放规则

  • 1–4 款:详细对比 = 总利益对比表(关键保单年度 保证/演示)+ IRR 对比(保证/演示)+ 分项维度(形态/领取方式/流动性/身故责任/费用)+ 现金流形态说明 + 适用场景匹配。理财类须附 scripts/make_chart.py 双轴折线图(见 §九)。标准结构见 examples/sample_对比_3款.md。
    • ⚠️ 参与对比的若有定额终身寿(杠杆寿):它不进 IRR 对比表,改按 §7.18.3 出杠杆率对比 + 身故赔付情景对照表;只在形态一致(都是定额)时才同台比。IRR 只在报告末尾作参考列出,并附「IRR 低≠产品差」提醒。
  • 5 款及以上:只出「总利益」摘要表(关键保单年度保证/演示)+ IRR 摘要一行,不再逐项拆;理财类仍须附 scripts/make_chart.py 双轴折线图(多色区分)。压缩摘要标准结构见 examples/sample_对比_5款以上.md。
  • ★ 只要参与对比的产品里有任何一款含分红/万能演示(即非保证部分 ≠ 0),无论几款,都必须附 scripts/stress_test.py 压力测试(§7.15:IRR + 翻倍年数 + 临界实现率)。全部为纯保证产品(如传统非分红年金/增额寿)时可省略,并在报告中注明「本批产品无非保证利益,无需压力测试」。
    • 5 款以上时压力测试表可压缩为一行摘要:实现率 0% → IRR 区间 与 临界实现率区间,但临界实现率必须保留。

九、输出规范

  • ★ 主交付 = HTML 文件:报告一律先出一份自包含 HTML(内联 CSS + 内联 SVG 图,单文件可直接双击打开)。理由:保色、保版式、图表原生可看,零依赖,任何环境都能出;反过来 DOCX/PDF 都依赖环境里有没有对应能力,先出它们容易出现「分析做完了但文档出不来」。
  • ★ 出完 HTML 必须做「交付格式二次问询」(使用人强要求,见 §9.1):主动问使用人要不要再转成 PDF / Word / Excel(可多选),选「不需要」就以 HTML 收尾。不要自作主张直接转,也不要不问。
  • Markdown:用于对话问询过程(非最终交付)。
  • 表格(必须提供):现金价值表、IRR 表、多品对比表(保证/演示总利益)、计算口径说明。
  • 图表(理财类必附):用 scripts/make_chart.py 生成双轴折线图(零依赖纯 SVG)。横坐标=持有年度(非年龄),左纵=总利益(元),右纵=IRR(%);单产品画总利益(实线)+IRR(虚线),多产品不同颜色区分。调用:python scripts/make_chart.py --input irr_A.csv [irr_B.csv ...] --names A [B ...] --out chart.svg [--mode demo|guaranteed|both]。非理财类(健康/财产)不强制附图。
  • 图表(杠杆寿必附,★ 与上面是两个脚本):定额终身寿用 scripts/leverage_chart.py 出杠杆率走势图,不要用 make_chart.py(那个的输入是 irr.py 的 CSV,量纲是总利益/IRR,套到杠杆寿上会全错)。横轴=保单年度(自动截断到稳定年 +3 年),左轴=金额(累计保费/一般身故赔付/基本保额),右轴=杠杆率(倍)。调用:python scripts/leverage_chart.py --extract A.json [--policy p.json] --out lev.svg [--mode compare] [--log] [--tail 3] [--xmax N]。详见 §7.18.5。
  • 每篇末尾固定:消费者避坑清单 + 免责声明(见 references/templates.md)。

9.1 ★ 交付格式二次问询(HTML 出完后必须问,可多选)

触发时机:HTML 报告文件已落盘、正文结论已在对话里给完之后,立刻问,不要等用户开口。 一次分析只问一次;用户选完就转,不要再追问第二遍。

问法(用多选问询,multiSelect: true):

报告已生成(HTML)。要不要再转成以下格式?可多选,也可以都不要。

选项 说明
PDF(推荐) 排版快照,与 HTML 视觉一致,适合发人 / 打印 / 存档
Word(DOCX) 可编辑文档,适合自己改字、加批注、走审批流;图表以图片插入
Excel(XLSX) 把报告里的表格拆成多个 sheet,图表挂在对应小节 sheet,方便自己再算
不需要,HTML 就行 到此收尾

★ 四条硬规则

  1. 必须给「不需要」这个出口——不问就转等于替使用人做决定,问了不留退路等于强迫。
  2. 可多选:使用人完全可以「既要 Excel 又要 Word」;问询与导出都要支持一次多个格式。
  3. Excel 导的是「表 + 图」不是「报告」:按小节拆 sheet(sheet 名取小节标题), 图表挂在所属小节的表下方,正文散文不进 Excel(塞进去只会变成一坨没人看的字)。
  4. 转不出来的格式要明说:某格式缺依赖(如没装 python-docx)必须当场讲清原因 + 给替代方案 (改用 CSV / 用本机 Word 打开 HTML 另存),不许静默跳过,更不许假装导好了。

执行:python scripts/export.py --html 报告.html --to pdf docx xlsx(--to 后面跟几个就转几个)。 转完逐个报出「格式 → 文件路径 → 大小 → 内容摘要(页数 / sheet 数 / 段落·表·图 数)」。

9.2 HTML 报告转 PDF / Word / Excel(scripts/export.py)

一个脚本管三种格式,见上。内部实现与降级要点如下。 PDF 走无头浏览器打印(环境里通常没有 weasyprint / playwright / pdfkit / fitz, 但一般装了 Chrome 或 Edge)→ 零安装。

打印样式(不加就是背景色全白 + 图表被拦腰截断;export.py 检测到 HTML 没有 @media print 时会自动注入,手写流程则自己加):

*{-webkit-print-color-adjust:exact;print-color-adjust:exact}
@media print{
  @page{size:A4;margin:11mm 9mm}
  html,body{background:#fff}
  body{padding:0;font-size:10pt;line-height:1.5}
  section{padding:13px 15px;margin-bottom:11px;page-break-inside:auto}
  h2,h3{page-break-after:avoid}
  figure,.note,tr,li{page-break-inside:avoid}
  table{font-size:9pt}
  figure svg{max-height:118mm}   /* 关键:防止单张图超过一页被切 */
}

无头打印(export.py --to pdf 内部就是这条命令):

CHROME="/c/Program Files/Google/Chrome/Application/chrome.exe"   # 或 Edge msedge.exe
"$CHROME" --headless=new --disable-gpu --no-sandbox \
  --no-pdf-header-footer --run-all-compositor-stages-before-draw \
  --virtual-time-budget=8000 --print-to-pdf="<out.pdf>" "<in.html>"
  • --virtual-time-budget 必须给,否则内嵌 SVG / 字体可能来不及渲染就出片。
  • --no-pdf-header-footer 去掉浏览器自带的页眉页脚(URL + 日期)。
  • 找不到浏览器时脚本会明说,并可退回到「手动用浏览器打开 HTML → 打印 → 另存为 PDF」。

结构化校验(pypdfium2 一般自带;export.py 会自动报页数)

import pypdfium2 as p, os
d = p.PdfDocument("out.pdf")
print(len(d))                       # 页数
print(d[0].get_size())              # A4 → (595.0, 841.9) pt
print(os.path.getsize("out.pdf"))

⚠️ 当前模型不支持读图:把 PDF 渲染成 PNG 再用图片方式核验这条路走不通, 只能做结构化校验(页数 / 纸张 / 体积 / page-break 规则是否写对)。 若用户反馈「图被切了 / 表跨页断了」,回头调 figure svg{max-height} 与 @page margin, 不要在没确认的情况下声称「已目视检查排版」。

Word(DOCX) —— export.py --to docx,依赖 python-docx + lxml:

  • 结构化转换:h1~h4 → 标题层级;p → 正文(保留 <b>/<i>);table → Word 表格(首行加粗); ul/ol → 项目符号/编号列表;figure → 图注 + 图片。
  • ★ 图表以 PNG 插入(v1.5.1 起):Word 装不下内联 SVG,但装得下 PNG, 所以先把 SVG 栅格化再 add_picture(见 §9.3)。不再只是占位说明。
  • 纸张强制设 A4 + 2.2cm 页边距:python-docx 默认模板是 Letter + 1.25in 边距, 与 PDF 的 A4 不一致,图会按 6.0in 插入再被 Word 缩放,白白糊一层。 图宽按「原始宽 vs 版心宽」取小,不撑出页面。
  • 中文字体同时设 font.name 与 w:eastAsia,否则 Word 里中文回落成宋体/方框。
  • 缺依赖时:明说 + 建议「用本机 Word 直接打开 HTML 另存为 .docx」。

Excel(XLSX) —— export.py --to xlsx,依赖 openpyxl(插图还需 Pillow):

  • 按小节拆 sheet:sheet 名取小节标题(去掉编号前缀与 ★), 该小节的表依次往下排,图表挂在本小节表格下方。只有「含表或含图」的小节才出 sheet。
  • 纯数字转数值、带单位/带 ★ 的留文本(不要把「万元」「倍」「%」的单位吃掉); 表头蓝底白字加粗、冻结首行、按 CJK 宽度自动列宽。
  • ★ 多张图的锚点行必须自己累加:ws.max_row 不会因为 add_image 而增加, 不累加会让两张图完全重叠(实测都锚在 A3)。
  • 也可用 --tables tables.json 直接喂结构化表格({sheet名: [[行…]]},此模式不含图)。
  • 缺依赖时:明说 + 退回 CSV 交付(表格文本可直接粘进 Excel)。

9.3 ★ SVG → PNG 栅格化(让 Word / Excel 也能带图)

export.py 内置,命令:chrome --headless=new --screenshot=<out.png> --window-size=W,H <in.svg>。 不需要 cairosvg / svglib / reportlab(这些库通常都没装),只要本机有 Chrome 或 Edge。

必须做的两件事(少一件就是废图):

  1. 把 SVG 根标签的 width/height 改成分辨率尺寸(原始宽 × scale,保留 viewBox)。 不改的话,Chrome 会把 980×580 的图画在 1960×1160 窗口的左上角—— 实测四个象限只有左上 6.98% 有内容,其余三个象限全是 0%,等于输出一张 3/4 空白的图。
  2. 补 xmlns 与 viewBox:独立 .svg 文件没有 xmlns 浏览器不解析; 没有 viewBox 光改 width/height 只会裁切不会缩放。

其它要点

  • --svg-scale 2(默认):出 2 倍图,Word / Excel 里按 1 倍尺寸显示 → 打印更清晰。要更清晰用 3。
  • SVG 必须从原始 HTML 文本里正则取,不要走 lxml 序列化:lxml 的 HTML 解析器会把 大小写敏感的属性名(viewBox)全小写,导出的 SVG 直接废掉。
  • PNG 默认落在 <out-dir>/<name>_图/,可用 --img-dir 指定;重跑会复用同名文件。
  • --no-images 可强制关闭插图(退回占位说明)。
  • 没浏览器 / 没 Pillow 时:图位留一行说明写清原因,不假装导好了。

一句话记住:PDF = 给人看的成品;Word = 给人改的稿子(带图);Excel = 给人再算的数据(带图)。 三者可以同时要,也可以一个都不要——让使用人选。

十、健康保障类(基础版)维度

非理财类:仅做「产品 + 条款」分析,不产出 IRR / 总利益表(见 §六路由)。

投保规则(年龄/职业/等待期/犹豫期/健康告知)、保障责任(重疾/中症/轻症种类与赔付次数、保额、身故、豁免)、免责条款(绝对不赔项,标「隐形坑」)、续保条件(一年期是否保证续保、费率上调风险)、赔付机制(免赔额/报销范围/赔付比例/医院限制)、杠杆率(年保费 vs 保额)、现金价值。专项:重疾分组/不分组·间隔期·多次赔;医疗外购药/质子重离子;意外伤残等级·猝死·职业限制。

10.1 ★ 多品对比:编号必须「一眼认得出是谁」(使用人强要求)

起因:四款重疾险对比的结论速览里写了「A 17,130 / B 24,075 / C 29.19 倍」, 可是报告开头从没说过 A/B/C/D 分别是哪款产品——读者得翻回产品一览才能对上号, 结论等于看不懂。

★ 三条硬规则

  1. 禁止在结论速览 / 任何结论性文字里只写字母编号。 结论速览(KPI 卡、一句话结论、洞察)里出现产品时,一律写产品简称 (如「太保真爱3号·无身故」「中英爱守护C款」),不要写「C 款」。
  2. 编号只在「对比大表」里用,且表头必须带产品名: A 阿基米德2025 / C 太保真爱3号·无身故——让读者跳到表格中段也认得出。
  3. 若确需用编号,必须在最开头(产品一览)给编号对照表,且放在结论速览之前或同屏可见。 ⚠️ 同一产品的两个方案档(如「太保真爱3号」有无身故两档)不能只叫「C / D」, 必须写成「太保真爱3号·无身故」「太保真爱3号·含身故」, 否则读者会误以为是两款不同产品(实测就踩了这个坑)。

10.2 ★ 健康险的现金价值:有就行,不要展开(使用人强要求)

起因:重疾险对比里做了一整节「现金价值与回本」,还列了「第 1 年现价(退保能拿回)65~910 元」、 现价峰值、峰值/总保费比,并专门画了现价走势图。使用人反馈: 重疾险测回本不是特别重要的事,有就可以了;尤其是首年退保现价基本不需要。

★ 处理规则

  • 健康险(重疾 / 医疗 / 意外 / 定寿)的现金价值压成「回本年」一行放进产品一览或对比表, 不做专章、不做走势图、不做峰值分析。
  • 不列「首年退保现价」:对买保障的人没有决策价值,只会制造噪音。 真要讲退保损失,一句「犹豫期后解除合同会有损失,前期尤甚」足够。
  • 不画「现价 − 累计保费」差额图(那是理财类才需要的)。
  • 唯一例外:若该健康险带储蓄/返还性质且回本早于 20 年(如两全型重疾), 才展开一段说明,并注明「返还型,保费里含储蓄成分」。
  • 仍须保留一句提示:犹豫期后解除合同会有损失,具体见合同现金价值表。

10.3 ★ 健康险必附「保障额度走势图」(scripts/benefit_chart.py)

健康险真正会随年龄变化的是保障额度,不是现价。典型情形:

  • 身故责任「61 岁前赔保额、61 岁后只赔已交保费 / 现价」→ 断崖;
  • 重疾 / 身故按「max(保额, 现价, 已交保费)」给付 → 后期随现价抬高;
  • 附加责任「18~70 周岁」有效 → 到期归零;
  • 医疗 / 意外险保额随续保年度或年龄档变化。

这些用表格看不出来,必须画成走势图(零依赖纯 SVG):

python scripts/benefit_chart.py --policy A.json B.json C.json D.json --out 保障走势.svg
python scripts/benefit_chart.py --policy A.json --mode single --out A_保障.svg
python scripts/benefit_chart.py --policy *.json --benefits 身故/全残,重大疾病 --out b.svg

policy JSON(手工构造,与 classify_life.py --policy 同风格):

{"name":"太保真爱3号·无身故","insured":"女 35 岁","entry_age":35,
 "years":[1,2,3],"ages":[36,37,38],
 "benefits":{"身故/全残":[0,0,0],"重大疾病":[500000,500000,500000],
             "中症":[300000,300000,300000],"轻症":[150000,150000,150000]}}
  • compare 模式(默认):一个责任一张图,一张图里每款产品一条线。
  • single 模式:一款产品一张图,一张图里每个责任一条线。
  • --combine 责任1,责任2,…(★ 推荐):把若干责任叠成一张图,纵向多格共用同一条 X 轴。 使用人明确要求:「重疾、轻症、中症的走势可以在同一张图里,不需要做那么多图」。
  • 横轴默认保单年度并标注对应年龄;Y 轴单位万元。
  • 自动标出断崖点(相邻年度跌幅 ≥10%)与归零点; 全程为 0 的责任标「无此责任」(否则读者分不清"没这项责任"与"金额为 0")。

★ 重疾险的标准配置就是两张图,不要出四张

# ① 疾病保障:重 / 中 / 轻三格合一,共用纵轴刻度(三档高低可直接比)
python scripts/benefit_chart.py --policy *.json \
    --combine 重大疾病,中症,轻症 --out 疾病保障走势.svg
# ② 身故保障:单独一张(形态与疾病完全不同,混一张会互相压扁)
python scripts/benefit_chart.py --policy *.json \
    --benefits 身故/全残 --out 身故保障走势.svg
  • ★ 多格图各格共用同一个 Y 轴刻度(取整图最大值): 这样 50 万 / 30 万 / 15 万三格的相对高低才可比; 各自独立缩放会让三格看起来"一样高",反而误导。
  • ★ 身故不与疾病合并在一张多格图里:身故常是 0(无责任)或断崖形态, 与疾病档位放一起会压扁疾病那几格。分两张,各讲一件事。
  • 常数的责任不必单独出图(如四款轻症都恒定 15 万 → 四条重合水平线,零信息); 但放进 --combine 的多格图里是合适的——顺带展示"这一档大家都是多少"。

十一、财产险类(基础版)维度

非理财类:仅做「产品 + 条款」分析,不产出 IRR / 总利益表(见 §六路由)。

车险:交强+商业构成、三者险保额、车损/座位、免赔、无赔款优待、免责(酒驾/自燃/涉水)。家财/责任:保额确定方式(定值/不定值)、免赔、责任范围、盗抢、管道破裂。

多款非理财类对比:只对比「产品与条款」维度(上述投保规则/保障责任/免责/续保/赔付机制/杠杆率等),不做收益(IRR/总利益)对比,因为非理财类无现金价值增值收益可比。结构见 references/templates.md 非理财类多品对比模板。

十二、限制声明(写进技能行为)

  1. 无实时产品数据库,所有结论基于用户提供的信息;解析不完美时以对话追问兜底。
  2. 本技能只做产品分析,不自动出保险计划书,不替代专业顾问。
  3. 演示利益非保证;最终以保险合同条款为准。
  4. 图片可经 OCR 转写后分析:本环境 OCR 可用(如 ocr-local 等);但密集数字表识别易错,来自 OCR 的关键数字必须让用户核对后再算 IRR,绝不凭未核对 OCR 数字直接计算。
  5. 文本型文件本环境原生可解析:PDF/DOCX/Excel 由自带带库 Python(openpyxl/python-docx/pdfplumber,经 scripts/extract.py)原生抽取;仅当运行环境无这些库时才降级为追问/粘贴参数。凡内嵌图片导致数字读不出时,转 OCR 或请用户以文本/Excel 提供关键列。
  6. 万能账户利率不校验:万能/分红的演示利率、保底利率直接采用用户提供的计划书/说明书标注值,本技能不反向验算这些账户值是否严格按声称利率复利(无独立利率引擎,也不应自行假设利率)。报告须显式披露所用利率假设(如万能中档 4.5% / 保底 1.5%),演示 ≠ 保证。
  7. 不自动对比分红实现率/达成率:分红实现率依赖保险公司历史经营数据,本技能无实时数据库,且自动拉取/对比会显著消耗用户 token;默认不对比、不展示分红实现率。如用户坚持要看,请其自行提供各产品历史实现率数据,技能仅做展示不负责抓取。
  8. 投连险不纳入收益对比:投连险价值随净值波动、不保证,按 §7.13 仅做底层资产穿透分析,不参与 IRR / 总利益对比表与现金流形态收益对比。

十三、工具与引用

  • scripts/classify_policy.py:生存总利益「计法判定器」(§7.16 决策树自动化,理财类拿到列名后第一个要跑的脚本)。 输入产品名与列名,逐层输出 ①大形态 ②是否分红 ③分红构成(现金/增额/终了/特别)④现金红利去向 (累积生息 / 现金领取 / 进入万能)⑤哪些现金流进了万能 → 建议 policy 字段映射 → 生存总利益公式 → 冲突检查。 调用:python scripts/classify_policy.py --name "<产品名>" --columns "<逗号分隔列名>"; 已构造好 policy 后用 --policy policy.json 反查字段填得对不对(含重复计入冲突检查)。
    • 总利益取谁、不取谁,由它说了算,不要凭印象自己拼公式;拿到它的输出再据此填 policy。
  • scripts/classify_life.py:终身寿险形态判定 + 杠杆率测算(§6.1 路由 / §7.18 口径)。 ★ 产品名带「终身寿」时第一个要跑的脚本(比 classify_policy.py 更早—— 先定它是定额还是增额,才知道该按杠杆率还是按 IRR 评价)。 自动从 extract.py 的 JSON 定位演示表、识别主身故责任与各项附加责任列, 输出 ①形态判定(定额终身寿 / 增额终身寿 / 无法判定)+ 置信度 + 逐条依据 ②四个杠杆率指标(首年杠杆 / 总保费杠杆 / 峰值赔付杠杆 / 单位保障成本) ③逐年杠杆衰减与退保差额表;多品时 --compare 出横向对比(自动给归一化指标打★)。 调用:python scripts/classify_life.py --extract <抽取.json> [--sum-assured 1000000]; 多品:python scripts/classify_life.py --extract A.json B.json --compare; 多品强烈建议加 --matrix:出「出险情景 × 产品」赔付矩阵(§7.18.3,杠杆寿对比信息量最大的表); 图片型表格(OCR 出来的数字)用手喂:--policy hand.json (字段:name/insured/sum_assured/premiums/death_benefit/extra_death/cv/ages)。
    • 判不出来就问用户,不要猜;脚本会明说缺哪一列(身故列 / 保额列 / 现价列 / 保费列)。
    • 保费列找不到时会自动用「累计保费」列差分还原;仍找不到则杠杆率全空,须 --sum-assured 或手工 policy。
    • 判为增额寿时脚本会提示改走 build_policy.py --run,不列逐年杠杆表。
  • scripts/benefit_chart.py:健康险保障额度走势图(§10.3,零依赖纯 SVG)。 ★ 重疾 / 医疗 / 意外 / 定寿等多品对比必附:画「保障额度随年龄怎么变」—— 身故责任 61 岁断崖、max(保额,现价,已交保费) 后期抬高、附加责任到期归零, 这些用表看不出来。调用:python scripts/benefit_chart.py --policy A.json B.json ... --out 保障走势.svg [--mode compare|single] [--benefits 身故/全残,重大疾病] [--combine 重大疾病,中症,轻症] [--x-age]。 compare 模式(默认)一个责任一张图、每款一条线;single 模式一款一张图、每个责任一条线; ★ --combine 把多个责任叠成一张多格图(重疾险就用它:疾病一张 + 身故一张)。 自动标断崖点(跌幅 ≥10%)、归零点,以及全程 0 = 「无此责任」。 输入是手工 policy 的 benefits 字典 ({"身故/全残":[...], "重大疾病":[...], "中症":[...], "轻症":[...]}), 不是 irr.py 的 CSV,也不是杠杆寿那套输入。
  • scripts/leverage_chart.py:杠杆寿杠杆率走势图(§7.18.5,零依赖纯 SVG)。 ★ 判为定额终身寿后必附;与 make_chart.py(理财类「总利益+IRR」图)是两个脚本,不要混用—— 后者输入是 irr.py 的 CSV、量纲是金额与百分比,套到杠杆寿上会全错。 复用 classify_life.analyze() 取数,横轴=保单年度、自动截断到「最后变化年 +3 年」 (--tail 可调 / --xmax 手动),左轴=金额(累计已交保费 / 一般身故赔付 / 基本保额水平参考线), 右轴=杠杆率(一般身故赔付 ÷ 累计已交保费);保额必须与累计保费同放左轴, 不能与杠杆率共右轴(量级差 4 个数量级会压平一条线)。 支持 --mode single|compare、--log(对数刻度,看后期台阶)、--annotate(指定标注年度); 自动检测并圈出附加责任到期的断崖(身故赔付下降 >10% 的年度)。
    • 命中「合计型列」(生存总利益 / 总现金价值 / 账户价值合计)时会直接建议采用该列, 并给出 基本保额现金价值 + 累积红利现价 = 总现金价值 的列位校验式(§7.11 铁律①)。
    • 冲突检查覆盖五类:① 缺 benefit_in_universal;② dividend_cv 与 dividend_cash 双填; ③ benefit/universal 重叠 ≥3 年;④ 两全满期年 cv 与 benefit 双非零; ⑤ 万能与现金红利并存但去向靠列名推断不出(双算高发处,必须回计划书确认)。有告警先修再算。
  • scripts/irr.py:XIRR / 回本年限 / 总利益(保证/演示)计算。调用方式见文件头注释。
    • policy 字段与「给付去向」:除 premiums / cv_* / benefit_* / dividend_cv_* / universal_* / terminal_bonus_* 外, 还支持 benefit_in_universal(年金已进万能 → 不累加 Σbenefit,防重复计入) 与 dividend_cash_*(现金领取红利,按流逐年计入,区别于存量的 dividend_cv_*)。 组件取舍一律查 §7.16 的互斥矩阵与险种公式速查表。
    • 内置自检:benefit 与 universal 同时非零 ≥3 个年度时,脚本会向 stderr 打印 「疑似重复计入」告警。看到告警必须回头确认年金去向,不要直接采信输出。
    • 输入体检(sanitize_policy):数值项统一转 float(容错 "1,000,000" / null)、 文件不存在或非法 JSON 给可行动提示而非 traceback、premiums 全 0 直接 exit 1、 价值数组之间长度不一致时告警(年度错位会让 IRR 静默算错)。 注意:premiums 天然比价值数组短(只覆盖交费期),不算长度异常。
    • 缴费期不显示 IRR(§7.10.2):未过缴费期的年度,IRR 两列输出「缴费期」文本而非数值, 表头会打印 缴费期: 第 1–P 年 → 该区间 IRR 列显示「缴费期」。只改显示,不动现金流。 缴费期判定 = payment_span()(连续前缀)+ premium_base()(0 基/1 基下标探测)。
    • 价值空洞体检:某年「总利益」= 0 而前后年度都有值 → 该年 IRR 必然是 NA 或极端负值, 脚本会告警。判定用总利益而非 cv:年金险开始领取后现金价值本就递减到 0 (实测第 39 年起 cv=0),但那些年度有生存金流入,不是空洞——只看 cv 会误报。
  • scripts/recognize_spec.py:产品说明书/计划书识别器。输入 extract.py 的 JSON,输出结构化产品属性 + 文档类别 + IRR 适用性说明。调用:python scripts/recognize_spec.py <extract.json> [--out recognition.json]。
    • 文本层已做 NFKC(康熙部首还原)+ 逐字重复伪影去重,对「现⾦价值」「保保单单年年年度度」这类 PDF 免疫。
    • 文本截断到 30 万字:xlsx 精算表(现金价值表/费率表动辄 2000+ 行)会让识别耗时飙到分钟级 (实测 2.7MB 输入 2 分 40 秒 → 截断后 2 秒),产品名/条款都在文档前部,不受影响。
    • 产品名抓不到就返回 null,绝不返回「具体保险」「过…万名客户提供保险服务」这类编造物(§7.17 ④)。
  • scripts/build_policy.py:说明书→IRR 自动链路(§5.1)。输入 extract.py 的 PDF JSON,自动定位示例利益演示表、按表头关键词映射列、稠密化年金(含「满期保险金」识别为 benefit)、构造 irr.py 输入并(--run)直接跑出 CSV。调用:python scripts/build_policy.py <extract.json> --name <产品> [--out policy.json] [--run] [--years 5,10,20,30,40,50,60,75]。合并表头错位时会 WARNING 并输出全零,此时改用手工「生存总利益」列构造(§7.11 铁律①)。
    • 只支持 kind=="pdf":xlsx/docx 抽取结果会明确报错并指向手工构造路径(精算表工作簿不是演示表,本就不该走自动链路)。
    • 四道硬失败(§7.17):① 未找到演示表 ② 总保费 = 0 ③ 演示表不足 5 个年度 ④ 总利益全为 0(列映射失手时的兜底,此前只是 WARNING 却照常落盘)。 四者都会 exit 1 并给出下一步建议,绝不静默产出垃圾。
    • 身故/全残守卫(§7.17 陷阱⑤):is_death() 拦下标签含「身故/全残」的列, 防止身故总利益被当成生存总利益(实测会让演示 IRR 严重高估)。 若表头重建把两者拼成同一标签,本脚本无法可靠区分 → 硬失败并要求手工指定列。
    • 已知边界:保费与演示表分处两张表时(如「基本信息/交纳的保费」表 + 「生存总利益」表), 本脚本只解析演示表,会因抓不到保费而硬失败。此时按 §7.11 手工合并两张表构造 policy。
  • scripts/make_chart.py:理财类双轴折线图生成器(§九)。输入 irr.py 产出的 CSV(可多个),输出纯 SVG(零依赖):横坐标=持有年度、左纵=总利益、右纵=IRR%、多产品不同颜色。调用:python scripts/make_chart.py --input irr_A.csv [irr_B.csv ...] --names A [B ...] --out chart.svg [--mode demo|guaranteed|both]。
    • ⚠️ 无演示列的产品在演示图里会整条线消失(如非分红年金)。做法是给它造一份「演示列 = 保证列」的副本 CSV 专供演示图,并在图注写明「该产品非分红,保证口径与演示口径相同,两图曲线一致」。
  • scripts/parse_pdf_table.py:文本层 PDF 利益演示表坐标重建解析器(§7.14)。当 extract.py 抽出的 tables 为 0、且数字被拆行时使用。子命令:--scan(全文档扫描导出 JSON + 年度连续性诊断)、--page N --header(看表头确认列义)、--page N --dump(看数据行每格 x0 对齐列位)。可调 --row-gap(默认 10.0)。
  • scripts/stress_test.py:分红实现率压力测试(§7.15,理财类含分红产品必附)。输出 实现率 × 产品 → (IRR% , 翻倍年数, 总利益) 表 + 临界实现率。调用:python scripts/stress_test.py --input A.json [B.json ...] --names A [B ...] --years N [--rates 0,25,50,75,100] [--times 2] [--benchmark 无分红.json | --target-irr 1.75] [--out stress.csv]。
    • 累积生息型红利需在 policy 补 dividend_annual_guaranteed / dividend_annual_demo(当年红利),脚本自动做复利修正并自动反推利率。
    • 自检铁律:X=0 行的 IRR 必须与 irr.py 的保证 IRR 一致,X=100% 行必须与演示 IRR 一致;对不上说明 policy 的保证/演示字段填错了。
    • 缴费期遮罩与 irr.py 同口径(§7.10.2):--years N 落在某产品缴费期内时,该产品的 IRR% 列写「缴费期」,翻倍年显示 >N,总利益照常给出;irr_at() 仍在完整现金流上求解, 只是不展示。避免与 irr.py 的 IRR 表自相矛盾。
  • scripts/export.py:二次交付格式导出器(§9.1 / §9.2)。HTML 报告 → PDF / Word / Excel。 调用:python scripts/export.py --html 报告.html --to pdf docx xlsx [--tables t.json] [--name 前缀] [--out-dir 目录] [--browser 路径| --prefer chrome|edge]。
    • --to 可跟多个(支持多选导出);--html 与 --to 必填。
    • PDF:无头 Chrome/Edge 打印,零 Python 依赖;HTML 没写 @media print 时自动注入打印样式。
    • DOCX:需 python-docx + lxml;图表栅格化成 PNG 后插入(§9.3),纸张强制 A4。
    • XLSX:需 openpyxl(插图还需 Pillow);按小节拆 sheet,表在下、图挂在表后(§9.2)。 也可用 --tables xxx.json({sheet名: [[行…]]})直接喂(此模式不含图)。
    • 插图参数:--svg-scale N(默认 2)、--img-dir 目录、--no-images(强制占位说明)。
    • 缺依赖一律明说 + 给替代方案(CSV / 本机 Word 另存 / 手动打印),绝不静默跳过。
    • ⚠️ 别用 sys.stdout = io.TextIOWrapper(sys.stdout.buffer, …) 改编码(会关掉调用方的 buffer), 用 sys.stdout.reconfigure(encoding="utf-8", errors="replace")。
  • scripts/feedback.py:反馈收集器(§十四)。把出错与改进意见变成作者能收到、能复现、且不泄露隐私的一封邮件。子命令:report(手动提,可加 --context 记录当时场景)、invite(打印邀请文案,供模型在 §14.5 主动反馈场景转述,不写文件)、list(查看)、export(打包 zip + 生成预填 .eml)、clear(清空)。
    • 零凭据、零依赖:不收集使用人邮箱,不联网,只用标准库 email 构造 .eml。
    • 强制脱敏:绝对路径 → <路径>/<文件>.pdf;4 位以上整数 → <数字>;但 IRR / 利率等小数保留(1.298、3.5),否则反馈会失去诊断价值。
  • scripts/_feedback_hook.py:自动故障捕获钩子。8 个脚本启动时各挂一行,sys.excepthook 会把未捕获异常自动写成脱敏快照。
    • 不影响主流程:钩子全程 try/except,异常照常打印,反馈失败也静默跳过。
    • 可用环境变量 INS_FEEDBACK=0 关闭(不落盘、不提示)。
    • 只抓未捕获异常;脚本里 sys.exit(1) 的主动硬失败(如「未找到演示表」)不在此列,若使用人认为属于误报,用 feedback.py report --type 数据问题 手动提。
  • 自我介绍.md:本技能的第一人称自述(Can Do / Cannot Do / 最易被误用处 / 什么情况别找我)。 凡涉及能力边界、立场、或「这个技能适不适合我」的问题,先读它,比功能列表准确。 可单独转发给他人以建立正确预期。
  • references/templates.md:储蓄类单品深析章节、多品对比章节、各类表格结构、投连险底层资产穿透模板、非理财类多品对比模板、理财类图表说明、健康/财产基础维度细化、消费者避坑清单、合规免责声明措辞。
  • examples/sample_对比_3款.md:≤4 款理财类对比范式(标准结构,数据示意)。
  • examples/sample_对比_5款以上.md:5 款及以上理财类压缩摘要范式(标准结构,数据示意)。

使用本技能时:先按「五、输入三路径」拿到数据 → 按「六、险种识别」路由(投连→§7.13 穿透;非理财→§十/§十一 仅条款: 多品对比时编号必须可识别(§10.1)、现价只留一行(§10.2)、必附 benefit_chart.py 保障走势图(§10.3))→ ★ 产品名带「终身寿」先跑 scripts/classify_life.py 分定额 / 增额(§6.1):判为定额 → 走 §7.18 杠杆率口径(主看杠杆,IRR 仅作辅助),并用 scripts/leverage_chart.py 出杠杆率走势图(§7.18.5);判为增额 → 才继续下面的储蓄理财链路;判不出来就问用户 → 储蓄理财类严格遵循「七」各小节口径(7.1–7.17)→ 先跑 scripts/classify_policy.py 定总利益计法(§7.16 五道判断题) → 据此构造 policy → 调用 scripts/irr.py 算 IRR/回本/总利益 → 理财类调用 scripts/make_chart.py 出双轴图 → 含分红产品调用 scripts/stress_test.py 做实现率压力测试 → 按 references/templates.md 与 examples/ 范式组装报告 → 先落一份自包含 HTML(主交付) → ★ 主动问询使用人要不要再转 PDF / Word / Excel(可多选,也可都不要,§9.1) → 要哪个就用 scripts/export.py 转哪个,并逐个报出「格式 → 路径 → 大小 → 内容摘要」→ 末尾附避坑清单与免责。 全程若脚本抛未捕获异常,会自动生成脱敏故障快照(§十四);使用人提出改进意见或质疑结果时,用 scripts/feedback.py report 记录,最后 export 打包发作者。


十四、反馈机制(使用人 → 作者)

技能内部没有服务端,反馈无法自动上报。链路是:

出错 / 提意见 → 落盘成 md → 打包 zip → 生成预填好的 .eml → 使用人双击、点发送

零凭据、零第三方依赖、零网络配置。

14.1 ★ 隐私铁律(本节第一优先)

保险计划书里有姓名、身份证号、保单号、保费金额,一个字都不能进反馈。

模型在写任何反馈前必须自检:

  • ❌ 不得写入:投保人/被保人姓名、身份证号、保单号、银行账号、具体保费与保额数值、联系方式
  • ❌ 不得写入:输入文件的完整路径(路径里常带姓名,如 桌面/张三的保单.pdf)
  • ✅ 可以写:输入文件类型(PDF / xlsx / docx)、产品类型与缴费方式(「增额寿,年交 ×5 年」)、 执行命令、IRR 与利率等小数(1.298、3.5)、列号与报错信息

feedback.py 会自动脱敏兜底,但不要把敏感信息当原文交出去再指望脱敏—— 描述阶段就该写成「年交约六位数,5 年交」这类不含原始数值的说法。

14.2 三种触发时机(模型必须做)

时机 模型动作
① 脚本抛未捕获异常 钩子已自动生成快照。主动告知使用人:已生成故障快照 + 路径,并问是否要 export 打包发作者。不要替使用人决定发送。
② 使用人提改进意见 调用 feedback.py report --type 建议 --content "…",把意见写成一句「期望什么 vs 现在是什么」。
③ 使用人质疑结果不对 调用 feedback.py report --type 数据问题,写清:输入类型、产品类型与缴费方式、执行命令、IRR 期望值 vs 实际值(小数可保留)、已排除的原因。
④ 使用人抱怨 / 想放弃(§14.5 主动) 先解决问题,事后再用 feedback.py invite 的一句话文案询问;只有得到肯定答复才记录。隐性信号(「算了不用来」「这个不能 XXX 吗」)比显性抱怨更容易漏,也更值得接住。

第 ③ 种尤其重要——数据问题就是潜在的静默算错 bug,是技能最需要收集的反馈。

14.3 命令速查

# 手动提一条(--context 记录当时在做什么,帮作者复现)
python scripts/feedback.py report --type 建议   --title "…" --content "…" --context "…"
python scripts/feedback.py report --type 数据问题 --title "…" --content "…" --context "…"

# 打印「要不要告诉开发者」的标准文案(§14.5 主动反馈用,不写文件)
python scripts/feedback.py invite --about "这次是关于什么"

# 查看 / 打包 / 清空
python scripts/feedback.py list
python scripts/feedback.py export            # 打包 + 生成 .eml + 自动打开邮件客户端
python scripts/feedback.py export --no-open  # 只生成不打开
python scripts/feedback.py clear --yes       # 导出后再清

--type 四选一:错误 / 建议 / 数据问题 / 其他。

重复提交是安全的:内容完全相同的反馈会被自动跳过(find_duplicate 只比对正文、忽略时间戳与环境),不会让作者收到两条一模一样的反馈——那既令人困惑,也会让人误判这个问题的普遍程度。内容哪怕只差一句,都会正常记为新的一条。

14.4 存放位置与开关

  • 反馈写到 ~/.workbuddypro/feedback/保险计划书分析大师/,不在技能目录内。 两个原因:重装/升级技能不丢反馈;绝不会被打进分发包泄露给别人。
  • 关闭自动捕获:设环境变量 INS_FEEDBACK=0(不落盘、不提示)。
  • 作者的收件邮箱写在 scripts/feedback.py 的 DEFAULT_CONFIG["author_email"], 当前为 zhenwei-shi@qq.com。使用人本机可在 ~/.workbuddypro/feedback/保险计划书分析大师/feedback_config.json 里覆盖。
  • ⚠️ 更换作者或邮箱时必须改代码里的默认值,不能只改本机配置:使用人机器上的配置是首次 运行时由默认值生成的,只有默认值对了,分发给别人时才指向正确邮箱。 若 export 时看到「收件邮箱还是占位符」的警告,说明默认值未配置, 模型应提醒使用人,不要盲目导出。
  • ⚠️ 该邮箱明文存在于技能包内,任何拿到技能包的使用人都能看到。

14.5 主动反馈:识别使用人的不满信号

§14.2 是被动的(报错了才收集)。本节是主动的:使用人未必会主动提意见, 更多时候只是抱怨一句、或者干脆说「算了,不用来」。这类信号如果不接住,作者永远收不到。

★ 机制先说清楚(避免误解):SKILL.md 没有运行时监听能力,脚本只在被调用时运行。 所谓「抓取负面评价」是模型读了本节规则后自行判断—— 这比关键词匹配更聪明(能识别隐性不满、不会因一个「不好」就误触发), 但也意味着本节能否生效完全取决于模型是否照做。

信号清单

显性(直接说出来)

  • 评价差:「这技能不行 / 太差 / 没用 / 一点都不准」
  • 说有问题:「算错了 / 不对 / 有 bug / 又报错 / 又崩了 / 结果离谱」
  • 抱怨体验:「太慢 / 太复杂 / 看不懂 / 步骤太多」

隐性(更容易漏掉,但信息量更大)

  • 同一个问题反复问(说明前面的回答没真正解决)
  • 「算了 / 不用来 / 我自己弄 / 换个方法吧」
  • 「这个不能 XXX 吗」「怎么没有 XXX」——表面是疑问,实际是功能诉求, 这类往往是最有价值的改进建议
  • 语气突然变短、只回一个字、连续追问
  • 明确表示要放弃使用或换工具

三段式动作(顺序不能乱)

  1. 先解决当下的问题——这是第一位的。绝不能为了收集反馈而打断或拖延。
  2. 再一句话询问——问题解决后(或确实解决不了时)才问。 调用 python scripts/feedback.py invite --about "<这次是关于什么>" 拿到标准文案, 转述给使用人。一句话、非阻塞、给明确退路,不要长篇大论。
  3. 只有得到肯定答复才记录——使用人回「好」、或直接说出意见,才调 report。 对方没接话就是拒绝,不要追问。

抑制条件(防骚扰,必须遵守)

  • 同一次对话里已经问过 → 不再问第二次(一次对话最多问一次)
  • 使用人已明确拒绝 → 本次对话不再提
  • 问题已解决且使用人表示满意 → 不问
  • 使用人明显赶时间或不耐烦 → 简短带过,或干脆不问

这套机制的目的是接住原本会流失的意见,不是增加交互负担。 宁可少问一次,也不要让人觉得被打扰。

记录要求

使用人同意后:

python scripts/feedback.py report --type 建议 \
  --title "一句话说清问题" \
  --content "期望什么 vs 现在是什么" \
  --context "当时在做什么(如:在给 5 年交年金做压力测试)"

--context 是作者复现时最需要、又最容易被漏掉的信息,尽量填。 内容同样受 §14.1 隐私铁律约束——脱敏后再写入。