Code Reader
首页
帮助
设计文档
首页
帮助
设计文档
  • nanobot 工具上下文注入机制深度分析

nanobot 工具上下文注入机制深度分析

概述:设计目标

nanobot 的工具上下文注入机制是一个精心设计的动态上下文管理系统,其核心设计目标是:

  1. 工具复用性:同一工具实例可在多个会话间共享,降低资源消耗
  2. 会话隔离性:确保不同渠道/会话的工具行为互不干扰
  3. 上下文传递:支持子代理向原始会话路由结果的能力
  4. 简化配置:通过默认值机制减少重复参数传递
  5. 运行时动态性:在消息处理循环中动态更新工具上下文

该机制通过 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)}"

上下文优先级:

  1. 显式参数(LLM 工具调用时指定的)
  2. 上下文注入值(_default_channel、_default_chat_id)
  3. 空值(返回错误)

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="")

作用:

  1. 类型提示:明确字段存在,避免 AttributeError
  2. 防御性编程:工具在未注入上下文时不会崩溃,返回清晰的错误信息
  3. 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)

并发场景分析:

  1. 消息A到达(channel=telegram, chat_id=123)

    • set_context("telegram", "123")
    • 工具上下文:telegram/123
    • 消息A处理完成
  2. 消息B到达(channel=discord, chat_id=456)

    • set_context("discord", "456") # 覆盖之前的上下文
    • 工具上下文:discord/456
    • 消息B处理完成
  3. 子代理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 选择运行时注入方案的原因:

  1. 轻量级设计:只需添加 set_context() 方法,无需复杂框架
  2. 单线程模型:天然支持,无需考虑并发问题
  3. 显式明确:上下文设置在代码中清晰可见
  4. 易于调试:上下文状态可直接检查
  5. 兼容性好:不影响工具的基类接口
  6. 学习成本低:简单的属性赋值,易于理解

核心权衡:

  • 牺牲:需要显式调用 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.py9-87MessageTool 完整实现
__init__agent/tools/message.py12-20初始化和默认值
set_contextagent/tools/message.py22-25上下文注入核心方法
executeagent/tools/message.py60-86上下文使用和消息发送
set_send_callbackagent/tools/message.py27-29发送回调设置

7.2 SpawnTool 相关

功能文件行号描述
类定义agent/tools/spawn.py11-66SpawnTool 完整实现
__init__agent/tools/spawn.py19-22初始化和默认值
set_contextagent/tools/spawn.py24-27上下文注入核心方法
executeagent/tools/spawn.py58-65上下文传递到 SubagentManager

7.3 Agent Loop 相关

功能文件行号描述
工具注册agent/loop.py66-87_register_default_tools 方法
MessageTool 实例化agent/loop.py82-83创建并注册 MessageTool
SpawnTool 实例化agent/loop.py86-87创建并注册 SpawnTool
消息处理入口agent/loop.py123-216_process_message 方法
普通消息上下文设置agent/loop.py144-150设置 MessageTool 和 SpawnTool 上下文
系统消息处理agent/loop.py218-308_process_system_message 方法
系统消息上下文恢复agent/loop.py241-248恢复原始会话上下文
直接消息处理agent/loop.py310-329process_direct 方法(CLI)

7.4 SubagentManager 相关

功能文件行号描述
类定义agent/subagent.py20-234SubagentManager 完整实现
spawn 方法agent/subagent.py44-81创建子代理并保存上下文
_run_subagent 方法agent/subagent.py83-166子代理执行逻辑
_announce_result 方法agent/subagent.py168-198通过消息总线发送结果
子代理工具注册agent/subagent.py94-101子代理的工具(无 MessageTool 和 SpawnTool)

7.5 基础类型相关

类型文件行号描述
Tool 基类agent/tools/base.py7-56工具抽象基类
ToolRegistryagent/tools/registry.py8-71工具注册表
InboundMessagebus/events.py9-24入站消息定义
OutboundMessagebus/events.py27-36出站消息定义
Sessionsession/manager.py14-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 的工具上下文注入机制是一个简单而强大的设计:

  1. 核心机制:通过 set_context() 方法在运行时动态注入会话信息
  2. 关键价值:
    • 支持工具实例复用(降低资源消耗)
    • 支持子代理结果路由(实现后台任务)
    • 支持多渠道并发(会话隔离)
  3. 设计哲学:
    • 极简主义:用最少的代码实现核心功能
    • 显式优于隐式:上下文设置清晰可见
    • 单线程优化:无需考虑并发问题

这个机制完美体现了 nanobot "轻量但完整" 的设计理念,是学习如何设计简单高效的工具系统的绝佳范例。