1. 从零搭建一个保险智能体:我踩过的坑和最终跑通的方案
去年年底,我接手了一个内部创新项目,目标很明确:做一个能真正帮上忙的保险智能体。不是那种只会念条款的问答机器人,而是能理解用户模糊需求、能对比产品、能辅助核保、甚至能给出配置建议的“数字顾问”。说实话,刚开始我觉得这事不难,不就是把大模型接上保险知识库嘛。但真正动手之后才发现,保险这个领域的复杂度远超我的预期——条款嵌套、责任免除、等待期、现金价值、核保规则,每一个环节都是坑。我前后迭代了四个版本,从最初的“人工智障”到后来能稳定处理八成以上的常见咨询,中间积累了不少实战经验。这篇文章就把整个开发过程拆开揉碎讲清楚,包括架构选型、知识库构建、意图识别、对话管理、核保辅助这些核心模块的实现思路和具体代码。如果你也在做垂直领域的智能体,或者对保险科技感兴趣,这篇记录应该能帮你省下不少试错时间。
1.1 为什么保险场景对智能体这么不友好
先说说保险这个领域的特殊性。普通电商客服机器人,用户问“这个多少钱”“什么时候发货”,意图非常明确,槽位也好抽取。但保险咨询完全是另一回事。用户会说“我想给刚出生的孩子买个保险,预算一年五千左右,最好能保大病”,这一句话里包含了被保人年龄、预算范围、保障需求、产品类型四个关键信息,而且“大病”这个词在保险语境下可能指重疾险、医疗险、或者特定疾病险,需要进一步澄清。更麻烦的是,保险产品的条款是法律文本,同一个词在不同产品里的定义可能完全不同。比如“意外伤害”,有的产品要求“外来的、突发的、非本意的、非疾病的”,有的还会把高风险运动除外。如果智能体直接拿通用大模型来回答,很容易给出看似正确但实际有偏差的建议,这在保险场景里是致命的。
我一开始就犯了这个错误。第一版直接调了一个通用大模型接口,把产品条款塞进提示词里让它回答。测试的时候问“等待期内生病能赔吗”,它回答“等待期内生病通常不能理赔,具体以合同为准”。这个回答本身没错,但太笼统了。用户真正想知道的是:我买的这个产品等待期是多久?等待期内确诊但等待期后住院,能不能赔?这些问题需要精确到条款细节。通用模型在没有结构化知识支撑的情况下,只能给出模糊的通用答案,无法满足实际需求。
所以第二版我开始构建专门的知识库。这里又遇到一个选择:是用向量数据库做语义检索,还是用图数据库做关系推理?我最后选了混合方案。向量检索负责从海量条款中快速找到相关段落,图数据库负责处理产品之间的对比关系和核保规则链。这个决策后面会详细展开。
1.2 整体架构:三层解耦的设计思路
经过几轮迭代,最终跑通的架构分成三层。最底层是知识层,包含产品条款库、核保规则库、疾病定义库、理赔案例库四个子库。中间层是能力层,包括意图识别、槽位填充、知识检索、推理引擎、对话管理五个模块。最上层是交互层,负责多轮对话、澄清追问、结果呈现。三层之间通过标准接口通信,每一层都可以独立替换和升级。
为什么这么设计?因为保险业务变化太快。今天出一个新产品,明天调整一个核保规则,如果架构是铁板一块,每次改动都要动全身。解耦之后,知识层更新不影响能力层的逻辑,能力层升级也不影响交互层的体验。举个例子,后来我们把核保规则从硬编码改成规则引擎配置,只动了知识层和推理引擎的接口,对话管理模块完全不用改。
具体到技术选型,知识层我用的是PostgreSQL加pgvector扩展存向量,Neo4j存产品关系图。能力层用Python写核心逻辑,意图识别用微调后的BERT模型,槽位填充用规则加模型混合方案。交互层用FastAPI提供接口,前端用React做对话界面。整个系统跑在一台16核64G的服务器上,日均处理咨询量在两千左右,响应时间控制在1.5秒以内。
注意:不要一上来就追求大而全的架构。我见过不少团队一开始就设计微服务、消息队列、分布式缓存,结果开发两周还在调通链路。保险智能体的核心难点在知识处理和推理逻辑,架构够用就行,先把核心流程跑通再考虑扩展。
2. 知识库构建:从PDF条款到结构化知识的完整流程
知识库是整个智能体的地基。地基不牢,上面盖什么都是危房。我在这上面花了将近一半的开发时间,现在回头看,每一分钟都值得。
2.1 条款解析:为什么直接OCR不够用
保险条款通常是PDF格式,几十页到上百页不等。第一反应肯定是OCR提取文字,但直接OCR出来的文本是一大坨,没有层级结构。条款里有章、节、条、款、项,还有各种表格和附件。如果只是把纯文本塞进向量库,检索出来的片段可能缺上下文,导致理解偏差。
我的做法是先用PDF解析库提取带坐标的文本块,然后根据字体大小、加粗、缩进这些排版特征重建层级结构。具体来说,标题通常字号大且加粗,条款正文有固定缩进,表格有独立的边框识别。解析完之后,每个条款变成一个JSON对象,包含条款编号、标题、正文、所属章节、关联条款这些字段。
# 条款解析核心逻辑示意 import pdfplumber from collections import defaultdict def parse_policy_pdf(pdf_path): clauses = [] with pdfplumber.open(pdf_path) as pdf: for page in pdf.pages: # 提取文本块及其字体信息 words = page.extract_words(extra_attrs=['fontname', 'size']) # 按行分组 lines = group_by_line(words) for line in lines: # 根据字体大小判断是否为标题 if is_heading(line): current_section = line['text'] # 根据缩进和编号识别条款 elif is_clause(line): clause = extract_clause(line, current_section) clauses.append(clause) return build_hierarchy(clauses)这里有个关键点:条款之间的引用关系必须保留。比如“本条款适用第3.2条约定”,如果解析时丢了这个引用,后续推理就会断链。我的处理方式是在解析阶段就抽取所有“第X条”“第X款”这样的引用标记,存成图数据库的边。这样当用户问到某个条款时,系统能自动把关联条款一起检索出来。
2.2 向量化策略:不是所有文本都值得嵌入
条款解析完之后,下一步是向量化。这里我踩过一个坑:一开始把所有条款不分青红皂白全部嵌入,结果检索效果很差。因为有些条款是通用说明,比如“本合同未尽事宜按相关法律法规处理”,这种条款跟用户问题相关性很低,但向量相似度可能不低,反而挤占了真正相关条款的位置。
后来我做了分层处理。核心条款——也就是定义、保障责任、责任免除、等待期、理赔流程这些——做精细嵌入,每个条款单独一个向量。辅助条款——比如通知义务、合同变更、争议处理——做摘要后嵌入,降低权重。通用条款直接不入向量库,只在需要时通过图关系检索。
嵌入模型的选择也折腾了一阵。最开始用通用中文嵌入模型,发现对保险术语的区分度不够。“重疾险”和“医疗险”在通用模型里向量距离很近,但在保险场景里这是两个完全不同的品类。后来我用保险领域的问答对做了微调,让模型学会区分这些专业概念。微调数据大概三千条,都是人工标注的“问题-相关条款”对。微调之后,检索准确率从百分之六十七提升到了百分之八十九。
2.3 核保规则库:把Excel变成可执行逻辑
核保是保险智能体最有价值的场景之一。用户输入年龄、性别、健康状况,系统判断能不能保、以什么条件保。传统做法是核保员查手册,智能体要做的是把手册变成可执行的规则。
原始资料是一堆Excel表格,行是疾病,列是核保结论。但直接读Excel不行,因为规则之间有依赖关系。比如“高血压”的核保结论取决于血压值、是否服药、有无并发症,这些子条件又各自有判断逻辑。我的做法是先把Excel转成决策表,再人工梳理成决策树,最后用规则引擎表达。
# 核保规则示例:高血压核保逻辑 def underwrite_hypertension(applicant): bp_systolic = applicant['blood_pressure_systolic'] bp_diastolic = applicant['blood_pressure_diastolic'] on_medication = applicant['on_medication'] complications = applicant['complications'] if bp_systolic < 140 and bp_diastolic < 90: return {'result': 'standard', 'reason': '血压正常范围'} elif bp_systolic < 160 and bp_diastolic < 100: if not on_medication and not complications: return {'result': 'standard', 'reason': '轻度高血压,无并发症'} elif on_medication and not complications: return {'result': 'substandard', 'reason': '服药控制中,需加费'} else: return {'result': 'decline', 'reason': '高血压伴并发症'} else: return {'result': 'decline', 'reason': '血压超出可保范围'}这套规则引擎跑起来之后,常见疾病的核保判断准确率能达到百分之九十五以上。但要注意,规则引擎只能处理明确的条件,遇到模糊描述比如“医生建议进一步检查”就需要转人工。我在系统里设了置信度阈值,低于阈值的自动转人工核保员,避免误判。
实操心得:核保规则库一定要版本化。保险公司的核保政策可能每季度调整,如果没有版本管理,某天更新了规则但没记录,出了问题根本查不到原因。我用Git管理规则文件,每次变更都有commit记录,线上出问题能快速回滚。
3. 对话引擎:让智能体听懂“人话”的核心逻辑
知识库建好之后,下一个难题是怎么让用户自然地提问,系统准确地理解。保险咨询的特点是用户表达非常口语化,而且经常省略关键信息。对话引擎要做的就是把这些模糊表达翻译成结构化查询。
3.1 意图识别:从“我想买保险”到具体需求
用户说“我想买保险”,这句话的意图是什么?可能是想了解产品,可能是想对比价格,可能是想直接投保。如果系统直接推荐产品,很可能不是用户想要的。我的做法是把意图分成三层:第一层是动作意图,比如咨询、对比、投保、理赔;第二层是对象意图,比如重疾险、医疗险、意外险;第三层是约束意图,比如预算、年龄、健康状况。
意图识别模型用的是BERT微调。训练数据来自历史客服对话记录,人工标注了五千条。这里有个技巧:不要只标注最终意图,要把中间澄清过程也标注进去。比如用户先说“我想买保险”,客服追问“您想给谁买”,用户回答“给孩子”,这个追问-回答对也是训练数据。这样模型不仅学会识别意图,还学会在信息不足时主动追问。
# 意图识别输出示例 { "action_intent": "consult", "object_intent": "critical_illness", "constraints": { "insured_age": 0, "budget": 5000, "payment_period": "20年" }, "confidence": 0.92, "missing_slots": ["health_condition"] }当置信度低于阈值或者关键槽位缺失时,对话引擎会触发澄清追问。追问的话术也经过设计,不能太生硬。比如不会直接问“请提供健康状况”,而是说“为了给您更准确的建议,方便告诉我被保人有没有既往病史吗”。
3.2 多轮对话管理:状态追踪与上下文继承
保险咨询很少是一问一答就结束的。用户可能先问重疾险,再问医疗险,然后对比两者,最后回到重疾险问具体条款。对话引擎必须记住整个会话的上下文,不能每轮都从零开始。
我用的是基于状态机的对话管理。每个会话有一个状态对象,记录当前讨论的产品、已收集的槽位、历史意图。当新消息进来时,先做意图识别和槽位填充,然后根据当前状态决定下一步动作。动作空间包括:直接回答、澄清追问、推荐产品、对比展示、转人工。
class DialogState: def __init__(self): self.current_product = None self.collected_slots = {} self.history = [] self.turn_count = 0 def update(self, user_input, nlu_result): self.history.append({ 'user': user_input, 'nlu': nlu_result, 'timestamp': time.time() }) # 继承上一轮的槽位 for slot, value in nlu_result.get('slots', {}).items(): if value is not None: self.collected_slots[slot] = value # 更新当前讨论产品 if nlu_result.get('object_intent'): self.current_product = nlu_result['object_intent'] self.turn_count += 1这里有个细节:槽位继承要有优先级。如果用户这轮明确说了“预算一万”,那就覆盖上一轮的“预算五千”。但如果这轮没提预算,就保留上一轮的值。我一开始没做优先级判断,导致用户改了口径系统还在用旧值,闹了不少笑话。
3.3 回复生成:模板加模型的双轨制
回复生成这块,我试过纯模型生成和纯模板两种方案,最后选了混合。纯模型生成灵活但不可控,保险场景里说错一句话可能引发合规问题。纯模板可控但生硬,用户体验差。
我的方案是:核心信息用模板保证准确性,过渡语句用模型生成保证自然度。比如推荐产品时,产品名称、保障责任、保费这些关键信息从知识库直接填充模板,而“根据您的情况,我建议...”这样的引导语由模型生成。这样既保证了信息准确,又让对话不那么机械。
def generate_response(intent, slots, knowledge): # 核心信息模板 if intent == 'recommend': products = knowledge['products'] template = "根据您{constraints}的情况,我筛选了{count}款产品:\n" for p in products: template += f"- {p['name']}:{p['feature']},参考保费{p['premium']}元/年\n" template += "您想详细了解哪一款?" return template.format(**slots) # 过渡语句由模型生成 elif intent == 'clarify': prompt = f"用户问了一个保险问题,需要澄清{slots['missing_slot']},请用友好的语气追问。" return model.generate(prompt)注意:保险智能体的所有输出都必须经过合规过滤。我建了一个敏感词库,包含“保证收益”“肯定赔”“最好”这类违规表述,生成结果先过一遍过滤器再返回。这个步骤不能省,否则可能给公司带来监管风险。
4. 核保辅助与产品对比:两个高频场景的落地细节
对话引擎跑通之后,我重点打磨了两个高频场景:核保辅助和产品对比。这两个场景用户需求最强烈,也最能体现智能体的价值。
4.1 核保辅助:从问卷到预核保结论
核保辅助的流程是这样的:用户输入基本信息,系统逐步追问健康状况,最后给出预核保结论。听起来简单,但实现起来有几个难点。
第一个难点是疾病描述的标准化。用户说“我血糖有点高”,这可能是空腹血糖受损,也可能是糖尿病。系统需要追问具体数值和诊断情况。我建了一个疾病同义词库,把口语化描述映射到标准疾病名称,然后再走核保规则。
第二个难点是追问逻辑。不是所有疾病都需要追问所有细节。比如用户说“做过阑尾炎手术”,系统只需要确认手术时间、是否痊愈、有无并发症,不需要问家族史。我根据疾病类型预设了追问模板,每种疾病对应一组必问问题。
# 疾病追问模板示例 FOLLOW_UP_TEMPLATES = { 'hypertension': [ {'question': '最近一次测量的血压值是多少?', 'slot': 'bp_value'}, {'question': '是否在服用降压药?', 'slot': 'on_medication'}, {'question': '有没有出现心脑血管并发症?', 'slot': 'complications'} ], 'diabetes': [ {'question': '确诊多久了?', 'slot': 'diagnosis_duration'}, {'question': '最近一次空腹血糖是多少?', 'slot': 'fasting_glucose'}, {'question': '是否使用胰岛素?', 'slot': 'insulin_usage'} ] }第三个难点是结论解释。核保结论不能只给一个“加费”或“拒保”,要说明原因。比如“因高血压伴并发症,本次投保需加费百分之三十”,这样用户才能理解。我在规则引擎里加了原因字段,每个判断节点都记录推理路径,最后生成解释文本。
实测下来,常见疾病的预核保准确率在百分之九十以上,用户满意度比纯人工核保高不少,因为智能体可以二十四小时在线,而且追问过程比人工更耐心。
4.2 产品对比:多维度打分与可视化呈现
产品对比是另一个高频需求。用户通常会说“我想对比一下A产品和B产品”,但直接列条款对比用户看不懂。我的做法是先提取对比维度,然后按用户关注点加权打分,最后用表格和图表呈现。
对比维度包括:保障范围、保费、等待期、免赔额、赔付比例、续保条件、增值服务。每个维度从知识库提取数据,然后归一化打分。权重根据用户需求动态调整。比如用户说“我最看重保费”,那保费维度权重就调高。
def compare_products(product_a, product_b, user_weights): dimensions = ['coverage', 'premium', 'waiting_period', 'deductible', 'reimbursement_ratio', 'renewal'] scores = {} for dim in dimensions: score_a = normalize(product_a[dim], dim) score_b = normalize(product_b[dim], dim) weight = user_weights.get(dim, 1.0) scores[dim] = { 'product_a': score_a * weight, 'product_b': score_b * weight, 'winner': 'A' if score_a > score_b else 'B' } return scores呈现方式上,我用表格展示各维度对比,用雷达图展示综合得分。用户一眼就能看出哪款产品更符合自己的需求。这里有个细节:对比结果不能只说“A更好”,要说明“在您关注的保费维度上,A比B低百分之十五,但等待期长三十天”。这样用户才能做权衡。
实操心得:产品对比功能上线后,我发现用户最喜欢的是“一键对比”按钮。用户不需要手动输入产品名称,系统根据之前的对话自动推荐对比对象。这个功能让对比功能的使用率提升了三倍。
5. 常见问题与排查技巧实录
开发过程中遇到的问题五花八门,我挑几个典型的整理成速查表,方便遇到类似情况时快速定位。
5.1 知识检索不准确怎么办
这是最常见的问题。用户问“等待期生病能赔吗”,系统检索出来的却是“等待期定义”而不是“等待期出险处理”。排查思路如下:
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 检索结果不相关 | 嵌入模型区分度不够 | 人工评估Top10结果 | 微调嵌入模型 |
| 相关条款排在后位 | 向量权重未分层 | 检查条款权重配置 | 核心条款提高权重 |
| 检索结果缺上下文 | 切片粒度太细 | 检查切片大小 | 按条款完整切片 |
| 多义词导致偏差 | 未做词义消歧 | 分析查询词 | 增加同义词映射 |
我遇到最典型的一次是用户问“猝死能赔吗”,系统检索出来的是“意外身故”条款。原因是“猝死”在医学上属于疾病范畴,但用户可能认为它是意外。后来我在知识库里加了“猝死”的专门条目,明确说明它通常不属于意外险保障范围,但部分产品有特别约定。这个问题才解决。
5.2 多轮对话中槽位丢失
用户在第一轮说了“预算五千”,第三轮系统却问“您的预算是多少”。这种问题通常是状态管理出了bug。排查步骤:先看会话状态对象是否在每轮正确更新,再看槽位继承逻辑是否有覆盖问题,最后检查是否有并发请求导致状态错乱。
我的解决方案是给会话状态加版本号,每次更新递增。如果发现版本号跳跃,说明有并发写入,需要加锁。另外槽位继承我改成了“显式覆盖”模式,只有用户明确说了新值才覆盖,否则保留旧值。
5.3 核保规则冲突
不同疾病之间的核保规则可能冲突。比如用户既有高血压又有糖尿病,单独看每个疾病都符合标准体,但组合起来风险倍增,应该加费。我一开始没处理组合规则,导致核保结论偏松。
后来我在规则引擎里加了组合规则层。先跑单病种规则,如果都是标准体,再跑组合规则。组合规则是一个矩阵,行和列都是疾病,交叉点是组合核保结论。这个矩阵由核保专家维护,我负责把它变成可执行代码。
5.4 回复内容合规风险
保险智能体最怕的是说了不该说的话。比如用户问“这个产品收益怎么样”,如果系统回答“收益很高”,就涉嫌误导。我的做法是建三层过滤:第一层是敏感词过滤,第二层是语义合规检查,第三层是人工抽检。
敏感词过滤比较简单,维护一个词库就行。语义合规检查用一个小模型判断回复是否包含承诺性表述。人工抽检每天随机抽一百条对话,由合规同事审核。三层过滤下来,合规风险基本可控。
注意:合规过滤不能只做一次。我见过有的系统只在生成时过滤,但用户追问时又绕过了。正确的做法是在每个输出节点都加过滤,确保任何返回给用户的内容都经过检查。
6. 上线后的效果与后续优化方向
系统上线三个月,日均处理咨询两千三百条,独立解决率百分之七十八,用户满意度四点六分(满分五分)。核保辅助模块的预核保准确率百分之九十一,产品对比功能使用率百分之六十三。这些数据比我预期的好,但也有很多可以优化的地方。
目前最大的瓶颈是复杂案例的处理。比如用户有多个既往症,或者涉及多个产品的组合配置,系统还是需要转人工。下一步我打算引入更强的推理能力,把图数据库的查询能力用得更充分,让系统能处理更复杂的规则链。
另一个优化方向是个性化。现在的推荐逻辑还是基于规则和通用模型,没有充分利用用户的历史行为数据。如果能把用户之前的咨询记录、浏览记录、投保记录都纳入进来,推荐会更精准。但这涉及数据隐私和合规问题,需要谨慎设计。
最后分享一个小技巧:智能体的对话日志是非常宝贵的资产。我每周会抽半天时间看用户和智能体的真实对话,把失败的案例整理成测试集,用来迭代模型和规则。这个习惯坚持了三个月,系统的解决率从最初的百分之五十二提升到了百分之七十八。很多时候,优化方向就藏在那些失败的对话里。