第三层:上下文构建

📌 核心文件nanobot/agent/context.py (~218 行)

概述

ContextBuilder 负责组装发送给 LLM 的完整上下文,包括系统提示、记忆、技能、对话历史等。这是 Agent “看到的世界”。

上下文的组成

一个完整的 LLM 请求包含:

messages = [
    {
        "role": "system",
        "content": "系统提示(由 ContextBuilder 组装)"
    },
    {
        "role": "user",
        "content": "之前的用户消息1"
    },
    {
        "role": "assistant",
        "content": "之前的助手回复1"
    },
    # ... 更多历史 ...
    {
        "role": "user",
        "content": "当前用户消息"
    }
]

系统提示的结构

完整组成

# 1. 核心身份
- 当前时间
- 工作区路径  
- 基本指令

# 2. Bootstrap 文件(如果存在)
## AGENTS.md
(Agent 行为规范)

## SOUL.md
(个性化设定)

## USER.md
(用户信息)
# 3. 记忆
从 MEMORY.md 和每日笔记加载

# 4. 技能
## 始终加载的技能(完整内容)
...

## 可用技能(仅摘要)
...

代码实现

class ContextBuilder:
    BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md", "IDENTITY.md"]
    
    def __init__(self, workspace: Path):
        self.workspace = workspace
        self.memory = MemoryStore(workspace)
        self.skills = SkillsLoader(workspace)
    
    def build_system_prompt(self, skill_names: list[str] | None = None) -> str:
        """构建完整的系统提示"""
        parts = []
        
        # 1. 核心身份
        parts.append(self._get_identity())
        
        # 2. Bootstrap 文件
        bootstrap = self._load_bootstrap_files()
        if bootstrap:
            parts.append(bootstrap)
        
        # 3. 记忆
        memory = self.memory.get_memory_context()
        if memory:
            parts.append(f"# Memory\n\n{memory}")
        
        # 4. 技能
        always_skills = self.skills.get_always_skills()
        if always_skills:
            always_content = self.skills.load_skills_for_context(always_skills)
            parts.append(f"# Active Skills\n\n{always_content}")
        
        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.

{skills_summary}""")
        
        return "\n\n---\n\n".join(parts)

核心身份

def _get_identity(self) -> str:
    """获取核心身份部分"""
    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.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.

Always be helpful, accurate, and concise."""

Bootstrap 文件

用户可以在工作区创建这些文件来定制 Agent:

def _load_bootstrap_files(self) -> str:
    """加载所有 Bootstrap 文件"""
    parts = []
    
    for filename in self.BOOTSTRAP_FILES:
        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 ""

示例 - SOUL.md(赋予 Agent 个性):

You are a friendly and patient assistant.
You prefer to explain concepts step by step.
When users make mistakes, you gently correct them.

消息构建

完整的消息列表

def build_messages(
    self,
    history: list[dict[str, Any]],
    current_message: str,
    skill_names: list[str] | None = None,
    media: list[str] | None = None,
) -> list[dict[str, Any]]:
    """构建完整的消息列表"""
    messages = []
    
    # 系统提示
    system_prompt = self.build_system_prompt(skill_names)
    messages.append({"role": "system", "content": system_prompt})
    
    # 历史消息
    messages.extend(history)
    
    # 当前消息(可能包含图片)
    user_content = self._build_user_content(current_message, media)
    messages.append({"role": "user", "content": user_content})
    
    return messages

支持图片输入

def _build_user_content(self, text: str, media: list[str] | None):
    """构建用户消息(支持图片)"""
    if not media:
        return text
    
    # 转换图片为 base64
    images = []
    for path in media:
        p = Path(path)
        mime, _ = mimetypes.guess_type(path)
        if not p.is_file() or not mime or not mime.startswith("image/"):
            continue
        
        b64 = base64.b64encode(p.read_bytes()).decode()
        images.append({
            "type": "image_url",
            "image_url": {"url": f"data:{mime};base64,{b64}"}
        })
    
    if not images:
        return text
    
    # OpenAI 格式:列表 [image, image, text]
    return images + [{"type": "text", "text": text}]

生成的消息格式

{
  "role": "user",
  "content": [
    {
      "type": "image_url",
      "image_url": {
        "url": "data:image/jpeg;base64,/9j/4AAQSkZJRg..."
      }
    },
    {
      "type": "text",
      "text": "这张图片里有什么?"
    }
  ]
}

工具调用的消息处理

添加助手消息(带工具调用)

def add_assistant_message(
    self,
    messages: list[dict[str, Any]],
    content: str | None,
    tool_calls: list[dict[str, Any]] | None = None
) -> list[dict[str, Any]]:
    """添加助手消息到消息列表"""
    msg = {"role": "assistant", "content": content or ""}
    
    if tool_calls:
        msg["tool_calls"] = tool_calls
    
    messages.append(msg)
    return messages

添加工具结果

def add_tool_result(
    self,
    messages: list[dict[str, Any]],
    tool_call_id: str,
    tool_name: str,
    result: str
) -> list[dict[str, Any]]:
    """添加工具执行结果"""
    messages.append({
        "role": "tool",
        "tool_call_id": tool_call_id,
        "name": tool_name,
        "content": result
    })
    return messages

完整的对话示例

用户:“读取 config.json 文件并总结”

第一轮 LLM 调用

messages = [
    {
        "role": "system",
        "content": "# nanobot 🐈\n\n..."  # 完整系统提示
    },
    {
        "role": "user",
        "content": "读取 config.json 文件并总结"
    }
]

LLM 返回工具调用

{
  "role": "assistant",
  "content": null,
  "tool_calls": [{
    "id": "call_123",
    "type": "function",
    "function": {
      "name": "read_file",
      "arguments": "{\"path\": \"config.json\"}"
    }
  }]
}

添加工具调用和结果

# 添加 LLM 的工具调用
messages = builder.add_assistant_message(
    messages, 
    content=None, 
    tool_calls=[...]
)

# 执行工具
result = await tools.execute("read_file", {"path": "config.json"})

# 添加工具结果
messages = builder.add_tool_result(
    messages,
    tool_call_id="call_123",
    tool_name="read_file",
    result=result
)

现在 messages 变成:

[
    {"role": "system", "content": "..."},
    {"role": "user", "content": "读取 config.json 文件并总结"},
    {
        "role": "assistant",
        "content": None,
        "tool_calls": [...]
    },
    {
        "role": "tool",
        "tool_call_id": "call_123",
        "name": "read_file",
        "content": "文件内容:{...}"
    }
]

第二轮 LLM 调用

LLM 看到工具结果后生成总结:

{
  "role": "assistant",
  "content": "这个配置文件包含了以下设置:..."
}

上下文长度管理

虽然当前实现没有限制历史长度,但可以轻松添加:

def build_messages(self, history, current_message, max_history=20):
    """限制历史消息数量"""
    messages = []
    
    # 系统提示
    messages.append({"role": "system", "content": system_prompt})
    
    # 只保留最近 N 条历史
    recent_history = history[-max_history:] if len(history) > max_history else history
    messages.extend(recent_history)
    
    # 当前消息
    messages.append({"role": "user", "content": current_message})
    
    return messages

动态技能加载

ContextBuilder 支持两种技能加载方式:

1. 始终加载的技能

# skills/github/SKILL.md
---
name: github
always_load: true  # 始终加载完整内容
---

# GitHub Skill
...

2. 按需加载的技能

# skills/weather/SKILL.md
---
name: weather
available: true  # 仅显示在摘要中,Agent 需要时读取
---

# Weather Skill
...

LLM 会看到:

# Skills

The following skills are available. To use a skill, read its SKILL.md file.

- **weather** - Get weather information
  Location: ~/.nanobot/skills/weather/SKILL.md

小结

  • ✅ 模块化的系统提示构建
  • ✅ Bootstrap 文件支持定制化
  • ✅ 自动集成记忆和技能
  • ✅ 支持图片输入(Vision 模型)
  • ✅ 工具调用的消息管理

下一步07-会话管理.md