首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Agent ToolCall 循环怎么定制?PI Extension 与 DeepAgents Middleware 两条岔路深度对比

Agent ToolCall 循环怎么定制?PI Extension 与 DeepAgents Middleware 两条岔路深度对比

作者头像
windealli
发布2026-05-19 11:03:12
发布2026-05-19 11:03:12
70
举报
文章被收录于专栏:windealliwindealli

当你想给 Agent 的 tool_call 循环挂自定义逻辑——拦截、改写、重试、注入工具——会发现两个明星框架选了两条互不重合的范式:PI 用"事件订阅",DeepAgents 用"中间件洋葱"。这篇把它们放在一起对比,告诉你根因在哪、各自的天花板在哪、你的项目该选哪种

导语:同一个 Loop,两套范式

Agent 的核心循环长这样:

用户输入 → 构造 Prompt → 调 LLM → 拿到 ToolCall → 执行 Tool → 把结果回灌给 LLM → 再来一轮

无论 PI(@earendil-works/pi-coding-agent)还是 DeepAgents(langchain-ai/deepagents),它们的 agent loop 形态完全一样。但当你想"在 tool_call 这段挂点自己的逻辑"时,两边的 API 长得完全不同:

  • PIpi.on("tool_call", handler)——熟悉的 EventEmitter 写法。
  • DeepAgentsdef wrap_tool_call(self, request, handler)——熟悉的 Koa 中间件写法。

差别不在"能不能做",而在"开放定制的范式"。这篇就把这条岔路一次讲清楚。


一、Agent ToolCall 循环:从一张图说起

1.1 共有的循环骨架

不管哪个框架,主循环都是固定的几个节点:User Input → Build Prompt → LLM Call → Tool Call → Tool Result → 回到 LLM Call,循环到模型 emit stop 为止。

用户的所有定制需求,本质上都落在每个节点的"前后"。

1.2 聚焦 tool_call:8 类典型诉求

把目光放到 tool_call 这一段,你会看到 8 类几乎所有 Agent 工程都会遇到的需求:

诉求

典型场景

拦截 / 阻断

rm -rf 弹确认;写 .env 直接 deny

改写参数

把 --force 静默去掉;绝对路径换成沙盒映射

改写结果

脱敏 API key / 邮箱 / 内网 IP;大输出截断或落盘

重试 / fallback

网络抖动重试 3 次;找不到文件转去问 LSP

注入新工具

MCP server、内部接口、领域专用动作

动态裁剪工具集

根据 backend / 角色 / 上下文关掉部分工具

埋点 / 审计

每次 tool_call 落日志或 Trace

人机协同(HITL)

关键 tool 暂停,等用户批准再继续

两个框架都能覆盖这 8 类——差别在"用什么形状的 API 让用户写出来"。


二、PI Extension:事件订阅式插件

2.1 心智模型:宿主 emit,插件 on

PI 把 agent loop 上所有关键时机做成命名事件tool_calltool_resultbefore_agent_startcontextbefore_provider_request

插件用 pi.on("event_name", handler) 订阅。宿主在主循环跑到对应时机,按既定策略(fire-and-forget / bail / waterfall)串行派发

  • tool_callbail 策略——任一插件返回 {block: true} 立即短路,拒绝执行。
  • tool_resultwaterfall 策略——上一个插件的输出作为下一个的输入,链式改写。
  • tool_execution_*fire-and-forget——只为埋点、UI 更新,不影响主流程。

2.2 tool_call 上的钩子表面

事件名

触发时机

派发语义

能干什么

tool_call

LLM 给出 toolCall,执行前

bail

改 event.input 改参数;返回 {block:true} 拒绝

tool_execution_start

真正调用 tool 之前

fire-and-forget

埋点、UI 更新

tool_execution_update

tool 流式输出过程中

fire-and-forget

进度展示

tool_execution_end

tool 执行完成

fire-and-forget

统计耗时

tool_result

结果回灌给 LLM 之前

waterfall

改写 content / details / isError

registerTool()

extension 加载期

注册而非事件

给 LLM 新增工具

2.3 代码:在 PI 里拦 bash 工具 + 脱敏(约 18 行 TS)

代码语言:javascript
复制
// ~/.pi/agent/extensions/safety.ts
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";

export default function (pi: ExtensionAPI) {
  // 1) 调用前拦截 — bail 语义
  pi.on("tool_call", async (event, ctx) => {
    if (event.toolName === "bash" && event.input.command?.includes("rm -rf")) {
      const ok = await ctx.ui.confirm("Dangerous", "Allow rm -rf?");
      if (!ok) return { block: true, reason: "User denied" };
    }
  });

  // 2) 结果回灌前脱敏 — waterfall 语义
  pi.on("tool_result", async (event) => {
    const redacted = event.content.map(c =>
      c.type === "text"
        ? { ...c, text: c.text.replace(/sk-[a-z0-9]+/gi, "sk-***") }
        : c
    );
    return { content: redacted };
  });
}

关键点tool_calltool_result两个独立的事件名,分别有不同的派发策略。"前"和"后"的拆分由框架硬编码,你只能选时机、不能改派发语义。


三、DeepAgents Middleware:洋葱式中间件

3.1 心智模型:包住 handler,自己决定调不调

DeepAgents 借用了 langchain.agents.middleware.AgentMiddleware 基类。每个 middleware 是一个 Python 类,通过覆盖 wrap_tool_call(request, handler) 这样的方法接入主循环。

多个 middleware 按 list 顺序嵌成洋葱——每一层显式决定要不要、要几次地调 handler(request)

  • • 不调 handler → 短路(拦截)
  • • 调一次 handler → 常规执行
  • • 调多次 handler → 重试 / A-B 比较
  • try / except 包住 handler → fallback
  • handler 前后改 request/response → 改参数 / 改结果

3.2 tool_call 上的钩子表面

方法

位置

语义

能干什么

before_agent

整个 agent 进入前

返回 state 更新

注入初始状态

before_model / after_model

LLM 调用前后

返回 state 更新

统计、限流

wrap_model_call(req, handler)

包住 LLM 调用

洋葱

prompt 注入、动态裁剪 tools、模型 fallback

wrap_tool_call(req, handler)

包住一次 tool 执行

洋葱

权限校验、重试、超时、大结果落盘、脱敏

self.tools = [...]

类属性

声明

向 agent 注入新工具

state_schema

类属性

声明 TypedDict

让 LangGraph 管跨步骤状态、checkpoint、replay

3.3 代码:等价的 bash 拦截 + 脱敏(约 22 行 Python)

代码语言:javascript
复制
# my_safety_middleware.py
from typing import Callable
from langchain.agents.middleware import AgentMiddleware
from langchain.tools.tool_node import ToolCallRequest
from langchain_core.messages import ToolMessage
from langgraph.types import Command, interrupt


class SafetyMiddleware(AgentMiddleware):
    def wrap_tool_call(
        self,
        request: ToolCallRequest,
        handler: Callable[[ToolCallRequest], ToolMessage | Command],
    ) -> ToolMessage | Command:
        call = request.tool_call

        # 1) 调用前拦截 — 不调 handler 即 short-circuit
        if call["name"] == "bash" and "rm -rf" in call["args"].get("command", ""):
            ok = interrupt({"prompt": "Allow rm -rf?"})
            if not ok:
                return ToolMessage(
                    content="Blocked by SafetyMiddleware",
                    name=call["name"], tool_call_id=call["id"], status="error",
                )

        # 2) 真实执行
        result = handler(request)

        # 3) 结果回灌前脱敏
        if isinstance(result, ToolMessage) and isinstance(result.content, str):
            result.content = redact_secrets(result.content)
        return result

关键点:拦截 / 改写 / 重试 / fallback 全部在一个方法里表达,靠"调不调 handler()、调几次、改 request 还是改 response"来区分语义。多个 middleware 自动嵌成洋葱(list 顺序 = 由外到内)。


四、把同一个需求写两遍:体感对比

光看 API 表面还不够。我们换一个稍微复杂的真实需求体感一下:

需求:给 bash 工具加 3 次重试,每次失败 sleep 1 秒;如果最后还是失败、且命令包含 git,自动 fallback 到本地 subprocess.run(['git', 'status']) 给一个兜底输出。

DeepAgents 写起来很自然

代码语言:javascript
复制
class GitRetryMiddleware(AgentMiddleware):
    def wrap_tool_call(self, request, handler):
        if request.tool_call["name"] != "bash":
            return handler(request)

        last_err = None
        for _ in range(3):
            try:
                return handler(request)
            except Exception as e:
                last_err = e
                time.sleep(1)

        if "git" in request.tool_call["args"].get("command", ""):
            out = subprocess.run(["git", "status"], capture_output=True, text=True)
            return ToolMessage(
                content=out.stdout, name="bash",
                tool_call_id=request.tool_call["id"],
            )
        raise last_err

"调几次 handler" 在洋葱模型里就是一个普通的 for 循环,自然到不需要思考。

PI 写起来非常违和

PI 的 on("tool_call", ...) 钩子的语义是"通知插件 LLM 要调用 tool 了",它不是执行点。插件这里没有"重跑 tool"的入口(emitToolCall 是 hook,不是 exec)。

所以 PI 上要做这个需求,你只剩两条路:

  1. 1. 放弃 PI 的 emit 机制,新写一个工具 bash_with_retry 完全替代 bash,把重试 / fallback 逻辑塞进新 tool 内部;
  2. 2. fork PI core,自己改 AgentSession 里 tool 执行的那一段代码。

结论:「重试 / fallback / 多次调用」不是 PI 缺 feature,而是范式天花板。事件订阅模型天生只能描述"在某时机做某事",描述不了"包住某次执行"。


五、逐项对比:核心 8 维

维度

PI · Extension

DeepAgents · Middleware

抽象单位

一个 .ts 文件,导出 (pi) => void 工厂

一个 Python 类继承 AgentMiddleware

接入方式

订阅 — pi.on("tool_call", fn),字符串选时机

覆盖 — def wrap_tool_call(req, handler),方法名选时机

"前"与"后"

拆成两个事件 tool_call / tool_result

同一个 wrap_tool_call 同时管前后,用 handler() 切开

派发策略

框架硬编码(fire-and-forget / bail / waterfall)

统一"洋葱包装",由 middleware 自己决定

阻断调用

返回 {block: true, reason}

不调 handler,直接 return ToolMessage(status="error")

重试 / fallback

无原生 hook,要在 extension 内自己包

原生:for _ in range(3): handler(request)

跨步骤状态

模块变量 / pi.appendEntry() 写 session 文件

state_schema + LangGraph 自动 reduce / checkpoint / replay

人机协同(HITL)

ctx.ui.confirm/select/input(同步阻塞、TUI 自动弹窗)

LangGraph interrupt()(暂停图、控制权抛出 SDK 外)

加载方式

运行时 jiti 动态加载 .ts,/reload 热重启

构造期 list,改完重启进程

分发

拷贝 .ts 到 ~/.pi/agent/extensions/ 即用

发 PyPI 包,受众 pip install 再 import

错误隔离

handler 抛错走 emitError,不影响主流程

默认上抛,需 handle_tool_errors 或自己 try/except

心智成本

中-高(要懂 LangGraph 状态机、Channel/Reducer)

适合形态

单进程 TUI 客户端 / 个人定制

SDK / 平台 / 多代理 / 线上服务

一句话总结这张表

PI 把"在哪儿插"做成了 API(命名事件);DeepAgents 把"怎么插"做成了 API(包 handler 的方法)。


六、各自的优劣分析

PI Extension

优势

  • 心智成本低pi.on("tool_call", fn) 就是"装个监听器",前端工程师 5 分钟上手。
  • 热加载:把文件丢进 ~/.pi/agent/extensions//reload 立即生效。
  • 一文件一切:events + tools + commands + UI + theme 都能塞同一个文件。
  • UI 一等公民ctx.ui.confirm/custom/setFooter/... 直接接 TUI,做"问一下用户"非常顺手。
  • 错误天然隔离:handler 抛错只产生 diagnostic,主流程不挂。

劣势

  • 派发策略硬编码:bail / waterfall / fire-and-forget 由框架决定,你想"在 tool_call 上也走 waterfall"?做不到。
  • 前后人为割裂:要同时拦前和改后,得写两个 on,自己用闭包/Map 串联状态。
  • 不擅长重试:没有"调 handler 多次"的概念,重试要自己包一层。
  • 组合性弱:两个 extension 想"先脱敏再截断",顺序由加载顺序决定,不容易显式编排。

DeepAgents Middleware

优势

  • 一处搞定前后wrap_tool_call 同时表达 before/after/around,天然支持重试、cache、fallback、circuit-breaker。
  • 组合显式middleware=[A, B, C] 即洋葱由外到内,谁先谁后一目了然
  • 状态托管:借 LangGraph 的 state + checkpointer,跨步骤状态、断点续跑、time-travel debug 直接可用。
  • 子代理一等公民SubAgentMiddleware / AsyncSubAgentMiddleware 把多代理、远程执行做成内置能力。
  • 生态对齐:所有 middleware 都能跑在 LangGraph / LangSmith / LangChain Studio,自带 trace、replay、debug。

劣势

  • 心智门槛高:要懂 LangGraph 状态机、洋葱模型、Channel/Reducer 才能写出正确的 middleware。
  • UI 不在 SDK 里:弹窗 / 进度条 / 主题要到 CLI 包(Textual)里去写,SDK 层只有 interrupt() 这种抽象 HITL 信号。
  • 调试栈深:错误堆栈穿透 LangGraph 多个节点 + 多层 middleware,定位问题比 PI 的 emit 路径费劲。
  • 无内置热加载:middleware 是 Python 类,改完得重启进程。
  • 分发重:标准路径是发 PyPI,不像 PI 一个文件 scp 过去就能用。

七、为什么各自选这条路?根因在"宿主形态"

这是这次对比里最值得记一笔的观察

扩展机制不是独立设计选项,而是被"宿主长什么样"反推出来的。

PI 选 Extension 的根因

PI 的宿主是 AgentSession——一个写死流程的 TypeScript 类,循环展开成顺序代码。在这种宿主上开放定制,最自然的方式就是「在关键时机喊一嗓子让插件知道」——也就是 EventEmitter 风格的命名事件。它根本没有"重新编排循环"的可能,所以 bail / waterfall 这种合并策略只能由宿主硬编码。

PI 沿袭的范式都是事件订阅系:

  • • Node EventEmitter
  • • Fastify hooks(onRequest / preHandler
  • • Hapi.js server.ext()
  • • Probot app.on("issues.opened")
  • • VSCode 扩展 context.subscriptions.push(workspace.onDidSaveTextDocument(...))

DeepAgents 选 Middleware 的根因

DeepAgents 的宿主是 LangGraph StateGraph——agent loop 是一张运行时编译的图,每个 middleware 编译进图节点。在这种宿主上开放定制,最自然的方式就是「让你的 middleware 也成为图的一部分」——也就是 Koa 风格的洋葱包装。handler 是 LangGraph 给你的"下一个节点",你可以选不调(短路)、多调(重试)、并行调(fork-join),都还在图模型里。

DeepAgents 沿袭的范式都是中间件链系:

  • • Connect / Express / Koa middleware
  • • Django middleware
  • • Webpack tapable AsyncSeriesWaterfallHook
  • • Rollup 插件 transform hook
  • • LangChain v0 CallbackHandler

这不是哪边更先进,是哪边更顺手

PI 的开发者大概率来自前端 / 工具链 / VSCode 扩展圈,自然写出事件订阅;DeepAgents 的开发者来自 LangChain / Python 后端圈,自然写出中间件。

底座一旦选定,扩展模型几乎就被锁死。

事件驱动 → 用命名事件订阅;图驱动 → 用节点中间件。


八、该选哪种?决策矩阵

一句话判据

你要表达的诉求

推荐

「我要在某个固定时机干件事」

Extension

「我要包住某段执行,可能不调、可能多次调」

Middleware

优先级三条建议

  1. 1. 场景驱动选型,不要先选框架再迁就业务。判据见上表,一句话版本:「我在某个固定时机插一段」→ Extension;「我要包住某段执行,能不调或多次调」→ Middleware。
  2. 2. 如果团队走 Python + 生产服务:默认 DeepAgents。需要的不只是 tool_call 定制,还有 checkpoint、replay、trace、子代理这些只有 LangGraph 系才能开箱给你的能力。
  3. 3. 如果是 TypeScript 单兵 / Coding Agent CLI 二开:默认 PI。把 .ts extension 放进 ~/.pi/agent/extensions/ 即用,/reload 调试节奏快。一旦发现自己在写"重试包一层"或"多 middleware 显式编排"的代码,再考虑迁移。

演进路径建议

  • 从 PI 起步、未来可能迁 DeepAgents 的项目:保持 extension 的"单一职责"——一个文件管一件事,避免 mutate state 跨多个事件;将来翻成 middleware 类时成本最低。
  • 从 DeepAgents 起步、缺 UI 的项目:把 UI 层独立做(Textual / Web),通过 LangGraph 的 interrupt 把 HITL 信号传出去,避免把 UI 逻辑塞进 middleware。
  • 既要 PI 的 UI 又要 DeepAgents 的组合表达力:可考虑把 DeepAgents 当 backend(Python 服务),PI 当 frontend harness(CLI),通过 ACP / 自定义 RPC 衔接。

九、结论:一句话浓缩

PI 把"在哪儿插"做成了 API(命名事件);DeepAgents 把"怎么插"做成了 API(包 handler 的方法)。 前者强在低门槛 + UI 直觉 + 热加载,是单兵作战工具的最优解; 后者强在组合表达 + 平台化 + 可观测,是构建产品级 Agent 平台的最优解。

两者并非互斥——它们其实展示了"Node 生态 vs Python 后端生态"在 Agent 这个新场景下的两条惯性路径:

  • • 从 VSCode / Fastify / Probot 走出来的人,自然会写出 PI 风格的事件总线;
  • • 从 Django / Koa / LangChain Callbacks 走出来的人,自然会写出 DeepAgents 风格的中间件栈。

当你要做"定制 tool_call 循环"这件事时,记住一个判断标准:

  • "我要在某个固定时机干件事" → 事件模型更顺;
  • "我要包住某个执行,并能选择性地多次/不调它" → 中间件模型更顺。

这条标准比"哪个框架更好"靠谱得多——它直接决定了你接下来要写多少胶水代码。


延伸阅读

涉及代码版本:

  • • PI f2b105dd@earendil-works/pi-coding-agent
  • • DeepAgents 2ac7d415langchain-ai/deepagents
  • • LangChain v1 d0d78f1aeb(DeepAgents middleware 基类来源)

外部参照范式:

  • • Webpack tapable
  • • Fastify Hooks
  • • VSCode Extension API
  • • Koa middleware(洋葱模型)
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-05-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 海天二路搬砖工 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导语:同一个 Loop,两套范式
  • 一、Agent ToolCall 循环:从一张图说起
    • 1.1 共有的循环骨架
    • 1.2 聚焦 tool_call:8 类典型诉求
  • 二、PI Extension:事件订阅式插件
    • 2.1 心智模型:宿主 emit,插件 on
    • 2.2 tool_call 上的钩子表面
    • 2.3 代码:在 PI 里拦 bash 工具 + 脱敏(约 18 行 TS)
  • 三、DeepAgents Middleware:洋葱式中间件
    • 3.1 心智模型:包住 handler,自己决定调不调
    • 3.2 tool_call 上的钩子表面
    • 3.3 代码:等价的 bash 拦截 + 脱敏(约 22 行 Python)
  • 四、把同一个需求写两遍:体感对比
    • DeepAgents 写起来很自然
    • PI 写起来非常违和
  • 五、逐项对比:核心 8 维
    • 一句话总结这张表
  • 六、各自的优劣分析
    • PI Extension
    • DeepAgents Middleware
  • 七、为什么各自选这条路?根因在"宿主形态"
    • PI 选 Extension 的根因
    • DeepAgents 选 Middleware 的根因
    • 这不是哪边更先进,是哪边更顺手
  • 八、该选哪种?决策矩阵
    • 一句话判据
    • 优先级三条建议
    • 演进路径建议
  • 九、结论:一句话浓缩
  • 延伸阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档