--- id: backend.java-spring.scenario.call-java-service title: Python Agent 调用公司已有的 Java 业务系统:读接口、封装工具、错误映射和联调 type: scenario domain: backend stack: [java-spring, function-calling, fastapi] tags: [系统对接, 工具封装, 错误码映射, OpenAPI, 联调, 幂等] level: intermediate prerequisites: [backend.java-spring.awareness, ai.function-calling.tool-loop] est_hours: 2 related: [backend.java-spring.interview, ai.function-calling.scenario.safe-business-tools] week: [14] --- # Python Agent 调用公司已有的 Java 业务系统:读接口、封装工具、错误映射和联调 ## 业务背景与问题 公司的订单、售后系统是多年的 Spring Boot 服务。新做的 AI 客服 Agent(Python)需要“查订单、查物流、发起退款申请”。问题: 1. 接口文档过期,字段含义要读代码才知道; 2. Java 服务的错误码体系复杂,Agent 不知道哪些错误可以重试、哪些要告诉用户; 3. 退款接口不是幂等的,Agent 重试导致重复申请; 4. 联调时双方互相指责。 ## 约束 - 不能大改 Java 服务,只能小范围加接口或参数。 - Agent 不能拥有超过用户本人的权限。 ## 方案设计 1. **读接口**:从 Controller 和 DTO 读出真实的字段、校验规则、错误码;如果服务集成了 OpenAPI(springdoc 一类工具),导出规范生成 Python 客户端;否则自己写 Pydantic 模型。 2. **封装工具**:每个业务能力一个工具,入参严格校验;工具层负责鉴权透传(用户令牌或服务账号 + 用户 id)、超时、重试(只对查询类和幂等接口)。 3. **错误映射**:把 Java 错误码分为三类——可重试(超时、限流)、需要告诉用户(订单不存在、超过退款期限)、需要转人工(系统异常);返回给模型的是结构化的可读信息,而不是堆栈。 4. **幂等**:和 Java 团队约定退款接口增加 `Idempotency-Key` 请求头(服务端用唯一约束实现),Agent 在一次会话中对同一操作使用同一个 key。 5. **联调**:先写契约测试(请求 / 响应样例),双方各自对着样例开发;用 request id 串起两边日志。 ## 关键代码 ```python class JavaErrorKind(str, Enum): RETRYABLE = "retryable" USER_FACING = "user_facing" HANDOFF = "handoff" ERROR_MAP = { "ORDER_NOT_FOUND": (JavaErrorKind.USER_FACING, "没有找到这个订单,请核对订单号"), "REFUND_EXPIRED": (JavaErrorKind.USER_FACING, "已超过可退款期限"), "RATE_LIMITED": (JavaErrorKind.RETRYABLE, "系统繁忙"), } def call_refund(client: httpx.Client, user: User, order_id: str, reason: str, idem_key: str) -> dict: r = client.post("/api/refunds", json={"orderId": order_id, "reason": reason}, headers={"Authorization": f"Bearer {user.token}", "Idempotency-Key": idem_key}, timeout=5) if r.is_success: return {"ok": True, "refund_id": r.json()["data"]["refundId"]} code = r.json().get("code", "UNKNOWN") kind, msg = ERROR_MAP.get(code, (JavaErrorKind.HANDOFF, "系统异常,已为你转人工")) return {"ok": False, "kind": kind, "message": msg} # 给模型的是结构化结果,不是堆栈 ``` 工具调用安全的完整讨论见[安全的业务工具](../../../ai/function-calling/scenarios/safe-business-tools.md)。 ## 取舍 | 选择 | 优点 | 缺点 | | --- | --- | --- | | **Python 工具层直接调用 Java 接口(本方案)** | 不改动 Java 服务主体 | 需要维护错误映射和模型定义 | | Java 团队提供专门给 Agent 的聚合接口 | 语义更清晰 | 依赖对方排期 | | 用 Spring AI 在 Java 侧实现 Agent | 技术栈统一 | 团队 AI 生态以 Python 为主 | ## 易错点 - Agent 使用高权限服务账号,绕过了用户级权限。 - 对非幂等接口自动重试。 - 把 Java 的异常堆栈直接交给模型,既泄露内部信息又让模型困惑。 - 金额字段在 JSON 中是字符串还是数字没有约定,精度丢失。 ## 面试怎么讲 > “我不是 Java 开发,但能读懂 Spring Boot 项目:从 Controller 和 DTO 读出真实的接口契约。对接时我在 Python 侧封装工具,鉴权透传用户身份;把 Java 的错误码分成可重试、告知用户、转人工三类,给模型结构化结果;和 Java 团队约定退款接口支持幂等键。联调前先写契约样例,用 request id 串联日志。”