ysoseri.us

实习日志 / 中文 / 保单知识库 / 保单里没有答案的时候:一个租车问题牵出的知识库重做

保单里没有答案的时候:一个租车问题牵出的知识库重做

中文 | English

10 月初为 demo 录制做采集时,有一句很普通的问题卡住了 Agent:"我的车修的这段时间,保险会付租车的钱吗?"

它先是什么都没查到,然后回答:"你可以去问问你的保险公司。"可 Northwind 就是保险公司。更糟的是,它还声称"已经升级处理",而那一轮根本没有任何升级动作。

查原因很快:车险保单里本来就没有租车条款,查不到是事实。但"查不到"之后怎么回答,以及为什么这么常见的问题没有条款,牵出来的是整个保单知识库的问题。这篇记的就是 10 月 2 日到 4 日围绕它做的调研和设计。

先看市场上怎么写租车保障

Northwind 的保单是演示用的合成文本,条款只需要合理、可执行,但我不想凭空编,所以先查了新西兰三家保险公司的做法:

保险公司内容限额不赔的情况
AMI车辆不能驾驶、正在维修或等待全损理赔期间,安排租车或报销其他交通费每天最多 40 新元,最多 21 天;综合险和第三方火盗险都有原文 PDF 打不开,没查到详细排除条款
Tower租同级车并投保,或报销出租车、公共交通的合理费用合计最多 1,000 新元事故后车辆仍可合法驾驶;车找回时可驾驶;修好以后
AA Insurance一项附加险(每年 50 新元),提供小型租车用到修好或理赔结案需要综合险,且理赔已被受理

四个共同点很清楚:以理赔被受理为前提;以车辆不能用为前提;有每日或总额的上限;修好、找回或结案后停止。AMI 和 Tower 的原文 PDF 当时都打不开,数字来自搜索结果的摘录,用来写合成条款足够,但不能拿来当真实产品的引用。

照着这几点,我给 Northwind 的车险写了一条 MTR-BEN-03:

## MTR-BEN-03 - Hire car, rental car and alternative transport while your car is repaired

If Northwind accepts a claim for loss to the insured vehicle under MTR-COV-01, including theft, and
the vehicle cannot be driven legally or safely, is being repaired by a Northwind-approved repairer,
or has been stolen and not recovered, Northwind may arrange a hire car of the same or a smaller
class, or reimburse reasonable costs of taxis, ride-share or public transport.

- Limit: up to NZD 60 a day, for up to 21 days per event.
- Starts: when the claim is accepted; for theft, when the theft is reported to the Police and to
  Northwind.
- Ends: when repairs are finished, when the vehicle is recovered and can be driven, or 7 days after
  Northwind offers a total-loss settlement, whichever is first.
- Not included: fuel, tolls, parking, fines, and the hire company's excess or damage charges; any
  period while the vehicle can still be driven legally and safely.
- No excess applies to this benefit. It applies whoever was at fault; Northwind may recover the cost
  from a responsible third party.
- Agree the hire with Northwind first. Costs paid by the policyholder are reimbursed up to the same
  daily limit.
- This benefit applies only when the policy schedule lists alternative transport.

最后一句是故意加的。没有它,所有车险保单都自动包含这项保障,也就不存在"这张保单里没有"的情况,而那恰恰是客户最需要被讲清楚的一种。

客户应该得到什么样的回答

在动手改知识库之前,我先把目标写成了三段对话,都在车险上:

  • 保单包含。客户问修车期间租车赔不赔,Agent 回答:理赔被受理以后,替代交通每天最多 60 新元、最多 21 天,你的这张保单包含这一项,并引用 MTR-BEN-03。
  • 保障里不包含的部分。客户接着问租车的油费赔不赔,Agent 回答:油费、过路费和停车费不包含,同样引用 MTR-BEN-03。
  • 这张保单没有。同样的租车问题,问在一张明细里没列替代交通的保单上,Agent 回答:条款规定只有明细列出这项保障时才适用,而这张保单的明细没有列,仍然引用 MTR-BEN-03。

另外两条规则适用于所有回答:

  • 不做决定。 Agent 永远不说某一单"保"或"会赔",条款加明细只能得出"你的保单包含某项保障,以理赔被受理为前提"。需要决定的,进待答清单,交给员工。
  • 不编引用。 没有条款能回答的问题,绝不能附一个条款编号。要么保单里有一条明确写着"不包含"的条款,要么 Agent 说它找不到答案,把问题放进待答清单。

问题不止一个

对着这三段对话去核对代码,一共查出六个问题。

P1:保单没覆盖客户常问的事。 三份保单,每份 15 到 16 条条款,每个险种大约 2,000 token。车险没有租车条款,版本号还带着 MVP 前缀。

P2:检索按字面匹配,不懂意思。 报案回合用一个关键词排序器找条款,查询词就是客户的原话。它会丢掉两个字母以内的词、一批停用词(policy、claim、what、how、much……),还有险种名词(car、vehicle、motor、home、house……);词形只去掉末尾的 s,所以 repaired 匹配不到 repair,rental 匹配不到 rent,hired 匹配不到 hire;打分是"标题命中数乘 4 加正文命中数"。后果是:只有条款恰好重复了客户的原词才找得到。"Will my policy pay for a rental car while mine is repaired?" 过滤完只剩 will、pay、rental、mine、repaired,一条标题叫"Alternative transport"的条款得零分。"Is my car covered?" 过滤完只剩一个 covered。要让它管用,条款文字就得追着各种同义词和词形去写。

P3:最多返回 3 条,每条被截到约 600 字符。 策略引用的解析预算上限是 600 token,规划器和工具 schema 各设了一道;每块条款被截成 max_tokens * 3 // 条数 个字符,返回 3 条时每条约 600 字符。可一条完整的保障条款有 900 到 1,200 字符,上面那条 MTR-BEN-03 约 1,300。模型永远看不到条款的尾巴,而排除项和"以明细为准"恰恰写在尾巴上。

P4:本地环境被钉死在旧版本上。 代码选知识库版本的顺序是:控制面当前的发布;没有就用知识管理里已发布的记录;再没有就用常量 'MVP-2026.1'。用 start-dev.ps1 起的普通本地环境前两样都没有,于是永远读那个常量。新版本就算入了库,也永远不会被读到。

P5:Agent 判断不了"这一张保单"包不包含某项保障。 保单明细的字段其实有,coverage_sections 是一个自由的字符串列表,按报案里的保单号去查。但本地和测试环境用的都是一个模拟的保单查询适配器,它的明细是代码里写死的一个字典,只有两张保单。测试的人随手输入一个保单号,结果一律是"没有证据",所以没有任何一个回合能看到一张包含或不包含某项保障的明细。

P6:没有任何东西在量"对的条款有没有进到回答里"。 已有 6 个评估用例,但它们只测搜索接口,没有一个是从报案对话进去的;一个回合显示"完成",也证明不了检索真的跑过。

让模型自己挑条款

P2 和 P3 的修法,我没有去做向量检索,因为保单很小:三个险种一共约 50 条条款,每个险种约 2,000 token。所以可以让模型直接读本险种的条款标题目录,自己说出需要哪几条,运行时再把这几条完整地取回来。把问题匹配到条款,变成本轮模型自己的一个判断,不需要另一个模型,也不需要向量索引。

查询回合的流程变成五步:

  1. 目录进上下文。 规划好查询配置、险种已确认时,上下文里加一个 policy_clause_directory,内容是 [{clause_id, title}],按险种和当前版本给出,约 16 条、300 token 左右。目录来自已经校验、入库的条款块,所以绝不会列出一条取不回来的条款。
  2. 模型点名。 context.resolve 多一个可选参数 clause_ids,最多 3 个,每个都必须来自目录。
  3. 运行时完整返回。 按和现在同样的范围过滤(险种、版本、生效日、可见性、权限、保险公司)取回点名的条款,每条全文返回、不截断,连同保单明细一起给模型;目录里不存在的 ID 列进 not_found,绝不拿另一条顶替。
  4. 引用规则不变。 本轮只有运行时实际取回的条款可以被引用,模型没法引用一条运行时没给它的条款。
  5. 没点名就不给条款。 clause_ids 为空时,只返回保单明细和 guidance_status: no_clause_selected,模型要么根据明细回答,要么把问题放进待答清单,不能引用任何条款。

预算也算过:3 条最长 1,500 字符的完整条款约 1,100 token,在新的 1,200 上限以内;目录约 300 token;而在 10 月 1 日到 3 日的本地真实模型运行里,查询回合只占 149 个回合里的 14 个,其余回合完全不受影响。

原来的关键词排序器没有删,内部搜索接口、管理端的评估和其他调用方照常用它,变的只是报案回合不再用它找条款。

失败的路径也各自写清了:知识库不可用,返回现有的"不可用"结果和限制说明,保单明细照常返回,已确认的事实保留,模型说明限制或者用待答清单;某个险种没有已批准的版本,同样处理;险种还没确认,既没有目录也不查询;模型点了一个目录里没有的 ID,只有它进 not_found;点了超过 3 个,工具 schema 直接拒绝。

版本从清单里读,不用常量

P4 的修法是用仓库里现成的一个函数,从入库清单里读"这个险种当前已批准的版本",员工端的 Agent 早就在用它,把报案 Agent 里的两处常量换成它就行。这个函数在一个险种有多个已批准版本时会返回空,所以旧的 MVP 条目要在同一个改动里标成 withdrawn,否则两边的 Agent 都会拿不到版本。返回空的时候,给出"不可用",永远不再退回某个常量。

新内容和 2026.2 版

P1 先从盘点开始:每个险种一张表,"客户会怎么问 → 对应哪条条款 → 已覆盖、部分覆盖还是缺失"。问题来自受控测试语料里的客户消息、演示场景,以及一份起始清单:

险种客户的问法现有条款状态
车险修车期间赔不赔租车?无缺失,新增 MTR-BEN-03
车险免赔额多少?不是我的错也要付吗?MTR-EXC-01部分(没有无责的规则)
车险能帮我拖车吗?MTR-BEN-01已覆盖
车险挡风玻璃裂了,保吗?有免赔额吗?无缺失
车险对方没有买保险无缺失
车险车全损了怎么办?MTR-SET-01部分
车险不是我的错,会影响无赔款优待吗?无缺失
房屋房子住不了,我住哪?HOM-BEN-02已覆盖
房屋能现在叫紧急水管工吗?HOM-BEN-01已覆盖
房屋慢慢渗漏保吗,还是只保突然的?HOM-EXC-02、HOM-BEN-03已覆盖
财产东西在车里被偷、旅行中丢了无缺失(离家保障)
财产没有收据,怎么证明是我的?CON-CLM-01?待查
财产首饰、笔记本有没有限额?CON-LIM-01已覆盖

每一行"缺失"都要补一条条款;Northwind 有意不提供的保障,也写一条明确说"不包含"的条款。

写条款有几条规则:

  1. 编号只往后续,不重排、不复用。 条款块的 ID、测试和已存的引用都依赖这些编号。
  2. 标题用白话说清楚这条管什么。 模型只看标题挑条款,所以要写成 Hire car, rental car and alternative transport while your car is repaired,光写 Alternative transport 不够。这样也就不需要同义词表。
  3. 每一部分都写明白:触发条件、提供什么、限额(每天、总额、期限)、何时开始何时结束、排除项、是否扣免赔额、明细决定什么。
  4. 每条控制在约 1,500 字符以内,一条只讲一件事,保证一次查询能装下三条。
  5. 合成文本参照新西兰的市场做法写,不照抄任何一家保险公司的原文,来源记在 PR 里。

新版本定为 2026.2,文档 ID 去掉版本后缀,每个版本都是同一份文档的新版本;开头去掉"用于 MVP 演示",保留"合成、非真实保险合同"。所有写死了 MVP-2026.1 的脚本、语料工具、配置、文档和测试都列了出来要一起改,只有测"入库或保护某个版本"本身的测试可以留一个固定的测试版本。

让明细能决定"包不包含"

P5 不需要新字段,也不需要数据库种子:coverage_sections 里加一个取值 alternative_transport,表示这张保单包含 MTR-BEN-03。在代码里那个写死的保单字典里加几张保单:一张有替代交通的车险、一张没有的车险,再各加一张房屋和财产,号码用测试的人打得出来的,比如 NW-MTR-204518,写进 README。号码对不上任何一张时,结果仍是"没有证据",Agent 说它查不到这张保单的明细,不去猜。

量出来

P6 的测量全部走正常的报案消息接口,在 start-dev.ps1 起的环境里、用配置好的模型跑,绝不走搜索接口。

选条款的准确率:盘点表里的每个问题对应一个场景和一个预期条款,从回合的运行记录里读出请求和返回的条款 ID、以及最终被引用的条款,按险种报告预期条款被取回的比例、被引用的比例,以及每一次失手和模型当时看到的目录标题。失手多半意味着某个标题没说清楚它管什么,或者某个问题根本没有条款能回答,所以要在内容上修。

四个端到端场景,每个跑 5 次:保单包含时问租车;保单不包含时问同样的问题;问一个没有条款能回答的问题,应当进待答清单、不附引用;停掉 MinIO,应当说明限制、保留已确认的事实、不附引用。

验收标准一条条对应前面的问题:盘点表里没有"缺失"行;按意思找到对的条款,有准确率报告;超过 600 字符的条款在查询回合里完整返回;本地环境没有发布和发布记录时读到 2026.2;明细能决定包含与否;没有编造的引用,不可用的时候如实说明;入库在全新的和已有旧版本的 MinIO 上都能跑。

没做的和做法

有几样明确留在范围外:向量或嵌入检索(等哪天条款目录大到装不进上下文再说);改提示词(那一块由另一个正在进行的 issue 负责,如果测出来模型不理会目录或明细,带着回合记录报告);员工端 Agent 的检索;真实保险公司的条款原文;生产级的保单系统。

这份设计我写成了英文,连同一段给编码 Agent 的完整提示词,交给一位负责后端的队友实现。提示词的第一句是让它先读仓库的规则和这份设计,在改任何东西之前,先在代码里核实每一条陈述的事实。

再看开头那句"去问问你的保险公司",它其实暴露了三层问题:内容里没有答案,检索找不到意思相近的答案,找不到时也没有一条诚实的出路。只补一条租车条款,能让这一句对话变好看,但下一个没写进条款的问题还会同样失败。所以要同时补三样:让内容覆盖客户真的会问的事,让模型按意思挑条款并看到全文,让"找不到"成为一条正常的路径。