1. 模块概述
ContextBuilder(nanobot/agent/context.py)负责为每次 LLM 调用构建完整的消息列表——包括系统提示(身份、记忆、技能)和用户消息(含运行时上下文、媒体附件)。
📍 源码:
nanobot/nanobot/agent/context.py#L1-L281
1.1 系统提示的组成部分
1 | ┌──────────────────────────────────────┐ |
1.2 用户消息的组成部分
1 | ┌──────────────────────────────────────┐ |
2. 命名体系与易混淆函数对比
2.1 命名规律拆解
| 前缀 | 操作对象 | 操作 | 含义 |
|---|---|---|---|
build_ |
system_prompt |
— | 构建系统提示 |
build_ |
messages |
— | 构建完整消息列表 |
_build_ |
user_content |
— | 构建用户消息内容(含图片) |
_build_ |
runtime_context |
— | 构建运行时上下文元数据块 |
_get_ |
identity |
— | 获取身份部分 |
_load_ |
bootstrap_files |
— | 加载引导文件 |
_is_ |
template_content |
— | 检查是否仍是模板内容 |
_merge_ |
message_content |
— | 合并消息内容 |
2.2 易混淆函数对比表
| 对比维度 | build_system_prompt |
build_messages |
|---|---|---|
| 返回值 | 单一字符串(系统提示) | 消息列表 [{role, content}] |
| 包含历史 | ❌ | ✅ 包含 history 参数 |
| 包含运行时上下文 | ❌ | ✅ 追加 Runtime Context |
| 调用频率 | 每次 build_messages 内部调用 | 每个 turn 一次 |
| 一句话区分 | build_system_prompt 构建 system 角色的提示 |
build_messages 构建完整的消息列表 |
3. API Signatures
1 | class ContextBuilder: |
4. 数据结构深度解析
4.1 ContextBuilder
4.a 结构体存在的理由
ContextBuilder 将提示构建的所有关注点集中在一起:身份定义、引导文件加载、记忆注入、技能注入和历史注入。如果没有这个类,这些逻辑将分散在 AgentLoop 的各个方法中,导致提示构建逻辑难以理解和维护。
4.b 字段三层分析表
| 字段 | 设计动机 | 反事实 | 替代方案 |
|---|---|---|---|
BOOTSTRAP_FILES |
定义需要加载的引导文件列表 | 硬编码在 _load_bootstrap_files 中 |
可以配置化,但引导文件是框架约定 |
memory |
持有 MemoryStore 实例,复用文件 I/O | 每次构建提示时重新创建 MemoryStore | 可以用单例,但参数注入更清晰 |
skills |
持有 SkillsLoader 实例,管理技能发现和加载 | 每次构建提示时重新扫描技能 | 可以缓存技能内容,但 SkillsLoader 已内置缓存 |
_RUNTIME_CONTEXT_TAG |
标记运行时上下文的起止边界 | 无法在持久化时剥离运行时上下文 | 可以用特殊分隔符,但明确的标记更易解析 |
5. 函数逐行精讲
5.1 build_system_prompt() — 系统提示构建
1 | def build_system_prompt(self, skill_names=None, channel=None, ...): |
5.2 build_messages() — 完整消息组装
1 | def build_messages(self, history, current_message, ...): |
6. 关键算法剖析
6.1 运行时上下文注入
运行时上下文(Runtime Context)是一个关键的架构设计。它被追加在用户消息末尾,用明确的 [Runtime Context] 标记包裹:
1 | [Runtime Context — metadata only, not instructions] |
设计要点:
- 标记为”metadata only, not instructions”以防止 LLM 将其视为用户指令
- 在持久化时被剥离(
_RUNTIME_CONTEXT_TAG用于识别起止边界) - 与用户内容合并为一个 user 消息,避免角色交替问题
7. 设计决策分析
7.1 为什么运行时上下文追加在用户消息末尾而不是作为独立的 system 消息?
决策:运行时上下文(时间、渠道、发送者)与用户内容合并为同一个 user 消息。
权衡:
- ✅ 避免 system 消息在对话中途出现(许多 Provider 不支持或行为不一致)
- ✅ 保持消息列表简洁(减少消息数)
- ❌ 运行时上下文在持久化时需要特殊处理来剥离
- ❌ 可能影响 prompt cache 命中(时间每次不同)
7.2 为什么模板内容检测(_is_template_content)很重要?
决策:如果 MEMORY.md 内容与模板完全相同(用户未自定义),则不注入到系统提示。
权衡:
- ✅ 减少无效 Token 消耗
- ✅ 防止 LLM 将模板指令误认为用户的真实记忆
- ❌ 如果用户恰好写了与模板相同的内容,会被忽略
8. 学习检查点
📝 本章小结
- 系统提示由 8 个部分组成:身份、引导文件、工具规范、记忆、技能、技能摘要、最近历史、归档摘要
- 运行时上下文包含时间、渠道、发送者等元数据,追加在用户消息末尾
- 模板内容检测避免将未自定义的模板注入系统提示
- 消息合并确保不产生连续同角色消息(Provider 兼容性)
🤔 思考题
为什么
build_messages要将运行时上下文和用户内容合并为一个消息,而不是分开发送?参考答案
合并(
nanobot/nanobot/agent/context.py#L226-L232)是为了避免两个连续的 user 消息。许多 Provider 拒绝或行为异常当遇到连续的同角色消息。同时,合并确保运行时上下文紧跟在用户文本之后,LLM 将它们视为同一轮输入,更自然地将”在 Telegram 频道中,用户 X 在 10:30 说:…”作为整体理解。为什么最近历史注入(Recent History)限制为 50 条和 8000 Token?
参考答案
50 条(
nanobot/nanobot/agent/context.py#L57)和 8000 Token(nanobot/nanobot/agent/context.py#L58)是经验性平衡:太少则 Dream 处理后的历史信息不足,太多则挤占对话历史的上下文空间。最近历史是从 history.jsonl 注入的已处理摘要,不是原始对话——它提供的是”过去发生过什么”的背景信息,不需要像当前对话那样完整保留。