ysyRAG - 为什么需要 AI 基础设施层

infra-ai 模块的定位

infra-ai 是业务层和模型供应商之间的中间层

业务层只依赖 infra-ai 暴露的三个接口(LLMServiceEmbeddingServiceRerankService),完全不感知具体供应商。供应商是谁、优先级怎么排、挂了怎么切——这些全部由 infra-ai 内部处理

一些配置

供应商配置

providers 下面的每个条目定义了一个供应商的连接信息:

  • url:供应商的基础 URL
  • api-key:认证密钥(Ollama 不需要,所以没配)
  • endpoints:按能力类型映射的端点路径

为什么要把端点路径放在配置里?因为不同供应商的端点路径不同

模型候选配置

chatembeddingrerank 三个模型组各自维护一个候选列表

字段 含义 示例
id 唯一标识,用于路由和日志 qwen-plus
provider 关联到哪个供应商 bailian
model 模型名称,传给供应商 API qwen-plus-latest
priority 优先级,数字越小越优先 1
enabled 是否启用(默认 true true
dimension 向量维度(Embedding 专用) 1536
supports-thinking 是否支持深度思考 false
url 模型级 URL 覆盖(可选) ——

重点是 priority 机制。同一种能力下可以配置多个候选,路由时按优先级从小到大排序。拿 Chat 模型组来看,百炼 qwen3-max 是默认模型,如果默认模型失败,继续优先级 0 的硅基流动 GLM-4.7 排第一,失败了自动切换到优先级 1 的百炼 qwen-plus,再不行还有优先级 2 的本地 Ollama qwen3:8b-fp16 依次兜底

三层接口设计

业务层接口

LLM 对话接口 LLMService 支持同步调用 chat() 和流式调用 streamChat()。流式调用返回一个 StreamCancellationHandle,业务层可以随时通过 handle.cancel() 取消正在进行的生成。

向量化接口 EmbeddingService 支持单条 embed() 和批量 embedBatch(),还提供 dimension() 方法返回向量维度,用于向量库 Schema 定义。

重排序接口 RerankService 最简单,就一个 rerank() 方法,传入 query 和候选文档列表,返回精排后的 topN 结果

供应商接口

和业务层接口相比,供应商接口有两个明显的区别:

第一,多了一个 provider() 方法,返回供应商标识(比如 "bailian""siliconflow""ollama")。这个方法用于注册——路由服务在启动时会收集所有供应商客户端,按 provider() 建立索引,调用时根据候选模型的 provider 字段查找对应的客户端。

第二,多了一个 ModelTarget 参数。ModelTarget 是一个 record,把候选配置和供应商配置打包在一起

路由核心:四个组件

1. ModelTarget——调用目标

ModelTarget 本身没有“执行过程”,它是模型路由链路里的一个只读数据载体,作用是把一次可调用的模型目标完整打包起来:里面包含模型唯一标识 id、模型候选配置 candidate,以及这个模型所属供应商的配置 provider;上层像 ModelSelector 会先根据能力、优先级、是否支持 thinking 等条件挑出一批候选模型,再把每个候选模型和对应的供应商配置组装成 ModelTarget,后续 RoutingLLMServiceModelRoutingExecutor、各类 ChatClient 就不需要再分别查“模型参数”和“供应商地址/API Key”了,而是直接拿这个统一对象完成路由、调用、失败切换和健康检查,所以它的核心意义不是干活,而是作为模型调用流程中的标准目标描述对象,把“选中了哪个模型、它属于哪个 provider、该怎么调它”一次性封装好并向下游传递

2. ModelSelector——选谁

ModelSelector 可以理解成“按照当前任务需求,从配置里挑出一批现在可用、顺序正确的模型目标”:当上层要发起聊天、EmbeddingRerank 时,它先从 AIModelProperties 里拿到对应的模型组配置;如果是聊天,还会根据是否开启 deepThinking 决定优先使用 deepThinkingModel 还是 defaultModel 作为首选模型;接着它会对候选模型列表做过滤,去掉被禁用的模型,以及在深度思考模式下不支持 thinking 的模型,然后按“是否首选模型优先、priority 数值优先、id 字典序”排序;排好序后,它再逐个把候选模型和对应的 provider 配置组装成 ModelTarget,同时跳过健康状态已经不可用的模型,以及 provider 配置缺失的模型,最后返回一组已经可直接用于路由调用的 ModelTarget 列表,供后面的 RoutingLLMService 按顺序尝试调用和失败切换

3. ModelHealthStore——能不能调用

ModelHealthStore 本质上是在每个模型上实现一个轻量级熔断器:它用一个 ConcurrentHashMap 按模型 id 记录健康状态,状态分为 CLOSED、OPEN、HALF_OPEN;正常情况下模型处于 CLOSED,请求可以直接通过,调用成功时 markSuccess 会把失败次数清零并恢复为关闭状态;如果连续失败次数达到配置里的阈值,markFailure 就会把模型切到 OPEN,并设置一个 openUntil 时间,在这段时间内 isUnavailableallowCall 都会把它视为不可用,路由层会跳过它;等熔断时间过了以后,下一次 allowCall 不会立刻完全恢复,而是把模型切到 HALF_OPEN只放行一个探测请求,并通过 halfOpenInFlight 防止同时有多个探测请求打进去;如果这个探测请求成功,markSuccess 会把模型恢复成 CLOSED,如果失败,markFailure 会立刻重新打回 OPEN 并再次进入熔断等待期,所以作用就是用“失败计数 -> 打开熔断 -> 半开试探 -> 成功恢复或失败再熔断”这一套状态流转,帮助模型路由层自动避开故障模型

4. ModelRoutingExecutor——怎么调用

ModelRoutingExecutor 就是把“按顺序尝试多个候选模型并自动失败切换”这件事统一封装起来:调用开始时它先拿到某种能力对应的候选 ModelTarget 列表,比如 ChatEmbeddingRerank,如果列表为空就直接抛错;然后它按顺序遍历每个目标模型,先通过 clientResolver 找到对应 provider 的客户端实现,再用 ModelHealthStore.allowCall 判断这个模型当前是否允许调用,如果客户端不存在或模型处于熔断期就跳过;一旦允许调用,它就执行 caller.call(client, target) 发起真正的远程请求,调用成功后立刻用 markSuccess 把模型健康状态恢复正常并返回结果,调用失败则记录异常、用 markFailure 更新该模型的健康状态,并打印日志后切到下一个候选模型继续尝试;如果所有模型都失败,最后再统一抛出一个 RemoteException,说明这一类能力的所有候选模型都不可用了,所以这个类本质上就是模型路由层里的“顺序重试 + 熔断联动 + fallback 切换”的执行器

5. 四个组件如何协作

  1. 业务层调用 LLMService.chat(request)

  2. RoutingLLMService 调用 ModelSelector.selectChatCandidates() 获取候选列表,返回的就是一组 ModelTarget

  3. RoutingLLMService 把候选列表传给 ModelRoutingExecutor.executeWithFallback()

  4. 执行器遍历候选列表,对每个 ModelTarget

    • 调用 ModelHealthStore.allowCall() 检查熔断器是否放行
    • 如果放行,通过 ModelCaller 调用对应的 ChatClient
    • 成功则 markSuccess() 并返回结果
    • 失败则 markFailure() 并切换到下一个候选
  5. 所有候选都失败,抛出 RemoteException

ModelTarget 是数据载体,ModelSelector 负责选谁,ModelHealthStore 负责判断能不能调,ModelRoutingExecutor 负责执行和切换。四个组件各有分工,组合起来就是一个完整的带故障转移的模型调用流程