🛠️ 构建 Agent 实践指南 — 工具系统

定义 · 调用 · 安全 · 扩展 — 从 6 个 Agent 项目源码提炼的工具系统设计方法论

目录

  1. 工具定义:从裸函数到类型安全 Schema
  2. Schema 自动生成:Python 类型提示 → JSON Schema 的三种路径
  3. 安全与策略管道:8 层过滤的 Policy Pipeline
  4. 审批流:PreToolUse / PermissionRequest / 三级审批
  5. 组合与扩展:Toolset、Bundle、MCP、Plugin
  6. 过滤与黑名单:从 Blocklist 到正则守卫
  7. 跨项目对比矩阵
  8. 6 个核心模式
  9. 构建建议:按场景选型

1 工具定义:从裸函数到类型安全 Schema

Agent 的「工具」本质是将一段代码暴露给 LLM 调用。但定义方式差异巨大——从简单的字典描述到完整的类型安全验证,这决定了工具系统的可靠性天花板。

Smolagents: 声明式类 + 强验证

Smolagents

Smolagents 的 Tool 基类要求开发者声明 namedescriptioninputsoutput_type 四个类属性,并在 __init_subclass__ 时自动触发验证:

class Tool(BaseTool):
    name: str
    description: str
    inputs: dict[str, dict[str, str | type | bool]]
    output_type: str
    output_schema: dict[str, Any] | None = None

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        validate_after_init(cls)  # 自动验证

    def validate_arguments(self):
        # 1. 验证必填属性存在且类型正确
        # 2. name 必须是合法 Python 标识符
        # 3. inputs 中每个字段必须有 type + description
        # 4. type 必须在 AUTHORIZED_TYPES 白名单内
        # 5. forward() 签名必须与 inputs 一致
        # 6. nullable 在 inputs 和签名中必须双向一致
Smolagents 的 validate_arguments() 在类定义时就做 6 层验证——类型白名单、签名一致性、nullable 双向检查。错误在定义时而非调用时暴露。

类型白名单机制

Smolagents 不允许任意类型,只接受预定义白名单:

AUTHORIZED_TYPES = [
    "string", "boolean", "integer", "number",
    "image", "audio", "array", "object", "any", "null"
]

这确保生成的 Schema 对 LLM 友好——不会出现 Python 特有的 PathUUID 等类型导致模型困惑。

PydanticAI: 函数即工具 + Pydantic 深度绑定

PydanticAI

PydanticAI 走了完全不同的路:不需要声明式类,直接把普通 Python 函数变成工具,用 Pydantic 的内部 API 自动生成验证器:

def function_schema(
    function: Callable[..., Any],
    schema_generator: type[GenerateJsonSchema],
    *,
    takes_ctx: bool | None = None,
    docstring_format: DocstringFormat = 'auto',
    require_parameter_descriptions: bool = False,
) -> FunctionSchema:

核心流程:

inspect.signature() get_type_hints() 构建 TypedDict core_schema create_schema_validator() GenerateJsonSchema

FunctionSchema 数据结构包含运行时所需的一切:

@dataclass(kw_only=True)
class FunctionSchema:
    function: Callable[..., Any]
    name: str
    description: str | None
    validator: SchemaValidator    # Pydantic 核心验证器
    json_schema: ObjectJsonSchema  # 给 LLM 的 JSON Schema
    takes_ctx: bool               # 是否接受 RunContext
    is_async: bool
    single_arg_name: str | None   # 单参数优化
    return_schema: ObjectJsonSchema  # 返回值 Schema

单参数优化:unwrap 模式

当函数只有一个 model-like 参数时,PydanticAI 做了一个巧妙的优化——JSON Schema 直接暴露参数的字段,而不是包一层 {arg_name: {...}}

# 普通多参数: {"query": ..., "limit": ...}
# 单参数 unwrap: 直接暴露 data 的字段,而非 {"data": {...}}

def _build_schema(fields, var_kwargs_schema, core_config):
    if len(fields) == 1 and var_kwargs_schema is None:
        name = next(iter(fields))
        td_field = fields[name]
        if td_field['metadata']['is_model_like']:
            # unwrap: JSON Schema 去掉外层包装
            # 但 validator 用 wrap 保留 {name: value} 格式
            return (
                core_schema.no_info_wrap_validator_function(
                    partial(_validate_single_arg, name=name),
                    td_field['schema']
                ),
                name,  # single_arg_name
            )
    # 多参数: 标准 TypedDict
    return core_schema.typed_dict_schema(fields, ...), None
PydanticAI 的单参数 unwrap 让 LLM 看到更扁平的 Schema(少一层嵌套 = 少一分混淆),但验证器内部保持 {name: value} 统一格式——对开发者透明,对模型友好。

两种路径对比

维度SmolagentsPydanticAI
定义方式声明式类 (name/inputs/output_type)普通函数 + 自动推断
验证时机类定义时 (__init_subclass__)注册时 (function_schema())
Schema 生成手动声明 → _convert_type_hints_to_json_schemaPydantic 内部 API → GenerateJsonSchema
类型约束白名单 (AUTHORIZED_TYPES)Pydantic 全类型支持
上下文注入RunContext[T] 自动识别
返回值 Schema可选 output_schema自动从返回类型注解生成
序列化/反序列化to_dict() / from_dict()依赖 Pydantic 序列化

2 Schema 自动生成:Python 类型提示 → JSON Schema 的三种路径

将 Python 函数签名转为 LLM 可用的 JSON Schema 是工具系统的核心能力。6 个项目中有 3 种实现路径:

路径 A: Pydantic 深度绑定 (PydanticAI)

PydanticAI

直接调用 Pydantic 的内部 API _generate_schema.GenerateSchema,绕过公开的高层 API:

# 步骤 1: 用 Pydantic 内部 API 构建核心 schema
config_wrapper = ConfigWrapper(ConfigDict(title=function.__name__))
gen_schema = _generate_schema.GenerateSchema(config_wrapper)

# 步骤 2: 遍历函数参数,为每个参数生成 TypedDictField
for name, p in sig.parameters.items():
    field_info = FieldInfo.from_annotation(annotation)  # 或 from_annotated_attribute
    fields[name] = gen_schema._generate_td_field_schema(
        field_name, field_info, decorators, required=required
    )

# 步骤 3: 构建整体验证 schema
schema, single_arg_name = _build_schema(fields, var_kwargs_schema, core_config)

# 步骤 4: 创建验证器
schema_validator = create_schema_validator(
    schema, function, function.__module__,
    function.__qualname__, 'validate_call', core_config,
    config_wrapper.plugin_settings
)

# 步骤 5: 生成 JSON Schema
json_schema = schema_generator().generate(schema)
PydanticAI 的关键决策:用 Pydantic 内部 API 而非公开 API。好处是获得完整的类型支持(包括 LiteralUnion、嵌套 model);代价是依赖内部实现,Pydantic 大版本升级时可能 breaking。

路径 B: AST 解析 + 手动映射 (Smolagents)

Smolagents

Smolagents 用 _function_type_hints_utils.py 做自己的类型提示解析:

from ._function_type_hints_utils import (
    _convert_type_hints_to_json_schema,
    _get_json_schema_type,
    get_imports,
    get_json_schema,
)

同时提供 @tool 装饰器,从普通函数快速创建工具:

@tool
def search_web(query: str, max_results: int = 5) -> str:
    """Search the web for information.
    
    Args:
        query: The search query string
        max_results: Maximum number of results to return
    """
    # ... implementation

关键区别:Smolagents 的 MethodChecker 做 AST 级别的静态验证——检查方法体中是否引用了未定义名称、是否有本地 import:

class MethodChecker(ast.NodeVisitor):
    """Checks that a method:
    - only uses defined names
    - contains no local imports
    """
    def visit_Name(self, node):
        if isinstance(node.ctx, ast.Load):
            if not (
                node.id in _BUILTIN_NAMES
                or node.id in self.arg_names
                or node.id in self.imports
                or node.id in self.assigned_names
                # ... 更多来源检查
            ):
                self.errors.append(f"Name '{node.id}' is undefined.")
Smolagents 的 validate_tool_attributes() 会在序列化时检查工具类是否可重建——禁止 from_spacefrom_langchainfrom_gradio 创建的工具调用 to_dict(),因为它们的代码不是自包含的。

路径 C: YAML/配置声明 (SWE-agent)

SWE-agent

SWE-agent 走了完全不同的路——工具不写 Python 代码,而是用 YAML 声明 + Shell 脚本实现:

# Bundle: config.yaml
tools:
  search_file:
    docstring: |
      Search for a pattern in files.
      Usage: search_file <pattern> [dir]
    signature: "search_file <pattern> [<dir>]"
    end_name: "EOF"
    usage: |
      Search for a regex pattern in a directory.
      If no directory is provided, searches in the current directory.
  edit:
    docstring: |
      Edit a file with a search/replace block.
    signature: "edit <file>"
    end_name: "end_of_edit"

state_command: "cat /root/state.json"

Bundle 在容器内安装和执行:

class Bundle(BaseModel):
    path: Path
    hidden_tools: list[str] = Field(default_factory=list)
    _config: BundleConfig = PrivateAttr(default=None)

    @property
    def commands(self) -> list[Command]:
        return [
            Command(name=tool, **tool_config)
            for tool, tool_config in self.config.tools.items()
            if tool not in self.hidden_tools  # 支持隐藏工具
        ]
SWE-agent 的 Bundle 模式将工具定义与实现彻底分离——YAML 定义接口,Shell 脚本实现逻辑,容器内执行。这是 ACI(Agent-Computer Interface)哲学的体现:为 Agent 设计专用接口,而非复用人类 CLI。

3 安全与策略管道:8 层过滤的 Policy Pipeline

OpenClaw

OpenClaw 的工具安全系统是所有项目中最精细的——8 层 Policy Pipeline 依次过滤可用工具,每一层都可以独立配置 allow/deny 规则:

┌─────────────────────────────────────────────────────────────────┐ │ Tool Policy Pipeline │ ├─────────────────────────────────────────────────────────────────┤ All Tools ──→ Step 1: tools.profile (profile 级别策略) ──→ Step 2: tools.byProvider.profile (按 provider 的 profile) ──→ Step 3: tools.allow (全局 allow/deny) ──→ Step 4: tools.byProvider.allow (按 provider 的全局策略) ──→ Step 5: agents.{id}.tools.allow (agent 级别) ──→ Step 6: agents.{id}.tools.byProvider.allow ──→ Step 7: group tools.allow (群组级别) ──→ Step 8: tools.toolsBySender (发送者级别) ──→ Filtered Tools ✓ └─────────────────────────────────────────────────────────────────┘

Pipeline 的核心实现:

export function applyToolPolicyPipeline(params: {
  tools: AnyAgentTool[];
  toolMeta: (tool: AnyAgentTool) => { pluginId: string } | undefined;
  warn: (message: string) => void;
  steps: ToolPolicyPipelineStep[];
}): AnyAgentTool[] {
  // 1. 识别核心工具 vs 插件工具
  const coreToolNames = new Set(
    params.tools
      .filter((tool) => !params.toolMeta(tool))
      .map((tool) => normalizeToolName(tool.name))
  );

  // 2. 构建插件工具分组
  const pluginGroups = buildPluginToolGroups({ tools, toolMeta });

  // 3. 逐层过滤
  let filtered = params.tools;
  for (const step of params.steps) {
    if (!step.policy) continue;

    // 3a. 如果启用 stripPluginOnlyAllowlist,分离插件/核心条目
    let policy = step.policy;
    if (step.stripPluginOnlyAllowlist) {
      const resolved = analyzeAllowlistByToolType(policy, pluginGroups, coreToolNames);
      // 对 unknown allowlist 条目发出警告
      if (resolved.unknownAllowlist.length > 0) { /* warn */ }
      policy = resolved.policy;
    }

    // 3b. 展开插件组 (group:plugins → 实际工具名)
    const expanded = expandPolicyWithPluginGroups(policy, pluginGroups);
    filtered = expanded ? filterToolsByPolicy(filtered, expanded) : filtered;
  }
  return filtered;
}

Owner-Only 工具保护

除了 Pipeline,OpenClaw 还有独立的 Owner-Only 机制——某些工具只有 owner 发送者才能调用:

const OWNER_ONLY_TOOL_APPROVAL_CLASS_FALLBACKS = new Map([
  ["cron", "control_plane"],    // cron 管理 → 控制面
  ["gateway", "control_plane"], // 网关管理 → 控制面
  ["nodes", "exec_capable"],    // 节点管理 → 执行面
]);

function wrapOwnerOnlyToolExecution(tool, authorized) {
  if (tool.ownerOnly !== true || authorized || !tool.execute) return tool;
  return {
    ...tool,
    execute: async () => {
      throw new Error("Tool restricted to owner senders.");
    }
  };
}
OpenClaw 的 Owner-Only 机制使用替换 execute 函数而非删除工具——工具仍然出现在列表中(让 LLM 知道它存在),但调用时会报错。这避免了「工具消失导致 LLM 幻觉调用」的问题。

Plugin 工具组展开

OpenClaw 的 PluginToolGroups 支持用 group:plugins 统一引用所有插件工具:

export function expandPluginGroups(list, groups) {
  const expanded = [];
  for (const entry of list) {
    if (normalizeToolName(entry) === "group:plugins") {
      expanded.push(...groups.all);     // 展开为所有插件工具
    } else {
      const tools = groups.byPlugin.get(normalized);
      if (tools?.length > 0) {
        expanded.push(...tools);         // 展开为特定插件的工具
      } else {
        expanded.push(normalized);
      }
    }
  }
  return Array.from(new Set(expanded));
}

4 审批流:PreToolUse / PermissionRequest / 三级审批

工具审批是 Agent 安全的最后一道防线。这里出现两种截然不同的设计:

Codex CLI: Hook 系统的双层审批

Codex CLI

Codex CLI 用 Rust 实现了一个完整的 Hook 事件系统,在工具调用前后各有一个拦截点:

LLM 请求调用工具 │ ▼ ┌─────────────────────┐ │ PreToolUse Hook │ ← 可修改 tool_input / 可阻塞 │ ┌───────────────┐ │ │ │ matcher regex │ │ ← 按工具名匹配 handler │ │ execute hook │ │ ← 运行外部命令 │ │ parse output │ │ ← 解析 JSON 输出 │ └───────────────┘ │ │ 决策: approve/block │ │ 副作用: updated_input│ │ 附加: additional_ctx │ └────────┬────────────┘ │ (如果未被阻塞) ▼ ┌─────────────────────┐ │ PermissionRequest │ ← 独立于 guardian/user 审批 │ ┌───────────────┐ │ │ │ handler 执行 │ │ │ │ 决策折叠 │ │ │ └───────────────┘ │ │ 决策: allow/deny │ │ 规则: deny 优先 │ └────────┬────────────┘ │ (如果允许) ▼ 执行工具 │ ▼ ┌─────────────────────┐ │ PostToolUse Hook │ ← 可修改输出 / 注入上下文 │ updatedMCPToolOutput│ │ additional_context │ └─────────────────────┘

PreToolUse 的三种决策:

pub(crate) enum PreToolUseDecisionWire {
    #[serde(rename = "approve")]
    Approve,       // 明确批准
    #[serde(rename = "block")]
    Block,         // 明确阻塞
}

pub(crate) enum PreToolUsePermissionDecisionWire {
    #[serde(rename = "allow")]
    Allow,         // 自动允许
    #[serde(rename = "deny")]
    Deny,          // 自动拒绝
    #[serde(rename = "ask")]
    Ask,           // 交给用户决定
}

PermissionRequest 的保守折叠策略——任何 deny 都覆盖 allow:

fn resolve_permission_request_decision<'a>(
    decisions: impl IntoIterator,
) -> Option {
    let mut resolved_allow = None;
    for decision in decisions {
        match decision {
            PermissionRequestDecision::Allow => {
                resolved_allow = Some(PermissionRequestDecision::Allow);
            }
            PermissionRequestDecision::Deny { message } => {
                // deny 立即生效,覆盖之前的 allow
                return Some(PermissionRequestDecision::Deny {
                    message: message.clone(),
                });
            }
        }
    }
    resolved_allow
}
Codex CLI 的 Hook 系统有两种阻塞机制:PreToolUse 用 exit code 2 + stderr 快速阻塞(无需 JSON 输出);PermissionRequest 用结构化 JSON 做精细决策。两者解耦——PreToolUse 可以在不接触权限 UI 的情况下阻塞。

OpenClaw: Before-Tool-Call 钩子 + 信任策略

OpenClaw

OpenClaw 的 pi-tools.before-tool-call.ts 实现了类似的 before-call 拦截,但加入了信任工具策略插件审批解析

// 关键逻辑伪代码
async function beforeToolCall(tool, args, context) {
  // 1. 检查 before-tool-call 钩子
  const hookResults = await runBeforeToolCallHooks(tool, args);
  if (hookResults.shouldBlock) return { blocked: true, reason };

  // 2. 插件审批解析
  const pluginApproval = resolvePluginApproval(tool, context);
  if (pluginApproval === "deny") return { blocked: true };

  // 3. 信任工具策略——某些工具自动信任,跳过审批
  if (isTrustedTool(tool, context)) return { approved: true };

  // 4. 工具结果观察——某些工具的输出需要后处理
  const observer = getToolOutcomeObserver(tool);

  return { approved: true, observer };
}

三级审批对比

级别Codex CLIOpenClaw触发条件
自动允许PreToolUse → approveTrusted tool 策略低风险工具(read, search 等)
需审批PermissionRequest → askBefore-tool-call → plugin approval写操作、外部通信
硬拒绝PreToolUse → block / exit 2Policy Pipeline deny安全策略明确禁止

5 组合与扩展:Toolset、Bundle、MCP、Plugin

当工具数量从几个增长到几十个,如何组织和组合成为核心问题。

Hermes: 76 工具 + Toolset 组合系统

Hermes

Hermes 拥有所有项目中最大的工具集——76 个工具,通过 Toolset 系统组织为 40+ 个命名工具集:

_HERMES_CORE_TOOLS (38 tools) ├── Web: web_search, web_extract ├── Terminal: terminal, process ├── File: read_file, write_file, patch, search_files ├── Vision: vision_analyze, image_generate ├── Browser: browser_navigate, browser_snapshot, ... (13 tools) ├── Skills: skills_list, skill_view, skill_manage ├── Planning: todo, memory ├── Delegation: execute_code, delegate_task ├── Smart Home: ha_list_entities, ha_get_state, ... ├── Kanban: kanban_show, kanban_list, ... (9 tools) └── Messaging: send_message, text_to_speech TOOLSETS (40+ named groups) ├── Leaf: web, search, terminal, file, vision, ... ├── Composite: debugging = web + file + terminal │ safe = web + vision + image_gen ├── Platform: hermes-telegram, hermes-discord, ... └── Union: hermes-gateway = all platforms combined

Toolset 的核心能力是递归组合

def resolve_toolset(name: str, visited: Set[str] = None) -> List[str]:
    if visited is None:
        visited = set()

    # 特殊别名: "all" / "*" → 所有工具集的并集
    if name in {"all", "*"}:
        all_tools = set()
        for toolset_name in get_toolset_names():
            resolved = resolve_toolset(toolset_name, visited.copy())
            all_tools.update(resolved)
        return sorted(all_tools)

    # 环检测 (diamond 依赖安全)
    if name in visited:
        return []
    visited.add(name)

    toolset = get_toolset(name)
    tools = set(toolset.get("tools", []))

    # 递归解析 includes
    for included_name in toolset.get("includes", []):
        included_tools = resolve_toolset(included_name, visited)
        tools.update(included_tools)

    return sorted(tools)

平台特化:每个消息平台一个工具集

Hermes 为 20+ 消息平台各定义了一个 hermes-{platform} 工具集,大部分共享 _HERMES_CORE_TOOLS,特定平台追加专属工具:

"hermes-discord": {
    "tools": _HERMES_CORE_TOOLS + ["discord", "discord_admin"],
},
"hermes-feishu": {
    "tools": _HERMES_CORE_TOOLS + [
        "feishu_doc_read", "feishu_drive_list_comments", ...
    ],
},
"hermes-gateway": {
    "tools": [],
    "includes": ["hermes-telegram", "hermes-discord",
                 "hermes-whatsapp", ...],  # 组合所有平台
}
Hermes 的 Toolset 系统解决了「工具爆炸」问题:76 个工具通过 40+ 命名工具集组织,支持递归组合、diamond 依赖安全、运行时动态创建。Gateway 工具集是所有平台的并集——确保跨平台部署时工具完整。

SWE-agent: Bundle 系统

SWE-agent

SWE-agent 的 Bundle 是另一种组合模式——文件系统级打包

class Bundle(BaseModel):
    path: Path                      # Bundle 所在目录
    hidden_tools: list[str] = []    # 隐藏的工具名

    @property
    def commands(self) -> list[Command]:
        return [
            Command(name=tool, **tool_config)
            for tool, tool_config in self.config.tools.items()
            if tool not in self.hidden_tools
        ]

Bundle 的安装流程:

上传 Bundle 目录到容器 /root/tools/ chmod +x bin/* source install.sh (如果存在) 验证 which <command>

OpenClaw: Plugin + MCP 双扩展

OpenClaw

OpenClaw 支持两种工具扩展机制:

  1. Plugin 工具:通过 PluginToolGroups 管理,支持 group:plugins 统一引用和按 pluginId 分组
  2. MCP (Model Context Protocol):外部工具服务器,通过标准协议连接

Plugin 工具在 Policy Pipeline 中的特殊处理:

export function analyzeAllowlistByToolType(
  policy, groups, coreTools
): AllowlistResolution {
  const normalized = normalizeToolList(policy.allow);
  const pluginIds = new Set(groups.byPlugin.keys());
  const pluginTools = new Set(groups.all);
  let hasOnlyPluginEntries = true;

  for (const entry of normalized) {
    const isPluginEntry =
      entry === "group:plugins" ||
      pluginIds.has(entry) ||
      pluginTools.has(entry);
    if (!isPluginEntry) hasOnlyPluginEntries = false;
    // 未知条目(既非核心也非插件)→ 发出警告
    if (!isCoreEntry && !isPluginEntry) {
      unknownAllowlist.push(entry);
    }
  }
  return { policy, unknownAllowlist, pluginOnlyAllowlist: hasOnlyPluginEntries };
}

6 过滤与黑名单:从 Blocklist 到正则守卫

工具安全不仅是「谁能调用」,还有「什么命令不该执行」。

SWE-agent: 三级过滤系统

SWE-agent

SWE-agent 的 ToolFilterConfig 实现了最精细的命令过滤:

class ToolFilterConfig(BaseModel):
    # 级别 1: 前缀黑名单 — 阻止以这些开头的命令
    blocklist: list[str] = [
        "vim", "vi", "emacs", "nano",       # 交互式编辑器
        "nohup", "gdb",                      # 后台/调试
        "less", "tail -f",                   # 交互式查看
        "python -m venv", "make",           # 长时间运行
    ]

    # 级别 2: 精确黑名单 — 阻止完全匹配的命令
    blocklist_standalone: list[str] = [
        "python", "python3", "ipython",     # 裸 Python shell
        "bash", "sh", "/bin/bash",         # 裸 shell
        "nohup", "vi", "vim", "emacs",
        "nano", "su",
    ]

    # 级别 3: 条件黑名单 — 除非满足正则才允许
    block_unless_regex: dict[str, str] = {
        "radare2": r"\b(?:radare2)\b.*\s+-c\s+.*",  # 只有 -c 模式允许
        "r2":      r"\b(?:radare2)\b.*\s+-c\s+.*",
    }

过滤逻辑:

def should_block_action(self, action: str) -> bool:
    action = action.strip()
    if not action:
        return False
    # 前缀匹配
    if any(action.startswith(f) for f in self.config.filter.blocklist):
        return True
    # 精确匹配
    if action in self.config.filter.blocklist_standalone:
        return True
    # 条件正则: 命令名匹配但参数不满足正则 → 阻止
    name = action.split()[0]
    if name in self.config.filter.block_unless_regex:
        if not re.search(self.config.filter.block_unless_regex[name], action):
            return True
    return False
SWE-agent 的三级过滤体现了 ACI 哲学的另一面:不是「哪些命令允许」而是「哪些交互模式不允许」。交互式编辑器、裸 shell、后台进程——这些对人类正常但对 Agent 有害(会导致会话卡住)。

OpenClaw: Tool Allowlist Guard

OpenClaw

OpenClaw 的 tool-allowlist-guard.ts 处理一种边界情况——当策略过滤后没有可用工具时:

// 核心逻辑:显式白名单验证
// 当所有工具都被策略拒绝时,给出有意义的错误信息
// 而非让 LLM 幻觉调用不存在的工具

7 跨项目对比矩阵

维度 PydanticAI OpenClaw Codex CLI Hermes SWE-agent Smolagents
定义方式 函数 + 自动推断 TS 对象 + 插件 内置固定 Python 函数 + Toolset YAML + Shell 声明式类
Schema 生成 Pydantic 内部 API 手动 JSON Schema JSON Schema (Rust) 手动声明 YAML → JSON AST + 类型映射
类型安全 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐ ⭐⭐ ⭐⭐⭐⭐
安全层数 1 (Pydantic 验证) 8 (Policy Pipeline) 3 (Hook 三层) 2 (check_fn + Toolset) 3 (三级 Blocklist) 2 (验证 + 白名单)
审批流 Before-call + 信任策略 PreToolUse + PermissionRequest dangerous command 检查 无 (容器隔离)
组合能力 Plugin + MCP Hook matcher Toolset 递归组合 Bundle 打包 从 LangChain/Gradio 转换
工具数量 用户定义 30+ 核心 + 插件 3 (Bash/Read/Write) 76 5-15 (Bundle) 用户定义 + Hub
扩展机制 Python 函数 Plugin API + MCP Hook 脚本 Toolset + check_fn Bundle 目录 @tool 装饰器 + Hub
运行时验证 Pydantic validator Schema 验证 JSON Schema (Rust) check_fn 门控 which 命令检查 AUTHORIZED_TYPES

8 6 个核心模式

模式 1: Schema-First 定义

问题:LLM 只能理解 JSON Schema,如何确保 Python/TS 函数与 Schema 一致?

方案:从函数签名自动生成 Schema,而非手写。PydanticAI 是最佳实践——用 Pydantic 内部 API 保证 Schema 与验证器同源。

永远不要手写 JSON Schema。从类型注解自动生成,确保 Schema 和验证逻辑永远一致。PydanticAI 的 function_schema() 是当前最完整的实现。

模式 2: 渐进式安全策略

问题:不同场景需要不同安全级别——个人 CLI 最宽松,群聊最严格。

方案:OpenClaw 的 8 层 Policy Pipeline——每一层独立配置,层层收紧。越具体的环境(sender > group > agent > global)越后执行,确保细粒度策略覆盖粗粒度。

宽松 严格 │ │ ▼ ▼ profile ──→ provider ──→ global ──→ agent ──→ group ──→ sender │ │ │ │ │ │ └───────────┴───────────┴──────────┴──────────┴──────────┘ 层层收紧,后者覆盖前者

模式 3: 保守折叠审批

问题:多个审批钩子可能给出冲突决策,如何合并?

方案:Codex CLI 的 resolve_permission_request_decision——deny 总是胜出。这是安全系统的黄金法则:宁可误拒,不可误放

模式 4: 递归工具集组合

问题:工具数量增长后,如何避免 N² 的配置爆炸?

方案:Hermes 的 Toolset 系统——定义原子工具集,用 includes 组合,resolve_toolset() 递归展开。Diamond 依赖用 visited set 安全处理。

工具集组合的关键是原子性 + 递归组合 + 循环安全。Hermes 的 visited: Set[str] 是处理 diamond 依赖的最简方案——重复访问直接返回空列表。

模式 5: 条件性工具门控

问题:某些工具只在特定条件下可用(如 Home Assistant 需要 token,Kanban 需要 kanban worker 上下文)。

方案:Hermes 的 check_fn 模式——工具定义时附带检查函数,运行时按条件决定是否暴露:

# 工具注册时附带 check_fn
"send_message": {
    "check_fn": lambda: gateway_is_running(),  # 只在网关运行时暴露
},
"kanban_show": {
    "check_fn": lambda: os.getenv("HERMES_KANBAN_TASK"),  # 只在 kanban worker 中暴露
},
"computer_use": {
    "check_fn": lambda: is_cua_driver_installed(),  # 只在驱动安装后暴露
}

模式 6: 交互模式黑名单

问题:Agent 执行交互式命令(vim、python shell)会导致会话卡死。

方案:SWE-agent 的三级过滤——前缀黑名单、精确黑名单、条件正则。核心洞察:不是过滤"危险命令",而是过滤"交互模式"

常见的错误做法:用 allowlist 列出所有允许的命令。问题是新命令不断出现,allowlist 永远过时。SWE-agent 的 blocklist + 条件正则是更好的策略——默认允许,只阻止已知有害模式。

9 构建建议:按场景选型

场景 A: 内部工具 / 个人助手

安全需求低扩展性需求高

场景 B: 多用户平台 / SaaS

安全需求高隔离需求高

场景 C: 代码修复 / 沙箱执行

隔离需求极高工具集固定

场景 D: 消息平台 Bot

平台特化需求高工具数量多

终极建议

🛠️ 工具系统设计检查清单

  1. Schema 自动生成:永远不要手写 JSON Schema,从类型注解自动生成
  2. 定义时验证:工具注册时就验证签名一致性、类型合法性,而非运行时
  3. 渐进式安全:从全局到具体层层收紧,越具体的策略越后执行
  4. 保守审批折叠:多个审批结果冲突时,deny 总是胜出
  5. 条件性门控:用 check_fn 延迟工具暴露决策到运行时
  6. 过滤交互模式:不只过滤危险命令,更要过滤交互式使用模式
  7. 工具集组合:原子工具集 + 递归组合 + 循环安全
  8. Owner-Only 保护:管理类工具替换 execute 而非删除,避免幻觉调用
  9. 空工具集处理:策略过滤后无可用工具时,给出明确错误而非静默
  10. 平台特化:核心工具集共享,平台工具集追加,避免重复定义

源码版本:PydanticAI (main) · OpenClaw (2026.5) · Codex CLI (codex-rs main) · Hermes Agent (main) · SWE-agent (main) · Smolagents (main)
生成时间:2026-05-17 · ← 返回目录