Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
P0-1: after_tool_call 中间件从未被调用 - 流式模式(run_streaming)和非流式模式(run)均添加 middleware_chain.run_after_tool_call() - ToolErrorMiddleware 错误计数恢复逻辑现在生效 - ToolOutputGuardMiddleware 敏感信息检测现在生效 P0-2: stream_errored 跳过所有工具执行 - 新增 completed_tool_ids 跟踪哪些工具已收到完整 ToolUseEnd - 流式错误时区分完整工具和不完整工具 - 完整工具照常执行(产物创建等不受影响) - 不完整工具发送取消 ToolEnd 事件(前端不再卡"执行中") - 工具执行后若 stream_errored,break outer 阻止无效 LLM 循环 参考文档: - docs/references/zclaw-toolcall-issues.md (10项问题分析) - docs/references/deerflow-toolcall-reference.md (DeerFlow工具调用完整参考)
134 lines
5.3 KiB
Markdown
134 lines
5.3 KiB
Markdown
# ZCLAW 工具调用问题分析
|
||
|
||
> 对比 DeerFlow 工具调用系统,排查 ZCLAW 工具调用问题。
|
||
> 分析日期:2026-04-24
|
||
> 更新日期:2026-04-24(P0+P0-stream_errored 已修复)
|
||
|
||
---
|
||
|
||
## 一、发现的问题
|
||
|
||
### P0: `after_tool_call` 中间件从未被调用 — ✅ 已修复 (2026-04-24)
|
||
|
||
**文件**: `crates/zclaw-runtime/src/loop_runner.rs`
|
||
|
||
在 `run()`(非流式,第 400-558 行)和 `run_streaming`(流式,第 893-1070 行)中,工具执行后直接 push `Message::tool_result` 到消息历史,**没有调用 `middleware_chain.run_after_tool_call()`**。
|
||
|
||
**影响**:
|
||
- `ToolErrorMiddleware.after_tool_call` 的错误计数和恢复消息逻辑不生效
|
||
- `ToolOutputGuardMiddleware.after_tool_call` 的敏感信息检测不生效
|
||
- 工具错误只能靠工具自身的错误返回传递,中间件层的防护形同虚设
|
||
|
||
**DeerFlow 对比**: `ToolErrorHandlingMiddleware` 通过 `wrap_tool_call` 钩子完整包裹每次工具执行。
|
||
|
||
### P0: `stream_errored` 跳过所有工具执行 — ✅ 已修复 (2026-04-24)
|
||
|
||
**文件**: `crates/zclaw-runtime/src/loop_runner.rs` 第 872-876 行
|
||
|
||
流式模式中,当 LLM 流出现任何错误(网络超时、API 错误、驱动错误)时,`stream_errored = true`,然后 `break 'outer` 直接退出循环,**跳过所有已解析的工具调用**。
|
||
|
||
**影响**:
|
||
- ToolStart 事件已发送给前端(用户看到"执行中"按钮),但工具从未实际执行
|
||
- ToolEnd 事件永远不会发送 → 前端工具状态卡在"执行中"
|
||
- 已完整接收(ToolUseEnd)的工具调用也被丢弃
|
||
|
||
**修复**: 区分完整工具(收到 ToolUseEnd)和不完整工具(仅收到 ToolUseStart/Delta)。完整工具照常执行,不完整工具发送取消 ToolEnd 事件。
|
||
|
||
### P1: 流式模式工具全串行
|
||
|
||
**文件**: `loop_runner.rs` 第 893-1070 行
|
||
|
||
非流式模式有 `JoinSet` + `Semaphore(3)` 并行执行 ReadOnly 工具,但流式模式用简单 `for` 循环串行执行所有工具。
|
||
|
||
**影响**: 多工具调用时延迟显著增加。
|
||
|
||
### P2: OpenAI 驱动工具参数静默替换
|
||
|
||
**文件**: `crates/zclaw-runtime/src/driver/openai.rs` 第 222-228 行
|
||
|
||
```rust
|
||
let parsed_args = if args.is_empty() {
|
||
serde_json::json!({})
|
||
} else {
|
||
serde_json::from_str(args).unwrap_or_else(|e| {
|
||
tracing::warn!("Failed to parse tool args '{}': {}", args, e);
|
||
serde_json::json!({})
|
||
})
|
||
};
|
||
```
|
||
|
||
JSON 解析失败时静默替换为 `{}`,结合 loop_runner.rs 的空参数处理(第 412-423 行),会注入 `_fallback_query` 替代实际参数。
|
||
|
||
### P2: ToolOutputGuard 过于激进
|
||
|
||
**文件**: `crates/zclaw-runtime/src/middleware/tool_output_guard.rs` 第 109 行
|
||
|
||
使用 `to_lowercase()` 匹配敏感模式,合法内容中包含 "password"、"system:" 等字符串会被误拦。
|
||
|
||
### P2: ToolErrorMiddleware 失败计数器是全局的
|
||
|
||
**文件**: `crates/zclaw-runtime/src/middleware/tool_error.rs` 第 27 行
|
||
|
||
`consecutive_failures: AtomicU32` 是结构体字段,所有 session 共享。高并发下 A session 失败 2 次 + B session 失败 1 次就会触发 AbortLoop(阈值 3)。
|
||
|
||
### P3: Gateway 客户端 onTool 回调语义不一致
|
||
|
||
**文件**: `desktop/src/lib/gateway-client.ts` 第 698-707 行
|
||
|
||
`tool_call` 和 `tool_result` 两个 case 共用 `onTool` 回调,但参数约定不同,调用者必须通过 `output` 是否为空判断 start/end。
|
||
|
||
---
|
||
|
||
## 二、根因分析
|
||
|
||
工具调用问题最常见的故障模式:
|
||
|
||
1. **LLM 返回的 tool_call 参数格式错误** → OpenAI 驱动静默替换为 `{}` → 工具以空参数执行 → 结果不符合预期
|
||
2. **工具执行异常** → after_tool_call 中间件未调用 → 错误未格式化 → LLM 收到原始错误信息无法恢复
|
||
3. **流被中断后重连** → DanglingToolMiddleware 修复悬挂 → 但如果修复逻辑本身有 bug(如重复修补),会导致消息膨胀
|
||
|
||
## 三、修复建议
|
||
|
||
### 修复 1: 在 loop_runner 中调用 after_tool_call
|
||
|
||
**优先级**: P0
|
||
**影响文件**: `loop_runner.rs`
|
||
|
||
在非流式模式的工具执行循环中(约第 530 行),工具执行后调用:
|
||
```rust
|
||
let after_result = middleware_chain.run_after_tool_call(
|
||
&name, &input_json, &output_str, &mut ctx
|
||
).await;
|
||
```
|
||
|
||
在流式模式的工具执行后(约第 1020 行),同样调用。
|
||
|
||
### 修复 2: 将 ToolErrorMiddleware 计数器改为 per-session
|
||
|
||
**优先级**: P2
|
||
**影响文件**: `middleware/tool_error.rs`
|
||
|
||
使用 `HashMap<String, u32>` 以 session_id 为 key 存储计数。
|
||
|
||
### 修复 3: ToolOutputGuard 改为精确匹配
|
||
|
||
**优先级**: P2
|
||
**影响文件**: `middleware/tool_output_guard.rs`
|
||
|
||
只在检测到独立的密钥值时触发(如 `sk-[48字符]`),而非单词级匹配。
|
||
|
||
---
|
||
|
||
## 四、关键文件
|
||
|
||
| 文件 | 作用 |
|
||
|------|------|
|
||
| `crates/zclaw-runtime/src/loop_runner.rs` | 主循环,工具调度 |
|
||
| `crates/zclaw-runtime/src/tool.rs` | ToolRegistry + Tool trait |
|
||
| `crates/zclaw-runtime/src/middleware/tool_error.rs` | 工具错误处理 |
|
||
| `crates/zclaw-runtime/src/middleware/tool_output_guard.rs` | 输出安全检查 |
|
||
| `crates/zclaw-runtime/src/middleware/dangling_tool.rs` | 断裂工具修复 |
|
||
| `crates/zclaw-runtime/src/driver/openai.rs` | OpenAI 兼容驱动 |
|
||
| `desktop/src/lib/gateway-client.ts` | 前端通信客户端 |
|
||
| `desktop/src/store/chat/streamStore.ts` | 前端流式处理 |
|