如何用动态代理透明地为 Agent 所有工具调用加上日志和耗时监控?
🎯 问题核心¶
如何在不修改任何工具源码的前提下,为 Agent 的所有工具调用统一加上日志和耗时监控?
答案很明确:基于接口的动态代理 + 统一的 InvocationHandler,在方法调用前后织入计时和日志逻辑,再把代理对象悄悄替换掉原来的工具实例,对 Agent 完全透明。
⚙️ 关键设计:让增强“零侵入”¶
想要透明,必须具备两个条件:
-
统一的工具接口 – 所有工具实现同一个接口(例如
Tool,方法execute)。 -
代理对象与真实对象类型一致 – Agent 持有的是接口引用,分不清代理还是真实对象。
JDK 动态代理正好满足:Proxy.newProxyInstance 创建一个实现相同接口的代理实例,调用任何方法都会经过 InvocationHandler,我们在这里植入日志和耗时计算。
💻 落地实现(面试可展示的核心代码)¶
① 工具接口
② 日志&耗时代理处理器
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 的所有工具调用透明增加横切关注点(日志、耗时、异常监控)的最佳实践,完美遵循开闭原则。