Nanobot Agent 核心机制深度分析
概述
nanobot 是一个超轻量级个人 AI 助手框架,核心代码约 4,000 行。其 Agent 核心机制的设计目标是构建一个轻量、可扩展、异步的 AI 助手框架,支持:
- 多通道消息处理 - 支持 Telegram、Discord、WhatsApp 等多种聊天平台
- 工具调用能力 - 支持文件操作、Shell 命令、Web 搜索等功能
- 子代理并行执行 - 通过子代理机制支持后台任务并行处理
- 持久化记忆系统 - 支持长期记忆和日记式记忆
- 渐进式技能加载 - 支持按需加载技能,优化上下文效率
核心架构采用事件驱动 + 异步消息总线模式,通过 MessageBus 进行消息分发,AgentLoop 作为核心处理引擎持续运行。
Agent Loop 处理流程
核心类:AgentLoop
位置:nanobot/agent/loop.py:24
AgentLoop 是 Agent 核心处理引擎,负责:
- 从消息总线接收消息
- 构建上下文(历史、记忆、技能)
- 调用 LLM
- 执行工具调用
- 发送响应
处理流程图
核心代码分析
主循环 (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
)
关键设计决策:
系统消息处理 (
loop.py:135-136):子代理完成的任务通过系统消息返回,格式为chat_id="origin_channel:origin_chat_id",确保响应路由回正确位置工具上下文更新 (
loop.py:144-150):MessageTool和SpawnTool需要知道当前通道和聊天 ID,用于正确路由消息迭代式工具执行 (
loop.py:163-202):默认max_iterations=20,支持链式工具调用(如读取文件 → 分析 → 写入文件)会话持久化 (
loop.py:207-210):使用 JSONL 格式存储历史记录,便于调试和恢复
工具注册机制
AgentLoop._register_default_tools() (loop.py:66-87) 注册以下默认工具:
| 工具名称 | 文件位置 | 功能 |
|---|---|---|
read_file | tools/filesystem.py | 读取文件 |
write_file | tools/filesystem.py | 写入文件 |
edit_file | tools/filesystem.py | 编辑文件 |
list_dir | tools/filesystem.py | 列出目录 |
exec | tools/shell.py | 执行 Shell 命令 |
web_search | tools/web.py | Web 搜索 |
web_fetch | tools/web.py | 获取网页内容 |
message | tools/message.py | 发送消息到聊天通道 |
spawn | tools/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"""
设计亮点:
- 动态时间 - 每次构建时获取当前时间,避免过时信息
- 绝对路径 - 使用
expanduser().resolve()转换为绝对路径 - 直接响应指令 - 明确区分直接对话和通过
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 消耗。
策略:
- Always Skills - 标记为
always: true的技能,直接加载完整内容 - 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() |
| 生命周期 | 持久运行 | 任务完成后结束 |
| 最大迭代次数 | 20 | 15 |
子代理生命周期流程
子代理生成流程 (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-2 句话)
- 上下文保持 - 使用
origin['channel']:origin['chat_id']格式保持原始会话上下文 - 自动清理 - 使用
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
设计亮点:
- 顺序执行 - 同一轮内的工具调用顺序执行(而非并行),保证上下文一致性
- 错误隔离 - 单个工具失败不会中断整个循环
- 工具 ID 追踪 - 使用
tool_call.id关联工具调用和结果
记忆系统
核心类:MemoryStore
位置:nanobot/agent/memory.py:9
MemoryStore 提供两种类型的记忆存储:
- 长期记忆 -
memory/MEMORY.md:持久化的重要信息 - 日记式记忆 -
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 ""
设计决策:
- 分离存储 - 长期记忆和日记式记忆分离,便于管理
- 按日切分 - 日记式记忆按日期存储,便于历史查询
- 增量写入 - 日记式记忆使用追加模式,保留历史记录
- 上下文优先级 - 先长期记忆,后今日笔记,确保重要信息优先展示
技能加载器
核心类:SkillsLoader
位置:nanobot/agent/skills.py:13
SkillsLoader 负责加载和管理 Agent 技能,支持:
- 双来源 - 内置技能(
nanobot/skills/)和工作区技能(workspace/skills/) - 元数据解析 - 从 YAML frontmatter 解析技能元数据
- 依赖检查 - 检查 CLI 命令和环境变量依赖
- 渐进式加载 - 区分 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"}
关键代码位置索引
核心类
| 类名 | 文件路径 | 行号 |
|---|---|---|
AgentLoop | nanobot/agent/loop.py | 24 |
ContextBuilder | nanobot/agent/context.py | 12 |
SubagentManager | nanobot/agent/subagent.py | 20 |
MemoryStore | nanobot/agent/memory.py | 9 |
SkillsLoader | nanobot/agent/skills.py | 13 |
ToolRegistry | nanobot/agent/tools/registry.py | 8 |
LLMProvider | nanobot/providers/base.py | 30 |
Session | nanobot/session/manager.py | 14 |
SessionManager | nanobot/session/manager.py | 61 |
关键方法
| 方法名 | 类名 | 文件路径 | 行号 |
|---|---|---|---|
run() | AgentLoop | nanobot/agent/loop.py | 89 |
_process_message() | AgentLoop | nanobot/agent/loop.py | 123 |
_process_system_message() | AgentLoop | nanobot/agent/loop.py | 218 |
build_system_prompt() | ContextBuilder | nanobot/agent/context.py | 27 |
build_messages() | ContextBuilder | nanobot/agent/context.py | 115 |
spawn() | SubagentManager | nanobot/agent/subagent.py | 44 |
_run_subagent() | SubagentManager | nanobot/agent/subagent.py | 83 |
_announce_result() | SubagentManager | nanobot/agent/subagent.py | 168 |
execute() | ToolRegistry | nanobot/agent/tools/registry.py | 38 |
get_always_skills() | SkillsLoader | nanobot/agent/skills.py | 193 |
get_memory_context() | MemoryStore | nanobot/agent/memory.py | 90 |
工具类
| 类名 | 文件路径 |
|---|---|
ReadFileTool | nanobot/agent/tools/filesystem.py |
WriteFileTool | nanobot/agent/tools/filesystem.py |
EditFileTool | nanobot/agent/tools/filesystem.py |
ListDirTool | nanobot/agent/tools/filesystem.py |
ExecTool | nanobot/agent/tools/shell.py |
WebSearchTool | nanobot/agent/tools/web.py |
WebFetchTool | nanobot/agent/tools/web.py |
MessageTool | nanobot/agent/tools/message.py |
SpawnTool | nanobot/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 助手框架,具有以下特点:
- 架构清晰 - 采用事件驱动 + 异步消息总线模式,各组件职责明确
- 扩展性强 - 支持自定义工具、技能和 LLM 提供商
- 异步优先 - 全面使用 asyncio,支持并发任务处理
- 持久化完善 - 会话、记忆、技能均支持持久化存储
- 用户友好 - 通过子代理机制支持后台任务,提升用户体验
核心代码约 4,000 行,实现了完整的功能集,是一个非常值得学习和参考的开源项目。