nanobot 工具系统深度分析
概述
nanobot 的工具系统是其核心能力模块,为 AI 代理提供了与外部世界交互的能力。该系统采用极简设计理念,通过抽象的 Tool 基类和 ToolRegistry 注册表实现工具的动态管理和执行。
设计目标
- 轻量级: 核心代码仅约 700 行(基类 + 注册表 + 7 个内置工具)
- 可扩展: 通过简单的抽象机制轻松添加新工具
- 标准化: 完全兼容 OpenAI Function Calling API 规范
- 异步优先: 所有工具执行都是异步的,确保高并发性能
- 类型安全: 使用 Python 类型注解增强代码可靠性
工具抽象与注册
Tool 基类设计
Tool 是所有工具的抽象基类,定义了工具的标准接口。
文件位置: nanobot/agent/tools/base.py:7-56
class Tool(ABC):
@property
@abstractmethod
def name(self) -> str:
"""Tool name used in function calls."""
pass
@property
@abstractmethod
def description(self) -> str:
"""Description of what the tool does."""
pass
@property
@abstractmethod
def parameters(self) -> dict[str, Any]:
"""JSON Schema for tool parameters."""
pass
@abstractmethod
async def execute(self, **kwargs: Any) -> str:
"""Execute the tool with given parameters."""
pass
设计要点:
- 使用
@property和@abstractmethod强制子类实现必需属性 - 所有工具执行都是异步的,避免阻塞主循环
- 执行结果统一返回字符串,便于 LLM 理解
parameters属性直接返回 JSON Schema 格式,无需额外转换
ToolRegistry 注册表
ToolRegistry 负责工具的注册、查找和执行管理。
文件位置: nanobot/agent/tools/registry.py:8-71
class ToolRegistry:
def __init__(self):
self._tools: dict[str, Tool] = {}
def register(self, tool: Tool) -> None:
"""Register a tool."""
self._tools[tool.name] = tool
async def execute(self, name: str, params: dict[str, Any]) -> str:
"""Execute a tool by name with given parameters."""
tool = self._tools.get(name)
if not tool:
return f"Error: Tool '{name}' not found"
try:
return await tool.execute(**params)
except Exception as e:
return f"Error executing {name}: {str(e)}"
def get_definitions(self) -> list[dict[str, Any]]:
"""Get all tool definitions in OpenAI format."""
return [tool.to_schema() for tool in self._tools.values()]
设计要点:
- 使用字典存储工具,实现 O(1) 时间复杂度的查找
- 执行时捕获所有异常并返回错误信息,确保系统稳定
get_definitions()方法一键获取所有工具的 OpenAI 格式定义
工具类图
OpenAI Function Schema 转换
to_schema() 方法将工具转换为 OpenAI Function Calling 格式。
文件位置: nanobot/agent/tools/base.py:46-55
def to_schema(self) -> dict[str, Any]:
"""Convert tool to OpenAI function schema format."""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": self.parameters,
}
}
JSON Schema 格式示例:
以 ReadFileTool 为例:
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read the contents of a file at the given path.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string",
"description": "The file path to read"
}
},
"required": ["path"]
}
}
}
内置工具详解
工具清单
| 工具名称 | 类名 | 文件位置 | 主要功能 |
|---|---|---|---|
read_file | ReadFileTool | filesystem.py:9-46 | 读取文件内容 |
write_file | WriteFileTool | filesystem.py:49-86 | 写入文件内容 |
edit_file | EditFileTool | filesystem.py:89-144 | 替换编辑文件 |
list_dir | ListDirTool | filesystem.py:147-191 | 列出目录内容 |
exec | ExecTool | shell.py:10-85 | 执行 Shell 命令 |
web_search | WebSearchTool | web.py:31-75 | 网页搜索 |
web_fetch | WebFetchTool | web.py:78-139 | 获取网页内容 |
message | MessageTool | message.py:9-86 | 发送消息 |
spawn | SpawnTool | spawn.py:11-65 | 生成子代理 |
1. 文件系统工具
ReadFileTool (read_file)
文件位置: nanobot/agent/tools/filesystem.py:9-46
功能: 读取指定路径的文件内容
关键实现:
async def execute(self, path: str, **kwargs: Any) -> str:
try:
file_path = Path(path).expanduser() # 展开 ~ 符号
if not file_path.exists():
return f"Error: File not found: {path}"
if not file_path.is_file():
return f"Error: Not a file: {path}"
content = file_path.read_text(encoding="utf-8")
return content
except PermissionError:
return f"Error: Permission denied: {path}"
except Exception as e:
return f"Error reading file: {str(e)}"
安全特性:
- 使用
Path.expanduser()处理用户目录路径 - 验证文件存在性和类型
- 捕获权限错误
WriteFileTool (write_file)
文件位置: nanobot/agent/tools/filesystem.py:49-86
功能: 将内容写入文件,自动创建父目录
关键实现:
async def execute(self, path: str, content: str, **kwargs: Any) -> str:
try:
file_path = Path(path).expanduser()
file_path.parent.mkdir(parents=True, exist_ok=True) # 自动创建目录
file_path.write_text(content, encoding="utf-8")
return f"Successfully wrote {len(content)} bytes to {path}"
except PermissionError:
return f"Error: Permission denied: {path}"
except Exception as e:
return f"Error writing file: {str(e)}"
安全特性:
parents=True, exist_ok=True确保目录创建安全- 返回写入的字节数供用户确认
EditFileTool (edit_file)
文件位置: nanobot/agent/tools/filesystem.py:89-144
功能: 通过精确匹配替换文本编辑文件
关键实现:
async def execute(self, path: str, old_text: str, new_text: str, **kwargs: Any) -> str:
try:
file_path = Path(path).expanduser()
if not file_path.exists():
return f"Error: File not found: {path}"
content = file_path.read_text(encoding="utf-8")
if old_text not in content:
return f"Error: old_text not found in file. Make sure it matches exactly."
# Count occurrences - 防止误替换
count = content.count(old_text)
if count > 1:
return f"Warning: old_text appears {count} times. Please provide more context to make it unique."
new_content = content.replace(old_text, new_text, 1) # 只替换第一次出现
file_path.write_text(new_content, encoding="utf-8")
return f"Successfully edited {path}"
except PermissionError:
return f"Error: Permission denied: {path}"
except Exception as e:
return f"Error editing file: {str(e)}"
安全特性:
- 检查旧文本是否存在
- 重复检测: 如果旧文本出现多次,要求提供更多上下文
replace(..., 1)确保只替换一次
ListDirTool (list_dir)
文件位置: nanobot/agent/tools/filesystem.py:147-191
功能: 列出目录内容,使用图标区分文件和文件夹
关键实现:
async def execute(self, path: str, **kwargs: Any) -> str:
try:
dir_path = Path(path).expanduser()
if not dir_path.exists():
return f"Error: Directory not found: {path}"
if not dir_path.is_dir():
return f"Error: Not a directory: {path}"
items = []
for item in sorted(dir_path.iterdir()): # 排序输出
prefix = "📁 " if item.is_dir() else "📄 "
items.append(f"{prefix}{item.name}")
if not items:
return f"Directory {path} is empty"
return "\n".join(items)
except PermissionError:
return f"Error: Permission denied: {path}"
except Exception as e:
return f"Error listing directory: {str(e)}"
用户体验:
- 使用表情符号 (📁/📄) 直观区分目录和文件
- 字母排序便于查找
2. Shell 执行工具
ExecTool (exec)
文件位置: nanobot/agent/tools/shell.py:10-85
功能: 异步执行 Shell 命令,捕获 stdout/stderr
关键实现:
class ExecTool(Tool):
def __init__(self, timeout: int = 60, working_dir: str | None = None):
self.timeout = timeout
self.working_dir = working_dir
async def execute(self, command: str, working_dir: str | None = None, **kwargs: Any) -> str:
cwd = working_dir or self.working_dir or os.getcwd()
try:
process = await asyncio.create_subprocess_shell(
command,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=cwd,
)
try:
stdout, stderr = await asyncio.wait_for(
process.communicate(),
timeout=self.timeout
)
except asyncio.TimeoutError:
process.kill() # 超时后强制终止进程
return f"Error: Command timed out after {self.timeout} seconds"
output_parts = []
if stdout:
output_parts.append(stdout.decode("utf-8", errors="replace"))
if stderr:
stderr_text = stderr.decode("utf-8", errors="replace")
if stderr_text.strip():
output_parts.append(f"STDERR:\n{stderr_text}")
if process.returncode != 0:
output_parts.append(f"\nExit code: {process.returncode}")
result = "\n".join(output_parts) if output_parts else "(no output)"
# Truncate very long output
max_len = 10000
if len(result) > max_len:
result = result[:max_len] + f"\n... (truncated, {len(result) - max_len} more chars)"
return result
except Exception as e:
return f"Error executing command: {str(e)}"
安全特性:
- 超时控制: 默认 60 秒超时,超时后自动终止进程
- 输出截断: 超过 10000 字符自动截断,防止内存溢出
- 错误隔离: 使用
errors="replace"处理解码错误 - 工作目录支持: 支持默认和动态工作目录
异步化设计:
- 使用
asyncio.create_subprocess_shell创建异步子进程 - 使用
asyncio.wait_for实现超时控制 - 使用
process.kill()强制终止超时进程
3. Web 工具
WebSearchTool (web_search)
文件位置: nanobot/agent/tools/web.py:31-75
功能: 使用 Brave Search API 进行网页搜索
关键实现:
class WebSearchTool(Tool):
name = "web_search"
description = "Search the web. Returns titles, URLs, and snippets."
parameters = {
"type": "object",
"properties": {
"query": {"type": "string", "description": "Search query"},
"count": {"type": "integer", "description": "Results (1-10)", "minimum": 1, "maximum": 10}
},
"required": ["query"]
}
def __init__(self, api_key: str | None = None, max_results: int = 5):
self.api_key = api_key or os.environ.get("BRAVE_API_KEY", "")
self.max_results = max_results
async def execute(self, query: str, count: int | None = None, **kwargs: Any) -> str:
if not self.api_key:
return "Error: BRAVE_API_KEY not configured"
try:
n = min(max(count or self.max_results, 1), 10) # 限制结果数量
async with httpx.AsyncClient() as client:
r = await client.get(
"https://api.search.brave.com/res/v1/web/search",
params={"q": query, "count": n},
headers={"Accept": "application/json", "X-Subscription-Token": self.api_key},
timeout=10.0
)
r.raise_for_status()
results = r.json().get("web", {}).get("results", [])
if not results:
return f"No results for: {query}"
lines = [f"Results for: {query}\n"]
for i, item in enumerate(results[:n], 1):
lines.append(f"{i}. {item.get('title', '')}\n {item.get('url', '')}")
if desc := item.get("description"):
lines.append(f" {desc}")
return "\n".join(lines)
except Exception as e:
return f"Error: {e}"
设计要点:
- 支持环境变量和构造函数注入 API Key
- 限制结果数量 (1-10),控制成本
- 使用
httpx.AsyncClient进行异步 HTTP 请求 - 10 秒超时控制
WebFetchTool (web_fetch)
文件位置: nanobot/agent/tools/web.py:78-139
功能: 获取 URL 内容并提取可读文本
辅助函数:
def _strip_tags(text: str) -> str:
"""Remove HTML tags and decode entities."""
text = re.sub(r'<script[\s\S]*?</script>', '', text, flags=re.I)
text = re.sub(r'<style[\s\S]*?</style>', '', text, flags=re.I)
text = re.sub(r'<[^>]+>', '', text)
return html.unescape(text).strip()
def _normalize(text: str) -> str:
"""Normalize whitespace."""
text = re.sub(r'[ \t]+', ' ', text)
return re.sub(r'\n{3,}', '\n\n', text).strip()
关键实现:
class WebFetchTool(Tool):
name = "web_fetch"
description = "Fetch URL and extract readable content (HTML → markdown/text)."
parameters = {
"type": "object",
"properties": {
"url": {"type": "string", "description": "URL to fetch"},
"extractMode": {"type": "string", "enum": ["markdown", "text"], "default": "markdown"},
"maxChars": {"type": "integer", "minimum": 100}
},
"required": ["url"]
}
def __init__(self, max_chars: int = 50000):
self.max_chars = max_chars
async def execute(self, url: str, extractMode: str = "markdown", maxChars: int | None = None, **kwargs: Any) -> str:
from readability import Document
max_chars = maxChars or self.max_chars
try:
async with httpx.AsyncClient() as client:
r = await client.get(url, headers={"User-Agent": USER_AGENT}, follow_redirects=True, timeout=30.0)
r.raise_for_status()
ctype = r.headers.get("content-type", "")
# JSON 处理
if "application/json" in ctype:
text, extractor = json.dumps(r.json(), indent=2), "json"
# HTML 处理
elif "text/html" in ctype or r.text[:256].lower().startswith(("<!doctype", "<html")):
doc = Document(r.text)
content = self._to_markdown(doc.summary()) if extractMode == "markdown" else _strip_tags(doc.summary())
text = f"# {doc.title()}\n\n{content}" if doc.title() else content
extractor = "readability"
else:
text, extractor = r.text, "raw"
truncated = len(text) > max_chars
if truncated:
text = text[:max_chars]
return json.dumps({"url": url, "finalUrl": str(r.url), "status": r.status_code,
"extractor": extractor, "truncated": truncated, "length": len(text), "text": text})
except Exception as e:
return json.dumps({"error": str(e), "url": url})
def _to_markdown(self, html: str) -> str:
"""Convert HTML to markdown."""
# 转换链接、标题、列表后再移除标签
text = re.sub(r'<a\s+[^>]*href=["\']([^"\']+)["\'][^>]*>([\s\S]*?)</a>',
lambda m: f'[{_strip_tags(m[2])}]({m[1]})', html, flags=re.I)
text = re.sub(r'<h([1-6])[^>]*>([\s\S]*?)</h\1>',
lambda m: f'\n{"#" * int(m[1])} {_strip_tags(m[2])}\n', text, flags=re.I)
text = re.sub(r'<li[^>]*>([\s\S]*?)</li>', lambda m: f'\n- {_strip_tags(m[1])}', html, flags=re.I)
text = re.sub(r'</(p|div|section|article)>', '\n\n', text, flags=re.I)
text = re.sub(r'<(br|hr)\s*/?>', '\n', text, flags=re.I)
return _normalize(_strip_tags(text))
设计要点:
- 智能内容类型检测: 自动识别 JSON/HTML/纯文本
- Readability 集成: 使用
readability-lxml提取主要内容 - Markdown 转换: 将 HTML 转换为 Markdown 格式
- 结构化输出: 返回 JSON 格式,包含元数据
- 用户代理: 使用真实的浏览器 UA,避免被屏蔽
Markdown 转换策略:
- 先转换链接 (
<a> → [text](url)) - 再转换标题 (
<h1-6> → # 文本) - 转换列表 (
<li> → - 文本) - 最后处理段落和换行
4. 消息发送工具
MessageTool (message)
文件位置: nanobot/agent/tools/message.py:9-86
功能: 向用户发送消息,支持跨渠道发送
关键实现:
class MessageTool(Tool):
def __init__(
self,
send_callback: Callable[[OutboundMessage], Awaitable[None]] | None = None,
default_channel: str = "",
default_chat_id: str = ""
):
self._send_callback = send_callback
self._default_channel = default_channel
self._default_chat_id = default_chat_id
def set_context(self, channel: str, chat_id: str) -> None:
"""Set the current message context."""
self._default_channel = channel
self._default_chat_id = chat_id
def set_send_callback(self, callback: Callable[[OutboundMessage], Awaitable[None]]) -> None:
"""Set the callback for sending messages."""
self._send_callback = callback
@property
def name(self) -> str:
return "message"
@property
def description(self) -> str:
return "Send a message to the user. Use this when you want to communicate something."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"content": {
"type": "string",
"description": "The message content to send"
},
"channel": {
"type": "string",
"description": "Optional: target channel (telegram, discord, etc.)"
},
"chat_id": {
"type": "string",
"description": "Optional: target chat/user ID"
}
},
"required": ["content"]
}
async def execute(
self,
content: str,
channel: str | None = None,
chat_id: str | None = None,
**kwargs: Any
) -> str:
channel = channel or self._default_channel
chat_id = chat_id or self._default_chat_id
if not channel or not chat_id:
return "Error: No target channel/chat specified"
if not self._send_callback:
return "Error: Message sending not configured"
msg = OutboundMessage(
channel=channel,
chat_id=chat_id,
content=content
)
try:
await self._send_callback(msg)
return f"Message sent to {channel}:{chat_id}"
except Exception as e:
return f"Error sending message: {str(e)}"
设计要点:
- 回调模式: 通过
send_callback注入消息发送逻辑 - 上下文注入:
set_context()动态设置目标渠道 - 渠道抽象: 统一
OutboundMessage格式,支持 Telegram/Discord 等多渠道 - 默认值机制: 可以指定默认 channel/chat_id,简化调用
使用场景:
# 在 AgentLoop 中初始化 (loop.py:82)
message_tool = MessageTool(send_callback=self.bus.publish_outbound)
self.tools.register(message_tool)
# 动态设置上下文 (loop.py:145-146)
message_tool = self.tools.get("message")
if isinstance(message_tool, MessageTool):
message_tool.set_context(msg.channel, msg.chat_id)
5. 子代理生成工具
SpawnTool (spawn)
文件位置: nanobot/agent/tools/spawn.py:11-65
功能: 生成子代理在后台执行耗时任务
关键实现:
class SpawnTool(Tool):
def __init__(self, manager: "SubagentManager"):
self._manager = manager
self._origin_channel = "cli"
self._origin_chat_id = "direct"
def set_context(self, channel: str, chat_id: str) -> None:
"""Set the origin context for subagent announcements."""
self._origin_channel = channel
self._origin_chat_id = chat_id
@property
def name(self) -> str:
return "spawn"
@property
def description(self) -> str:
return (
"Spawn a subagent to handle a task in the background. "
"Use this for complex or time-consuming tasks that can run independently. "
"The subagent will complete the task and report back when done."
)
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"task": {
"type": "string",
"description": "The task for the subagent to complete",
},
"label": {
"type": "string",
"description": "Optional short label for the task (for display)",
},
},
"required": ["task"],
}
async def execute(self, task: str, label: str | None = None, **kwargs: Any) -> str:
"""Spawn a subagent to execute the given task."""
return await self._manager.spawn(
task=task,
label=label,
origin_channel=self._origin_channel,
origin_chat_id=self._origin_chat_id,
)
设计要点:
- 依赖注入: 通过
SubagentManager管理子代理生命周期 - 结果回调: 子代理完成后通过消息总线通知主代理
- 上下文传递: 保存原始渠道信息,确保结果发送到正确位置
- 任务标签: 支持可选的任务标签,便于用户追踪
子代理工具集限制:
# 子代理不包含 message 和 spawn 工具 (subagent.py:94-101)
tools = ToolRegistry()
tools.register(ReadFileTool())
tools.register(WriteFileTool())
tools.register(ListDirTool())
tools.register(ExecTool(working_dir=str(self.workspace)))
tools.register(WebSearchTool(api_key=self.brave_api_key))
tools.register(WebFetchTool())
# 注意: 没有 MessageTool 和 SpawnTool,防止无限递归
工具执行流程
完整执行流程
工具注册流程
工具执行详细流程
文件位置: nanobot/agent/loop.py:163-202
# Agent loop
iteration = 0
final_content = None
while iteration < self.max_iterations:
iteration += 1
# 调用 LLM
response = await self.provider.chat(
messages=messages,
tools=self.tools.get_definitions(), # 获取所有工具定义
model=self.model
)
# 处理工具调用
if response.has_tool_calls:
# 添加助手消息(包含工具调用)
tool_call_dicts = [
{
"id": tc.id,
"type": "function",
"function": {
"name": tc.name,
"arguments": json.dumps(tc.arguments)
}
}
for tc in response.tool_calls
]
messages = self.context.add_assistant_message(
messages, response.content, tool_call_dicts
)
# 执行工具
for tool_call in response.tool_calls:
args_str = json.dumps(tool_call.arguments)
logger.debug(f"Executing tool: {tool_call.name} with arguments: {args_str}")
result = await self.tools.execute(tool_call.name, tool_call.arguments) # ← 工具执行
messages = self.context.add_tool_result(
messages, tool_call.id, tool_call.name, result
)
else:
# 没有工具调用,结束循环
final_content = response.content
break
错误处理机制
所有工具都遵循统一的错误处理模式:
try:
# 执行工具逻辑
result = ...
return result
except PermissionError:
return f"Error: Permission denied: {path}"
except FileNotFoundError:
return f"Error: File not found: {path}"
except Exception as e:
return f"Error executing {name}: {str(e)}"
优点:
- 错误信息对 LLM 友好(纯文本)
- 不会因工具错误导致整个循环崩溃
- 便于 LLM 理解错误原因并尝试恢复
JSON Schema 格式说明
标准 JSON Schema 结构
nanobot 工具使用的 JSON Schema 遵循 OpenAI Function Calling 规范:
{
"type": "object",
"properties": {
"param1": {
"type": "string",
"description": "参数描述"
},
"param2": {
"type": "integer",
"description": "参数描述",
"minimum": 0,
"maximum": 100
}
},
"required": ["param1"]
}
支持的数据类型
| 类型 | 说明 | 示例 |
|---|---|---|
string | 字符串 | "path": {"type": "string"} |
integer | 整数 | "count": {"type": "integer", "minimum": 1, "maximum": 10} |
boolean | 布尔值 | "force": {"type": "boolean"} |
array | 数组 | "items": {"type": "array", "items": {"type": "string"}} |
object | 对象 | "config": {"type": "object"} |
高级特性示例
枚举类型
"extractMode": {"type": "string", "enum": ["markdown", "text"], "default": "markdown"}
可选参数
"properties": {
"required_param": {"type": "string"},
"optional_param": {"type": "string"}
},
"required": ["required_param"] # 只列出必需参数
关键代码位置索引
核心模块
| 文件 | 路径 | 行号 | 说明 |
|---|---|---|---|
| Tool 基类 | nanobot/agent/tools/base.py | 7-56 | 工具抽象基类 |
| ToolRegistry | nanobot/agent/tools/registry.py | 8-71 | 工具注册表 |
| ReadFileTool | nanobot/agent/tools/filesystem.py | 9-46 | 读文件工具 |
| WriteFileTool | nanobot/agent/tools/filesystem.py | 49-86 | 写文件工具 |
| EditFileTool | nanobot/agent/tools/filesystem.py | 89-144 | 编辑文件工具 |
| ListDirTool | nanobot/agent/tools/filesystem.py | 147-191 | 列目录工具 |
| ExecTool | nanobot/agent/tools/shell.py | 10-85 | Shell 执行工具 |
| WebSearchTool | nanobot/agent/tools/web.py | 31-75 | 网页搜索工具 |
| WebFetchTool | nanobot/agent/tools/web.py | 78-139 | 网页获取工具 |
| MessageTool | nanobot/agent/tools/message.py | 9-86 | 消息发送工具 |
| SpawnTool | nanobot/agent/tools/spawn.py | 11-65 | 子代理生成工具 |
集成代码
| 文件 | 路径 | 行号 | 说明 |
|---|---|---|---|
| 工具注册 | nanobot/agent/loop.py | 66-87 | _register_default_tools |
| 工具执行 | nanobot/agent/loop.py | 192-198 | 执行工具调用 |
| 上下文设置 | nanobot/agent/loop.py | 144-150 | 设置 MessageTool/SpawnTool 上下文 |
| 子代理管理 | nanobot/agent/subagent.py | 20-234 | SubagentManager 实现 |
数据结构
| 文件 | 路径 | 行号 | 说明 |
|---|---|---|---|
| InboundMessage | nanobot/bus/events.py | 8-24 | 入站消息 |
| OutboundMessage | nanobot/bus/events.py | 26-36 | 出站消息 |
深挖价值点
1. 异步化工具执行设计 ⭐⭐⭐⭐⭐
为什么值得深挖:
- 所有工具都是异步的,充分利用 Python 的 asyncio 优势
asyncio.create_subprocess_shell实现非阻塞 Shell 执行asyncio.wait_for实现优雅的超时控制- 超时后自动
process.kill()防止僵尸进程
深挖方向:
- 多工具并发执行优化
- 任务取消和清理机制
- 资源限制(CPU/内存)集成
2. 工具上下文注入机制 ⭐⭐⭐⭐⭐
为什么值得深挖:
MessageTool和SpawnTool支持动态上下文设置- 通过
set_context()在运行时注入渠道信息 - 解决了工具在不同场景下的复用问题
深挖方向:
- 更通用的上下文传递机制
- 上下文链追踪
- 上下文隔离策略
3. EditFileTool 的安全替换策略 ⭐⭐⭐⭐
为什么值得深挖:
- 检查重复出现,防止误替换
- 只替换第一次出现 (
replace(..., 1)) - 要求精确匹配,提高安全性
深挖方向:
- 支持正则表达式替换
- 支持行号定位替换
- 支持 diff 预览模式
4. 子代理工具集限制 ⭐⭐⭐⭐⭐
为什么值得深挖:
- 子代理不包含
message和spawn工具 - 防止无限递归生成子代理
- 确保子代理专注于单一任务
深挖方向:
- 更细粒度的工具权限控制
- 工具依赖关系管理
- 资源配额管理
5. WebFetchTool 的 Markdown 转换 ⭐⭐⭐
为什么值得深挖:
- 自定义 HTML 到 Markdown 转换逻辑
- 先转换结构化元素,再移除标签
- 保留链接、标题、列表语义
深挖方向:
- 集成更强大的 markdown 库(如 markdownify)
- 代码块和表格支持
- 图片 alt 文本提取
6. 输出截断和错误隔离 ⭐⭐⭐⭐
为什么值得深挖:
ExecTool限制输出为 10000 字符WebFetchTool限制提取为 50000 字符- 所有工具都捕获异常并返回友好错误信息
深挖方向:
- 自适应截断(按段落/句子)
- 错误重试机制
- 流式输出支持
扩展工具示例
示例:数据库查询工具
from nanobot.agent.tools.base import Tool
import asyncpg
class DatabaseTool(Tool):
def __init__(self, connection_string: str):
self._conn_string = connection_string
@property
def name(self) -> str:
return "db_query"
@property
def description(self) -> str:
return "Execute a SQL query and return results. Use with caution."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "SQL query to execute"
}
},
"required": ["query"]
}
async def execute(self, query: str, **kwargs: Any) -> str:
try:
conn = await asyncpg.connect(self._conn_string)
rows = await conn.fetch(query)
await conn.close()
if not rows:
return "Query returned no results"
# Format results
lines = []
for i, row in enumerate(rows, 1):
lines.append(f"Row {i}: {dict(row)}")
return "\n".join(lines)
except Exception as e:
return f"Database error: {str(e)}"
示例:HTTP API 调用工具
import httpx
from nanobot.agent.tools.base import Tool
class ApiCallTool(Tool):
@property
def name(self) -> str:
return "api_call"
@property
def description(self) -> str:
return "Make an HTTP API call. Supports GET, POST, PUT, DELETE."
@property
def parameters(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
"url": {"type": "string", "description": "API endpoint URL"},
"method": {"type": "string", "enum": ["GET", "POST", "PUT", "DELETE"], "default": "GET"},
"body": {"type": "string", "description": "JSON body for POST/PUT"},
"headers": {"type": "object", "description": "HTTP headers"}
},
"required": ["url"]
}
async def execute(self, url: str, method: str = "GET", body: str | None = None, headers: dict | None = None, **kwargs: Any) -> str:
try:
json_body = json.loads(body) if body else None
async with httpx.AsyncClient() as client:
response = await client.request(
method=method,
url=url,
json=json_body,
headers=headers,
timeout=30.0
)
response.raise_for_status()
return json.dumps(response.json(), indent=2)
except Exception as e:
return f"API call failed: {str(e)}"
总结
nanobot 的工具系统设计展现了极简主义与实用主义的完美结合:
优点
- 极简架构: 核心代码约 700 行,易于理解和维护
- 标准化: 完全兼容 OpenAI Function Calling API
- 异步优先: 所有工具都是异步的,性能优秀
- 安全可靠: 完善的错误处理和资源限制
- 易于扩展: 清晰的抽象,添加新工具非常简单
设计亮点
- 属性模式: 使用
@property定义工具元数据,代码简洁 - 上下文注入: 动态设置目标渠道,实现工具复用
- 输出截断: 防止长输出影响性能
- 子代理隔离: 限制子代理工具集,防止递归爆炸
适用场景
- 个人 AI 助手
- 自动化任务执行
- 代码审查和重构
- 文档生成和管理
- 数据分析和可视化
这个工具系统证明了:简洁的设计也能实现强大的功能。