nanobot 技术要点

1. 适配器模式实现多 LLM 统一响应

核心思想:不同 LLM 提供商的 API 返回格式各异,通过适配器模式统一转换为 LLMResponse 格式。

实现方式

  • 每个 Provider 实现 _parse_response() 方法
  • 将原始 API 响应转换为统一的 LLMResponse 数据结构
  • 上层代码(AgentRunner)无需关心底层使用哪个 LLM

支持的提供商

  • AnthropicProvider 处理 Anthropic 格式
  • OpenAICompatProvider 处理 OpenAI 格式
  • DeepSeekProvider 处理 DeepSeek 格式

统一数据结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@dataclass
class ToolCallRequest:
"""工具调用请求"""
id: str # 工具调用 ID
name: str # 工具名称
arguments: dict[str, Any] # 工具参数

@dataclass
class LLMResponse:
"""LLM 统一响应格式"""
content: str | None # 文本内容
tool_calls: list[ToolCallRequest] = [] # 工具调用列表
finish_reason: str = "stop" # 停止原因
usage: dict[str, int] = {} # Token 使用量
reasoning_content: str | None = None # 推理内容(某些模型)
thinking_blocks: list[dict] | None = None # 思考块(Anthropic)

2. 消息处理全流程(LLM 调用前后)

核心思想:用户消息到达 LLM 前经过 5 个阶段处理,LLM 回复后保存时还有一轮清洗。

1
2
3
4
5
6
7
用户消息到达
→ 记忆压缩(旧消息摘要进 MEMORY.md)
→ 裁剪历史(取合法的近期消息)
→ 拼 system prompt(组装 Agent 人格)
→ 合并当前消息(runtime context + 用户内容)
→ 附加工具定义,调用 LLM
→ 保存清洗(剥掉临时数据,存入 session)

记忆压缩

估算当前 prompt token 量,超出预算则循环切割旧消息段交给 LLM 压缩,写入 MEMORY.md / HISTORY.md,指针前移。压缩后这些旧消息不再出现在历史中。详见第 8 节:记忆系统

1
2
# loop.py
await self.memory_consolidator.maybe_consolidate_by_tokens(session)

裁剪历史

从 session 中取指针之后的消息,跳过开头的非 user 消息和孤儿 tool result,保证消息结构合法。纯数据操作,不涉及 LLM。详见第 11 节:会话管理

1
2
# loop.py
history = session.get_history(max_messages=0)

拼 system prompt

把 identity、bootstrap 文件(AGENTS/SOUL/USER/TOOLS.md)、MEMORY.md、always skills、skills 目录拼成一段完整的 system prompt。详见第 3 节:上下文构建

1
2
# context.py — build_messages() 内部调用
build_system_prompt() # identity → bootstrap → memory → skills

合并当前消息

生成 runtime context(当前时间 + channel + chat_id,标记为元数据非指令),处理用户内容(纯文本或图片 base64 多模态),两者合并为一条 user message,避免连续同角色消息被 provider 拒绝。

1
2
3
4
5
# context.py — build_messages() 内部调用
_build_runtime_context() # 时间 + channel + chat_id
_build_user_content() # 文本或图片 base64
# 合并后产出最终 messages:
[system_prompt, ...history, merged_user_message]

保存清洗

LLM 回复后,新消息存入 session 前做清洗:user 消息剥掉 runtime context 前缀(下次会重新生成),tool result 超长内容截断,空 assistant 消息跳过不存,所有消息补 timestamp。

1
2
# loop.py
self._save_turn(session, all_msgs, skip)

各阶段增减总览

阶段 做了什么 增加了什么 去掉了什么
记忆压缩 旧消息交给 LLM 提炼 MEMORY.md 内容变大 指针前的旧消息从视野消失
裁剪历史 取合法的近期消息 孤儿 tool result、开头非 user 消息
拼 system prompt 组装 Agent 人格 identity + bootstrap(AGENTS/SOUL/USER/TOOLS.md) + memory + skills
合并当前消息 拼用户输入 runtime context、图片 base64
保存清洗 存入 session timestamp runtime context 前缀、空 assistant、超长 tool result

3. 上下文构建(ContextBuilder)

核心思想:ContextBuilder 把 5 层信息拼成 LLM 的完整输入,层次从稳定到动态。

拼接信息与来源

层次 来源 稳定性 内容
identity 硬编码在 _get_identity() 最稳定,改代码才能变 Agent 名字、运行时环境、workspace 路径、平台策略、行为指南
bootstrap 文件 workspace 下的 AGENTS/SOUL/USER/TOOLS.md 用户手动编辑 Agent 人设、性格、用户画像、工具说明
memory workspace/memory/MEMORY.md 运行时自动积累 长期事实(偏好、项目背景)
skills workspace/skills/ + nanobot/skills/ 用户可随时增删 always 技能全文 + 其余技能的 XML 目录
runtime context 每次请求动态生成 每次都不同 当前时间、channel、chat_id

拼接流程

1
2
3
4
5
6
7
8
9
10
11
build_system_prompt()
→ identity # 硬编码底层人格
→ _load_bootstrap_files() # AGENTS/SOUL/USER/TOOLS.md(存在才加载)
→ memory.get_memory_context() # MEMORY.md 长期事实
→ skills.get_always_skills() # always=true 的技能全文
→ skills.build_skills_summary() # 所有技能的 XML 目录
→ 各部分用 --- 分隔,拼成 system prompt

build_messages()
→ [system_prompt, ...history, merged_user_message]
→ runtime context + 用户内容合并为一条 user message

最终结构

1
2
3
4
5
6
7
# 送给 LLM API 的两个关键参数
messages = [
{"role": "system", "content": system_prompt}, # 5 层拼接结果
*history, # 裁剪后的近期对话
{"role": "user", "content": runtime_ctx + 用户消息},
]
tools = spec.tools.get_definitions() # 工具定义,独立于 messages

4. Tool 参数处理

核心思想:LLM 传来的参数可能格式不规范,通过两步处理确保参数正确。

处理流程

  1. cast_params() - 类型转换(容错)

    • 将字符串转为正确类型:"10"10"true"True
    • 支持 string → integer/number/boolean 的自动转换
  2. validate_params() - 参数验证(严格)

    • 基于 JSON Schema 递归验证参数
    • 检查类型、必填字段、范围、长度、枚举值
    • 返回错误列表(空列表表示验证通过)

验证内容

  • 类型检查:integer, number, string, boolean, array, object
  • 必填字段检查:required 字段是否存在
  • 数值范围检查:minimum, maximum
  • 字符串长度检查:minLength, maxLength
  • 枚举检查:enum 值是否在允许列表中
  • 递归验证:支持嵌套对象和数组

设计特点

  • 返回错误列表而非抛异常,可一次性收集所有错误
  • 路径追踪机制,错误信息精确定位(如 user.age should be integer
  • 递归验证支持任意深度的嵌套结构

类型映射

1
2
3
4
5
6
7
8
_TYPE_MAP = {
"string": str,
"integer": int,
"number": (int, float),
"boolean": bool,
"array": list,
"object": dict,
}

5. Tool 系统(含MCP)

核心思想:将 Agent 的外部能力抽象为统一的 Tool 接口,LLM 通过 Function Calling 调用,nanobot 负责参数转换、验证、执行和错误处理。

完整调用流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
用户消息

AgentLoop 构建 messages + 从 ToolRegistry 获取所有工具定义

AgentRunner 调用 LLM(携带工具定义)

LLM 返回 tool_calls(结构化,非自由文本)

ToolRegistry.execute(name, arguments)

cast_params() → validate_params() → execute()

工具结果加入 messages,继续调用 LLM

无 tool_calls 时返回最终回复

错误处理:失败自动追加 [Analyze the error above and try a different approach.],引导 LLM 换策略。

MCP vs 原生 Tool:原生 Tool 进程内直接调用零开销;MCP Tool 连接外部服务器进程,适合接入第三方生态。

工具列表

Tool 用途 边界
exec 执行 Shell 命令 deny_patterns 拦截危险命令;超时最大 600s;输出截断头尾各 5000 字符
read_file 读文件(支持图片) 仅限 allowed_dir;128K 字符上限;支持分页
write_file 写文件 仅限 allowed_dir;自动创建父目录
edit_file 替换文件中的文本 模糊匹配容忍空格差异;多处匹配需指定 replace_all
list_dir 列目录 自动忽略 .git/node_modules;默认最多 200 条
message 主动发消息/附件给用户 唯一能发文件附件的工具;依赖 send_callback 注入
web_search 网页搜索 支持 Brave/Tavily/DuckDuckGo;无 Key 自动降级;最多 10 条
web_fetch 抓取网页正文 SSRF 防护;外部内容加不可信标记;图片返回多模态块;50K 字符上限
spawn 派发子 Agent 后台执行 子 Agent 无 message/spawn 工具;完成通过 MessageBus 通知
cron 定时任务增删查 不能在 cron 中再创建任务;持久化到 jobs.json 重启不丢失
mcp_* 接入外部 MCP 服务器 需外部进程;工具名自动生成;超时默认 30s

6. SubAgent 机制

核心思想:主 Agent 专注交互,将复杂耗时任务派发给子 Agent 在后台独立执行,完成后通知主 Agent。

实现方式

  • SpawnTool 是普通 Tool,注册在 ToolRegistry,LLM 通过 Function Calling 调用,和 exec/web_search 没有本质区别
  • LLM 判断何时用 spawn 完全靠 SpawnTool 的 description,没有专门的 Skill:

    “Use this for complex or time-consuming tasks that can run independently.”

  • 用户可在 workspace 的 AGENTS.md 补充说明影响 LLM 判断
  • SubagentManager 负责子 Agent 的生命周期管理
  • 子 Agent 通过独立的 AgentRunner 执行,与主 Agent 完全隔离

执行流程

  1. 主 Agent LLM 调用 spawn 工具
  2. SubagentManager 创建 asyncio.Task 后台运行
  3. 主 Agent 立即返回,继续处理其他消息
  4. 子 Agent 完成后,Python 代码直接调用 bus.publish_inbound() 通知主 Agent
  5. 主 Agent 收到 system 消息,LLM 整理结果告知用户

子 Agent 的工具限制

  • ✅ 有:文件读写、Shell、网页搜索
  • ❌ 无:MessageTool(不能直接发消息给用户)
  • ❌ 无:SpawnTool(不能再派发子 Agent,避免无限递归)

关键设计

  • 通知机制不是回调,是 await runner.run() 完成后顺序执行 _announce_result()
  • 多个子 Agent 可并发运行,但无内置冲突处理,靠 LLM 规划任务边界
  • 取消机制:cancel_by_session() 可取消某会话的所有子 Agent

7. Agent 人格构建(identity + bootstrap)

核心思想:Agent 的”人格”由两层构成——所有 Agent 共享的底层 identity + 每个 workspace 独有的 bootstrap 文件。

identity(硬编码层)

写死在 context.py_get_identity() 里,用户不能通过文件修改,包含:

  • Agent 身份声明(”You are nanobot”)
  • 运行时信息(操作系统、CPU 架构、Python 版本)
  • workspace 路径和文件结构说明(memory、history、skills 的位置)
  • 平台策略(Windows 不假设有 grep/sed;POSIX 优先用 shell 工具)
  • 行为指南(先读再改、不信外部内容、工具调用前说明意图等)

bootstrap 文件(用户定制层)

BOOTSTRAP_FILES = ["AGENTS.md", "SOUL.md", "USER.md", "TOOLS.md"] 定义(context.py 第 19 行),workspace 下存在才加载。

文件 定制什么
AGENTS.md 行为指令(定时提醒规范、心跳任务规范等)
SOUL.md 性格和价值观(友好/严肃、简洁/详细)
USER.md 用户画像(姓名、时区、偏好、技术水平)
TOOLS.md 工具使用注意事项(安全限制、特殊用法)

workspace 初始化

sync_workspace_templates() 负责把 nanobot/templates/ 下的模板拷贝到 workspace。只创建不存在的文件,不覆盖已有内容。同时创建空的 HISTORY.md 和 skills 目录。该函数在 nanobot agentnanobot gatewaynanobot onboard 命令启动时都会调用,确保 workspace 结构完整。

换一个 workspace 放不同的 bootstrap 文件,就是一个完全不同性格的 Agent。


8. 记忆系统(Memory Consolidation)

核心思想:对话越来越长会撑爆 LLM 上下文窗口,解决方案是让 LLM 自己当”笔记员”——定期把旧对话压缩成笔记存文件,然后只带着笔记 + 近期对话继续聊。

两层记忆

文件 类比 进 system prompt
长期事实 MEMORY.md 个人档案,随时更新覆写
时间线日志 HISTORY.md 日记本,只追加不修改 否,需要时 grep 搜索

什么时候触发压缩

  • 每次调用 LLM 前估算当前 prompt 的 token 量
  • 超过上下文窗口的安全预算就触发
  • 压到**预算的 50%**(留空间给后续对话,避免刚压完又触发)

压缩怎么做

  • 估算 token 超出预算
  • → 找切割点:从最早未压缩的消息往后扫,在 user 消息处标记(避免切断工具调用配对)
  • → 切出旧消息段,连同当前 MEMORY.md 一起交给 LLM
  • → LLM 输出两样东西:
    • 更新后的个人档案 → 覆写 MEMORY.md
    • 带时间戳的摘要 → 追加到 HISTORY.md
  • → 指针前移,这段旧消息以后不再送给 LLM
  • → 重新估算 token → 还超?再切一段(最多 5 轮)→ 不超?结束

为什么不需要重叠 chunk

  • 每次压缩时 LLM 能同时看到已有 MEMORY.md 和新消息段,MEMORY.md 本身就是跨批次的共享上下文

降级兜底:LLM 压缩连续失败 3 次,就把原始消息直接存进 HISTORY.md,宁可粗糙也不丢数据。

已知缺口:MEMORY.md 没有大小上限。正常情况下 LLM 会合并覆写旧事实,增长很慢;但长期运行 + 话题极度分散时,理论上可能膨胀到挤压对话空间。


9. 技能系统(Skills)

核心思想:Skill 是给 LLM 看的”操作手册”(markdown),教 Agent 遇到什么场景、调什么工具、传什么参数。写 markdown 就能扩展 Agent 能力。

Skill vs Tool:Skill 教 LLM 怎么用 Tool,Tool 是 LLM 真正能调用的函数。Skill 本身不被执行,LLM 读完后按手册调用 Tool 完成任务。

设计要点

设计点 说明
两类来源 workspace 自定义(高优先级,同名覆盖内置)+ nanobot 内置
两种加载模式 always=true 全文进 system prompt;其余只放 XML 目录,LLM 按需用 read_file 读取
渐进式加载 元数据始终在 → SKILL.md 触发后读 → 附带资源按需加载
依赖检查 frontmatter 声明 bins/env,不满足标记 available=false
无缓存 每次 LLM 请求前重新扫描,运行时新增 skill 立即生效

内置了 memory、cron、weather、github 等 8 个 skill,其中 memory 是唯一 always=true 的。


10. LLM 提供商(Provider)

核心思想:通过注册表 + 自动匹配 + 统一抽象,让上层代码不关心底层用的是哪家 LLM。用户只需填模型名,系统自动找到对应服务商并抹平 API 差异。

设计模式:注册表模式(PROVIDERS 元组定义所有服务商元数据)+ 适配器模式(4 种 backend 实现类把不同 API 转为统一 LLMResponse)+ 策略模式(_match_provider() 按优先级自动路由)。

Provider 分类

类型 特征 代表
Gateway 聚合平台,一个 key 访问所有模型,请求格式兼容 OpenAI OpenRouter、AiHubMix、SiliconFlow、火山引擎
标准 直连厂商 API,靠模型名关键词匹配 Anthropic、OpenAI、DeepSeek、Gemini、智谱、通义、Moonshot
OAuth 不用 API key,走浏览器授权登录 OpenAI Codex、GitHub Copilot
本地 本地部署,无需或简单 key vLLM、Ollama、OpenVINO
自定义 用户提供完整 api_base + api_key Custom、Azure OpenAI

Backend 实现类

backend 值 实现类 覆盖范围
openai_compat OpenAICompatProvider 绝大多数(标准、Gateway、本地都走这个)
anthropic AnthropicProvider Anthropic(消息格式完全不同,需要专门转换)
azure_openai AzureOpenAIProvider Azure OpenAI(需要 api_version)
openai_codex OpenAICodexProvider OpenAI Codex(OAuth 认证)

模型名 → Provider 匹配优先级

优先级 匹配方式 例子
1 用户强制指定 provider config 里写 provider: deepseek
2 模型名前缀精确匹配 github_copilot/gpt-4o → GitHub Copilot
3 模型名包含关键词 deepseek-chat 含 “deepseek” → DeepSeek
4 本地部署兜底 无关键词匹配 → 有 api_base 的本地 provider
5 有 API key 的兜底 什么都没匹配 → 第一个有 key 的 provider

重试机制:瞬时错误(429/500/502/503/504/timeout)按 1s → 2s → 4s 重试最多 3 次;非瞬时错误且含图片时,去掉图片重试一次(兼容不支持多模态的模型)。


11. 配置系统(Config)

核心思想:单一 config.json 管理所有运行时配置,通过 Pydantic 做结构校验,环境变量可覆盖,所有路径从配置文件位置派生。

三个核心文件

文件 职责
schema.py 用 Pydantic 定义配置结构(Config 根对象),声明字段类型和默认值,自动校验不合法配置
loader.py 负责加载 config.json → 版本迁移 → Pydantic 验证 → 返回 Config 对象
paths.py 所有路径从 config.json 所在目录派生(data、cron、media、logs、workspace 等)

Config 根对象结构

1
2
3
4
5
6
Config
├── agents: AgentDefaults # model、workspace、temperature、max_tokens、context_window_tokens、timezone
├── channels: ChannelsConfig # Telegram/Slack/Discord/飞书等渠道配置
├── providers: ProvidersConfig # 所有 LLM 提供商的 api_key / api_base
├── gateway: GatewayConfig # HTTP 网关设置
└── tools: ToolsConfig # 工具安全限制(deny_patterns 等)

关键机制

机制 说明
Pydantic 校验 声明字段类型,写入时自动检测类型错误(如字符串填到整数字段)
环境变量覆盖 NANOBOT_ 前缀 + __ 分隔嵌套层级,如 NANOBOT_AGENTS__MODEL=gpt-4o
版本迁移 _migrate_config() 处理旧版配置字段位置变更
多实例支持 通过 --config 参数指定不同配置文件,全局变量 _config_path 记录当前实例路径
Provider 自动匹配 Config 初始化时调用 _match_provider(),按 5 级优先级为模型名找到对应 provider
路径派生 所有数据目录从 get_config_path().parent 派生,无硬编码路径

config.json 维护方式:用户手动编辑,Agent 自身没有修改配置的工具。


12. 会话管理(Session)⭐

核心思想:按 channel:chat_id 隔离对话历史,持久化到 JSONL,append-only 保证 LLM prompt cache 命中。

两个类

职责
Session 持有 messages 列表,提供裁剪、清空、合法性修正
SessionManager JSONL 读写、内存 cache、生命周期管理

get_or_create 三种情况

情况 处理
内存 cache 命中 直接返回
磁盘有文件 _load() 恢复,含游标
都没有 新建空 Session

隔离规则:key 格式 channel:chat_id,不同用户或平台各自独立。唯一共享的是 Agent 级别的 MEMORY.md / HISTORY.md


last_consolidated 游标

messages[0:last_consolidated] 已压缩进 MEMORY.md / HISTORY.md,游标持久化在 JSONL 第一行,重启后恢复。

1
2
3
4
5
6
messages: [msg_0 ... msg_49 | msg_50 ... msg_99]

last_consolidated = 50
get_history() 只返回右侧部分

msg_0~49 已压缩进 MEMORY.md(摘要),不再原文给 LLM

LLM 每次收到的完整上下文

1
2
3
4
5
6
7
8
9
10
11
┌─ system prompt ──────────────────────────────┐
│ identity + MEMORY.md摘要 + HISTORY.md + skills │
└──────────────────────────────────────────────┘
+
┌─ get_history() ──────────────────────────────┐
│ messages[last_consolidated:] │
│ → 最后 N 条 → 从第一条 user 开始 │
│ → 丢弃孤儿 tool result │
└──────────────────────────────────────────────┘
+
本次用户新消息

孤儿 tool result:截断后 window 开头是 tool result,但对应 assistant tool_calls 已被截掉,provider 会拒绝。_find_legal_start() 扫描找合法起始点,丢弃孤儿。


append-only 与 LLM Cache

是一个LLM层的机制,但实际底层就是KV Cache,来避免前面重复计算:provider 对消息前缀做 hash,前缀不变则复用缓存。

因此传过去的消息,只追加不修改 → 前缀稳定 → cache 命中。

1
2
3
4
5
6
7
8
9
10
11
第1次 [A,B,C]      → 缓存前缀
第2次 [A,B,C,D] → 前缀命中,只算 D
第3次 [A,B,C,D,E] → 前缀命中,只算 E

截断后:
第4次 [C,D,E,F] → 缓存失效,全量计算,缓存新前缀
第5次 [C,D,E,F,G] → 新前缀命中,只算 G

Memory Consolidation:
[A,B,C] 压缩进 MEMORY.md → last_consolidated 前移
messages 不动 → 缓存前缀不受影响

13. 事件总线(Bus)

核心思想:两个 asyncio.Queue(inbound / outbound),所有需要触达 AgentLoop 的入口统一走 inbound queue,AgentLoop 回复走 outbound queue。各方只和 Bus 交互,互不认识。

1
2
3
4
5
6
7
8
9
10
Channel / SubAgent / Cron
→ publish_inbound(msg)

[inbound queue]

AgentLoop.consume_inbound() → 处理 → publish_outbound()

[outbound queue]

channel.consume_outbound() → 发回用户

三类 inbound 来源

发送方 场景
Channel 用户发来消息
SubAgent 子 Agent 完成后通知主 Agent
Cron 定时任务触发

session_key_overrideInboundMessage 默认 session key 是 channel:chat_id,此字段可覆盖,用于 Slack thread 等需要在同一 chat_id 下隔离子会话的场景。


14. 集成层 Channels

核心思想:Channel 是平台适配层,统一把各平台消息转为 InboundMessage 塞进 Bus,互相完全隔离,只和 Bus 交互。

BaseChannel 最少实现

方法 职责
start() 连接平台,长期监听,收到消息调 _handle_message()
stop() 断开连接,清理资源
send(msg) 发送 OutboundMessage 给用户

可选:send_delta()(流式)、login()(交互登录)

_handle_message 流程:平台消息 → 白名单检查 → 封装 InboundMessage → bus.publish_inbound()

ChannelManager:读 config 决定启用哪些 Channel,统一启动,持续消费 outbound queue 分发回对应 Channel,失败按 1s/2s/4s 重试,流式 delta 合并后再发。

Registry 发现机制:内置(pkgutil 扫描目录)+ 插件(entry_points),内置优先。用户可通过第三方包注册插件,config.json 开启即用。

内置 Channel:Telegram、Slack、Discord、飞书、企业微信、微信、钉钉、QQ、Mochat、Matrix、Email、WhatsApp

不走 Channel 体系的入口

入口 说明
CLI 手动构建 Bus + AgentLoop,直接 publish_inbound
SubAgent 完成后直接 publish_inbound 通知主 Agent

15. 定时计划(Cron / Heartbeat)

Cron — 定时任务

LLM 通过 CronTool 增删查任务,持久化到 jobs.json,重启不丢失。三种调度方式:

kind 说明
at 指定时间点执行一次
every 固定间隔循环
cron 标准 cron 表达式,支持时区

执行时调 on_job 回调(AgentLoop 注入),到期后重新计算下次时间。

Heartbeat — 周期性唤醒

用户用自然语言在 HEARTBEAT.md 写任务条件(如”每天9点提醒喝水”),LLM 定期读取判断当前是否该执行。用 tool call 强制返回结构化 {action: skip/run},避免解析自由文本。有任务则跑完整 agent loop,结果值得通知时才发送。


16. CLI 入口与运行模式

两种启动模式

终端模式(nanobot agent 服务模式(nanobot gateway
交互入口 键盘终端输入 Telegram/Slack 等 Channel
面向用户 你自己 部署给多人使用
典型部署 本地 PC 服务器常驻
Channel 启动 ChannelManager
Heartbeat 启动 HeartbeatService

注:代码里命令名叫 gateway,与 Provider 层的”聚合平台 Gateway”(OpenRouter 等)是两个不同概念,此处称”服务模式”以作区分。

终端模式启动流程

1
2
3
4
5
6
7
_load_runtime_config()      → 加载 config.json
sync_workspace_templates() → 初始化 workspace 目录结构
MessageBus() → 创建事件总线
_make_provider() → 构建 LLM provider
CronService() → 初始化定时任务
AgentLoop() → 注入所有依赖,构建主运行时
键盘输入循环 → 直接 publish_inbound,读取回复打印

服务模式启动流程

1
2
3
4
5
6
7
8
9
_load_runtime_config()      → 加载 config.json
sync_workspace_templates() → 初始化 workspace 目录结构
MessageBus() → 创建事件总线
_make_provider() → 构建 LLM provider
SessionManager() → 初始化会话管理
CronService() → 初始化定时任务
AgentLoop() → 注入所有依赖,构建主运行时
ChannelManager() → 启动所有 Channel
HeartbeatService() → 启动心跳,常驻运行

多用户限制:单进程 asyncio,消息进 inbound queue 排队串行处理。小规模(几十人)够用,规模扩大时:

问题 原因
串行处理 用户A等LLM时,用户B的消息在排队
LLM 调用慢 每次几秒到几十秒,排队体验差
单点故障 进程崩了所有用户断连
无水平扩展 不能加机器分摊压力

项目本身不内置高并发方案,大规模需在外层自行添加负载均衡和多进程部署。


17. 沙盒机制

结论:这不是真正的沙盒。 没有独立沙盒模块,没有 Docker / seccomp / chroot 等操作系统级隔离,只是代码层面的路径检查和命令过滤。

机制 位置 作用
restrict_to_workspace config.json + ExecTool / FilesystemTool 文件读写和命令执行只能在 workspace 目录内
deny_patterns ExecTool 正则黑名单,阻止危险 shell 命令(如 rm -rf /

生产部署时官方推荐开启 restrictToWorkspace: true,但本质仍是应用层防护,绕过方式理论上存在。