AI基础设施层宏观设计
前面的 RAG 理论系列和知识库实战系列,我们从 RAG 的概念讲到了评估优化,从文件上传讲到了分块存储。到这里,你应该已经清楚一个 RAG 系统的完整链路:用户提问 → 检索知识库 → 重排序 → 喂给大模型生成回答。
但有一个问题一直被我们当作黑盒处理——模型调用。
前面的文章里,每次需要调大模型的时候,我们都是直接写 OkHttp 请求,往百炼或者硅基流动发一个 HTTP 调用,拿到结果就完事。这在教学环境里没问题,但放到 Ragent 这样一个需要同时支持百炼、硅基流动、Ollama 等多个供应商,并且覆盖 Chat、Embedding、Rerank 三种模型能力的项目里,事情就没这么简单了。
这些模型调用在项目中到底是怎么组织的?直接在 Service 里写 OkHttp 调用?如果百炼挂了怎么办?如果要加一个新的供应商怎么办?
从这篇开始,我们进入大模型调度引擎实战系列。这个系列会逐篇拆解 Ragent 项目的 infra-ai 模块——它是整个项目的 AI 基础设施层,负责屏蔽供应商差异、实现多模型路由和故障转移。这一篇先从宏观视角讲清楚它的整体设计 。
为什么需要 AI 基础设施层
1. 没有 infra 层的世界
假设你在做一个电商客服知识库系统。产品需求很明确:用户提问时,先调 Embedding 模型把问题向量化,再去向量数据库检索相关文档,用 Reranker 精排一遍,最后把 chunk 喂给 Chat 模型生成回答。
一开始,你选了百炼作为模型供应商,直接在业务 Service 里写调用代码:
// ChatService.java —— 直接调百炼 Chat API
public String chat(String prompt) {
JsonObject body = new JsonObject();
body.addProperty("model", "qwen-plus");
body.add("messages", buildMessages(prompt));
Request request = new Request.Builder()
.url("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions")
.addHeader("Authorization", "Bearer " + BAILIAN_API_KEY)
.post(RequestBody.create(body.toString(), JSON))
.build();
Response response = httpClient.newCall(request).execute();
// ... 解析响应
}
能跑,没毛病。Embedding 调用也差不多,换个 URL 和请求格式就行。
然后问题来了。
场景一:百炼 API 挂了。 某天下午百炼服务抖了一下,返回 500 错误。你的整个客服系统直接瘫痪——因为所有对话都走百炼,没有备选方案。老板问你为什么系统挂了,你说供应商挂了。老板说那你怎么不做个备份?
场景二:加一个硅基流动做备份。 老板的要求合理,你开始加硅基流动。但硅基流动的 API 地址不一样(https://api.siliconflow.cn/v1/chat/completions),认证 方式一样但模型名不同(deepseek-ai/DeepSeek-V3)。你在 ChatService 里加了一堆 if-else:
public String chat(String prompt) {
if ("bailian".equals(currentProvider)) {
// 百炼的 URL、Header、模型名
} else if ("siliconflow".equals(currentProvider)) {
// 硅基流动的 URL、Header、模型名
}
// 失败了还要 try-catch 切换到另一个供应商...
}
场景三:再加一个 Ollama 做本地部署。 测试环境不想烧钱调云端 API,装了一个 Ollama 跑本地模型。但 Ollama 不需要 API Key,端点路径也不一样(http://localhost:11434/v1/chat/completions)。又是一套 if-else。
场景四:Embedding 和 Rerank 也要同样的逻辑。 你发现 EmbeddingService 也需要做供应商切换和容错,RerankService 也要。三个 Service 里面,每个都有一堆重复的 HTTP 调用代码、供应商判断逻辑、错误处理代码。
到这里,代码已经变成了这个样子:三个 Service 里散落着三个供应商的 if-else 判断,每个 Service 都在重复做 HTTP 请求构建、响应解析、错误处理。供应商的 URL、API Key、模型名硬编码在代码里。想加个新供应商?每个 Service 都得改。想调整优先级?改代码重新部署。
这就是没有基础设施层的代价。
2. 我们需要什么
从上面的反面例子里,可以提炼出四个核心需求:
统一接口,屏蔽差异。 业务层不应该关心当前调的是百炼还是硅基流动,更不应该知道各家 API 的请求格式有什么区别。业务层只需要说我要做一次 Chat 调用,传入 Prompt,拿到结果。
多模型路由与故障转移。 同一种能力(比如 Chat)可以配置多个候选模型,按优先级排序。高优先级的模型挂了,自动切换到下一个,业务层完全无感知。
配置驱动,零代码切换。 加一个新供应商、调整模型优先级、切换默认模型——这些操作不应该改代码,改配置文件就够了。
新供应商可插拔扩展。 哪天 要接入一个 vLLM 私有部署的推理服务,只需要实现一个客户端类,不需要动任何已有代码。
这就是 AI 基础设施层要解决的问题。
3. infra-ai 模块的定位
用一句话概括:infra-ai 是业务层和模型供应商之间的中间层。
业务层只依赖 infra-ai 暴露的三个接口(LLMService、EmbeddingService、RerankService),完全不感知具体供应商。供应商是谁、优先级怎么排、挂了怎么切——这些全部由 infra-ai 内部处理。
整体架构总览
1. 模块包结构
infra-ai 模块共有 9 个包,每个包有明确的职责边界:
| 包名 | 职责 | 核心类 |
|---|---|---|
config | 配置根 | AIModelProperties——将 YAML 配置绑定为类型安全的 Java 对象 |
enums | 类型词汇表 | ModelProvider(供应商枚举)、ModelCapability(能力枚举) |
model | 路由核心 | ModelSelector(选谁)、ModelHealthStore(能不能调)、ModelRoutingExecutor(怎么调)、ModelTarget(调用目标)、ModelCaller(函数式调用接口) |
http | HTTP 基础设施 | ModelUrlResolver(URL 解析)、HttpResponseHelper(响应工具)、ModelClientException(统一异常)、ModelClientErrorType(错误分类)、HttpMediaTypes(常量) |
chat | LLM 对话子系统 | LLMService(业务接口)、ChatClient(供应商接口)、AbstractOpenAIStyleChatClient(模板方法基类)、三个供应商实现、RoutingLLMService(路由服务)、流式相关组件 |
embedding | 向量化子系统 | EmbeddingService(业务接口)、EmbeddingClient(供应商接口)、两个供应商实现、RoutingEmbeddingService(路由服务) |
rerank | 重排序子系统 | RerankService(业务接口)、RerankClient(供应商接口)、BaiLianRerankClient(百炼实现)、NoopRerankClient(空对象实现)、RoutingRerankService(路由服务) |
token | Token 估算 | TokenCounterService(接口)、HeuristicTokenCounterService(启发式实现) |
util | 工具类 | LLMResponseCleaner(清理大模型响应中的 Markdown 代码围栏) |
整个模块约 40 个类,代码量不大,但设计上很紧凑。9 个包可以分成三层来理解:
- 底层基础设施:
config、enums、http、token、util——提供配置、常量、HTTP 工具等基础能力 - 路由核心:
model——提供模型选择、健康检查、故障转移执行器,是整个模块的骨架 - 能力子系统:
chat、embedding、rerank——三条并行的业务线,每条线都建立在路由核心之上
2. 三种能力的并行结构
Chat、Embedding、Rerank 三条线的结构高度一致,都遵循同样的三层模式:
关键点在于:三种能力子系统共享同一套路由核心(ModelSelector、ModelHealthStore、ModelRoutingExecutor)。这意味着路由逻辑、熔断逻辑、故障转移逻辑只需要写一次,三种能力都能复用。