Java + Python 联调
Java + Python 联调的核心是让 Spring Boot 负责业务入口,让 FastAPI 负责 AI 能力,并用统一协议、超时、重试和日志把两边稳定连接起来。
为什么要 Java 调 Python
在 AI Agent 项目里,Java 和 Python 的优势不一样。
Java 适合:
业务接口
权限认证
用户体系
数据库事务
后台管理
稳定部署
Python 适合:
LangChain
LangGraph
RAG
Embedding
文档解析
Agent 工作流
模型实验
所以一个现实的架构是:
前端
-> Java Spring Boot
-> Python FastAPI
-> LangChain / LangGraph / RAG
Java 对外提供稳定入口。
Python 对内提供 AI 能力。
联调目标
本节不追求复杂功能。
目标是把链路跑通:
Spring Boot 接收请求
-> 调用 FastAPI
-> FastAPI 返回 AI 结果
-> Spring Boot 统一返回前端
这一节要完成:
统一请求格式
统一响应格式
连接超时
读取超时
基础重试
请求日志
错误兜底
Python 侧接口
FastAPI 先提供一个 AI 聊天接口。
创建 main.py:
from fastapi import FastAPI
from pydantic import BaseModel, Field
app = FastAPI(title="Python AI Service")
class PythonChatRequest(BaseModel):
request_id: str = Field(min_length=1)
message: str = Field(min_length=1)
class PythonChatData(BaseModel):
answer: str
class ApiResponse(BaseModel):
code: str
message: str
data: PythonChatData | None = None
request_id: str
@app.get("/health")
def health():
return {
"status": "ok"
}
@app.post("/api/agent/chat", response_model=ApiResponse)
def chat(request: PythonChatRequest):
return ApiResponse(
code="OK",
message="success",
data=PythonChatData(
answer=f"Python AI 收到:{request.message}"
),
request_id=request.request_id,
)
启动:
uvicorn main:app --reload --host 127.0.0.1 --port 8000
测试:
curl -X POST http://127.0.0.1:8000/api/agent/chat \
-H "Content-Type: application/json" \
-d '{"request_id":"req-001","message":"什么是 RAG?"}'
期望返回:
{
"code": "OK",
"message": "success",
"data": {
"answer": "Python AI 收到:什么是 RAG?"
},
"request_id": "req-001"
}
统一响应格式
统一响应格式很重要。
否则 Java 调 Python 时会经常写很多特殊判断。
建议先统一成:
{
"code": "OK",
"message": "success",
"data": {},
"request_id": "req-001"
}
字段含义:
code:业务状态码
message:状态说明
data:真正的数据
request_id:请求追踪 ID
错误也保持同样结构:
{
"code": "AI_SERVICE_ERROR",
"message": "Python AI 服务处理失败",
"data": null,
"request_id": "req-001"
}
不要一会儿返回字符串,一会儿返回对象。
协议越稳定,联调越轻松。
Java 请求响应对象
Java 侧定义请求:
public record PythonChatRequest(
String requestId,
String message
) {
}
返回数据:
public record PythonChatData(
String answer
) {
}
统一响应:
public record PythonApiResponse<T>(
String code,
String message,
T data,
String requestId
) {
}
注意字段命名。
Python 默认是 request_id。
Java 默认是 requestId。
你可以选择两种方案:
方案一:Python 也用 requestId
方案二:Java 用 @JsonProperty("request_id")
学习阶段建议统一用 Java 风格:
{
"requestId": "req-001"
}
两边少一点转换问题。
Java RestClient
Spring Boot 里可以用 RestClient 调 Python。
示例:
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestClient;
import java.util.UUID;
@Service
public class PythonAgentClient {
private final RestClient restClient;
public PythonAgentClient(RestClient.Builder builder) {
this.restClient = builder
.baseUrl("http://127.0.0.1:8000")
.build();
}
public String chat(String message) {
String requestId = UUID.randomUUID().toString();
PythonApiResponse<PythonChatData> response = restClient.post()
.uri("/api/agent/chat")
.body(new PythonChatRequest(requestId, message))
.retrieve()
.body(new ParameterizedTypeReference<>() {
});
if (response == null || response.data() == null) {
throw new IllegalStateException("Python AI 服务无响应");
}
if (!"OK".equals(response.code())) {
throw new IllegalStateException("Python AI 服务返回失败:" + response.message());
}
return response.data().answer();
}
}
需要导入:
import org.springframework.core.ParameterizedTypeReference;
如果不想处理泛型,学习阶段也可以先写固定响应类型:
public record PythonChatApiResponse(
String code,
String message,
PythonChatData data,
String requestId
) {
}
这样更直观。
加超时
Java 调 Python 一定要设置超时。
否则 Python 服务卡住时,Java 请求也会一直挂着。
可以配置一个专用 RestClient:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.web.client.RestClient;
import java.time.Duration;
@Configuration
public class PythonClientConfig {
@Bean
RestClient pythonRestClient(RestClient.Builder builder) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory();
requestFactory.setConnectTimeout(Duration.ofSeconds(3));
requestFactory.setReadTimeout(Duration.ofSeconds(30));
return builder
.baseUrl("http://127.0.0.1:8000")
.requestFactory(requestFactory)
.build();
}
}
超时可以先这样定:
连接超时:3 秒
读取超时:30 秒
后面如果 Agent 工作流很长,再根据接口类型单独调整。
基础重试
不是所有错误都应该重试。
适合重试:
连接失败
网络抖动
临时 502 / 503 / 504
读取超时
不适合重试:
参数错误
认证失败
文件不存在
用户问题非法
明确的业务失败
学习阶段可以先写一个简单重试:
public String chatWithRetry(String message) {
RuntimeException lastError = null;
for (int i = 0; i < 2; i++) {
try {
return chat(message);
} catch (RuntimeException e) {
lastError = e;
}
}
throw new IllegalStateException("Python AI 服务暂时不可用", lastError);
}
正式项目可以使用:
Spring Retry
Resilience4j
网关重试策略
消息队列异步补偿
本节只需要理解重试边界。
加日志
联调最怕不知道请求卡在哪里。
Java 侧至少记录:
requestId
Python 接口路径
请求耗时
成功或失败
错误类型
示例:
public String chat(String message) {
String requestId = UUID.randomUUID().toString();
long start = System.currentTimeMillis();
try {
PythonChatApiResponse response = restClient.post()
.uri("/api/agent/chat")
.body(new PythonChatRequest(requestId, message))
.retrieve()
.body(PythonChatApiResponse.class);
long elapsedMs = System.currentTimeMillis() - start;
log.info("python chat success requestId={} elapsedMs={}", requestId, elapsedMs);
if (response == null || response.data() == null) {
throw new IllegalStateException("Python AI 服务无响应");
}
return response.data().answer();
} catch (Exception e) {
long elapsedMs = System.currentTimeMillis() - start;
log.warn("python chat failed requestId={} elapsedMs={}", requestId, elapsedMs, e);
throw e;
}
}
日志不要记录完整敏感内容。
尤其是:
API Key
用户隐私
内部 token
大段文档原文
Java 对外 Controller
Java 对前端可以继续提供自己的接口。
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/13")
public class JavaPythonChatController {
private final PythonAgentClient pythonAgentClient;
public JavaPythonChatController(PythonAgentClient pythonAgentClient) {
this.pythonAgentClient = pythonAgentClient;
}
@PostMapping("/chat")
public JavaChatResponse chat(@RequestBody JavaChatRequest request) {
String answer = pythonAgentClient.chatWithRetry(request.message());
return new JavaChatResponse(answer);
}
}
前端只知道 Java:
POST /api/13/chat
前端不需要知道 Python 服务地址。
这样以后 Python 服务换端口、换机器、换实现,前端不用改。
联调顺序
不要一上来就两边一起调。
建议顺序:
1. 单独启动 FastAPI
2. curl 调 Python 接口成功
3. 启动 Spring Boot
4. Java 单元测试调 Python client
5. curl 调 Java 接口
6. 前端调 Java 接口
每一步都确认成功,再往下走。
否则出错时很难判断是:
Python 代码问题
Java 请求问题
网络问题
JSON 字段问题
前端请求问题
常见问题
为什么不是前端直接调 Python
可以,但不建议作为主架构。
因为 Java 通常负责:
登录态
权限
审计
限流
统一错误码
业务数据
前端直接调 Python 会绕过这些业务控制。
Python 服务没启动怎么办
Java 侧应该返回明确兜底错误。
比如:
AI 服务暂时不可用,请稍后再试
日志里记录具体连接异常。
不要把底层异常堆栈直接返回给用户。
requestId 谁生成
入口服务生成。
在这个架构里,一般由 Java 生成 requestId,然后传给 Python。
Python 日志里也打印同一个 requestId。
这样两边日志能串起来。
超时设置多少合适
看接口类型。
普通聊天:
10-30 秒
文档解析:
可能需要异步任务
复杂 Agent:
可以拆成任务状态查询,不要一直阻塞 HTTP
练习清单
完成几件事情:
写 FastAPI /api/agent/chat
定义 Python 请求响应模型
返回统一 ApiResponse
Java 定义 PythonChatRequest
Java 定义 PythonChatResponse
Java 用 RestClient 调 FastAPI
配置连接超时和读取超时
补一个基础重试
打印 requestId 和 elapsedMs
通过 Java /api/13/chat 调通完整链路
建议目录:
ai-agent-study
├── java
│ └── spring-ai-chat-demo
│ └── PythonAgentClient.java
├── python
│ └── fastapi-ai-service
│ └── main.py
└── docs
└── java-python-integration.md
小结
本节的结论:
Java + Python 联调不是简单 HTTP 调用,关键是统一协议、超时、重试、日志和错误兜底。
最小链路:
前端
-> Java Controller
-> Java PythonAgentClient
-> FastAPI
-> Python AI 能力
-> Java 统一返回
只要这条链路稳定,后面 LangChain、LangGraph、RAG 都可以放到 Python 服务里逐步增强。


Comments | 0 条评论