共计 1618 个字符,预计需要花费 5 分钟才能阅读完成。
问题现场:一个 CPU 爆满的深夜
上周排查生产环境报警时,发现一台 16 核服务器 CPU 持续 100% 长达 2 小时。日志显示是 LangChain4j 的天气查询工具链在疯狂调用自身,形成了 WeatherTool -> parseJSON -> WeatherTool 的死循环。最终导致每秒产生 300+ 次无效 API 调用,直到被运维强制终止。

循环调用的三大元凶
1. LangChain4j 的决策机制
LangChain4j 的 Agent 工作流程可以简化为:
- 接收用户输入(Input)
- 选择工具(Tool Selection)
- 执行工具(Tool Execution)
- 解析结果(Result Parsing)
- 判断是否继续(Loop Check)
当步骤 4 返回的结果被错误解析为新的工具调用指令时,就会形成循环。
2. 常见触发场景
-
错误工具注册:两个工具相互引用形成闭环
@Tool(name="A") void A() { B(); } @Tool(name="B") void B() { A(); } -
返回值解析异常:JSON 解析器将错误信息误判为指令
{"error":"try WeatherTool again"} -
递归 Prompt 设计:AI 误解了继续深入的指令
"请逐步分析:先执行 X,再用 X 的结果执行 X"
解决方案实战
方案一:调用链追踪器
public class ToolUsageTracker {
private static final int MAX_DEPTH = 5;
private final ThreadLocal<Deque<String>> callStack = ThreadLocal.withInitial(ArrayDeque::new);
public boolean checkRecursion(String toolName) {Deque<String> stack = callStack.get();
if (stack.contains(toolName)) {return true; // 检测到循环}
if (stack.size() >= MAX_DEPTH) {return true; // 防止栈溢出}
stack.push(toolName);
return false;
}
public void cleanup() {callStack.remove(); // 必须显式清理防止内存泄漏
}
}
方案二:Spring Retry 熔断
# application.yml
resilience4j.circuitbreaker:
instances:
toolChain:
registerHealthIndicator: true
failureRateThreshold: 50
minimumNumberOfCalls: 10
automaticTransitionFromOpenToHalfOpenEnabled: true
方案三:Armeria 异步编排
[用户请求]
→ [API Gateway]
→ [Async Tool Executor]
→ [Tool A] → [Tool B]
→ [Result Aggregator]
← [响应合并]
性能优化成果
| 指标 | 原始方案 | 优化后 |
|---|---|---|
| QPS | 82 | 217 |
| 平均延迟(ms) | 450 | 120 |
| 99 线(ms) | 2100 | 350 |
JProfiler 内存分析显示,优化后工具实例数从高峰期 1200+ 降低到稳定在 50 以下。
生产环境最佳实践
-
DAG 校验工具
public void validateToolDAG(List<Tool> tools) {// 使用 Tarjan 算法检测环} -
日志规范
- 必须包含:调用链 ID、工具名、耗时、结果状态码
- 示例:
[ToolChain] traceId=abc123 tool=WeatherTool cost=45ms code=200
思考题延伸
当工具 A 升级到 v2 版本但工具 B 仍依赖 v1 时,如何设计版本协商机制?建议考虑:
- 接口语义化版本(SemVer)
- 运行时版本嗅探
- 自动降级策略
希望这些实战经验能帮你避开 AI 工具链的循环陷阱。如果遇到具体场景问题,欢迎在评论区交流讨论。
正文完
发表至: 技术分享
近两天内
