Mobile wallpaper 1Mobile wallpaper 2Mobile wallpaper 3Mobile wallpaper 4
1944 字
10 分钟
花了一周重构 AI 网关层后,新增供应商只需写 3 行代码

最开始的样子#

面面通最开始接 AI 的方式,用一个字形容就是——

每个 Service 各自 new 一个 RestTemplate,拼 JSON 请求体,解析返回的 JSON,如果抛异常了各自 try-catch。

代码大概长这样:

// 最初的写法:InterviewService 里
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + apiKey);
// ... 拼消息体、调 API、解析 response、处理异常

这段代码在 InterviewService 里有,在 PolishService 里又写了一遍,在 ResumeAnalysisService 里再写一遍。

7 个 Service,7 份几乎一样的代码。

当需求只是”调 DeepSeek”的时候,勉强能跑。但当需求变成:

  • 用户想用自己的 API Key 怎么办?
  • 有人想用通义千问,有人想用其他模型怎么办?
  • 怎么限制每天调用次数?
  • 怎么统计每个用户花了多少 token?

每个需求都要去改 7 个 Service。改漏了就是 bug。

于是决定重构。


目标:再新增一个 Service 时,不需要关心 AI 怎么调#

我的目标很简单:后面再写一个 Service,它只需要说「我要调 AI」,不需要关心用哪个模型、用谁的 Key、扣不扣配额。

最终落地的就是 AiGateway 网关层。它不是为了炫设计模式,而是为了把”选供应商、选 Key、选模型、扣配额”这些脏活集中到一个地方。

AiGateway 架构


第一步:抽接口#

不管 DeepSeek 还是通义千问,做的事情都是同一个:你发一段文本,它返回一段文本。有的支持流式,有的不支持。

所以接口只有 3 个方法:

public interface ProviderAdapter {
String name(); // 供应商名称
AiResponse chat(AiRequest request, String apiKey); // 同步
void streamChat(AiRequest request, String apiKey, AiStreamHandler handler); // 流式
}

18 行。不多不少。

然后写一个 OpenAI 兼容适配器——因为 DeepSeek、通义千问、豆包、智谱这类供应商都可以按类似的 chat/completions 结构接进来:

public class OpenAiCompatibleAdapter implements ProviderAdapter {
private final ProviderConfig config;
public AiResponse chat(AiRequest request, String apiKey) {
// 拼 Authorization: Bearer + apiKey
// 拼 {"model":"...", "messages":[...]}
// 调 config.endpoint()
// 解析 {"choices":[{"message":{"content":"..."}}]}
// 返回 AiResponse
}
}

以后想加一个新供应商,只要它也兼容这套格式,基本就是加一份配置、一个 Adapter Bean、注册到 Map 里。

比如现在的 AiProviderConfig 里已经有 DeepSeek、Qwen、豆包和智谱。再加一个兼容供应商,核心变化大概就是这样:

ProviderConfig kimi = new ProviderConfig("kimi", endpoint, model, List.of("moonshot-v1-8k"));
OpenAiCompatibleAdapter kimiAdapter = new OpenAiCompatibleAdapter(kimi);
adapters.put("kimi", kimiAdapter);

当然,真实项目里会写成 Spring Bean,不会直接塞在方法里。但意思就是这个:只要接口长得像,业务 Service 完全不用知道多了一个供应商。

不兼容的?自己实现 ProviderAdapter,代价也不大。


第二步:注册表管理#

供应商统一注册到一个注册表里:

public class ProviderRegistry {
private final Map<String, ProviderAdapter> adapters; // 预设供应商
private final Map<String, ProviderAdapter> customAdapters; // 用户自定义端点
public ProviderAdapter getAdapter(String name) {
return adapters.get(name);
}
public ProviderAdapter getOrCreateCustomAdapter(String endpoint) {
// 同端点的适配器缓存复用
return customAdapters.computeIfAbsent(endpoint, ep ->
new OpenAiCompatibleAdapter(new ProviderConfig("custom", ep, "", ...))
);
}
public ProviderAdapter getDefault() {
return adapters.get(defaultProvider);
}
}

customAdapters 这块有意思——面面通允许用户配置自定义的 API 端点。比如你有自己的模型代理服务,填个地址就行。系统会帮你创建一个对应的适配器并缓存起来,下次再用同一个端点就直接命中缓存。


第三步:门面#

门面是所有消费者的统一入口,也是整个网关层最核心的类:

@Service
public class AiGateway {
public AiResponse chat(AiRequest request, Long userId) {
// 1. 选供应商
ProviderAdapter adapter = resolveAdapter(userId);
// 2. 决定用什么 Key
String apiKey = keyResolver.resolve(userId);
// 3. 决定用什么模型
String model = modelResolver.resolve(userId, request.model(), null);
// 4. 扣配额
quotaPolicy.consume(userId, request.taskType(), model);
// 5. 真正调 AI
return adapter.chat(resolvedRequest, apiKey);
}
private ProviderAdapter resolveAdapter(Long userId) {
// 用户配置了自定义端点 → 用自定义
// 用户选了预设供应商 → 用对应的
// 都没选 → 回退到默认
}
}

chat 和 streamChat 两个方法,加起来 100 行。

核心的 Resolve 逻辑是一个优先级链

供应商选择:自定义端点 > 用户预设 > 默认
Key 选择: 用户自己的 Key > 系统 Key > 配额用户
模型选择: 请求指定 > 用户偏好 > 任务默认 > 供应商默认

每一层都用单独的 Resolver 类封装,互不依赖。想改 Key 的分配逻辑?改 KeyResolver 一个文件。


迁移过程#

重构最怕的是”改完了跑不起来”。

重构前后的差别其实很直观:以前每个 Service 都自己拉线去调 AI;抽出网关层之后,业务 Service 只把请求交给同一个入口。

AiGateway 重构前后对比

面面通的做法是:不改现有代码,直接新增网关层。

  1. 先写好 AiGateway + ProviderAdapter + Registry
  2. 把 7 个 Service 从”自己调 API”改为”注入 AiGateway,调它的方法”
  3. 逐个 Service 验证——面试能跑通就把 InterviewService 切过去,简历能跑通就切 ResumeAnalysisService
  4. 验证完所有 Service 后,删除旧的 API 调用代码

这个过程花了一周。工具链熟悉的话可能两三天就行,但面面通的每个 Service 对错误处理、超时、重试的策略都不太一样,迁移时要逐个适配。

当时最麻烦的不是”怎么抽象”,而是每迁一个 Service 都得确认三件事:

  • 原来的错误提示有没有丢
  • 流式输出有没有提前断
  • 配额扣减有没有重复或漏掉

最终效果是:新增消费者时只需要注入 AiGateway,调用 chat 或 streamChat 方法。不用关心用户用的是 DeepSeek 还是自定义端点,不用关心配额扣没扣。

// 重构后:InterviewService
private final AiGateway aiGateway;
public void startInterview() {
AiResponse response = aiGateway.chat(request, userId);
// 处理 response ...
}
// 重构后:PolishService
private final AiGateway aiGateway;
public void runPolish() {
aiGateway.streamChat(request, userId, token -> {
// 接收流式 token ...
});
}

两个 Service 的调用代码几乎一模一样——这就是我要的效果。


额外收获#

重构完网关层之后,有几个意料之外的好处:

1. 测试变简单了

以前测试一个 Service 要 mock RestTemplate,现在 mock AiGateway 就行了。一个接口两个方法,mock 成本很低。

后端测试基础设施就是在这个阶段加的——383368c0 test: 添加后端测试基础设施和消费者冒烟测试

2. 配额统计统一了

所有 AI 调用都经过 AiGateway,配额消耗在网关层统一记录,不会漏。

后来加的 QuotaPolicy 也是在网关层切进去的,不需要改动任何消费者代码。

3. 多供应商切换对前端透明

用户在 ProfileView 里配置想用的供应商和模型,后端 AiGateway 自动路由。前端不用知道后端接了多少个 AI 供应商——它只调接口,网关层决定怎么路由。


一点感想#

面面通最开始也是那种”先跑起来再说”的代码。7 个 Service 各自调 API,跑是能跑,但每加一个需求就要改 7 遍。

重构网关层之后,后面加论文润色服务时,代码量少了很多。新 Service 只需要关注业务逻辑——关键的 AI 调用直接丢给 AiGateway。

这次重构让我学到的不是设计模式有多厉害,而是:当你要改第 3 遍的时候,就该抽层了。 不要等第 7 遍。


面面通的完整代码在 GitHub 上,AiGateway 的源码在 service/ai/gateway/ 包下,总代码不到 500 行。

下一篇会讲面面通里最复杂的模块——AI 面试引擎,包括怎么用 Prompt 控制 AI 扮演面试官、SSE 流式对话怎么保证不丢数据、编程环节怎么跟面试流程整合。

花了一周重构 AI 网关层后,新增供应商只需写 3 行代码
https://qiandaos.top/posts/mianmiantong-series/02-ai-gateway/
作者
千岛寒流
发布于
2026-06-13
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时

封面
Sample Song
Sample Artist
封面
Sample Song
Sample Artist
0:00 / 0:00