Java 工程师 AI 智能体学习路线 · 阶段 3:编程调用 LLM 详解
本阶段目标:用代码把大模型接进 Java 应用,理解请求/响应结构,掌握流式输出、错误处理、密钥管理等工程要点,为后续智能体开发打基础。
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
3.1 HTTP 直接调用(不依赖框架)
讲解
所有 SDK 本质都是对 HTTP 接口的封装。理解原始调用有助于排查问题、对接小众厂商。
- 端点 :通常为
POST /v1/chat/completions。 - 鉴权 :请求头
Authorization: Bearer <API_KEY>。 - 请求体 :
model、messages、temperature等参数(JSON)。 - 响应体 :
choices[].message.content、usage等。
示例(请求结构)
json
POST https://api.openai.com/v1/chat/completions
Headers: { "Authorization": "Bearer sk-xxx", "Content-Type": "application/json" }
Body:
{
"model": "gpt-4o",
"messages": [
{ "role": "system", "content": "你是一名 Java 专家" },
{ "role": "user", "content": "什么是 Stream API?" }
],
"temperature": 0.3
}
示例(Java 原生调用,WebClient 风格)
java
WebClient client = WebClient.create("https://api.openai.com");
String resp = client.post()
.uri("/v1/chat/completions")
.header("Authorization", "Bearer " + apiKey)
.bodyValue(requestBody)
.retrieve()
.bodyToMono(String.class)
.block();
常见场景
- 对接未提供官方 SDK 的厂商。
- 网关层统一封装、加审计/限流。
- 调试时直接抓原始报文。
面试知识点
- Q:API Key 放在请求头还是请求体?为什么?
A:放请求头(Authorization),避免被日志记录到 URL/Body 中泄露;且符合 HTTP 鉴权惯例,便于网关统一拦截。
3.2 SDK 与框架抽象
讲解
直接用 HTTP 调用繁琐且易错,实际项目多用 SDK/框架统一抽象。Java 主流选择:
- Spring AI :
ChatClient抽象,最贴合 Spring 生态。 - LangChain4j :
ChatLanguageModel抽象,链式/Agent API 成熟。 - 官方 SDK:OpenAI Java SDK、各厂商 Java SDK。
示例(Spring AI ChatClient)
java
@Autowired
private ChatClient chatClient;
public String ask(String question) {
return chatClient.prompt()
.system("你是一名严谨的 Java 架构师")
.user(question)
.call()
.content();
}
示例(LangChain4j)
java
ChatLanguageModel model = OpenAiChatModel.builder()
.apiKey(apiKey)
.modelName("gpt-4o")
.temperature(0.3)
.build();
String answer = model.generate("用一句话解释 JVM 内存模型");
常见场景
- 业务代码只依赖抽象接口,便于切换厂商。
- 多模型路由:按任务类型选择不同
ChatClient实例。
面试知识点
- Q:为什么要用 ChatClient 这类抽象而不是直接写 HTTP?
A:屏蔽厂商差异、统一消息模型、内置重试/流式/拦截器,降低耦合,便于灰度切换与单元测试。
3.3 消息模型(请求/响应结构)
讲解
理解 SDK 背后的对象模型,有助于正确组装多轮对话与解析结果。
- Message :
SystemMessage/UserMessage/AssistantMessage/ToolMessage。 - Prompt:一组 Message 的容器,传给模型。
- ChatResponse / Completion :模型返回,含
Result(内容)、Metadata(模型名、用量)。 - Usage :
promptTokens/completionTokens/totalTokens,用于成本核算。
示例(组装多轮消息,Spring AI)
java
Prompt prompt = new Prompt(List.of(
new SystemMessage("你是 Java 导师"),
new UserMessage("什么是泛型?"),
new AssistantMessage("泛型提供编译期类型安全..."),
new UserMessage("那通配符上下界呢?")
));
ChatResponse response = chatClient.call(prompt);
String answer = response.getResult().getOutput().getText();
常见场景
- 多轮对话需把历史 User/Assistant 持续拼回 Prompt。
- 解析
usage做成本监控。
面试知识点
- Q:多轮对话是把历史全量发过去吗?有什么问题?
A:通常是,但会随轮次增长超出上下文窗口并增加成本;工程上需截断/摘要历史,或只保留关键上下文。
3.4 流式处理(Streaming)
讲解
流式(SSE)让模型"边生成边返回",提升用户体验。Java 侧常用响应式(Flux)或 SseEmitter。
- 协议 :服务端以
text/event-stream持续推送片段(delta)。 - Spring AI 流式:
java
chatClient.prompt()
.user(question)
.stream()
.content() // Flux<String>
.subscribe(token -> System.out.print(token));
- Web 层(SseEmitter):
java
@GetMapping("/chat/stream")
public SseEmitter stream(@RequestParam String q) {
SseEmitter emitter = new SseEmitter();
chatClient.prompt().user(q).stream().content()
.subscribe(
token -> { try { emitter.send(token); } catch (IOException e) {} },
e -> emitter.completeWithError(e),
emitter::complete
);
return emitter;
}
- 前端 :用
fetch+ReadableStream或EventSource逐字渲染。
常见场景
- 聊天机器人打字机效果。
- 长文本生成避免前端长时间白屏。
面试知识点
- Q:流式输出和非流式在成本/延迟上有什么区别?
A:成本相同(按 Token 计费);流式首字延迟低、体验好,但需处理断连、背压与超时;非流式实现简单,但需等全部生成完才返回。
3.5 错误处理与重试
讲解
生产调用必须处理以下异常:
- 429 限流(Too Many Requests):触发速率/配额限制。
- 408/超时:网络或服务端慢。
- 5xx:服务端错误。
- 内容审核拦截:输入/输出被安全策略拒绝。
重试策略
- 指数退避 + 抖动(如 1s、2s、4s 后重试)。
- 区分可重试(429/5xx/超时)与不可重试(400/鉴权失败)。
- 设置最大重试次数与总体超时。
- 降级:重试耗尽后返回兜底文案或降级模型。
示例(Spring Retry 简化示意)
java
@Retryable(retryFor = {RateLimitException.class, TimeoutException.class},
maxAttempts = 3, backoff = @Backoff(delay = 1000, multiplier = 2))
public String askWithRetry(String q) { return chatClient...; }
@Recover
public String fallback(String q, Exception e) { return "服务繁忙,请稍后再试"; }
常见场景
- 高并发批量任务易触发 429,需限流+退避。
- 关键链路配置熔断(Resilience4j)。
面试知识点
- Q:收到 429 应该怎么处理?立即重试可以吗?
A:不可以立即重试,会加重限流。应读取Retry-After头,按指数退避+抖动重试;并配合本地限流、批量降速,必要时降级到更小模型。
3.6 密钥与配置管理
讲解
- 环境变量 / 配置中心 :API Key 不写死在代码,放
application.yml(占位${AI_KEY})或 Nacos/Apollo。 - 密钥轮转:定期更换 Key,支持多 Key 负载均衡与故障切换。
- 多模型路由:配置多个模型实例,按任务/成本选择。
- 安全:Key 仅服务端持有,绝不暴露给前端;日志脱敏。
示例(多 Key 轮询,伪代码)
java
List<String> keys = List.of(k1, k2, k3);
AtomicInteger idx = new AtomicInteger();
String nextKey() {
return keys.get(idx.getAndIncrement() % keys.size());
}
常见场景
- 配额分摊:多个 Key 避免单 Key 限流。
- 合规:密钥加密存储、访问审计。
面试知识点
- Q:前端能直接持有 LLM 的 API Key 吗?为什么?
A:不能。前端暴露 Key 等于公开凭证,会被盗刷且无法审计;必须由后端代理调用,Key 仅存服务端。
3.7 阶段 3 自测清单
- 能不依赖框架直接用 HTTP 调用一次模型
- 能用 Spring AI / LangChain4j 实现对话
- 能实现流式输出接口(SseEmitter / Flux)
- 能处理 429 限流并重试/降级
- 能把 API Key 放到配置中心而非代码
3.8 阶段 3 面试题与参考答案
1. API Key 应该放请求头还是请求体?为什么?
放请求头(Authorization: Bearer),避免被 URL/Body 日志记录泄露,且便于网关统一拦截与审计。
2. 为什么推荐用 ChatClient / ChatLanguageModel 抽象而不是直接写 HTTP?
屏蔽厂商差异、统一消息模型、内置重试/流式/拦截器,降低耦合,便于切换、灰度与单元测试。
3. 多轮对话如何管理历史?全量发送有什么问题?
把历史 User/Assistant 持续拼回 Prompt。全量发送会随轮次增长超出上下文窗口并增加成本,需截断/摘要历史。
4. 流式输出和非流式在成本、延迟、体验上的区别?
成本相同(按 Token);流式首字延迟低、体验好,但需处理断连/背压/超时;非流式实现简单但需等全部生成完才返回。
5. 收到 429 限流应该如何处理?
不立即重试;读取 Retry-After,按指数退避+抖动重试;配合本地限流与降速,必要时降级到更小模型或返回兜底。
6. 哪些错误可重试、哪些不可重试?
可重试:429 限流、408/超时、5xx;不可重试:401/403 鉴权失败、400 参数错误、内容审核拦截(需改输入而非重试)。
7. 前端能直接持有 LLM API Key 吗?
不能。前端暴露 Key 会被盗刷且无法审计,必须由后端代理调用,Key 仅存服务端,并做日志脱敏。
8. 什么是 SSE?Java 侧如何实现流式推送?
SSE(Server-Sent Events)是服务端单向持续推送文本的协议。Java 侧可用 Spring 的
SseEmitter或响应式Flux把模型生成的 token 逐段推送给前端。
9. 多 Key 轮询有什么好处?
分摊配额避免单 Key 限流,支持故障切换与密钥轮转,提升可用性与并发能力。
10. 如何对 LLM 调用做成本核算?
解析响应中的
usage(prompt/completion/total tokens),按厂商单价累计;结合日志/可观测平台做按业务/用户的成本归因与告警。