一、结论先行

设计 agent 的工具(tool),正确的起点不是「先枚举这个 agent 需要哪些动作」,而是「先为这个 agent 服务的领域建模型」。把坐标压到最短就是一句:

领域对象 × 动作 = 工具。

过去我习惯的那套「从动作推导」方法——先列出观察 / 修改 / 执行 / 搜索 / 提问 / 分治 / 记忆 / 网络这些原语,再给每个原语配最少工具——不是不能用,但它不该当第 1 步的起点,而该降级为设计完成后的完备性校验。排序放错,是这套框架最隐蔽的硬伤。

二、问题出在哪:纯「动作推导」为什么不好分析

「动作」是一组原语级、跨域同质的能力。observe / modify / execute / search / ask / decompose / remember / network 这套词,套到任何 agent 身上都长成同一副骨架:

症状表现
不区分 agent套一个「代码评审 agent」和套一个「客服 agent」,第一版工具列表几乎一样:Read / Write / Bash / Grep / Ask…
不携带领域语义create_pr 和 send_email 在动作层都是「modify / execute」,但领域天差地别
只适合校验、不适合推导原语清单能帮你回看「有没有漏掉记忆 / 提问 / 分治」,却给不出「这个 agent 到底干什么业务」

所以「从动作推导」这第 1 步,本质上是把一张跨 agent 都通用的校验清单误当成了推导起点。它本身没错,放错了位置——这是我自己的错。

三、领域驱动强在哪,但也踩坑(双向批判)

把「领域驱动」抬到起点,并不是说它无懈可击。两个视角摆在一起才诚实:

动作推导(原第 1 步)领域驱动(主张抬到起点)
维度纵向「原语」,任何 agent 通用横向「对象」,区分 agent 的是它服务的领域
好处保证不漏必备能力工具名即业务语言,人和 LLM 同频;边界 = 领域边界,天然框定「该管 / 不该管」
坑不区分 agent、无业务语义① 易工具爆炸;② 仍逃不掉动作;③ 边界划错更贵

领域驱动自己的三个坑必须正面接:

  • 易工具爆炸。 每个领域概念都顺手做一个 tool,正是你之前在 Cursor 上踩过的「9 个 MCP 撑大上下文」那种痛。领域驱动如果不加收口闸,会从一个极端(动作太抽象)滑到另一个极端(工具太碎)。
  • 仍逃不掉动作。 tool 终归是「对某个对象做某事」。纯领域对象列出来只是名词,必须再叠一层动作才有可执行性。
  • 边界划错更贵。 领域边界一旦划错,要么越权(agent 碰了不该碰的对象),要么漏权(该管的没给工具)。而动作推导在这点上反而更稳——原语级清单天然覆盖全场景。

关键结论:两个维度不是对立,是矩阵的两个轴。 纯动作推导产出无差异骨架,纯领域驱动会工具爆炸;真正可落地的是「领域对象(行)× 动作(列)」的矩阵。

四、关键:领域对象 × 动作 = 工具

把行定义为领域对象(你熟悉的领域语言 / 业务实体),列固定为几类动作。设计立刻有了抓手:

create read update execute query notify ┌─────────────────────────────────────────────────────────┐ PR │ ✓ ✓ ✓ ✗ ✓ ✗ │ Diff │ ✗ ✓ ✗ ✗ ✓ ✗ │ 规范 / Rule │ ✗ ✓ ✗ ✗ ✓ ✗ │ 评论 / Comment │ ✗ ✓ ✓ ✗ ✗ ✓ │ └─────────────────────────────────────────────────────────┘

每个「✓」格子就是一个候选工具;相邻同动作的格子可以合并成一个带 action 参数的工具。这张表同时回答了「这 agent 管什么」(行)和「它对这些对象能做什么」(列)。

五、重排后的设计步骤(领域优先)

把起点换成领域建模,动作推导退居校验,完整步骤如下:

  • 领域建模。 定 bounded context + 领域对象 + ubiquitous language(先定「行」)。这是和 DDD 同源的一步——聚合根、上下文地图在这里不是摆设,而是工具边界的直接来源。
  • 对象上派生动作。 在每个对象上填 create / read / update / execute / query / notify(填「列」)。
  • 同动作合并(防工具爆炸的关键闸)。 create_pr / review_pr / merge_pr 合成一个带 action 参数的工具。这一步决定领域驱动能不能真正落地,而不是变成工具汪洋。
  • 通用工具单独一层。 Read / Write / Bash 作跨域基础设施,不占主域,避免每个领域都重造轮子。
  • 权限按风险分级。 读免审批、写 / 执行需审批——Claude Code 的 Bash 内置只读命令集免提示,正是「低风险动作免打扰」的现成范例。
  • MCP / Skill 长尾挂载。 工具列表的增长只来自「新的动作类别」或「新的领域对象」,不来自「新的业务细节」。长尾能力不进主列表。
  • 描述即性能。 工具描述写 3–4 句(做什么 / 何时用 / 何时不用 / 参数含义),返回只留高信号字段——描述质量直接决定 LLM 选工具和解析结果的准确率。
原第 1 步(动作推导)退居第 0 步校验:设计完回看是否覆盖 observe / modify / execute / remember / ask / decompose——尤其「记忆 / 提问 / 分治」是非领域但必需的能力,领域驱动最容易漏。

六、一个直观例子:代码评审 Agent

对比最直观:

  • 纯动作推导 → Read / Write / Bash / Grep。完全看不出这是个「评审 agent」,换成交付 agent 也是这套。
  • 领域驱动 → 对象 {PR, Diff, 规范, 评论} × 动作 {list / get / review / post / check} → 工具收敛为 PR操作(合成 action 参数)/ diff分析 / 规范检查 / 评论发布。业务语义一眼清,且天然不爆炸。
代码评审 Agent(领域驱动) 领域对象 动作 收敛后的工具 ───────── ───────── ────────────── PR list/get PR操作(action=list|get) Diff read/analyze diff分析 规范 Rule check 规范检查 评论 Comment post 评论发布 (通用层:Read / Grep / Bash 作为兜底基础设施,不占主域)

这正好接上你的真实痛点:工具爆炸撑大上下文,根因是「按业务能力堆 tool」,而不是「按领域对象 × 动作收敛」。

七、接回我们的实践:04_toolcall / Claude Code / e2b

这层抽象不是空谈,它直接解释了我们手边几件事:

  • 04_toolcall.py(我们自己的 toy code-agent)。 它的本质就是「LLM 只吐文本 → runtime 按 function.name 分流」。这里的「分流目标」即工具;我们声明 run_python / run_bash 两个工具,本质就是在「代码执行」这一个领域对象上,分出 Python / Bash 两个动作子类。所谓 runtime 的执行分流,正是「领域对象 × 动作」矩阵在运行时被查表路由。
  • Claude Code 的 46 个内置 tool。 它按「动作类别」组织(文件 / 执行 / 搜索 / 编排 / Web / MCP),拆开看就是「本机文件系统 / 命令行 / 网络 / 子代理」这些领域对象 × 标准动作。它的权限分布(14 个需提示、32 个免提示)就是「权限按风险分级」这条原则的工业级落地。
  • e2b 沙箱。 它解决的是「不可信代码执行的隔离」——对应「执行」这一动作类别里的「代码执行」细分子类。是领域驱动里「通用执行动作」的一个安全变体,而不是另一条路线。

和 Agent Engineering:从 Workflow 到 Loop,再到 Control Plane 里谈的 control plane 视角也自洽:tool 是 control plane 向下派发的「动作原语」,而领域模型决定了这些原语该挂在哪些对象上。

八、工程启示 / 我的结论

落到可执行,给三条:

  1. 设计任何 agent 的 tool,先问「这个 agent 服务哪个领域」,再问「在这个领域对象上做什么动作」。 顺序反了就会长出一堆跨 agent 通用、却看不出业务身份的骨架。
  2. 工具爆炸的根因是「按业务能力堆 tool」,解法是「按领域对象 × 动作收敛 + 同动作合并」。 你 Cursor 上的 9 个 MCP 痛点,药方不在「少接 MCP」,而在「按对象 × 动作归并后再挂」。
  3. 一句话收口:领域驱动给工具一个名字,动作推导给工具一个边界。 两者不是二选一,而是「名字由领域给、边界由原语校验」的分工。

如果借用 Kubernetes × Agent:从容器编排到目标收敛 里的说法:tool 设计这件事,真正难的从来不是「能调什么」,而是「在正确的领域边界内,把正确的动作收敛成可被 LLM 稳定选中的少数几个」。