Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • Nanobot Agent 核心机制深度分析

Nanobot Agent 核心机制深度分析

概述

nanobot 是一个超轻量级个人 AI 助手框架,核心代码约 4,000 行。其 Agent 核心机制的设计目标是构建一个轻量、可扩展、异步的 AI 助手框架,支持:

  1. 多通道消息处理 - 支持 Telegram、Discord、WhatsApp 等多种聊天平台
  2. 工具调用能力 - 支持文件操作、Shell 命令、Web 搜索等功能
  3. 子代理并行执行 - 通过子代理机制支持后台任务并行处理
  4. 持久化记忆系统 - 支持长期记忆和日记式记忆
  5. 渐进式技能加载 - 支持按需加载技能,优化上下文效率

核心架构采用事件驱动 + 异步消息总线模式,通过 MessageBus 进行消息分发,AgentLoop 作为核心处理引擎持续运行。


Agent Loop 处理流程

核心类:AgentLoop

位置:nanobot/agent/loop.py:24

AgentLoop 是 Agent 核心处理引擎,负责:

  1. 从消息总线接收消息
  2. 构建上下文(历史、记忆、技能)
  3. 调用 LLM
  4. 执行工具调用
  5. 发送响应

处理流程图

核心代码分析

主循环 (run() 方法)

# nanobot/agent/loop.py:89-121
async def run(self) -> None:
    """Run the agent loop, processing messages from the bus."""
    self._running = True
    logger.info("Agent loop started")

    while self._running:
        try:
            # Wait for next message
            msg = await asyncio.wait_for(
                self.bus.consume_inbound(),
                timeout=1.0
            )

            # Process it
            try:
                response = await self._process_message(msg)
                if response:
                    await self.bus.publish_outbound(response)
            except Exception as e:
                logger.error(f"Error processing message: {e}")
                # Send error response
                await self.bus.publish_outbound(OutboundMessage(
                    channel=msg.channel,
                    chat_id=msg.chat_id,
                    content=f"Sorry, I encountered an error: {str(e)}"
                ))
        except asyncio.TimeoutError:
            continue

设计决策:

  • 使用 asyncio.wait_for 设置 1 秒超时,支持优雅停止(_running 标志)
  • 捕获所有异常并返回用户友好的错误消息,避免循环崩溃

消息处理 (_process_message() 方法)

# nanobot/agent/loop.py:123-216
async def _process_message(self, msg: InboundMessage) -> OutboundMessage | None:
    # Handle system messages (subagent announces)
    if msg.channel == "system":
        return await self._process_system_message(msg)

    # Get or create session
    session = self.sessions.get_or_create(msg.session_key)

    # Update tool contexts (message_tool, spawn_tool)
    message_tool = self.tools.get("message")
    if isinstance(message_tool, MessageTool):
        message_tool.set_context(msg.channel, msg.chat_id)

    spawn_tool = self.tools.get("spawn")
    if isinstance(spawn_tool, SpawnTool):
        spawn_tool.set_context(msg.channel, msg.chat_id)

    # Build initial messages
    messages = self.context.build_messages(
        history=session.get_history(),
        current_message=msg.content,
        media=msg.media if msg.media else None,
    )

    # Agent loop
    iteration = 0
    final_content = None

    while iteration < self.max_iterations:
        iteration += 1

        # Call LLM
        response = await self.provider.chat(
            messages=messages,
            tools=self.tools.get_definitions(),
            model=self.model
        )

        # Handle tool calls
        if response.has_tool_calls:
            # Add assistant message with 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
            )

            # Execute tools
            for tool_call in response.tool_calls:
                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:
            # No tool calls, we're done
            final_content = response.content
            break

    if final_content is None:
        final_content = "I've completed processing but have no response to give."

    # Save to session
    session.add_message("user", msg.content)
    session.add_message("assistant", final_content)
    self.sessions.save(session)

    return OutboundMessage(
        channel=msg.channel,
        chat_id=msg.chat_id,
        content=final_content
    )

关键设计决策:

  1. 系统消息处理 (loop.py:135-136):子代理完成的任务通过系统消息返回,格式为 chat_id="origin_channel:origin_chat_id",确保响应路由回正确位置

  2. 工具上下文更新 (loop.py:144-150):MessageTool 和 SpawnTool 需要知道当前通道和聊天 ID,用于正确路由消息

  3. 迭代式工具执行 (loop.py:163-202):默认 max_iterations=20,支持链式工具调用(如读取文件 → 分析 → 写入文件)

  4. 会话持久化 (loop.py:207-210):使用 JSONL 格式存储历史记录,便于调试和恢复

工具注册机制

AgentLoop._register_default_tools() (loop.py:66-87) 注册以下默认工具:

工具名称文件位置功能
read_filetools/filesystem.py读取文件
write_filetools/filesystem.py写入文件
edit_filetools/filesystem.py编辑文件
list_dirtools/filesystem.py列出目录
exectools/shell.py执行 Shell 命令
web_searchtools/web.pyWeb 搜索
web_fetchtools/web.py获取网页内容
messagetools/message.py发送消息到聊天通道
spawntools/spawn.py生成子代理

上下文构建机制

核心类:ContextBuilder

位置:nanobot/agent/context.py:12

ContextBuilder 负责组装系统提示和消息列表,组件包括:

  • Bootstrap 文件(AGENTS.md, SOUL.md, USER.md 等)
  • 记忆系统(长期记忆 + 日记式记忆)
  • 技能系统(always skills + available skills)
  • 当前时间和工作区信息

上下文构建流程图

系统提示结构

# nanobot/agent/context.py:27-70
def build_system_prompt(self, skill_names: list[str] | None = None) -> str:
    parts = []

    # Core identity
    parts.append(self._get_identity())

    # Bootstrap files
    bootstrap = self._load_bootstrap_files()
    if bootstrap:
        parts.append(bootstrap)

    # Memory context
    memory = self.memory.get_memory_context()
    if memory:
        parts.append(f"# Memory\n\n{memory}")

    # Skills - progressive loading
    # 1. Always-loaded skills: include full content
    always_skills = self.skills.get_always_skills()
    if always_skills:
        always_content = self.skills.load_skills_for_context(always_skills)
        if always_content:
            parts.append(f"# Active Skills\n\n{always_content}")

    # 2. Available skills: only show summary
    skills_summary = self.skills.build_skills_summary()
    if skills_summary:
        parts.append(f"""# Skills

The following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool.
Skills with available="false" need dependencies installed first - you can try installing them with apt/brew.

{skills_summary}""")

    return "\n\n---\n\n".join(parts)

核心身份 (_get_identity())

# nanobot/agent/context.py:72-101
def _get_identity(self) -> str:
    """Get the core identity section."""
    from datetime import datetime
    now = datetime.now().strftime("%Y-%m-%d %H:%M (%A)")
    workspace_path = str(self.workspace.expanduser().resolve())

    return f"""# nanobot 🐈

You are nanobot, a helpful AI assistant. You have access to tools that allow you to:
- Read, write, and edit files
- Execute shell commands
- Search the web and fetch web pages
- Send messages to users on chat channels
- Spawn subagents for complex background tasks

## Current Time
{now}

## Workspace
Your workspace is at: {workspace_path}
- Memory files: {workspace_path}/memory/MEMORY.md
- Daily notes: {workspace_path}/memory/YYYY-MM-DD.md
- Custom skills: {workspace_path}/skills/{{skill-name}}/SKILL.md

IMPORTANT: When responding to direct questions or conversations, reply directly with your text response.
Only use the 'message' tool when you need to send a message to a specific chat channel (like WhatsApp).
For normal conversation, just respond with text - do not call the message tool.

Always be helpful, accurate, and concise. When using tools, explain what you're doing.
When remembering something, write to {workspace_path}/memory/MEMORY.md"""

设计亮点:

  1. 动态时间 - 每次构建时获取当前时间,避免过时信息
  2. 绝对路径 - 使用 expanduser().resolve() 转换为绝对路径
  3. 直接响应指令 - 明确区分直接对话和通过 message 工具发送消息的场景

Bootstrap 文件加载

# nanobot/agent/context.py:103-113
def _load_bootstrap_files(self) -> str:
    """Load all bootstrap files from workspace."""
    parts = []

    for filename in self.BOOTSTRAP_FILES:  # ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md", "IDENTITY.md"]
        file_path = self.workspace / filename
        if file_path.exists():
            content = file_path.read_text(encoding="utf-8")
            parts.append(f"## {filename}\n\n{content}")

    return "\n\n".join(parts) if parts else ""

渐进式技能加载策略

设计目标:避免将所有技能内容一次性加载到上下文,降低 token 消耗。

策略:

  1. Always Skills - 标记为 always: true 的技能,直接加载完整内容
  2. Available Skills - 仅显示摘要(名称、描述、路径、可用性),需要时通过 read_file 工具加载
# Always Skills (完整加载)
always_skills = self.skills.get_always_skills()
always_content = self.skills.load_skills_for_context(always_skills)
parts.append(f"# Active Skills\n\n{always_content}")

# Available Skills (仅摘要)
skills_summary = self.skills.build_skills_summary()
parts.append(f"""# Skills

The following skills extend your capabilities. To use a skill, read its SKILL.md file using the read_file tool.
Skills with available="false" need dependencies installed first - you can try installing them with apt/brew.

{skills_summary}""")

技能摘要格式

<skills>
  <skill available="true">
    <name>python</name>
    <description>Python development tools and debugging</description>
    <location>/workspace/skills/python/SKILL.md</location>
  </skill>
  <skill available="false">
    <name>docker</name>
    <description>Docker container management</description>
    <location>/workspace/skills/docker/SKILL.md</location>
    <requires>CLI: docker</requires>
  </skill>
</skills>

子代理架构

核心类:SubagentManager

位置:nanobot/agent/subagent.py:20

SubagentManager 负责管理后台子代理的创建和执行。子代理是轻量级的 Agent 实例,共享相同的 LLM 提供商,但具有隔离的上下文和专注的系统提示。

子代理 vs 主代理对比

特性主代理子代理
系统提示完整(包含身份、记忆、技能)专注(仅任务描述)
工具集所有默认工具有限工具集(无 message、无 spawn)
上下文会话历史 + 系统提示仅任务描述
通信方式直接响应用户通过消息总线 announce_result()
生命周期持久运行任务完成后结束
最大迭代次数2015

子代理生命周期流程

子代理生成流程 (spawn() 方法)

# nanobot/agent/subagent.py:44-81
async def spawn(
    self,
    task: str,
    label: str | None = None,
    origin_channel: str = "cli",
    origin_chat_id: str = "direct",
) -> str:
    """Spawn a subagent to execute a task in the background."""
    task_id = str(uuid.uuid4())[:8]
    display_label = label or task[:30] + ("..." if len(task) > 30 else "")

    origin = {
        "channel": origin_channel,
        "chat_id": origin_chat_id,
    }

    # Create background task
    bg_task = asyncio.create_task(
        self._run_subagent(task_id, task, display_label, origin)
    )
    self._running_tasks[task_id] = bg_task

    # Cleanup when done
    bg_task.add_done_callback(lambda _: self._running_tasks.pop(task_id, None))

    logger.info(f"Spawned subagent [{task_id}]: {display_label}")
    return f"Subagent [{display_label}] started (id: {task_id}). I'll notify you when it completes."

子代理执行流程 (_run_subagent() 方法)

# nanobot/agent/subagent.py:83-166
async def _run_subagent(
    self,
    task_id: str,
    task: str,
    label: str,
    origin: dict[str, str],
) -> None:
    """Execute the subagent task and announce the result."""
    logger.info(f"Subagent [{task_id}] starting task: {label}")

    try:
        # Build subagent tools (no message tool, no spawn tool)
        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())

        # Build messages with subagent-specific prompt
        system_prompt = self._build_subagent_prompt(task)
        messages: list[dict[str, Any]] = [
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": task},
        ]

        # Run agent loop (limited iterations)
        max_iterations = 15
        iteration = 0
        final_result: str | None = None

        while iteration < max_iterations:
            iteration += 1

            response = await self.provider.chat(
                messages=messages,
                tools=tools.get_definitions(),
                model=self.model,
            )

            if response.has_tool_calls:
                # Add assistant message with 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.append({
                    "role": "assistant",
                    "content": response.content or "",
                    "tool_calls": tool_call_dicts,
                })

                # Execute tools
                for tool_call in response.tool_calls:
                    logger.debug(f"Subagent [{task_id}] executing: {tool_call.name}")
                    result = await tools.execute(tool_call.name, tool_call.arguments)
                    messages.append({
                        "role": "tool",
                        "tool_call_id": tool_call.id,
                        "name": tool_call.name,
                        "content": result,
                    })
            else:
                final_result = response.content
                break

        if final_result is None:
            final_result = "Task completed but no final response was generated."

        logger.info(f"Subagent [{task_id}] completed successfully")
        await self._announce_result(task_id, label, task, final_result, origin, "ok")

    except Exception as e:
        error_msg = f"Error: {str(e)}"
        logger.error(f"Subagent [{task_id}] failed: {e}")
        await self._announce_result(task_id, label, task, error_msg, origin, "error")

结果公告机制 (_announce_result() 方法)

# nanobot/agent/subagent.py:168-198
async def _announce_result(
    self,
    task_id: str,
    label: str,
    task: str,
    result: str,
    origin: dict[str, str],
    status: str,
) -> None:
    """Announce the subagent result to the main agent via the message bus."""
    status_text = "completed successfully" if status == "ok" else "failed"

    announce_content = f"""[Subagent '{label}' {status_text}]

Task: {task}

Result:
{result}

Summarize this naturally for the user. Keep it brief (1-2 sentences). Do not mention technical details like "subagent" or task IDs."""

    # Inject as system message to trigger main agent
    msg = InboundMessage(
        channel="system",
        sender_id="subagent",
        chat_id=f"{origin['channel']}:{origin['chat_id']}",
        content=announce_content,
    )

    await self.bus.publish_inbound(msg)
    logger.debug(f"Subagent [{task_id}] announced result to {origin['channel']}:{origin['chat_id']}")

设计亮点:

  1. 智能摘要 - 要求主代理将技术细节转化为用户友好的摘要(1-2 句话)
  2. 上下文保持 - 使用 origin['channel']:origin['chat_id'] 格式保持原始会话上下文
  3. 自动清理 - 使用 add_done_callback 自动清理完成的任务

子代理系统提示

# nanobot/agent/subagent.py:200-229
def _build_subagent_prompt(self, task: str) -> str:
    """Build a focused system prompt for the subagent."""
    return f"""# Subagent

You are a subagent spawned by the main agent to complete a specific task.

## Your Task
{task}

## Rules
1. Stay focused - complete only the assigned task, nothing else
2. Your final response will be reported back to the main agent
3. Do not initiate conversations or take on side tasks
4. Be concise but informative in your findings

## What You Can Do
- Read and write files in the workspace
- Execute shell commands
- Search the web and fetch web pages
- Complete the task thoroughly

## What You Cannot Do
- Send messages directly to users (no message tool available)
- Spawn other subagents
- Access the main agent's conversation history

## Workspace
Your workspace is at: {self.workspace}

When you have completed the task, provide a clear summary of your findings or actions."""

工具执行流程

工具注册表:ToolRegistry

位置:nanobot/agent/tools/registry.py:8

ToolRegistry 是工具管理的核心组件,支持动态注册和执行。

工具基类接口

# nanobot/agent/tools/base.py (推断)
class Tool(ABC):
    """Base class for all tools."""

    @property
    @abstractmethod
    def name(self) -> str:
        """Tool name."""
        pass

    @property
    @abstractmethod
    def description(self) -> str:
        """Tool description for LLM."""
        pass

    @property
    @abstractmethod
    def parameters(self) -> dict[str, Any]:
        """Tool parameters schema (JSON Schema)."""
        pass

    @abstractmethod
    async def execute(self, **kwargs) -> str:
        """Execute the tool and return result."""
        pass

    def to_schema(self) -> dict[str, Any]:
        """Convert to OpenAI function calling format."""
        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description,
                "parameters": self.parameters,
            }
        }

工具执行流程图

工具注册

# nanobot/agent/loop.py:66-87
def _register_default_tools(self) -> None:
    """Register the default set of tools."""
    # File tools
    self.tools.register(ReadFileTool())
    self.tools.register(WriteFileTool())
    self.tools.register(EditFileTool())
    self.tools.register(ListDirTool())

    # Shell tool
    self.tools.register(ExecTool(working_dir=str(self.workspace)))

    # Web tools
    self.tools.register(WebSearchTool(api_key=self.brave_api_key))
    self.tools.register(WebFetchTool())

    # Message tool
    message_tool = MessageTool(send_callback=self.bus.publish_outbound)
    self.tools.register(message_tool)

    # Spawn tool (for subagents)
    spawn_tool = SpawnTool(manager=self.subagents)
    self.tools.register(spawn_tool)

工具执行 (execute() 方法)

# nanobot/agent/tools/registry.py:38-59
async def execute(self, name: str, params: dict[str, Any]) -> str:
    """
    Execute a tool by name with given parameters.

    Args:
        name: Tool name.
        params: Tool parameters.

    Returns:
        Tool execution result as string.

    Raises:
        KeyError: If tool not found.
    """
    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)}"

迭代式工具执行

# nanobot/agent/loop.py:163-202
while iteration < self.max_iterations:
    iteration += 1

    # Call LLM
    response = await self.provider.chat(
        messages=messages,
        tools=self.tools.get_definitions(),
        model=self.model
    )

    # Handle tool calls
    if response.has_tool_calls:
        # Add assistant message with 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
        )

        # Execute tools
        for tool_call in response.tool_calls:
            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:
        # No tool calls, we're done
        final_content = response.content
        break

设计亮点:

  1. 顺序执行 - 同一轮内的工具调用顺序执行(而非并行),保证上下文一致性
  2. 错误隔离 - 单个工具失败不会中断整个循环
  3. 工具 ID 追踪 - 使用 tool_call.id 关联工具调用和结果

记忆系统

核心类:MemoryStore

位置:nanobot/agent/memory.py:9

MemoryStore 提供两种类型的记忆存储:

  1. 长期记忆 - memory/MEMORY.md:持久化的重要信息
  2. 日记式记忆 - memory/YYYY-MM-DD.md:按日期记录的日常笔记

记忆存储结构

记忆系统代码分析

初始化

# nanobot/agent/memory.py:16-19
def __init__(self, workspace: Path):
    self.workspace = workspace
    self.memory_dir = ensure_dir(workspace / "memory")
    self.memory_file = self.memory_dir / "MEMORY.md"

长期记忆操作

# nanobot/agent/memory.py:46-54
def read_long_term(self) -> str:
    """Read long-term memory (MEMORY.md)."""
    if self.memory_file.exists():
        return self.memory_file.read_text(encoding="utf-8")
    return ""

def write_long_term(self, content: str) -> None:
    """Write to long-term memory (MEMORY.md)."""
    self.memory_file.write_text(content, encoding="utf-8")

日记式记忆操作

# nanobot/agent/memory.py:21-44
def get_today_file(self) -> Path:
    """Get path to today's memory file."""
    return self.memory_dir / f"{today_date()}.md"

def read_today(self) -> str:
    """Read today's memory notes."""
    today_file = self.get_today_file()
    if today_file.exists():
        return today_file.read_text(encoding="utf-8")
    return ""

def append_today(self, content: str) -> None:
    """Append content to today's memory notes."""
    today_file = self.get_today_file()

    if today_file.exists():
        existing = today_file.read_text(encoding="utf-8")
        content = existing + "\n" + content
    else:
        # Add header for new day
        header = f"# {today_date()}\n\n"
        content = header + content

    today_file.write_text(content, encoding="utf-8")

获取历史记忆

# nanobot/agent/memory.py:56-80
def get_recent_memories(self, days: int = 7) -> str:
    """
    Get memories from the last N days.

    Args:
        days: Number of days to look back.

    Returns:
        Combined memory content.
    """
    from datetime import timedelta

    memories = []
    today = datetime.now().date()

    for i in range(days):
        date = today - timedelta(days=i)
        date_str = date.strftime("%Y-%m-%d")
        file_path = self.memory_dir / f"{date_str}.md"

        if file_path.exists():
            content = file_path.read_text(encoding="utf-8")
            memories.append(content)

    return "\n\n---\n\n".join(memories)

构建记忆上下文

# nanobot/agent/memory.py:90-109
def get_memory_context(self) -> str:
    """
    Get memory context for the agent.

    Returns:
        Formatted memory context including long-term and recent memories.
    """
    parts = []

    # Long-term memory
    long_term = self.read_long_term()
    if long_term:
        parts.append("## Long-term Memory\n" + long_term)

    # Today's notes
    today = self.read_today()
    if today:
        parts.append("## Today's Notes\n" + today)

    return "\n\n".join(parts) if parts else ""

设计决策:

  1. 分离存储 - 长期记忆和日记式记忆分离,便于管理
  2. 按日切分 - 日记式记忆按日期存储,便于历史查询
  3. 增量写入 - 日记式记忆使用追加模式,保留历史记录
  4. 上下文优先级 - 先长期记忆,后今日笔记,确保重要信息优先展示

技能加载器

核心类:SkillsLoader

位置:nanobot/agent/skills.py:13

SkillsLoader 负责加载和管理 Agent 技能,支持:

  1. 双来源 - 内置技能(nanobot/skills/)和工作区技能(workspace/skills/)
  2. 元数据解析 - 从 YAML frontmatter 解析技能元数据
  3. 依赖检查 - 检查 CLI 命令和环境变量依赖
  4. 渐进式加载 - 区分 always skills 和 available skills

技能目录结构

workspace/
├── skills/
│   ├── python/
│   │   └── SKILL.md
│   ├── docker/
│   │   └── SKILL.md
│   └── git/
│       └── SKILL.md

技能元数据格式

---
description: Python development tools and debugging
requires: |-
  {
    "nanobot": {
      "always": false,
      "requires": {
        "bins": ["python3", "pip"],
        "env": []
      }
    }
  }
metadata: |-
  {
    "nanobot": {
      "always": false,
      "requires": {
        "bins": ["python3", "pip"],
        "env": []
      }
    }
  }
---

# Python Development Skill

Instructions for Python development...

技能列表

# nanobot/agent/skills.py:26-57
def list_skills(self, filter_unavailable: bool = True) -> list[dict[str, str]]:
    """
    List all available skills.

    Args:
        filter_unavailable: If True, filter out skills with unmet requirements.

    Returns:
        List of skill info dicts with 'name', 'path', 'source'.
    """
    skills = []

    # Workspace skills (highest priority)
    if self.workspace_skills.exists():
        for skill_dir in self.workspace_skills.iterdir():
            if skill_dir.is_dir():
                skill_file = skill_dir / "SKILL.md"
                if skill_file.exists():
                    skills.append({"name": skill_dir.name, "path": str(skill_file), "source": "workspace"})

    # Built-in skills
    if self.builtin_skills and self.builtin_skills.exists():
        for skill_dir in self.builtin_skills.iterdir():
            if skill_dir.is_dir():
                skill_file = skill_dir / "SKILL.md"
                if skill_file.exists() and not any(s["name"] == skill_dir.name for s in skills):
                    skills.append({"name": skill_dir.name, "path": str(skill_file), "source": "builtin"})

    # Filter by requirements
    if filter_unavailable:
        return [s for s in skills if self._check_requirements(self._get_skill_meta(s["name"]))]
    return skills

Always Skills 获取

# nanobot/agent/skills.py:193-201
def get_always_skills(self) -> list[str]:
    """Get skills marked as always=true that meet requirements."""
    result = []
    for s in self.list_skills(filter_unavailable=True):
        meta = self.get_skill_metadata(s["name"]) or {}
        skill_meta = self._parse_nanobot_metadata(meta.get("metadata", ""))
        if skill_meta.get("always") or meta.get("always"):
            result.append(s["name"])
    return result

LLM 提供商接口

核心类:LLMProvider

位置:nanobot/providers/base.py:30

LLMProvider 是抽象基类,定义了 LLM 提供商的统一接口。

数据结构

# nanobot/providers/base.py:8-27
@dataclass
class ToolCallRequest:
    """A tool call request from the LLM."""
    id: str
    name: str
    arguments: dict[str, Any]

@dataclass
class LLMResponse:
    """Response from an LLM provider."""
    content: str | None
    tool_calls: list[ToolCallRequest] = field(default_factory=list)
    finish_reason: str = "stop"
    usage: dict[str, int] = field(default_factory=dict)

    @property
    def has_tool_calls(self) -> bool:
        """Check if response contains tool calls."""
        return len(self.tool_calls) > 0

抽象接口

# nanobot/providers/base.py:42-69
class LLMProvider(ABC):
    """Abstract base class for LLM providers."""

    def __init__(self, api_key: str | None = None, api_base: str | None = None):
        self.api_key = api_key
        self.api_base = api_base

    @abstractmethod
    async def chat(
        self,
        messages: list[dict[str, Any]],
        tools: list[dict[str, Any]] | None = None,
        model: str | None = None,
        max_tokens: int = 4096,
        temperature: float = 0.7,
    ) -> LLMResponse:
        """Send a chat completion request."""
        pass

    @abstractmethod
    def get_default_model(self) -> str:
        """Get the default model for this provider."""
        pass

会话管理

核心类:SessionManager

位置:nanobot/session/manager.py:61

SessionManager 负责管理对话会话,使用 JSONL 格式存储历史记录。

Session 数据结构

# nanobot/session/manager.py:14-59
@dataclass
class Session:
    """A conversation session."""
    key: str  # channel:chat_id
    messages: list[dict[str, Any]] = field(default_factory=list)
    created_at: datetime = field(default_factory=datetime.now)
    updated_at: datetime = field(default_factory=datetime.now)
    metadata: dict[str, Any] = field(default_factory=dict)

    def add_message(self, role: str, content: str, **kwargs: Any) -> None:
        """Add a message to the session."""
        msg = {
            "role": role,
            "content": content,
            "timestamp": datetime.now().isoformat(),
            **kwargs
        }
        self.messages.append(msg)
        self.updated_at = datetime.now()

    def get_history(self, max_messages: int = 50) -> list[dict[str, Any]]:
        """Get message history for LLM context."""
        recent = self.messages[-max_messages:] if len(self.messages) > max_messages else self.messages
        return [{"role": m["role"], "content": m["content"]} for m in recent]

JSONL 存储格式

{"_type": "metadata", "created_at": "2026-02-03T10:00:00", "updated_at": "2026-02-03T12:00:00", "metadata": {}}
{"role": "user", "content": "Hello", "timestamp": "2026-02-03T10:00:00"}
{"role": "assistant", "content": "Hi there!", "timestamp": "2026-02-03T10:00:01"}

关键代码位置索引

核心类

类名文件路径行号
AgentLoopnanobot/agent/loop.py24
ContextBuildernanobot/agent/context.py12
SubagentManagernanobot/agent/subagent.py20
MemoryStorenanobot/agent/memory.py9
SkillsLoadernanobot/agent/skills.py13
ToolRegistrynanobot/agent/tools/registry.py8
LLMProvidernanobot/providers/base.py30
Sessionnanobot/session/manager.py14
SessionManagernanobot/session/manager.py61

关键方法

方法名类名文件路径行号
run()AgentLoopnanobot/agent/loop.py89
_process_message()AgentLoopnanobot/agent/loop.py123
_process_system_message()AgentLoopnanobot/agent/loop.py218
build_system_prompt()ContextBuildernanobot/agent/context.py27
build_messages()ContextBuildernanobot/agent/context.py115
spawn()SubagentManagernanobot/agent/subagent.py44
_run_subagent()SubagentManagernanobot/agent/subagent.py83
_announce_result()SubagentManagernanobot/agent/subagent.py168
execute()ToolRegistrynanobot/agent/tools/registry.py38
get_always_skills()SkillsLoadernanobot/agent/skills.py193
get_memory_context()MemoryStorenanobot/agent/memory.py90

工具类

类名文件路径
ReadFileToolnanobot/agent/tools/filesystem.py
WriteFileToolnanobot/agent/tools/filesystem.py
EditFileToolnanobot/agent/tools/filesystem.py
ListDirToolnanobot/agent/tools/filesystem.py
ExecToolnanobot/agent/tools/shell.py
WebSearchToolnanobot/agent/tools/web.py
WebFetchToolnanobot/agent/tools/web.py
MessageToolnanobot/agent/tools/message.py
SpawnToolnanobot/agent/tools/spawn.py

深挖价值点

以下技术点值得进一步深入研究:

1. 迭代式工具执行的上下文累积机制

位置: nanobot/agent/loop.py:163-202

价值: 每次工具调用的结果都通过 add_tool_result() 添加到 messages 列表中,形成完整的对话历史。这种设计使得 LLM 可以基于之前的工具调用结果做出后续决策,实现复杂的链式推理。

深挖方向:

  • 如何优化上下文大小,避免 token 溢出?
  • 是否可以引入上下文压缩或摘要机制?

2. 子代理的消息路由机制

位置: nanobot/agent/subagent.py:168-198, nanobot/agent/loop.py:218-308

价值: 子代理通过 chat_id="origin_channel:origin_chat_id" 格式将结果路由回原始会话,主代理通过 _process_system_message() 处理这些结果。这种设计实现了子代理和主代理之间的优雅解耦。

深挖方向:

  • 是否可以支持多级子代理嵌套?
  • 如何处理子代理超时和失败重试?

3. 渐进式技能加载策略

位置: nanobot/agent/context.py:52-68, nanobot/agent/skills.py:193-201

价值: 通过区分 always skills 和 available skills,避免将所有技能内容一次性加载到上下文,降低 token 消耗。这种设计在技能数量较多时尤为重要。

深挖方向:

  • 如何实现智能技能推荐(基于任务上下文)?
  • 是否可以支持技能依赖图(某些技能依赖其他技能)?

4. 异步任务清理机制

位置: nanobot/agent/subagent.py:77-78

价值: 使用 bg_task.add_done_callback(lambda _: self._running_tasks.pop(task_id, None)) 自动清理完成的任务,避免内存泄漏。这是一种优雅的资源管理模式。

深挖方向:

  • 如何实现任务取消机制?
  • 是否可以支持任务优先级和资源限制?

5. 系统消息处理的双通道设计

位置: nanobot/agent/loop.py:135-136, nanobot/agent/loop.py:218-308

价值: 系统消息(如子代理结果)和用户消息使用不同的处理流程,但最终都返回到原始会话。这种设计既保持了灵活性,又确保了用户体验的一致性。

深挖方向:

  • 是否可以支持更多类型的系统消息(如通知、警告)?
  • 如何处理系统消息队列积压?

6. 记忆系统的双模式设计

位置: nanobot/agent/memory.py:9-109

价值: 长期记忆和日记式记忆的分离设计,支持不同类型的信息存储需求。长期记忆用于持久化重要信息,日记式记忆用于记录日常笔记。

深挖方向:

  • 是否可以支持记忆检索和搜索功能?
  • 如何实现记忆的重要性评分和自动归档?

7. 会话历史的 JSONL 存储格式

位置: nanobot/session/manager.py:136-154

价值: JSONL 格式(每行一个 JSON 对象)便于增量写入和流式读取,同时支持元数据和消息的混合存储。这种设计在保持灵活性的同时,也便于调试和恢复。

深挖方向:

  • 如何实现会话压缩和清理策略?
  • 是否可以支持会话导出和导入?

总结

nanobot 的 Agent 核心机制是一个精心设计的轻量级 AI 助手框架,具有以下特点:

  1. 架构清晰 - 采用事件驱动 + 异步消息总线模式,各组件职责明确
  2. 扩展性强 - 支持自定义工具、技能和 LLM 提供商
  3. 异步优先 - 全面使用 asyncio,支持并发任务处理
  4. 持久化完善 - 会话、记忆、技能均支持持久化存储
  5. 用户友好 - 通过子代理机制支持后台任务,提升用户体验

核心代码约 4,000 行,实现了完整的功能集,是一个非常值得学习和参考的开源项目。