LLM 集成
Provider 抽象、提示詞緩存、模型路由
Minara Agent 通過統一的內部接口對接多個 LLM 服務商。 在 Anthropic、OpenAI、OpenRouter 或本地模型之間切換, 無需改動 Agent 循環。 本頁介紹 provider 層的結構、提示詞緩存的組織方式,以及模型路由的工作原理。
為什麼用自定義路由器,而不直接調用各廠商 SDK? Agent 循環、回測、反思、子 Agent 等功能對成本和延遲的要求各不相同。 Minara 的路由器可以在同一代碼庫中,將需要穩定緩存的高成本任務路由到 Sonnet, 將低成本批量評估路由到 Haiku;當某個 provider 觸發限流時也能優雅降級。 直接綁定單一廠商 SDK,會將你鎖定在其定價體系中。
實際使用:通過環境變量
(ANTHROPIC_API_KEY、OPENAI_API_KEY、OPENROUTER_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.ts | Anthropic | ANTHROPIC_API_KEY |
anthropic-oauth.ts | Anthropic(Claude.ai) | OAuth refresh token |
anthropic-wire.ts | 共享 wire 協議 | |
openai-wire.ts | OpenAI / OpenRouter | OPENAI_API_KEY / OPENROUTER_API_KEY |
openrouter.ts | OpenRouter 路由器 | OPENROUTER_API_KEY |
select-provider.ts
在啟動時按優先級順序,根據環境變量選擇具體客戶端:
- Anthropic OAuth(
CLAUDE_CODE_OAUTH_TOKEN已設置時) - Anthropic API key(
ANTHROPIC_API_KEY已設置時) - OpenRouter(
OPENROUTER_API_KEY已設置時) - 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 },
]提示詞構建器強制執行以下不變式:
- 可緩存塊靠前排列。 非可緩存塊之後的內容無法緩存,因為緩存鍵採用嚴格的前綴匹配。
- 可緩存塊保持穩定。 identity 和 catalog 僅在技能註冊表或基礎提示詞變更時才會更新,不會按輪次變化。
- 動態內容置於末尾。 活躍技能片段、信號上下文、待確認項以及對話歷史,均放在緩存邊界之後。
anthropic-wire.ts
將 SystemPromptBlock[] 映射為 Anthropic 的
cache_control: {type: "ephemeral"} 標記,
並在響應元數據中追蹤緩存命中率;
結構化日誌中對應字段為
llm.cache_read_input_tokens 和 llm.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/:
/auth/anthropic/init啟動 PKCE 流程,返回授權 URL。- 用戶訪問該 URL,授權後被重定向回應用。
/auth/anthropic/exchange用授權碼換取 refresh token。- Refresh token 使用本地密鑰加密後存入 SQLite。
- 每次 LLM 調用在需要時會自動透明地刷新 access token。
OpenAI 和 OpenRouter 採用相同模式。 具體路由請參閱鑑權接口參考文檔。
新增 Provider
接入新的 LLM provider:
- 在
apps/agent/src/llm/<name>-wire.ts中實現LLMClient。 提示詞緩存支持是可選項,但強烈建議實現。 - 在
select-provider.ts中添加對應的選擇分支。 - 將環境變量添加到
.env.example及 環境變量清單。 - 在
tests/integration/llm/下編寫集成測試, 使用tests/fakes/llm/中的 wire 層 fake。
不要修改 Agent 循環來添加 provider 專屬代碼路徑。 若某個 provider 有特殊行為,應將其封裝在 wire 層內部處理。