LangChain Middleware
vs Pi 执行管线对比

统一 Middleware 抽象 vs 三层 Hook 架构

技术架构白皮书 · 2026年6月

执行摘要
LangChain v1 引入了 AgentMiddleware 统一抽象,将所有代理扩展能力(观测、拦截、工具注册、状态管理)集中到一个类中。Pi 没有 "Middleware" 概念名,但其三层架构(Agent Loop 回调 + AgentEvent 流 + Harness/Extension Hook)实现了等价甚至更细分的能力。本文档从架构设计、钩子映射、拦截能力、组合模式四维度进行系统对比,揭示两种设计哲学背后的权衡与适用场景。

一、问题定义

Agent 框架需要一个标准化的扩展机制,允许开发者在不修改框架核心代码的情况下实现:

  1. 观测 — 日志、监控、追踪 Agent 执行过程
  2. 修改 — 改写消息、替换模型、注入指令
  3. 阻断 — 拦截危险的工具调用或 LLM 响应
  4. 注册 — 向 Agent 添加额外的工具/资源
  5. 控制 — 决定 Agent 的循环终止条件
核心问题:LangChain 的 AgentMiddleware 在 Pi 中对应哪些部分?它们的共同之处和本质差异在哪里?

二、架构设计对比

2.1 宏观架构

维度LangChain MiddlewarePi 三层架构
核心抽象 AgentMiddleware 统一类(12 个钩子方法) 三层:AgentLoopConfig + AgentEvent + ExtensionRunner
钩子类型 生命周期钩子(before/after)+ 拦截器(wrap)+ 工具注册 + 状态 schema Loop 回调(控制流)+ Event(观测)+ Harness HookEvent(拦截)+ Extension 工具注册
组合方式 图节点链(生命周期)+ 洋葱闭包(拦截器) 内联函数调用(Loop)+ 多播通知(Event)+ 顺序管道&短路(Harness)
执行引擎 LangGraph(图执行) 纯函数式 ReAct Loop
类型系统 Python 泛型(StateT, ContextT, ResponseT) TypeScript 联合类型 + 泛型 + Phantom Type
创建方式 子类化 + 7 个装饰器 设置 Agent 属性 + agent.subscribe() + ExtensionFactory
热插拔 不支持 Extension 层支持 reload

2.2 层级对应关系

关键发现:LangChain Middleware 的 12 个钩子方法 分散映射到 Pi 的三个独立层次,没有一个 Union 层面的等价物。
LC Middleware 能力数量Pi 等价系统对应关系
生命周期钩子 (before/after_model, before/after_agent) 8 个方法 Agent Loop 层 + Harness events transformContextconvertToLlmbefore_agent_start
拦截器 (wrap_model_call, wrap_tool_call) 4 个方法 Harness 层 (HookEvent) + Loop 层 (beforeToolCall) contextbefore_provider_requesttool_calltool_result
工具注册 (tools) 1 个属性 Extension 层 registerTool()
状态 schema (state_schema) 1 个属性 Agent 直接持有 + Extension rootState() Agent._stateExtensionAPI.rootState()
流变换器 (transformers) 1 个属性 无直接等价 Pi 通过 onPayload/onResponse 回调处理
装饰器 (@before_model 等) 7 个装饰器 无直接等价 Pi 的 AgentLoopConfig 回调通过对象属性赋值
一句话:LangChain 把能力集中到一个类,Pi 把能力拆到三个系统中。LC 的 Middleware 同时承担了 Pi 的 Loop 层、Event 层、Extension 层的职责。

三、钩子方法逐一映射

3.1 生命周期钩子映射

LC 方法触发时机Pi 等价实现方式
before_agent Agent 执行开始前(一次) before_agent_start Harness 事件 Extension 注册 on("before_agent_start"),可修改 systemPrompt、注入初始消息
after_agent Agent 执行完成后(一次) agent_end AgentEvent + Harness 事件 agent.subscribe() 观测;Extension 处理器不参与控制
before_model 每次 LLM 调用前 transformContext + convertToLlm + before_provider_request Loop 回调(消息级修改)+ Harness 事件(请求级修改)
after_model 每次 LLM 调用后 after_provider_response Harness 事件 Extension 管道,在 LLM 响应完成后 emit

3.2 拦截器映射

LC 方法能力Pi 等价能力对比
wrap_model_call 拦截 LLM 调用:可重试、短路、修改请求/响应 context + before_provider_request + before_provider_payload + after_provider_response 组合 ⚠️ Pi 可修改请求和响应,但不能重试 handler(重试是 provider 层的事)
wrap_tool_call 拦截工具调用:可重试、短路、修改请求/返回 beforeToolCall(Loop) + afterToolCall(Loop) + tool_call(Harness) + tool_result(Harness) ✅ 功能等价,可以 block / patch 结果
关键差异:LC 的 wrap_model_call 通过洋葱闭包模式可以多次调用 handler(request) 实现重试。Pi 的 LLM 调用封装在 streamFn 闭包中,外部无法获取 handler 引用。Pi 设计上认为重试是 provider 层的事(SimpleStreamOptions.maxRetries),不是扩展层该管的。

3.3 工具注册和状态映射

LC 能力Pi 等价实现方式
tools 属性(Middleware 注册工具) ExtensionAPI.registerTool() 扩展注册工具,_refreshToolRegistry() 合并到 agent 的工具列表
state_schema(Middleware 自定义状态) rootState() + Agent._state 扩展通过 API 读写根状态;Agent 直接持有 _state: MutableAgentState
transformers(流变换器链) 无直接等价 Pi 通过 onPayload/onResponse 在 provider 层做流处理,无通用 transformer 链

四、组合模式对比

4.1 洋葱包装 vs 链式管道

维度LangChain 洋葱包装Pi 链式管道
实现 outer(inner(core_handler)) — 右到左包装成闭包链 for h in handlers: result = await h(event) — 顺序遍历
调用栈 完整的洋葱调用栈,外层包裹内层 扁平顺序,下游处理器看不到上游的修改(除非原地修改 event)
handler 控制 每个 Middleware 自主决定是否/何时/如何调用 handler 没有 handler 概念,每个处理器只是处理一个事件
短路机制 不调用 handler → 链中断 返回 cancel: trueblock: true → 剩余处理器跳过
重试能力 ✅ 多次调用 handler 实现重试 ❌ 无法重试(没有 handler 可调用)

LangChain 洋葱包装实现

# factory.py:221
def _chain_model_call_handlers(mw_list, core_handler):
    handler = core_handler
    for m in reversed(mw_list):           # 从右向左包装
        handler = make_handler(m, handler)
    return handler
# 结果:m[0](m[1](m[2](core_handler)))

Pi 链式管道实现

// runner.ts:693
async emit(event) {
    for (const ext of this.extensions) {
        const handlers = ext.handlers.get(event.type);
        for (const handler of handlers) {
            const result = await handler(event, ctx);
            if (result.cancel) return result;  // 短路
        }
    }
}

4.2 生命周期钩子编排

维度LangChain 图节点Pi 内联调用
编排方式 每个 before/after hook 是独立的 LangGraph 节点,用边连接 streamAssistantResponseexecuteToolCalls 中直接 await 回调
执行顺序 START → before_m[0] → ... → before_m[n] → model → after_m[n] → ... → after_m[0] 在函数体中按书写顺序 await
条件跳转 ✅ 通过 @hook_config(can_jump_to=["end","tools","model"]) 条件跳转 ✅ 通过 shouldStopAfterTurn 终止、prepareNextTurn 修改下一轮
开销 每个 hook 是一个独立的图节点 trip 零开销内联调用
可观测性 图节点自动记录到 LangSmith 需要显式 emit 事件到 Harness 层

五、代码示例:同一功能对比

5.1 重试失败的模型调用

LangChain(洋葱包装重试)
class ModelRetryMiddleware(AgentMiddleware):
    def wrap_model_call(self, request, handler):
        for attempt in range(3):
            try:
                return handler(request)  # 可多次调用
            except Exception:
                if attempt == 2: raise
                time.sleep(2 ** attempt)
Pi(provider 层重试)
// Pi 无法在 AgentLoopConfig 层面重试 LLM 调用
// 重试由 provider 层处理:
// SimpleStreamOptions.maxRetries = 2
// 如果需要自定义重试逻辑,在 provider 注册时处理
// 设计哲学:重试是基础设施问题,不是扩展问题

5.2 阻断危险工具调用

LangChain
class SafeGuardMiddleware(AgentMiddleware):
    def wrap_tool_call(self, request, handler):
        if request.tool_call["name"] in ["rm", "delete"]:
            return ToolMessage(
                content="Blocked: tool not allowed",
                tool_call_id=request.tool_call["id"]
            )
        return handler(request)
Pi(Loop 层 + Extension 层都支持)
// Loop 层
agent.beforeToolCall = async ({ toolCall }) => {
    if (["rm", "delete"].includes(toolCall.name))
        return { block: true };
};

// Extension 层
extension.on("tool_call", async (event, ctx) => {
    if (event.toolName === "rm") {
        const approved = await ctx.ui.confirm("Approve rm?");
        if (!approved) return { block: true };
    }
});

5.3 消息上下文裁剪

LangChain(before_model 返回状态更新)
class ContextTrimMiddleware(AgentMiddleware):
    def before_model(self, state, runtime):
        msgs = state["messages"]
        if count_tokens(msgs) > MAX:
            summary = summarize(msgs[:-10])
            state["messages"] = [sys(summary)] + msgs[-10:]
            return {"messages": state["messages"]}
Pi(transformContext 直接返回消息)
agent.transformContext = async (messages) => {
    if (estimateTokens(messages) > MAX) {
        const summary = await summarize(
            messages.slice(0, -10)
        );
        return [systemMsg(summary), ...messages.slice(-10)];
    }
    return messages;
};

六、设计哲学对比

6.0 本质差异:钩子的组织方式

核心洞察:两种系统的 Agent 均独立于钩子存在——不带 Middleware 的 LangChain agent 一样能跑,不注册任何 hook 的 Pi agent 也照常执行。本质差异不是谁定义生命周期,而是钩子的组织方式
维度LangChain(打包派)Pi(松散派)
钩子组织 一个 AgentMiddleware 子类囊括多个阶段的钩子(before_model + wrap_tool_call + after_agent + ...) 三处独立注册:Loop 回调设到 Agent 属性、观测用 subscribe()、拦截用 extension.on()
耦合度 一个类 = 一组生命周期干预点,所有钩子内聚在一个文件中 互不依赖,各自注册。改 beforeToolCall 不会影响 Extension 层的 tool_result processor
心智模型 "我需要创建一个 Middleware 来介入 Agent" "我需要在 X 阶段做 Y 事,去对应的注册点挂一个回调/事件处理器"
典型用法 create_agent(model, middleware=[MyMiddleware()]) agent.beforeToolCall = fn + agent.subscribe(listener) + extension.on("tool_call", handler)

6.1 各自的设计哲学

Pi — "拦截与修改框架"
LangChain Middleware — "大一统扩展入口"

七、完整能力矩阵

能力LangChain MiddlewarePi Loop 层Pi Event 层Pi Harness 层
观测执行流程✅ 生命周期钩子 + 图节点追踪subscribe() 全部事件on("*_end")
修改 LLM 请求wrap_model_call + before_modeltransformContextcontext + before_provider_request
修改 LLM 响应wrap_model_call 返回修改after_provider_response + message_end
重试 LLM 调用wrap_model_call 多次调 handler❌(provider 层处理)
阻断工具调用wrap_tool_call 返回伪造结果beforeToolCall { block: true }tool_call { block: true }
修改工具结果wrap_tool_call 返回值afterToolCall { patch }tool_result { content, isError }
重试工具调用wrap_tool_call 多次调 handler
控制循环终止jump_to="end"shouldStopAfterTurn
注册工具tools 属性registerTool()
自定义状态state_schema 合并❌(直接操作 Agent._state)rootState()
UI 交互ctx.ui.select/confirm/input
热插拔✅ add/remove listener✅ reload extensions
条件跳转@hook_config(can_jump_to=[...])

八、适用场景对比

8.1 选择 LangChain Middleware 的场景

8.2 选择 Pi 三层架构的场景

九、一句话总结

LangChain Middleware = 大一统的"瑞士军刀":一个类解决所有扩展问题,统一、直接、powerful。

Pi 三层架构 = 职责分离的"微服务":Loop 层控执行、Event 层做观测、Extension 层管拦截——各司其职,互不干扰。

两者不是竞争,而是不同设计哲学的体现:Monolith vs Modular。LangChain 把能力合在 Middleware 里追求一站式体验;Pi 把能力拆到三层中追求灵活度和可组合性。在实际项目中,可以根据复杂度需求选择或借鉴对方的设计思路。