Install
openclaw skills install @danielshisafe/insurance面向消费者/投保人的保险产品分析器。当用户上传/粘贴/描述一款或多款保险产品(储蓄理财类如增额寿、年金、分红、两全、万能;健康保障类如寿/重/医/意外;财产险如车险/家财),希望「分析这款保险值不值」「对比几款产品」「算算 IRR/回本/总利益」「看懂条款坑点」时使用。支持单品深析与多品横向对比,内置红利/万能计算规则与消费者避坑清单。注意:本技能只做产品分析,不自动出保险计划书。
openclaw skills install @danielshisafe/insurance消费者视角的保险产品分析器(不是销售工具,也不自动出保险计划书)。
目标:帮投保人客观看懂一款/多款保险产品的真实利益、坑点与适配性。
输入路由 → 险种识别 → 字段抽取
→ 定总利益计法(classify_policy.py · §7.16 五道判断题)
→ 构造 policy → 计算(irr.py) → 分红压力测试(stress_test.py)
→ 按险种模板分析 → 输出(DOCX/PDF+表格)
储蓄理财类必须先跑
classify_policy.py再构造 policy。跳过这一步等于靠印象拼公式, 实测会漏项或双算(最严重一次总利益虚高 73.8%、IRR 从 2.97% 虚高到 7.61%)。
openpyxl / python-docx / pdfplumber,已实测可用),可直接抽取结构化表格与文本。调用:
python scripts/extract.py <文件> → 输出 JSON(含 sheets/tables/pages 与文本),加 --md 输出 Markdown 便于人读核对。脚本会自动定位带库解释器,无需手动指定路径。tables 为空、只有图),数字读不出 → 转路径②(配合 OCR 技能)或路径③(粘贴参数),不得编造。ocr-local 技能,基于 tesseract.js;或 ocr-pro-v2、tencentcloud-ocr 等)。流程:
三条路径最终汇入同一分析引擎。若上传解析与用户描述冲突,以用户明确确认的参数为准。
用户上传的文档可能是产品说明书(描述性,无个性化保费/年龄,只含示例利益演示)而非保险计划书(含具体被保人现金价值/IRR 数字)。两者分析目标不同:
scripts/extract.py 抽取后,先跑 scripts/recognize_spec.py <extract.json> 判定文档类别并结构化识别关键产品属性:
文档类别(初判)(产品说明书 / 保险计划书)、产品名称、险种类型、投保年龄、保险期间、交费方式、养老年金专属(开始领取年龄/保证领取期间/领取方式)、红利方式、保障责任(摘录)、责任免除(摘录)、条款要点关键词、利益演示、IRR适用性说明。scripts/irr.py 测算。dedup_cjk 去中文文本层字符重复伪影;产品名允许「号」前后空格并 collapse;条款段落以「在本合同/因下列」为锚点优先抓真实条款、截到下一标题,不误抓末尾投保提示套话;万能型仅认「万能型/万能保险」并排除「非万能型」否定词;年金专属抓「按 N 个保单年度」式保证领取。recognize_spec.py 判为计划书且含个性化数字,则按「七」走 IRR 测算;判为说明书则只做属性识别与消费者提示,不编造 IRR。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。build_policy.py 自动映射会取空→触发 WARNING 并输出全零。此时按 §7.11 铁律①直接用说明书打印的「生存总利益/总利益」列数值手工构造 policy(参考 examples/ 与「储蓄理财收益对比报告」做法),切勿拿全零结果跑 irr.py。验证列映射:跑 build_policy.py 后看其打印的「保费/交费期/最大年度」是否合理、再核对首年总利益是否等于打印值。extract.py 只能抽出标题、表格数为 0。按以下顺序处理,不要一上来就让用户补数据:
pypdfium2 把该页按 300~400 DPI 渲染成 PNG,统计版心区域的暗像素占比。若 ≈0%,说明源文件的演示表本来就是空白页,直接判定「数据缺失」并在报告中写明是源文件问题,不要反复折腾 OCR。pip install pypdfium2,page.render(scale=dpi/72).to_pil()。⚠️ 不要用 pdfplumber 的 page.to_image(),遇到 4bit 图像会抛 Unsupported bitspercomponent: 4。word/media/*,按像素面积排序,面积最大的几张通常是数据表页。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。rapidocr-onnxruntime(纯 pip,自带模型)。⚠️ 在部分 Windows 机器上 onnxruntime 会报 DLL load failed ... 动态链接库(DLL)初始化例程失败,降级 numpy/onnxruntime 通常无效,此时直接改用第 4 步的 Windows 内置引擎,不要在这上面耗时间。判断归属并路由到对应模板:
险种识别不确定时,向用户确认后再分析。
「终身寿险」是两种完全不同的产品共用的名字,不判定就开算必然出错:
| 定额终身寿(杠杆寿) | 增额终身寿(储蓄型) | |
|---|---|---|
| 保额 | 终身恒定(如固定 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 年, 判据 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 # 多品横向比杠杆
所有形态(增额寿、年金、分红、万能、两全及组合)最终折算成同一条指标:
总利益(N) = 现金价值(N) + 累计已领生存金/年金(N) + 红利价值(N) + 万能账户价值(N)
每款产品同时给**「保证总利益」和「演示总利益」**(演示取中档、标注低/高档区间)。异形态(年金定期领 vs 增额寿减保取现)之所以能同台比,正是靠这个统一口径——落到「第 N 年把保单整体兑现/累计拿到手的总额」是同一个概念。
⚠️ 这个公式是「并集」,没有任何一款产品会全部命中,不要照抄。 真正决定某一项该不该加的是这笔钱的去向:年金进了万能就不能再累加年金;红利现金领走了就不能按存量计。 算任何产品前先走 §7.16 的五道判断题(大形态 → 是否分红 → 哪几种红利 → 红利去向 → 哪些流进了万能), 走完每一项该如何计就唯一确定了。可自动化:
python scripts/classify_policy.py --name "<产品名>" --columns "<逗号分隔列名>"。
生存总利益衡量的是「第 N 年把保单兑现能拿回多少钱」。保额(保险金额)不是钱,一分都不能进。
总利益的每一个组成项都必须是现金价值口径,没有任何一项是保额:
| 组件 | 要取的值 | ⛔ 绝不能取 |
|---|---|---|
| 主险 | 现金价值 / 退保金 | |
| 现金红利(累积生息) | 累积现金红利(含息) | |
| 增额红利 / 交清增额 | 增额红利的现金价值 | |
| 终了红利 | 终了红利的现金价值 | |
| 特别红利 | 特别红利的现金价值 | |
| 万能账户 | 账户价值 / 账户现金价值 | |
| 年金 / 生存金 | 各年实际领取额 |
scripts/irr.py 的字段表里没有任何保额字段——只有 cv_*、benefit_*、dividend_cv_*、
dividend_cash_*、universal_*、terminal_bonus_*。若你在构造 policy 时发现自己在填某个
带「保额」的列,立刻停下重读本节。★ 总利益抓取铁律(防止 IRR 算错,最重要):
一句话:有总利益列就用它;没有才按 7.11 公式求和;求和后用总利益列(若有)做校验,避免重复计入或漏计。
对比时先比保证总利益,再附演示总利益;演示 ≠ 保证的提醒放在旁边。
做法:先识别组件、分别算各组件价值,再求和得该产品总利益;报告标注「含万能中档演示」等,防止糊涂账。
⚠️ 年金 IRR 评估窗口:年金险算 IRR 必须测至给付期末(见 7.10.1),禁止在领取首年/早年退保点算 IRR(会算出负数伪影)。组合型年金(年金+万能)的生存总利益 = 主险现价(含红利) + 万能账户值;万能中档演示时须核对「现金红利是否进入万能」避免重复计入。
若参与产品保费基础不可比(年缴/总保费/缴费期不同),总利益无直接可比性 → 省略总利益对比表,仅展示 IRR 对比(保证/演示)并注明原因;仅当保费基础可比(同总保费/同年缴)时才给总利益对比表。IRR 始终展示(它本是费率归一化的)。
总利益只反映「总额」,不反映「何时能拿到」。对比必附「现金流形态」说明:
让用户看清「同样总利益,一个给终身工资、一个给灵活备用金」。
万能/分红产品的演示总利益高度依赖演示利率。对比表中保证与演示分两行/两列,并标注利率假设(如万能中档 4.5% / 保底 1.5%;分红中档)。提醒:演示 ≠ 保证。
每款产品标注其假设(红利领取方式:累积生息 / 交清增额 / 进万能;年金领取频率与起始年龄)。不同假设导致总利益构成不同,必须透明。
各产品满期年可能不同(如 80 岁 vs 终身 vs 60 岁)。总利益按保单年度对齐(7.4);满期总利益仅在各自满期年出现并标注「产品 X 满期于第 N 年」,不强行对齐满期。
IRR(N) = 假设持有至第 N 保单年度终止的内部收益率,用 XIRR(不规则现金流):
★ 保费截断铁律(易错,已修复并验证):算第 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 画到轴上), 所以右轴不会再被缴费期的极负值拉爆——但这只是兜底, 仍需按上面的规则从缴费期结束后起画,否则曲线左端会缺一截、读者会以为是数据缺失。
年金 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万」的截断伪影,非产品缺陷)。
正确做法:
③ 稠密化(cashflow 构造必做):年金演示表常只列关键年(30/35/40…),须 carry-forward 把年金自首次给付年起逐年补齐(年金自起领年起恒为定值),否则 Σ养老年金少计 → IRR 假阴性(实例曾因稀疏表只计 10 期而非 46 期而算成负)。
④ 一句话自检:年金 IRR 若算出负数 → 先查「是不是在给付期内某年退保算的」与「年金表是否稠密化」,而非怀疑产品。年金产品 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~P 年为缴费期,保费尚未交完,此期间 IRR 会出现 −60% 之类的极端负值, 对决策没有参考价值,故不展示数值;保费现金流仍逐年完整计入, 第 P+1 年及以后的 IRR 已包含全部已缴保费。
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%。irr.py 与 stress_test.py 已内置同一口径
(payment_span() 连续前缀 + premium_base() 下标基准探测),
两处对同一个保单年度不会一个写「缴费期」、一个写数字。★ 实现要点(改代码时别踩):
build_policy.py 产出 0 基
(premiums[n] 在 t=n,首期 t=0),但手工 JSON 常见 1 基(premiums[0] 是占位 0,
保费在 premiums[1..P])。若不做探测,连续前缀会在第 0 位就断掉、
payment_span() 返回 0 → 整个遮罩静默失效,表上又冒出 −30% 这种数。当计划书/说明书直接给出「总利益/生存总利益/账户价值合计」 → 直接抓取,不再逐项算(最准)。仅在缺失时走组件求和:
总利益(N) = 主险现金价值(N) + 附加险现金价值(N) + 红利价值(N) + 万能账户价值(N)
★ 总利益重建公式(无现成总利益列时按此求和):
增额寿(分红型)生存总利益 = 增额寿本身现金价值(保证,不含红利) + 现金红利的现金价值(累积生息→累积现金红利(含息);进万能→暂不计入,已并入万能账户;现金领取→不计入,已流出) + 增额红利的现金价值 + 终了红利的现金价值 + 特别红利的现金价值
年金(含分红/万能):在以上基础上 + 万能账户价值,合计为总利益。
简化:很多产品没有那么多类型的红利,没有的类别直接不参与求和。
★ 防错「三步自检」(求和前必做,分红型必做):
| 步骤 | 操作 | 判定与处理 |
|---|---|---|
| ① 看列 | 文档里有没有「总利益/生存总利益/账户价值合计」列? | 有 → 直接用此列,跳③,不要求和。 |
| ② 重建 | 无总利益列时,按上面「总利益重建公式」求和。注意:「现金价值」通常是不含红利的保证值,需把各红利现金价值加上去;若某表的「现金价值」列已明显≈总利益(已含红利),则红利现金价值记 0,不重复加。 | 得到重建总利益(N)。 |
| ③ 核对 | 若文档另处有「总利益」列,比较 重建值 ≈ 该列? | 偏差 > 1% → 停下,疑似重复计入/漏计/读错数字,回②重判或请用户以文本/Excel 提供数值。 |
★ 真实表格常见「两列现金价值」的识别(极易踩坑):
很多分红型演示表同时给出两列:保证现价(保证、不含任何红利)与 总现金价值(保证+红利) / 演示现金价值(= 保证现价 + 现金红利,红利已含在内)。正确映射:
保证现价。总现金价值(保证+红利) 作为演示现金价值;不要再把现金红利单列加回去(否则重复计入、IRR 虚高)。特别红利/终了红利/增额红利 等独立红利列,则:总利益 = 总现金价值(保证+红利) + 独立红利列。这正是「总利益」列本身的构成。★ 保费现金流时点(错一位 IRR 就失真,已用真实数据验证):
scripts/irr.py 中 premiums[n] 发生在第 n 年初(t = n 年,按 365 天/年):
premiums[0] = 趸交额,其后全为 0。premiums[0..k-1] 各填年保费,对应第 1…k 保单年度初;其后为 0。premiums[1](=第1年末/第2年初),否则持有期被算短一年 → IRR 明显虚高(实测:30万趸交第5年 IRR 从正确 1.65% 虚到 2.07%)。红利价值(N) —— 核心是「红利的现金价值」而非「红利保额」(§7.1 零保额铁律):
万能账户价值(N):抓取「万能账户价值/现金价值」(非保额);若红利/生存金流入万能,已含在内,不重复。仅年金/带万能的组合才加这一项。
★ 数据来源可靠性红线(防读错数字):
每份分析加一行「计算口径说明」,明说总利益含了哪些、没含哪些(如「现金价值不含红利,已加累积现金红利与终了红利,进万能部分计入万能账户」「现金价值已含红利,红利未重复计入」),让结论透明。
两全险的「满期生存金 / 满期保险金」是一笔到期一次性大额给付,IRR 计算与年金同源:把它当作「一笔大额年金分配」,计入满期年的 benefit 流入,与年金险年金现金流同一套算法:
benefit[满期年] = 满期生存金,其余年 benefit 为 0(或含少量生存金)。cv[满期年] 应填 0(或确认表中该列确实为 0),满期金全落在 benefit[满期年]。
若表在满期年仍列有非零现价,必须核实:那通常是「满期前身故/退保」口径列,
与满期金分属两种终止方式,相加即重复计入。红利按中档计入演示口径。build_policy.py 的 locate_columns 已把列名含「满期保险金 / 满期生存金 / 生存保险金」的列识别为 benefit 列(满期年单列、其余年空),无需手工处理;手工构造时令 benefit[满期年] = 满期金 即可。投连险保单价值 = 持有单位数 × 单位价格(净值),随底层资产波动、不保证,无法套用「总利益(保证/演示)」统一口径,故:
症状:计划书 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)。
★ 解析后必做的三件事(防错铁律):
--scan 会自动诊断)。有缺年 → 参数不对,重调。--header 的 x0 对齐来区分,不要凭经验猜。常见列结构参考(供对照,仍以实际表头为准):
| 产品类型 | 列数 | 关键列 |
|---|---|---|
| 增额终身寿(分红·增额红利) | 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 规定演示利率直接采用文档标注值, 反推只在「文档未标注」时用于理解数据结构,不得用反推值去替代合同约定的保证利率。
为什么必做:理财类报告如果只给「保证 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 所需的最低实现率 | 把「要不要赌红利」变成阈值判断,是全部表格里决策价值最高的一列 |
临界实现率的基准怎么选(二选一,报告中写明用的是哪个):
--benchmark 无分红产品.json,
脚本自动取它第 N 年的保证 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.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。
(决策树走完后用它做一遍交叉核对)
| 给付去向 | cv 现价(存量) | benefit(流) | dividend_cv(存量) | dividend_cash(流) | universal(存量) |
|---|---|---|---|---|---|
| 年金/生存金 · 现金领取 | √ | √ | — | — | — |
| 年金/生存金 · 自动进万能 | √ | × 已入万能 | — | — | √ |
| 红利 · 累积生息 | √ | — | √ | — | — |
| 红利 · 现金领取 | √ | — | × | √ | — |
| 红利 · 进万能 | √ | — | × 已入万能 | — | √ |
| 红利 · 交清增额 | √(红利已含在现价内) | — | × | — | — |
| 险种形态 | 生存总利益(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 |
Σ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 个年度会直接告警。dividend_cv 是存量,会把多年红利整笔压在第 N 年末,损失时间价值。
实测 10 年每年领 3,000:IRR 3.198% vs 正确 3.594%,低估约 0.4 个百分点。
→ 现金领取的红利请填 dividend_cash_*(按流),不要填 dividend_cv_*。cv(75)=0、满期金 5,078,895 全落在 benefit(75),口径正确。)build_policy.py 已自动排除);手工构造时不要把这些列的值填进 cv。累积红利保额、终了红利保额
这些是保险金额不是钱。实测误用后总利益虚高 42.9%、IRR 从 4.56% 虚到 6.01%(+1.45pp)、
回本年限从 8 年提前到 6 年。之所以危险,是因为结果看起来更漂亮,容易被当成算对了。
→ 每项都取现金价值口径;build_policy.py 会拦下保额列并告警,classify_policy.py 会点名列出。累积红利 列名在「留在主险累积生息」和
「红利已转入万能」两种情形下完全一样,但前者要加 dividend_cv(N)、后者一律不加(只计 universal)。
蒙错一次就把整笔红利双算。→ 回计划书看「红利领取方式」,或看万能账户价值曲线是否已被红利抬高;
classify_policy.py 命中此组合必告警,有告警不许跳过(第 ⑤ 步已详述)。benefit[n] 默认发生在第 n 年末。若条款约定「保单周年日/年初」领取,
真实 IRR 会略高于脚本值;差异随持有期收敛,但假设必须在口径说明中写明。premiums(现金流出),否则 IRR 虚高。premiums 剔除,否则流出被高估、IRR 偏低。benefit,且 cv 必须取减保后的现价(同一份表内口径要一致)。本轮对 12 份真实文档做全量回归时挖出的五个「不报错但算错」的坑。它们的共同特征是
退出码为 0、CSV 正常生成、数字看起来像那么回事,只有交叉核对才会暴露 —— 比崩溃危险得多。
现已在 build_policy.py / irr.py / recognize_spec.py 中全部加了守卫,此处记录以便人工排查时对照。
稀疏「保单年度」列 → 保费少算、IRR 成倍虚报
合并单元格会让首列只在第 5/10/15… 年有值,其余为空。
旧逻辑以「首列第一个纯数字行」当表头结束,把前几行真实数据当表头吞掉;
且年份为空的行被整行 continue。
实测某年金:实为年交 100 万 × 5 年 = 500 万,脚本只抓到 1 期 100 万,
IRR 从真实的 -0.2% 虚报到 29.4%。
→ 现改为「首行含 ≥2 个纯数字单元格即数据行」+ 年份空则按上一年度 +1 递推。
康熙部首字符 → 整表列定位失守
部分 PDF 的 ToUnicode 把汉字映射成康熙部首(U+2F00–U+2FDF,如 ⾦ U+2FA6 代 金、
⽣ U+2F63 代 生)。「现⾦价值」匹配不到关键词「现金价值」,locate_columns
把 prem/cv/g/d/ann 全部判为 None,随后静默产出空结果。
→ 现统一做 unicodedata.normalize("NFKC", ...) 还原。
逐字重复伪影 → 整表判为「未找到演示表」
部分 PDF 用重叠字重模拟加粗,文本层每个汉字出现两次(保保\n单单\n年年\n度度)。
recognize_spec.py 早有去重,build_policy.py 此前没有,导致关键词全部失配。
→ 现两边都做 dedup_cjk。
多层合并表头 → 保证档/演示档认错列 三层表头「生存总利益 ‖ 低档/中性红利 ‖ 万能最低保证利率/中性演示利率」抽取后, 子列只剩最下层标签,丢了上层分组名,低档与中性会被认串。 → 现改为先行内右向填充、再按列纵向拼接(合并单元格的标准还原法)。
「身故/全残保险金」列被当成「生存总利益」→ 演示 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),它八成是身故/全残给付列,不是价值列。
分析了德华安顾鑫传家的两个档位(男30 / 100万 / 20年交,年交 26,500 vs 23,780)后发现:
为什么这是陷阱: ① 若只看到「71 岁后两档现价一样」,容易误判「多交的钱后来都退回来了」——恰恰相反, 多交的 54,400 元一分不退,现价相同只是因为附加险已到期消失。 ② 若只看末年 IRR(0.94% vs 1.11%),会以为只是费率差异,看不出多交的钱买的是纯消费型定期保障。
★ 判定与处理手法(遇到同一产品多档位时必做):
多交保费总额 / 附加保额 = 每万元保额的纯消费成本,
并列出不同退保时点的「多交保费 − 多退现价 = 净成本」。判为定额终身寿后走本节,不再按 §7 的收益口径组织报告。
| 指标 | 公式 | 怎么读 |
|---|---|---|
| 首年杠杆 | 身故保额 ÷ 首年保费 | 第 1 年撬动多少倍,最直观地体现"杠杆" |
| 总保费杠杆 | 身故保额 ÷ 缴费期总保费 | ★ 掏完所有钱之后还能撬动几倍,横向可比的核心指标 |
| 峰值赔付杠杆 | 责任期内最高身故赔付 ÷ 总保费 | 最有利单一情景(互斥不叠加)能到几倍;必须附「构成明细 + 极端性注解」(见 §7.18.3) |
| 单位保障成本 | 总保费 ÷ 保额(万元) | ★ 每买 1 万元身故保障要花多少,越低越划算;与总保费杠杆互为倒数 |
| 逐年静态杠杆 | 第 N 年身故赔付 ÷ 累计已交保费 | 随持有年限自然衰减的曲线 |
| 退保差额 | 第 N 年累计已交保费 − 第 N 年现金价值 | 正数 = 若当年退保白花多少(保障的净花费);负数 = 现价已超保费 |
单位保障成本与总保费杠杆是同一个事实的两种说法,报告里二选一主打即可,不要并列当两个结论讲。
irr.py),但放在「退保能拿回多少」小节,
不放进结论速览的第一行,也不作为产品优劣的评判依据。IRR 长期在 0~1% 是杠杆寿的本来面目——保费主要被拿去买身故保障了, 剩下来的才进现金价值。这不是产品差。
★ 对比表必须单列「缴费期(年)」这一行(用户强要求)
因为 总保费 = 年交 × 缴费期,缴费期直接决定总保费,进而决定总保费杠杆:
| 指标 | 受缴费期影响吗 | 说明 |
|---|---|---|
| 首年杠杆(保额 ÷ 首年保费) | ❌ 不受 | 只跟第 1 年交的钱有关 |
| 总保费杠杆(保额 ÷ 总保费) | ✅ 受很大影响 | 交得越久,总保费越高,杠杆越低 |
| 单位保障成本(总保费 ÷ 保额万元) | ✅ 受很大影响 | 同上,交得越久越"贵" |
| 峰值杠杆(峰值赔付 ÷ 总保费) | ✅ 受影响 | 分母是总保费 |
⚠️ 关键:同一款产品拉长缴费期,总保费杠杆必然下降、单位成本必然上升, 但这纯粹是数学必然,不代表产品变差——拉长缴费反而提高了首年杠杆、 降低了每年缴费压力,是"用时间换保障"的合理安排。
classify_life.py --compare 已自动加「缴费期(年)」行,
并在缴费期不一致时自动打印告警(说明为什么不能直接比、该看什么)。classify_life.py --matrix):
一般身故/全残 · 猝死 · 其他意外 · 客运交通意外 · 航空意外 · 动车高铁意外 · 附加责任到期后。
因为同样是 100 万保额,附加责任不同,实际赔付可能差 3 倍(实测 100 万 ~ 315 万)。★ 峰值赔付的三条叠加铁律(算错就会让产品错误登顶):
★ 峰值赔付必须做「构成 + 极端性」注解(用户强要求,漏了等于误导):
classify_life.py 现在会在单品与对比里同时输出:
合计 315万 = 一般身故100万 + 关爱金100万 + 航空意外100万);常规 / 特定(非意外) / 交通意外 / 极端小概率
(航空、动车高铁、自然灾害属此类,发生概率极低)。show() 在杠杆率表后单列「峰值赔付构成」块;
对比表新增「峰值情景」行并逐款列构成。三者口径一致,照输出即可。1. 结论速览(★ 第一行放杠杆率,不是 IRR + 一句「为什么不看 IRR」)
2. 形态判定(写明依据与置信度;置信度非「高」要照实写)
3. 杠杆率对比(四个核心指标 + 算式 + 打★规则)
4. ★ 出险情景 × 产品 赔付矩阵(互斥/上位/通用三条铁律 + 峰值构成/极端性注解)
5. 峰值赔付构成注解(单品必列:由哪几部分组成 + 是否极端小概率情景)
6. ★ 杠杆率走势图(`leverage_chart.py`,横轴自动截断,见 §7.18.5)
7. 附加责任与到期年龄(★ 到期归零,最容易漏)
8. 逐年杠杆衰减与退保差额(若有现价)
9. 现金价值与回本(参考信息,附「IRR 低不等于产品差」的提醒)
10. 加档成本拆解(若同产品有多档位,见 §7.17b)
11. 怎么选(按**出险情景**匹配,不按 IRR)
12. 计算口径与免责声明
scripts/leverage_chart.py)杠杆寿必附一张走势图(零依赖纯 SVG;与理财类的 make_chart.py 是两个脚本,不要混用)。
坐标轴设置(★ 保额放左轴,不要跟杠杆率共右轴)
| 轴 | 内容 | 理由 |
|---|---|---|
| 横轴 | 保单年度 | — |
| 左轴(金额) | 累计已交保费 · 一般身故赔付 · 基本保额(水平虚线参考) | 保额本质是金额,与累计保费同量纲,放一起才能直接比「交的钱 vs 能赔的钱」 |
| 右轴(倍数) | 杠杆率 = 一般身故赔付 ÷ 累计已交保费 | 独占右轴 |
⚠️ 常见错误:把「保额 + 杠杆率」一起放右轴。 保额是 100 万级、杠杆率是 1.9~78 倍,量级差 4 个数量级,共用一轴必有一条被压成贴底直线。 ⭐ 画「一般身故赔付」而不是只画「保额」:身故赔付含无条件附加(关爱金), 能看出附加责任到期时的断崖跳变(实测德华安顾第 41 年 150 万→100 万)。 保额只是恒定 100 万的水平线,单独画信息量很小,留作水平参考虚线即可。
★ 横轴必须截断(后期是死重量,占一半画布却没信息)
--tail 3,可调),
轴末端画折断符号,并注明「第 N 年后各指标恒定,已截断(实际保障至第 M 年)」。
「最后一个变化年」= 累计保费与身故赔付都停止变化的年度(缴费结束 / 附加责任到期的较晚者)。--xmax 30。★ 线性 vs 对数(--log)
杠杆率是 1/x 衰减(实测德华 A:第 1 年 56.6x → 第 2 年 28.3x → 第 5 年 11.3x →
第 20 年 2.83x → 第 41 年 1.89x 后恒定):
--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 档)。
实测踩到的坑,会影响 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 年, 回本年度与逐年表全错)。
处理规则:
years 字段显式给出每行真实年度;脚本优先用 years[i],
取不到才回退 range(1, n+1)。索引 = N-1。max(years),不是数据点数 n。irr.py 约定 cv[n] = 第 n 年末,假设数组连续,抽样年度表不能直接用;
改为按真实年度构造现金流自己解 XIRR(现金流 [(t, 金额)],流出为负)。
make_chart.py 横坐标按值映射,支持抽样年度,可直接喂真实年度的 CSV。遇到「年龄和年度对不上」的输出,第一反应就该查是不是踩了这个坑。
scripts/make_chart.py 双轴折线图(见 §九)。标准结构见 examples/sample_对比_3款.md。
scripts/make_chart.py 双轴折线图(多色区分)。压缩摘要标准结构见 examples/sample_对比_5款以上.md。scripts/stress_test.py 压力测试(§7.15:IRR + 翻倍年数 + 临界实现率)。全部为纯保证产品(如传统非分红年金/增额寿)时可省略,并在报告中注明「本批产品无非保证利益,无需压力测试」。
实现率 0% → 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。触发时机:HTML 报告文件已落盘、正文结论已在对话里给完之后,立刻问,不要等用户开口。 一次分析只问一次;用户选完就转,不要再追问第二遍。
问法(用多选问询,multiSelect: true):
报告已生成(HTML)。要不要再转成以下格式?可多选,也可以都不要。
| 选项 | 说明 |
|---|---|
| PDF(推荐) | 排版快照,与 HTML 视觉一致,适合发人 / 打印 / 存档 |
| Word(DOCX) | 可编辑文档,适合自己改字、加批注、走审批流;图表以图片插入 |
| Excel(XLSX) | 把报告里的表格拆成多个 sheet,图表挂在对应小节 sheet,方便自己再算 |
| 不需要,HTML 就行 | 到此收尾 |
★ 四条硬规则
执行:python scripts/export.py --html 报告.html --to pdf docx xlsx(--to 后面跟几个就转几个)。
转完逐个报出「格式 → 文件路径 → 大小 → 内容摘要(页数 / sheet 数 / 段落·表·图 数)」。
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 + 日期)。结构化校验(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 → 图注 + 图片。add_picture(见 §9.3)。不再只是占位说明。font.name 与 w:eastAsia,否则 Word 里中文回落成宋体/方框。Excel(XLSX) —— export.py --to xlsx,依赖 openpyxl(插图还需 Pillow):
ws.max_row 不会因为 add_image 而增加,
不累加会让两张图完全重叠(实测都锚在 A3)。--tables tables.json 直接喂结构化表格({sheet名: [[行…]]},此模式不含图)。export.py 内置,命令:chrome --headless=new --screenshot=<out.png> --window-size=W,H <in.svg>。
不需要 cairosvg / svglib / reportlab(这些库通常都没装),只要本机有 Chrome 或 Edge。
必须做的两件事(少一件就是废图):
width/height 改成分辨率尺寸(原始宽 × scale,保留 viewBox)。
不改的话,Chrome 会把 980×580 的图画在 1960×1160 窗口的左上角——
实测四个象限只有左上 6.98% 有内容,其余三个象限全是 0%,等于输出一张 3/4 空白的图。xmlns 与 viewBox:独立 .svg 文件没有 xmlns 浏览器不解析;
没有 viewBox 光改 width/height 只会裁切不会缩放。其它要点
--svg-scale 2(默认):出 2 倍图,Word / Excel 里按 1 倍尺寸显示 → 打印更清晰。要更清晰用 3。viewBox)全小写,导出的 SVG 直接废掉。<out-dir>/<name>_图/,可用 --img-dir 指定;重跑会复用同名文件。--no-images 可强制关闭插图(退回占位说明)。一句话记住:PDF = 给人看的成品;Word = 给人改的稿子(带图);Excel = 给人再算的数据(带图)。 三者可以同时要,也可以一个都不要——让使用人选。
非理财类:仅做「产品 + 条款」分析,不产出 IRR / 总利益表(见 §六路由)。
投保规则(年龄/职业/等待期/犹豫期/健康告知)、保障责任(重疾/中症/轻症种类与赔付次数、保额、身故、豁免)、免责条款(绝对不赔项,标「隐形坑」)、续保条件(一年期是否保证续保、费率上调风险)、赔付机制(免赔额/报销范围/赔付比例/医院限制)、杠杆率(年保费 vs 保额)、现金价值。专项:重疾分组/不分组·间隔期·多次赔;医疗外购药/质子重离子;意外伤残等级·猝死·职业限制。
起因:四款重疾险对比的结论速览里写了「A 17,130 / B 24,075 / C 29.19 倍」, 可是报告开头从没说过 A/B/C/D 分别是哪款产品——读者得翻回产品一览才能对上号, 结论等于看不懂。
★ 三条硬规则
A 阿基米德2025 / C 太保真爱3号·无身故——让读者跳到表格中段也认得出。起因:重疾险对比里做了一整节「现金价值与回本」,还列了「第 1 年现价(退保能拿回)65~910 元」、 现价峰值、峰值/总保费比,并专门画了现价走势图。使用人反馈: 重疾险测回本不是特别重要的事,有就可以了;尤其是首年退保现价基本不需要。
★ 处理规则
scripts/benefit_chart.py)健康险真正会随年龄变化的是保障额度,不是现价。典型情形:
这些用表格看不出来,必须画成走势图(零依赖纯 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]}}
--combine 责任1,责任2,…(★ 推荐):把若干责任叠成一张图,纵向多格共用同一条 X 轴。
使用人明确要求:「重疾、轻症、中症的走势可以在同一张图里,不需要做那么多图」。★ 重疾险的标准配置就是两张图,不要出四张
# ① 疾病保障:重 / 中 / 轻三格合一,共用纵轴刻度(三档高低可直接比)
python scripts/benefit_chart.py --policy *.json \
--combine 重大疾病,中症,轻症 --out 疾病保障走势.svg
# ② 身故保障:单独一张(形态与疾病完全不同,混一张会互相压扁)
python scripts/benefit_chart.py --policy *.json \
--benefits 身故/全残 --out 身故保障走势.svg
--combine 的多格图里是合适的——顺带展示"这一档大家都是多少"。非理财类:仅做「产品 + 条款」分析,不产出 IRR / 总利益表(见 §六路由)。
车险:交强+商业构成、三者险保额、车损/座位、免赔、无赔款优待、免责(酒驾/自燃/涉水)。家财/责任:保额确定方式(定值/不定值)、免赔、责任范围、盗抢、管道破裂。
多款非理财类对比:只对比「产品与条款」维度(上述投保规则/保障责任/免责/续保/赔付机制/杠杆率等),不做收益(IRR/总利益)对比,因为非理财类无现金价值增值收益可比。结构见 references/templates.md 非理财类多品对比模板。
ocr-local 等);但密集数字表识别易错,来自 OCR 的关键数字必须让用户核对后再算 IRR,绝不凭未核对 OCR 数字直接计算。openpyxl/python-docx/pdfplumber,经 scripts/extract.py)原生抽取;仅当运行环境无这些库时才降级为追问/粘贴参数。凡内嵌图片导致数字读不出时,转 OCR 或请用户以文本/Excel 提供关键列。scripts/classify_policy.py:生存总利益「计法判定器」(§7.16 决策树自动化,理财类拿到列名后第一个要跑的脚本)。
输入产品名与列名,逐层输出 ①大形态 ②是否分红 ③分红构成(现金/增额/终了/特别)④现金红利去向
(累积生息 / 现金领取 / 进入万能)⑤哪些现金流进了万能 → 建议 policy 字段映射 → 生存总利益公式 → 冲突检查。
调用:python scripts/classify_policy.py --name "<产品名>" --columns "<逗号分隔列名>";
已构造好 policy 后用 --policy policy.json 反查字段填得对不对(含重复计入冲突检查)。
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 / 回本年限 / 总利益(保证/演示)计算。调用方式见文件头注释。
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 天然比价值数组短(只覆盖交费期),不算长度异常。缴费期: 第 1–P 年 → 该区间 IRR 列显示「缴费期」。只改显示,不动现金流。
缴费期判定 = payment_span()(连续前缀)+ premium_base()(0 基/1 基下标探测)。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]。
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 抽取结果会明确报错并指向手工构造路径(精算表工作簿不是演示表,本就不该走自动链路)。exit 1 并给出下一步建议,绝不静默产出垃圾。is_death() 拦下标签含「身故/全残」的列,
防止身故总利益被当成生存总利益(实测会让演示 IRR 严重高估)。
若表头重建把两者拼成同一标签,本脚本无法可靠区分 → 硬失败并要求手工指定列。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]。
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]。
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 必填。@media print 时自动注入打印样式。python-docx + lxml;图表栅格化成 PNG 后插入(§9.3),纸张强制 A4。openpyxl(插图还需 Pillow);按小节拆 sheet,表在下、图挂在表后(§9.2)。
也可用 --tables xxx.json({sheet名: [[行…]]})直接喂(此模式不含图)。--svg-scale N(默认 2)、--img-dir 目录、--no-images(强制占位说明)。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 会把未捕获异常自动写成脱敏快照。
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 → 使用人双击、点发送
零凭据、零第三方依赖、零网络配置。
保险计划书里有姓名、身份证号、保单号、保费金额,一个字都不能进反馈。
模型在写任何反馈前必须自检:
桌面/张三的保单.pdf)1.298、3.5)、列号与报错信息feedback.py 会自动脱敏兜底,但不要把敏感信息当原文交出去再指望脱敏——
描述阶段就该写成「年交约六位数,5 年交」这类不含原始数值的说法。
| 时机 | 模型动作 |
|---|---|
| ① 脚本抛未捕获异常 | 钩子已自动生成快照。主动告知使用人:已生成故障快照 + 路径,并问是否要 export 打包发作者。不要替使用人决定发送。 |
| ② 使用人提改进意见 | 调用 feedback.py report --type 建议 --content "…",把意见写成一句「期望什么 vs 现在是什么」。 |
| ③ 使用人质疑结果不对 | 调用 feedback.py report --type 数据问题,写清:输入类型、产品类型与缴费方式、执行命令、IRR 期望值 vs 实际值(小数可保留)、已排除的原因。 |
| ④ 使用人抱怨 / 想放弃(§14.5 主动) | 先解决问题,事后再用 feedback.py invite 的一句话文案询问;只有得到肯定答复才记录。隐性信号(「算了不用来」「这个不能 XXX 吗」)比显性抱怨更容易漏,也更值得接住。 |
第 ③ 种尤其重要——数据问题就是潜在的静默算错 bug,是技能最需要收集的反馈。
# 手动提一条(--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 只比对正文、忽略时间戳与环境),不会让作者收到两条一模一样的反馈——那既令人困惑,也会让人误判这个问题的普遍程度。内容哪怕只差一句,都会正常记为新的一条。
~/.workbuddypro/feedback/保险计划书分析大师/,不在技能目录内。
两个原因:重装/升级技能不丢反馈;绝不会被打进分发包泄露给别人。INS_FEEDBACK=0(不落盘、不提示)。scripts/feedback.py 的 DEFAULT_CONFIG["author_email"],
当前为 zhenwei-shi@qq.com。使用人本机可在
~/.workbuddypro/feedback/保险计划书分析大师/feedback_config.json 里覆盖。export 时看到「收件邮箱还是占位符」的警告,说明默认值未配置,
模型应提醒使用人,不要盲目导出。§14.2 是被动的(报错了才收集)。本节是主动的:使用人未必会主动提意见, 更多时候只是抱怨一句、或者干脆说「算了,不用来」。这类信号如果不接住,作者永远收不到。
★ 机制先说清楚(避免误解):SKILL.md 没有运行时监听能力,脚本只在被调用时运行。 所谓「抓取负面评价」是模型读了本节规则后自行判断—— 这比关键词匹配更聪明(能识别隐性不满、不会因一个「不好」就误触发), 但也意味着本节能否生效完全取决于模型是否照做。
显性(直接说出来)
隐性(更容易漏掉,但信息量更大)
python scripts/feedback.py invite --about "<这次是关于什么>" 拿到标准文案,
转述给使用人。一句话、非阻塞、给明确退路,不要长篇大论。report。
对方没接话就是拒绝,不要追问。这套机制的目的是接住原本会流失的意见,不是增加交互负担。 宁可少问一次,也不要让人觉得被打扰。
使用人同意后:
python scripts/feedback.py report --type 建议 \
--title "一句话说清问题" \
--content "期望什么 vs 现在是什么" \
--context "当时在做什么(如:在给 5 年交年金做压力测试)"
--context 是作者复现时最需要、又最容易被漏掉的信息,尽量填。
内容同样受 §14.1 隐私铁律约束——脱敏后再写入。