多个 Agent 用不同模型(Opus / Sonnet / Haiku 等)在同一个群里吵吵闹闹,遵守世界书、
有导演调度、有开关键。核心引擎 transport-agnostic,首个落地面是多个 Telegram bot,
后续支持 FoundryVTT。设计论证见 spec_claude_20260707.md,
落地计划见 ~/.claude/plans/transient-discovering-wilkinson.md。
当前进度:M1 —— Telegram 多 bot 热闹群聊(在 M0 引擎骨架上加:Transport 抽象、 异步 Orchestrator 主循环、Director 调度、开关键、SQLite 持久化、基础 compaction、 Telegram 落地面)。M0 骨架(Model Gateway / 分层 Prompt Builder / 单角色单调用多气泡 + 记忆增量)继续沿用。
# 跑测试(uv 临时装 pytest + anthropic)
uv run --with pytest --with anthropic python -m pytest -q
# 离线回放演示(MockGateway,无需 API key,可复现)
uv run --with anthropic python scripts/replay_demo.py --turns 6
# 真实调用(需 ANTHROPIC_API_KEY,见 .env.example)
uv run --with anthropic python scripts/replay_demo.py --live --turns 6离线回放的日志会打印每回合的 cache_read / cache_creation:首回合冷启动写缓存,
其后 warm 回合命中缓存读——这正是分层 Prompt 布局要验证的目标。
# 1) BotFather 建 1 个观察者 bot + 每个角色各 1 个 bot;观察者务必 /setprivacy → Disable
# 2) 把所有 bot 拉进同一个群,拿到 chat_id;填好 .env(见 .env.example)与预设 JSON
# 3) 运行(懒加载 python-telegram-bot)
uv run --with anthropic --with python-telegram-bot \
python scripts/run_telegram.py --preset examples/room.example.json群里 /pause 暂停自动 chatter、/resume 恢复、/stop 停机;人类照常插话即被摄入调度。
摄入与发送分离:观察者 bot 收群里人类消息形成唯一共享历史,角色 bot 只负责发
(Telegram 规则下 bot 收不到别的 bot 的消息,角色发言由引擎直接入历史,天然不重复)。
| 子包 / 模块 | 职责 |
|---|---|
domain/ |
领域层:types.py(WorldBook/Agent/RoomState/TurnResult/PacingConfig ...)+ markers.py(控制标记词表) |
gateway/ |
Model Gateway:base.py(协议+辅助)、anthropic_gateway.py、openai_gateway.py(含兼容端点)、gemini_gateway.py、router.py(按 model_id 分发)、factory.py(按 key 装配)、mock.py(模拟前缀缓存) |
prompt/ |
builder.py:分层 Prompt 组装 + 显式 cache_control 断点 |
engine/ |
运行层:parsing.py(多气泡+记忆增量)、pacing.py(气泡节奏)、turn.py(发言回合)、compaction.py(历史压缩) |
director/ |
调度层:rule.py(RoundRobin,离线)、model.py(ModelDirector,Haiku 决定谁说话) |
transport/ |
收发边界:base.py(Transport 协议)、memory.py(测试)、telegram.py(M1 落地,懒加载 ptb) |
persistence/ |
store.py:SQLite 会话状态(历史去重 / 记忆快照 / 摘要) |
runtime/ |
编排层:orchestrator.py(异步 tick 主循环)、switch.py(开关键) |
presets.py |
房间预设加载(手写世界书 + 角色卡,见 examples/room.example.json) |
config.py / logging_setup.py |
跨层基础设施:配置与日志 |
常用符号在顶层再导出:from aichatgroup import Agent, Orchestrator, ModelDirector, Store, load_preset。
Orchestrator 用 asyncio 把两条协程跑在一起:_ingest_loop(摄入人类/外部消息,按
external_id 去重入库)与 _speak_loop(Director 选下一个说话者 → 组装 prompt → 调模型 →
按气泡节奏 typing+发送 → 记忆增量入库)。只有网络调用 gateway.complete 下放线程池,
prompt 组装/解析/历史读写全在事件循环线程内完成,两条协程对 RoomState 无并发竞争。
Telegram 只是实现了 Transport 协议的一层薄适配;Foundry(M5)复用同一接口。
gateway/ 是多套适配器 + 一个路由器。规范线格式是「Anthropic 形状」(system block +
cache_control 断点),每个非 Anthropic 适配器把它翻译成自家格式(丢掉断点、只取文本):
| 适配器 | 覆盖 | 缓存字段来源 |
|---|---|---|
AnthropicGateway |
claude-* |
显式断点,usage.cache_read/creation |
OpenAIGateway |
gpt-*/o1..o4,及任意 OpenAI 兼容端点(DeepSeek/Groq/OpenRouter/本地 vLLM,靠 base_url) |
自动前缀缓存,prompt_tokens_details.cached_tokens |
GeminiGateway |
gemini-* |
usage_metadata.cached_content_token_count |
模型选择的规范形式是 provider_alias#model,别名与模型名解耦:
anthropic#claude-opus-4-8 openai#gpt-4o gemini#gemini-2.0-flash
deepseek#deepseek-chat # deepseek 是自定义的兼容端点别名
RouterGateway 只看 # 前的别名分发(换 provider 只改别名、不动模型名;同一模型名可挂在
不同端点上互不影响),自身也实现 ModelGateway 协议 → engine / Orchestrator 完全不用改。
不带 # 的裸模型名仍按前缀推断路由(向后兼容)。于是角色卡里给不同角色写不同 model_id 即可混用多家。
新增一个 provider(声明式):写一个 providers.json(放项目根自动加载,或 AICG_PROVIDERS_FILE
指定;见 examples/providers.example.json),每个 provider 一个块——正是你要的形状:
{ "providers": [
{ "alias": "deepseek", "type": "openai",
"provider_url": "https://api.deepseek.com/v1", "api_key_env": "DEEPSEEK_API_KEY" }
] }type = openai/anthropic/gemini(openai 覆盖一切兼容端点),provider_url 是端点地址,
api_key_env 指向 .env 里的密钥变量名(密钥不进配置文件)。预设 JSON 也可内嵌同样的 providers
数组。装配是懒实例化:某家 key 在环境里、但没装它的 SDK、又从没路由到它时,不会拖垮启动——
只有真正拿它发消息才会因缺包报错。内置 anthropic/openai/gemini 仍由标准 key 自动登记。
system:
[第0层 世界观圣经 + 群聊规则] ← cache breakpoint 1
[第1层 长期摘要 + 客观关系图谱] ← cache breakpoint 2
messages:
[第2层 共享历史,逐条 user 消息,append-only]
└─ 最后一条历史消息 ← 滚动 cache breakpoint 3
[第3层尾部 人设 + 私有记忆 + 导演指令 + 输出契约] ← 不缓存
共享块前置、人设沉尾,使 system + 历史 前缀对所有 Agent 逐字节相同 → 命中同一缓存条目。
硬规则:永远不让一个模型生成不归它管的角色内容。
被点名的角色一次调用输出 1~3 条气泡,相邻两条之间用 {{SEPARATOR}} 分隔;可选在末尾追加
{{MEMORY}} + 一段 JSON 作为记忆增量,引擎在外部合并进该角色的私有快照。控制标记词表集中在
markers.py,解析对大小写/空白容忍,并剥掉模型误补的闭合标记
({{/MEMORY}} 甚至旧式 </MEMORY>);后续将由预设配置驱动。用 {{…}} 而非 <<…>> 是因为
尖括号会诱发模型「XML 要闭合」的本能弄脏 JSON,双花括号既避开这点、又与 SillyTavern 宏一致。
Prompt 结构方向:PromptBuilder 会逐步向 SillyTavern 预设结构靠拢(命名 prompt 片段 + 顺序/开关 + marker 占位 + 深度注入,见
preset/example.json),并支持导入 SillyTavern 预设 来控制模型行为——预设不替代 Builder,只提供结构与文案。当前四层组装是该模型的一个特例。
气泡之间的停顿由 pacing.py 推断,结果放在 TurnResult.pauses,
供 M1 的 Telegram 发送层(typing 提示 + sleep)消费。两条来源:
- 模型显式:分隔符可带秒数
{{SEPARATOR:2}}; - 缺省推断:无显式值时按下一条气泡长度估算。
原则:一切涉及推断的行为都要留有与角色性格接驳的口子——把可调 factor 存进角色设定。
停顿是第一个落点:每个 Agent 带一个 PacingConfig(per_char_s / explicit_scale 等),
急性子停得短、慢性子停得长。后续新增的推断行为应循同一模式挂到角色上。
M0 引擎骨架✅M1(MVP) Telegram 多 bot 热闹群聊:Transport 抽象、异步 Orchestrator、Director 调度器、 手写世界书/角色卡、开关键、SQLite 持久化、基础 compaction✅ (待办:BotFather 实机冒烟——建 bot、关 privacy、进群跑一轮)- M2 Storyteller(编导/压力源)+ 更强记忆。
- M3 知识隔离。 M4 世界书生成 + RAG。 M5 FoundryVTT 支持。