多LLM模型自由切换:capsule-react注册表解析、优先级路由与"绝不伪造模型"原则
【免费下载链接】capsule-reactReAct loop coordinator. Stateless state machine for reasoning-and-action cycle. Part of Unicity AOS.项目地址: https://gitcode.com/gh_mirrors/ca/capsule-react
在 Astrid OS 的 AI 智能体架构中,capsule-react(包名astrid-capsule-react)是驱动"推理—行动"循环的核心协调胶囊。它本身不调用任何大模型,却决定了每个请求由哪个 LLM 提供商、哪个模型来回答——通过注册表解析、两级优先级路由和缓存同步机制,你可以在 Anthropic、OpenAI 兼容端点(如 Ollama 的qwen2.5-coder:32b、llama3.3:70b)之间自由切换,而它保证:没有可选模型时宁可终止回合,也绝不伪造一个模型 ID。
一句话理解:它是"调度员",不是"答题者"
capsule-react 是一个无状态状态机,通过 IPC 事件总线协调五个协作胶囊:Session(会话历史)、Identity(系统提示词)、Prompt Builder(提示组装)、Provider(LLM 流式生成)、Tool Router(工具分发)。它的状态机循环如下:
Idle → AwaitingIdentity → AwaitingPromptBuild → Streaming → AwaitingTools → Streaming → … → Idle
(状态机定义见 Phase 枚举,整体架构说明见 README.md)
正因为模型选择与推理逻辑解耦,切换 LLM 只需改变"发往哪个 topic、携带哪个 model 字段",其余胶囊一行代码都不用动。
注册表解析:一个请求如何找到它的模型
每次要发出 LLM 请求前,react 都会调用active_llm()解析"当前生效的提供商 + 模型",其解析顺序是四层兜底(实现见 active_llm()):
| 优先级 | 来源 | 说明 |
|---|---|---|
| 1️⃣ | 本主体(per-principal)KV 缓存 | 由注册表广播预热,命中则零 IPC 开销 |
| 2️⃣ | 运维环境变量覆盖 | 仅携带 topic,不携带模型 ID |
| 3️⃣ | 懒加载:向注册表胶囊查询 | 通过registry.v1.get_active_model往返,5 秒超时,结果回写缓存 |
| 4️⃣ | 合理默认 topic | 仅指向 Anthropic topic,模型为None |
注意一个关键细节:缓存键react.llm_provider_model是按主体隔离的,"一个主体的模型选择不会钉死其他所有主体"(KV 键定义)。注册表查询通道在 Capsule.toml 中声明,响应通道在 subscribe 配置 中声明。
解析函数 parse_active_provider 只认两种输入:request_topic(必须以llm.v1.request.generate.前缀开头,否则视为伪造直接拒绝)+id(选中的模型 ID)。模型 ID 对它而言是不透明字符串——冒号包裹的llama3.3:70b也会原样透传,react 从不拆解或改写它。
优先级路由:单次请求覆盖可以"赢过"注册表
真正发出请求前,resolve_request_model()再做一次两级优先级裁决(源码):
- 单次请求覆盖:网关随 prompt 传来的
context.model,仅对当前回合生效; - 注册表选择:
ProviderEntry.id。
每一级都会跳过空串与纯空白值,空白覆盖永远不会"影子屏蔽"一个可用的注册表 ID。
这个覆盖的生命周期被精心设计(TurnState 字段):
- ✅ 同一回合内经历多次"工具调用 → 继续生成"迭代时,覆盖持续生效;
- ✅ 用户发出新 prompt 时,
reset_conversation_turn()将其清空,绝不泄漏到下一轮对话(重置逻辑,回归测试 override_cleared_on_new_turn)。
所以你可以做到:全局用注册表选的模型 A,个别请求临时指定模型 B,回合结束后一切自动回到 A。
"绝不伪造模型":没有选择时的终止行为
这是 capsule-react 最值得称道的设计原则。旧实现曾有一个字面量兜底(历史上是一个 Claude 模型 ID)——这很危险:把一个 Claude ID 盖在非 Anthropic 提供商的请求上,会产生诡异的失败。
现在,当两级优先级都解析不出可用模型时:
- 解析结果为
None→ 触发 fail_no_model_selected: - 向用户推送一条可操作的错误:"No LLM model is selected. Run
astrid modelsto choose one, or install/configure an LLM provider." - 清理在途映射、重置回合、回到
Idle,不发出一条llm.v1.request.generate.*请求; - 空 ID、纯空白 ID 一律视为"不存在",而不是"空模型名"(回归测试 whitespace_only_model_id_is_absent_across_paths)。
由于注册表在安装任意提供商时会自动选择默认提供商,None意味着真的什么都没有可用——此时终止回合是唯一诚实的行为。
缓存一致性:切换模型时,缓存必须"成组搬家"
多模型切换最容易出的 bug 是"新 topic 配旧模型"。react 的 handle_model_changed 拦截器订阅registry.v1.active_model_changed广播,处理三种信号(决策函数):
| 广播载荷 | 动作 | 场景 |
|---|---|---|
携带合法 topic +id | 写入 topic 与模型 ID,并缓存上下文窗口/最大输出 token | 正常切换 |
合法 topic 但无id | 写入 topic,删除旧模型 ID | 迁移窗口,防止新 topic 配旧模型 |
裸 JSONnull | 删除全部 4 个缓存键 | astrid models unset清除选择 |
那 4 个必须成组清除的键由 cleared_cache_keys 统一列出——上下文窗口限制若不一起删掉,新提供商会继续被旧提供商的 token 预算钳制。此外,任何前缀不匹配的 topic(如evil.topic)都会走RejectTopic分支被拒之门外,形成与懒加载路径对称的纵深防御(测试见 active_provider_rejects_bad_prefix_topic)。
快速上手与延伸阅读
📌用户视角:用astrid models系列命令选择/清除模型,react 侧无需任何配置;网关侧可通过请求上下文里的model字段做单请求级覆盖。
📖 想深入源码,建议按这条线索阅读 src/lib.rs:
- KV 缓存键定义 —— 理解"缓存了什么";
- resolve_request_model —— 优先级路由全貌;
- fail_no_model_selected —— "绝不伪造"的终止路径;
- fetch_active_llm_topic_from_registry —— 注册表懒加载与缓存回填。
构建与部署相关配置见 Cargo.toml 与 Capsule.toml(构建命令见 README.md)。整套选择逻辑由 tests 模块 中的十余个回归测试钉死,包括空 ID、空白 ID、伪造 topic、清除广播等边界场景——这正是"多模型自由切换"能放心运行的底气所在。
【免费下载链接】capsule-reactReAct loop coordinator. Stateless state machine for reasoning-and-action cycle. Part of Unicity AOS.项目地址: https://gitcode.com/gh_mirrors/ca/capsule-react
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考