通过 Nanobot 源码学习架构---(5)Context

发布时间:2026/7/3 2:44:21
通过 Nanobot 源码学习架构---(5)Context OpenClaw 应该有40万行代码阅读理解起来难度过大因此本系列通过Nanobot来学习 OpenClaw 的特色。Nanobot是由香港大学数据科学实验室(HKUDS)开源的超轻量级个人 AI 助手框架定位为Ultra-Lightweight OpenClaw。非常适合学习Agent架构。丰富的上下文信息是 Agent 有效规划和行动的基础。一个 Agent 在工作时需要访问的”上下文”如下上下文类型举例存储方式对话历史用户刚才说了什么JSON / 数据库长期记忆用户偏好、过往总结向量数据库 / 知识图谱 / 文本外部知识RAG 检索的文档向量数据库 / API / 文本工具定义可调用的函数描述代码 / MCP 协议 / 文本人类输入标注、纠正、审核文本 / 表单临时草稿推理中间结果内存 / 临时文件这些东西格式不同、存储不同、访问方式不同。如果没有统一抽象每接一个新资源就得写一堆胶水代码。这些东西怎么存、怎么选、怎么压缩、怎么塞进那个有限的 token 窗口里——这才是真正决定 AI 效果的关键。ContextBuilder类是 Nanobot Agent 的「上下文大脑」将分散的身份、记忆、技能、运行时信息整合为 LLM 可识别的标准化对话上下文其核心价值为屏蔽了上下文构建的复杂性为 Agent 提供「开箱即用」的完整对话上下文是连接 Agent 各模块与 LLM 的核心枢纽。注本系列借鉴的文章过多可能在参考文献中有遗漏的文章如果有还请大家指出。0x01 提示词系统1.1 OpenClawOpenClaw 的提示词系统由一组放置在工作区目录下的Markdown文件组成每个文件承担特定职责。这几个被注入的Markdown文件来自Workspace 的一组 .md 文件每个文件都有独特的作用而且易于读写AGENTS.md操作手册。Agent 应该如何思考何时使用哪个工具遵循什么安全规则按什么顺序做事。SOUL.md性格与灵魂。语气、边界、优先级。希望 Agent 简洁明了不给多余建议写在这里。想要一个友好的助手也写在这里。USER.md你的用户画像。如何称呼你你的职业你的偏好。Agent 在每次回复前都会读取这个文件。MEMORY.md长期记忆。绝不能丢失的事实。YYYY-MM-DD.md每日日志。今天发生了什么哪些任务正在进行你们讨论了什么。到了明天Agent 会打开昨天的日志并接续上下文。BOOTSTRAP.md首次运行仪式一次性仅全新工作空间注入如引导对话等IDENTITY.md身份与氛围。很短的文件但它奠定了整体的基调。HEARTBEAT.md定期检查清单。“检查邮件”、“看看监控是否在运行”。TOOLS.md本地工具提示。脚本存放在哪里哪些命令可用。这样 Agent 就不需要去猜而是确切知道。1.2 Nanoboot在 Nanoboot 中也是类似的 Markdown 文件系统比如BOOTSTRAP_FILES [AGENTS.md, SOUL.md, USER.md, TOOLS.md, IDENTITY.md]SOUL.md 的内容如下# Soul I am nanobot , a personal AI assistant. ## Personality - Helpful and friendly - Concise and to the point - Curious and eager to learn ## Values - Accuracy over speed - User privacy and safety - Transparency in actions ## Communication Style - Be clear and direct - Explain reasoning when helpful - Ask clarifying questions when neededAGENTS.md内容如下# Agent Instructions You are a helpful AI assistant. Be concise, accurate, and friendly. ## Scheduled Reminders When user asks for a reminder at a specific time, use exec to run: nanobot cron add --name reminder --message Your message --at YYYY-MM-DDTHH:MM:SS --deliver --to USER_ID --channel CHANNEL Get USER_ID and CHANNEL from the current session (e.g., 8281248569 and telegram from telegram:8281248569). **Do NOT just write reminders to MEMORY.md** — that wont trigger actual notifications. ## Heartbeat Tasks HEARTBEAT.md is checked every 30 minutes. Use file tools to manage periodic tasks: - **Add**: edit_file to append new tasks - **Remove**: edit_file to delete completed tasks - **Rewrite**: write_file to replace all tasks When the user asks for a recurring/periodic task, update HEARTBEAT.md instead of creating a one-time cron reminder.1.3 Claw0我们用Claw0进行对比论证Claw0中指出系统提示词从磁盘上的文件组装. 换文件, 换性格。其架构如下Startup Per-Turn BootstrapLoader User Input load SOUL.md, IDENTITY.md, ... | truncate per file (20k) v cap total (150k) _auto_recall(user_input) | search memory by TF-IDF v | SkillsManager v scan directories for SKILL.md build_system_prompt() parse frontmatter assemble 8 layers: deduplicate by name 1. Identity | 2. Soul (personality) v 3. Tools guidance bootstrap_data skills_block 4. Skills (cached for all turns) 5. Memory (evergreen recalled) 6. Bootstrap (remaining files) 7. Runtime context 8. Channel hints | v LLM API call Earlier layers stronger influence on behavior. SOUL.md is at layer 2 for exactly this reason.其要点如下BootstrapLoader: 从工作区加载最多 8 个 markdown 文件, 有单文件和总量上限.SkillsManager: 扫描多个目录查找带 YAML frontmatter 的SKILL.md文件.MemoryStore: 双层存储 (常驻 MEMORY.md 每日 JSONL), TF-IDF 搜索._auto_recall(): 用用户消息搜索记忆, 将结果注入提示词.build_system_prompt(): 将 8 个层组装为一个字符串, 每轮重新构建.0x02 ContextBuilder 基本功能ContextBuilder是 Nanobot 框架中智能体对话上下文的核心构建器负责将「身份定义、引导文件、长期记忆、技能信息、运行时元数据、用户消息」等多维度信息整合为标准化的 LLM 对话上下文system prompt 消息列表是连接 Agent 各模块MemoryStore/SkillsLoader与 LLM 的关键桥梁。2.1 定义class ContextBuilder: Builds the context (system prompt messages) for the agent. BOOTSTRAP_FILES [AGENTS.md, SOUL.md, USER.md, TOOLS.md, IDENTITY.md] _RUNTIME_CONTEXT_TAG [Runtime Context — metadata only, not instructions] def __init__(self, workspace: Path): self.workspace workspace self.memory MemoryStore(workspace) self.skills SkillsLoader(workspace)数据依赖层级图如下ContextBuilder顶层 ├─ workspace输入参数 ├─ Memorystore依赖实例 │ ├─ workspace输入参数 │ ├─ MEMORY.md文件路径 │ └─ HISTORY.md文件路径 │ └─ SkillsLoader依赖实例 ├─ workspace输入参数 ├─ workspace/skills/工作区技能目录流程闭环初始化 → 上下文构建 → LLM 调用 / 工具执行 → 记忆整合 → 上下文更新 → 循环形成完整的 Agent 执行闭环。2.2 核心特色模块化上下文构建将系统提示词拆分为「身份核心、引导文件、记忆、常驻技能、技能摘要」多个模块按需拼接结构清晰且可扩展多源信息融合整合静态引导文件AGENTS.md/SOUL.md 等、动态记忆MemoryStore、技能体系SkillsLoader、运行时元数据时间 / 渠道 / 环境形成完整的 Agent 上下文多媒体兼容支持用户消息中嵌入 Base64 编码的图片适配多模态 LLM 的输入格式标准化消息管理提供工具调用结果、助手回复的标准化添加方法严格遵循 LLM 对话消息格式规范运行时元数据隔离将渠道、时间等运行时元数据标记为「仅元数据非指令」避免干扰 LLM 的核心决策逻辑灵活的技能加载区分「常驻技能alwaystrue」和「技能摘要」常驻技能直接嵌入上下文其他技能仅提供摘要需通过 read_file 工具读取平衡上下文长度与功能完整性。2.3 如何调用2.3.1 _process_message_process_message是单条消息处理的核心入口支持系统消息、斜杠命令、普通对话三种场景完成「上下文构建→代理循环→结果保存→响应返回」全流程。async def _process_message( self, msg: InboundMessage, session_key: str | None None, on_progress: Callable[[str], Awaitable[None]] | None None, ) - OutboundMessage | None: Process a single inbound message and return the response. # System messages: parse origin from chat_id (channel:chat_id) if msg.channel system: messages self.context.build_messages( historyhistory, current_messagemsg.content, channelchannel, chat_idchat_id, ) final_content, _, all_msgs await self._run_agent_loop(messages) self._save_turn(session, all_msgs, 1 len(history)) self.sessions.save(session) return OutboundMessage(channelchannel, chat_idchat_id, contentfinal_content or Background task completed.)2.3.2 _run_agent_loop()_run_agent_loop 函数是智能体的核心执行循环通过不断调用大模型并根据响应决定是否调用工具直到模型返回最终回答或达到最大迭代次数。_run_agent_loop 会调用 ContextBuilder 来构建消息。async def _run_agent_loop( self, initial_messages: list[dict], on_progress: Callable[..., Awaitable[None]] | None None, ) - tuple[str | None, list[str], list[dict]]: messages initial_messages while iteration self.max_iterations: response await self.provider.chat( messagesmessages, ) if response.has_tool_calls: messages self.context.add_assistant_message( messages, response.content, tool_call_dicts, reasoning_contentresponse.reasoning_content, ) for tool_call in response.tool_calls: messages self.context.add_tool_result( messages, tool_call.id, tool_call.name, result ) else: messages self.context.add_assistant_message( messages, clean, reasoning_contentresponse.reasoning_content, ) return final_content, tools_used, messages0x03 图例3.1 关键交互ContextBuilder 是核心枢纽聚合 SkillsLoader技能和 MemoryStore记忆的输出构建标准化 LLM 上下文详细交互如下ContextBuilder 与 MemoryStore 交互ContextBuilder初始化(workspace) ↓ Memorystore(workspace) ← 创建实例 ↓ build_system_prompt() → memory.get_memory_context() ← 获取长期记忆 ↓ 返回记忆上下文字符串ContextBuilder与SkillsLoader交互ContextBuilderskillsLoader ↓ ContextBuilder初始化(workspace) SkillsLoader(workspace) ← 创建实例 ↓ build_system_prompt()→skills.get_always_skills() ← 获取常驻技能列表 ↓ load_skills_for_context() ← 加载技能内容 ↓ build_skills_summary() ← 构建技能摘要 ↓ 返回技能相关内容字符串3.2 设计特点0x03 重点函数我们依据核心流程流程图来梳理重点函数。3.1 build_messages()3.1.1 返回值build_messages() 最终返回一个符合 LLM 对话格式的消息列表list [dict [str, Any]]每个字典代表一条对话消息严格遵循「role content」核心结构扩展支持工具调用、多模态等字段。这个列表是 Nanobot 调用 LLM 时的完整输入上下文包含系统提示、历史对话、运行时元数据、当前用户消息支持文本 图片是 Agent 与 LLM 交互的核心载体。返回的消息列表按固定顺序包含以下 5 类核心内容无内容时仍保留结构空值会被上游逻辑过滤消息角色 (role)内容 (content) 核心构成特殊字段 / 说明system由build_system_prompt()生成的完整系统提示核心基座无特殊字段纯文本是整个 Agent 的「身份 规则 技能 记忆 环境」总定义继承自 history历史对话消息可能包含 user/assistant/tool 等角色完全复用传入的history列表结构保留所有历史上下文user运行时元数据时间 / 时区 / 渠道 / 聊天 ID带固定标签[Runtime Context — metadata only, not instructions]纯文本仅作为元数据LLM 不会将其视为用户指令user当前用户消息文本 可选的 base64 编码图片单文本 / 文本 图片列表图片为image_url格式兼容 OpenAI 多模态 API 规范3.1.2 生成逻辑build_messages()的生成逻辑如下核心内容生成依赖build_system_prompt()系统提示、_build_runtime_context()元数据、_build_user_content()用户消息三大辅助函数生成逻辑是模块化拼接 条件过滤兼顾灵活性支持多模态 / 技能 / 记忆和规范性符合 LLM API 格式def build_messages( self, history: list[dict[str, Any]], current_message: str, skill_names: list[str] | None None, media: list[str] | None None, channel: str | None None, chat_id: str | None None, ) - list[dict[str, Any]]: Build the complete message list for an LLM call. return [ {role: system, content: self.build_system_prompt(skill_names)}, *history, {role: user, content: self._build_runtime_context(channel, chat_id)}, {role: user, content: self._build_user_content(current_message, media)}, ]逐行对应代码的生成步骤如下第一步生成系统提示调用build_system_prompt()整合身份、引导文件、记忆、技能等所有系统级配置。{role: system, content: self.build_system_prompt(skill_names)}第二步拼接历史对话用 Python 解包语法将历史消息列表直接插入到系统消息后。history是 list[dict[str, Any]]保留所有历史角色和字段包括 tool_calls、reasoning_content 等扩展字段。第三步添加运行时元数据生成包含时间 / 渠道 / 聊天 ID 的元数据作为独立的 user 消息避免污染用户真实指令{role: user, content: self._build_runtime_context(channel, chat_id)}第四步添加当前用户消息处理文本 图片生成最终的用户输入内容。{role: user, content: self._build_user_content(current_message, media)}最终将以上四部分按顺序组合成列表返回。3.1.3 完整消息构建流程流程图如下。3.2 build_system_prompt()system 消息核心由build_system_prompt()生成包含 6 个子模块。build_system_prompt() ├─ get_identity()返回身份信息 ├─ _load_bootstrap_files()加载引导文件 ├─ memory.get_memory_context()获取记忆内容 ├─ skills.get_always_skills()获取常驻技能列表 │ └─ skills.load_skills_for_context()→加载技能内容 │ └─ skills.build_skills_summary()构建技能摘要3.2.1 逻辑子模块顺序 生成逻辑如下核心身份_get_identity()nanobot 基础定义 运行时环境系统/架构/Python版本工作空间路径memory/skills 目录位置核心行为准则工具调用/文件操作/错误处理等引导文件_load_bootstrap_files()加载 workspace 下的 AGENTS.md/SOUL.md/USER.md/TOOLS.md/IDENTITY.mdAGENTS.md操作手册。Agent 应该如何思考何时使用哪个工具遵循什么安全规则按什么顺序做事。SOUL.md性格与灵魂。语气、边界、优先级。希望 Agent 简洁明了不给多余建议写在这里。想要一个友好的助手也写在这里。USER.md你的用户画像。如何称呼你你的职业你的偏好。Agent 在每次回复前都会读取这个文件。IDENTITY.md身份与氛围。很短的文件但它奠定了整体的基调。TOOLS.md本地工具提示。脚本存放在哪里哪些命令可用。这样 Agent 就不需要去猜而是确切知道。存在则拼接不存在则跳过记忆上下文memory.get_memory_context()从 MemoryStore 获取长期记忆内容存在则添加「# Memory」标题常驻技能skills.get_always_skills() load_skills_for_context()标记 alwaystrue 的技能内容存在则添加「# Active Skills」标题技能摘要skills.build_skills_summary()所有技能的 XML 格式摘要名称/描述/路径/可用性含使用说明拼接规则各模块用「\n\n---\n\n」分隔空模块自动过滤最终得到完整的系统提示。3.2.2 构建流程图3.3 _build_runtime_context作用构建运行时上下文元数据块包含固定包含当前时间格式YYYY-MM-DD HH:MM (星期) 时区可选包含渠道channel、聊天 IDchat_id仅当传入非空时开头固定标签[Runtime Context — metadata only, not instructions]明确告知 LLM 这是元数据而非指令。3.4 _build_user_content作用构建用户消息内容根据是否包含媒体内容决定返回格式无媒体文件mediaNone直接返回传入的current_message文本有媒体文件过滤非图片 / 不存在的文件将图片转为 base64 编码拼接data:{mime};base64,{b64}URL返回格式[{type: image_url, image_url: {url: ...} }, ..., {type: text, text: 用户文本}]。0x04 代码关键设计分层构建系统提示词按「身份→引导→记忆→技能」分层拼接逻辑清晰且可按需扩展多模态支持自动将图片转为 Base64 编码的 data URI适配多模态 LLM 输入元数据隔离运行时信息标记为「仅元数据」避免干扰 LLM 核心决策标准化消息提供工具结果、助手回复的统一添加方法严格遵循 LLM 对话格式class ContextBuilder: Builds the context (system prompt messages) for the agent. # 定义引导文件列表这些文件会被加载到系统提示词中定义Agent的基础行为/身份 BOOTSTRAP_FILES [AGENTS.md, SOUL.md, USER.md, TOOLS.md, IDENTITY.md] # 运行时上下文标签标记该部分为元数据非指令避免LLM误将其作为执行指令 _RUNTIME_CONTEXT_TAG [Runtime Context — metadata only, not instructions] def __init__(self, workspace: Path): # 初始化工作区路径Agent的核心工作目录 self.workspace workspace # 初始化记忆存储实例关联MemoryStore管理长期记忆/历史日志 self.memory MemoryStore(workspace) # 初始化技能加载器实例关联SkillsLoader管理Agent技能 self.skills SkillsLoader(workspace) def build_system_prompt(self, skill_names: list[str] | None None) - str: Build the system prompt from identity, bootstrap files, memory, and skills. # 初始化系统提示词片段列表按优先级拼接 parts [self._get_identity()] # 第一步添加核心身份定义优先级最高 # 第二步加载引导文件内容AGENTS.md/SOUL.md等 bootstrap self._load_bootstrap_files() if bootstrap: # 引导文件非空时添加 parts.append(bootstrap) # 第三步添加长期记忆内容 memory self.memory.get_memory_context() if memory: # 记忆非空时添加且包裹为# Memory标题 parts.append(f# Memory\n\n{memory}) # 第四步添加常驻技能alwaystrue的技能直接嵌入上下文 always_skills self.skills.get_always_skills() if always_skills: # 有常驻技能时 # 加载常驻技能的核心内容 always_content self.skills.load_skills_for_context(always_skills) if always_content: # 技能内容非空时添加包裹为# Active Skills标题 parts.append(f# Active Skills\n\n{always_content}) # 第五步添加所有技能的摘要XML格式供Agent按需读取 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 availablefalse need dependencies installed first - you can try installing them with apt/brew. {skills_summary}) # 将所有片段用分隔线---拼接为完整的系统提示词 return \n\n---\n\n.join(parts) def _get_identity(self) - str: Get the core identity section. # 获取工作区的绝对路径展开用户目录、解析符号链接 workspace_path str(self.workspace.expanduser().resolve()) # 获取操作系统类型Windows/Linux/macOS system platform.system() # 构建运行时环境信息系统版本架构 Python版本 runtime f{macOS if system Darwin else system} {platform.machine()}, Python {platform.python_version()} # 返回Agent的核心身份定义系统提示词的基础部分 return f# nanobot You are nanobot, a helpful AI assistant. ## Runtime {runtime} ## Workspace Your workspace is at: {workspace_path} - Long-term memory: {workspace_path}/memory/MEMORY.md (write important facts here) - History log: {workspace_path}/memory/HISTORY.md (grep-searchable) - Custom skills: {workspace_path}/skills/{{skill-name}}/SKILL.md ## nanobot Guidelines - State intent before tool calls, but NEVER predict or claim results before receiving them. - Before modifying a file, read it first. Do not assume files or directories exist. - After writing or editing a file, re-read it if accuracy matters. - If a tool call fails, analyze the error before retrying with a different approach. - Ask for clarification when the request is ambiguous. Reply directly with text for conversations. Only use the message tool to send to a specific chat channel. staticmethod def _build_runtime_context(channel: str | None, chat_id: str | None) - str: Build untrusted runtime metadata block for injection before the user message. # 获取当前时间格式年-月-日 时:分 (星期) now datetime.now().strftime(%Y-%m-%d %H:%M (%A)) # 获取时区信息无则默认UTC tz time.strftime(%Z) or UTC # 初始化运行时元数据行列表 lines [fCurrent Time: {now} ({tz})] # 若提供了渠道和聊天ID添加到元数据中 if channel and chat_id: lines [fChannel: {channel}, fChat ID: {chat_id}] # 拼接元数据开头添加标记后续是具体元数据行 return ContextBuilder._RUNTIME_CONTEXT_TAG \n \n.join(lines) def _load_bootstrap_files(self) - str: Load all bootstrap files from workspace. # 初始化引导文件内容片段列表 parts [] # 遍历所有预设的引导文件 for filename in self.BOOTSTRAP_FILES: # 拼接文件路径工作区根目录下 file_path self.workspace / filename # 仅处理存在的文件 if file_path.exists(): # 读取文件内容UTF-8编码 content file_path.read_text(encodingutf-8) # 按「## 文件名」的格式包裹内容添加到片段列表 parts.append(f## {filename}\n\n{content}) # 拼接所有引导文件内容无则返回空字符串 return \n\n.join(parts) if parts else def build_messages( self, history: list[dict[str, Any]], current_message: str, skill_names: list[str] | None None, media: list[str] | None None, channel: str | None None, chat_id: str | None None, ) - list[dict[str, Any]]: Build the complete message list for an LLM call. # 构建完整的LLM消息列表包含以下部分 # 1. 系统消息核心提示词身份/引导/记忆/技能 # 2. 历史消息之前的对话记录 # 3. 运行时元数据时间/渠道等标记为仅元数据 # 4. 当前用户消息含文本可选多媒体 return [ {role: system, content: self.build_system_prompt(skill_names)}, *history, # 解包历史消息列表 {role: user, content: self._build_runtime_context(channel, chat_id)}, {role: user, content: self._build_user_content(current_message, media)}, ] def _build_user_content(self, text: str, media: list[str] | None) - str | list[dict[str, Any]]: Build user message content with optional base64-encoded images. # 无媒体文件时直接返回文本 if not media: return text # 初始化图片列表存储Base64编码的图片信息 images [] # 遍历所有媒体文件路径 for path in media: p Path(path) # 猜测文件的MIME类型如image/png、image/jpeg mime, _ mimetypes.guess_type(path) # 过滤条件1. 是文件 2. 能识别MIME类型 3. MIME类型以image/开头 if not p.is_file() or not mime or not mime.startswith(image/): continue # 读取文件字节并编码为Base64字符串 b64 base64.b64encode(p.read_bytes()).decode() # 按多模态LLM格式添加图片信息data URI格式 images.append({type: image_url, image_url: {url: fdata:{mime};base64,{b64}}}) # 无有效图片时返回纯文本 if not images: return text # 有图片时返回「图片列表 文本消息」的组合格式适配多模态输入 return images [{type: text, text: text}] def add_tool_result( self, messages: list[dict[str, Any]], tool_call_id: str, tool_name: str, result: str, ) - list[dict[str, Any]]: Add a tool result to the message list. # 按LLM规范添加工具调用结果消息 # - role: tool固定 # - tool_call_id: 关联的工具调用ID # - name: 工具名称 # - content: 工具执行结果 messages.append({role: tool, tool_call_id: tool_call_id, name: tool_name, content: result}) # 返回更新后的消息列表 return messages def add_assistant_message( self, messages: list[dict[str, Any]], content: str | None, tool_calls: list[dict[str, Any]] | None None, reasoning_content: str | None None, ) - list[dict[str, Any]]: Add an assistant message to the message list. # 初始化助手消息核心是role和content msg: dict[str, Any] {role: assistant, content: content} # 若有工具调用指令添加到消息中 if tool_calls: msg[tool_calls] tool_calls # 若有推理过程内容添加到消息中用于调试/跟踪Agent思考过程 if reasoning_content is not None: msg[reasoning_content] reasoning_content # 将助手消息添加到消息列表 messages.append(msg) # 返回更新后的消息列表 return messages