经典 ReAct Agent Loop 改造
记录日期:2026-04-02
1. 背景
旧行为:工具执行完后直接到 response 透传,工具结果(如 list_plans 返回的原始 JSON)直接展示给用户,无 LLM 再加工。
问题:用户看到的是 [工具成功] list_plans\n{...} 而非自然语言总结;与经典 Agent Loop 语义不符(工具结果应回到 LLM 生成最终回答)。
目标:改为经典 ReAct 模式——工具执行结果追加进 messages 后,必须再经 intent_recognition 调用 LLM,由 LLM 决定继续调工具、结束对话或进入「待确认写库」路径。
2. 图结构(与 agent/graph.py 一致)
- 入口:默认
intent_recognition;confirm=true预处理注入_confirm_request时入口为confirm_recovery(见get_entry_node)。 - 边:
confirm_recovery→execute_tools;intent_recognition→ 条件边 →persistence|execute_tools|response;persistence→response仅这一条出边(不得再连intent_recognition,否则会同轮并行、绕过 App 确认,见《Agent状态持久化》2.10);execute_tools→stage_mutating_pending|intent_recognition;stage_mutating_pending→persistence;response→END。
3. 执行流程(简图)
┌─ chat / error ──────────────────────► response ─► END
│
intent_recognition ─┤─ execute_confirmed(App 已 confirm)──► execute_tools
│ │
│ └─►(完毕)route_after_execute_tools
│ ├─ 有 staged_mutating_calls ─► stage_mutating_pending ─► persistence ─► response ─► END
│ └─ 否则 ─────────────────────► intent_recognition(再调 LLM,形成闭环)
│
├─ tool_call_staged_reads ─► execute_tools ──(同上)
│
├─ tool_call / mixed:需确认且 pending 非空 ─► persistence ─► response ─► END
│ (本轮结束;等用户 confirm 下一次请求)
│
└─ tool_call / mixed:只读或已带 confirmed ─► execute_tools ──(同上)
说明
execute_tools之后:默认回到intent_recognition,把新追加的ToolMessage交给 LLM,再继续 ReAct;仅 先读后写 时先进入stage_mutating_pending,把写操作挪入pending再persistence。persistence之后:只到response,本 HTTP 请求结束;真正执行写库在后续confirm=true请求:入口confirm_recovery→execute_tools→ 再intent_recognition。
4. 关键路由逻辑
4.1 route_after_intent(intent_recognition 出发)
| 条件 | 目标 | 说明 |
| _loop_after_tools 且 pending_tool_calls 非空 | execute_tools | 刚从工具侧返回且仍有待执行调用(如 confirm 注入;以合并后状态为准) | | _loop_after_tools 且 pending 为空 | response | 工具链在本轮收束,直接收束输出 | | intent == "execute_confirmed" | execute_tools | App 确认后必须执行 | | intent == "tool_call_staged_reads" | execute_tools | 先执行只读批次 | | intent in ("chat","error") 等 | response | 纯对话或错误展示 | | needs_confirmation 且 pending_tool_calls 非空 | persistence | mutating 需落库待确认 | | intent 为 tool_call / mixed / client_tool 且可自动跑服务端工具 | execute_tools | 只读白名单或已 confirmed 与 pending 对齐 |
intent_recognition_node 入口会 pop 掉 _loop_after_tools;主循环通常是:execute_tools → intent_recognition(LLM)→ 按 intent / needs_confirmation 再路由。
4.2 route_after_execute_tools(execute_tools 出发)
| 条件 | 目标 |
| staged_mutating_calls 非空 | stage_mutating_pending | | 否则 | intent_recognition(经典 ReAct:带着 ToolMessage 再问过 LLM) |
5. 状态标记 _loop_after_tools
execute_tools 在返回里置 _loop_after_tools=True,供 route_after_intent 识别「刚从工具节点出来」的分支;intent_recognition 开头会清除该标记,避免与下一轮混淆。与 仅以 intent 字段路由 的组合以线上 graph.py + 日志 为准。
6. 工具执行后的状态清理
execute_tools 在一轮成功路径末尾会 pending_tool_calls / confirmed_tool_calls 等置空并清除 needs_confirmation(具体见 execute_tools_node),避免死循环;待确认会话的权威来源是 NoSQL persistence 写入,不在此重复携带 pending 进下一轮 SSE。
与 App 契约:persistence_node 合并回图状态时应保留或设置 needs_confirmation: true(仅清空内存侧 pending_tool_calls),以便 STATE_SNAPSHOT 触发确认 UI(详见《Agent状态持久化 2.9 / 2.10》)。
7. 确认机制与 ReAct 的衔接
- 提案轮:
intent_recognition→persistence(waiting_confirmation+pending_tool_calls入文档库)→response→ 本轮结束。 - 确认轮:新请求
confirm=true→confirm_recovery(必要时从文档库恢复 checkpoint)→execute_tools→intent_recognition→ 继续循环或response。
不在 intent_recognition 内根据 waiting_confirmation 自动当「已确认」执行写工具,避免与确认 UI 脱节。
8. 实际踩坑记录(格式见 docs/开发记录博客/agents.md)
以下为 2025–2026 年联调/真机路径上真实出现的问题,按仓库约定的博客结构(问题背景 → 问题列表 → 根因链 → 方案 → 采纳 → 用例 → 小结)整理,便于与《Agent状态持久化》§2.10 / §2.11 交叉索引。
8.1 问题背景
用户表述类似「给我创建一个计划,晨跑」时,观测到:
- 未在 App 内点「确认执行」,
plans表已新增行(create_plan已走 RDB 写入)。 - 无确认条 / 未触发「待确认」交互(与产品「先提案、确认、再写库」不一致)。
- 落库的
title为整句「给我创建一个计划,晨跑」之类,而非短标「晨跑」。
8.2 问题列表
8.2.1 未确认即执行写库(同轮偷跑 create_plan)
根因(证据级)
- why1:bug:同一次
POST /send-message内完成 待确认写库。因为persistence写出waiting_confirmation后,仍在本轮内触发了execute_tools中的create_plan。 - why2:为什么会这样——
persistence曾同时有无条件出边 →response与intent_recognition(agent/graph.py编译结果中两边并存),LangGraph 可在同一推进步内 并行调度两后继。 - why3:为什么并行会导致写库——并行的
intent_recognition读文档库看到status: waiting_confirmation,旧代码把pending_tool_calls直接当已确认,路由execute_confirmed→execute_tools,无需用户再发confirm=true。 - why4(辅助证据):云上日志可在
[persistence_node] <<< OK ... waiting App confirm之后立刻出现[execute_tools_node] INVOKE ... create_plan(且该次请求体 非 confirm 流)。
方案(每题不超过两个)
- 短期方案:① 去掉
persistence → intent_recognition,只保留persistence → response;② 删除intent_recognition内「读 DBwaiting_confirmation即自动execute_confirmed」及类似needs_confirmation ∧ pending直执行分支。
优点:改动集中、与集成测「提案轮不执行写工具」一致。缺点:强依赖 App 确认请求继续携带 confirm + 约定 messages。
- 长远方案:对
persistence后继使用显式条件边(或合并为单后继),从拓扑上消灭「多无条件边」歧义。
优点:可读、可审计。缺点:要梳理所有进入 persistence 的场景。
采纳方案
- 已采纳 短期方案(
shenmiren-agent/agent/graph.py、agent/nodes/intent.py)。与 NoSQL 会话字段、RLS 侧_openid等交叉说明见《Agent状态持久化》§2.10。
补充用例
tests/test_e2e_multiturn_create_plan.py、tests/test_e2e_http.py:创建提案轮断言last_snapshot(..., "needs_confirmation") is True,且 confirm 前不因并行执行而出现[工具成功] create_plan。test_llm_create_plan_confirm_after_process_restart_restores_from_nosql:进程重启后仅靠 confirm 仍须能落库。
8.2.2 App 不展示「确认执行」(needsConfirmation 恒 false)
根因(证据级)
- why1:bug:前端不接收到「需要确认」。因为
zhiyuxing-app/src/lib/agent-chat.ts约定:仅当 SSESTATE_SNAPSHOT对象上显式带有needs_confirmation时才置needsConfirmation/onNeedsConfirmation。 - why2:为什么快照里没有——旧版
persistence_node合并回图状态时将needs_confirmation设为false(本意是防 intent 误判,结果连 App 依赖的快照标志一并关掉)。
方案
- 短期方案:
persistence_node返回更新为needs_confirmation: True(仍pending_tool_calls: None,避免重复进 persistence)。
优点:与现有 App 协议对齐。缺点:checkpoint 中该位需由 confirm/执行完成流正确清掉(见 execute_tools_node)。
- 长远方案:独立 AG-UI / SSE 「pending_tools」事件,不完全依赖快照布尔字段。
优点:契约稳定。缺点:要动 SDK 与客户端解析。
采纳方案
- 已采纳 短期方案(
agent/nodes/persistence.py)。与 §8.2.1 同一轮修复可一并验证。
补充用例
- 真机:流结束前的
STATE_SNAPSHOT中needs_confirmation === true;日志streamAgentChat.toolCalls/needsConfirmation。
8.2.3 计划标题整句入库(非短标题「晨跑」)
根因(证据级)
- why1:bug:
plans.title为「给我创建一个计划,晨跑」等。因为模型把整句填进plan_title。 - why2:为什么执行层没纠正——
enrich_create_plan_calls_from_messages在plan_title已有值时不再覆盖,此前只补「空参」场景。
方案
- 短期方案:在
execute_tools调用工具前对create_plan做normalize_plan_title_for_create(同时参考 模型参数 与 最近一条用户话 的首行)。
优点:与 App new-plan.tsx 人工建计划的短标题体验一致。缺点:依赖中文话术启发式,需随反馈加规则或用例。
- 长远方案:工具 schema 强约束 + 失败重试 / 小型二次结构化抽取。
优点:少魔术字符串。缺点:成本与延迟。
采纳方案
- 已采纳 短期方案(
agent/tool_calls_util.py,tests/test_plan_title_normalize.py)。持久化/RLS 字段侧见《Agent状态持久化》§2.11(及plans._openid与 App RLS 的讨论)。
补充用例
pytest tests/test_plan_title_normalize.py;confirm 后list_plans核对最新行title。
8.3 小结(本组踩坑)
- 多无条件出边(
persistence→ 两后继)+ DB 恢复即当已确认 → 同轮绕过确认写库;单出边至response+ 移除 intent 内偷跑分支 后消除。 needs_confirmation必须在 persistence 后的快照中为 true,否则 App 不展示确认条。- 标题规范化与图拓扑无关,但与「创建计划」用户路径同时暴露,故在同一文档记录;详细会话/表字段级叙述以《Agent状态持久化》为准。
9. 小结(改造目标回顾)
- 经典循环:工具结果 →
intent_recognition(LLM)→ 再决策,而非工具后直接response。 - 拓扑约束:
persistence只连response,保证待确认写库不会在同一send-message内被并行intent偷跑执行。 - 先读后写:
stage_mutating_pending→persistence→response,写操作仍在用户确认后执行。
参考:shenmiren-agent/agent/graph.py、《开发记录博客/Agent状态持久化(中断需用户确认场景).md》(§2.10 / §2.11)、docs/开发记录博客/agents.md(博客结构模板)。