两行注解把企业 RPC 接口变成 AI 工具

标签Java Agent j-langchain RPC Dubbo Feign @AgentTool @Param @ParamDesc
前置阅读AgentExecutor:用一行代码启动 ReAct Agent
适合人群:希望把已有 Dubbo / Feign / gRPC 等 RPC 服务快速接入 AI Agent 的 Java 开发者


一、背景

企业里有大量存量 RPC 服务,想让 AI Agent 调用这些服务,最自然的期望是:

  • 不改动已有 RPC 接口定义
  • 只做最少的"配置",而不是"重写一套工具层"
  • 不管底层是 Dubbo 还是 Feign,接入方式应该一致

j-langchain 的 @AgentTool + @Param / @ParamDesc 体系正好满足这个需求。同一套工具包装类既可以交给 AgentExecutor(ReAct),也可以交给 McpAgentExecutor(Function Calling),无需任何修改。


二、参数描述的三种方式

工具的参数 Schema 是给 LLM 看的,框架按以下优先级生成:

优先级 方式 适用场景
1 @AgentTool.params 内联 @ParamDesc VO 来自第三方包,无法修改字段
2 VO 字段上的 @Param 自己定义的 VO
3 方法参数上的 @Param 简单基础类型参数

三种方式互为补充,向下兼容,按实际情况选择即可。


三、场景一:Dubbo --- 第三方 VO + @AgentTool.params 内联描述

VO 来自合作方三方包,字段上无法加 @Param。改用 @AgentTool.params 内联描述,效果完全相同。

工具包装类

java 复制代码
@Component
public class EcommerceDubboTools {

    @DubboReference
    private OrderFacade orderFacade;

    @DubboReference
    private RefundFacade refundFacade;

    @DubboReference
    private LogisticsFacade logisticsFacade;

    @AgentTool(
        value = "查询用户订单信息",
        params = {
            @ParamDesc(name = "orderId",   desc = "订单ID,格式:ORD-2024-XXXXXX,如不知道可留空"),
            @ParamDesc(name = "userId",    desc = "用户ID,格式:USR-XXXXXX,如不知道可留空"),
            @ParamDesc(name = "queryType", desc = "查询类型:LATEST(最近一笔)/ ALL(全部订单),默认 LATEST")
        }
    )
    public String queryOrder(OrderQueryRequest request) {
        return orderFacade.queryOrder(request).toString();
    }

    @AgentTool(
        value = "提交退款申请",
        params = {
            @ParamDesc(name = "orderId", desc = "订单ID,格式:ORD-2024-XXXXXX"),
            @ParamDesc(name = "reason",  desc = "退款原因,如:质量问题 / 物流超时 / 不想要了 / 收到错误商品"),
            @ParamDesc(name = "amount",  desc = "退款金额(元),不填则申请全额退款")
        }
    )
    public String applyRefund(RefundRequest request) {
        return refundFacade.applyRefund(request).toString();
    }

    @AgentTool(
        value = "查询物流配送状态",
        params = {
            @ParamDesc(name = "orderId",    desc = "订单ID,格式:ORD-2024-XXXXXX"),
            @ParamDesc(name = "trackingNo", desc = "快递单号(可选,不填则通过订单ID自动关联查询)")
        }
    )
    public String trackLogistics(LogisticsQueryRequest request) {
        return logisticsFacade.track(request).toString();
    }
}

框架读取 @AgentTool.params,自动生成如下 Schema:

复制代码
查询用户订单信息
  Input JSON keys (OrderQueryRequest):
    - orderId (String): 订单ID,格式:ORD-2024-XXXXXX,如不知道可留空
    - userId (String): 用户ID,格式:USR-XXXXXX,如不知道可留空
    - queryType (String): 查询类型:LATEST(最近一笔)/ ALL(全部订单),默认 LATEST
  Action Input format: JSON, e.g. {"orderId": ..., "userId": ..., "queryType": ...}

示例用例(使用 AgentExecutor --- ReAct)

对应 dubboAgentDemo()

java 复制代码
@Test
public void dubboAgentDemo() {
    AgentExecutor agent = AgentExecutor.builder(chainActor)
        .llm(ChatAliyun.builder().model("qwen-plus").temperature(0f).build())
        .tools(ecommerceDubboTools)
        .maxIterations(8)
        .build();

    agent.invoke("我的订单 ORD-2024-001 付款已经好几天了,显示运输中但迟迟没有送达," +
        "帮我查一下:1) 这个订单的基本信息;2) 目前物流到哪了;" +
        "3) 如果快递确实异常,帮我提交一个退款申请,原因是物流超时。");
}

同一套 ecommerceDubboTools 也可以直接传给 McpAgentExecutor,工具定义无需修改。

执行过程

复制代码
[思考] 先查询订单基本信息。
Action: query_order  {"orderId": "ORD-2024-001"}
[服务返回] 订单 ORD-2024-001:「索尼 WH-1000XM5 耳机」¥2199,运输中(已超时2天)

[思考] 订单确实超时,查询物流详情。
Action: track_logistics  {"orderId": "ORD-2024-001"}
[服务返回] SF1234567890,2024-03-12 09:00 在成都青羊区网点,此后无更新

[思考] 物流异常,提交退款。
Action: apply_refund  {"orderId": "ORD-2024-001", "reason": "物流超时"}
[服务返回] 退款申请已提交,¥2199(全额),工单号 TKT-20240316-8821

Final Answer: 已为您处理完毕...

四、场景二:Feign --- 自有 VO + @Param 标注字段

VO 是自己团队定义的,可以直接在字段上加 @Param,工具方法本身保持干净。

VO 定义

java 复制代码
@Data
public class ProductDetailRequest {
    @Param("商品ID,格式:PROD-XXXXXX")
    private String productId;

    @Param("商品类目,如:手机/笔记本/耳机,可留空")
    private String category;
}

@Data
public class InventoryQueryRequest {
    @Param("商品SKU,格式:SKU-XXXXXX")
    private String sku;

    @Param("仓库区域:EAST/WEST/SOUTH/NORTH,可留空(查全部仓库)")
    private String region;
}

@Data
public class PriceQueryRequest {
    @Param("商品SKU,格式:SKU-XXXXXX")
    private String sku;

    @Param("用户等级:VIP/NORMAL,影响折扣,默认 NORMAL")
    private String userLevel;
}

工具包装类

java 复制代码
@Component
public class RetailFeignTools {

    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private InventoryFeignClient inventoryFeignClient;

    @Autowired
    private PricingFeignClient pricingFeignClient;

    @AgentTool("查询商品详情")
    public String getProductDetail(ProductDetailRequest request) {
        return productFeignClient.getProductDetail(request).toString();
    }

    @AgentTool("查询商品库存")
    public String queryInventory(InventoryQueryRequest request) {
        return inventoryFeignClient.queryInventory(request).toString();
    }

    @AgentTool("查询商品价格")
    public String queryPrice(PriceQueryRequest request) {
        return pricingFeignClient.queryPrice(request).toString();
    }
}

框架扫描 VO 字段上的 @Param,自动生成如下 Schema:

复制代码
查询商品库存
  Input JSON keys (InventoryQueryRequest):
    - sku (String): 商品SKU,格式:SKU-XXXXXX
    - region (String): 仓库区域:EAST/WEST/SOUTH/NORTH,可留空(查全部仓库)
  Action Input format: JSON, e.g. {"sku": ..., "region": ...}

示例用例(使用 McpAgentExecutor --- Function Calling)

对应 feignAgentDemo()

java 复制代码
@Test
public void feignAgentDemo() {
    McpAgentExecutor agent = McpAgentExecutor.builder(chainActor)
        .llm(ChatAliyun.builder().model("qwen3.6-plus").temperature(0f).build())
        .tools(retailFeignTools)
        .maxIterations(8)
        .build();

    agent.invoke("我想买索尼 WH-1000XM5 耳机(商品ID:PROD-SONY-001,SKU:SKU-SONY-WH1000XM5)," +
        "帮我查一下:1) 这款产品的详情;2) 华东仓库是否有库存;3) 我是 VIP 用户,购买价格是多少?");
}

同一套 retailFeignTools 也可以直接传给 AgentExecutor,工具定义无需修改。

执行过程

复制代码
[思考] 先查询商品详情。
Action: get_product_detail  {"productId": "PROD-SONY-001"}
[服务返回] 索尼 WH-1000XM5,主动降噪,30h 续航,黑色/银色

[思考] 查华东仓库库存。
Action: query_inventory  {"sku": "SKU-SONY-WH1000XM5", "region": "EAST"}
[服务返回] EAST 仓库:黑色 47 件,银色 12 件,均可当日发货

[思考] 查 VIP 价格。
Action: query_price  {"sku": "SKU-SONY-WH1000XM5", "userLevel": "VIP"}
[服务返回] ¥1979(9折 VIP 优惠,立省 ¥220)

Final Answer: 索尼 WH-1000XM5 详情、库存、VIP 价格已查完...

五、两种场景对比

维度 Dubbo(@AgentTool.params) Feign(@Param on field)
VO 是否可修改 否(二方包提供) 是(自有定义)
参数描述位置 工具方法注解 VO 字段注解
LLM 看到的 Schema 完全相同 完全相同
AgentExecutor 可用
McpAgentExecutor 可用

两种方式生成的 Schema 对 LLM 完全透明,差别仅在于描述写在哪里。


六、总结

  • 自有 VO@Param 标注在字段上,描述集中在数据模型层
  • 第三方 VO@AgentTool.params 内联 @ParamDesc,描述集中在工具方法上
  • AgentExecutor 与 McpAgentExecutor 通用:同一套工具定义,无需为不同执行器维护两份代码
  • 框架透明:Schema 生成、JSON 反序列化、方法调用全部自动处理

完整示例:Article19RpcMcpTools.java

相关推荐
迷藏4942 小时前
**绿色AI:用Python构建节能型机器学习模型的实践与优化策略**在人工智能飞速发展的今天,模型训练和
java·人工智能·python·机器学习
juniperhan2 小时前
Flink 系列第13篇:Flink 生产环境中的并行度与资源配置
java·大数据·数据仓库·分布式·flink
Foreer黑爷2 小时前
Spring MVC原理与源码:从请求到响应的全流程解析
java·spring·mvc
xxjj998a2 小时前
Spring Boot 实战:轻松实现文件上传与下载功能
java·数据库·spring boot
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第3题:ArrayList和LinkedList有什么区别
java·开发语言·后端·面试·list
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题】【Java基础篇】第4题:LinkedList是单向链表还是双向链表
java·开发语言·数据结构·后端·链表·面试·list
Lyyaoo.4 小时前
【JAVA基础面经】JVM的内存模型
java·开发语言·jvm
杨凯凡4 小时前
【017】泛型与通配符:API 设计里怎么用省心
java·开发语言
IT利刃出鞘4 小时前
Spring工具类--ObjectUtils的使用
java·后端·spring