共计 2750 个字符,预计需要花费 7 分钟才能阅读完成。
现象复现:当工具开始 ” 鬼打墙 ”
先看一个典型的循环调用场景。假设我们有两个工具类相互调用,这在复杂业务中很常见:

public class WeatherTool {@Tool(name = "get_weather", description = "获取当前天气")
public String getWeather(String location) {
// 这里间接调用了另一个工具
return "当前" + location + "天气晴朗,温度:" + new CalculatorTool().recommendTemp();
}
}
public class CalculatorTool {@Tool(name = "calc_temp", description = "计算推荐温度")
public String recommendTemp() {
// 又调回天气工具
return new WeatherTool().getWeather("北京").split(":")[1];
}
}
注册工具后调用时就会陷入死循环:
AgentExecutor executor = AgentExecutor.builder()
.tools(new WeatherTool(), new CalculatorTool())
.build();
executor.execute("查询今日适宜温度"); // 栈溢出警告!
原理分析:调用链如何形成闭环
用 mermaid 展示调用流程就一目了然了:
sequenceDiagram
participant A as Agent
participant W as WeatherTool
participant C as CalculatorTool
A->>W: getWeather("北京")
W->>C: recommendTemp()
C->>W: getWeather("北京")
W->>C: recommendTemp()
Note right of C: 无限循环开始...
关键问题在于:LangChain4j 默认不跟踪调用堆栈深度,工具间相互调用时没有终止机制。
解决方案三板斧
方案 1:给工具加上 ” 定时炸弹 ”
最直接的防护是在工具方法添加超时注解:
public class WeatherTool {
@Tool(name = "get_weather",
description = "获取当前天气",
timeout = 3000) // 3 秒后自动终止
public String getWeather(String location) {// 方法实现...}
}
超时后会抛出 ToolExecutionTimeoutException,记得捕获处理:
try {executor.execute(query);
} catch (ToolExecutionTimeoutException e) {logger.warn("工具执行超时", e);
return "查询超时,请简化问题";
}
方案 2:给调用装上 ” 深度计 ”
实现 ExecutionListener 监控调用深度:
public class DepthMonitor implements ExecutionListener {
private static final int MAX_DEPTH = 5;
private ThreadLocal<Integer> depth = ThreadLocal.withInitial(() -> 0);
@Override
public void beforeToolExecution(ToolExecutionRequest request) {if (depth.get() >= MAX_DEPTH) {throw new IllegalStateException("调用深度超过" + MAX_DEPTH + "层");
}
depth.set(depth.get() + 1);
}
@Override
public void afterToolExecution(ToolExecutionResult result) {depth.set(depth.get() - 1);
}
}
注册监听器:
AgentExecutor executor = AgentExecutor.builder()
.tools(tools)
.listeners(new DepthMonitor())
.build();
方案 3:自定义执行器的 ” 紧急制动 ”
重写 DefaultToolExecutor 实现主动中断:
public class SafeToolExecutor extends DefaultToolExecutor {private final AtomicBoolean stopped = new AtomicBoolean(false);
public void stopExecution() {stopped.set(true);
}
@Override
public ToolExecutionResult execute(ToolExecutionRequest request) {if (stopped.get()) {throw new ToolExecutionException("执行已被手动终止");
}
return super.execute(request);
}
}
使用时通过单独线程监控:
SafeToolExecutor executor = new SafeToolExecutor();
new Thread(() -> {
try {Thread.sleep(5000);
executor.stopExecution();} catch (InterruptedException ignored) {}}).start();
生产环境避坑指南
- 工具依赖闭环:
- 问题:工具 A→B→C→A 形成调用环
-
对策:使用 @Tool(requires=”B,C”)显式声明依赖
-
递归调用失控:
- 问题:工具自身递归没有终止条件
-
对策:强制添加 maxIterations 参数
-
上下文污染:
- 问题:多次调用间共享可变状态
- 对策:用 ThreadLocal 保存工具状态
延伸思考
- 如何结合断路器模式(Circuit Breaker)实现工具调用的熔断机制?
- 在分布式环境下,如何全局控制工具调用链深度?
推荐工具:
– LangChain4j 官方文档:https://langchain4j.github.io
– 调试神器:在 VM options 添加 -Dlangchain4j.debug.tools=true 开启调用日志
实战心得
遇到工具循环调用时,不要急于修改业务逻辑。先通过超时控制 + 深度监控建立安全防护,再分析调用链路图找出设计缺陷。记住:好的工具设计应该是无状态的、单向依赖的。如果发现工具间必须循环调用,很可能需要重新规划工具边界了。
建议在新工具上线前,用单元测试模拟极端调用场景。我们团队曾经在 QA 环境发现一个深藏 6 层的调用环,提前规避了线上事故。
正文完
发表至: 编程开发
近两天内
