loop中hooks的设计

3 阅读3分钟

agent_runner_loop 8 个 Hooks 详细分析

1. agent_before (第 49 行)

触发时机: 整个 agent 任务开始前,进入 while 循环之前
当前用途:

  • Langfuse 追踪: 创建顶层 agent.task trace observation,记录 user_input
  • Project Mode: 在 user message 末尾追加项目上下文(L1 层注入),包括项目规则、记忆文件指针、收尾纪律

优化空间:

  • ✅ 两个插件职责清晰,互不干扰
  • ⚠️ 当前 ctx 是 locals() 快照,project_mode 直接 mutate ctx['messages'] 列表来注入内容,这种副作用式注入依赖 messages 是 list 引用的事实,比较脆弱。如果未来 messages 变成不可变结构会出问题

2. turn_before (第 57 行)

触发时机: 每个 turn 开始,LLM 调用之前
当前用途: 目前没有任何插件注册此 hook,是预留的空 hook

优化空间:

  • ⚠️ 完全空置,可以考虑删除,或者明确文档化它的用途
  • 💡 潜在用途:turn 级别的限流、安全检查、上下文压缩触发点

3. llm_before (第 58 行)

触发时机: 每次 LLM 调用前
当前用途:

  • Langfuse 追踪: 创建 llm.chat generation span,记录输入 messages(截断到 20000 字符)

优化空间:

  • ✅ 职责单一
  • ⚠️ str(ctx.get('messages', ''))[:20000] 这种粗暴截断可能丢失关键信息,可考虑更智能的截断策略(如保留 system prompt 开头 + 最后几条 user message)

4. llm_after (第 67 行)

触发时机: LLM 返回响应后,工具执行前
当前用途:

  • Langfuse 追踪: 结束 generation span,记录 response 内容(截断到 20000 字符)和 usage_details

优化空间:

  • ✅ 职责单一
  • ⚠️ 同样存在截断问题。另外 usage_details 依赖 _tls._usage,这个变量是在 SSE parser wrapper 中异步填充的,存在时序风险——如果 parser 还没跑完,usage 可能为空

5. tool_before (第 22 行,在 BaseHandler.dispatch 内)

触发时机: 每个工具调用前
当前用途:

  • Langfuse 追踪: 创建 tool span,记录工具名和参数(过滤掉 _ 开头的内部参数)

优化空间:

  • ✅ 职责单一
  • ⚠️ 参数过滤 {k: v for k, v in args.items() if not k.startswith('_')} 是合理的,但要注意某些工具可能确实有以 _ 开头的业务参数

6. tool_after (第 24 行,在 BaseHandler.dispatch 内)

触发时机: 每个工具执行完成后
当前用途:

  • Langfuse 追踪: 结束 tool span,记录返回值 ret.dataret.next_promptret.should_exit

优化空间:

  • ✅ 职责单一
  • ⚠️ 当前只记录 ret(StepOutcome),但如果工具是 generator(yield 中间结果),中间输出没有被追踪到

7. turn_after (第 103 行)

触发时机: 每个 turn 完全结束后(工具执行完、next_prompt 确定后)
当前用途: 目前没有任何插件注册此 hook,是预留的空 hook

优化空间:

  • ⚠️ 完全空置,与 turn_before 一样是预留位
  • 💡 潜在用途:turn 级别的统计(token 消耗汇总)、异常检测、自动保存 checkpoint

8. agent_after (第 106 行)

触发时机: 整个 agent 任务结束后(while 循环退出后)
当前用途:

  • Langfuse 追踪: 结束顶层 trace,记录 exit_reason,flush 上报
  • Eval 评估: 轨迹匹配 + 终态断言 + 挂分 Langfuse + 落盘

优化空间:

  • ✅ 两个插件职责清晰
  • ⚠️ eval 插件的耦合风险_eval_trajectory 通过 handler.parent.llmclient.backend.history 深层链访问 history,这是 4 层属性穿透(handler → parent → llmclient → backend → history),极其脆弱,任何一个中间对象为 None 就会静默跳过。建议改为通过 hook ctx 直接传递 history 引用
  • ⚠️ Langfuse flush 时机_on_agent_after 中调用 _lf.flush() 是同步阻塞的网络 IO,如果网络慢会拖慢整个 agent 结束流程。可考虑异步 flush 或移到后台线程