M1 · AgentLoop 主循环与终止安全#
简历 Bullet Point: 设计 Think→Act→Observe→Reflect 有状态 AgentLoop,四层终止机制(深度闸 / 超时+迭代上限 / 结果截断 / 循环检测)替代 prompt 祈祷确保弱模型不死循环不爆 token;从固定购物链路重构为意图驱动能力菜单(planner 输出 tasks 按需组合工具子集),planner 确定性预置省一轮 LLM 往返
开场钩子#
场景#
项目早期用 Flash 系列做子 Agent 跑端到端测试,五个平台并行检索”旅行三件套”。其中两个子一直不收尾——不停换关键词重搜 item_search。更糟的是第五个子自己调了 shopping_summary 输出完整推荐清单——主 loop 期望 JSON 候选列表,拿到的是自然语言文案。5 分钟超时,用户什么都没拿到。
这次事故逼出了核心设计原则:对 LLM 的行为约束,prompt 是辅助,机制才是兜底。
面试官切入#
“你简历上写了’四层终止机制’——能展开讲讲为什么 prompt 不够用,需要四层机制?“
一、模块运作流程#
1.1 一句话定位#
主 AgentLoop 是系统执行核心:有状态 Think→Act→Observe→Reflect 循环,不写死步数,模型自判够了就调终结性工具收尾。四层机制兜底弱模型。
1.2 全景流程图#
run_agent() 入口
├─ thread_scope + Harness on_session_start
├─ 读 Store 注入长期偏好 + P_t
├─ _build_main_agent(FULL_TOOL_SET, system_prompt, middleware)
│
▼
主 AgentLoop
├─ awrap_model_call:
│ ├─ pre_think Hook(压缩 + 预算路由 + reasoning_boost 首轮开思考)
│ ├─ 调模型(Think)
│ └─ post_reflect Hook(漂移检测 + 终结纪律 + 断言汇总)
│ └─ 模型没调工具就想收尾?→ retry_nudge 当场重发
├─ awrap_tool_call:
│ ├─ pre_tool_call Hook(硬闸链 8 道,priority 5→48)
│ │ └─ 被拦?→ 哨兵消息回模型,工具不执行
│ ├─ 执行工具(Act)
│ ├─ post_tool_call Hook(截断 + 循环检测 + 收尾催促)
│ └─ Observe:模型读取工具返回
├─ Reflect:够了 → 调终结性工具;不够 → 回 Think
│
▼
终结性工具被调用 → terminal_reached=True → 后续任何工具一律拦下
├─ items_preview 事件(商品卡先出)
├─ summary_delta 流式文案 → WebSocket 逐字渲染
├─ Harness on_session_end(输出审核)
└─ curator 扫本轮 → 更新偏好plaintext1.3 分步详解#
主循环组装
_build_main_agent() 核心调用:create_agent(model, tools=FULL_TOOL_SET, system_prompt, middleware)。主与子用同一份工具集(13 个)、同 prompt、同中间件栈。区别只在 thread_id 隔离和模型档位(子用 get_fast_llm() 关 reasoning)。
终结性工具
TERMINAL_TOOLS = {"shopping_summary", "chat_fallback"}。LangGraph 检测到被调用后不再调度下一轮。留最后一轮让模型把结构化结果转成收尾文案,自然终止不丢文案。
四层终止机制
| 层 | 机制 | 解决的失控类型 |
|---|---|---|
| ① 深度闸 | MAX_FORK_DEPTH=1,ContextVar 跟踪 | 递归 fork 指数爆炸 |
| ② 超时+迭代上限 | 主 300s/30 轮,子 90s/6 轮 | 跑太久/转太多圈 |
| ③ 结果截断 | 每次工具返回 ≤4000 token | 单工具撑爆上下文 |
| ④ 循环检测 | 滑动窗口 6 次内同名工具 4 次 | 反复刷同一工具 |
四层互不依赖、层层递进。④ 是软提示(不硬停),不行还有 ② 硬的兜底。
planner 确定性预置
开局第一轮模型几乎 100% 会调 planner,所以直接在 run_agent 入口确定性预置 planner 调用,省掉”模型花一轮只为说出 planner 这个词”的 LLM 往返。延迟减法的一刀。
reasoning_boost
pre_think Hook 在第一轮开 reasoning(开思考、降温度),后续轮关闭。因为第一轮意图拆解最需要推理,后续轮工具驱动不需要深度思考。追问轮 reuse 复用候选时也不开 reasoning。
收尾流式
shopping_summary 调用后不等模型完整生成收尾文案再推送——items_preview 事件先推商品卡(先出货),summary_delta 事件流式推文案(后出文案)。用户感知延迟大幅降低。
1.4 技术选型决策表#
| 组件 | 选了什么 | 为什么选它 | 什么情况下换 |
|---|---|---|---|
| Agent 框架 | LangGraph create_agent | 原生 middleware + recursion_limit + 终结工具检测 | 需条件分支/审批节点 → StateGraph |
| 终止策略 | 终结工具 + 四层兜底 | 模型自判收尾(正常)+ 机制保底(异常) | 确定性极高 → 固定步数 DAG |
| 循环检测 | 滑动窗口(6,4) | 5 平台各搜 1 次不误判 | 工具种类极多 → embedding 行为相似度 |
| 控制面禁用 | 执行层哨兵(工具表恒定) | 保 prompt cache 前缀匹配 | 不依赖 cache → 摘工具更彻底 |
二、踩坑实录#
坑 1:Prompt 级终止纪律对弱模型约等于”祈祷”#
- 现象:Flash 做子 Agent 无视”找够用就停”,重搜 7 圈或自行调终结工具输出推荐文案。
- 修法:四层 fork 安全 + 阶段状态机 + 检索预算。prompt 降级为辅助。
- 教训:机制打底,prompt 辅助。
坑 2:recursion_limit 是 super-step 数不是迭代数#
- 现象:设 30 只跑了 15 轮被截断。
- 修法:
recursion_limit = MAX_ITERATIONS * 2 + 1(一轮 = 两个 super-step + 1 最终响应)。 - 教训:框架参数语义不要想当然,看源码怎么消费的。
坑 3:<termination> 放 prompt 开头等于没写#
- 现象:终止规则放 prompt 开头,5-6 轮后被工具结果”稀释”,模型不收尾。
- 修法:挪到末尾 +
item_picker返回时定点注入SUMMARY_NUDGE。 - 教训:prompt 的位置和时机比内容更重要。
坑 4:意图驱动改造时哨兵文案里藏着固定链#
- 现象:prompt 改成能力菜单但模型仍走全链——middleware 5 条哨兵命令”依次 price_compare → shipping_calc → …”。
- 修法:哨兵文案泛化成”按需”,刻意不把 tasks 注入做硬约束。机制管失控,不管品味。
三、验收与量化#
回归测试#
| 场景 | 验证点 | 覆盖机制 |
|---|---|---|
| 递归 fork 被拦 | 子调 dispatch_tool → 深度超限错误 | ①深度闸 |
| 子超时/超限 | 90s/6 轮 → 异常转字符串回传 | ②超时+迭代 |
| 结果截断 | >4000 token → 尾部截断 | ③截断 |
| 循环检测 | 窗口内同工具 4 次 → 换策略提示 | ④循环 |
| 终结纪律重发 | 纯文字收尾 → 追加提示重发 | terminal_enforcer |
| 终结后硬停 | 调完终结工具再调 → 哨兵拦下 | terminal_reached_gate |
| 子越权被拦 | 子调 shopping_summary → 深度闸拦 | depth_gate |
四层机制加入后,弱模型子 Agent 的”不收尾死循环”和”越权自行终结”类失败从 ~30% 降到零——不是弱模型变听话了,是每次越界都被机制拦住。
四、面试问答#
Q1: 主循环怎么判断”该终止了”?#
正常路径:模型自判调终结性工具。异常路径:四层机制逐层兜底。两条互不依赖。
追问:循环检测是硬中断还是软提示? 软提示——5 平台各搜 1 次不是循环。软的不行有超时/迭代硬的兜底。
Q2: 只想推荐几个商品却比价算运费全跑——是披着 loop 的 workflow?#
早期确实半 workflow 半 loop。流程次序没有一行代码强制,但 prompt 和哨兵隐含固定链。改造成意图驱动后 planner 输出 tasks,模型按需组合。哨兵文案也从”必四步”泛化成”按需”。
Q3: 四层之外还有什么防失控?#
三道”动机层面的防护”——fork 预算(max_parallel=1)、检索预算(TREE_RETRIEVAL_BUDGET=8,module dict 按 session_dir 共享)、token 预算(三级 ok/soft/hard)。和四层正交:四层管”怎么失控”,三道管”为什么失控”。
追问:检索预算为什么不用 ContextVar? 子 set 了新值父读不到——asyncio.create_task 复制 context 快照。改 module dict + session_dir 做 key。隔离用 ContextVar,共享用 module dict。
Q4: terminal_enforcer 为什么必须当轮重发?#
模型没产 tool_call,框架判定”该结束了”——不会有下一轮。所以 post_reflect 置 retry_nudge,同一次 awrap_model_call 里追加提示重发。子 Agent 不受约束——子的正常收尾就是吐文字返回主 loop。
Q5: 哨兵拦截 vs 摘工具——浪费一次 tool_call 值吗?#
值。摘工具改 prompt 内容破坏 cache 前缀。cache miss 多付 ~11000 token 输入费 vs 一次无效 tool_call ~50-100 token。statewright 说”缩小工具空间效果更好”——哨兵实现了等效效果,选择空间被等效缩小,只是”调了被拦”而非”看不到”。
五、前沿概念#
5.1 AgentLoop vs Workflow vs ReAct vs Plan-and-Execute#
AgentLoop 核心优势是自适应步数 + 收敛判断。配合意图驱动,同一套代码处理 2 步品类查询和 15 步跨平台全链路。Workflow 步数写死;ReAct 缺 Reflect 收敛判断;Plan-and-Execute 计划可能过时。
5.2 机制优于 Prompt#
代码级确定性 + prompt 灵活性互补。prompt 覆盖正常路径(80%),机制兜底异常(20%)。
5.3 终结性工具模式#
把”终止判断”转化为”工具选择”——模型选工具的能力远强于遵守格式约定。shopping_summary 既是终结信号又是功能工具,一个工具承担”判断够了”和”产出结果”两个职责。
六、诚实边界#
| 维度 | 做了 | 没做 | 什么时候该做 |
|---|---|---|---|
| 终止安全 | 四层机制 + 终结纪律 + 终结后硬停 | 纯 prompt 方案已验证不可靠 | — |
| 意图驱动 | planner tasks + 能力菜单 + 哨兵泛化 | middleware 不感知 tasks | 偶发方差影响分数时 |
| 循环检测 | 滑动窗口同名工具计数 | 语义行为相似度检测 | 工具种类极多时 |
| checkpointer | 未挂 | 中途续跑/human-in-the-loop | 需可恢复任务时 |
| 多模型校准 | 参数对 Flash/GPT 有实测调优 | 无 per-model 自动参数调节 | 接入新模型时 |