MINARA

LLM 集成

Provider 抽象、提示词缓存、模型路由

Minara Agent 通过统一的内部接口对接多个 LLM 服务商。 在 Anthropic、OpenAI、OpenRouter 或本地模型之间切换, 无需改动 Agent 循环。 本页介绍 provider 层的结构、提示词缓存的组织方式,以及模型路由的工作原理。

为什么用自定义路由器,而不直接调用各厂商 SDK? Agent 循环、回测、反思、子 Agent 等功能对成本和延迟的要求各不相同。 Minara 的路由器可以在同一代码库中,将需要稳定缓存的高成本任务路由到 Sonnet, 将低成本批量评估路由到 Haiku;当某个 provider 触发限流时也能优雅降级。 直接绑定单一厂商 SDK,会将你锁定在其定价体系中。

实际使用:通过环境变量ANTHROPIC_API_KEYOPENAI_API_KEYOPENROUTER_API_KEY)配置 provider。

Provider 接口

所有 provider 均实现 apps/agent/src/core/agent-loop.ts 中的 LLMClient

interface LLMClient {
  createMessage(params: {
    model: string;
    system: SystemPrompt;           // string 或可缓存块
    messages: MessageParam[];
    tools?: ToolDefinition[];
    maxTokens?: number;
    stream?: boolean;
  }): Promise<LLMResponse>;

  streamMessage?(params: ...): AsyncIterable<LLMStreamEvent>;
  vision?(call: LLMVisionCall): Promise<LLMVisionResult>;
}

具体实现位于 apps/agent/src/llm/

文件Provider鉴权方式
anthropic-api-key.tsAnthropicANTHROPIC_API_KEY
anthropic-oauth.tsAnthropic(Claude.ai)OAuth refresh token
anthropic-wire.ts共享 wire 协议
openai-wire.tsOpenAI / OpenRouterOPENAI_API_KEY / OPENROUTER_API_KEY
openrouter.tsOpenRouter 路由器OPENROUTER_API_KEY

select-provider.ts 在启动时按优先级顺序,根据环境变量选择具体客户端:

  1. Anthropic OAuth(CLAUDE_CODE_OAUTH_TOKEN 已设置时)
  2. Anthropic API key(ANTHROPIC_API_KEY 已设置时)
  3. OpenRouter(OPENROUTER_API_KEY 已设置时)
  4. OpenAI(OPENAI_API_KEY 已设置时)

四项均未配置时,进程将拒绝启动并输出明确报错信息。 选择逻辑在 app.ts 中只有一行;下游全部通过 LLMClient 通信。

提示词缓存:不可省略

Anthropic 提示词缓存的 TTL 为 5 分钟,命中缓存时费用仅为正常输入 token 价格的 10%。 对于每轮都会发起大量相似调用的 Agent(每次工具调用往返都需要 catalog 加 identity), 缓存命中率决定了成本是"可接受"还是"过于昂贵"。

因此,系统提示词被拆分为若干显式块:

system: [
  { type: "text", text: identityPrompt,      cache: true  },
  { type: "text", text: skillCatalog,        cache: true  },
  { type: "text", text: activeSkillPrompts,  cache: false },
  { type: "text", text: signalContextBlock,  cache: false },
  { type: "text", text: pendingConfirmation, cache: false },
]

提示词构建器强制执行以下不变式:

  1. 可缓存块靠前排列。 非可缓存块之后的内容无法缓存,因为缓存键采用严格的前缀匹配。
  2. 可缓存块保持稳定。 identity 和 catalog 仅在技能注册表或基础提示词变更时才会更新,不会按轮次变化。
  3. 动态内容置于末尾。 活跃技能片段、信号上下文、待确认项以及对话历史,均放在缓存边界之后。

anthropic-wire.tsSystemPromptBlock[] 映射为 Anthropic 的 cache_control: {type: "ephemeral"} 标记, 并在响应元数据中追踪缓存命中率; 结构化日志中对应字段为 llm.cache_read_input_tokensllm.cache_creation_input_tokens

对于不支持提示词缓存的 provider(如撰写本文时的 OpenAI), wire 层会静默地将所有块拼接为单一系统字符串。 调用方无感知,行为也不会产生差异。

模型选择

每轮使用的模型由 apps/agent/src/learning/llm-router.ts 决定(注意:此路由器与技能路由器无关,专门负责将 LLM 调用路由到具体模型)。 默认策略如下:

  • 主 Agent 轮次:Claude Sonnet 4.6,上下文窗口 1M。
  • 深度研究子 Agent:Claude Opus 4.6(高推理能力)。
  • 视觉调用:选用支持视觉能力的 provider。
  • 向量嵌入:通过专用环境变量 EMBEDDING_PROVIDER 指定。

每次调用都会将 {provider, model, reason} 写入审计日志, 可直接用 SQL 查询回答"为什么这轮花费了 X",无需逐层排查代码。

覆盖方式:

  • AGENT_MODEL 固定主 Agent 循环使用的模型。 也可通过 minara config set model.defaultModel <id>/model 斜杠命令交互式设置, 两者均会持久化到 $MINARA_DATA_DIR/env

工具调用格式

Anthropic 和 OpenAI 均支持工具调用,但 wire 格式不同。 Agent 循环内部采用 Anthropic 的格式, openai-wire.ts 负责双向转换。 具体影响如下:

  • 工具 schema 只定义一次,以 JSON Schema 形式存于 ToolEntry.schema.parameters,两个 wire 层共用同一份 schema。
  • 停止原因已归一化。 Anthropic 的 "end_turn" / "tool_use" / "max_tokens" 与 OpenAI 的 "stop" / "tool_calls" / "length" 均映射为循环消费的统一枚举值。
  • 流式事件已归一化为统一的 LLMStreamEvent 联合类型, /chat/stream 无需感知上游 provider。

视觉能力

视觉调用通过独立的 LLMClient.vision() 方法处理, 因为并非所有 provider 都支持在工具调用中内联视觉能力。 vision_analyze 工具会显式调用该方法; 需要读取截图的工具(browser.* 工具集)会先将图片编码为 base64 再发起调用。

Anthropic OAuth 流程

Anthropic OAuth 允许用户使用 Claude.ai 账号登录,无需提供 API key。 相关流程位于 apps/agent/src/llm/oauth/

  1. /auth/anthropic/init 启动 PKCE 流程,返回授权 URL。
  2. 用户访问该 URL,授权后被重定向回应用。
  3. /auth/anthropic/exchange 用授权码换取 refresh token。
  4. Refresh token 使用本地密钥加密后存入 SQLite。
  5. 每次 LLM 调用在需要时会自动透明地刷新 access token。

OpenAI 和 OpenRouter 采用相同模式。 具体路由请参阅鉴权接口参考文档。

新增 Provider

接入新的 LLM provider:

  1. apps/agent/src/llm/<name>-wire.ts 中实现 LLMClient。 提示词缓存支持是可选项,但强烈建议实现。
  2. select-provider.ts 中添加对应的选择分支。
  3. 将环境变量添加到 .env.example环境变量清单
  4. tests/integration/llm/ 下编写集成测试, 使用 tests/fakes/llm/ 中的 wire 层 fake。

不要修改 Agent 循环来添加 provider 专属代码路径。 若某个 provider 有特殊行为,应将其封装在 wire 层内部处理。

本页目录