最近在开发一个需要接入大模型API的Java后端项目时,遇到了不少意料之外的问题。作为一个有多年Java开发经验的工程师,本以为调用API是件简单的事,但实际操作中却踩了不少坑。这里记录下整个过程,希望能帮到有类似需求的开发者。
大模型API接入看似简单,实则涉及网络请求、数据解析、异常处理、性能优化等多个方面。特别是在生产环境中,还需要考虑稳定性、重试机制、限流等问题。我选择的是目前市面上比较成熟的某大模型API(具体名称不便透露),但遇到的问题和解决方案对其他API也基本适用。
首先需要考虑的是API版本的选择。目前主流的大模型API通常提供多个版本,有的按token计费,有的按请求次数计费。我最终选择了按token计费的版本,因为我们的应用场景中请求内容长度差异较大,这种计费方式更划算。
认证方面,大多数API都采用API Key的方式。这里有个小技巧:不要把API Key硬编码在代码中,而是应该放在环境变量或配置中心。我使用的是Spring Cloud Config来管理配置,这样即使Key泄露也能快速更换。
java复制@Value("${ai.api.key}")
private String apiKey;
Java生态中有多种HTTP客户端可选:HttpURLConnection、Apache HttpClient、OkHttp、Spring的WebClient等。经过对比,我选择了OkHttp,主要考虑以下几点:
java复制OkHttpClient client = new OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(60, TimeUnit.SECONDS)
.writeTimeout(60, TimeUnit.SECONDS)
.build();
注意:超时设置非常重要,大模型API的响应时间可能较长,特别是处理复杂请求时。建议根据API文档的预期响应时间适当调整。
大模型API通常要求JSON格式的请求体,包含prompt、temperature等参数。这里遇到了第一个坑:特殊字符处理。当prompt中包含换行符、引号等特殊字符时,直接序列化会导致JSON解析错误。
解决方案是使用Jackson的ObjectMapper进行严格序列化:
java复制ObjectMapper mapper = new ObjectMapper();
String requestBody = mapper.writeValueAsString(aiRequest);
此外,某些API对JSON字段的顺序有要求(虽然理论上JSON是无序的)。这时可以使用@JsonPropertyOrder注解指定字段顺序:
java复制@JsonPropertyOrder({"model", "prompt", "temperature"})
public class AIRequest {
// 字段定义
}
一些高级的大模型API支持流式响应(streaming),可以实时获取生成结果。这在实现聊天功能时特别有用。但流式处理也带来了新的挑战:
OkHttp对流式响应有很好的支持:
java复制Request request = new Request.Builder()
.url(apiUrl)
.post(RequestBody.create(requestBody, JSON))
.build();
client.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
try (ResponseBody body = response.body()) {
BufferedSource source = body.source();
while (!source.exhausted()) {
String chunk = source.readUtf8Line();
// 处理每个chunk
}
}
}
});
所有大模型API都有严格的速率限制(rate limiting),这是遇到的第三个大坑。当请求超过限制时,API会返回429状态码。简单的解决方案是使用指数退避算法进行重试。
我最终实现了如下重试策略:
java复制int retryCount = 0;
boolean success = false;
while (!success && retryCount < MAX_RETRY) {
try {
Response response = executeRequest(request);
success = true;
return response;
} catch (RateLimitException e) {
long waitTime = (long) Math.pow(2, retryCount) * 1000;
Thread.sleep(waitTime);
retryCount++;
}
}
大模型API调用通常是I/O密集型操作,良好的连接池配置可以显著提升性能。OkHttp默认的连接池配置可能不适合高并发场景,需要调整:
java复制OkHttpClient client = new OkHttpClient.Builder()
.connectionPool(new ConnectionPool(50, 5, TimeUnit.MINUTES))
.build();
这个配置表示:
在高并发场景下,同步调用会导致线程大量阻塞。我最终采用了CompletableFuture实现异步调用:
java复制public CompletableFuture<String> getAIResponseAsync(String prompt) {
return CompletableFuture.supplyAsync(() -> {
try {
return getAIResponse(prompt);
} catch (Exception e) {
throw new CompletionException(e);
}
}, executorService);
}
配合线程池配置:
java复制ExecutorService executorService = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() * 2
);
对于一些相对固定的prompt,可以考虑缓存API响应结果。我使用了Caffeine缓存:
java复制Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(1, TimeUnit.HOURS)
.build();
缓存键可以使用prompt的MD5哈希值:
java复制String cacheKey = DigestUtils.md5Hex(prompt);
在生产环境中,我们遇到了偶发的超时问题。经过分析发现主要有两个原因:
解决方案:
java复制// 动态超时示例
long timeout = calculateDynamicTimeout();
Request request = new Request.Builder()
.url(apiUrl)
.post(RequestBody.create(requestBody, JSON))
.tag(timeout) // 使用tag传递超时时间
.build();
在高负载下,我们发现应用内存持续增长。通过Heap Dump分析,发现是响应体没有正确关闭导致的。修复方法:
java复制try (Response response = client.newCall(request).execute();
ResponseBody body = response.body()) {
// 处理响应
}
为了更好掌握API调用情况,我们添加了以下监控指标:
使用Micrometer实现:
java复制Timer timer = Metrics.timer("ai.api.latency");
timer.record(() -> {
// API调用代码
});
当prompt中包含用户敏感信息时,需要特别注意:
java复制// 日志脱敏示例
logger.info("API request with prompt: {}", maskSensitiveInfo(prompt));
定期轮换API密钥是基本安全实践。我们实现了自动化的密钥轮换机制:
java复制public String getCurrentApiKey() {
// 从多个密钥中选择当前可用的
}
Mock API响应进行单元测试:
java复制@Mock
private OkHttpClient httpClient;
@Test
public void testGetAIResponse() throws Exception {
// 设置mock响应
Response mockResponse = new Response.Builder()
.request(new Request.Builder().url("http://test").build())
.protocol(Protocol.HTTP_1_1)
.code(200)
.body(ResponseBody.create("mock response", JSON))
.build();
when(httpClient.newCall(any()).execute()).thenReturn(mockResponse);
// 测试业务逻辑
}
使用Testcontainers进行集成测试:
java复制@Testcontainers
class AIServiceIntegrationTest {
@Container
static MockServerContainer mockServer = new MockServerContainer();
@Test
void testRealConnection() {
// 配置mock服务器行为
// 执行真实HTTP调用测试
}
}
使用JMeter模拟高并发场景,重点关注:
实现专门的健康检查端点,验证API连通性:
java复制@GetMapping("/health/ai")
public ResponseEntity<String> checkAIHealth() {
try {
String testPrompt = "test";
getAIResponse(testPrompt);
return ResponseEntity.ok("OK");
} catch (Exception e) {
return ResponseEntity.status(503).body("AI service unavailable");
}
}
不同环境使用不同配置:
使用Spring Profile管理:
yaml复制# application-prod.yml
ai:
api:
timeout: 30000
准备完整的灾备方案:
java复制@CircuitBreaker(fallbackMethod = "getCachedResponse")
public String getAIResponseWithFallback(String prompt) {
return getAIResponse(prompt);
}
经过这个项目的实践,我总结了以下几点经验:
对于计划接入大模型API的团队,我建议:
最后,大模型API的技术发展很快,要保持对新技术、新特性的关注,定期评估是否需要调整实现方案。