# Hook 系统在 Lambdagent 体系下的实现分析 > 基于 Paper10 操作语义框架(`src/lambdagent/`)的设计分析 > 日期:2026-04-01 --- ## 一、Hook 的形式化本质 "Hook" 在操作语义层面是在特定 CEK 转移规则触发的时刻插入额外计算。 更精确地,一个 Hook 是一个 **Term 变换器** $H$: $$H : \text{Term} \to \text{Term}$$ 它将原始 Agent $a$ 包装成 $H(a)$,使得在 $a$ 的 β-规约的某个时刻(之前、之后、或条件判断中),$H$ 注入的计算被执行。 **关键发现**:这个定义与 Lambdagent 的现有原语完全对齐—— | Hook 类型 | 对应 Lambdagent 原语 | CEK 触发规则 | |---|---|---| | Pre-hook(执行前拦截)| `Compose(Tool(pre_fn), agent)` | C-Comp + C-Tool | | Post-hook(执行后处理)| `Compose(agent, Tool(post_fn))` | C-CompRet + C-Tool | | Guard-hook(条件拦截/阻断)| `Guard(agent, validator, k)` | C-Guard + C-GuardOK/Retry/Fail | | Event-hook(观察但不干预)| CEK Machine 的 Transition trace | 所有规则 | --- ## 二、三种实现策略 ### Layer 1:Term-Level Hook(纯 Lambda 演算,零基础设施改动) Hook 直接编码为现有原语的组合,**今天就能实现**: ```python from lambdagent.primitives import Lam, Compose, Tool from lambdagent.extensions import Guard # Pre-hook: 执行前校验/记录 def pre_hook(fn): """装饰器:在 agent 执行前运行 fn(不修改输入)""" def wrapper(agent): side_effect = Tool("pre_hook", lambda x: (fn(x), x)[1]) # pass-through return Compose(side_effect, agent) return wrapper # Post-hook: 执行后处理/记录 def post_hook(fn): """装饰器:在 agent 执行后运行 fn(可修改输出)""" def wrapper(agent): transformer = Tool("post_hook", fn) return Compose(agent, transformer) return wrapper # Blocking-hook: 条件阻断(等价于 Guard) def blocking_hook(predicate, retry=0): """装饰器:输出不满足 predicate 时重试或阻断""" def wrapper(agent): return Guard(agent, predicate, k=retry) return wrapper ``` **使用示例**: ```python import logging @pre_hook(lambda x: logging.info(f"[PreHook] input={x}")) @post_hook(lambda x: x.upper()) @blocking_hook(lambda x: len(x) < 500, retry=2) def my_agent(): return Lam("writer", "Write a short summary:", "gpt-4o-mini") # 展开后等价于: # Guard( # Compose( # Tool(log_fn), # Compose(Lam("writer",...), Tool(uppercase)) # ), # predicate=lambda x: len(x) < 500, # k=2 # ) ``` **优点**:符合 Paper10 的操作语义——Hook 就是 Lambda 项,可被形式推理。 **局限**:无法在 Tool/LLM 调用的**内部**(C-Lam 规则触发的瞬间)插入逻辑。 --- ### Layer 2:HookTerm(混合方案,与形式语义完全兼容) 将 Hook 建模为一个**新的 Term**,具有自己的小步规则,在 Paper10 的形式体系内完全可推理: ```python class HookTerm(Term): """ Hook 作为一等 Lambda 项 小步规则(新增 E-Hook*): E-HookPre: ⟨Hook(a,pre,post) v, E, K, σ, c⟩ → ⟨a v, E, HookK(post, v) :: K, σ, c⟩ E-HookPost: ⟨v, E, HookK(post, orig) :: K, σ, c⟩ → ⟨post(v), E, K, σ, c⟩ """ def __init__( self, agent: Term, pre: Callable | None = None, # pre(input) → None (side effect only) post: Callable | None = None, # post(output) → output (可变换输出) event_filter: set[str] | None = None, # {'llm', 'tool', 'guard', ...} ): super().__init__(f"Hook({agent._name})") self.agent = agent self.pre = pre or (lambda x: x) self.post = post or (lambda x: x) self.event_filter = event_filter def apply(self, input, ctx=None): ctx = ctx or Context() self.pre(input) # E-HookPre fires result = self.agent.apply(input, ctx) return self.post(result) # E-HookPost fires ``` 对应两条新的小步规则(可写入 Paper10 §4): ``` E-HookPre: ⟨Hook(a, pre, post) v, E, K, σ, c⟩ —→τ ⟨a v, E, HookK(post, v) :: K, σ, c⟩ (前提: pre(v) 已求值) E-HookPost: ⟨v', E, HookK(post, orig) :: K, σ, c⟩ —→τ ⟨post(v'), E, K, σ, c⟩ ``` --- ### Layer 3:Machine-Level Hook(Observer 模式,扩展 CEK Machine) 在 CEK Machine 的精确位置注入回调,**只需改动 `cek_machine.py` 约 30 行**: ```python from dataclasses import dataclass, field from typing import Callable, List @dataclass class HookRegistry: """ Hook 注册表:在 CEK 转移规则触发点插入回调 事件类型(对应 CEK 规则): pre_llm — C-Lam 触发前(LLM 调用前) post_llm — C-Lam 完成后(LLM 响应后,可修改输出) pre_tool — C-Tool 触发前 post_tool — C-Tool 完成后 on_guard_ok — C-GuardOK on_guard_retry — C-GuardRetry on_guard_fail — C-GuardFail on_loop_unfold — C-LoopUnfold on_step — 每次 CEK 转移后 on_halt — 机器终止时 """ pre_llm: List[Callable] = field(default_factory=list) post_llm: List[Callable] = field(default_factory=list) pre_tool: List[Callable] = field(default_factory=list) post_tool: List[Callable] = field(default_factory=list) on_guard_ok: List[Callable] = field(default_factory=list) on_guard_retry: List[Callable] = field(default_factory=list) on_guard_fail: List[Callable] = field(default_factory=list) on_loop_unfold: List[Callable] = field(default_factory=list) on_step: List[Callable] = field(default_factory=list) on_halt: List[Callable] = field(default_factory=list) def register(self, event: str, fn: Callable): getattr(self, event).append(fn) def fire(self, event: str, **kwargs): for fn in getattr(self, event, []): fn(**kwargs) ``` `_dispatch_app` 中插入钩子点(以 C-Lam 为例): ```python def _dispatch_app(self, op, arg): s = self.state if isinstance(op, Lam): # ★ Pre-LLM Hook self.hooks.fire("pre_llm", term=op, input=arg, state=s) result = op.apply(arg, ctx) # ★ Post-LLM Hook(hook 可通过 dict 修改输出) hook_result = {"value": result} self.hooks.fire("post_llm", term=op, input=arg, output=hook_result, cost=cost_llm, state=s) result = hook_result["value"] s.control = result return "C-Lam", label ``` **实际使用**: ```python registry = HookRegistry() # 成本审计 registry.register("post_llm", lambda term, input, output, cost, state: print(f"[Audit] {term._name}: {cost.tokens} tokens")) # PII 过滤 import re def pii_filter(term, input, output, cost, state): output["value"] = re.sub(r'\b\d{11}\b', '[PHONE]', str(output["value"])) registry.register("post_llm", pii_filter) # Guard 失败报警 registry.register("on_guard_fail", lambda **kw: send_alert(f"Guard failed: {kw['term']._name}")) machine = AgentCEKMachine(hooks=registry) result = machine.run(my_agent, "input") ``` --- ## 三、三层可以并存(不是三选一) ### 正交性 三层的**作用域完全正交**,分别作用于不同的对象: ``` Layer 3: Machine-Level Hook │ 作用域:CEK Machine 的所有转移,全局、跨切面 │ 对象:执行(execution) │ └─► 观察并包含 ↓ Layer 2: HookTerm │ 作用域:特定 Term 的包装,局部、可复用 │ 对象:程序结构(term structure) │ └─► 展开为 ↓ Layer 1: Compose + Tool + Guard 作用域:单次、内联的变换 对象:Lambda 项的代数组合 ``` Layer 1/2 是"程序长什么样",Layer 3 是"程序运行时发生了什么"。 ### Layer 3 "看到" Layer 2 的内部 当 Layer 2 的 `HookTerm` 执行时,它内部的 `pre_fn` 和 `post_fn` 在 CEK Machine 里触发 `C-Tool` 规则,Layer 3 的 `pre_tool`/`post_tool` 钩子会**同样观察到它们**: ``` 执行 HookTerm(Lam("writer"), pre=log, post=upper) 时: CEK 序列: C-Hook → C-Tool(log) ← Layer 3 的 pre_tool hook 也触发 → C-Lam(writer) ← Layer 3 的 pre_llm/post_llm 触发 → C-Tool(upper) ← Layer 3 的 post_tool hook 也触发 ``` ### 唯一的危险:重复触发 如果三层对同一事件做同一件事,会触发多次: ```python # ❌ 错误:三层都在记录同一个 LLM 的输出 agent = Compose(Lam("writer", ...), Tool("log1", log_fn)) # Layer 1 agent = HookTerm(agent, post=log_fn) # Layer 2 再记录 registry.register("post_llm", lambda **kw: log_fn(...)) # Layer 3 再记录 ``` 解决方法:**不同层负责不同关切**。 --- ## 四、推荐的职责划分 | 层级 | 定位 | 典型用途 | 特点 | |---|---|---|---| | **Layer 3** | 基础设施层 | 全局 token 审计、限流、OpenTelemetry、异常上报 | 对业务代码透明 | | **Layer 2** | 策略层 | PII 过滤、安全沙盒、领域特定 retry 策略 | 作为库主动引用 | | **Layer 1** | 业务层 | 一次性输入预处理、快速原型调试打印 | 内联写在业务中 | --- ## 五、并存示例 ```python # ── Layer 3: 全局基础设施(系统级,一次性配置)── registry = HookRegistry() registry.register("post_llm", cost_auditor) # 审计所有 LLM 调用 registry.register("on_guard_fail", pager_duty) # 任何 Guard 失败就报警 machine = AgentCEKMachine(hooks=registry) # ── Layer 2: 领域策略(库级,按需引用)── class MedicalHook(HookTerm): """医疗场景专用:PII 过滤 + HIPAA 合规日志""" def __init__(self, agent): super().__init__( agent, pre=lambda x: verify_patient_consent(x), post=lambda x: strip_pii(x), ) # ── Layer 1: 业务逻辑(业务级,内联)── diagnosis_agent = MedicalHook( # Layer 2 包装 Compose( Tool("fetch_ehr", get_records), # Layer 1 inline Lam("diagnoser", MEDICAL_PROMPT), Tool("format_icd", icd_encoder), # Layer 1 inline ) ) # Layer 3 的 registry 自动观察整个执行过程 result = machine.run(diagnosis_agent, patient_id) ``` 执行时每一层做自己该做的事: ``` Layer 3 看到:fetch_ehr → verify_consent → diagnoser_llm → strip_pii → format_icd └─ cost_auditor 汇总所有 LLM token ─────────────────────────────────┘ Layer 2 做了:verify_consent(pre)和 strip_pii(post) Layer 1 做了:fetch_ehr 和 format_icd 的数据流组合 ``` --- ## 六、与 Claude Code Hook 系统的映射 Claude Code 的 Hook 事件类型与 Lambdagent CEK 规则的精确对应: | Claude Code Hook | Lambdagent 对应 | CEK 触发点 | |---|---|---| | `PreToolUse` | `HookTerm(tool, pre=fn)` | C-Tool 之前 | | `PostToolUse` | `HookTerm(tool, post=fn)` | C-ToolRet 之后 | | `Stop` | `Guard(agent, λv.False, 0)` | C-GuardFail | | `Notification` | `Tool(notify_fn)` in trace | on_step callback | | `SubagentStop` | Loop bound check | C-LoopBound | --- ## 七、形式化角度:代数效果的三层 这个结构在 PL 理论里对应**代数效果(Algebraic Effects)的处理器栈**: ``` Effect handlers (Layer 3) ← 全局 handler,捕获所有 perform ↕ Effect operations (Layer 2) ← 局部 perform,可复用 ↕ Pure terms (Layer 1) ← 无效果的纯变换 ``` 三层天然并存,因为它们描述的是**同一个计算的不同视角**,不是同一个问题的三个备选解法: - 选一层 → 只有一个视角 - 三层并存 → 完整的可观测性(Layer 3)+ 可复用性(Layer 2)+ 灵活性(Layer 1) --- ## 八、结论 > **Hook 系统不是 Lambdagent 体系之外的东西,它本身就是 Lambda 演算的 `Compose`(函数组合)和 `Guard`(依赖类型验证)的特例。** Claude Code 的 Hook 系统在形式上等价于在 CEK Machine 的 YIELD 点(LLM/Tool 调用前后)插入额外的 β-规约步骤。三层实现策略分别对应 Lambda 演算代数、一等 Term 扩展、执行时观察者三个层次,正交互补,推荐并存使用。