docs: 添加 ReAct 循环分离设计文档
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
199
docs/superpowers/specs/2026-05-08-react-separation-design.md
Normal file
199
docs/superpowers/specs/2026-05-08-react-separation-design.md
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
# ReAct 循环分离设计
|
||||||
|
|
||||||
|
**日期**: 2026-05-08
|
||||||
|
**状态**: 已批准
|
||||||
|
**目标**: 将 "fat node" 中的 ReAct 循环拆分为独立的 agent/tools 图节点
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 目标
|
||||||
|
|
||||||
|
将当前 `agent.py` 中的"厚节点"(包含完整 while 循环、工具执行、循环检测)重构为标准 LangGraph 图架构:
|
||||||
|
- **推理节点 (agent)**:只负责单步 LLM 调用
|
||||||
|
- **工具节点 (tools)**:只负责执行工具
|
||||||
|
- **条件边**:控制循环逻辑
|
||||||
|
- **finalize 节点**:轻量后处理
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 新图结构
|
||||||
|
|
||||||
|
```
|
||||||
|
START
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
init_state ──→ memory_trigger ──→ agent ──┬──→ (条件边) ──→ tools ──→ agent (循环)
|
||||||
|
│ │ ▲
|
||||||
|
│ └──────────────┘
|
||||||
|
│ (无工具调用时)
|
||||||
|
▼
|
||||||
|
finalize
|
||||||
|
│
|
||||||
|
▼
|
||||||
|
END
|
||||||
|
```
|
||||||
|
|
||||||
|
### 节点职责
|
||||||
|
|
||||||
|
| 节点 | 职责 | 输入 | 输出 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| `init_state` | 初始化状态,重置步数 | 无 | `{current_step: 0, max_steps: N}` |
|
||||||
|
| `memory_trigger` | 检测记忆指令,触发 Mem0 存储 | AgentState | 无修改 |
|
||||||
|
| `agent` | 单步 LLM 调用,输出 AIMessage | AgentState | `{messages: [AIMessage], ...}` |
|
||||||
|
| `tools` | 执行 tool_calls,返回 ToolMessage | AgentState | `{messages: [ToolMessage], current_step: N+1, ...}` |
|
||||||
|
| `finalize` | 轻量后处理 | AgentState | `{final_reply: str, metadata: {...}}` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 状态定义
|
||||||
|
|
||||||
|
在 `AgentState` 中新增字段:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@dataclass
|
||||||
|
class AgentState:
|
||||||
|
# 现有字段保留...
|
||||||
|
|
||||||
|
# 新增字段
|
||||||
|
tool_call_history: List[dict] = field(default_factory=list) # [{"name": "...", "args": {...}}]
|
||||||
|
tool_result_history: List[str] = field(default_factory=list) # ["结果1", "结果2", ...]
|
||||||
|
stop: bool = False # 手动停止标志
|
||||||
|
stop_reason: str = "" # 停止原因
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 节点实现
|
||||||
|
|
||||||
|
### 4.1 agent 节点
|
||||||
|
|
||||||
|
**职责**:单步 LLM 调用,不执行工具
|
||||||
|
|
||||||
|
**流程**:
|
||||||
|
1. 步数检查(已达上限则用无工具模型)
|
||||||
|
2. 循环检测(检测到异常则设置 `stop=True`)
|
||||||
|
3. 调用 LLM(带工具绑定)
|
||||||
|
4. 流式推送 token
|
||||||
|
|
||||||
|
**返回值**:
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"messages": [AIMessage(content=..., tool_calls=[...])],
|
||||||
|
"stop": bool,
|
||||||
|
"stop_reason": str,
|
||||||
|
"llm_calls": int + 1
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 tools 节点
|
||||||
|
|
||||||
|
**职责**:执行 `AIMessage.tool_calls`,生成 `ToolMessage`
|
||||||
|
|
||||||
|
**流程**:
|
||||||
|
1. 获取最后一条 `AIMessage`
|
||||||
|
2. 提取 `tool_calls` 列表
|
||||||
|
3. 遍历执行每个工具
|
||||||
|
4. 记录历史(tool_call_history, tool_result_history)
|
||||||
|
5. 更新步数 `current_step += 1`
|
||||||
|
6. 发送工具开始/结束事件(非 token 流)
|
||||||
|
|
||||||
|
**返回值**:
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"messages": [ToolMessage(...), ...],
|
||||||
|
"current_step": current_step + 1,
|
||||||
|
"tool_call_history": [...],
|
||||||
|
"tool_result_history": [...],
|
||||||
|
"tools_used": [tool_names] # 新增:记录本轮使用的工具
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 finalize 节点
|
||||||
|
|
||||||
|
**职责**:轻量后处理
|
||||||
|
|
||||||
|
**流程**:
|
||||||
|
1. 从 `messages` 中提取最后一条 `AIMessage.content` 作为最终回复
|
||||||
|
2. 汇总元数据:步数、使用的工具、停止原因
|
||||||
|
3. 如果 `final_reply` 为空且有 `stop_reason`,生成说明文本
|
||||||
|
|
||||||
|
**返回值**:
|
||||||
|
```python
|
||||||
|
{
|
||||||
|
"final_reply": str,
|
||||||
|
"metadata": {
|
||||||
|
"steps_taken": int,
|
||||||
|
"tools_used": List[str],
|
||||||
|
"stop_reason": str,
|
||||||
|
"llm_calls": int
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.4 条件边
|
||||||
|
|
||||||
|
```python
|
||||||
|
def should_continue(state: AgentState) -> Literal["tools", "finalize"]:
|
||||||
|
"""根据 agent 节点输出决定下一步"""
|
||||||
|
# 手动停止标志
|
||||||
|
if getattr(state, "stop", False):
|
||||||
|
return "finalize"
|
||||||
|
|
||||||
|
# 检查是否有工具调用
|
||||||
|
last_msg = state.messages[-1]
|
||||||
|
if isinstance(last_msg, AIMessage) and last_msg.tool_calls:
|
||||||
|
return "tools"
|
||||||
|
|
||||||
|
return "finalize"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.5 循环回边
|
||||||
|
|
||||||
|
```
|
||||||
|
tools 节点执行完后 → 无条件回到 agent 节点
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 流式事件
|
||||||
|
|
||||||
|
| 阶段 | 事件类型 | 内容 |
|
||||||
|
|------|----------|------|
|
||||||
|
| agent | `node_start` | `{"node": "agent"}` |
|
||||||
|
| agent | `llm_token` | `{"token": "...", "reasoning_token": "..."}` |
|
||||||
|
| agent | `node_end` | `{"node": "agent"}` |
|
||||||
|
| tools | `tool_start` | `{"tool": "name", "args": {...}, "id": "..."}` |
|
||||||
|
| tools | `tool_end` | `{"tool": "name", "id": "...", "result": "..."}` |
|
||||||
|
| finalize | 无 | - |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 循环检测
|
||||||
|
|
||||||
|
保留现有 `_should_stop_for_loop` 函数,放在 agent 节点中。
|
||||||
|
|
||||||
|
**检测逻辑**:连续 2 次调用相同工具 + 参数相似 + 结果相似 → 设置 `stop=True, stop_reason="loop_detected"`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 文件变更
|
||||||
|
|
||||||
|
### 新增文件
|
||||||
|
- `backend/app/main_graph/nodes/tools.py` — tools 节点实现
|
||||||
|
- `backend/app/main_graph/nodes/finalize.py` — finalize 节点实现
|
||||||
|
|
||||||
|
### 修改文件
|
||||||
|
- `backend/app/main_graph/state.py` — 新增状态字段
|
||||||
|
- `backend/app/main_graph/main_graph_builder.py` — 重构图构建逻辑
|
||||||
|
- `backend/app/main_graph/nodes/agent.py` — 移除 while 循环和工具执行逻辑
|
||||||
|
|
||||||
|
### 删除
|
||||||
|
- 无
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 兼容性
|
||||||
|
|
||||||
|
- 对外接口(`process_message`, `process_message_stream`)保持不变
|
||||||
|
- 返回值格式调整:新增 `metadata` 字段
|
||||||
|
- checkpointer 兼容性:新增字段需设置默认值
|
||||||
Reference in New Issue
Block a user