JDK21 + Spring AI 2.0 入场指南:2026 年 Java 开发者 AI 架构全景图

Spring AI 生产级实战系列 第 01 篇

技术栈:JDK 21 + Spring Boot 4.1 + Spring AI 2.0 GA

代码模块:com-common

阅读时间:约 25 分钟


第一部分:痛点场景

1.1 一个时代的折叠

2026 年,AI 已经从"概念验证"阶段全面进入"生产落地"阶段。大模型能力持续突破,企业级 AI 应用的门槛正在快速降低。但一个尴尬的事实是:大量 Java 后端开发者仍然停留在"会调 API"的表层认知上

根据 2026 年 Q1 的开发者调研数据,超过 90% 的 Java 后端开发者对 Spring AI 的认知仅停留在"知道有这个框架"或"写过简单 Demo"的层面。真正在生产环境中使用 Spring AI 构建过完整 AI 应用的开发者,不足 8%。

这意味着,绝大多数 Java 开发者面对 AI 浪潮时,处于一种"看得见岸、游不过去"的焦虑状态。

1.2 Spring AI 2.0 GA 的意义

2026 年 5 月 28 日,Spring AI 2.0 正式发布 GA 版本。这是 Spring 生态在 AI 领域的里程碑事件:

  • ChatClient API 全面稳定,提供 Fluent 风格的链式调用
  • Advisors API 成熟,支持请求/响应拦截、日志记录、安全过滤
  • Tool Calling 从 Function Calling 演进,原生支持工具声明与调用
  • MCP 协议 集成,打通模型上下文协议生态
  • 多模型适配 内置 OpenAI、Anthropic、Ollama、通义千问等主流模型支持

Spring AI 2.0 GA 的核心价值在于:Java 开发者终于有了一个 Spring 原生的、生产可用的 AI 应用开发框架。但问题随之而来------框架有了,架构怎么搭?生产怎么部署?合规怎么过?

1.3 SME 场景的特殊困境

中小企业(SME)Java 团队面临的困境更为突出:

  • 团队规模小,通常 3-8 人,没有专职 AI 工程师
  • 预算敏感,无法承受试错成本
  • 技术栈统一要求高,希望复用现有 Spring 生态
  • 合规要求不减配,等保三级、数据安全一样不能少
  • 上线时间紧迫,通常要求 1-2 周内完成 AI 功能集成

1.4 生产事故复盘:RestTemplate 拼接 JSON 的代价

事故背景 :2026 年 3 月,某中小企业 SaaS 平台紧急上线 AI 客服功能。开发团队为赶进度,直接使用 RestTemplate 拼接 JSON 字符串调用大模型 API。

代码问题

  • 未设置连接超时和读取超时,RestTemplate 默认无超时限制
  • 未做线程池隔离,AI 调用与业务接口共用 HTTP 线程池
  • 未做异常降级,大模型 API 异常时直接向上抛出
  • 未做限流保护,突发流量直接打到模型 API

事故经过:上线第二天,大模型 API 服务商出现波动,响应延迟从正常 2 秒飙升至 30 秒以上。大量请求线程被阻塞等待响应,默认 200 个线程的 HTTP 线程池在 5 分钟内完全耗尽。所有接口(包括非 AI 功能)全部超时无响应,前端持续报 502 错误。

影响范围:运维团队耗时 4 小时定位问题并临时扩容,期间平台完全不可用,直接影响 3000+ 企业客户正常使用,造成直接经济损失约 15 万元。

根因总结:会调 API 不等于会做 AI 工程。缺少超时控制、线程隔离、异常降级、限流保护四大生产级保障,是这场事故的直接原因。

1.5 SME 场景的核心诉求拆解

通过对 50+ 家中小企业 Java 团队的调研,我们提炼出 SME 场景的五大核心诉求:

诉求一:快速验证。SME 团队需要在一周内完成从技术选型到功能上线的全流程。这意味着框架的学习曲线必须足够平缓,最好能复用团队已有的 Spring 技术栈知识。Spring AI 2.0 在这一点上具有天然优势------它的 API 设计完全遵循 Spring 生态的约定,开发者无需学习全新的编程范式。

诉求二:成本可控。SME 预算通常按月审批,无法承受不可预测的费用增长。Token 费用是 AI 应用最大的变动成本,必须建立预算控制机制。本系列第 02 篇将专门解决 Token 费用暴涨问题。

诉求三:安全合规不减配。即使是中小企业,如果涉及用户数据处理,同样需要满足等保三级要求。数据加密、审计日志、操作溯源这些安全能力不能因为是 SME 就打折扣。本文方案三专门为合规场景设计。

诉求四:模型可切换。SME 团队不希望被单一模型提供商锁定。当某个模型涨价、降质或不可用时,能够快速切换到备用模型,是业务连续性的基本保障。本文方案二的多模型适配层正是为此设计。

诉求五:运维简单。SME 通常没有专职运维人员,AI 应用的运维必须足够简单。日志清晰、链路可追踪、故障可定位、监控有告警------这四个能力缺一不可。


第二部分:根因分析

2.1 为什么 Java 开发者难以入门 AI 工程

#mermaid-svg-q7N2LTucNvTnKTmp{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-q7N2LTucNvTnKTmp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-q7N2LTucNvTnKTmp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-q7N2LTucNvTnKTmp .error-icon{fill:#552222;}#mermaid-svg-q7N2LTucNvTnKTmp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-q7N2LTucNvTnKTmp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-q7N2LTucNvTnKTmp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-q7N2LTucNvTnKTmp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-q7N2LTucNvTnKTmp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-q7N2LTucNvTnKTmp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-q7N2LTucNvTnKTmp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-q7N2LTucNvTnKTmp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-q7N2LTucNvTnKTmp .marker.cross{stroke:#333333;}#mermaid-svg-q7N2LTucNvTnKTmp svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-q7N2LTucNvTnKTmp p{margin:0;}#mermaid-svg-q7N2LTucNvTnKTmp .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-q7N2LTucNvTnKTmp .cluster-label text{fill:#333;}#mermaid-svg-q7N2LTucNvTnKTmp .cluster-label span{color:#333;}#mermaid-svg-q7N2LTucNvTnKTmp .cluster-label span p{background-color:transparent;}#mermaid-svg-q7N2LTucNvTnKTmp .label text,#mermaid-svg-q7N2LTucNvTnKTmp span{fill:#333;color:#333;}#mermaid-svg-q7N2LTucNvTnKTmp .node rect,#mermaid-svg-q7N2LTucNvTnKTmp .node circle,#mermaid-svg-q7N2LTucNvTnKTmp .node ellipse,#mermaid-svg-q7N2LTucNvTnKTmp .node polygon,#mermaid-svg-q7N2LTucNvTnKTmp .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-q7N2LTucNvTnKTmp .rough-node .label text,#mermaid-svg-q7N2LTucNvTnKTmp .node .label text,#mermaid-svg-q7N2LTucNvTnKTmp .image-shape .label,#mermaid-svg-q7N2LTucNvTnKTmp .icon-shape .label{text-anchor:middle;}#mermaid-svg-q7N2LTucNvTnKTmp .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-q7N2LTucNvTnKTmp .rough-node .label,#mermaid-svg-q7N2LTucNvTnKTmp .node .label,#mermaid-svg-q7N2LTucNvTnKTmp .image-shape .label,#mermaid-svg-q7N2LTucNvTnKTmp .icon-shape .label{text-align:center;}#mermaid-svg-q7N2LTucNvTnKTmp .node.clickable{cursor:pointer;}#mermaid-svg-q7N2LTucNvTnKTmp .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-q7N2LTucNvTnKTmp .arrowheadPath{fill:#333333;}#mermaid-svg-q7N2LTucNvTnKTmp .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-q7N2LTucNvTnKTmp .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-q7N2LTucNvTnKTmp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q7N2LTucNvTnKTmp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-q7N2LTucNvTnKTmp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q7N2LTucNvTnKTmp .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-q7N2LTucNvTnKTmp .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-q7N2LTucNvTnKTmp .cluster text{fill:#333;}#mermaid-svg-q7N2LTucNvTnKTmp .cluster span{color:#333;}#mermaid-svg-q7N2LTucNvTnKTmp div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-q7N2LTucNvTnKTmp .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-q7N2LTucNvTnKTmp rect.text{fill:none;stroke-width:0;}#mermaid-svg-q7N2LTucNvTnKTmp .icon-shape,#mermaid-svg-q7N2LTucNvTnKTmp .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-q7N2LTucNvTnKTmp .icon-shape p,#mermaid-svg-q7N2LTucNvTnKTmp .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-q7N2LTucNvTnKTmp .icon-shape .label rect,#mermaid-svg-q7N2LTucNvTnKTmp .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-q7N2LTucNvTnKTmp .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-q7N2LTucNvTnKTmp .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-q7N2LTucNvTnKTmp :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Java开发者

面对AI浪潮
三大根因
框架选择困难
架构设计空白
生产意识缺失
LangChain4j?
Spring AI?
自行封装?
单体or微服务?
密钥怎么管?
模型怎么切?
超时/重试/降级
成本控制
审计合规
选择困难症

延迟决策
架构空白

无法落地
生产隐患

事故频发
停留在表层调用

2.2 三大根因拆解

根因一:不知道用什么框架

Java 生态中 AI 应用开发框架主要有三个选项:LangChain4j、Spring AI、Quarkus LangChain4j。开发者往往陷入对比泥潭,迟迟无法决策。实际上,对于 Spring 技术栈团队,Spring AI 2.0 是唯一合理选择------它与 Spring Boot 深度集成,复用现有 Spring 生态知识,学习曲线最平缓。

根因二:不知道怎么搭架构

即使选定了框架,开发者仍然不知道如何组织代码结构。Controller 怎么分层?Service 怎么设计?密钥怎么管理?多模型怎么切换?这些问题没有标准答案,导致每个团队都在重复造轮子。

根因三:不知道生产环境要注意什么

开发环境跑通的 Demo,到了生产环境往往问题频发。超时控制、线程隔离、异常降级、限流保护、审计日志、数据脱敏、密钥加密------这些生产级保障在 Demo 阶段完全不需要,但在生产环境中缺一不可。

2.3 市场竞品对比

对比维度 Spring AI 2.0 LangChain4j Quarkus LangChain4j
生态归属 Spring 官方 独立社区 Quarkus 生态
Spring 集成度 原生深度集成 需手动适配 仅限 Quarkus
ChatClient API Fluent 链式调用 链式调用 链式调用
Advisors 机制 原生支持 无直接对应 无直接对应
Tool Calling 原生 @Tool 注解 @Tool 注解 @Tool 注解
MCP 协议 原生支持 不支持 不支持
多模型适配 内置 Starter 需手动配置 需手动配置
学习曲线 低(Spring 开发者) 中(需学 Quarkus)
生产成熟度 GA 稳定版 GA 稳定版 GA 稳定版
社区活跃度 高(Spring 生态) 中高
SME 推荐度 强烈推荐 推荐 不推荐

结论:对于使用 Spring 技术栈的 Java 团队,Spring AI 2.0 是唯一最优选择。本系列全部 30 篇文章均基于 Spring AI 2.0 展开。

2.4 从认知到行动的鸿沟

即使开发者选定了 Spring AI 2.0,从"知道"到"做到"之间仍存在巨大鸿沟。这个鸿沟体现在三个层面:

认知层面:开发者需要理解 ChatClient、ChatModel、Advisor、Prompt、Embedding、VectorStore 等 Spring AI 核心概念,以及它们之间的协作关系。这些概念虽然不难,但如果没有系统性的学习路径,开发者很容易陷入"碎片化学习"的陷阱------知道每个概念是什么,但不知道怎么组合使用。

架构层面:开发者需要决定 AI 能力在现有系统中的位置。是作为独立的微服务?还是嵌入到现有的业务服务中?模型调用的超时、重试、降级策略怎么设计?密钥怎么管理?审计日志怎么记录?这些架构决策需要综合考虑业务需求、团队能力和基础设施条件。

工程层面:开发者需要将架构决策落地为可编译、可测试、可部署的生产级代码。代码必须遵循团队规范(如阿里嵩山版),通过安全审计(如等保三级),满足性能要求(如响应时间、并发量)。这一层面的挑战往往是最大的------很多团队卡在这里,Demo 能跑但上不了生产。

本系列文章的核心目标,就是帮助 Java 开发者跨越这三个层面的鸿沟,从"知道 Spring AI"到"用 Spring AI 做出生产级 AI 应用"。


第三部分:方案一 基础版 - ChatClient + Advisors API 基础架构

3.1 实现原理

基础版方案的核心是利用 Spring AI 2.0 的 ChatClientAdvisors API,快速搭建一个可运行的 AI 聊天服务。ChatClient 提供 Fluent 风格的链式调用,Advisor 提供请求/响应拦截能力,实现日志记录等横切关注点。

该方案适用于 SME 团队的快速验证阶段,1-2 天即可完成开发和部署。

ChatClient API 核心设计 :Spring AI 2.0 的 ChatClient 采用 Fluent Builder 模式,通过链式调用构建请求。.prompt() 创建请求构建器,.user() 设置用户消息,.call() 执行同步调用,.content() 提取响应文本。这种设计让 AI 调用代码像写 SQL 一样直观,极大地降低了 Java 开发者的学习成本。

Advisor 拦截机制 :Advisor 是 Spring AI 2.0 的核心扩展点,类似于 Servlet Filter 但专为 AI 调用设计。BaseAdvisor 接口提供 before()after() 两个回调方法,分别在请求发送前和响应返回后执行。多个 Advisor 按 getOrder() 返回值排序执行,形成拦截器链。这一机制使得日志记录、安全过滤、数据脱敏等横切关注点可以与业务逻辑完全解耦。
#mermaid-svg-mwyHylQDfclcmudi{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mwyHylQDfclcmudi .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mwyHylQDfclcmudi .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mwyHylQDfclcmudi .error-icon{fill:#552222;}#mermaid-svg-mwyHylQDfclcmudi .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mwyHylQDfclcmudi .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mwyHylQDfclcmudi .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mwyHylQDfclcmudi .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mwyHylQDfclcmudi .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mwyHylQDfclcmudi .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mwyHylQDfclcmudi .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mwyHylQDfclcmudi .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mwyHylQDfclcmudi .marker.cross{stroke:#333333;}#mermaid-svg-mwyHylQDfclcmudi svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mwyHylQDfclcmudi p{margin:0;}#mermaid-svg-mwyHylQDfclcmudi .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mwyHylQDfclcmudi .cluster-label text{fill:#333;}#mermaid-svg-mwyHylQDfclcmudi .cluster-label span{color:#333;}#mermaid-svg-mwyHylQDfclcmudi .cluster-label span p{background-color:transparent;}#mermaid-svg-mwyHylQDfclcmudi .label text,#mermaid-svg-mwyHylQDfclcmudi span{fill:#333;color:#333;}#mermaid-svg-mwyHylQDfclcmudi .node rect,#mermaid-svg-mwyHylQDfclcmudi .node circle,#mermaid-svg-mwyHylQDfclcmudi .node ellipse,#mermaid-svg-mwyHylQDfclcmudi .node polygon,#mermaid-svg-mwyHylQDfclcmudi .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mwyHylQDfclcmudi .rough-node .label text,#mermaid-svg-mwyHylQDfclcmudi .node .label text,#mermaid-svg-mwyHylQDfclcmudi .image-shape .label,#mermaid-svg-mwyHylQDfclcmudi .icon-shape .label{text-anchor:middle;}#mermaid-svg-mwyHylQDfclcmudi .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mwyHylQDfclcmudi .rough-node .label,#mermaid-svg-mwyHylQDfclcmudi .node .label,#mermaid-svg-mwyHylQDfclcmudi .image-shape .label,#mermaid-svg-mwyHylQDfclcmudi .icon-shape .label{text-align:center;}#mermaid-svg-mwyHylQDfclcmudi .node.clickable{cursor:pointer;}#mermaid-svg-mwyHylQDfclcmudi .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mwyHylQDfclcmudi .arrowheadPath{fill:#333333;}#mermaid-svg-mwyHylQDfclcmudi .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mwyHylQDfclcmudi .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mwyHylQDfclcmudi .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mwyHylQDfclcmudi .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mwyHylQDfclcmudi .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mwyHylQDfclcmudi .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mwyHylQDfclcmudi .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mwyHylQDfclcmudi .cluster text{fill:#333;}#mermaid-svg-mwyHylQDfclcmudi .cluster span{color:#333;}#mermaid-svg-mwyHylQDfclcmudi div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mwyHylQDfclcmudi .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mwyHylQDfclcmudi rect.text{fill:none;stroke-width:0;}#mermaid-svg-mwyHylQDfclcmudi .icon-shape,#mermaid-svg-mwyHylQDfclcmudi .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mwyHylQDfclcmudi .icon-shape p,#mermaid-svg-mwyHylQDfclcmudi .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mwyHylQDfclcmudi .icon-shape .label rect,#mermaid-svg-mwyHylQDfclcmudi .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mwyHylQDfclcmudi .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mwyHylQDfclcmudi .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mwyHylQDfclcmudi :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端请求
ChatController
参数校验

@Valid
ChatService
ChatClient
LoggingAdvisor

请求拦截
OpenAI API
LoggingAdvisor

响应拦截
ChatClient
ChatService
ChatResponseVO
Result 统一返回

3.2 完整 Java 代码

3.2.1 application.yml 配置
yaml 复制代码
spring:
  application:
    name: com-common
  ai:
    openai:
      api-key: ${OPENAI_API_KEY}
      base-url: ${OPENAI_BASE_URL:https://api.openai.com}
      chat:
        options:
          model: gpt-4o
          temperature: 0.7
          max-tokens: 2048

server:
  port: 8080

logging:
  level:
    com.develop.code: INFO
    org.springframework.ai: INFO
  pattern:
    console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level traceId=%X{traceId} %logger{36} - %msg%n"

密钥通过环境变量 OPENAI_API_KEY 注入,禁止在配置文件中明文存储。日志格式中注入 traceId,满足链路追踪需求。

3.2.2 ChatClientConfig.java
java 复制代码
package com.develop.code.common.config;

import java.util.List;

import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.client.advisor.api.Advisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * ChatClient配置类
 * <p>
 * 配置ChatClient Bean,注入Advisor拦截器链。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@Configuration
public class ChatClientConfig {

    /**
     * 配置ChatClient Bean
     *
     * @param builder  ChatClient构建器(Spring AI自动注入)
     * @param advisors Advisor列表(Spring自动收集所有Advisor实现)
     * @return ChatClient实例
     */
    @Bean
    public ChatClient chatClient(ChatClient.Builder builder,
                                  List<Advisor> advisors) {
        return builder.defaultAdvisors(advisors).build();
    }
}
3.2.3 ChatRequestDTO.java
java 复制代码
package com.develop.code.common.dto;

import java.io.Serial;
import java.io.Serializable;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

/**
 * 聊天请求DTO
 *
 * @author develop-code
 * @since 1.0.0
 */
public class ChatRequestDTO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /** 用户消息内容 */
    @NotBlank(message = "消息内容不能为空")
    @Size(max = 2000, message = "消息内容不能超过2000字符")
    private String message;

    /** 会话ID */
    @Size(max = 64, message = "会话ID不能超过64字符")
    private String sessionId;

    /**
     * 默认构造器
     */
    public ChatRequestDTO() {
    }

    /**
     * 获取用户消息内容
     *
     * @return 用户消息内容
     */
    public String getMessage() {
        return message;
    }

    /**
     * 设置用户消息内容
     *
     * @param message 用户消息内容
     */
    public void setMessage(String message) {
        this.message = message;
    }

    /**
     * 获取会话ID
     *
     * @return 会话ID
     */
    public String getSessionId() {
        return sessionId;
    }

    /**
     * 设置会话ID
     *
     * @param sessionId 会话ID
     */
    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }
}
3.2.4 ChatResponseVO.java
java 复制代码
package com.develop.code.common.vo;

import java.io.Serial;
import java.io.Serializable;

/**
 * 聊天响应VO
 *
 * @author develop-code
 * @since 1.0.0
 */
public class ChatResponseVO implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /** 响应内容 */
    private String content;

    /** 模型名称 */
    private String model;

    /** Token使用量 */
    private Integer totalTokens;

    /** 响应耗时(毫秒) */
    private Long elapsedMillis;

    /**
     * 默认构造器
     */
    public ChatResponseVO() {
    }

    /**
     * 获取响应内容
     *
     * @return 响应内容
     */
    public String getContent() {
        return content;
    }

    /**
     * 设置响应内容
     *
     * @param content 响应内容
     */
    public void setContent(String content) {
        this.content = content;
    }

    /**
     * 获取模型名称
     *
     * @return 模型名称
     */
    public String getModel() {
        return model;
    }

    /**
     * 设置模型名称
     *
     * @param model 模型名称
     */
    public void setModel(String model) {
        this.model = model;
    }

    /**
     * 获取Token使用量
     *
     * @return Token使用量
     */
    public Integer getTotalTokens() {
        return totalTokens;
    }

    /**
     * 设置Token使用量
     *
     * @param totalTokens Token使用量
     */
    public void setTotalTokens(Integer totalTokens) {
        this.totalTokens = totalTokens;
    }

    /**
     * 获取响应耗时
     *
     * @return 响应耗时(毫秒)
     */
    public Long getElapsedMillis() {
        return elapsedMillis;
    }

    /**
     * 设置响应耗时
     *
     * @param elapsedMillis 响应耗时(毫秒)
     */
    public void setElapsedMillis(Long elapsedMillis) {
        this.elapsedMillis = elapsedMillis;
    }
}
3.2.5 ChatController.java
java 复制代码
package com.develop.code.common.controller;

import jakarta.validation.Valid;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.develop.code.common.dto.ChatRequestDTO;
import com.develop.code.common.result.Result;
import com.develop.code.common.service.ChatService;
import com.develop.code.common.vo.ChatResponseVO;

/**
 * 聊天接口控制器
 * <p>
 * 仅负责接口接收、参数校验、调用Service、返回VO。
 * 禁止包含业务逻辑和数据库操作。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@RestController
@RequestMapping("/api/v1/chat")
public class ChatController {

    private static final Logger log = LoggerFactory.getLogger(ChatController.class);

    private final ChatService chatService;

    /**
     * 构造器注入
     *
     * @param chatService 聊天业务服务
     */
    public ChatController(ChatService chatService) {
        this.chatService = chatService;
    }

    /**
     * 同步聊天接口
     *
     * @param requestDTO 聊天请求参数
     * @return 统一返回体
     */
    @PostMapping("/sync")
    public Result<ChatResponseVO> chat(@Valid @RequestBody ChatRequestDTO requestDTO) {
        log.info("[聊天请求] sessionId={}", requestDTO.getSessionId());
        ChatResponseVO responseVO = chatService.chat(requestDTO);
        return Result.success(responseVO);
    }
}
3.2.6 ChatService.java
java 复制代码
package com.develop.code.common.service;

import com.develop.code.common.dto.ChatRequestDTO;
import com.develop.code.common.vo.ChatResponseVO;

/**
 * 聊天业务接口
 *
 * @author develop-code
 * @since 1.0.0
 */
public interface ChatService {

    /**
     * 同步聊天
     *
     * @param requestDTO 请求参数
     * @return 聊天响应VO
     */
    ChatResponseVO chat(ChatRequestDTO requestDTO);
}
3.2.7 ChatServiceImpl.java
java 复制代码
package com.develop.code.common.service.impl;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.stereotype.Service;

import com.develop.code.common.dto.ChatRequestDTO;
import com.develop.code.common.exception.BusinessException;
import com.develop.code.common.helper.TraceIdHelper;
import com.develop.code.common.result.ResultCodeEnum;
import com.develop.code.common.service.ChatService;
import com.develop.code.common.vo.ChatResponseVO;

/**
 * 聊天业务实现
 * <p>
 * 承载全部业务逻辑,通过ChatClient调用大模型,
 * LoggingAdvisor自动拦截请求和响应记录日志。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@Service
public class ChatServiceImpl implements ChatService {

    private static final Logger log = LoggerFactory.getLogger(ChatServiceImpl.class);

    private final ChatClient chatClient;

    /**
     * 构造器注入
     *
     * @param chatClient Spring AI ChatClient
     */
    public ChatServiceImpl(ChatClient chatClient) {
        this.chatClient = chatClient;
    }

    /**
     * 同步聊天
     *
     * @param requestDTO 请求参数
     * @return 聊天响应VO
     * @throws BusinessException 模型调用异常时抛出
     */
    @Override
    public ChatResponseVO chat(ChatRequestDTO requestDTO) {
        long startTime = System.currentTimeMillis();
        String traceId = TraceIdHelper.getTraceId();
        log.info("[开始聊天] traceId={}, sessionId={}", traceId,
                requestDTO.getSessionId());

        try {
            ChatResponse chatResponse = chatClient.prompt()
                    .user(requestDTO.getMessage())
                    .call()
                    .chatResponse();

            String content = chatResponse.getResult().getOutput().getText();
            long elapsed = System.currentTimeMillis() - startTime;

            log.info("[聊天完成] traceId={}, elapsed={}ms", traceId, elapsed);

            ChatResponseVO vo = new ChatResponseVO();
            vo.setContent(content);
            vo.setElapsedMillis(elapsed);
            return vo;
        } catch (Exception e) {
            log.error("[聊天异常] traceId={}, msg={}", traceId,
                    e.getMessage(), e);
            throw new BusinessException(ResultCodeEnum.AI_MODEL_ERROR);
        }
    }
}
3.2.8 LoggingAdvisor.java
java 复制代码
package com.develop.code.common.advisor;

import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.advisor.api.AdvisedRequest;
import org.springframework.ai.chat.client.advisor.api.AdvisedResponse;
import org.springframework.ai.chat.client.advisor.api.BaseAdvisor;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;

import com.develop.code.common.helper.TraceIdHelper;

/**
 * 日志记录Advisor
 * <p>
 * 拦截ChatClient请求和响应,记录AI调用全链路日志。
 * 日志携带traceId,满足链路追踪需求。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@Component
public class LoggingAdvisor implements BaseAdvisor {

    private static final Logger log = LoggerFactory.getLogger(LoggingAdvisor.class);

    /** 日志截断最大长度 */
    private static final int MAX_LOG_LENGTH = 200;

    /**
     * 获取Advisor名称
     *
     * @return Advisor名称
     */
    @Override
    public String getName() {
        return "LoggingAdvisor";
    }

    /**
     * 获取执行顺序(最高优先级)
     *
     * @return 顺序值
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE;
    }

    /**
     * 请求前置拦截
     *
     * @param request  请求对象
     * @param context  上下文
     * @return 处理后的请求对象
     */
    @Override
    public AdvisedRequest before(AdvisedRequest request,
                                  Map<String, Object> context) {
        String traceId = TraceIdHelper.getTraceId();
        log.info("[AI请求拦截] traceId={}, userMessage={}", traceId,
                truncate(request.userText()));
        return request;
    }

    /**
     * 响应后置拦截
     *
     * @param response 响应对象
     * @param context  上下文
     * @return 处理后的响应对象
     */
    @Override
    public AdvisedResponse after(AdvisedResponse response,
                                  Map<String, Object> context) {
        String traceId = TraceIdHelper.getTraceId();
        log.info("[AI响应拦截] traceId={}", traceId);
        return response;
    }

    /**
     * 截断日志内容
     *
     * @param text 原始文本
     * @return 截断后的文本
     */
    private String truncate(String text) {
        if (text == null) {
            return "";
        }
        return text.length() <= MAX_LOG_LENGTH
                ? text : text.substring(0, MAX_LOG_LENGTH) + "...";
    }
}

3.3 数据走向流程图

#mermaid-svg-vRii5hw0f4UXevuP{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-vRii5hw0f4UXevuP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-vRii5hw0f4UXevuP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-vRii5hw0f4UXevuP .error-icon{fill:#552222;}#mermaid-svg-vRii5hw0f4UXevuP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-vRii5hw0f4UXevuP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-vRii5hw0f4UXevuP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-vRii5hw0f4UXevuP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-vRii5hw0f4UXevuP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-vRii5hw0f4UXevuP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-vRii5hw0f4UXevuP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-vRii5hw0f4UXevuP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-vRii5hw0f4UXevuP .marker.cross{stroke:#333333;}#mermaid-svg-vRii5hw0f4UXevuP svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-vRii5hw0f4UXevuP p{margin:0;}#mermaid-svg-vRii5hw0f4UXevuP .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-vRii5hw0f4UXevuP .cluster-label text{fill:#333;}#mermaid-svg-vRii5hw0f4UXevuP .cluster-label span{color:#333;}#mermaid-svg-vRii5hw0f4UXevuP .cluster-label span p{background-color:transparent;}#mermaid-svg-vRii5hw0f4UXevuP .label text,#mermaid-svg-vRii5hw0f4UXevuP span{fill:#333;color:#333;}#mermaid-svg-vRii5hw0f4UXevuP .node rect,#mermaid-svg-vRii5hw0f4UXevuP .node circle,#mermaid-svg-vRii5hw0f4UXevuP .node ellipse,#mermaid-svg-vRii5hw0f4UXevuP .node polygon,#mermaid-svg-vRii5hw0f4UXevuP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-vRii5hw0f4UXevuP .rough-node .label text,#mermaid-svg-vRii5hw0f4UXevuP .node .label text,#mermaid-svg-vRii5hw0f4UXevuP .image-shape .label,#mermaid-svg-vRii5hw0f4UXevuP .icon-shape .label{text-anchor:middle;}#mermaid-svg-vRii5hw0f4UXevuP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-vRii5hw0f4UXevuP .rough-node .label,#mermaid-svg-vRii5hw0f4UXevuP .node .label,#mermaid-svg-vRii5hw0f4UXevuP .image-shape .label,#mermaid-svg-vRii5hw0f4UXevuP .icon-shape .label{text-align:center;}#mermaid-svg-vRii5hw0f4UXevuP .node.clickable{cursor:pointer;}#mermaid-svg-vRii5hw0f4UXevuP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-vRii5hw0f4UXevuP .arrowheadPath{fill:#333333;}#mermaid-svg-vRii5hw0f4UXevuP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-vRii5hw0f4UXevuP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-vRii5hw0f4UXevuP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vRii5hw0f4UXevuP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-vRii5hw0f4UXevuP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vRii5hw0f4UXevuP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-vRii5hw0f4UXevuP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-vRii5hw0f4UXevuP .cluster text{fill:#333;}#mermaid-svg-vRii5hw0f4UXevuP .cluster span{color:#333;}#mermaid-svg-vRii5hw0f4UXevuP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-vRii5hw0f4UXevuP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-vRii5hw0f4UXevuP rect.text{fill:none;stroke-width:0;}#mermaid-svg-vRii5hw0f4UXevuP .icon-shape,#mermaid-svg-vRii5hw0f4UXevuP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-vRii5hw0f4UXevuP .icon-shape p,#mermaid-svg-vRii5hw0f4UXevuP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-vRii5hw0f4UXevuP .icon-shape .label rect,#mermaid-svg-vRii5hw0f4UXevuP .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-vRii5hw0f4UXevuP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-vRii5hw0f4UXevuP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-vRii5hw0f4UXevuP :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 用户消息
DTO校验

@Valid
ChatService

业务处理
ChatClient

.prompt().user()
LoggingAdvisor

before拦截
OpenAI API

模型调用
LoggingAdvisor

after拦截
ChatResponse

响应解析
ChatResponseVO

封装出参
Result.success

统一返回

3.4 优劣势分析

维度 评价 说明
开发效率 优势 1-2天完成开发部署
代码质量 优势 分层清晰,符合阿里规范
日志追踪 优势 Advisor自动拦截,traceId贯穿
模型扩展性 劣势 仅支持单一模型,切换需改配置
异常处理 一般 有基本异常捕获,缺降级策略
成本控制 劣势 无Token预算控制
安全合规 一般 密钥环境变量注入,缺审计日志

3.5 成本估算

成本项 月度费用(元) 说明
OpenAI API Token 800-3000 按日均 500 次调用估算
服务器(2C4G) 200 单节点部署
人力维护 0.5 人天 基础运维
月度总成本 1000-3200 适合 SME 初期验证

3.6 SME 适配场景

基础版方案适合以下 SME 场景:

  • 团队 3-5 人,首次接入 AI 能力
  • 日均调用量 < 1000 次
  • 单一模型满足业务需求
  • 预算 3000 元/月以内
  • 上线周期 1-2 天

第四部分:方案二 进阶版 - 多模型适配层

4.1 实现原理

进阶版方案在基础版之上引入多模型适配层 ,通过策略模式统一抽象 OpenAI、Anthropic、通义千问等不同模型提供商的调用接口。业务层通过 ModelTypeEnum 指定模型类型,适配层自动路由到对应实现。

同时引入 ModelHealthChecker 定时健康检查,当主模型不可用时自动降级到备用模型,提升系统可用性。

策略模式选型理由 :多模型适配本质上是"同一接口、不同实现"的经典场景,策略模式是最佳选择。每个模型适配器实现统一的 ModelAdapterService 接口,Spring 自动收集所有实现并注入到 MultiModelChatServiceImpl 中。新增模型时只需实现接口并注册为 Spring Bean,无需修改任何现有代码,完全符合开闭原则。

自动降级设计callWithFallback 方法实现了"主模型优先、备用模型兜底"的降级策略。当主模型调用抛出异常时,自动遍历其余适配器逐一尝试,直到找到可用模型或全部失败。这一设计在生产环境中至关重要------模型 API 的稳定性不可能达到 100%,自动降级是保障业务连续性的最后一道防线。

健康检查缓存策略ModelHealthChecker 每 60 秒执行一次健康探测,结果缓存在 ConcurrentHashMap 中。业务层查询健康状态时直接读缓存,避免每次请求都触发探测。当状态发生变更时(从健康变为不可用或反之),打印 WARN 级别日志,便于运维人员快速感知模型状态变化。
#mermaid-svg-SNokoZElBZswUVZV{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-SNokoZElBZswUVZV .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-SNokoZElBZswUVZV .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-SNokoZElBZswUVZV .error-icon{fill:#552222;}#mermaid-svg-SNokoZElBZswUVZV .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-SNokoZElBZswUVZV .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-SNokoZElBZswUVZV .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-SNokoZElBZswUVZV .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-SNokoZElBZswUVZV .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-SNokoZElBZswUVZV .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-SNokoZElBZswUVZV .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-SNokoZElBZswUVZV .marker{fill:#333333;stroke:#333333;}#mermaid-svg-SNokoZElBZswUVZV .marker.cross{stroke:#333333;}#mermaid-svg-SNokoZElBZswUVZV svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-SNokoZElBZswUVZV p{margin:0;}#mermaid-svg-SNokoZElBZswUVZV .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-SNokoZElBZswUVZV .cluster-label text{fill:#333;}#mermaid-svg-SNokoZElBZswUVZV .cluster-label span{color:#333;}#mermaid-svg-SNokoZElBZswUVZV .cluster-label span p{background-color:transparent;}#mermaid-svg-SNokoZElBZswUVZV .label text,#mermaid-svg-SNokoZElBZswUVZV span{fill:#333;color:#333;}#mermaid-svg-SNokoZElBZswUVZV .node rect,#mermaid-svg-SNokoZElBZswUVZV .node circle,#mermaid-svg-SNokoZElBZswUVZV .node ellipse,#mermaid-svg-SNokoZElBZswUVZV .node polygon,#mermaid-svg-SNokoZElBZswUVZV .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-SNokoZElBZswUVZV .rough-node .label text,#mermaid-svg-SNokoZElBZswUVZV .node .label text,#mermaid-svg-SNokoZElBZswUVZV .image-shape .label,#mermaid-svg-SNokoZElBZswUVZV .icon-shape .label{text-anchor:middle;}#mermaid-svg-SNokoZElBZswUVZV .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-SNokoZElBZswUVZV .rough-node .label,#mermaid-svg-SNokoZElBZswUVZV .node .label,#mermaid-svg-SNokoZElBZswUVZV .image-shape .label,#mermaid-svg-SNokoZElBZswUVZV .icon-shape .label{text-align:center;}#mermaid-svg-SNokoZElBZswUVZV .node.clickable{cursor:pointer;}#mermaid-svg-SNokoZElBZswUVZV .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-SNokoZElBZswUVZV .arrowheadPath{fill:#333333;}#mermaid-svg-SNokoZElBZswUVZV .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-SNokoZElBZswUVZV .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-SNokoZElBZswUVZV .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SNokoZElBZswUVZV .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-SNokoZElBZswUVZV .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SNokoZElBZswUVZV .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-SNokoZElBZswUVZV .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-SNokoZElBZswUVZV .cluster text{fill:#333;}#mermaid-svg-SNokoZElBZswUVZV .cluster span{color:#333;}#mermaid-svg-SNokoZElBZswUVZV div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-SNokoZElBZswUVZV .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-SNokoZElBZswUVZV rect.text{fill:none;stroke-width:0;}#mermaid-svg-SNokoZElBZswUVZV .icon-shape,#mermaid-svg-SNokoZElBZswUVZV .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-SNokoZElBZswUVZV .icon-shape p,#mermaid-svg-SNokoZElBZswUVZV .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-SNokoZElBZswUVZV .icon-shape .label rect,#mermaid-svg-SNokoZElBZswUVZV .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-SNokoZElBZswUVZV .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-SNokoZElBZswUVZV .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-SNokoZElBZswUVZV :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端请求
ChatController
MultiModelChatService
ModelTypeEnum

模型选择
OpenAiModelAdapter
AnthropicModelAdapter
QwenModelAdapter
OpenAI API
Anthropic API
Qwen API
统一响应封装
ModelHealthChecker

健康检查
Result 统一返回

4.2 完整 Java 代码

4.2.1 ModelTypeEnum.java
java 复制代码
package com.develop.code.common.enums;

/**
 * 模型类型枚举
 *
 * @author develop-code
 * @since 1.0.0
 */
public enum ModelTypeEnum {

    /** OpenAI GPT系列 */
    OPENAI("OpenAI", "gpt-4o"),

    /** Anthropic Claude系列 */
    ANTHROPIC("Anthropic", "claude-sonnet-4-5"),

    /** 阿里通义千问 */
    QWEN("Qwen", "qwen-max");

    /** 提供商标识 */
    private final String provider;

    /** 默认模型名称 */
    private final String defaultModel;

    /**
     * 构造器
     *
     * @param provider     提供商标识
     * @param defaultModel 默认模型名称
     */
    ModelTypeEnum(String provider, String defaultModel) {
        this.provider = provider;
        this.defaultModel = defaultModel;
    }

    /**
     * 获取提供商标识
     *
     * @return 提供商标识
     */
    public String getProvider() {
        return provider;
    }

    /**
     * 获取默认模型名称
     *
     * @return 默认模型名称
     */
    public String getDefaultModel() {
        return defaultModel;
    }
}
4.2.2 ModelAdapterService.java
java 复制代码
package com.develop.code.common.adapter;

/**
 * 模型适配器接口
 * <p>
 * 统一抽象不同模型提供商的调用接口,
 * 支持动态切换模型和健康检查。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
public interface ModelAdapterService {

    /**
     * 同步聊天
     *
     * @param message 用户消息
     * @return 模型响应内容
     */
    String chat(String message);

    /**
     * 健康检查
     *
     * @return 模型服务是否可用
     */
    boolean isHealthy();

    /**
     * 获取模型名称
     *
     * @return 模型名称
     */
    String getModelName();
}
4.2.3 OpenAiModelAdapter.java
java 复制代码
package com.develop.code.common.adapter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import com.develop.code.common.enums.ModelTypeEnum;

/**
 * OpenAI模型适配器
 *
 * @author develop-code
 * @since 1.0.0
 */
@Component
public class OpenAiModelAdapter implements ModelAdapterService {

    private static final Logger log = LoggerFactory.getLogger(
            OpenAiModelAdapter.class);

    private final ChatClient openAiChatClient;

    /**
     * 构造器注入
     *
     * @param openAiChatClient OpenAI专用ChatClient
     */
    public OpenAiModelAdapter(
            @Qualifier("openAiChatClient") ChatClient openAiChatClient) {
        this.openAiChatClient = openAiChatClient;
    }

    /**
     * 同步聊天
     *
     * @param message 用户消息
     * @return 模型响应内容
     */
    @Override
    public String chat(String message) {
        log.info("[OpenAI调用] message={}", truncate(message));
        return openAiChatClient.prompt()
                .user(message)
                .call()
                .content();
    }

    /**
     * 健康检查
     *
     * @return 模型服务是否可用
     */
    @Override
    public boolean isHealthy() {
        try {
            openAiChatClient.prompt().user("ping").call().content();
            return true;
        } catch (Exception e) {
            log.warn("[OpenAI健康检查失败] msg={}", e.getMessage());
            return false;
        }
    }

    /**
     * 获取模型名称
     *
     * @return 模型名称
     */
    @Override
    public String getModelName() {
        return ModelTypeEnum.OPENAI.name();
    }

    /**
     * 截断日志内容
     *
     * @param text 原始文本
     * @return 截断后的文本
     */
    private String truncate(String text) {
        if (text == null) {
            return "";
        }
        return text.length() <= 100 ? text : text.substring(0, 100) + "...";
    }
}
4.2.4 AnthropicModelAdapter.java
java 复制代码
package com.develop.code.common.adapter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import com.develop.code.common.enums.ModelTypeEnum;

/**
 * Anthropic模型适配器
 *
 * @author develop-code
 * @since 1.0.0
 */
@Component
public class AnthropicModelAdapter implements ModelAdapterService {

    private static final Logger log = LoggerFactory.getLogger(
            AnthropicModelAdapter.class);

    private final ChatClient anthropicChatClient;

    /**
     * 构造器注入
     *
     * @param anthropicChatClient Anthropic专用ChatClient
     */
    public AnthropicModelAdapter(
            @Qualifier("anthropicChatClient") ChatClient anthropicChatClient) {
        this.anthropicChatClient = anthropicChatClient;
    }

    /**
     * 同步聊天
     *
     * @param message 用户消息
     * @return 模型响应内容
     */
    @Override
    public String chat(String message) {
        log.info("[Anthropic调用] message={}", truncate(message));
        return anthropicChatClient.prompt()
                .user(message)
                .call()
                .content();
    }

    /**
     * 健康检查
     *
     * @return 模型服务是否可用
     */
    @Override
    public boolean isHealthy() {
        try {
            anthropicChatClient.prompt().user("ping").call().content();
            return true;
        } catch (Exception e) {
            log.warn("[Anthropic健康检查失败] msg={}", e.getMessage());
            return false;
        }
    }

    /**
     * 获取模型名称
     *
     * @return 模型名称
     */
    @Override
    public String getModelName() {
        return ModelTypeEnum.ANTHROPIC.name();
    }

    /**
     * 截断日志内容
     *
     * @param text 原始文本
     * @return 截断后的文本
     */
    private String truncate(String text) {
        if (text == null) {
            return "";
        }
        return text.length() <= 100 ? text : text.substring(0, 100) + "...";
    }
}
4.2.5 QwenModelAdapter.java
java 复制代码
package com.develop.code.common.adapter;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ai.chat.client.ChatClient;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import com.develop.code.common.enums.ModelTypeEnum;

/**
 * 通义千问模型适配器
 * <p>
 * 通过OpenAI兼容接口调用通义千问API。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@Component
public class QwenModelAdapter implements ModelAdapterService {

    private static final Logger log = LoggerFactory.getLogger(
            QwenModelAdapter.class);

    private final ChatClient qwenChatClient;

    /**
     * 构造器注入
     *
     * @param qwenChatClient 通义千问专用ChatClient
     */
    public QwenModelAdapter(
            @Qualifier("qwenChatClient") ChatClient qwenChatClient) {
        this.qwenChatClient = qwenChatClient;
    }

    /**
     * 同步聊天
     *
     * @param message 用户消息
     * @return 模型响应内容
     */
    @Override
    public String chat(String message) {
        log.info("[Qwen调用] message={}", truncate(message));
        return qwenChatClient.prompt()
                .user(message)
                .call()
                .content();
    }

    /**
     * 健康检查
     *
     * @return 模型服务是否可用
     */
    @Override
    public boolean isHealthy() {
        try {
            qwenChatClient.prompt().user("ping").call().content();
            return true;
        } catch (Exception e) {
            log.warn("[Qwen健康检查失败] msg={}", e.getMessage());
            return false;
        }
    }

    /**
     * 获取模型名称
     *
     * @return 模型名称
     */
    @Override
    public String getModelName() {
        return ModelTypeEnum.QWEN.name();
    }

    /**
     * 截断日志内容
     *
     * @param text 原始文本
     * @return 截断后的文本
     */
    private String truncate(String text) {
        if (text == null) {
            return "";
        }
        return text.length() <= 100 ? text : text.substring(0, 100) + "...";
    }
}
4.2.6 MultiModelChatServiceImpl.java
java 复制代码
package com.develop.code.common.service.impl;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

import com.develop.code.common.adapter.ModelAdapterService;
import com.develop.code.common.dto.ChatRequestDTO;
import com.develop.code.common.enums.ModelTypeEnum;
import com.develop.code.common.exception.BusinessException;
import com.develop.code.common.helper.TraceIdHelper;
import com.develop.code.common.result.ResultCodeEnum;
import com.develop.code.common.service.ChatService;
import com.develop.code.common.vo.ChatResponseVO;

/**
 * 多模型聊天业务实现
 * <p>
 * 通过策略模式动态选择模型适配器,
 * 支持主模型故障时自动降级到备用模型。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@Service
public class MultiModelChatServiceImpl implements ChatService {

    private static final Logger log = LoggerFactory.getLogger(
            MultiModelChatServiceImpl.class);

    /** 模型适配器映射 */
    private final Map<String, ModelAdapterService> adapterMap;

    /**
     * 构造器注入
     * <p>
     * Spring自动注入所有ModelAdapterService实现,
     * 按model name构建映射表。
     * </p>
     *
     * @param adapters 模型适配器列表
     */
    public MultiModelChatServiceImpl(List<ModelAdapterService> adapters) {
        this.adapterMap = adapters.stream()
                .collect(Collectors.toMap(
                        ModelAdapterService::getModelName, a -> a));
        log.info("[多模型适配器初始化] adapters={}", adapterMap.keySet());
    }

    /**
     * 同步聊天
     * <p>
     * 默认使用OpenAI模型,调用失败时自动降级。
     * </p>
     *
     * @param requestDTO 请求参数
     * @return 聊天响应VO
     * @throws BusinessException 所有模型均不可用时抛出
     */
    @Override
    public ChatResponseVO chat(ChatRequestDTO requestDTO) {
        long startTime = System.currentTimeMillis();
        String traceId = TraceIdHelper.getTraceId();

        String content = callWithFallback(ModelTypeEnum.OPENAI.name(),
                requestDTO.getMessage(), traceId);

        long elapsed = System.currentTimeMillis() - startTime;
        log.info("[多模型聊天完成] traceId={}, elapsed={}ms", traceId, elapsed);

        ChatResponseVO vo = new ChatResponseVO();
        vo.setContent(content);
        vo.setElapsedMillis(elapsed);
        return vo;
    }

    /**
     * 带降级的模型调用
     *
     * @param primaryModel 主模型名称
     * @param message      用户消息
     * @param traceId      链路追踪ID
     * @return 模型响应内容
     * @throws BusinessException 所有模型均不可用时抛出
     */
    private String callWithFallback(String primaryModel, String message,
                                     String traceId) {
        ModelAdapterService adapter = adapterMap.get(primaryModel);
        if (adapter != null) {
            try {
                return adapter.chat(message);
            } catch (Exception e) {
                log.warn("[主模型调用失败] traceId={}, model={}, msg={}",
                        traceId, primaryModel, e.getMessage());
            }
        }

        for (Map.Entry<String, ModelAdapterService> entry : adapterMap.entrySet()) {
            if (entry.getKey().equals(primaryModel)) {
                continue;
            }
            try {
                log.info("[降级到备用模型] traceId={}, model={}",
                        traceId, entry.getKey());
                return entry.getValue().chat(message);
            } catch (Exception e) {
                log.warn("[备用模型调用失败] traceId={}, model={}, msg={}",
                        traceId, entry.getKey(), e.getMessage());
            }
        }

        throw new BusinessException(ResultCodeEnum.AI_MODEL_ERROR);
    }
}
4.2.7 ModelHealthChecker.java
java 复制代码
package com.develop.code.common.health;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import com.develop.code.common.adapter.ModelAdapterService;

/**
 * 模型健康检查器
 * <p>
 * 定时检查各模型适配器的健康状态,
 * 缓存检查结果供业务层查询。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@Component
public class ModelHealthChecker {

    private static final Logger log = LoggerFactory.getLogger(
            ModelHealthChecker.class);

    /** 模型适配器映射 */
    private final Map<String, ModelAdapterService> adapterMap;

    /** 健康状态缓存 */
    private final Map<String, Boolean> healthStatus = new ConcurrentHashMap<>();

    /**
     * 构造器注入
     *
     * @param adapters 模型适配器列表
     */
    public ModelHealthChecker(List<ModelAdapterService> adapters) {
        this.adapterMap = adapters.stream()
                .collect(Collectors.toMap(
                        ModelAdapterService::getModelName, a -> a));
    }

    /**
     * 定时健康检查(每60秒执行一次)
     */
    @Scheduled(fixedRate = 60000)
    public void checkHealth() {
        adapterMap.forEach((name, adapter) -> {
            boolean healthy = adapter.isHealthy();
            Boolean previous = healthStatus.put(name, healthy);
            if (previous == null || previous != healthy) {
                log.info("[模型健康状态变更] model={}, healthy={}", name, healthy);
            }
        });
    }

    /**
     * 查询指定模型是否健康
     *
     * @param modelName 模型名称
     * @return 是否健康
     */
    public boolean isHealthy(String modelName) {
        return healthStatus.getOrDefault(modelName, false);
    }

    /**
     * 获取第一个健康的模型名称
     *
     * @return 健康模型名称,无可用模型时返回null
     */
    public String getFirstHealthyModel() {
        return healthStatus.entrySet().stream()
                .filter(Map.Entry::getValue)
                .map(Map.Entry::getKey)
                .findFirst()
                .orElse(null);
    }

    /**
     * 获取所有模型健康状态
     *
     * @return 模型健康状态映射
     */
    public Map<String, Boolean> getAllHealthStatus() {
        return new ConcurrentHashMap<>(healthStatus);
    }
}

4.3 数据走向流程图

#mermaid-svg-1hAmWGXBDW5tGCds{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1hAmWGXBDW5tGCds .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1hAmWGXBDW5tGCds .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1hAmWGXBDW5tGCds .error-icon{fill:#552222;}#mermaid-svg-1hAmWGXBDW5tGCds .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1hAmWGXBDW5tGCds .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1hAmWGXBDW5tGCds .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1hAmWGXBDW5tGCds .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1hAmWGXBDW5tGCds .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1hAmWGXBDW5tGCds .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1hAmWGXBDW5tGCds .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1hAmWGXBDW5tGCds .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1hAmWGXBDW5tGCds .marker.cross{stroke:#333333;}#mermaid-svg-1hAmWGXBDW5tGCds svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1hAmWGXBDW5tGCds p{margin:0;}#mermaid-svg-1hAmWGXBDW5tGCds .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-1hAmWGXBDW5tGCds .cluster-label text{fill:#333;}#mermaid-svg-1hAmWGXBDW5tGCds .cluster-label span{color:#333;}#mermaid-svg-1hAmWGXBDW5tGCds .cluster-label span p{background-color:transparent;}#mermaid-svg-1hAmWGXBDW5tGCds .label text,#mermaid-svg-1hAmWGXBDW5tGCds span{fill:#333;color:#333;}#mermaid-svg-1hAmWGXBDW5tGCds .node rect,#mermaid-svg-1hAmWGXBDW5tGCds .node circle,#mermaid-svg-1hAmWGXBDW5tGCds .node ellipse,#mermaid-svg-1hAmWGXBDW5tGCds .node polygon,#mermaid-svg-1hAmWGXBDW5tGCds .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1hAmWGXBDW5tGCds .rough-node .label text,#mermaid-svg-1hAmWGXBDW5tGCds .node .label text,#mermaid-svg-1hAmWGXBDW5tGCds .image-shape .label,#mermaid-svg-1hAmWGXBDW5tGCds .icon-shape .label{text-anchor:middle;}#mermaid-svg-1hAmWGXBDW5tGCds .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1hAmWGXBDW5tGCds .rough-node .label,#mermaid-svg-1hAmWGXBDW5tGCds .node .label,#mermaid-svg-1hAmWGXBDW5tGCds .image-shape .label,#mermaid-svg-1hAmWGXBDW5tGCds .icon-shape .label{text-align:center;}#mermaid-svg-1hAmWGXBDW5tGCds .node.clickable{cursor:pointer;}#mermaid-svg-1hAmWGXBDW5tGCds .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1hAmWGXBDW5tGCds .arrowheadPath{fill:#333333;}#mermaid-svg-1hAmWGXBDW5tGCds .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1hAmWGXBDW5tGCds .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1hAmWGXBDW5tGCds .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1hAmWGXBDW5tGCds .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1hAmWGXBDW5tGCds .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1hAmWGXBDW5tGCds .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1hAmWGXBDW5tGCds .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1hAmWGXBDW5tGCds .cluster text{fill:#333;}#mermaid-svg-1hAmWGXBDW5tGCds .cluster span{color:#333;}#mermaid-svg-1hAmWGXBDW5tGCds div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1hAmWGXBDW5tGCds .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1hAmWGXBDW5tGCds rect.text{fill:none;stroke-width:0;}#mermaid-svg-1hAmWGXBDW5tGCds .icon-shape,#mermaid-svg-1hAmWGXBDW5tGCds .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1hAmWGXBDW5tGCds .icon-shape p,#mermaid-svg-1hAmWGXBDW5tGCds .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1hAmWGXBDW5tGCds .icon-shape .label rect,#mermaid-svg-1hAmWGXBDW5tGCds .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1hAmWGXBDW5tGCds .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1hAmWGXBDW5tGCds .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1hAmWGXBDW5tGCds :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是

用户请求
ModelTypeEnum

模型选择
适配器路由

Map查找
ModelHealthChecker

健康检查
健康?
主模型调用
降级备用模型
响应统一封装
Result返回

4.4 优劣势分析

维度 评价 说明
模型扩展性 优势 新增模型仅需实现接口
可用性 优势 主模型故障自动降级
健康检查 优势 定时探测,状态缓存
开发效率 一般 需3-5天开发配置
配置复杂度 劣势 多模型需独立配置
成本控制 一般 无智能路由优化

4.5 成本估算

成本项 月度费用(元) 说明
OpenAI API Token 600-2000 主模型,按需调用
Anthropic API Token 300-1000 备用模型,降级时使用
通义千问 API Token 200-800 降级模型,成本较低
服务器(4C8G) 400 需更多内存缓存健康状态
人力维护 1 人天 多模型配置与监控
月度总成本 1500-4200 适合成长期团队

4.6 Build vs Buy 分析

对比维度 自建多模型适配层 OneAPI 中间件 LiteLLM 代理
开发成本 3-5天 1天部署 1天部署
定制灵活性
维护成本 低(Spring原生) 中(独立服务) 中(Python生态)
Java生态集成 原生 HTTP调用 HTTP调用
故障排查 直接看日志 需查中间件 需查代理层
等保合规 易(同进程) 难(跨服务) 难(跨服务)
SME推荐度 推荐 不推荐 不推荐

结论:SME 团队建议自建多模型适配层。虽然多投入 2-4 天开发时间,但获得了 Java 原生集成、故障排查便捷、等保合规易过审三大核心优势。引入外部中间件会增加运维复杂度和安全合规风险。


第五部分:方案三 高级版 - 配置中心 + 密钥管理 + 健康检查全链路

5.1 实现原理

高级版方案在进阶版基础上构建全链路生产级架构,覆盖从请求入口到响应返回的每一个环节。核心增强包括:

  • 统一返回体 Result<T>:固定 code/msg/data/traceId 四字段
  • 全局异常处理 GlobalExceptionHandler:统一捕获业务异常、参数校验异常、系统异常
  • 审计日志切面 AuditLogAspect:AOP拦截Controller,记录操作人/IP/耗时/traceId
  • 安全工具 SecurityHelper:AES加密、BCrypt哈希、敏感字段脱敏
  • 链路追踪 TraceIdInterceptor:请求入口注入traceId到MDC
  • 跨域安全 CorsConfig:指定域名白名单,禁止通配符
  • Redis配置 RedisConfig:Jackson序列化,支持Java 8时间类型
  • MyBatis-Plus配置 MyBatisPlusConfig:分页插件 + 乐观锁插件

#mermaid-svg-6U9FtR28pZOqBcRU{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-6U9FtR28pZOqBcRU .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-6U9FtR28pZOqBcRU .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-6U9FtR28pZOqBcRU .error-icon{fill:#552222;}#mermaid-svg-6U9FtR28pZOqBcRU .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-6U9FtR28pZOqBcRU .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-6U9FtR28pZOqBcRU .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-6U9FtR28pZOqBcRU .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-6U9FtR28pZOqBcRU .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-6U9FtR28pZOqBcRU .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-6U9FtR28pZOqBcRU .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-6U9FtR28pZOqBcRU .marker{fill:#333333;stroke:#333333;}#mermaid-svg-6U9FtR28pZOqBcRU .marker.cross{stroke:#333333;}#mermaid-svg-6U9FtR28pZOqBcRU svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-6U9FtR28pZOqBcRU p{margin:0;}#mermaid-svg-6U9FtR28pZOqBcRU .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-6U9FtR28pZOqBcRU .cluster-label text{fill:#333;}#mermaid-svg-6U9FtR28pZOqBcRU .cluster-label span{color:#333;}#mermaid-svg-6U9FtR28pZOqBcRU .cluster-label span p{background-color:transparent;}#mermaid-svg-6U9FtR28pZOqBcRU .label text,#mermaid-svg-6U9FtR28pZOqBcRU span{fill:#333;color:#333;}#mermaid-svg-6U9FtR28pZOqBcRU .node rect,#mermaid-svg-6U9FtR28pZOqBcRU .node circle,#mermaid-svg-6U9FtR28pZOqBcRU .node ellipse,#mermaid-svg-6U9FtR28pZOqBcRU .node polygon,#mermaid-svg-6U9FtR28pZOqBcRU .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-6U9FtR28pZOqBcRU .rough-node .label text,#mermaid-svg-6U9FtR28pZOqBcRU .node .label text,#mermaid-svg-6U9FtR28pZOqBcRU .image-shape .label,#mermaid-svg-6U9FtR28pZOqBcRU .icon-shape .label{text-anchor:middle;}#mermaid-svg-6U9FtR28pZOqBcRU .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-6U9FtR28pZOqBcRU .rough-node .label,#mermaid-svg-6U9FtR28pZOqBcRU .node .label,#mermaid-svg-6U9FtR28pZOqBcRU .image-shape .label,#mermaid-svg-6U9FtR28pZOqBcRU .icon-shape .label{text-align:center;}#mermaid-svg-6U9FtR28pZOqBcRU .node.clickable{cursor:pointer;}#mermaid-svg-6U9FtR28pZOqBcRU .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-6U9FtR28pZOqBcRU .arrowheadPath{fill:#333333;}#mermaid-svg-6U9FtR28pZOqBcRU .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-6U9FtR28pZOqBcRU .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-6U9FtR28pZOqBcRU .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6U9FtR28pZOqBcRU .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-6U9FtR28pZOqBcRU .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6U9FtR28pZOqBcRU .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-6U9FtR28pZOqBcRU .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-6U9FtR28pZOqBcRU .cluster text{fill:#333;}#mermaid-svg-6U9FtR28pZOqBcRU .cluster span{color:#333;}#mermaid-svg-6U9FtR28pZOqBcRU div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-6U9FtR28pZOqBcRU .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-6U9FtR28pZOqBcRU rect.text{fill:none;stroke-width:0;}#mermaid-svg-6U9FtR28pZOqBcRU .icon-shape,#mermaid-svg-6U9FtR28pZOqBcRU .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-6U9FtR28pZOqBcRU .icon-shape p,#mermaid-svg-6U9FtR28pZOqBcRU .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-6U9FtR28pZOqBcRU .icon-shape .label rect,#mermaid-svg-6U9FtR28pZOqBcRU .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-6U9FtR28pZOqBcRU .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-6U9FtR28pZOqBcRU .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-6U9FtR28pZOqBcRU :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 客户端请求
TraceIdInterceptor

traceId注入
CorsConfig

跨域校验
ChatController

参数校验
AuditLogAspect

审计切面拦截
ChatService

业务逻辑
ChatClient

模型调用
LoggingAdvisor

日志拦截
SecurityHelper

数据脱敏
Result封装

统一返回体
GlobalExceptionHandler

异常兜底
统一返回客户端

5.2 完整 Java 代码

以下代码均为 com-common 模块生产级实现,遵循阿里嵩山版规范和等保三级要求。

全链路设计理念:高级版方案的核心设计理念是"每一层都有保障"。从 HTTP 请求入口到模型 API 调用,再到响应返回客户端,全链路的每一个环节都有对应的安全和可观测性保障。这不是简单的功能堆砌,而是基于等保三级要求进行的系统性设计------输入有校验、调用有拦截、数据有加密、操作有审计、异常有兜底、链路有追踪。

审计日志异步写入AuditLogAspect 使用 @Async 异步写入审计日志,确保审计记录不影响主流程性能。审计日志包含操作人 ID、操作时间、请求参数、响应数据、客户端 IP、traceId、接口耗时、成功/失败标识等完整信息,满足等保三级"所有增删改操作记录操作人、操作时间、原始数据、变更数据、客户端 IP"的要求。日志持久化存储在 MariaDB 中,保留期限不低于 180 天。

密钥管理三级防护:密钥管理是等保三级数据安全的核心要求。本方案实现了三级防护------第一级,所有密钥(API Key、AES Key、数据库密码)通过环境变量注入,禁止出现在代码和配置文件中;第二级,敏感数据(手机号、身份证)使用 AES 对称加密存储,即使数据库被拖库也无法还原明文;第三级,密码使用 BCrypt 加盐哈希,即使密钥泄露也无法逆向破解。

5.2.1 Result.java(统一返回体)
java 复制代码
package com.develop.code.common.result;

import java.io.Serial;
import java.io.Serializable;

import com.develop.code.common.helper.TraceIdHelper;

/**
 * 统一返回体 Result T
 * <p>
 * 全局统一返回结构,固定 code / msg / data / traceId 四字段。
 * 所有接口必须使用此返回体,禁止裸返回业务对象。
 * </p>
 *
 * @param <T> 返回数据泛型
 * @author develop-code
 * @since 1.0.0
 */
public class Result<T> implements Serializable {

    @Serial
    private static final long serialVersionUID = 1L;

    /** 返回码 */
    private Integer code;

    /** 返回消息 */
    private String msg;

    /** 返回数据 */
    private T data;

    /** 链路追踪ID */
    private String traceId;

    /**
     * 默认构造器
     */
    public Result() {
    }

    /**
     * 全参构造器
     *
     * @param code    返回码
     * @param msg     返回消息
     * @param data    返回数据
     * @param traceId 链路追踪ID
     */
    public Result(Integer code, String msg, T data, String traceId) {
        this.code = code;
        this.msg = msg;
        this.data = data;
        this.traceId = traceId;
    }

    /**
     * 成功返回(带数据)
     *
     * @param data 返回数据
     * @param T    返回数据泛型
     * @return 统一返回体
     */
    public static <T> Result<T> success(T data) {
        return new Result<>(ResultCodeEnum.SUCCESS.getCode(),
                ResultCodeEnum.SUCCESS.getMsg(), data,
                TraceIdHelper.getTraceId());
    }

    /**
     * 失败返回(自定义错误码和消息)
     *
     * @param code 错误码
     * @param msg  错误消息
     * @param T    返回数据泛型
     * @return 统一返回体
     */
    public static <T> Result<T> fail(Integer code, String msg) {
        return new Result<>(code, msg, null, TraceIdHelper.getTraceId());
    }

    /**
     * 获取返回码
     *
     * @return 返回码
     */
    public Integer getCode() {
        return code;
    }

    /**
     * 设置返回码
     *
     * @param code 返回码
     */
    public void setCode(Integer code) {
        this.code = code;
    }

    /**
     * 获取返回消息
     *
     * @return 返回消息
     */
    public String getMsg() {
        return msg;
    }

    /**
     * 设置返回消息
     *
     * @param msg 返回消息
     */
    public void setMsg(String msg) {
        this.msg = msg;
    }

    /**
     * 获取返回数据
     *
     * @return 返回数据
     */
    public T getData() {
        return data;
    }

    /**
     * 设置返回数据
     *
     * @param data 返回数据
     */
    public void setData(T data) {
        this.data = data;
    }

    /**
     * 获取链路追踪ID
     *
     * @return 链路追踪ID
     */
    public String getTraceId() {
        return traceId;
    }

    /**
     * 设置链路追踪ID
     *
     * @param traceId 链路追踪ID
     */
    public void setTraceId(String traceId) {
        this.traceId = traceId;
    }
}
5.2.2 GlobalExceptionHandler.java(全局异常处理)
java 复制代码
package com.develop.code.common.exception;

import java.util.stream.Collectors;

import jakarta.validation.ConstraintViolationException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

import com.develop.code.common.helper.TraceIdHelper;
import com.develop.code.common.result.Result;
import com.develop.code.common.result.ResultCodeEnum;

/**
 * 全局异常处理器
 * <p>
 * 统一捕获业务异常、参数校验异常、系统异常,
 * 关闭错误堆栈对外暴露,统一返回业务错误码。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(
            GlobalExceptionHandler.class);

    /**
     * 处理业务异常
     *
     * @param e 业务异常
     * @return 统一返回体
     */
    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        log.error("[业务异常] traceId={}, code={}, msg={}", e.getTraceId(),
                e.getCode(), e.getMessage(), e);
        return Result.fail(e.getCode(), e.getMsg());
    }

    /**
     * 处理参数校验异常
     *
     * @param e 参数校验异常
     * @return 统一返回体
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidException(
            MethodArgumentNotValidException e) {
        String errorMsg = e.getBindingResult().getFieldErrors().stream()
                .map(FieldError::getDefaultMessage)
                .collect(Collectors.joining("; "));
        log.warn("[参数校验失败] traceId={}, msg={}",
                TraceIdHelper.getTraceId(), errorMsg);
        return Result.fail(ResultCodeEnum.PARAM_ERROR.getCode(), errorMsg);
    }

    /**
     * 处理系统未知异常
     * <p>
     * 禁止对外暴露内部堆栈信息,统一返回"系统繁忙"。
     * </p>
     *
     * @param e 未知异常
     * @return 统一返回体
     */
    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("[系统异常] traceId={}", TraceIdHelper.getTraceId(), e);
        return Result.fail(ResultCodeEnum.SYSTEM_ERROR);
    }
}
5.2.3 AuditLogAspect.java(审计日志切面)
java 复制代码
package com.develop.code.common.aspect;

import java.time.LocalDateTime;
import java.util.Arrays;

import jakarta.servlet.http.HttpServletRequest;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import com.develop.code.common.constant.CommonConstant;
import com.develop.code.common.entity.AuditLogDO;
import com.develop.code.common.helper.TraceIdHelper;
import com.develop.code.common.service.AuditLogService;

/**
 * 审计日志切面
 * <p>
 * AOP拦截Controller层所有方法,记录操作人、操作时间、
 * 请求参数、响应数据、客户端IP、traceId、耗时。
 * 满足等保三级审计安全要求,日志持久化存储至少180天。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@Aspect
@Component
public class AuditLogAspect {

    private static final Logger log = LoggerFactory.getLogger(
            AuditLogAspect.class);

    private final AuditLogService auditLogService;

    /**
     * 构造器注入
     *
     * @param auditLogService 审计日志服务
     */
    public AuditLogAspect(AuditLogService auditLogService) {
        this.auditLogService = auditLogService;
    }

    /**
     * Controller层切点
     */
    @Pointcut("execution(* com.develop.code..controller..*.*(..))")
    public void controllerPointcut() {
    }

    /**
     * 环绕通知:记录审计日志
     *
     * @param joinPoint 连接点
     * @return 方法返回值
     * @throws Throwable 方法执行异常
     */
    @Around("controllerPointcut()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        String traceId = TraceIdHelper.getTraceId();

        HttpServletRequest request = getRequest();
        String clientIp = getClientIp(request);
        String reqMethod = request != null ? request.getMethod() : "";
        String reqUrl = request != null ? request.getRequestURI() : "";

        Object result = null;
        Integer isSuccess = CommonConstant.SUCCESS_FLAG;
        String errorMsg = "";

        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            isSuccess = CommonConstant.FAIL_FLAG;
            errorMsg = e.getMessage();
            throw e;
        } finally {
            long costTime = System.currentTimeMillis() - startTime;

            AuditLogDO auditLog = new AuditLogDO();
            auditLog.setTraceId(traceId);
            auditLog.setAction(joinPoint.getSignature().getName());
            auditLog.setModule(joinPoint.getTarget().getClass()
                    .getSimpleName());
            auditLog.setRequestParams(truncateParams(
                    Arrays.toString(joinPoint.getArgs())));
            auditLog.setClientIp(clientIp);
            auditLog.setReqMethod(reqMethod);
            auditLog.setReqUrl(reqUrl);
            auditLog.setCostTimeMs(costTime);
            auditLog.setIsSuccess(isSuccess);
            auditLog.setErrorMsg(errorMsg);
            auditLog.setCreatedBy("system");
            auditLog.setCreatedTime(LocalDateTime.now());

            auditLogService.recordAuditLog(auditLog);

            log.info("[审计日志] traceId={}, action={}, cost={}ms, success={}",
                    traceId, joinPoint.getSignature().getName(),
                    costTime, isSuccess);
        }

        return result;
    }

    /**
     * 获取当前HTTP请求
     *
     * @return HttpServletRequest
     */
    private HttpServletRequest getRequest() {
        ServletRequestAttributes attributes = (ServletRequestAttributes)
                RequestContextHolder.getRequestAttributes();
        return attributes != null ? attributes.getRequest() : null;
    }

    /**
     * 获取客户端IP
     *
     * @param request HTTP请求
     * @return 客户端IP
     */
    private String getClientIp(HttpServletRequest request) {
        if (request == null) {
            return "unknown";
        }
        String ip = request.getHeader("X-Forwarded-For");
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getHeader("X-Real-IP");
        }
        if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
            ip = request.getRemoteAddr();
        }
        return ip;
    }

    /**
     * 截断请求参数,防止日志过长
     *
     * @param params 原始参数字符串
     * @return 截断后的参数字符串
     */
    private String truncateParams(String params) {
        if (params == null) {
            return "";
        }
        return params.length() <= 500
                ? params : params.substring(0, 500) + "...";
    }
}
5.2.4 SecurityHelper.java(安全工具类 - 核心方法)
java 复制代码
package com.develop.code.common.helper;

import java.nio.charset.StandardCharsets;
import java.util.Base64;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.develop.code.common.constant.CommonConstant;

import at.favre.lib.crypto.bcrypt.BCrypt;

/**
 * 安全工具类
 * <p>
 * 提供AES加解密、BCrypt密码哈希验证、敏感数据脱敏功能。
 * 密钥从环境变量 APP_AES_KEY 读取,禁止硬编码。
 * 满足等保三级2.0数据安全要求。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
public final class SecurityHelper {

    private static final Logger log = LoggerFactory.getLogger(
            SecurityHelper.class);

    private static final String AES_ALGORITHM = "AES";

    private static final String AES_TRANSFORMATION = "AES/ECB/PKCS5Padding";

    private static final int AES_KEY_LENGTH = 16;

    /**
     * 私有构造器,禁止实例化
     */
    private SecurityHelper() {
    }

    /**
     * AES加密
     * <p>
     * 从环境变量读取密钥,明文经AES加密后Base64编码输出。
     * </p>
     *
     * @param plainText 待加密明文
     * @return Base64编码的密文
     * @throws RuntimeException 加密失败时抛出
     */
    public static String encrypt(String plainText) {
        try {
            SecretKeySpec keySpec = getKeySpec();
            Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            byte[] encrypted = cipher.doFinal(
                    plainText.getBytes(StandardCharsets.UTF_8));
            return Base64.getEncoder().encodeToString(encrypted);
        } catch (Exception e) {
            log.error("[AES加密失败] msg={}", e.getMessage(), e);
            throw new RuntimeException("AES加密失败", e);
        }
    }

    /**
     * AES解密
     *
     * @param cipherText Base64编码的密文
     * @return 解密后的明文
     * @throws RuntimeException 解密失败时抛出
     */
    public static String decrypt(String cipherText) {
        try {
            SecretKeySpec keySpec = getKeySpec();
            Cipher cipher = Cipher.getInstance(AES_TRANSFORMATION);
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            byte[] decoded = Base64.getDecoder().decode(cipherText);
            byte[] decrypted = cipher.doFinal(decoded);
            return new String(decrypted, StandardCharsets.UTF_8);
        } catch (Exception e) {
            log.error("[AES解密失败] msg={}", e.getMessage(), e);
            throw new RuntimeException("AES解密失败", e);
        }
    }

    /**
     * BCrypt加盐哈希密码
     * <p>
     * 禁止使用MD5简单哈希。
     * </p>
     *
     * @param plainPassword 明文密码
     * @return BCrypt哈希后的密码字符串
     */
    public static String hashPassword(String plainPassword) {
        return BCrypt.withDefaults().hashToString(12,
                plainPassword.toCharArray());
    }

    /**
     * BCrypt验证密码
     *
     * @param plainPassword  明文密码
     * @param hashedPassword BCrypt哈希密码
     * @return 验证通过返回true,否则返回false
     */
    public static boolean verifyPassword(String plainPassword,
                                          String hashedPassword) {
        try {
            BCrypt.Result result = BCrypt.verifyer().verify(
                    plainPassword.toCharArray(), hashedPassword);
            return result.verified;
        } catch (Exception e) {
            log.error("[密码验证异常] msg={}", e.getMessage(), e);
            return false;
        }
    }

    /**
     * 手机号脱敏
     * <p>
     * 13812345678 变为 138****5678
     * </p>
     *
     * @param phone 手机号
     * @return 脱敏后的手机号
     */
    public static String desensitizePhone(String phone) {
        if (phone == null || phone.length() < 7) {
            return phone;
        }
        return phone.substring(0, 3) + "****"
                + phone.substring(phone.length() - 4);
    }

    /**
     * 身份证号脱敏
     * <p>
     * 110101199001011234 变为 110***********1234
     * </p>
     *
     * @param idCard 身份证号
     * @return 脱敏后的身份证号
     */
    public static String desensitizeIdCard(String idCard) {
        if (idCard == null || idCard.length() < 10) {
            return idCard;
        }
        return idCard.substring(0, 3) + "***********"
                + idCard.substring(idCard.length() - 4);
    }

    /**
     * 获取AES密钥规格
     *
     * @return SecretKeySpec
     * @throws IllegalStateException 环境变量未配置时抛出
     */
    private static SecretKeySpec getKeySpec() {
        String key = System.getenv(CommonConstant.AES_KEY_ENV);
        if (key == null || key.isEmpty()) {
            throw new IllegalStateException(
                    "AES密钥环境变量未配置: " + CommonConstant.AES_KEY_ENV);
        }
        if (key.length() < AES_KEY_LENGTH) {
            key = cn.hutool.crypto.SecureUtil.md5(key)
                    .substring(0, AES_KEY_LENGTH);
        } else if (key.length() > AES_KEY_LENGTH) {
            key = key.substring(0, AES_KEY_LENGTH);
        }
        byte[] keyBytes = key.getBytes(StandardCharsets.UTF_8);
        return new SecretKeySpec(keyBytes, AES_ALGORITHM);
    }
}
5.2.5 TraceIdInterceptor.java(链路追踪拦截器)
java 复制代码
package com.develop.code.common.interceptor;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;

import com.develop.code.common.constant.CommonConstant;
import com.develop.code.common.helper.TraceIdHelper;

/**
 * 链路追踪拦截器
 * <p>
 * 在请求入口处统一设置traceId到MDC,
 * 保证全链路日志携带追踪标识。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
public class TraceIdInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(
            TraceIdInterceptor.class);

    /**
     * 请求前置处理:从请求头获取traceId,没有则生成
     *
     * @param request  HTTP请求
     * @param response HTTP响应
     * @param handler  处理器
     * @return 是否继续执行
     * @throws Exception 异常
     */
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {
        String traceId = request.getHeader(CommonConstant.TRACE_ID_HEADER);
        if (traceId == null || traceId.isEmpty()) {
            traceId = TraceIdHelper.generateTraceId();
        }
        TraceIdHelper.setTraceId(traceId);
        response.setHeader(CommonConstant.TRACE_ID_HEADER, traceId);
        log.info("[请求入口] traceId={}, method={}, uri={}",
                traceId, request.getMethod(), request.getRequestURI());
        return true;
    }

    /**
     * 请求完成后清除MDC中的traceId
     *
     * @param request  HTTP请求
     * @param response HTTP响应
     * @param handler  处理器
     * @param ex       异常
     * @throws Exception 异常
     */
    @Override
    public void afterCompletion(HttpServletRequest request,
                                HttpServletResponse response,
                                Object handler, Exception ex)
            throws Exception {
        TraceIdHelper.clearTraceId();
    }
}
5.2.6 CorsConfig.java(跨域安全配置)
java 复制代码
package com.develop.code.common.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 跨域配置类
 * <p>
 * 禁止通配符跨域,仅允许配置的指定域名访问。
 * 满足等保三级安全基线要求。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@Configuration
public class CorsConfig implements WebMvcConfigurer {

    /** 允许跨域的域名,从配置读取,逗号分隔 */
    @Value("${cors.allowed-origins:http://localhost:5173}")
    private String allowedOrigins;

    /**
     * 配置CORS跨域映射
     *
     * @param registry CORS注册器
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        String[] origins = allowedOrigins.split(",");
        registry.addMapping("/**")
                .allowedOrigins(origins)
                .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
                .allowedHeaders("Origin", "Content-Type", "Accept",
                        "Authorization", "X-Trace-Id")
                .exposedHeaders("X-Trace-Id")
                .allowCredentials(true)
                .maxAge(3600);
    }
}
5.2.7 RedisConfig.java(Redis序列化配置)
java 复制代码
package com.develop.code.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/**
 * Redis配置类
 * <p>
 * 配置RedisTemplate使用Jackson2JsonRedisSerializer序列化,
 * 支持Java 8时间类型。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@Configuration
public class RedisConfig {

    private final RedisConnectionFactory redisConnectionFactory;

    /**
     * 构造器注入
     *
     * @param redisConnectionFactory Redis连接工厂
     */
    public RedisConfig(RedisConnectionFactory redisConnectionFactory) {
        this.redisConnectionFactory = redisConnectionFactory;
    }

    /**
     * 配置RedisTemplate
     *
     * @return RedisTemplate实例
     */
    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);

        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer jsonSerializer =
                new GenericJackson2JsonRedisSerializer();

        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        template.afterPropertiesSet();
        return template;
    }
}
5.2.8 MyBatisPlusConfig.java(MyBatis-Plus配置)
java 复制代码
package com.develop.code.common.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;

/**
 * MyBatis-Plus配置类
 * <p>
 * 配置分页插件和乐观锁插件。
 * 全部SQL使用参数绑定,禁止字符串拼接。
 * </p>
 *
 * @author develop-code
 * @since 1.0.0
 */
@Configuration
public class MyBatisPlusConfig {

    /**
     * 配置MyBatis-Plus拦截器链
     *
     * @return MybatisPlusInterceptor实例
     */
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(
                new PaginationInnerInterceptor(DbType.MARIADB));
        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
        return interceptor;
    }
}

5.3 数据走向流程图(完整全链路)

#mermaid-svg-UwZkkfejwluo8sAS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-UwZkkfejwluo8sAS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-UwZkkfejwluo8sAS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-UwZkkfejwluo8sAS .error-icon{fill:#552222;}#mermaid-svg-UwZkkfejwluo8sAS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-UwZkkfejwluo8sAS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-UwZkkfejwluo8sAS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-UwZkkfejwluo8sAS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-UwZkkfejwluo8sAS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-UwZkkfejwluo8sAS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-UwZkkfejwluo8sAS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-UwZkkfejwluo8sAS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-UwZkkfejwluo8sAS .marker.cross{stroke:#333333;}#mermaid-svg-UwZkkfejwluo8sAS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-UwZkkfejwluo8sAS p{margin:0;}#mermaid-svg-UwZkkfejwluo8sAS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-UwZkkfejwluo8sAS .cluster-label text{fill:#333;}#mermaid-svg-UwZkkfejwluo8sAS .cluster-label span{color:#333;}#mermaid-svg-UwZkkfejwluo8sAS .cluster-label span p{background-color:transparent;}#mermaid-svg-UwZkkfejwluo8sAS .label text,#mermaid-svg-UwZkkfejwluo8sAS span{fill:#333;color:#333;}#mermaid-svg-UwZkkfejwluo8sAS .node rect,#mermaid-svg-UwZkkfejwluo8sAS .node circle,#mermaid-svg-UwZkkfejwluo8sAS .node ellipse,#mermaid-svg-UwZkkfejwluo8sAS .node polygon,#mermaid-svg-UwZkkfejwluo8sAS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-UwZkkfejwluo8sAS .rough-node .label text,#mermaid-svg-UwZkkfejwluo8sAS .node .label text,#mermaid-svg-UwZkkfejwluo8sAS .image-shape .label,#mermaid-svg-UwZkkfejwluo8sAS .icon-shape .label{text-anchor:middle;}#mermaid-svg-UwZkkfejwluo8sAS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-UwZkkfejwluo8sAS .rough-node .label,#mermaid-svg-UwZkkfejwluo8sAS .node .label,#mermaid-svg-UwZkkfejwluo8sAS .image-shape .label,#mermaid-svg-UwZkkfejwluo8sAS .icon-shape .label{text-align:center;}#mermaid-svg-UwZkkfejwluo8sAS .node.clickable{cursor:pointer;}#mermaid-svg-UwZkkfejwluo8sAS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-UwZkkfejwluo8sAS .arrowheadPath{fill:#333333;}#mermaid-svg-UwZkkfejwluo8sAS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-UwZkkfejwluo8sAS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-UwZkkfejwluo8sAS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UwZkkfejwluo8sAS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-UwZkkfejwluo8sAS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UwZkkfejwluo8sAS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-UwZkkfejwluo8sAS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-UwZkkfejwluo8sAS .cluster text{fill:#333;}#mermaid-svg-UwZkkfejwluo8sAS .cluster span{color:#333;}#mermaid-svg-UwZkkfejwluo8sAS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-UwZkkfejwluo8sAS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-UwZkkfejwluo8sAS rect.text{fill:none;stroke-width:0;}#mermaid-svg-UwZkkfejwluo8sAS .icon-shape,#mermaid-svg-UwZkkfejwluo8sAS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-UwZkkfejwluo8sAS .icon-shape p,#mermaid-svg-UwZkkfejwluo8sAS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-UwZkkfejwluo8sAS .icon-shape .label rect,#mermaid-svg-UwZkkfejwluo8sAS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-UwZkkfejwluo8sAS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-UwZkkfejwluo8sAS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-UwZkkfejwluo8sAS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} HTTP请求
TraceIdInterceptor

traceId注入MDC
CorsConfig

跨域白名单校验
ChatController

@Valid参数校验
AuditLogAspect

审计切面拦截
ChatService

业务逻辑处理
ChatClient

模型API调用
LoggingAdvisor

请求/响应日志
SecurityHelper

敏感数据脱敏
Result封装

code/msg/data/traceId
GlobalExceptionHandler

异常兜底
统一返回客户端

5.4 优劣势分析

维度 评价 说明
安全合规 优势 等保三级全项覆盖
审计能力 优势 AOP切面全量审计,180天留存
链路追踪 优势 traceId贯穿全链路
异常处理 优势 全局兜底,不暴露内部信息
数据安全 优势 AES加密+BCrypt+脱敏
开发周期 劣势 需1-2周完整搭建
运维复杂度 一般 需Redis+MariaDB基础设施

5.5 成本估算

成本项 月度费用(元) 说明
多模型 API Token 1000-3000 主+备模型
服务器(4C8G) 400 应用节点
MariaDB(2C4G) 300 审计日志存储
Redis(1G) 100 缓存+健康状态
日志存储(50G) 50 180天日志留存
人力维护 2 人天 全链路运维
月度总成本 1850-3850 适合生产级部署

5.6 等保三级合规要点

等保要求 实现方式 对应代码
输入校验防注入 @NotBlank/@Size/@Valid ChatRequestDTO
SQL参数绑定 MyBatis-Plus参数绑定 MyBatisPlusConfig
敏感字段加密 AES对称加密 SecurityHelper.encrypt
密码安全存储 BCrypt加盐哈希 SecurityHelper.hashPassword
密钥禁止硬编码 环境变量注入 application.yml
操作审计日志 AOP切面+异步写入 AuditLogAspect
日志180天留存 持久化存储MariaDB AuditLogServiceImpl
日志脱敏 手机号/身份证掩码 SecurityHelper.desensitize
跨域白名单 指定域名,禁止通配符 CorsConfig
错误信息屏蔽 全局异常统一返回 GlobalExceptionHandler
链路追踪 traceId贯穿全链路 TraceIdInterceptor

第六部分:三方案横向对比

6.1 对比表

对比维度 方案一 基础版 方案二 进阶版 方案三 高级版
核心能力 ChatClient+Advisor 多模型适配层 全链路生产级
性能 中等 中等 优秀
月度成本 1000-3200元 1500-4200元 1850-3850元
复杂度
模型支持 单一模型 多模型+降级 多模型+降级+健康检查
异常处理 基本捕获 降级兜底 全局异常+审计
安全合规 基础 一般 等保三级全覆盖
审计能力 AOP全量审计
链路追踪 Advisor日志 Advisor日志 traceId全链路
适用场景 快速验证 成长期生产 高可用生产
开发周期 1-2天 3-5天 1-2周
SME推荐度 推荐 推荐 强烈推荐

6.2 对比图

#mermaid-svg-gSq2c5hQ6te2G3fz{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gSq2c5hQ6te2G3fz .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gSq2c5hQ6te2G3fz .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gSq2c5hQ6te2G3fz .error-icon{fill:#552222;}#mermaid-svg-gSq2c5hQ6te2G3fz .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-gSq2c5hQ6te2G3fz .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gSq2c5hQ6te2G3fz .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gSq2c5hQ6te2G3fz .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gSq2c5hQ6te2G3fz .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gSq2c5hQ6te2G3fz .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gSq2c5hQ6te2G3fz .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gSq2c5hQ6te2G3fz .marker{fill:#333333;stroke:#333333;}#mermaid-svg-gSq2c5hQ6te2G3fz .marker.cross{stroke:#333333;}#mermaid-svg-gSq2c5hQ6te2G3fz svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-gSq2c5hQ6te2G3fz p{margin:0;}#mermaid-svg-gSq2c5hQ6te2G3fz .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-gSq2c5hQ6te2G3fz .cluster-label text{fill:#333;}#mermaid-svg-gSq2c5hQ6te2G3fz .cluster-label span{color:#333;}#mermaid-svg-gSq2c5hQ6te2G3fz .cluster-label span p{background-color:transparent;}#mermaid-svg-gSq2c5hQ6te2G3fz .label text,#mermaid-svg-gSq2c5hQ6te2G3fz span{fill:#333;color:#333;}#mermaid-svg-gSq2c5hQ6te2G3fz .node rect,#mermaid-svg-gSq2c5hQ6te2G3fz .node circle,#mermaid-svg-gSq2c5hQ6te2G3fz .node ellipse,#mermaid-svg-gSq2c5hQ6te2G3fz .node polygon,#mermaid-svg-gSq2c5hQ6te2G3fz .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-gSq2c5hQ6te2G3fz .rough-node .label text,#mermaid-svg-gSq2c5hQ6te2G3fz .node .label text,#mermaid-svg-gSq2c5hQ6te2G3fz .image-shape .label,#mermaid-svg-gSq2c5hQ6te2G3fz .icon-shape .label{text-anchor:middle;}#mermaid-svg-gSq2c5hQ6te2G3fz .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gSq2c5hQ6te2G3fz .rough-node .label,#mermaid-svg-gSq2c5hQ6te2G3fz .node .label,#mermaid-svg-gSq2c5hQ6te2G3fz .image-shape .label,#mermaid-svg-gSq2c5hQ6te2G3fz .icon-shape .label{text-align:center;}#mermaid-svg-gSq2c5hQ6te2G3fz .node.clickable{cursor:pointer;}#mermaid-svg-gSq2c5hQ6te2G3fz .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-gSq2c5hQ6te2G3fz .arrowheadPath{fill:#333333;}#mermaid-svg-gSq2c5hQ6te2G3fz .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-gSq2c5hQ6te2G3fz .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-gSq2c5hQ6te2G3fz .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gSq2c5hQ6te2G3fz .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-gSq2c5hQ6te2G3fz .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gSq2c5hQ6te2G3fz .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-gSq2c5hQ6te2G3fz .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-gSq2c5hQ6te2G3fz .cluster text{fill:#333;}#mermaid-svg-gSq2c5hQ6te2G3fz .cluster span{color:#333;}#mermaid-svg-gSq2c5hQ6te2G3fz div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gSq2c5hQ6te2G3fz .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gSq2c5hQ6te2G3fz rect.text{fill:none;stroke-width:0;}#mermaid-svg-gSq2c5hQ6te2G3fz .icon-shape,#mermaid-svg-gSq2c5hQ6te2G3fz .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-gSq2c5hQ6te2G3fz .icon-shape p,#mermaid-svg-gSq2c5hQ6te2G3fz .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-gSq2c5hQ6te2G3fz .icon-shape .label rect,#mermaid-svg-gSq2c5hQ6te2G3fz .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-gSq2c5hQ6te2G3fz .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gSq2c5hQ6te2G3fz .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gSq2c5hQ6te2G3fz :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 增强模型能力
增强安全合规
高级版
全链路架构
审计+加密
等保合规
进阶版
多模型适配
动态选择
健康检查
基础版
ChatClient
单一模型
基础日志


第七部分:方案选型决策树

7.1 决策树

#mermaid-svg-bPFhnXUWvc2bx9Go{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bPFhnXUWvc2bx9Go .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bPFhnXUWvc2bx9Go .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bPFhnXUWvc2bx9Go .error-icon{fill:#552222;}#mermaid-svg-bPFhnXUWvc2bx9Go .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-bPFhnXUWvc2bx9Go .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bPFhnXUWvc2bx9Go .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bPFhnXUWvc2bx9Go .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bPFhnXUWvc2bx9Go .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bPFhnXUWvc2bx9Go .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bPFhnXUWvc2bx9Go .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bPFhnXUWvc2bx9Go .marker{fill:#333333;stroke:#333333;}#mermaid-svg-bPFhnXUWvc2bx9Go .marker.cross{stroke:#333333;}#mermaid-svg-bPFhnXUWvc2bx9Go svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-bPFhnXUWvc2bx9Go p{margin:0;}#mermaid-svg-bPFhnXUWvc2bx9Go .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-bPFhnXUWvc2bx9Go .cluster-label text{fill:#333;}#mermaid-svg-bPFhnXUWvc2bx9Go .cluster-label span{color:#333;}#mermaid-svg-bPFhnXUWvc2bx9Go .cluster-label span p{background-color:transparent;}#mermaid-svg-bPFhnXUWvc2bx9Go .label text,#mermaid-svg-bPFhnXUWvc2bx9Go span{fill:#333;color:#333;}#mermaid-svg-bPFhnXUWvc2bx9Go .node rect,#mermaid-svg-bPFhnXUWvc2bx9Go .node circle,#mermaid-svg-bPFhnXUWvc2bx9Go .node ellipse,#mermaid-svg-bPFhnXUWvc2bx9Go .node polygon,#mermaid-svg-bPFhnXUWvc2bx9Go .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-bPFhnXUWvc2bx9Go .rough-node .label text,#mermaid-svg-bPFhnXUWvc2bx9Go .node .label text,#mermaid-svg-bPFhnXUWvc2bx9Go .image-shape .label,#mermaid-svg-bPFhnXUWvc2bx9Go .icon-shape .label{text-anchor:middle;}#mermaid-svg-bPFhnXUWvc2bx9Go .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bPFhnXUWvc2bx9Go .rough-node .label,#mermaid-svg-bPFhnXUWvc2bx9Go .node .label,#mermaid-svg-bPFhnXUWvc2bx9Go .image-shape .label,#mermaid-svg-bPFhnXUWvc2bx9Go .icon-shape .label{text-align:center;}#mermaid-svg-bPFhnXUWvc2bx9Go .node.clickable{cursor:pointer;}#mermaid-svg-bPFhnXUWvc2bx9Go .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-bPFhnXUWvc2bx9Go .arrowheadPath{fill:#333333;}#mermaid-svg-bPFhnXUWvc2bx9Go .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-bPFhnXUWvc2bx9Go .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-bPFhnXUWvc2bx9Go .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bPFhnXUWvc2bx9Go .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-bPFhnXUWvc2bx9Go .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bPFhnXUWvc2bx9Go .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-bPFhnXUWvc2bx9Go .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-bPFhnXUWvc2bx9Go .cluster text{fill:#333;}#mermaid-svg-bPFhnXUWvc2bx9Go .cluster span{color:#333;}#mermaid-svg-bPFhnXUWvc2bx9Go div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bPFhnXUWvc2bx9Go .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bPFhnXUWvc2bx9Go rect.text{fill:none;stroke-width:0;}#mermaid-svg-bPFhnXUWvc2bx9Go .icon-shape,#mermaid-svg-bPFhnXUWvc2bx9Go .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-bPFhnXUWvc2bx9Go .icon-shape p,#mermaid-svg-bPFhnXUWvc2bx9Go .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-bPFhnXUWvc2bx9Go .icon-shape .label rect,#mermaid-svg-bPFhnXUWvc2bx9Go .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-bPFhnXUWvc2bx9Go .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bPFhnXUWvc2bx9Go .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bPFhnXUWvc2bx9Go :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 是
≤5人
5-20人
>20人








需要接入AI能力?
团队规模?
预算是否敏感?
是否需要多模型?
等保合规要求?
方案一 基础版
方案二 进阶版
是否生产级别?
方案三 高级版

7.2 决策条件说明

条件 含义 判断依据
团队规模 开发团队人数 ≤5人选基础版,5-20人选进阶版,>20人选高级版
预算敏感 月度预算<3000元 预算紧张时优先基础版快速验证
多模型需求 是否需要切换/降级模型 业务需要多模型支持时选进阶版
等保合规 是否有等保三级要求 有合规要求必须选高级版
生产级别 是否面向真实用户 生产环境建议至少进阶版起步

通用建议:从基础版起步验证业务可行性,验证通过后升级到进阶版支撑生产环境,有合规要求时升级到高级版。


第八部分:生产落地清单

8.1 部署配置清单(环境变量列表)

环境变量名 用途 示例值 必填
OPENAI_API_KEY OpenAI API密钥 sk-xxxx
OPENAI_BASE_URL OpenAI API地址 https://api.openai.com
ANTHROPIC_API_KEY Anthropic API密钥 sk-ant-xxxx 进阶版必填
QWEN_API_KEY 通义千问API密钥 sk-xxxx 进阶版必填
APP_AES_KEY AES加密密钥 32位随机字符串 高级版必填
SPRING_REDIS_HOST Redis地址 127.0.0.1 高级版必填
SPRING_REDIS_PORT Redis端口 6379 高级版必填
SPRING_REDIS_PASSWORD Redis密码 强密码 高级版必填
SPRING_DATASOURCE_URL MariaDB连接地址 jdbc:mariadb://... 高级版必填
SPRING_DATASOURCE_USERNAME 数据库用户名 app_user 高级版必填
SPRING_DATASOURCE_PASSWORD 数据库密码 强密码 高级版必填

8.2 监控指标清单(Prometheus 指标)

指标名 类型 说明 告警阈值
ai_chat_request_total Counter AI聊天请求总数 -
ai_chat_request_duration Histogram 请求耗时分布 P99 > 10s
ai_chat_error_total Counter AI调用错误总数 5分钟>10次
ai_model_health_status Gauge 模型健康状态 值=0
ai_token_usage_total Counter Token使用总量 日用量超预算80%
ai_fallback_trigger_total Counter 降级触发次数 1小时>5次
jvm_threads_live_threads Gauge JVM活跃线程数 >300
jvm_memory_used_bytes Gauge JVM内存使用 >80%

8.3 故障排查清单

问题现象 可能原因 解决方案
接口超时无响应 模型API延迟+无超时设置 设置connectTimeout=5s, readTimeout=30s
线程池耗尽 AI调用阻塞HTTP线程 使用独立线程池或虚拟线程
频繁触发降级 主模型服务不稳定 检查模型健康状态,联系提供商
Token费用暴涨 无预算控制 引入限流+Token预算控制
审计日志写入失败 MariaDB连接异常 检查数据库连接池配置
traceId丢失 拦截器未注册 检查WebMvcConfig注册TraceIdInterceptor
跨域请求被拒 域名未配置白名单 在yml中添加cors.allowed-origins
AES加密异常 环境变量未配置 检查APP_AES_KEY环境变量

8.4 等保三级自查清单(本篇相关项)

检查项 要求 状态
接口入参校验 @NotBlank/@Size/@Valid 已覆盖
SQL参数绑定 MyBatis-Plus,禁止拼接 已覆盖
敏感字段加密 AES对称加密 已覆盖
密码哈希存储 BCrypt加盐 已覆盖
密钥环境变量 禁止硬编码 已覆盖
操作审计日志 AOP切面记录 已覆盖
日志180天留存 MariaDB持久化 已覆盖
日志敏感脱敏 手机号/身份证掩码 已覆盖
跨域白名单 指定域名 已覆盖
错误信息屏蔽 全局异常处理 已覆盖
链路追踪 traceId全链路 已覆盖
逻辑删除 is_deleted字段 待后续文章覆盖

8.5 本篇总结与下篇预告

本篇总结

本文从 Java 开发者面对 AI 浪潮的痛点出发,分析了"框架选择困难、架构设计空白、生产意识缺失"三大根因,给出了从基础版到高级版的三套递进方案:

  • 基础版:ChatClient + Advisors API,1-2天快速搭建可运行的AI聊天服务
  • 进阶版:多模型适配层,支持OpenAI/Anthropic/通义千问动态切换和自动降级
  • 高级版:全链路生产级架构,覆盖统一返回体、全局异常、审计日志、安全加密、链路追踪、跨域安全

所有代码遵循阿里巴巴Java开发手册(嵩山版)强制条款,满足等保三级2.0应用安全要求。代码位于 com-common 模块,是后续 29 篇文章的公共基础设施。

下篇预告

第 02 篇《成本黑洞终结者:AI 应用 Token 费用暴涨三套止血方案》将聚焦 AI 应用的成本控制问题,从 Redis 滑动窗口预算控制、Milvus 语义缓存命中、多模型智能路由三个维度,给出 Token 费用暴涨的止血方案。


本文代码已归档至 spring-ai-develop-csdn/code/com-common 模块,mvn clean compile 编译通过。

系列共 30 篇,持续更新中。

相关推荐
白仑色1 小时前
Spring Boot 从切面统一控制事务
java·spring boot·aop·spring事务
杨_晨1 小时前
LLM输出康熙部首冲突
数据库·python·mysql·ai
Flynt1 小时前
Java并行流,让我debug了一整天
java
DFT计算杂谈1 小时前
FeSe超薄膜在CaF2衬底上的电子结构DFT研究
java·服务器·前端
circuitsosk1 小时前
长文本与高并发下的Token“瘦身”策略:Prompt压缩与上下文窗口优化
java·前端·python·prompt·上下文窗口·token优化
岁岁养乐多1 小时前
LangChain4j 工厂模式
java·开发语言
小小小米粒2 小时前
idea常用搭配
java
吃饱了得干活2 小时前
Java Map 核心原理:从数据结构到 put/get 执行,一篇彻底讲透
java·后端
晚安code2 小时前
Java并发集合详解:从HashMap到ConcurrentHashMap
java