找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

3157

积分

0

好友

423

主题
发表于 1 小时前 | 查看: 2| 回复: 0

面向中高级 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 直接“决定规则”。正确做法是:

  1. LLM 只负责抽取候选规则草案。
  2. JSON Schema 做格式校验。
  3. 规则引擎做硬约束校验。
  4. 规则冲突检测服务做版本和适用范围校验。
  5. 高风险规则进入人工审批。

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:

  1. 业务数据和 outbox 事件写入同一个本地事务。
  2. 后台发布器扫描 outbox 表发送 Kafka。
  3. 消费者按事件版本幂等处理。
  4. 失败进入重试队列或死信队列。

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 上线闭环

上线流程建议如下:

  1. 规则草案由 AI 生成。
  2. 规则引擎和冲突检测自动校验。
  3. 核保专家审批。
  4. 合规人员确认话术和解释边界。
  5. Golden Set 回归通过。
  6. 灰度 10% 渠道流量。
  7. 观察人工推翻率、人工复核率、投诉率、拒保争议。
  8. 无异常后全量发布。

十七、从 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 落地的实践经验。




上一篇:Fireworks-Tech-Graph:说句话就生成高清架构图的开源利器
下一篇:Hermes HUD:AI Agent黑盒可视化仪表盘,一键拆解运行与纠错
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-4-26 03:41 , Processed in 0.843986 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表