保险计划书分析大师

面向消费者/投保人的保险产品分析器。当用户上传/粘贴/描述一款或多款保险产品(储蓄理财类如增额寿、年金、分红、两全、万能;健康保障类如寿/重/医/意外;财产险如车险/家财),希望「分析这款保险值不值」「对比几款产品」「算算 IRR/回本/总利益」「看懂条款坑点」时使用。支持单品深析与多品横向对比,内置红利/万能计算规则与消费者避坑清单。注意:本技能只做产品分…

danielshisafe

@danielshisafe

Install

$ openclaw skills install @danielshisafe/insurance

保险计划书分析大师

一、角色与定位

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

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

二、核心原则

  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 隐私铁律约束——脱敏后再写入。

Top skills in this category

Stock Analysis

@udiedrichsen

Analyze stocks and cryptocurrencies using Yahoo Finance data. Supports portfolio management, watchlists with alerts, dividend analysis, 8-dimension stock scoring, viral trend detection (Hot Scanner), and rumor/early signal detection. Use for stock analysis, portfolio tracking, earnings reactions, crypto monitoring, trending stocks, or finding rumors before they hit mainstream.

25959k

Stock Market Pro

@kys42

Yahoo Finance (yfinance) powered stock analysis skill: quotes, fundamentals, ASCII trends, high-resolution charts (RSI/MACD/BB/VWAP/ATR), plus optional web a...

15532k

Stock Watcher

@robin797860

Manage and monitor a personal stock watchlist with support for adding, removing, listing stocks, and summarizing their recent performance using data from 10jqka.com.cn. Use when the user wants to track specific stocks, get performance summaries, or manage their watchlist.

8837k

A股量化 AkShare

@mbpz

A股量化数据分析工具,基于AkShare库获取A股行情、财务数据、板块信息等。用于回答关于A股股票查询、行情数据、财务分析、选股等问题。

10232k

Intelligent Stocks Screener

@financial-ai-analyst

基于东方财富数据库,支持通过自然语言输入筛选A港美股、基金、债券等多种资产,支持多元指标筛选,含技术面、消息面、基本面及市场情绪等,可用于全球资产速筛、跨市场监控、投资组合构建、策略回测等场景。返回结果包含数据说明及 csv 文件。Natural language screener for investment...

10627k