Skill.md
Agent Tool Builder
帮助你使用 fail-closed 设计模式定义 Agent 工具: 统一的类将身份标识、Schema、安全属性与执行逻辑放在一起, fail-closed 默认值确保新工具默认安全。
为什么这个模式重要
临时工具定义缺少的三件事:
- Fail-closed 默认值 —
is_read_only、is_destructive、is_concurrency_safe全部默认为 False。 忘记声明属性的工具会被保守地视为具有写入能力。 - 分层执行 —
validate_semantics → check_permissions → _call是独立方法, 验证逻辑不会混入权限逻辑或业务逻辑。 - 自包含定义 — Schema、描述、安全元数据和执行逻辑全部在同一个地方。无需额外的中间件连接。
工作流程
Step 1 — 确定目标框架
询问工具将在哪个 Agent 框架中注册(例如 hermes-agent、LangChain、纯 Python)。 这决定了导入路径和注册方式,但设计原则完全相同。
检查项目的 utils/tools 目录中是否存在 agent_tool_base.py。
如果不存在,从此 skill 目录的 references/agent_tool_base.py 复制过去。
告知用户文件放置位置。
Step 2 — 采访用户
收集以下问题的答案。显示了默认值——对于默认值明显合适的问题可跳过。
命名约定:使用 {service}_{action}_{resource} 格式,带服务前缀,确保同时加载多个工具集时不产生歧义(例如 stock_get_price、stock_list_symbols、stock_search_news)。以动词开头:get、list、search、create、delete。
| 字段 | 问题 | 默认值 |
|---|---|---|
name | 工具名(格式:{service}_{action}_{resource},例如 stock_get_price) | — 必填 |
description | 给 LLM 看的一句话描述:精确匹配实际功能,不要模糊扩大,否则 agent 会在不该用的场景误调用 | — 必填 |
| Schema 字段 | 工具接受哪些参数?(字段名、类型、说明;在 Field description 里加 example) | — 必填 |
is_read_only | 这个工具只读数据,不写入/不产生副作用吗? | False |
is_destructive | 这个工具会做不可逆操作(删除、覆盖)吗? | False |
is_concurrency_safe | 这个工具可以和其他工具同时运行吗? | False |
response_format | 返回数据是给 agent 程序化处理(JSON)还是给用户展示(Markdown)? | 视场景,默认 Markdown |
| 是否列表工具 | 如果返回多条记录,要支持分页吗? | 超过 50 条建议加 |
_validate_input_semantics | 有没有需要在执行前拦截的语义问题?(如:参数太短、格式不对) | 不需要 |
_check_permissions | 有没有需要检查的权限?(如:需要某个 env var、调用方身份限制) | 不需要 |
_call | 工具的核心执行逻辑是什么? | — 必填 |
不必一次性提问所有问题——从上下文推断合理答案。
例如,"search" 或 "get" 工具几乎肯定是 is_read_only=True, is_concurrency_safe=True。
Step 3 — 生成工具文件
为工具创建一个 .py 文件。遵循以下字段顺序:
1. imports
2. 输入 schema(Pydantic BaseModel)
3. 工具类:
a. name, description, args_schema — 身份标识
b. is_read_only, is_destructive, is_concurrency_safe, max_result_chars — 安全元数据
c. _validate_input_semantics() — 语义验证(不需要时省略)
d. _check_permissions() — 权限检查(不需要时省略)
e. _call() — 实际逻辑
建议与项目工具目录结构一致的文件路径。
Step 4 — 展示安全属性摘要
生成后,打印工具安全态势的一行摘要:
StockGetPriceTool: read_only=True destructive=False concurrency_safe=True max_result=10K
输出模板
"""<tool_name>.py — <一行描述>"""
from typing import Optional
from pydantic import BaseModel, Field
from base.utils.agent_tool_base import AgentTool
# ---------------------------------------------------------------------------
# 输入 schema
# ---------------------------------------------------------------------------
class <ToolName>Input(BaseModel):
<field_name>: <type> = Field(description="<描述>. e.g. '<示例>'")
# ... 更多字段
# ---------------------------------------------------------------------------
# 工具类
# ---------------------------------------------------------------------------
class <ToolName>Tool(AgentTool):
# — 身份标识 —
name: str = "<tool_name>"
description: str = "<给 LLM 的一句话描述>"
args_schema = <ToolName>Input
# — 安全元数据(fail-closed:只有验证后才设为 True)—
is_read_only: bool = <True/False>
is_destructive: bool = <True/False>
is_concurrency_safe: bool = <True/False>
max_result_chars: int = 10_000
# — 语义验证(不需要输入约束时省略)—
def _validate_input_semantics(self, <params>, **kwargs) -> tuple[bool, Optional[str]]:
if not <condition>:
return False, "<为什么无效>. 尝试 <具体修复>"
return True, None
# — 权限检查(不需要访问控制时省略)—
def _check_permissions(self, <params>, **kwargs) -> tuple[bool, Optional[str]]:
if not <allowed>:
return False, "<为什么拒绝>. <建议下一步>"
return True, None
# — 核心逻辑 —
def _call(self, <params>, **kwargs) -> str:
# ... 在此实现工具逻辑
return result
常见安全属性模式
| 工具类型 | is_read_only | is_destructive | is_concurrency_safe |
|---|---|---|---|
| 搜索 / 查询 | True | False | True |
| 文件读取 | True | False | True |
| 文件写入 / 修改 | False | False | False |
| 删除操作 | False | True | False |
| API 调用(GET) | True | False | True |
| API 调用(POST/DELETE) | False | 视情况 | False |
| 数据库查询 | True | False | True |
| 数据库写入 | False | False | False |
输出设计原则
原子工具——一个工具,一个职责
保持每个工具专注于单一操作。让 agent 组合多个工具来完成复杂任务。功能过多的工具更难被 agent 复用和推理。
响应格式——JSON vs Markdown
| 格式 | 使用场景 |
|---|---|
| JSON | Agent 需要以编程方式解析/过滤结果 |
| Markdown | 结果将直接展示给用户 |
不确定时两种都支持——接受可选的 response_format: str = "markdown" 参数并在 _call 中分支。JSON 输出使用 json.dumps(data, ensure_ascii=False, indent=2)。
列表工具分页
任何可能返回超过约 50 条记录的工具都应支持分页:
return json.dumps({
"items": [...],
"total": 150,
"count": 20,
"offset": 0,
"has_more": True,
"next_offset": 20,
}, ensure_ascii=False, indent=2)
在输入 schema 中添加 offset: int = Field(default=0, description="分页偏移量") 和 limit: int = Field(default=20, description="最多返回条数")。
可操作的错误消息
错误字符串必须引导 agent 找到修复方法——而不仅仅是描述失败:
# 不好:agent 卡住了
return False, "查询太短。"
# 好:agent 确切知道下一步该做什么
return False, "查询太短(得到 2 个字符,需要 >= 3)。请提供更具体的搜索词。"
参考文件
references/agent_tool_base.py— 完整的 AgentTool 基类(纯 Python,无框架依赖)