nanobot 工具上下文注入机制深度分析
概述:设计目标
nanobot 的工具上下文注入机制是一个精心设计的动态上下文管理系统,其核心设计目标是:
- 工具复用性:同一工具实例可在多个会话间共享,降低资源消耗
- 会话隔离性:确保不同渠道/会话的工具行为互不干扰
- 上下文传递:支持子代理向原始会话路由结果的能力
- 简化配置:通过默认值机制减少重复参数传递
- 运行时动态性:在消息处理循环中动态更新工具上下文
该机制通过 set_context() 方法实现,让工具在执行时能访问当前的会话信息(channel、chat_id),而无需在每次工具调用时显式传递这些参数。
一、上下文注入的必要性分析
1.1 多渠道并发场景
nanobot 需要同时处理来自多个渠道(Telegram、Discord、Slack、CLI 等)的消息。这些消息具有不同的 channel 和 chat_id,但共享同一个工具实例池。
问题:如果没有上下文注入机制,每次工具调用都需要显式传递 channel 和 chat_id,这会导致:
- LLM 需要在每个工具调用中生成这些参数
- 参数传递冗余且易出错
- 无法在子代理完成后将结果路由回原始会话
解决:通过上下文注入,工具在执行时自动使用当前会话的 channel/chat_id。
1.2 工具实例复用
nanobot 在 AgentLoop.__init__ 中注册所有工具,每个工具只有一个实例(参见 loop.py:66-87):
def _register_default_tools(self) -> None:
# 工具只注册一次
message_tool = MessageTool(send_callback=self.bus.publish_outbound)
self.tools.register(message_tool)
spawn_tool = SpawnTool(manager=self.subagents)
self.tools.register(spawn_tool)
必要性:
- 降低内存占用(每个会话不创建新工具实例)
- 统一工具配置(如 send_callback、brave_api_key)
- 便于工具生命周期管理
1.3 子代理结果路由
当使用 SpawnTool 创建子代理后,子代理需要在后台执行任务,并将结果通知回原始会话。这需要保存原始会话的上下文信息:
问题:子代理在后台独立运行,无法访问原始请求的会话信息。
解决:通过 set_context() 将原始会话信息注入到 SpawnTool,子代理通过 origin_channel 和 origin_chat_id 路由结果。
1.4 没有上下文注入的问题
如果没有上下文注入机制,会导致以下问题:
| 问题 | 影响 |
|---|---|
| 消息路由错误 | MessageTool 无法确定消息发送目标 |
| 子代理结果丢失 | 子代理完成后无法通知原始用户 |
| LLM 参数冗余 | 每次工具调用都需要传递 channel/chat_id |
| 会话混淆 | 多渠道并发时消息可能发送到错误渠道 |
| 工具不可用 | 子代理无法使用 MessageTool(因为它没有上下文) |
二、MessageTool 上下文注入详解
2.1 类结构与字段
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 # 动态上下文
关键字段:
_send_callback:消息发送回调函数(静态,初始化时设置)_default_channel:默认消息渠道(动态,由 set_context 更新)_default_chat_id:默认聊天ID(动态,由 set_context 更新)
2.2 set_context() 实现
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
设计要点:
- 简单的属性赋值,无复杂逻辑
- 每次处理新消息时调用,覆盖之前的上下文
- 线程安全依赖 AgentLoop 的单线程模型
2.3 execute() 中的上下文使用
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)}"
上下文优先级:
- 显式参数(LLM 工具调用时指定的)
- 上下文注入值(
_default_channel、_default_chat_id) - 空值(返回错误)
2.4 Agent Loop 中的上下文设置
普通消息处理(loop.py:143-150)
# 获取或创建会话
session = self.sessions.get_or_create(msg.session_key)
# 更新工具上下文
message_tool = self.tools.get("message")
if isinstance(message_tool, MessageTool):
message_tool.set_context(msg.channel, msg.chat_id)
时机:在 _process_message() 开始时,调用 LLM 之前。
系统消息处理(loop.py:241-248)
# 解析原始上下文
if ":" in msg.chat_id:
parts = msg.chat_id.split(":", 1)
origin_channel = parts[0]
origin_chat_id = parts[1]
# 更新工具上下文(使用原始会话)
message_tool = self.tools.get("message")
if isinstance(message_tool, MessageTool):
message_tool.set_context(origin_channel, origin_chat_id)
特殊处理:系统消息来自子代理,其 chat_id 格式为 "channel:chat_id",需要解析后恢复原始上下文。
2.5 MessageTool 时序图
2.6 默认值机制作用
初始化默认值:
MessageTool(default_channel="", default_chat_id="")
作用:
- 类型提示:明确字段存在,避免 AttributeError
- 防御性编程:工具在未注入上下文时不会崩溃,返回清晰的错误信息
- CLI 场景:CLI 模式下不需要动态注入,可在初始化时设置固定值
未注入上下文时的行为:
# 如果没有调用 set_context()
channel = channel or self._default_channel # -> ""
if not channel or not chat_id:
return "Error: No target channel/chat specified"
三、SpawnTool 上下文注入详解
3.1 类结构与字段
class SpawnTool(Tool):
def __init__(self, manager: "SubagentManager"):
self._manager = manager
self._origin_channel = "cli" # 默认值
self._origin_chat_id = "direct" # 默认值
关键字段:
_manager:子代理管理器(静态,初始化时设置)_origin_channel:原始会话渠道(动态,由 set_context 更新)_origin_chat_id:原始会话ID(动态,由 set_context 更新)
3.2 set_context() 实现
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
设计要点:
- 与 MessageTool 相同的简单实现
- 保存原始会话信息供子代理路由结果使用
- 默认值
"cli"和"direct"适配命令行场景
3.3 execute() 中的上下文传递
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, # 传递上下文
)
上下文传递链路:
SpawnTool.set_context(channel, chat_id)
↓
SpawnTool._origin_channel = channel
SpawnTool._origin_chat_id = chat_id
↓
SpawnTool.execute(task, label)
↓
SubagentManager.spawn(..., origin_channel, origin_chat_id)
↓
_subagent._run_subagent(..., origin={channel, chat_id})
↓
_announce_result(..., origin={channel, chat_id})
↓
InboundMessage(chat_id=f"{channel}:{chat_id}")
3.4 子代理结果路由
SubagentManager.spawn() (subagent.py:44-81)
async def spawn(
self,
task: str,
label: str | None = None,
origin_channel: str = "cli",
origin_chat_id: str = "direct",
) -> str:
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,
}
# 创建后台任务
bg_task = asyncio.create_task(
self._run_subagent(task_id, task, display_label, origin)
)
return f"Subagent [{display_label}] started (id: {task_id}). I'll notify you when it completes."
_announce_result() (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."""
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."""
# 通过 chat_id 传递原始会话信息
msg = InboundMessage(
channel="system", # 系统消息
sender_id="subagent",
chat_id=f"{origin['channel']}:{origin['chat_id']}", # 编码原始信息
content=announce_content,
)
await self.bus.publish_inbound(msg)
关键设计:使用 chat_id 字段编码原始会话信息(格式:"channel:chat_id"),在系统消息处理时解析。
3.5 Agent Loop 系统消息处理
async def _process_system_message(self, msg: InboundMessage) -> OutboundMessage | None:
# 解析原始会话信息
if ":" in msg.chat_id:
parts = msg.chat_id.split(":", 1)
origin_channel = parts[0]
origin_chat_id = parts[1]
else:
origin_channel = "cli"
origin_chat_id = msg.chat_id
# 使用原始会话
session_key = f"{origin_channel}:{origin_chat_id}"
session = self.sessions.get_or_create(session_key)
# 更新工具上下文(恢复原始会话)
message_tool = self.tools.get("message")
if isinstance(message_tool, MessageTool):
message_tool.set_context(origin_channel, origin_chat_id)
spawn_tool = self.tools.get("spawn")
if isinstance(spawn_tool, SpawnTool):
spawn_tool.set_context(origin_channel, origin_chat_id)
# ... 处理消息并返回响应
return OutboundMessage(
channel=origin_channel,
chat_id=origin_chat_id,
content=final_content
)
3.6 SpawnTool 时序图
3.7 origin_channel 和 origin_chat_id 的完整传递链路
四、上下文的生命周期分析
4.1 上下文状态图
4.2 上下文设置和使用的时机
| 阶段 | 位置 | 操作 | 上下文状态 |
|---|---|---|---|
| 初始化 | AgentLoop.__init__ | 创建工具实例 | 未初始化(空字符串) |
| 消息到达 | AgentLoop.run() | 从 MessageBus 获取消息 | 未初始化 |
| 处理开始 | _process_message() | 调用 set_context() | 已注入(当前会话) |
| 工具调用 | ToolRegistry.execute() | 工具使用上下文 | 已注入(读取) |
| 子代理通知 | _process_system_message() | 恢复原始上下文 | 已注入(原始会话) |
| 下一条消息 | 重新开始 | 覆盖上下文 | 已注入(新会话) |
4.3 多渠道并发时的上下文管理
模型:AgentLoop 是单线程的,消息是串行处理的。
async def run(self) -> None:
while self._running:
# 阻塞等待下一条消息
msg = await asyncio.wait_for(
self.bus.consume_inbound(),
timeout=1.0
)
# 处理消息(串行)
response = await self._process_message(msg)
并发场景分析:
消息A到达(channel=telegram, chat_id=123)
set_context("telegram", "123")- 工具上下文:telegram/123
- 消息A处理完成
消息B到达(channel=discord, chat_id=456)
set_context("discord", "456")# 覆盖之前的上下文- 工具上下文:discord/456
- 消息B处理完成
子代理A的结果到达(系统消息,chat_id="telegram:123")
- 解析原始上下文:telegram/123
set_context("telegram", "123")# 恢复原始会话- 工具上下文:telegram/123
- 系统消息处理完成
关键特性:
- 上下文在每次消息处理时重新设置,无需清理
- 不需要锁或同步机制(单线程模型)
- 系统消息携带原始会话信息,确保正确恢复上下文
4.4 上下文冲突的可能性和解决方案
可能的冲突场景
| 场景 | 问题 | 解决方案 |
|---|---|---|
| 多个消息同时到达 | 不可能(单线程,消息队列) | 天然避免 |
| 子代理递归调用 | 可能导致循环上下文 | 子代理无 spawn 工具 |
| 系统消息格式错误 | 无法解析原始上下文 | 默认回退到 "cli" |
子代理递归防护
子代理的工具注册(subagent.py:94-101):
# 子代理没有 MessageTool 和 SpawnTool
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
设计意图:
- 防止子代理直接发送消息(所有结果必须通过主代理)
- 防止子代理递归创建子代理
- 确保结果路由的一致性
系统消息容错(loop.py:227-235)
# 解析原始会话信息
if ":" in msg.chat_id:
parts = msg.chat_id.split(":", 1)
origin_channel = parts[0]
origin_chat_id = parts[1]
else:
# 回退到默认值
origin_channel = "cli"
origin_chat_id = msg.chat_id
容错策略:
- 格式错误时回退到默认值
- 不会导致系统崩溃
- 错误消息会被记录(logger)
五、上下文注入的替代方案对比
5.1 方案对比表
| 方案 | 描述 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 当前方案:运行时注入 | 每次处理消息时调用 set_context() | 简单、灵活、低内存 | 需要显式调用 | nanobot 当前方案 |
| 工具参数传递 | 在工具参数中显式传递 channel/chat_id | 明确、无需额外方法 | LLM 参数冗余、易忘 | 静态配置场景 |
| 每会话独立工具 | 每个会话创建独立的工具实例 | 完全隔离、线程安全 | 内存占用高、生命周期复杂 | 多线程环境 |
| 上下文对象传递 | 创建 Context 对象,工具通过方法参数获取 | 类型安全、可扩展 | 复杂度高、依赖注入 | 大型框架 |
| 全局上下文(TLS) | 使用线程本地存储 | 自动、透明 | 依赖异步模型、难以测试 | 严格单线程 |
| 消息携带上下文 | 在消息对象中携带会话信息 | 自然、符合消息驱动 | 工具需要访问消息对象 | 纯事件驱动 |
5.2 详细分析
方案 1:工具参数传递
实现示例:
class MessageTool(Tool):
async def execute(
self,
content: str,
channel: str, # 必填参数
chat_id: str, # 必填参数
) -> str:
msg = OutboundMessage(channel=channel, chat_id=chat_id, content=content)
await self._send_callback(msg)
return f"Message sent to {channel}:{chat_id}"
优点:
- 工具接口明确,无需额外方法
- 不依赖运行时状态
缺点:
# LLM 需要在每次调用时生成这些参数
tool_calls = [
{
"name": "message",
"arguments": {
"content": "Hello",
"channel": "telegram", # 冗余
"chat_id": "123" # 冗余
}
}
]
对比结论:当前方案更优,减少 LLM 参数生成负担。
方案 2:每会话独立工具
实现示例:
class AgentLoop:
def __init__(self, ...):
self._session_tools = {} # {session_key: ToolRegistry}
async def _process_message(self, msg: InboundMessage):
session_key = msg.session_key
# 为每个会话创建独立工具
if session_key not in self._session_tools:
tools = ToolRegistry()
tools.register(MessageTool(..., default_channel=msg.channel, default_chat_id=msg.chat_id))
tools.register(SpawnTool(...))
self._session_tools[session_key] = tools
tools = self._session_tools[session_key]
# 使用 tools 执行...
优点:
- 完全隔离,无需手动管理上下文
- 天然线程安全
缺点:
- 内存占用高(每会话独立工具实例)
- 工具生命周期复杂(需要清理闲置会话)
- 静态配置(如 send_callback)无法共享
对比结论:当前方案更优,nanobot 是单线程模型,共享工具实例更高效。
方案 3:上下文对象传递
实现示例:
@dataclass
class ToolContext:
channel: str
chat_id: str
session_key: str
class Tool(ABC):
@abstractmethod
async def execute(self, ctx: ToolContext, **kwargs: Any) -> str:
pass
class MessageTool(Tool):
async def execute(self, ctx: ToolContext, content: str, **kwargs) -> str:
msg = OutboundMessage(channel=ctx.channel, chat_id=ctx.chat_id, content=content)
await self._send_callback(msg)
return "Sent"
优点:
- 类型安全
- 易于扩展(可添加更多上下文字段)
缺点:
- 需要修改 Tool 基类接口
- 执行时需要额外参数传递
- 增加 API 复杂度
对比结论:当前方案更简单,满足需求且不增加复杂度。
方案 4:全局上下文(TLS)
实现示例:
import contextvars
# 定义上下文变量
current_channel = contextvars.ContextVar("current_channel", default="")
current_chat_id = contextvars.ContextVar("current_chat_id", default="")
class MessageTool(Tool):
async def execute(self, content: str, **kwargs) -> str:
channel = current_channel.get()
chat_id = current_chat_id.get()
# ...
优点:
- 工具代码无需显式传递上下文
- 透明获取上下文
缺点:
- 依赖 Python asyncio 的 contextvars
- 调试困难(上下文隐式传递)
- 测试时需要手动设置 contextvars
对比结论:当前方案更明确,易于理解和调试。
5.3 为什么选择当前方案
nanobot 选择运行时注入方案的原因:
- 轻量级设计:只需添加
set_context()方法,无需复杂框架 - 单线程模型:天然支持,无需考虑并发问题
- 显式明确:上下文设置在代码中清晰可见
- 易于调试:上下文状态可直接检查
- 兼容性好:不影响工具的基类接口
- 学习成本低:简单的属性赋值,易于理解
核心权衡:
- 牺牲:需要显式调用
set_context() - 获得:简单、清晰、高效的实现
六、上下文注入的扩展性分析
6.1 支持更多上下文参数
当前限制
目前只支持 channel 和 chat_id 两个参数:
def set_context(self, channel: str, chat_id: str) -> None:
self._default_channel = channel
self._default_chat_id = chat_id
扩展方案 1:扩展 set_context 参数
class MessageTool(Tool):
def __init__(
self,
send_callback: Callable[[OutboundMessage], Awaitable[None]] | None = None,
default_channel: str = "",
default_chat_id: str = "",
default_user_id: str = "", # 新增
default_thread_id: str = "", # 新增
):
self._send_callback = send_callback
self._default_channel = default_channel
self._default_chat_id = default_chat_id
self._default_user_id = default_user_id # 新增
self._default_thread_id = default_thread_id # 新增
def set_context(
self,
channel: str,
chat_id: str,
user_id: str | None = None, # 新增
thread_id: str | None = None, # 新增
) -> None:
self._default_channel = channel
self._default_chat_id = chat_id
if user_id is not None:
self._default_user_id = user_id
if thread_id is not None:
self._default_thread_id = thread_id
优点:
- 简单直接
- 向后兼容(新增参数可选)
缺点:
- 参数列表会越来越长
- 缺乏灵活性
扩展方案 2:使用 Context 对象
@dataclass
class ToolContext:
channel: str
chat_id: str
user_id: str = ""
thread_id: str = ""
metadata: dict[str, Any] = field(default_factory=dict)
class MessageTool(Tool):
def __init__(
self,
send_callback: Callable[[OutboundMessage], Awaitable[None]] | None = None,
default_context: ToolContext | None = None,
):
self._send_callback = send_callback
self._context = default_context or ToolContext(channel="", chat_id="")
def set_context(self, context: ToolContext) -> None:
self._context = context
async def execute(
self,
content: str,
channel: str | None = None,
chat_id: str | None = None,
**kwargs
) -> str:
# 优先级:显式参数 > 上下文对象
channel = channel or self._context.channel
chat_id = chat_id or self._context.chat_id
user_id = kwargs.get("user_id") or self._context.user_id
# ...
优点:
- 易于扩展(添加新字段无需修改方法签名)
- 可携带结构化数据(metadata)
- 类型安全
缺点:
- 需要引入新的数据类
- 调用方式略有变化
扩展方案 3:动态属性(灵活但不够类型安全)
class MessageTool(Tool):
def __init__(self, ...):
self._send_callback = send_callback
self._context = {} # 字典存储
def set_context(self, **kwargs) -> None:
self._context.update(kwargs)
def get_context(self, key: str, default: Any = None) -> Any:
return self._context.get(key, default)
优点:
- 极其灵活
- 无需修改代码即可添加新上下文
缺点:
- 缺乏类型安全
- 容易拼写错误
- IDE 无法提供代码补全
推荐方案:方案 2(Context 对象),在保持简洁的同时提供类型安全和可扩展性。
6.2 实现上下文继承
场景需求
某些情况下需要子代理继承主代理的部分上下文,例如:
- 共享的用户配置
- 会话级别的元数据
- 工具访问权限
实现方案:Context 对象继承
@dataclass
class ToolContext:
channel: str
chat_id: str
user_id: str = ""
metadata: dict[str, Any] = field(default_factory=dict)
def inherit(self, **kwargs) -> "ToolContext":
"""创建继承当前上下文的新上下文对象"""
new_context = ToolContext(
channel=self.channel,
chat_id=self.chat_id,
user_id=self.user_id,
metadata=self.metadata.copy(), # 深拷贝
)
# 覆盖指定的字段
for key, value in kwargs.items():
if hasattr(new_context, key):
setattr(new_context, key, value)
return new_context
使用示例:
# 主代理上下文
main_context = ToolContext(
channel="telegram",
chat_id="123",
user_id="alice",
metadata={"locale": "en", "timezone": "UTC"}
)
# 子代理继承上下文(但修改 chat_id)
sub_context = main_context.inherit(chat_id="subagent_456")
# sub_context: {channel="telegram", chat_id="subagent_456", user_id="alice", ...}
在 nanobot 中应用
class SubagentManager:
async def spawn(
self,
task: str,
parent_context: ToolContext | None = None, # 新增参数
**kwargs
) -> str:
# 子代理上下文继承(但标记为子代理)
if parent_context:
sub_context = parent_context.inherit(
chat_id=f"subagent:{uuid.uuid4()[:8]}",
metadata={"parent_channel": parent_context.channel}
)
else:
sub_context = ToolContext(channel="system", chat_id="")
# 传递给子代理
bg_task = asyncio.create_task(
self._run_subagent(task_id, task, display_label, sub_context)
)
6.3 实现上下文验证和清理
验证机制
@dataclass
class ToolContext:
channel: str
chat_id: str
def validate(self) -> tuple[bool, str]:
"""验证上下文的有效性"""
if not self.channel:
return False, "Channel is required"
if not self.chat_id:
return False, "Chat ID is required"
if len(self.channel) > 50:
return False, "Channel too long"
return True, ""
class MessageTool(Tool):
def set_context(self, context: ToolContext) -> None:
valid, error = context.validate()
if not valid:
raise ValueError(f"Invalid context: {error}")
self._context = context
清理机制
场景:长时间运行的 Agent 可能积累过时的上下文
class ToolRegistry:
def __init__(self):
self._tools: dict[str, Tool] = {}
self._context_expiry: dict[str, datetime] = {}
def set_context(self, tool_name: str, context: ToolContext, ttl: int = 3600) -> None:
"""设置工具上下文(带过期时间)"""
tool = self._tools.get(tool_name)
if tool:
tool.set_context(context)
self._context_expiry[tool_name] = datetime.now() + timedelta(seconds=ttl)
def get_context(self, tool_name: str) -> ToolContext | None:
"""获取工具上下文(检查是否过期)"""
expiry = self._context_expiry.get(tool_name)
if expiry and datetime.now() > expiry:
# 过期则清理
tool = self._tools.get(tool_name)
if tool:
tool.set_context(ToolContext(channel="", chat_id=""))
del self._context_expiry[tool_name]
return None
return tool._context if tool else None
线程安全的上下文管理(预留)
如果 nanobot 未来需要多线程支持:
import threading
class ThreadSafeContext:
def __init__(self):
self._context = ToolContext(channel="", chat_id="")
self._lock = threading.Lock()
def set_context(self, context: ToolContext) -> None:
with self._lock:
self._context = context
def get_context(self) -> ToolContext:
with self._lock:
return self._context
6.4 扩展性建议总结
| 扩展方向 | 当前实现 | 建议方案 | 优先级 |
|---|---|---|---|
| 更多上下文参数 | 2个参数(channel, chat_id) | 引入 Context 对象 | 中 |
| 上下文继承 | 不支持 | Context.inherit() | 低 |
| 上下文验证 | 无 | Context.validate() | 低 |
| 上下文清理 | 无 | 消息级自动清理 | 低(当前模型无需) |
| 线程安全 | 单线程模型 | 预留接口 | 低(未来可能) |
七、关键代码位置索引
7.1 MessageTool 相关
| 功能 | 文件 | 行号 | 描述 |
|---|---|---|---|
| 类定义 | agent/tools/message.py | 9-87 | MessageTool 完整实现 |
__init__ | agent/tools/message.py | 12-20 | 初始化和默认值 |
set_context | agent/tools/message.py | 22-25 | 上下文注入核心方法 |
execute | agent/tools/message.py | 60-86 | 上下文使用和消息发送 |
set_send_callback | agent/tools/message.py | 27-29 | 发送回调设置 |
7.2 SpawnTool 相关
| 功能 | 文件 | 行号 | 描述 |
|---|---|---|---|
| 类定义 | agent/tools/spawn.py | 11-66 | SpawnTool 完整实现 |
__init__ | agent/tools/spawn.py | 19-22 | 初始化和默认值 |
set_context | agent/tools/spawn.py | 24-27 | 上下文注入核心方法 |
execute | agent/tools/spawn.py | 58-65 | 上下文传递到 SubagentManager |
7.3 Agent Loop 相关
| 功能 | 文件 | 行号 | 描述 |
|---|---|---|---|
| 工具注册 | agent/loop.py | 66-87 | _register_default_tools 方法 |
| MessageTool 实例化 | agent/loop.py | 82-83 | 创建并注册 MessageTool |
| SpawnTool 实例化 | agent/loop.py | 86-87 | 创建并注册 SpawnTool |
| 消息处理入口 | agent/loop.py | 123-216 | _process_message 方法 |
| 普通消息上下文设置 | agent/loop.py | 144-150 | 设置 MessageTool 和 SpawnTool 上下文 |
| 系统消息处理 | agent/loop.py | 218-308 | _process_system_message 方法 |
| 系统消息上下文恢复 | agent/loop.py | 241-248 | 恢复原始会话上下文 |
| 直接消息处理 | agent/loop.py | 310-329 | process_direct 方法(CLI) |
7.4 SubagentManager 相关
| 功能 | 文件 | 行号 | 描述 |
|---|---|---|---|
| 类定义 | agent/subagent.py | 20-234 | SubagentManager 完整实现 |
spawn 方法 | agent/subagent.py | 44-81 | 创建子代理并保存上下文 |
_run_subagent 方法 | agent/subagent.py | 83-166 | 子代理执行逻辑 |
_announce_result 方法 | agent/subagent.py | 168-198 | 通过消息总线发送结果 |
| 子代理工具注册 | agent/subagent.py | 94-101 | 子代理的工具(无 MessageTool 和 SpawnTool) |
7.5 基础类型相关
| 类型 | 文件 | 行号 | 描述 |
|---|---|---|---|
| Tool 基类 | agent/tools/base.py | 7-56 | 工具抽象基类 |
| ToolRegistry | agent/tools/registry.py | 8-71 | 工具注册表 |
| InboundMessage | bus/events.py | 9-24 | 入站消息定义 |
| OutboundMessage | bus/events.py | 27-36 | 出站消息定义 |
| Session | session/manager.py | 14-59 | 会话数据类 |
八、深挖价值点
8.1 设计亮点
1. 极简主义的上下文传递
nanobot 使用最简单的方式(属性赋值)实现了复杂的上下文管理需求:
def set_context(self, channel: str, chat_id: str) -> None:
self._default_channel = channel
self._default_chat_id = chat_id
价值:
- 零学习成本:无需理解框架概念
- 零运行时开销:无代理、无注入、无魔法
- 零调试困难:上下文状态一目了然
2. 消息编码的上下文传递
使用 chat_id 字段编码原始会话信息:
msg = InboundMessage(
channel="system",
sender_id="subagent",
chat_id=f"{origin['channel']}:{origin['chat_id']}", # 编码原始信息
content=announce_content,
)
价值:
- 无需新增字段:复用现有数据结构
- 自解释:格式清晰,易于解析
- 兼容性好:不影响其他消息类型
3. 子代理的工具限制
子代理不注册 MessageTool 和 SpawnTool:
# 子代理工具(无 MessageTool 和 SpawnTool)
tools.register(ReadFileTool())
tools.register(WriteFileTool())
# ...
价值:
- 防止循环:避免子代理递归创建子代理
- 结果统一:所有结果必须通过主代理返回
- 资源控制:子代理不能直接与用户交互
8.2 架构优势
单线程模型的优势
async def run(self) -> None:
while self._running:
msg = await self.bus.consume_inbound()
response = await self._process_message(msg) # 串行处理
价值:
- 无锁设计:上下文管理无需考虑并发
- 顺序保证:消息按到达顺序处理
- 调试简单:执行流线性可追踪
共享工具实例的优势
def __init__(self, ...):
self.tools = ToolRegistry()
self._register_default_tools() # 只注册一次
def _register_default_tools(self) -> None:
message_tool = MessageTool(...)
self.tools.register(message_tool) # 单例
价值:
- 内存效率:N 个会话共享 M 个工具(M << N)
- 配置统一:send_callback、api_key 只设置一次
- 状态一致性:所有会话使用相同工具配置
8.3 可扩展性的设计智慧
优先级机制
channel = channel or self._default_channel
价值:
- 灵活性:允许覆盖默认值
- 向后兼容:新增参数不影响现有代码
- 显式优先:显式参数 > 上下文注入
默认值的防御性设计
def __init__(self, ..., default_channel: str = "", default_chat_id: str = ""):
self._default_channel = default_channel
self._default_chat_id = default_chat_id
价值:
- 避免 AttributeError:字段始终存在
- 清晰的错误信息:未注入时返回友好错误
- 支持 CLI 场景:初始化时设置固定值
8.4 实际应用场景
场景 1:多渠道聊天机器人
Telegram 用户 A → nanobot → 响应发送到 Telegram
Discord 用户 B → nanobot → 响应发送到 Discord
WhatsApp 用户 C → nanobot → 响应发送到 WhatsApp
机制:
- 每条消息触发
set_context(channel, chat_id) MessageTool自动使用正确的渠道和会话- 无需 LLM 显式指定目标
场景 2:后台任务通知
用户: "分析这个大文件"
↓
nanobot: spawn 子代理(保存原始会话)
↓
子代理: 在后台分析...
↓
子代理完成: 发送系统消息(携带原始会话信息)
↓
nanobot: 恢复原始上下文,生成友好总结
↓
用户: "分析完成:..."
机制:
SpawnTool.set_context()保存原始会话SubagentManager._announce_result()编码原始信息_process_system_message()解析并恢复上下文
场景 3:CLI 模式
msg = InboundMessage(
channel="cli",
sender_id="user",
chat_id="direct",
content=content
)
机制:
- 固定的 channel="cli", chat_id="direct"
- 上下文注入后,工具自动使用 CLI 输出
- 无需修改任何工具代码
8.5 潜在改进方向
1. 类型安全的 Context 对象
# 当前:字符串参数
message_tool.set_context("telegram", "123")
# 改进:Context 对象
context = ToolContext(channel="telegram", chat_id="123")
message_tool.set_context(context)
收益:
- 类型安全
- 易于扩展
- IDE 支持更好
2. 上下文生命周期钩子
class Tool:
def on_context_set(self, context: ToolContext) -> None:
"""上下文设置时的回调"""
pass
def on_context_clear(self) -> None:
"""上下文清理时的回调"""
pass
收益:
- 工具可响应上下文变化
- 支持缓存预热/清理
- 便于调试和监控
3. 上下文中间件
class ContextMiddleware:
async def before_context_set(self, context: ToolContext) -> ToolContext:
"""上下文设置前的预处理"""
context.metadata["timestamp"] = datetime.now()
return context
async def after_context_set(self, context: ToolContext) -> None:
"""上下文设置后的后处理"""
logger.debug(f"Context set: {context.channel}:{context.chat_id}")
收益:
- AOP(面向切面)编程
- 统一的日志/监控
- 上下文验证/转换
总结
nanobot 的工具上下文注入机制是一个简单而强大的设计:
- 核心机制:通过
set_context()方法在运行时动态注入会话信息 - 关键价值:
- 支持工具实例复用(降低资源消耗)
- 支持子代理结果路由(实现后台任务)
- 支持多渠道并发(会话隔离)
- 设计哲学:
- 极简主义:用最少的代码实现核心功能
- 显式优于隐式:上下文设置清晰可见
- 单线程优化:无需考虑并发问题
这个机制完美体现了 nanobot "轻量但完整" 的设计理念,是学习如何设计简单高效的工具系统的绝佳范例。