HTTP请求封装与处理
HTTP请求封装与处理
面试回答:AI 场景下的 HTTP 请求封装与处理
面试官您好,我结合之前大模型业务接入的项目经验,从场景特点、封装设计、核心技术点三个维度来回答这个问题。
先明确:AI 场景的 HTTP 请求有啥不一样 🎯
很多人上来就讲 HTTP 客户端工具,但 AI 接口和普通业务 HTTP 接口差异极大,这是封装设计的核心前提,也是面试的区分点:
| 对比维度 | 普通业务 HTTP 接口 | AI 大模型 HTTP 接口 |
|---|---|---|
| 响应模式 | 一次性返回完整结果 | 主流为 SSE 流式逐块返回 |
| 耗时特性 | 毫秒级,波动小 | 秒级~几十秒,耗时随输出长度波动极大 |
| 错误类型 | 参数错误、业务错误为主 | 限流、模型过载、上下文超限、服务不可用占比高 |
| 报文体积 | 多为 KB 级 | 多模态场景可达 MB 级(图片 / 音频 Base64) |
| 鉴权方式 | 多为登录态 Cookie/Token | 统一 API Key、AK/SK 签名,鉴权逻辑高度通用 |
封装的分层架构设计 📦
我们当时基于 OkHttp 做底层,做了三层解耦的封装,做到业务层零感知 HTTP 细节,整体架构如下:
每层职责边界非常清晰:
- 业务调用层:只暴露业务方法,比如
streamChat(),入参出参都是业务对象,完全看不到 HTTP 相关代码 - 统一封装层:核心逻辑层,所有通用能力通过拦截器、工具类实现,和底层 HTTP 客户端解耦,方便后续替换实现
- 协议执行层:单例 OkHttpClient,配置连接池、超时参数,只负责最底层的网络 IO
核心处理的关键技术点 ⚙️
这部分是面试的加分项,我挑 4 个最核心的场景来讲:
1. 鉴权逻辑统一拦截
所有鉴权逻辑下沉到请求拦截器,业务层完全无感知:
- 通用 Bearer Token:拦截器统一注入 Header
Authorization: Bearer xxx - 复杂签名场景:比如 AK/SK + 时间戳签名,也在拦截器内计算完成后注入
- 优势:切换厂商、轮换密钥时,只改配置,业务代码零侵入
2. SSE 流式响应处理(AI 场景核心)
这是和普通 HTTP 封装最大的区别,也是最容易踩坑的地方:
- 底层用 OkHttp 的
ResponseBody.charStream()拿到字符流,逐行读取,避免整包加载导致 OOM - 按 SSE 协议解析:匹配
data:开头的行,转成事件对象回调给业务层,遇到data: [DONE]结束流 - 强制要求:用 try-with-resources 管理 ResponseBody,必须关闭流,否则会造成连接泄漏
- 线程模型:流式处理是长耗时操作,必须放到独立线程池执行,不能阻塞 Tomcat 业务线程
3. 精细化超时与重试策略
- 超时不搞一刀切:连接超时统一设短(如 5s),读超时按场景拆分 —— 向量接口设 30s,长文本生成接口设 120s
- 重试只对幂等场景生效:仅 5xx 服务错误、429 限流触发重试,采用指数退避算法;400 参数错误、401 鉴权失败绝对不重试
- 重试上限 3 次,避免雪崩效应打爆下游服务
4. 异常统一语义封装
把 HTTP 状态码、厂商自定义错误码,统一转换成业务语义异常,比如AiRateLimitException、AiModelOverloadException,业务层可以针对性做降级、排队处理,不用关心底层 HTTP 状态码。
核心代码示例 💻
一段最具代表性的流式请求核心实现,基于 OkHttp 封装:
/**
* 流式对话请求核心逻辑
* @param request 对话请求业务对象
* @param eventCallback 事件回调,业务层处理逐块返回的内容
*/
public void streamChat(ChatRequest request, Consumer<ChatEvent> eventCallback) {
// 1. 序列化请求体
RequestBody body = RequestBody.create(
jacksonMapper.writeValueAsString(request),
MediaType.parse("application/json")
);
// 2. 构建HTTP请求(鉴权由拦截器统一注入,此处无需处理)
Request httpRequest = new Request.Builder()
.url(modelConfig.getChatEndpoint())
.post(body)
.build();
// 3. 异步执行,不阻塞业务线程
okHttpClient.newCall(httpRequest).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
// try-with-resources 自动关闭响应体,避免连接泄漏
try (ResponseBody responseBody = response.body()) {
if (!response.isSuccessful()) {
throw new AiServiceException("AI服务异常,状态码:" + response.code());
}
// 4. 逐行解析SSE流
BufferedReader reader = new BufferedReader(responseBody.charStream());
String line;
while ((line = reader.readLine()) != null) {
if (line.startsWith("data:")) {
String data = line.substring(5).trim();
if ("[DONE]".equals(data)) break; // 流结束标记
// 解析事件并回调给业务层
ChatEvent event = jacksonMapper.readValue(data, ChatEvent.class);
eventCallback.accept(event);
}
}
}
}
@Override
public void onFailure(Call call, IOException e) {
// 异常兜底:进入重试策略,不可重试则抛出业务异常
if (!retryStrategy.tryRetry(request, e, eventCallback)) {
throw new AiNetworkException("AI请求网络异常", e);
}
}
});
}实战踩坑总结 ⚠️
这几个都是项目里真实踩过的坑,也是面试里体现实战经验的亮点:
- 流资源泄漏:最常见的问题,ResponseBody 不关闭会导致连接池耗尽,并发一高就直接炸,必须用 try-with-resources 强制关闭
- 超时配置一刀切:初期流式接口也设了 10s 读超时,长文本生成频繁超时,后来按接口场景拆分了超时配置
- 无差别重试:一开始所有错误都重试,结果参数错误的请求反复触发限流,反而加重故障
- 大报文 OOM:多模态场景直接把图片 Base64 读进内存序列化,大图片直接触发 OOM,后续改成流传输方式解决
(收尾,语气平稳)总结一下,AI 场景的 HTTP 封装,核心是围绕「流式响应、长耗时、高容错」三个特点做设计,目标是让业务层专注业务逻辑,不用感知底层 HTTP 细节,同时具备良好的容错能力和可观测性。我的回答完了,谢谢面试官。
真实面试模拟
真实面试模拟
面试官 👨💼:
看你项目里用了大模型 API,先聊聊整体,你是怎么封装 HTTP 请求调用 AI 服务的?不用背术语,说思路和关键点就行。
候选人 🙋:
好的。我把调用链路抽象成三层:组装请求 → 可靠传输 → 解析响应。
具体一点,就是业务侧给我 prompt 和参数,我内部用一个 HTTP 客户端发给大模型的 /chat/completions 端点,拿到 JSON 或 SSE 流,再转成业务对象返回。
核心是要在传输层解决好超时、重试、熔断,以及 AI 特有的流式输出处理。我先画个时序图直观看一下:
面试官 👨💼:
图很清晰。那接着问,HTTP 客户端这一层,你用什么?连接池和超时怎么配的?这里面坑不少。
候选人 🙋:
对,坑都在细节里。我用的 Java 11 原生 HttpClient,它支持 HTTP/2、异步、连接池。
关键配置我列一下:
| 配置项 | 值 / 策略 | 为什么要这样 |
|---|---|---|
| 连接池大小 | 200 个连接,并开启 keep-alive | AI 调用量大且频繁,避免频繁握手 |
| 空闲连接回收 | 30 秒回收 | 防止占用服务端资源 |
| 连接超时 | 5 秒 | 连不上赶紧失败,别拖着 |
| 读取超时 | 至少 60 秒 | 大模型一次推理可能 30 秒+,设短了直接超时 ❌ |
| 异步请求 | 用 CompletableFuture 或 WebClient | 不阻塞业务线程,尤其是流式场景 |
特别是读取超时,很多新手会按普通接口设个 3 秒,一跑大模型就超时,这是高频踩坑点 💣。
面试官 👨💼:
说到流式,AI 输出长文本时都是一点一点吐的,这个流式请求你怎么封装?这个我挺看重的。
候选人 🙋:
流式处理是 AI 封装里最有区分度的地方。大模型用的是 SSE(Server-Sent Events)协议,响应体是一串 data: {"choices":[{"delta":{"content":"你"}}]} 这样的格式。
我封装时坚持三个原则:
- 边读边解析:用
BufferedReader逐行读,识别data:前缀,每遇到空行就完成一个事件,立刻解析出 delta 内容推给上层。 - 背压控制:如果上层消费慢了,不能无限制缓存,我用 Reactor 的
Flux做自然背压,或者用有界队列 + 回调。 - 资源释放:用户取消请求时,必须能取消底层 HTTP 请求,用
future.cancel(true)或subscription.cancel()。
代码骨架类似这样,我口头说一下:
用 WebClient 发出 POST,retrieve() 后取 bodyToFlux(String.class),过滤出 data: 行,去掉 [DONE],实时 map 出 token,用 doOnCancel 做清理。这样既流畅又安全。
看下类之间的关系会更清楚:
上层只需实现 StreamCallback 就能拿到一个个 token,完全不关心底层 HTTP 细节。
面试官 👨💼:
健壮性方面,重试和熔断你是怎么设计的?AI 接口挂一下,你服务可不能跟着挂。
候选人 🙋:
这部分是标准的弹性模式,但和 AI 特性结合一下。
- 重试:只对 网络超时、5xx 这类瞬时错误重试,4xx(参数错、认证错)绝不重试,重试也没用。指数退避,最大 3 次,间隔 1s/2s/4s,加上随机抖动,防止雷同重试风暴。
- 熔断:用 Resilience4j 的熔断器,配置错误率阈值 50%,10 秒内超过就熔断 30 秒,然后进入半开状态探测。熔断后直接返回降级结果或兜底文案,不让上游等死。
- 幂等性保障:流式请求比较特殊,因为已经消费了一部分,不能简单重试。我通常让上层感知到异常后,重新发起一次全新的请求,并清空之前的局部输出。所以流式接口的重试是在业务层做的,封装层只负责报错。
面试官 👨💼:
你刚才讲得挺扎实。最后两个小点:有哪些容易被忽略,但生产上很要命的点?你踩过什么坑?
候选人 🙋:
有四个点我印象特别深:
- 成本监控 💸:每次响应里都有
usage字段,记录了prompt/completion的 token 数,我把它埋点到日志或指标系统里,能按业务线算钱,否则 AI 调用多了账单莫名暴涨。 - finish_reason 校验:响应必须检查
finish_reason,如果是 "length" 表示被max_tokens截断了,这时候返回的内容不完整,我会上报告警,业务侧往往要提示“内容过长已截断”。 - API Key 管理:坚决不硬编码在代码里,用配置中心或环境变量,支持动态刷新。泄露一次,一晚上可能跑掉几千美金 😱。
- 多模态扩展:处理图片输入时,要把图片转 Base64 放进
image_url里。注意请求体可能膨胀到几 MB,要考虑压缩或者外置 URL 上传。
面试官 👨💼:
不错,从整体链路、流式处理到弹性、运营,都讲得很到位,看得出是实打实跑过生产的。HTTP 封装表面上简单,但能把这些点都落地的候选人不多。这轮聊得很好 👍。
候选人 🙋:
谢谢面试官,这些都是在实际调用 AI 的时候一点点磨出来的,继续学习。😄
