🐎 第5章 防微杜渐:Hooks 事件驱动自动化
📄 一句话概括

钩子挂在工具调用后面,它慢一次,整段就慢一次。异步是唯一的解法——但异步钩子返回不了决定。 所以能不能异步,判据只有一条:这个钩子的失败会不会影响正确性?不会就异步,会就必须等。

📝 本章和前面几章的关系

上一节讲子智能体门口那两道钩子,这一节讲一个更普遍的问题:钩子本身要花时间。 这一节有真跑的计时——用一条耗时三十毫秒的钩子,挂在十次调用后面,两种跑法各测一遍。

一、慢是怎么被放大的

工具调用后这个事件,一次会话要触发九次左右(5.2 节算过)。 所以挂在它上面的钩子,慢三十毫秒就是两百七十毫秒,慢三百毫秒就是两秒七。 而钩子通常是「顺手做一件小事」:写日志、发通知、上传统计。这些事本身就不该让人等。

演示里用真跑计时:一条耗时三十毫秒的钩子,挂十次。同步累计三百零九毫秒,异步零点零一毫秒。 同步那十次里,几乎全是纯粹在等钩子;异步那边这个数接近零。

二、异步丢掉了什么

丢掉的是「结果」。异步钩子返回不了决定——它起的是后台进程,主流程不等它,也就拿不到它的返回。 所以它不能拦任何东西。拦必须当场判,当场判就必须等。 它也不能「注入」:注入的内容要在下一步之前进上下文,异步来不及。

它也不能「门控」:门控要决定「能不能结束」,那是个必须当场给答案的问题。 所以能异步的只有一类:只记录、不影响主流程的动作。审计、上报、统计、备份。

三、还有一个坑:进程被一起带走

异步钩子起的是后台进程。会话一结束,父进程退出,它可能被一起带走——最后几条日志就丢了。 所以异步钩子要么写自己的日志文件(丢了也能补),要么在会话结束时等一次收口。 这条坑很隐蔽:平时看不出问题,出事的时候才发现最后那几条记录没了——而那几条往往最关键。

处理办法有两个:一是让异步钩子自己落盘,不依赖父进程;二是在会话结束事件里等一次。 两个办法不冲突,成熟的做法是两个都做。

四、判据:失败会不会影响正确性

这个问题一问,答案基本就出来了。 审计写失败——不影响这次改动的正确性,可以异步。 格式化失败——影响,文件就是没格式化,必须同步。

测试门控失败——影响,可能把没测过的代码交出去,必须同步。 拦截失败——影响,危险命令就执行了,必须同步。 所以四类钩子里,只有「记录类」能异步。 其余三类都得等——而它们的等待通常很短,因为能用规则判的活本来就快。

五、一折中:慢的同步钩子该怎么办

有一种情况两难:这个钩子必须同步(影响正确性),但它很慢(比如要跑一次完整测试)。 办法是缩小它的作用域:只跑受影响的测试,不跑全量。用改动的文件去筛测试集。 这条在工程上很实用:全量测试三分钟、增量测试十五秒,门控就变得可接受了。

另一个办法是换位置:把「必须跑全量」的那次放到提交时(一个更稀疏的事件),而不是每次工具调用后。 一句话:慢不是异步的理由,不影响正确性才是。慢的同步钩子,该改的是它的工作量。

六、动手 Demo:一条耗时钩子,同步跑和异步跑差多少

真跑计时:一条耗时三十毫秒的钩子,挂在十次工具调用后面。同步跑一遍、异步跑一遍。 再看异步换来了什么、丢掉了什么。

def slow():
    time.sleep(0.03)          # 一条三十毫秒的审计钩子

for _ in range(10):
    slow()                    # 同步:十次都等完

实跑输出:

--------------------------------------------------------------------
一条耗时钩子,同步跑和异步跑差多少
--------------------------------------------------------------------

  审计钩子要写日志、发通知,天然慢。挂在 PostToolUse 上,它慢一次,整段工作就慢一次。
  这里用真跑计时:一条耗时三十毫秒的钩子,挂在十次工具调用后面,同步与异步各跑一遍。

  ① 同步:每一次都等
  --------------------------------------------------------------------

  ② 异步:发起就跑,不等
  --------------------------------------------------------------------
| 做法 | 十次调用总耗时(毫秒) | 说明 |
|---|---:|---:|
| 同步 | 308.4 | 十次都等完 |
| 异步 | 0.01 | 发起就返回 |

  同步累计 308.4 毫秒,异步 0.01 毫秒。同步那十次里,几乎全是纯粹在等钩子;异步那边这个数接近零。
  注意这个差距跟钩子本身快慢成正比:钩子越慢,同步的代价越大。
  一条三百毫秒的钩子挂在每次工具调用后面,一次会话就能拖出十几秒。

  ③ 异步换来了什么、丢掉了什么
  --------------------------------------------------------------------
  换来的是主流程不被拖住。你敲一句、它答一句,感觉不到后台在写日志。
  丢掉的是「结果」:异步钩子返回不了决定。它不能拦任何东西——拦必须当场判。
  所以能不能异步,取决于这个钩子要不要影响主流程。审计、上报、统计可以;拦截、注入、门控不行。

  ④ 还有一个坑:进程还没跑完,会话结束了
  --------------------------------------------------------------------
  异步钩子起的是后台进程。会话一结束,父进程退出,它可能被一起带走——最后几条日志就丢了。
  所以异步钩子要么写自己的日志文件(丢了也能补),要么在会话结束时等一次收口。
  ! 判据很简单:这个钩子的失败会不会影响正确性?不会,就异步;会,就必须同步等它。

①同步累计三百零九毫秒,异步零点零一毫秒。同步那十次里,几乎全是纯粹在等钩子; 异步那边这个数接近零。 这个差距跟钩子本身快慢成正比:钩子越慢,同步的代价越大。 一条三百毫秒的钩子挂在每次工具调用后面,一次会话就能拖出十几秒。 ②异步丢掉的是「结果」:它起的是后台进程,主流程不等它,就拿不到它的返回。 所以它不能拦、不能注入、不能门控。 能异步的只有一类:只记录、不影响主流程的动作——审计、上报、统计、备份。 ③还有一个坑:进程被一起带走。会话一结束父进程退出,后台钩子可能跟着没了,最后几条日志就丢。 这条很隐蔽:平时看不出问题,出事时才发现最后那几条记录没了——而那几条往往最关键。 所以要让它自己落盘,或者在会话结束事件里等一次收口。 ④判据只有一条:这个钩子的失败会不会影响正确性?不会就异步,会就必须同步等它。 所以四类钩子里只有「记录类」能异步。其余三类都得等,而它们的等待通常很短——能用规则判的活本来就快。

30 秒实验

  1. 把钩子耗时从三十毫秒改成三百毫秒,重跑一遍。同步累计变成多少? 按一次会话九次调用算,会拖出多少秒?
  2. 把十次改成一次,重跑一遍。同步和异步的差距还值得管吗? 这说明「挂在哪个事件上」比「用什么跑法」更要紧?
  3. 给异步钩子加一条「会话结束时等它收口」,重跑一遍。它解决了什么?代价是什么?

常见误区

  1. 为了快把拦截钩子改成异步。异步返回不了决定,拦不住。
  2. 把注入钩子改成异步。注入要在下一步之前进上下文,异步来不及。
  3. 异步钩子不落盘,靠父进程带着跑。会话一结束它可能被一起带走,最后几条日志就丢。
  4. 以为「慢」就该异步。判据是「失败会不会影响正确性」,不是「快不快」。
  5. 让同步钩子跑全量测试。该改的是它的工作量(增量测试),不是把它变异步。
自测三问(点开对答案) 1. 异步钩子为什么不能拦? —— 因为拦必须当场给答案,而异步起的是后台进程、主流程不等它,拿不到它的返回。 2. 能异步的钩子有哪一类? —— 只记录、不影响主流程的动作:审计、上报、统计、备份。四类里只有这一类能异步。 3. 异步钩子的进程被一起带走,会有什么后果? —— 最后几条日志会丢,而出事的时候恰恰是那几条最关键。 所以要让它自己落盘,或者在会话结束事件里等一次收口。

跑法定了,下一节讲最不好受的一段:钩子不报错,所以出问题怎么查。

相关笔记