Logo
热心市民王先生

关键代码验证

技术研究 AI Agent 代码分析

nanobot 核心代码片段分析,验证其技术实现的简洁性和可扩展性

快速开始验证

安装与初始化

nanobot 的安装极为简单,体现了其”开箱即用”的设计理念:

# 方式一:从 PyPI 安装(稳定版)
pip install nanobot-ai

# 方式二:使用 uv 安装(更快)
uv tool install nanobot-ai

# 方式三:从源码安装(最新功能)
git clone https://github.com/HKUDS/nanobot.git
cd nanobot
pip install -e .

初始化仅需一条命令:

nanobot onboard

这会创建 ~/.nanobot/config.json 配置文件,并引导用户完成基本配置。

最小配置示例

{
  "providers": {
    "openrouter": {
      "apiKey": "sk-or-v1-xxx"
    }
  },
  "agents": {
    "defaults": {
      "model": "anthropic/claude-opus-4-5",
      "provider": "openrouter"
    }
  }
}

启动 Agent:

nanobot agent

从安装到运行,仅需 2 分钟。

核心代码结构

项目目录结构

nanobot/
├── nanobot/
│   ├── agent/           # Agent 核心逻辑
│   │   ├── __init__.py
│   │   ├── core.py      # 主循环
│   │   └── tools.py     # 工具定义
│   ├── providers/       # LLM Provider
│   │   ├── __init__.py
│   │   ├── registry.py  # Provider 注册表
│   │   └── base.py      # 基类
│   ├── channels/        # 消息平台
│   │   ├── telegram.py
│   │   ├── discord.py
│   │   ├── whatsapp.py
│   │   └── ...          # 其他平台
│   ├── memory/          # 记忆系统
│   │   └── store.py
│   ├── tools/           # 内置工具
│   │   ├── web_search.py
│   │   ├── memory.py
│   │   └── schedule.py
│   └── config/          # 配置管理
│       └── schema.py
├── bridge/              # WhatsApp Bridge (Go)
├── tests/
└── pyproject.toml

核心代码量验证

运行项目提供的脚本:

bash core_agent_lines.sh

输出示例:

Core agent lines: 3935

这证实了 nanobot 的核心代码确实控制在约 4,000 行以内。

关键代码片段

1. Provider 注册机制

nanobot/providers/registry.py 的核心设计:

from dataclasses import dataclass
from typing import Tuple, Optional

@dataclass
class ProviderSpec:
    """Provider 规格定义"""
    name: str                           # 配置字段名
    keywords: Tuple[str, ...]          # 模型名关键词
    env_key: str                        # 环境变量名
    display_name: str                   # 显示名称
    litellm_prefix: Optional[str] = None  # LiteLLM 前缀
    skip_prefixes: Tuple[str, ...] = ()   # 跳过前缀

# Provider 注册表
PROVIDERS = {
    "openai": ProviderSpec(
        name="openai",
        keywords=("gpt", "o1", "o3"),
        env_key="OPENAI_API_KEY",
        display_name="OpenAI",
        litellm_prefix="openai",
    ),
    "anthropic": ProviderSpec(
        name="anthropic",
        keywords=("claude",),
        env_key="ANTHROPIC_API_KEY",
        display_name="Anthropic",
        litellm_prefix="anthropic",
    ),
    "deepseek": ProviderSpec(
        name="deepseek",
        keywords=("deepseek",),
        env_key="DEEPSEEK_API_KEY",
        display_name="DeepSeek",
        litellm_prefix="deepseek",
    ),
    # ... 更多 Provider
}

def match_provider(model: str) -> Optional[ProviderSpec]:
    """根据模型名自动匹配 Provider"""
    model_lower = model.lower()
    for spec in PROVIDERS.values():
        if any(kw in model_lower for kw in spec.keywords):
            return spec
    return None

设计亮点

  • 使用 dataclass 简化定义
  • 注册表模式避免 if-elif 链
  • 自动匹配降低配置复杂度

2. Agent 核心循环

nanobot/agent/core.py 的简化实现:

import asyncio
from typing import List, Dict, Any

class Agent:
    def __init__(self, provider, tools: List[Tool]):
        self.provider = provider
        self.tools = {t.name: t for t in tools}
        self.max_iterations = 10
    
    async def run(self, messages: List[Dict]) -> str:
        """运行 Agent 循环"""
        iteration = 0
        
        while iteration < self.max_iterations:
            # 调用 LLM
            response = await self.provider.chat(
                messages=messages,
                tools=list(self.tools.values())
            )
            
            # 检查是否有工具调用
            if not response.tool_calls:
                return response.content
            
            # 执行工具调用
            tool_results = await asyncio.gather(*[
                self.execute_tool(tc) 
                for tc in response.tool_calls
            ])
            
            # 将结果加入消息历史
            messages.append({
                "role": "assistant",
                "tool_calls": response.tool_calls
            })
            for result in tool_results:
                messages.append({
                    "role": "tool",
                    "content": result
                })
            
            iteration += 1
        
        return "达到最大迭代次数"
    
    async def execute_tool(self, tool_call) -> str:
        """执行单个工具调用"""
        tool = self.tools.get(tool_call.function.name)
        if not tool:
            return f"未知工具: {tool_call.function.name}"
        
        args = json.loads(tool_call.function.arguments)
        return await tool.execute(**args)

设计亮点

  • 清晰的循环终止条件
  • 工具并行执行(asyncio.gather
  • 迭代次数限制防止死循环

3. Channel 接口抽象

nanobot/channels/base.py

from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Optional

@dataclass
class Message:
    """统一消息格式"""
    content: str
    sender_id: str
    session_id: str
    channel: str
    attachments: Optional[List[Attachment]] = None

class BaseChannel(ABC):
    """Channel 基类"""
    
    @abstractmethod
    async def start(self):
        """启动 Channel"""
        pass
    
    @abstractmethod
    async def stop(self):
        """停止 Channel"""
        pass
    
    @abstractmethod
    async def send_message(
        self, 
        session_id: str, 
        content: str,
        **kwargs
    ):
        """发送消息"""
        pass
    
    async def handle_message(self, message: Message):
        """处理收到的消息(通用逻辑)"""
        # 1. 权限检查
        if not self.is_allowed(message.sender_id):
            return
        
        # 2. 转发给 Agent
        response = await self.agent.run([
            {"role": "user", "content": message.content}
        ])
        
        # 3. 发送回复
        await self.send_message(message.session_id, response)

设计亮点

  • ABC 定义统一接口
  • Message 数据类统一消息格式
  • 通用逻辑在基类实现,平台特定逻辑在子类

4. 添加新的 Channel

以 Telegram 为例:

# nanobot/channels/telegram.py
import telebot
from .base import BaseChannel, Message

class TelegramChannel(BaseChannel):
    def __init__(self, config, agent):
        self.bot = telebot.TeleBot(config.token)
        self.agent = agent
        self.allow_from = config.allow_from
    
    async def start(self):
        @self.bot.message_handler(func=lambda m: True)
        def handle(message):
            # 转换为统一消息格式
            msg = Message(
                content=message.text,
                sender_id=str(message.from_user.id),
                session_id=str(message.chat.id),
                channel="telegram"
            )
            asyncio.create_task(self.handle_message(msg))
        
        self.bot.infinity_polling()
    
    async def send_message(self, session_id: str, content: str):
        self.bot.send_message(int(session_id), content)
    
    def is_allowed(self, sender_id: str) -> bool:
        return sender_id in self.allow_from or "*" in self.allow_from

新增 Channel 只需

  1. 继承 BaseChannel
  2. 实现三个抽象方法
  3. 在配置中启用

5. Memory 系统实现

nanobot/memory/store.py

import json
from pathlib import Path
from typing import List, Dict
import subprocess

class MemoryStore:
    """基于文件的记忆存储"""
    
    def __init__(self, base_path: Path):
        self.memory_file = base_path / "memory.jsonl"
        self.context_file = base_path / "context.json"
    
    def add_memory(self, content: str, metadata: Dict = None):
        """添加记忆条目"""
        entry = {
            "content": content,
            "metadata": metadata or {},
            "timestamp": datetime.now().isoformat()
        }
        with open(self.memory_file, "a") as f:
            f.write(json.dumps(entry) + "\n")
    
    def search(self, query: str, limit: int = 5) -> List[Dict]:
        """搜索相关记忆(使用 grep)"""
        # 使用 grep 进行文本搜索
        result = subprocess.run(
            ["grep", "-i", query, str(self.memory_file)],
            capture_output=True,
            text=True
        )
        
        memories = []
        for line in result.stdout.strip().split("\n")[:limit]:
            if line:
                memories.append(json.loads(line))
        
        return memories
    
    def get_context(self) -> Dict:
        """获取当前会话上下文"""
        if self.context_file.exists():
            return json.loads(self.context_file.read_text())
        return {}
    
    def save_context(self, context: Dict):
        """保存会话上下文"""
        self.context_file.write_text(json.dumps(context, indent=2))

设计亮点

  • 零依赖(仅使用标准库)
  • JSONL 格式便于追加和检索
  • grep 实现简单有效的关键词搜索

配置系统集成

配置 Schema

nanobot/config/schema.py 使用 Pydantic:

from pydantic import BaseModel
from typing import Optional, List, Dict

class ProviderConfig(BaseModel):
    apiKey: Optional[str] = None
    apiBase: Optional[str] = None

class ProvidersConfig(BaseModel):
    openrouter: ProviderConfig = ProviderConfig()
    openai: ProviderConfig = ProviderConfig()
    anthropic: ProviderConfig = ProviderConfig()
    deepseek: ProviderConfig = ProviderConfig()
    # ... 其他 Provider

class TelegramConfig(BaseModel):
    enabled: bool = False
    token: Optional[str] = None
    allowFrom: List[str] = ["*"]

class ChannelsConfig(BaseModel):
    telegram: TelegramConfig = TelegramConfig()
    discord: DiscordConfig = DiscordConfig()
    # ... 其他 Channel

class AgentConfig(BaseModel):
    model: str = "gpt-4"
    provider: Optional[str] = None
    maxIterations: int = 10

class Config(BaseModel):
    providers: ProvidersConfig = ProvidersConfig()
    channels: ChannelsConfig = ChannelsConfig()
    agents: Dict[str, AgentConfig] = {"defaults": AgentConfig()}

优势

  • 自动验证配置格式
  • 默认值减少必填项
  • 类型安全

扩展示例:添加新工具

定义工具

from nanobot.tools import Tool, ToolResult

class WeatherTool(Tool):
    name = "get_weather"
    description = "获取指定城市的天气信息"
    parameters = {
        "type": "object",
        "properties": {
            "city": {
                "type": "string",
                "description": "城市名称"
            }
        },
        "required": ["city"]
    }
    
    async def execute(self, city: str) -> ToolResult:
        # 调用天气 API
        weather_data = await self.fetch_weather(city)
        return ToolResult(
            success=True,
            content=f"{city} 当前天气:{weather_data}"
        )

注册工具

# 在 nanobot/tools/__init__.py 中注册
TOOLS = [
    WebSearchTool(),
    MemoryTool(),
    ScheduleTool(),
    WeatherTool(),  # 新增
]

工具会自动出现在 LLM 的工具列表中,无需额外配置。

参考资料