基于 Paper10 操作语义框架(
src/lambdagent/)的设计分析 日期:2026-04-01
"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 | 所有规则 |
Hook 直接编码为现有原语的组合,今天就能实现:
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
使用示例:
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 规则触发的瞬间)插入逻辑。
将 Hook 建模为一个新的 Term,具有自己的小步规则,在 Paper10 的形式体系内完全可推理:
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⟩
在 CEK Machine 的精确位置注入回调,只需改动 cek_machine.py 约 30 行:
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 为例):
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
实际使用:
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 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 也触发
如果三层对同一事件做同一件事,会触发多次:
# ❌ 错误:三层都在记录同一个 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 | 业务层 | 一次性输入预处理、快速原型调试打印 | 内联写在业务中 |
# ── 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 事件类型与 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) ← 无效果的纯变换
三层天然并存,因为它们描述的是同一个计算的不同视角,不是同一个问题的三个备选解法:
Hook 系统不是 Lambdagent 体系之外的东西,它本身就是 Lambda 演算的
Compose(函数组合)和Guard(依赖类型验证)的特例。
Claude Code 的 Hook 系统在形式上等价于在 CEK Machine 的 YIELD 点(LLM/Tool 调用前后)插入额外的 β-规约步骤。三层实现策略分别对应 Lambda 演算代数、一等 Term 扩展、执行时观察者三个层次,正交互补,推荐并存使用。