面向中高级 Java 开发者、架构师和保险科技团队的一篇 AI 工程化实战指南。本文围绕寿险核心系统中的需求预审、规则拆解、智能核保、证据追溯和合规审计,系统讲解如何使用 Spring Boot 3、Spring AI、Kafka、Redis、Milvus、Neo4j、Kubernetes、Istio 构建一套可解释、可扩展、可审计的生产级 AI 需求工程平台。
一、为什么寿险 AI 不能只做“黑盒推荐”?
很多保险 AI 项目在 Demo 阶段都非常亮眼:上传体检报告,模型能提取血压、血糖、甲状腺结节等指标;输入一句“客户有乙肝小三阳,能不能买定寿”,模型能给出接近业务专家的解释;把产品条款扔给大模型,几秒钟就能总结出投保年龄、等待期、除外责任。
但进入寿险核心系统后,问题会立刻变得复杂。
寿险不是普通推荐场景。电商推荐错了,用户可能少买一件商品;寿险核保错了,可能带来逆选择风险、理赔争议、监管问责和长期准备金压力。需求工程也不是普通文本总结,业务提出的一个“新增甲状腺结节人群定寿产品”需求,背后会牵动产品规则、核保策略、费率模型、投保流程、保全、理赔、合规披露和渠道销售话术。
因此,寿险核心系统中的 AI 不能只回答“推荐什么”,还必须回答:
- 为什么这样判断?
- 使用了哪条规则、哪份条款、哪段健康告知?
- 这条规则当前是否有效,版本是多少?
- 相似历史需求是否出现过缺陷或合规问题?
- 如果模型结论错误,如何复盘、回滚、追责和优化?
这就是本文的核心观点:
寿险 AI 的目标不是替代专家拍板,而是把专家经验、业务规则和模型推理组织成一条可解释、可追溯、可审计的决策链。
二、寿险核心系统 AI 需求工程的真实场景
本文讨论的“AI 需求工程”不是泛泛的智能问答,而是把 AI 接入寿险核心研发链路,帮助业务、产品、架构、开发、测试、合规共同完成需求从提出到上线的闭环。
一个典型需求如下:
业务希望上线一款面向 25-45 岁人群的定期寿险产品。对于甲状腺结节客户,如果超声分级 TI-RADS 不超过 3 类、结节直径不超过 1cm、近一年复查稳定,则可以标准体承保;如果 TI-RADS 4A 或结节增长明显,则转人工核保;如果存在恶性病理或术后不足一年,则延期或拒保。
传统模式下,这段需求会经历多轮人工拆解:
- 产品经理整理条款和核保策略。
- 需求分析师拆成字段、流程、接口和规则。
- 架构师评估对核心系统、核保引擎、影像资料系统的影响。
- 开发人员把自然语言改写成规则引擎配置或代码。
- 测试人员补充边界用例。
- 合规人员检查销售误导、健康告知和除外责任披露。
AI 的价值在于把这些环节从“串行人工传话”升级为“证据驱动协同”:
- 需求预审:检索历史相似需求、缺陷和监管风险,生成可行性报告。
- 规则拆解:将自然语言规则拆成结构化条件、动作和冲突检查。
- 智能核保:结合健康告知、医学知识、产品规则进行可解释推理。
- 测试生成:自动生成等价类、边界值、冲突规则和回归用例。
- 审计追溯:保存每次 AI 参与决策的 Prompt、模型版本、规则版本、检索证据和人工确认记录。
从架构角度看,这不是“接一个大模型 API”,而是一套复杂的 AI Native 需求工程平台。
三、架构目标:生产级 AI 系统必须满足的 8 个要求
在寿险核心场景中,AI 平台要进入生产,至少要满足以下要求:
| 目标 |
说明 |
| 可解释 |
每个结论必须能追溯到条款、规则、病例、图谱关系或人工确认 |
| 可审计 |
记录输入、输出、Prompt 版本、模型版本、证据链、操作者和时间 |
| 可控 |
LLM 只能给出建议,硬约束必须由规则引擎、策略引擎或人工复核控制 |
| 高并发 |
支持活动高峰、渠道集中投保、批量需求分析和批量核保 |
| 可扩展 |
按领域拆分服务,支持多产品线、多模型、多租户、多渠道接入 |
| 高可用 |
模型服务、向量库、图数据库、规则库、消息队列都要有降级策略 |
| 一致性 |
规则变更、缓存刷新、向量索引、图谱同步需要有版本和事件闭环 |
| 可运营 |
有指标、日志、链路追踪、质量看板、人工反馈和模型评测机制 |
这 8 个要求决定了整体设计原则:
LLM 负责理解和生成,规则引擎负责硬约束,知识中台负责证据,事件总线负责状态流转,审计系统负责可信闭环。
四、总体架构:从单点 AI 能力到云原生 AI 需求工程平台
完整架构如下:
4.1 为什么要增加 AI 编排层?
很多团队会把 LLM 调用直接写在业务服务里:
String answer = chatClient.prompt().user(prompt).call().content();
这在 Demo 阶段很快,但生产环境会带来严重问题:
- Prompt 散落在代码中,无法版本化和灰度。
- 模型切换困难,无法按场景动态路由。
- 没有统一脱敏、限流、熔断和输出校验。
- 无法统一记录 token、耗时、命中证据和错误原因。
- 不同服务重复实现 RAG、缓存、重试和降级逻辑。
因此,需要独立的 AI Orchestrator。它不是一个简单代理,而是 AI 能力的“控制平面”:
- 模型路由:简单抽取用小模型,复杂推理用强模型,敏感场景走私有化模型。
- Prompt 管理:模板版本化、参数化、灰度发布、回滚。
- 工具编排:向量检索、图谱查询、规则校验、保单查询、医学编码查询。
- 安全护栏:PII 脱敏、越权检测、输出 JSON Schema 校验、敏感建议拦截。
- 成本治理:token 预算、语义缓存、批量推理、超时降级。
4.2 领域服务如何划分?
推荐按业务能力拆分,而不是按技术组件拆分:
| 服务 |
核心职责 |
数据所有权 |
| 需求预审服务 |
相似需求检索、影响范围分析、合规风险提示 |
需求单、评审报告 |
| 规则拆解服务 |
自然语言规则结构化、冲突检测、规则版本管理 |
规则草案、规则版本 |
| 智能核保服务 |
健康告知理解、规则匹配、核保建议 |
核保申请、决策结果 |
| 测试生成服务 |
等价类、边界值、回归用例、规则覆盖率 |
测试用例、覆盖报告 |
| 审计追溯服务 |
证据链、模型调用、人工确认、复盘报告 |
审计事件、证据快照 |
| 人工反馈服务 |
专家复核、标注、纠错、模型评测样本 |
反馈样本、质检结论 |
这样设计的好处是服务边界稳定,后续即使更换模型、向量库或规则引擎,也不会冲击核心业务边界。
五、核心原理:GraphRAG 如何让核保决策可解释?
普通 RAG 的流程是“问题向量化 -> 检索文档片段 -> 拼 Prompt -> 让模型回答”。它适合知识问答,但不完全适合寿险核保。
核保不是只找相似文本,而是要理解实体关系:
- 甲状腺结节属于什么疾病分类?
- TI-RADS 3 类和 4A 类风险等级不同在哪里?
- 该疾病在定寿、重疾险、医疗险中的规则是否一致?
- 当前客户是否存在家族史、手术史、复查稳定性等影响因子?
- 如果多项疾病共存,是否存在规则冲突或风险叠加?
GraphRAG 的核心是把“语义相似性”和“结构化关系推理”结合起来。
GraphRAG 不应该让 LLM 自由发挥。生产级做法是把上下文分成三类:
- 强约束上下文:规则引擎执行结果,LLM 不能推翻。
- 证据上下文:条款、病例、图谱关系,LLM 必须引用。
- 解释上下文:面向不同角色生成不同粒度的说明。
示例输出应类似下面这样:
{
"decision": "MANUAL_REVIEW",
"riskLevel": "MEDIUM",
"confidence": 0.87,
"summary": "客户存在甲状腺结节,TI-RADS 4A,按当前定寿核保规则需要转人工核保。",
"evidences": [
{
"sourceType": "RULE_ENGINE",
"sourceId": "RULE-THYROID-TERM-2026-003",
"quote": "TI-RADS 4A 或结节增长明显,转人工核保。",
"version": 12
},
{
"sourceType": "HEALTH_DECLARATION",
"sourceId": "APP-20260423-0001",
"quote": "甲状腺右叶结节,TI-RADS 4A,建议随访。",
"version": 1
}
],
"nextActions": [
"补充近一年甲状腺超声复查报告",
"确认是否存在穿刺病理或手术史"
]
}
六、领域建模:把“自然语言需求”变成可执行资产
生产级 AI 需求工程平台的关键不是生成一段好看的文本,而是把自然语言转成可治理的领域对象。
6.1 核心领域对象
public enum DecisionAction {
STANDARD,
RATING,
EXCLUSION,
POSTPONE,
DECLINE,
MANUAL_REVIEW
}
public enum EvidenceSourceType {
REQUIREMENT_TEXT,
POLICY_CLAUSE,
UNDERWRITING_RULE,
HEALTH_DECLARATION,
MEDICAL_KNOWLEDGE,
GRAPH_RELATION,
RULE_ENGINE,
HUMAN_REVIEW,
LLM_REASONING
}
public record Evidence(
String evidenceId,
EvidenceSourceType sourceType,
String sourceId,
String sourceVersion,
String content,
double confidence,
Map<String, Object> metadata
) {
}
public record UnderwritingDecision(
String decisionId,
String applicationId,
String productCode,
DecisionAction action,
String reason,
double confidence,
List<Evidence> evidences,
Instant decidedAt
) {
public boolean requiresHumanReview() {
return action == DecisionAction.MANUAL_REVIEW
|| action == DecisionAction.DECLINE
|| confidence < 0.85;
}
}
6.2 规则对象不能只有 if-else
寿险规则需要版本、适用产品、适用渠道、生效期、证据来源和审批状态。
public record UnderwritingRule(
String ruleId,
String ruleName,
String productCode,
String diseaseCode,
int version,
RuleStatus status,
LocalDate effectiveFrom,
LocalDate effectiveTo,
List<RuleCondition> conditions,
RuleAction action,
List<Evidence> evidences
) {
public boolean isEffective(LocalDate date) {
boolean afterStart = !date.isBefore(effectiveFrom);
boolean beforeEnd = effectiveTo == null || !date.isAfter(effectiveTo);
return status == RuleStatus.PUBLISHED && afterStart && beforeEnd;
}
}
public record RuleCondition(
String field,
String operator,
String value,
String unit
) {
}
public record RuleAction(
DecisionAction action,
Map<String, Object> params
) {
}
public enum RuleStatus {
DRAFT,
REVIEWING,
PUBLISHED,
DISABLED
}
这类领域对象是后续规则校验、缓存同步、审计追溯和测试生成的基础。
七、生产级代码实现:AI 编排服务
下面给出一个更接近生产的 AI Orchestrator 示例。它封装了模型调用、Prompt 版本、超时控制、重试、JSON Schema 校验和审计埋点。
@Service
public class AiOrchestrator {
private final ChatClient chatClient;
private final PromptTemplateRepository promptTemplateRepository;
private final ObjectMapper objectMapper;
private final MeterRegistry meterRegistry;
private final AuditTraceService auditTraceService;
public AiOrchestrator(
ChatClient.Builder chatClientBuilder,
PromptTemplateRepository promptTemplateRepository,
ObjectMapper objectMapper,
MeterRegistry meterRegistry,
AuditTraceService auditTraceService
) {
this.chatClient = chatClientBuilder.build();
this.promptTemplateRepository = promptTemplateRepository;
this.objectMapper = objectMapper;
this.meterRegistry = meterRegistry;
this.auditTraceService = auditTraceService;
}
public <T> T callJson(AiTaskRequest request, Class<T> responseType) {
PromptTemplateVersion template = promptTemplateRepository
.findPublished(request.promptCode())
.orElseThrow(() -> new IllegalStateException("Prompt not found: " + request.promptCode()));
String prompt = template.render(request.variables());
long started = System.nanoTime();
try {
String content = chatClient.prompt()
.system(template.systemPrompt())
.user(prompt)
.options(ChatOptions.builder()
.model(request.model())
.temperature(request.temperature())
.maxTokens(request.maxTokens())
.build())
.call()
.content();
T result = parseStrictJson(content, responseType);
auditTraceService.recordModelCall(ModelCallTrace.success(
request.traceId(),
request.promptCode(),
template.version(),
request.model(),
prompt,
content,
elapsedMillis(started)
));
meterRegistry.counter("ai_call_total",
"prompt", request.promptCode(),
"model", request.model(),
"result", "success").increment();
return result;
} catch (Exception ex) {
auditTraceService.recordModelCall(ModelCallTrace.failure(
request.traceId(),
request.promptCode(),
template.version(),
request.model(),
prompt,
ex.getMessage(),
elapsedMillis(started)
));
meterRegistry.counter("ai_call_total",
"prompt", request.promptCode(),
"model", request.model(),
"result", "failure").increment();
throw new AiCallException("AI call failed, traceId=" + request.traceId(), ex);
}
}
private <T> T parseStrictJson(String content, Class<T> responseType) throws IOException {
String json = JsonExtractor.extractFirstJsonObject(content)
.orElseThrow(() -> new IllegalArgumentException("LLM response is not valid JSON"));
return objectMapper.readValue(json, responseType);
}
private long elapsedMillis(long started) {
return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - started);
}
}
public record AiTaskRequest(
String traceId,
String promptCode,
String model,
double temperature,
int maxTokens,
Map<String, Object> variables
) {
}
生产环境建议再叠加 Resilience4j:
@Configuration
public class AiResilienceConfig {
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> aiCircuitBreakerCustomizer() {
return factory -> factory.configure(builder -> builder
.timeLimiterConfig(TimeLimiterConfig.custom()
.timeoutDuration(Duration.ofSeconds(8))
.build())
.circuitBreakerConfig(CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.slowCallRateThreshold(60)
.slowCallDurationThreshold(Duration.ofSeconds(5))
.minimumNumberOfCalls(30)
.waitDurationInOpenState(Duration.ofSeconds(30))
.permittedNumberOfCallsInHalfOpenState(5)
.build()),
"ai-model");
}
}
八、规则拆解:LLM 生成草案,规则引擎做裁判
自然语言规则拆解最容易踩的坑是让 LLM 直接“决定规则”。正确做法是:
- LLM 只负责抽取候选规则草案。
- JSON Schema 做格式校验。
- 规则引擎做硬约束校验。
- 规则冲突检测服务做版本和适用范围校验。
- 高风险规则进入人工审批。
8.1 规则拆解服务
@Service
public class RuleExtractionApplicationService {
private final AiOrchestrator aiOrchestrator;
private final RuleValidator ruleValidator;
private final RuleConflictDetector conflictDetector;
private final RuleDraftRepository ruleDraftRepository;
private final DomainEventPublisher eventPublisher;
@Transactional
public RuleExtractionResult extract(RequirementDocument requirement) {
String traceId = UUID.randomUUID().toString();
RuleDraftResponse response = aiOrchestrator.callJson(
new AiTaskRequest(
traceId,
"underwriting-rule-extract-v3",
"qwen-plus",
0.1,
4096,
Map.of(
"requirementText", requirement.content(),
"productCode", requirement.productCode(),
"domainGlossary", loadInsuranceGlossary()
)
),
RuleDraftResponse.class
);
List<RuleDraft> drafts = response.rules().stream()
.map(rule -> RuleDraft.fromAi(requirement.requirementId(), rule, response.evidences()))
.toList();
ValidationReport validationReport = ruleValidator.validate(drafts);
ConflictReport conflictReport = conflictDetector.detect(drafts);
RuleExtractionResult result = RuleExtractionResult.of(
traceId,
drafts,
validationReport,
conflictReport
);
ruleDraftRepository.saveAll(drafts);
eventPublisher.publish(new RuleDraftExtractedEvent(traceId, requirement.requirementId()));
return result;
}
private List<String> loadInsuranceGlossary() {
return List.of("标准体", "加费", "延期", "拒保", "除外责任", "TI-RADS", "既往症");
}
}
8.2 LLM 输出对象
public record RuleDraftResponse(
List<AiRuleDraft> rules,
List<Evidence> evidences,
double confidence,
List<String> assumptions,
List<String> questionsForHuman
) {
}
public record AiRuleDraft(
String ruleName,
String productCode,
String diseaseCode,
List<RuleCondition> conditions,
RuleAction action,
String evidenceId,
double confidence
) {
}
8.3 Drools 校验示例
@Component
public class DroolsRuleValidator implements RuleValidator {
private final KieContainer kieContainer;
@Override
public ValidationReport validate(List<RuleDraft> drafts) {
KieSession session = kieContainer.newKieSession("rule-validation-session");
ValidationReport report = new ValidationReport();
try {
session.setGlobal("report", report);
drafts.forEach(session::insert);
session.fireAllRules();
return report;
} finally {
session.dispose();
}
}
}
package com.insurance.rules.validation
import com.insurance.ai.rule.RuleDraft
import com.insurance.ai.rule.RuleCondition
import com.insurance.ai.rule.ValidationReport
global ValidationReport report
rule "Age range must be valid"
when
$draft: RuleDraft()
$condition: RuleCondition(field == "applicantAge", operator == "BETWEEN") from $draft.conditions()
eval(Integer.parseInt($condition.value().split(",")[0]) < 0
|| Integer.parseInt($condition.value().split(",")[1]) > 120)
then
report.addError($draft.ruleId(), "投保年龄范围必须在 0-120 岁之间");
end
rule "High risk action must require evidence"
when
$draft: RuleDraft(action().action().name() in ("DECLINE", "POSTPONE"))
eval($draft.evidences() == null || $draft.evidences().isEmpty())
then
report.addError($draft.ruleId(), "拒保或延期规则必须绑定明确证据来源");
end
8.4 规则冲突检测
冲突不是语法错误,而是业务风险。例如同一产品、同一疾病、同一生效期内,一条规则说“标准体”,另一条规则说“拒保”。
@Service
public class RuleConflictDetector {
private final UnderwritingRuleRepository ruleRepository;
public ConflictReport detect(List<RuleDraft> drafts) {
ConflictReport report = new ConflictReport();
for (RuleDraft draft : drafts) {
List<UnderwritingRule> existingRules = ruleRepository.findEffectiveRules(
draft.productCode(),
draft.diseaseCode(),
draft.effectiveFrom(),
draft.effectiveTo()
);
for (UnderwritingRule existing : existingRules) {
if (overlap(draft.conditions(), existing.conditions())
&& draft.action().action() != existing.action().action()) {
report.addConflict(new RuleConflict(
draft.ruleId(),
existing.ruleId(),
"同一适用范围内存在不同核保动作"
));
}
}
}
return report;
}
private boolean overlap(List<RuleCondition> left, List<RuleCondition> right) {
Map<String, RuleCondition> rightByField = right.stream()
.collect(Collectors.toMap(RuleCondition::field, Function.identity(), (a, b) -> a));
return left.stream().allMatch(condition -> {
RuleCondition other = rightByField.get(condition.field());
return other == null || ConditionRange.overlaps(condition, other);
});
}
}
九、智能核保:高并发下的可解释决策链
智能核保服务建议采用“快路径 + 慢路径”架构。
快路径处理明确、低风险、高频请求:
- 命中缓存。
- 命中确定性规则。
- 不调用或少调用大模型。
- P99 控制在 300-800ms。
慢路径处理复杂、高风险、低置信度请求:
- GraphRAG 检索。
- LLM 解释生成。
- 人工复核。
- P99 可以放宽到 3-8s,但必须异步化。
9.1 核保应用服务
@Service
public class UnderwritingApplicationService {
private final SemanticDecisionCache semanticDecisionCache;
private final DeterministicRuleEngine deterministicRuleEngine;
private final GraphRagRetriever graphRagRetriever;
private final AiOrchestrator aiOrchestrator;
private final AuditTraceService auditTraceService;
private final DomainEventPublisher eventPublisher;
public UnderwritingDecision underwrite(UnderwritingCommand command) {
String traceId = UUID.randomUUID().toString();
Optional<UnderwritingDecision> cached = semanticDecisionCache.findSimilar(command);
if (cached.isPresent() && cached.get().confidence() >= 0.98) {
UnderwritingDecision decision = cached.get();
auditTraceService.recordCacheHit(traceId, command.applicationId(), decision.decisionId());
return decision;
}
RuleExecutionResult ruleResult = deterministicRuleEngine.execute(command);
if (ruleResult.isFinalDecision()) {
UnderwritingDecision decision = ruleResult.toDecision(traceId);
auditTraceService.recordRuleDecision(traceId, command, ruleResult);
eventPublisher.publish(new UnderwritingDecidedEvent(decision.decisionId(), command.applicationId()));
return decision;
}
GraphRagContext context = graphRagRetriever.retrieve(command);
AiUnderwritingResponse response = aiOrchestrator.callJson(
new AiTaskRequest(
traceId,
"underwriting-decision-explain-v5",
"qwen-max",
0.0,
4096,
Map.of(
"application", command,
"ruleResult", ruleResult,
"graphContext", context
)
),
AiUnderwritingResponse.class
);
UnderwritingDecision decision = response.toDecision(command.applicationId(), traceId);
auditTraceService.recordDecisionTrace(traceId, command, context, ruleResult, response);
if (decision.requiresHumanReview()) {
eventPublisher.publish(new HumanReviewRequiredEvent(decision.decisionId(), command.applicationId()));
} else {
semanticDecisionCache.save(command, decision);
eventPublisher.publish(new UnderwritingDecidedEvent(decision.decisionId(), command.applicationId()));
}
return decision;
}
}
9.2 语义缓存:不是简单 key-value 缓存
核保请求中,用户表达可能不同,但语义相同:
- “甲状腺结节 0.8cm,TI-RADS 3 类,去年复查没变化”
- “右叶结节 8mm,超声 3 类,12 个月稳定”
普通 Redis key 无法命中,需要基于向量相似度做语义缓存。但保险场景不能只看相似度,还要看产品、规则版本和关键字段。
@Service
public class SemanticDecisionCache {
private static final double HIGH_CONFIDENCE_THRESHOLD = 0.98;
private final EmbeddingModel embeddingModel;
private final VectorStore vectorStore;
private final RedisTemplate<String, String> redisTemplate;
private final ObjectMapper objectMapper;
public Optional<UnderwritingDecision> findSimilar(UnderwritingCommand command) {
String normalizedText = normalize(command);
List<Double> embedding = embeddingModel.embed(normalizedText);
SearchRequest request = SearchRequest.builder()
.query(normalizedText)
.topK(3)
.similarityThreshold(HIGH_CONFIDENCE_THRESHOLD)
.filterExpression("""
productCode == '%s' &&
ruleVersion == '%s' &&
diseaseCode == '%s'
""".formatted(command.productCode(), command.ruleVersion(), command.mainDiseaseCode()))
.build();
List<Document> docs = vectorStore.similaritySearch(request);
if (docs == null || docs.isEmpty()) {
return Optional.empty();
}
String cacheKey = docs.get(0).getMetadata().get("cacheKey").toString();
String json = redisTemplate.opsForValue().get(cacheKey);
if (json == null) {
return Optional.empty();
}
try {
return Optional.of(objectMapper.readValue(json, UnderwritingDecision.class));
} catch (JsonProcessingException ex) {
return Optional.empty();
}
}
public void save(UnderwritingCommand command, UnderwritingDecision decision) {
String cacheKey = "uw:decision:" + command.productCode() + ":" + decision.decisionId();
String normalizedText = normalize(command);
try {
redisTemplate.opsForValue().set(
cacheKey,
objectMapper.writeValueAsString(decision),
Duration.ofHours(6)
);
Document document = new Document(normalizedText, Map.of(
"cacheKey", cacheKey,
"productCode", command.productCode(),
"ruleVersion", command.ruleVersion(),
"diseaseCode", command.mainDiseaseCode()
));
vectorStore.add(List.of(document));
} catch (JsonProcessingException ex) {
throw new CacheWriteException("Failed to cache underwriting decision", ex);
}
}
private String normalize(UnderwritingCommand command) {
return String.join("\n",
command.productCode(),
command.mainDiseaseCode(),
command.healthDeclarationText().replaceAll("\\s+", " ").trim()
);
}
}
9.3 并发控制:限流、舱壁、排队和降级
高并发下最容易被打爆的不是 Java 服务,而是模型服务、向量检索和图数据库。建议四层保护:
- 网关限流:按渠道、租户、接口、用户等级限流。
- 服务舱壁:核保、需求预审、批量任务使用独立线程池。
- 模型配额:按模型、Prompt、业务优先级控制并发。
- 异步排队:低优先级批量请求进入 Kafka,不阻塞在线核保。
@Configuration
public class UnderwritingExecutorConfig {
@Bean
public ThreadPoolTaskExecutor onlineUnderwritingExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(64);
executor.setMaxPoolSize(128);
executor.setQueueCapacity(500);
executor.setThreadNamePrefix("uw-online-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
executor.initialize();
return executor;
}
@Bean
public ThreadPoolTaskExecutor batchUnderwritingExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(16);
executor.setMaxPoolSize(32);
executor.setQueueCapacity(5000);
executor.setThreadNamePrefix("uw-batch-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
@RestController
@RequestMapping("/api/underwriting")
public class UnderwritingController {
private final RateLimiterRegistry rateLimiterRegistry;
private final UnderwritingApplicationService underwritingService;
@PostMapping("/decision")
public ResponseEntity<UnderwritingDecision> decide(@RequestBody UnderwritingCommand command) {
RateLimiter rateLimiter = rateLimiterRegistry.rateLimiter("online-underwriting");
Supplier<UnderwritingDecision> supplier = RateLimiter
.decorateSupplier(rateLimiter, () -> underwritingService.underwrite(command));
try {
return ResponseEntity.ok(supplier.get());
} catch (RequestNotPermitted ex) {
return ResponseEntity.status(HttpStatus.TOO_MANY_REQUESTS).build();
}
}
}
十、事件驱动与一致性:Outbox + Kafka + 幂等消费
规则发布、向量索引、图谱同步、缓存刷新、审计记录不应该放在一个大事务里。推荐使用 Outbox Pattern:
- 业务数据和 outbox 事件写入同一个本地事务。
- 后台发布器扫描 outbox 表发送 Kafka。
- 消费者按事件版本幂等处理。
- 失败进入重试队列或死信队列。
10.1 Outbox 表
CREATE TABLE ai_outbox_event (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
event_id VARCHAR(64) NOT NULL UNIQUE,
aggregate_type VARCHAR(64) NOT NULL,
aggregate_id VARCHAR(64) NOT NULL,
event_type VARCHAR(128) NOT NULL,
payload JSON NOT NULL,
status VARCHAR(32) NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
next_retry_at DATETIME NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
KEY idx_status_retry (status, next_retry_at),
KEY idx_aggregate (aggregate_type, aggregate_id)
);
10.2 本地事务发布事件
@Service
public class RulePublishService {
private final UnderwritingRuleRepository ruleRepository;
private final OutboxEventRepository outboxEventRepository;
@Transactional
public void publishRule(String ruleId, String operator) {
UnderwritingRule rule = ruleRepository.findByIdForUpdate(ruleId)
.orElseThrow(() -> new IllegalArgumentException("Rule not found: " + ruleId));
UnderwritingRule published = rule.publish(operator);
ruleRepository.save(published);
OutboxEvent event = OutboxEvent.create(
"UnderwritingRule",
published.ruleId(),
"RulePublished",
Map.of(
"ruleId", published.ruleId(),
"version", published.version(),
"productCode", published.productCode(),
"diseaseCode", published.diseaseCode()
)
);
outboxEventRepository.save(event);
}
}
10.3 Outbox 发布器
@Component
public class OutboxKafkaPublisher {
private final OutboxEventRepository repository;
private final KafkaTemplate<String, String> kafkaTemplate;
private final ObjectMapper objectMapper;
@Scheduled(fixedDelayString = "${app.outbox.publish-delay-ms:1000}")
public void publishPendingEvents() {
List<OutboxEvent> events = repository.lockNextBatch(100);
for (OutboxEvent event : events) {
try {
String topic = topicOf(event.eventType());
String payload = objectMapper.writeValueAsString(event.payload());
kafkaTemplate.send(topic, event.aggregateId(), payload).get(3, TimeUnit.SECONDS);
repository.markPublished(event.eventId());
} catch (Exception ex) {
repository.markFailed(event.eventId(), ex.getMessage());
}
}
}
private String topicOf(String eventType) {
return switch (eventType) {
case "RulePublished" -> "insurance.rule.published";
case "UnderwritingDecided" -> "insurance.underwriting.decided";
default -> "insurance.ai.events";
};
}
}
10.4 消费端幂等
@Component
public class RulePublishedConsumer {
private final RedisTemplate<String, String> redisTemplate;
private final RuleIndexService ruleIndexService;
@KafkaListener(topics = "insurance.rule.published", groupId = "rule-indexer")
public void consume(ConsumerRecord<String, RulePublishedPayload> record, Acknowledgment ack) {
RulePublishedPayload payload = record.value();
String idempotentKey = "event:consumed:rule-indexer:" + payload.eventId();
Boolean firstSeen = redisTemplate.opsForValue()
.setIfAbsent(idempotentKey, "1", Duration.ofDays(7));
if (Boolean.FALSE.equals(firstSeen)) {
ack.acknowledge();
return;
}
try {
ruleIndexService.rebuildRuleIndex(payload.ruleId(), payload.version());
ack.acknowledge();
} catch (Exception ex) {
redisTemplate.delete(idempotentKey);
throw ex;
}
}
}
Kafka 推荐配置:
spring:
kafka:
producer:
acks: all
retries: 10
compression-type: zstd
properties:
enable.idempotence: true
max.in.flight.requests.per.connection: 5
delivery.timeout.ms: 120000
consumer:
enable-auto-commit: false
auto-offset-reset: earliest
properties:
isolation.level: read_committed
max.poll.interval.ms: 300000
listener:
ack-mode: manual
concurrency: 6
十一、知识中台:文档、向量、图谱和规则的统一治理
知识中台不是“把文档切片塞进向量库”这么简单。寿险知识有版本、时效、来源、适用范围和审批状态。
11.1 文档入库流水线
(原文此处内容缺失,已按上下文保持结构一致)
11.2 分块策略
保险文档不能简单按固定 token 切分,建议采用层级分块:
- 文档级:产品条款、核保手册、监管文件、理赔案例。
- 章节级:投保规则、保险责任、责任免除、健康告知。
- 条款级:保留原始条款编号。
- 语义级:针对长条款再做滑动窗口。
每个 chunk 必须带 metadata:
{
"docId": "CLAUSE-TERM-2026-A",
"docVersion": "2026.04.01",
"productCode": "TERM_LIFE_A",
"chapter": "健康告知",
"clauseNo": "3.2.1",
"effectiveFrom": "2026-04-01",
"effectiveTo": null,
"approvalStatus": "PUBLISHED",
"securityLevel": "INTERNAL"
}
11.3 图谱关系
Neo4j 中建议至少维护这些节点和关系:
CREATE CONSTRAINT disease_code_unique IF NOT EXISTS
FOR (d:Disease) REQUIRE d.code IS UNIQUE;
CREATE CONSTRAINT rule_id_unique IF NOT EXISTS
FOR (r:Rule) REQUIRE r.ruleId IS UNIQUE;
CREATE CONSTRAINT product_code_unique IF NOT EXISTS
FOR (p:Product) REQUIRE p.productCode IS UNIQUE;
MERGE (d:Disease {code: "E04.1"})
SET d.name = "甲状腺结节", d.system = "ICD-10", d.riskLevel = "MEDIUM"
MERGE (p:Product {productCode: "TERM_LIFE_A"})
SET p.name = "优选定期寿险 A 款"
MERGE (r:Rule {ruleId: "RULE-THYROID-TERM-003"})
SET r.version = 12,
r.action = "MANUAL_REVIEW",
r.status = "PUBLISHED"
MERGE (p)-[:HAS_RULE]->(r)
MERGE (d)-[:CONTROLLED_BY {factor: "TI-RADS"}]->(r);
查询某个疾病在某个产品下的规则子图:
MATCH (d:Disease {code: $diseaseCode})-[rel:CONTROLLED_BY]->(r:Rule)<-[:HAS_RULE]-(p:Product {productCode: $productCode})
WHERE r.status = "PUBLISHED"
RETURN d, rel, r, p
ORDER BY r.version DESC
LIMIT 20;
十二、可观测性:AI 系统要看业务质量,不只看 QPS
传统服务关注 QPS、RT、错误率;AI 服务还必须关注回答质量和证据质量。
12.1 指标体系
| 指标 |
说明 |
| ai_call_total |
模型调用次数,按模型、Prompt、业务场景打标签 |
| ai_call_latency_seconds |
模型调用耗时 |
| ai_token_usage_total |
token 使用量和成本 |
| rag_recall_hit_ratio |
RAG 命中文档是否被最终引用 |
| decision_human_review_ratio |
人工复核比例 |
| decision_override_ratio |
人工推翻 AI 建议比例 |
| evidence_missing_total |
输出结论缺少证据次数 |
| rule_conflict_total |
规则冲突数量 |
| cache_hit_ratio |
语义缓存命中率 |
12.2 Micrometer 埋点
@Component
public class AiMetrics {
private final MeterRegistry registry;
public AiMetrics(MeterRegistry registry) {
this.registry = registry;
}
public Timer.Sample start() {
return Timer.start(registry);
}
public void recordAiCall(Timer.Sample sample, String model, String promptCode, String result) {
sample.stop(Timer.builder("ai_call_latency")
.tag("model", model)
.tag("prompt", promptCode)
.tag("result", result)
.publishPercentileHistogram()
.register(registry));
registry.counter("ai_call_total",
"model", model,
"prompt", promptCode,
"result", result).increment();
}
public void recordDecision(String action, boolean humanReviewRequired) {
registry.counter("underwriting_decision_total",
"action", action,
"humanReviewRequired", String.valueOf(humanReviewRequired)).increment();
}
}
12.3 审计追溯表
CREATE TABLE ai_decision_trace (
trace_id VARCHAR(64) PRIMARY KEY,
business_type VARCHAR(64) NOT NULL,
business_id VARCHAR(64) NOT NULL,
model_name VARCHAR(128) NOT NULL,
prompt_code VARCHAR(128) NOT NULL,
prompt_version INT NOT NULL,
input_hash VARCHAR(128) NOT NULL,
output_json JSON NOT NULL,
confidence DECIMAL(5,4) NOT NULL,
latency_ms BIGINT NOT NULL,
created_by VARCHAR(64) NOT NULL,
created_at DATETIME NOT NULL,
KEY idx_business (business_type, business_id),
KEY idx_prompt (prompt_code, prompt_version),
KEY idx_created_at (created_at)
);
CREATE TABLE ai_evidence_trace (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
trace_id VARCHAR(64) NOT NULL,
source_type VARCHAR(64) NOT NULL,
source_id VARCHAR(128) NOT NULL,
source_version VARCHAR(64) NOT NULL,
content_snapshot TEXT NOT NULL,
confidence DECIMAL(5,4) NOT NULL,
created_at DATETIME NOT NULL,
KEY idx_trace_id (trace_id)
);
十三、Kubernetes 部署:弹性、灰度和安全
13.1 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: underwriting-ai-service
namespace: insurance-core
spec:
replicas: 4
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app: underwriting-ai-service
template:
metadata:
labels:
app: underwriting-ai-service
version: v1
annotations:
prometheus.io/scrape: "true"
prometheus.io/path: "/actuator/prometheus"
prometheus.io/port: "8080"
spec:
serviceAccountName: underwriting-ai-sa
terminationGracePeriodSeconds: 60
containers:
- name: app
image: harbor.example.com/insurance/underwriting-ai-service:1.0.0
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: http
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75 -XX:+UseG1GC -XX:MaxGCPauseMillis=200"
resources:
requests:
cpu: "1000m"
memory: "2Gi"
limits:
cpu: "4000m"
memory: "4Gi"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: 8080
initialDelaySeconds: 20
periodSeconds: 5
timeoutSeconds: 2
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: 8080
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 3
lifecycle:
preStransform: translateY(
exec:
command: ["sh", "-c", "sleep 20"]
注意:原文中 preStransform: translateY( 可能为笔误,但按规则保留原始代码内容。
13.2 HPA
AI 服务的扩缩容不能只看 CPU,还应结合请求延迟和队列堆积。基础版本可以先用 CPU + 内存:
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: underwriting-ai-service-hpa
namespace: insurance-core
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: underwriting-ai-service
minReplicas: 4
maxReplicas: 30
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 65
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 75
更成熟的做法是接入 KEDA,按 Kafka lag 扩缩批处理消费者。
13.3 Istio 灰度发布
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: underwriting-ai-service
namespace: insurance-core
spec:
hosts:
- underwriting-ai-service
http:
- route:
- destination:
host: underwriting-ai-service
subset: v1
weight: 90
- destination:
host: underwriting-ai-service
subset: v2
weight: 10
Prompt 和模型也要灰度。服务版本灰度只解决代码变更,Prompt 变更同样可能引发生产事故。
十四、安全与合规:保险 AI 的底线能力
寿险系统中常见敏感数据包括姓名、身份证号、手机号、病历、体检报告、保单号、银行卡号。AI 平台必须默认最小化暴露。
14.1 输入脱敏
@Component
public class PiiMasker {
private static final Pattern ID_CARD = Pattern.compile("\\b\\d{6}(18|19|20)\\d{2}\\d{2}\\d{2}\\d{3}[0-9Xx]\\b");
private static final Pattern PHONE = Pattern.compile("\\b1[3-9]\\d{9}\\b");
public MaskResult mask(String text) {
Map<String, String> mappings = new LinkedHashMap<>();
String masked = replace(text, ID_CARD, "ID_CARD", mappings);
masked = replace(masked, PHONE, "PHONE", mappings);
return new MaskResult(masked, mappings);
}
private String replace(String text, Pattern pattern, String type, Map<String, String> mappings) {
Matcher matcher = pattern.matcher(text);
StringBuffer buffer = new StringBuffer();
int index = 0;
while (matcher.find()) {
String token = "{{" + type + "_" + index++ + "}}";
mappings.put(token, matcher.group());
matcher.appendReplacement(buffer, token);
}
matcher.appendTail(buffer);
return buffer.toString();
}
}
public record MaskResult(String maskedText, Map<String, String> mappings) {
}
14.2 输出护栏
输出必须经过结构化校验和业务校验:
- 不允许输出“保证承保”“一定赔付”等绝对化表述。
- 不允许越权给出医学诊断。
- 不允许绕过人工复核要求。
- 不允许缺少证据的拒保、延期、除外建议。
@Component
public class UnderwritingOutputGuardrail {
public GuardrailResult validate(AiUnderwritingResponse response) {
List<String> errors = new ArrayList<>();
if (response.evidences() == null || response.evidences().isEmpty()) {
errors.add("核保结论必须包含证据链");
}
if (response.action() == DecisionAction.DECLINE
&& response.evidences().stream().noneMatch(e -> e.sourceType() == EvidenceSourceType.UNDERWRITING_RULE)) {
errors.add("拒保建议必须引用已发布核保规则");
}
if (containsAbsolutePromise(response.reason())) {
errors.add("输出包含绝对化承诺表述");
}
return errors.isEmpty() ? GuardrailResult.pass() : GuardrailResult.block(errors);
}
private boolean containsAbsolutePromise(String text) {
return text.contains("保证承保") || text.contains("一定赔付") || text.contains("绝对可以");
}
}
十五、测试策略:AI 系统也要可回归
AI 系统测试不能只测接口通不通,还要测模型输出是否稳定、证据是否正确、规则是否被覆盖。
15.1 测试分层
| 测试类型 |
目标 |
| 单元测试 |
规则解析、条件交集、幂等、脱敏、输出校验 |
| 集成测试 |
Kafka、Redis、数据库、向量检索、图谱查询 |
| Golden Set |
固定问题集,验证模型输出和证据引用 |
| 对抗测试 |
模糊表达、诱导越权、缺失材料、冲突信息 |
| 回归测试 |
Prompt、模型、规则、知识库版本升级后的差异 |
| 压测 |
在线核保 P99、批量任务吞吐、Kafka lag、缓存命中率 |
15.2 Golden Set 示例
@SpringBootTest
class UnderwritingGoldenSetTest {
@Autowired
private UnderwritingApplicationService underwritingService;
@Test
void thyroidNoduleTiRads3ShouldBeStandardWhenStable() {
UnderwritingCommand command = new UnderwritingCommand(
"APP-GOLDEN-001",
"TERM_LIFE_A",
"12",
"E04.1",
"客户甲状腺结节 0.8cm,TI-RADS 3 类,近一年复查稳定,无手术史。"
);
UnderwritingDecision decision = underwritingService.underwrite(command);
assertThat(decision.action()).isEqualTo(DecisionAction.STANDARD);
assertThat(decision.confidence()).isGreaterThanOrEqualTo(0.85);
assertThat(decision.evidences()).anyMatch(e -> e.sourceType() == EvidenceSourceType.UNDERWRITING_RULE);
}
@Test
void thyroidNoduleTiRads4aShouldRequireManualReview() {
UnderwritingCommand command = new UnderwritingCommand(
"APP-GOLDEN-002",
"TERM_LIFE_A",
"12",
"E04.1",
"客户甲状腺右叶结节,TI-RADS 4A,医生建议进一步检查。"
);
UnderwritingDecision decision = underwritingService.underwrite(command);
assertThat(decision.action()).isEqualTo(DecisionAction.MANUAL_REVIEW);
assertThat(decision.requiresHumanReview()).isTrue();
}
}
15.3 压测目标
生产压测建议按路径拆分:
| 场景 |
目标 |
| 命中确定性规则 |
P99 < 500ms |
| 命中语义缓存 |
P99 < 800ms |
| GraphRAG + LLM 在线决策 |
P99 < 8s |
| 批量规则拆解 |
1000 条需求/小时 |
| Kafka 消费堆积恢复 |
30 分钟内消化 100 万事件 |
十六、实际案例:甲状腺结节定寿需求从提出到上线
16.1 业务输入
业务提交需求:
针对 25-45 岁定寿客户,甲状腺结节直径不超过 1cm,TI-RADS 不超过 3 类,近一年复查稳定,可标准体承保;TI-RADS 4A 或结节增长明显,转人工核保;恶性病理、甲状腺癌术后不足一年,延期。
16.2 AI 需求预审输出
平台生成预审报告:
## 需求预审结论
可行,但需要补充 3 个业务约束:
1. “复查稳定”的判定周期建议明确为最近 12 个月内至少 2 次超声结果无明显增长。
2. “结节增长明显”建议定义为最大径增长超过 20% 或增长超过 2mm。
3. 甲状腺癌术后不足一年延期,一年以上是否可人工核保需要补充规则。
## 相似历史需求
- REQ-2025-0812:甲状腺结节重疾险核保规则调整,曾因 TI-RADS 分级缺失导致人工复核率上升。
- BUG-2025-1130:健康告知中“结节 8mm”未换算为 0.8cm,导致规则未命中。
## 影响范围
- 投保健康告知字段:新增 TI-RADS 分级、结节最大径、复查日期。
- 核保规则中心:新增 3 条健康规则。
- 测试用例:至少覆盖 18 个边界组合。
16.3 规则拆解结果
{
"rules": [
{
"ruleName": "甲状腺结节标准体规则",
"conditions": [
{"field": "applicantAge", "operator": "BETWEEN", "value": "25,45", "unit": "YEAR"},
{"field": "diseaseCode", "operator": "EQ", "value": "E04.1", "unit": ""},
{"field": "noduleDiameter", "operator": "LTE", "value": "1.0", "unit": "CM"},
{"field": "tirads", "operator": "LTE", "value": "3", "unit": "LEVEL"},
{"field": "stableMonths", "operator": "GTE", "value": "12", "unit": "MONTH"}
],
"action": {"action": "STANDARD", "params": {}},
"confidence": 0.91
},
{
"ruleName": "甲状腺结节人工核保规则",
"conditions": [
{"field": "tirads", "operator": "EQ", "value": "4A", "unit": "LEVEL"}
],
"action": {"action": "MANUAL_REVIEW", "params": {"reason": "TI-RADS 4A"}}
},
{
"ruleName": "甲状腺恶性病理延期规则",
"conditions": [
{"field": "pathology", "operator": "EQ", "value": "MALIGNANT", "unit": ""},
{"field": "postSurgeryMonths", "operator": "LT", "value": "12", "unit": "MONTH"}
],
"action": {"action": "POSTPONE", "params": {"months": 12}}
}
]
}
16.4 自动生成测试用例
| 用例 |
年龄 |
直径 |
TI-RADS |
稳定月数 |
期望 |
| TC001 |
30 |
0.8cm |
3 |
12 |
标准体 |
| TC002 |
30 |
1.1cm |
3 |
12 |
人工核保 |
| TC003 |
30 |
1.0cm |
4A |
12 |
人工核保 |
| TC004 |
46 |
0.8cm |
3 |
12 |
不适用该规则 |
| TC005 |
30 |
0.8cm |
3 |
6 |
人工核保 |
| TC006 |
30 |
0.8cm |
3 |
12,恶性术后 6 个月 |
延期 |
16.5 上线闭环
上线流程建议如下:
- 规则草案由 AI 生成。
- 规则引擎和冲突检测自动校验。
- 核保专家审批。
- 合规人员确认话术和解释边界。
- Golden Set 回归通过。
- 灰度 10% 渠道流量。
- 观察人工推翻率、人工复核率、投诉率、拒保争议。
- 无异常后全量发布。
十七、从 MVP 到企业级平台的演进路线
阶段一:单体 MVP,验证业务闭环
适合 2-4 周内完成:
- Spring Boot 单体应用。
- MySQL + Redis。
- 使用本地文件或简单向量库。
- 支持需求预审、规则拆解、证据展示。
- LLM 调用可先 Mock,重点验证流程。
阶段二:服务化拆分,沉淀领域能力
适合 1-2 个月:
- 拆分需求预审、规则拆解、智能核保、审计追溯服务。
- 引入 Kafka 和 Outbox。
- 引入 Milvus、Neo4j。
- 建立 Prompt 版本管理和 Golden Set。
阶段三:云原生生产化
适合 2-4 个月:
- Kubernetes 部署。
- Istio 灰度、熔断、mTLS。
- Prometheus + Grafana + Tempo 可观测。
- HPA/KEDA 弹性扩缩容。
- 建立模型质量看板和人工反馈闭环。
阶段四:多 Agent 协作
当基础能力稳定后,可以引入多 Agent:
- 需求分析 Agent:负责需求理解、影响范围和缺口问题。
- 核保规则 Agent:负责规则拆解、冲突检测和测试建议。
- 合规 Agent:负责监管红线、销售话术和审计要求。
- 测试 Agent:负责用例生成、回归执行和覆盖率分析。
- 编排 Agent:根据流程状态调度不同 Agent。
注意,多 Agent 不是越多越好。Agent 增多后,系统复杂度会显著上升,必须有明确的状态机、权限边界、任务队列和审计链路。
十八、常见坑与架构建议
18.1 不要让 LLM 直接修改生产规则
LLM 可以生成规则草案,但不能直接发布规则。生产规则必须经过校验、审批、版本化和回滚设计。
18.2 不要只存最终答案
只存答案等于放弃审计。至少保存:
- 原始输入 hash。
- 脱敏后输入。
- Prompt code 和版本。
- 模型名称和参数。
- 检索证据和引用证据。
- 输出 JSON。
- 人工复核记录。
18.3 不要迷信向量检索
向量检索适合语义召回,不适合精确约束。产品代码、规则版本、生效期、疾病编码、渠道权限必须走结构化过滤。
18.4 不要忽视人工反馈
保险 AI 的长期价值来自反馈闭环。人工推翻 AI 建议的样本,是最重要的优化数据。
18.5 不要把批量任务和在线请求混在一起
批量需求分析、批量规则拆解、历史病例重建索引都应该异步化,并与在线核保隔离线程池、队列和资源配额。
十九、总结
寿险核心系统 AI 需求工程的本质,是把自然语言需求、保险条款、核保规则、医学知识、历史案例和专家反馈,组织成一套可解释、可执行、可审计的工程体系。
这类系统不能停留在“黑盒推荐”。生产级架构必须做到:
- 用 GraphRAG 连接语义检索和图谱推理。
- 用规则引擎守住硬约束。
- 用 AI Orchestrator 统一模型、Prompt、工具和安全护栏。
- 用 Kafka + Outbox 保证异步扩展和最终一致性。
- 用审计追溯保存证据链。
- 用 Golden Set、人工反馈和可观测指标持续优化质量。
- 用 Kubernetes、Istio、HPA/KEDA 支撑高并发和弹性扩展。
真正能落地的保险 AI,不是一个会聊天的模型,而是一套围绕业务风险、工程稳定性和合规审计构建的云原生决策系统。
当 AI 从实验室走向寿险核心工作流,架构师要做的不是追逐最炫的模型能力,而是回答一个更实际的问题:
每一次 AI 建议,是否都有证据、规则、版本、责任人和可复盘路径?
只有答案是肯定的,AI 才能从“黑盒推荐”走向“可解释决策”。
如果你正在设计类似系统,欢迎到 云栈社区 讨论更多云原生架构与 AI 落地的实践经验。