跳转至

如何用动态代理透明地为 Agent 所有工具调用加上日志和耗时监控?

🎯 问题核心

如何在不修改任何工具源码的前提下,为 Agent 的所有工具调用统一加上日志和耗时监控?

答案很明确:基于接口的动态代理 + 统一的 InvocationHandler,在方法调用前后织入计时和日志逻辑,再把代理对象悄悄替换掉原来的工具实例,对 Agent 完全透明。


⚙️ 关键设计:让增强“零侵入”

想要透明,必须具备两个条件:

  1. 统一的工具接口 – 所有工具实现同一个接口(例如 Tool,方法 execute)。

  2. 代理对象与真实对象类型一致 – Agent 持有的是接口引用,分不清代理还是真实对象。

JDK 动态代理正好满足:Proxy.newProxyInstance 创建一个实现相同接口的代理实例,调用任何方法都会经过 InvocationHandler,我们在这里植入日志和耗时计算。


💻 落地实现(面试可展示的核心代码)

① 工具接口

public interface Tool {
    String execute(String input);
}

② 日志&耗时代理处理器

public class ToolLogTimingProxy implements InvocationHandler {
    private final Tool target;

    public ToolLogTimingProxy(Tool target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            // 调用真实工具
            return method.invoke(target, args);
        } finally {
            long cost = System.currentTimeMillis() - start;
            System.out.printf("[Tool:%s] method=%s args=%s cost=%dms%n",
                target.getClass().getSimpleName(),
                method.getName(),
                Arrays.toString(args),
                cost);
        }
    }
}

③ 透明织入 Agent

如何用动态代理透明地为 Agent 所有工具调用加上日志和耗时监控
答案很明确基于接口的动态代理 + 统一的 InvocationHandler在方法调用前后织入计时和日志逻辑再把代理对象悄悄替换掉原来的工具实例 Agent 完全透明
⚙️ 关键设计让增强零侵入
想要透明必须具备两个条件
统一的工具接口  所有工具实现同一个接口例如 Tool方法 execute)。
代理对象与真实对象类型一致  Agent 持有的是接口引用分不清代理还是真实对象
JDK 动态代理正好满足Proxy.newProxyInstance 创建一个实现相同接口的代理实例调用任何方法都会经过 InvocationHandler我们在这里植入日志和耗时计算
💻 落地实现面试可展示的核心代码
 工具接口
代码块
public interface Tool {
    String execute(String input);
}
 日志&耗时代理处理器
代码块
public class ToolLogTimingProxy implements InvocationHandler {
    private final Tool target;

    public ToolLogTimingProxy(Tool target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            // 调用真实工具
            return method.invoke(target, args);
        } finally {
            long cost = System.currentTimeMillis() - start;
            System.out.printf("[Tool:%s] method=%s args=%s cost=%dms%n",
                target.getClass().getSimpleName(),
                method.getName(),
                Arrays.toString(args),
                cost);
        }
    }
}
 透明织入 Agent

📊 效果与价值

代理生效后,控制台会自动输出类似:

[Tool:WebSearchTool] method=execute args=[今天北京天气] cost=235ms
[Tool:Calculator]     method=execute args=[3.14*15]        cost=1ms

价值:

  • 无需修改任何工具类,连 WebSearchTool 内部一行代码都不用动。

  • 可以快速定位慢工具,排查性能瓶颈。

  • 插拔自如:想去掉监控,只需不包装代理,恢复原始列表。


💡 进阶思考(让面试官觉得你有深度)

  • 异常记录:在 catch 中捕获并打印异常 + 耗时,方便定位失败调用。

  • 接入监控系统:把 System.out 换成 Micrometer/Prometheus 打点,就可以出图。

  • 异步方法处理:如果工具有 CompletableFuture<Tool> 返回,可以在 future 上挂 thenApply 记录结束时间,才能准确计时。

  • 非接口工具的兜底:若某些工具没实现接口,可以改用 CGLIB 或 Javassist 做类代理,思想完全一致。


✅ 一句话总结

“动态代理 + 统一接口”是在不侵入业务代码的前提下,为 Agent 的所有工具调用透明增加横切关注点(日志、耗时、异常监控)的最佳实践,完美遵循开闭原则。