Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • nanobot 工具系统深度分析

nanobot 工具系统深度分析

概述

nanobot 的工具系统是其核心能力模块,为 AI 代理提供了与外部世界交互的能力。该系统采用极简设计理念,通过抽象的 Tool 基类和 ToolRegistry 注册表实现工具的动态管理和执行。

设计目标

  1. 轻量级: 核心代码仅约 700 行(基类 + 注册表 + 7 个内置工具)
  2. 可扩展: 通过简单的抽象机制轻松添加新工具
  3. 标准化: 完全兼容 OpenAI Function Calling API 规范
  4. 异步优先: 所有工具执行都是异步的,确保高并发性能
  5. 类型安全: 使用 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_fileReadFileToolfilesystem.py:9-46读取文件内容
write_fileWriteFileToolfilesystem.py:49-86写入文件内容
edit_fileEditFileToolfilesystem.py:89-144替换编辑文件
list_dirListDirToolfilesystem.py:147-191列出目录内容
execExecToolshell.py:10-85执行 Shell 命令
web_searchWebSearchToolweb.py:31-75网页搜索
web_fetchWebFetchToolweb.py:78-139获取网页内容
messageMessageToolmessage.py:9-86发送消息
spawnSpawnToolspawn.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 转换策略:

  1. 先转换链接 (<a> → [text](url))
  2. 再转换标题 (<h1-6> → # 文本)
  3. 转换列表 (<li> → - 文本)
  4. 最后处理段落和换行

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.py7-56工具抽象基类
ToolRegistrynanobot/agent/tools/registry.py8-71工具注册表
ReadFileToolnanobot/agent/tools/filesystem.py9-46读文件工具
WriteFileToolnanobot/agent/tools/filesystem.py49-86写文件工具
EditFileToolnanobot/agent/tools/filesystem.py89-144编辑文件工具
ListDirToolnanobot/agent/tools/filesystem.py147-191列目录工具
ExecToolnanobot/agent/tools/shell.py10-85Shell 执行工具
WebSearchToolnanobot/agent/tools/web.py31-75网页搜索工具
WebFetchToolnanobot/agent/tools/web.py78-139网页获取工具
MessageToolnanobot/agent/tools/message.py9-86消息发送工具
SpawnToolnanobot/agent/tools/spawn.py11-65子代理生成工具

集成代码

文件路径行号说明
工具注册nanobot/agent/loop.py66-87_register_default_tools
工具执行nanobot/agent/loop.py192-198执行工具调用
上下文设置nanobot/agent/loop.py144-150设置 MessageTool/SpawnTool 上下文
子代理管理nanobot/agent/subagent.py20-234SubagentManager 实现

数据结构

文件路径行号说明
InboundMessagenanobot/bus/events.py8-24入站消息
OutboundMessagenanobot/bus/events.py26-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 的工具系统设计展现了极简主义与实用主义的完美结合:

优点

  1. 极简架构: 核心代码约 700 行,易于理解和维护
  2. 标准化: 完全兼容 OpenAI Function Calling API
  3. 异步优先: 所有工具都是异步的,性能优秀
  4. 安全可靠: 完善的错误处理和资源限制
  5. 易于扩展: 清晰的抽象,添加新工具非常简单

设计亮点

  • 属性模式: 使用 @property 定义工具元数据,代码简洁
  • 上下文注入: 动态设置目标渠道,实现工具复用
  • 输出截断: 防止长输出影响性能
  • 子代理隔离: 限制子代理工具集,防止递归爆炸

适用场景

  • 个人 AI 助手
  • 自动化任务执行
  • 代码审查和重构
  • 文档生成和管理
  • 数据分析和可视化

这个工具系统证明了:简洁的设计也能实现强大的功能。