99 lines
3.9 KiB
Markdown
99 lines
3.9 KiB
Markdown
---
|
||
title: "Gate without caller silently disables feature"
|
||
date: 2026-07-06
|
||
module: experts/orchestrator, core/reflexion
|
||
tags: [gate, trigger_reason, dead-code, design-pattern, code-review]
|
||
problem_type: logic_error
|
||
severity: P1
|
||
resolution_type: code_fix
|
||
category: logic-errors
|
||
---
|
||
|
||
# Gate without caller silently disables feature
|
||
|
||
## Problem
|
||
|
||
`_decompose_task` 新增了 `trigger_reason: str | None = None` 参数和门控逻辑,用于控制 `retrieve_prompt_reflection` 的调用。但两个生产调用方都不传 `trigger_reason`,导致门控条件恒为 False,功能被静默禁用。
|
||
|
||
## Symptoms
|
||
|
||
- `retrieve_prompt_reflection` 在生产中永远不会被调用(死代码)
|
||
- 无错误日志,无异常 — 功能只是"消失"了
|
||
- 单元测试全部通过(因为测试直接传入 `trigger_reason="verify_retry"`,未覆盖生产调用路径)
|
||
- 修复前是"每次分解都检索"(浪费 token),修复后变成"永不检索"(功能丢失)
|
||
|
||
## What Didn't Work
|
||
|
||
1. **单元测试无法发现**:测试直接调用 `_decompose_task(..., trigger_reason="verify_retry")`,验证门控逻辑本身正确,但未覆盖生产调用路径(两个调用方都不传 trigger_reason)
|
||
2. **功能测试无法发现**:`retrieve_prompt_reflection` 返回空时不报错,只是降级到默认 prompt,行为差异不明显
|
||
3. **类型检查无法发现**:`trigger_reason` 有默认值 `None`,类型检查通过
|
||
|
||
## Solution
|
||
|
||
在 `_rebalance` 路径传入 `trigger_reason="loop_detection"`,使重新规划时检索历史反思:
|
||
|
||
```python
|
||
# 修复前 — 两个调用方都不传 trigger_reason
|
||
phases = await self._decompose_task(lead, task) # 正常分解
|
||
new_phases = await self._decompose_task(lead, augmented_task) # 重分解
|
||
|
||
# 修复后 — _rebalance 路径传入 trigger_reason
|
||
new_phases = await self._decompose_task(
|
||
lead, augmented_task, trigger_reason="loop_detection"
|
||
)
|
||
```
|
||
|
||
并在正常分解路径添加注释说明 verify/schema 重试路径待接入。
|
||
|
||
## Why This Works
|
||
|
||
门控设计的核心假设是"有调用方会传入触发条件"。当这个假设不成立时,门控会静默禁用功能:
|
||
|
||
- `trigger_reason: str | None = None` — 默认 None
|
||
- `None not in frozenset({"verify_retry", ...})` — 门控条件恒为 False
|
||
- 无错误、无日志、无异常 — 功能只是"消失"了
|
||
|
||
修复方式是确保至少有一个调用方传入触发条件。`_rebalance` 是重新规划路径,传入 `loop_detection` 语义合理(检测到规划问题后重新规划)。
|
||
|
||
## Prevention
|
||
|
||
### 1. 门控设计检查清单
|
||
|
||
设计门控/开关时,必须:
|
||
- 列出所有调用方,确保至少有一个会传入触发条件
|
||
- 添加断言或日志:如果门控条件从未为 True,发出警告
|
||
- 在 docstring 中明确说明"调用方必须传入 X 才能触发 Y"
|
||
|
||
### 2. 测试覆盖生产调用路径
|
||
|
||
```python
|
||
# 不要只测试门控逻辑本身
|
||
async def test_gate_with_trigger():
|
||
await decompose_task(lead, task, trigger_reason="verify_retry")
|
||
assert retrieve_called # 这只验证门控逻辑
|
||
|
||
# 还要测试生产调用路径
|
||
async def test_production_call_path_does_not_silently_disable():
|
||
# 模拟生产调用(不传 trigger_reason)
|
||
await decompose_task(lead, task)
|
||
# 如果功能应该被禁用,assert 它被禁用
|
||
# 如果功能不应该被禁用,assert 它仍然工作
|
||
```
|
||
|
||
### 3. 代码审查重点
|
||
|
||
审查门控逻辑时,追踪所有调用方:
|
||
- 门控条件是否可达?
|
||
- 是否有调用方传入触发条件?
|
||
- 默认值是否会导致功能被静默禁用?
|
||
|
||
### 4. 死代码检测
|
||
|
||
定期检查从未执行的代码路径。`retrieve_prompt_reflection` 如果从未被调用,是死代码的信号。
|
||
|
||
## Related
|
||
|
||
- `docs/solutions/runtime-errors/async-generator-yield-before-await-deadlock.md` — 同一 IQ-boost 工作中的另一个非显而易见 bug
|
||
- PR #27 (feat/agent-iq-boost) — 原始 trigger_reason 门控引入
|
||
- PR fix/agent-iq-boost-gaps — 本次修复
|