【AI全栈后端12-03】Spring Boot 用多模型路由把智能客服成本降下来

本文是「Spring Boot + AI 全栈后端」系列第 03 篇。第 02 篇跑通了 AI 对话接口,这一篇解决上线后最现实的问题:账单。示例基于 Spring AI 2.0 / Boot 4.1。

让财务皱眉的场景

真是巧了,给做客服系统的团队做方案评审,几乎每家都踩过同一个坑:接口接上了查物流、问营业时间、退换货政策,本以为省了人力,月底账单却傻了眼------占比 80% 以上的「几点下班」「快递到哪了」这类简单问题,和真正的投诉、理赔走的是同一张账单,因为代码里写死了一个最贵的旗舰模型。它按 token 计费、单价是轻量档好几倍,简单问题「高射炮打蚊子」,钱就这么悄悄烧没了。

问题很具体:不是模型不行,是没分档------问题全喂给最贵的。

解决思路就一条:Spring AI 把模型抽象成 ChatModel,一套接口挂多个实例(便宜轻量 + 强旗舰),再写路由策略挑档。业务只认抽象,简单问题走轻量档、复杂风险才上旗舰,成本立降。V哥 的建议是:这一步别等账单爆了才做,接口一上线就把路由留出来。

先算一笔账,看看能省多少

别凭感觉,先拿数字说话。假设日均 1 万条客服问答,八成是简单问题,单条输入输出合计约 800 token,旗舰档单价约为轻量档的 6 倍(各家的公开单价不一样,你按自己的签约价代入即可):

方案 旗舰调用 轻量调用 相对成本
全部走旗舰 10 000 条 0 条 100%
全部走轻量 0 条 10 000 条 约 17%
按复杂度路由 2 000 条 8 000 条 约 33%

结论很清楚:路由方案用两成的旗舰调用,拿到了接近全轻量的成本,还保住了复杂问题的回答质量。全切轻量倒是便宜,但客户来投诉理赔的时候给它一个敷衍的答案,省下的钱还不够赔一次客诉。

这张表还有个隐藏价值:它是你和领导谈预算时的抓手------省钱要能被算出来,方案才立得住。

第一步:把"分档"抽象成策略

别把 if (question.contains("退款")) 散落在业务代码里。先抽象出"档位"和"策略"两个概念:

java 复制代码
public enum ModelTier {
    LITE,
    FLAGSHIP
}

public interface RoutingStrategy {
    ModelTier route(String question);
}

最朴素的关键词命中:高风险词走旗舰,其余轻量:

java 复制代码
@Component
public class KeywordRoutingStrategy implements RoutingStrategy {

    private static final Set<String> COMPLEX_MARKERS = Set.of(
            "退款", "投诉", "纠纷", "赔偿", "违约金", "起诉", "仲裁");

    @Override
    public ModelTier route(String question) {
        if (question == null || question.isBlank()) {
            return ModelTier.LITE;
        }
        for (String marker : COMPLEX_MARKERS) {
            if (question.contains(marker)) {
                return ModelTier.FLAGSHIP;
            }
        }
        return ModelTier.LITE;
    }
}

三种路由策略怎么选

关键词只是起点。V哥 把常见做法列成一张表,你按团队阶段挑:

策略 怎么判断 优点 代价 适合阶段
关键词/规则 命中高风险词、超长文本、带附件就上旗舰 零成本、可解释、出问题能立刻定位 覆盖不全,新词得人工补 刚上线,先把八成的钱省下来
规则 + 打分 关键词、长度、用户等级各占权重,过阈值上旗舰 比纯规则平滑,误判少 阈值要调,得有人盯数据 跑了一两个月,有历史数据
小模型分类器 先让轻量模型判断"这个问题难不难",再决定档位 覆盖率高,能识别没见过的问题 多一次调用,多一份延迟和成本 量大、且前两种已榨干收益

无论选哪种,RoutingStrategy 这个接口都不用改------换策略是换实现类,不是改业务代码 。这就是为什么第一步要先抽象:今天用关键词,明天换分类器,CustomerRouterService 一个字都不用动。

第二步:按档选模型,顺手把降级和计数做了

服务用 @Qualifier 注入两档 ChatModel,先选档再调用。这里多了两件上线必备的事------降级 和计数:

java 复制代码
@Service
public class CustomerRouterService {

    private final ChatModel flagship;
    private final ChatModel lite;
    private final RoutingStrategy strategy;

    /** 分档调用计数:最小可观测实现,生产里换成 Micrometer 埋点或落库报表。 */
    private final Map<ModelTier, AtomicLong> usage = new EnumMap<>(ModelTier.class);

    public CustomerRouterService(
            @Qualifier("flagship") ChatModel flagship,
            @Qualifier("lite") ChatModel lite,
            RoutingStrategy strategy) {
        this.flagship = flagship;
        this.lite = lite;
        this.strategy = strategy;
    }

    public CsReply answer(String question) {
        ModelTier tier = strategy.route(question);
        boolean degraded = false;
        String text;

        try {
            text = call(tier == ModelTier.FLAGSHIP ? flagship : lite, question);
        } catch (RuntimeException ex) {
            if (tier != ModelTier.FLAGSHIP) {
                // 轻量档都挂了,说明模型服务整体不可用,交给全局异常返回 503
                throw ex;
            }
            tier = ModelTier.LITE;
            degraded = true;
            text = call(lite, question);
        }

        usage.computeIfAbsent(tier, k -> new AtomicLong()).incrementAndGet();
        return new CsReply(question, text, tier.name(), degraded);
    }

    /** 某个档位累计被调用了多少次:省没省钱,得有数才算数。 */
    public long usageOf(ModelTier tier) {
        AtomicLong counter = usage.get(tier);
        return counter == null ? 0L : counter.get();
    }

    private String call(ChatModel model, String question) {
        return model.call(new Prompt(new UserMessage(question)))
                .getResult()
                .getOutput()
                .getText();
    }
}

为什么要降级

路由之后有个新问题:旗舰档比轻量档慢、也更容易被限流。一个来投诉的用户,本来就憋着火,再让他等三十秒最后弹个 503,这单客诉基本就定了。

上面的 catch 干的事很朴素------旗舰档失败就退到轻量档,宁可答得简单点,也别让用户干等。同时把 degraded 标成 true 带回响应,方便你单独统计和告警:降级次数突然涨了,说明旗舰档或它的上游出问题了,这是要人盯的信号,不是可以默默吞掉的异常。

注意那个 if (tier != ModelTier.FLAGSHIP) throw ex;:轻量档自己都挂了,说明是模型服务整体不可用,这时降级没有意义,直接抛出去让全局异常处理返回 503 才是正解。

为什么要计数

tier 和 usageOf() 加起来,就是成本可观测的最小闭环。有了它,你能回答三个此前答不上来的问题:

  1. 这个月旗舰档被调用了多少次?占比多少?
  2. 昨天调了关键词表之后,旗舰占比降了没有?
  3. 降级发生了几次?集中在哪个时段?

V哥 见过太多团队"上了路由就以为省钱了",问省了多少,答不上来。没有埋点的优化,等于没优化------因为你既证明不了收益,也发现不了回退。

第三步:接上接口

控制器只收请求做校验:

java 复制代码
@RestController
@RequestMapping("/api/cs")
public class CsController {

    private final CustomerRouterService routerService;

    public CsController(CustomerRouterService routerService) {
        this.routerService = routerService;
    }

    @PostMapping
    public CsReply ask(@RequestBody @Valid CsAskRequest request) {
        return routerService.answer(request.question());
    }
}

public record CsAskRequest(@NotBlank(message = "问题不能为空") String question) {}
public record CsReply(String question, String answer, String tier, boolean degraded) {}

CsReply 里的两个字段各有分工:tier 告诉前端"这次是轻量档答的"(前端可选择性标注),degraded 是给运维看的告警信号。V哥 建议这两个字段不要藏着,直接进日志和监控------它们是这套路由系统唯一的自我证明。

第四步:配置里挂两档模型

真正的「分档」在配置:用同一 OpenAI 客户端包出两档 ChatModel,靠 app.chat.provider=openai 开关,测试默认不加载:

java 复制代码
@Configuration
@ConditionalOnProperty(name = "app.chat.provider", havingValue = "openai")
public class ChatModelsConfig {

    @Bean
    @Qualifier("flagship")
    public ChatModel flagshipModel(OpenAIClient openAIClient) {
        OpenAiChatOptions options = OpenAiChatOptions.builder().model("gpt-4o").build();
        return OpenAiChatModel.builder().openAiClient(openAIClient).options(options).build();
    }

    @Bean
    @Qualifier("lite")
    public ChatModel liteModel(OpenAIClient openAIClient) {
        OpenAiChatOptions options = OpenAiChatOptions.builder().model("gpt-4o-mini").build();
        return OpenAiChatModel.builder().openAiClient(openAIClient).options(options).build();
    }
}

配置与代码分离,正是「配置切换不动业务」:轻量档明天换国产模型,只改这个类。 你的 CustomerRouterService、CsController、RoutingStrategy 全都不知情,也不需要知情。

怎么验证

路由逻辑不需要真去调模型,用两个桩模型(轻量/旗舰,回复里带不同标记)就能验证:问「几点下班」→ 命中 tier = LITE;问带退款、投诉、赔偿的问题 → 命中 tier = FLAGSHIP;问题为空 → 400 校验拦截。

再给旗舰桩加一点"脾气"------让它在遇到特定问题时抛异常,就能验证降级链路:原本该走旗舰的投诉问题,在旗舰失败后应该返回 tier = LITE、degraded = true,且答案来自轻量档。最后连问几次简单问题,断言 usageOf(LITE) 正好递增了对应次数。整个过程不需要真实密钥,也不依赖网络。

落地要点

  • 策略可替换:关键词只是入门,线上用小模型做复杂度分类或按意图、长度加权即可,接口不变。
  • 默认便宜档 :拿不准就默认 LITE,别让每句话都烧旗舰的钱。
  • 降级要留痕 :degraded 进日志、进监控,别让降级悄悄发生。
  • 成本要出报表 :usageOf() 的计数定期落到报表里,不然"省了多少"永远是一句口头承诺。
  • 关键词表外置 :把 COMPLEX_MARKERS 挪到配置中心,运营同学发现新类型的客诉时能自己加词,不用走一次发版。
  • V哥 的提醒:别一上来就搞「智能路由」,先用关键词把八成的省钱拿到手,再用数据决定要不要上分类器------省钱这件事,见效快的方案先落地。

最后一句:别再让每句「在吗」都烧旗舰模型的钱------接上多模型路由,简单问题交给便宜档,复杂问题留给旗舰,失败还能自动降级,成本这道坎几行配置就跨过去。下一篇(04)V哥 带你解决另一种「看得见却用不了」的尴尬:让模型按你定义的字段输出,把简历解析结果直接落库。

相关推荐
云计算-Security1 小时前
AI 赋能运维:UniRack 主机纳管平台的架构与实践
运维·人工智能·架构
霸道流氓气质1 小时前
AI模型幻觉检测与抑制完全指南:从规则引擎到RAG对比的Java生产级实战
java·开发语言·人工智能
weixin_404551241 小时前
使用 ZCode 改造 PPT 模板:实践复盘与能力边界
人工智能·powerpoint
陈希瑞1 小时前
LongCat-2.5-Preview 的web逆向能力实测
人工智能·算法·wasm
曲鸟1 小时前
体验完鸿蒙AI后的几点感受
人工智能·华为·harmonyos
niucloud-admin1 小时前
JAVA V6 多商户商城 开发文档——手机端前端
java·开发语言·前端
新知图书1 小时前
6.3 美食烹饪
人工智能·提示词·美食·提示词工程
IT古董1 小时前
《FDE前沿部署工程师实战教程》27 - Enterprise AI Gateway实战:统一路由、限流、降级与成本控制
人工智能
云安全助手2 小时前
自建接入VS聚合平台:企业 AI 调用的选型思路与迁移成本拆解
java·大数据·数据库·人工智能·ai大模型