ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

ai-memory 的 LLM Provider 有序故障转移链:从 `llm_fallbacks` 配置到熔断与健康观测

ai-memory 的 LLM Provider 有序故障转移链:从 `llm_fallbacks` 配置到熔断与健康观测 ai-memory 的 LLM Provider 有序故障转移链从llm_fallbacks配置到熔断与健康观测【免费下载链接】ai-memorySolution for long term memory for agent coding CLIs and to facilitate handoff between different agent vendors项目地址: https://gitcode.com/GitHub_Trending/ai/ai-memory导读ai-memory 通过一个Arcdyn LlmProvider抽象来驱动 bootstrap、consolidation、lint、review 等所有 LLM 依赖操作。当单一上游发生限流或宕机时即使系统里还配置着另一个健康的 provider任务依然会降级失败。本文围绕设计文档 docs/llm-provider-fallback.mdissue #648展开结合其在仓库中的完整实现讲解如何通过[[llm_fallbacks]]配置一条有序的、opt-in 的 provider 故障转移链只对瞬时故障429 / 5xx / 超时 / 连接错误切换候选同时严格保留原始请求、JSON schema 与逻辑操作 id并通过进程内熔断和被动健康上报让故障转移结果可观测。读完本文你将掌握该链路的完整配置语法、失败判定策略、熔断语义以及从源码到测试的验证路径。问题背景为什么单一 provider 的 retry 不够在引入故障转移之前服务端为每个 LLM 操作只构造一个Arcdyn LlmProvider。LlmProvidertrait 定义在 crates/ai-memory-llm/src/provider.rs是整个工作区依赖的唯一 LLM 抽象暴露四个入口点complete普通文本补全complete_with_operation_id携带既有逻辑操作 id 的文本补全complete_structured_raw受 JSON schema 约束的结构化补全原始 schema 进、原始值出complete_structured_raw_with_operation_id结构化补全 操作 id。原有的 retry 循环可以对同一个 provider重试瞬时失败却无法在重试之间切换到第二个已配置的 provider。因此一次上游限流或宕机即使另一个 provider 完全健康bootstrap、consolidation、lint、review 等操作也会一并降级。这正是设计文档开篇定义的 Problem。设计目标与非目标文档明确了五个 Goals仅在瞬时失败后切换沿用既有的LlmError::is_transient()判定策略保留请求原貌每个候选尝试都携带原始 request、JSON schema 与逻辑操作 id凭证安全凭证仍在一次性配置加载中解析不进入日志、status 载荷或持久化状态无全局超时策略每个候选使用各自配置的请求超时可观测通过被动的 provider-health 上报暴露“选中了哪个候选、是否发生过故障转移”。同时明确 Non-goals不为任意第三方 API 或 Command Code 集成做路由、不从其他应用数据库读凭证、不安装 provider CLI、不重试确定性失败鉴权、非法请求、不支持的 schema、畸形响应第一版实现也不改动 embeddings、hook 延迟、wiki/存储行为或 MCP schema。配置形态config.toml中的[[llm_fallbacks]]顶层 LLM 字段llm_provider/llm_model仍是主 profile故障转移链通过 TOML 数组段配置llm_provider opencode llm_model mimo-v2.5-free [[llm_fallbacks]] provider openai-compat model poolside/laguna-s-2.1-free base_url http://127.0.0.1:49375/v1 api_key_env AI_MEMORY_LOCAL_ROUTER_TOKEN [[llm_fallbacks]] provider gemini model gemini-3.5-flash api_key_env GEMINI_API_KEY配置要点均可在源码中得到印证provider与llm_provider相同的 wire 名称集合。在 crates/ai-memory-cli/src/config.rs 的fallback_provider_config中通过provider_choice_from_str解析可选值包括anthropic、openai、gemini、openai-compat、openai-oauth、copilot、anthropic-oauth、opencode。无法识别的值直接报llm_fallbacks[i].provider... is not one of ...启动即失败。model候选模型 id必须非空llm_fallbacks[i].model must not be empty。base_url可选。对openai-compat是必填源码build_provider对缺失 base_url 返回LlmError::NotConfigured。从实现看openai-compat与opencode是仅有的两个“端点由运维决定”的方言ProviderChoice::endpoint_is_operator_chosen()其余 provider 都指向固定厂商主机。api_key_env环境变量名而非凭证明文——这是设计文档强调的“profile 携带 base URL 与凭证环境变量名、不把凭证值写进 config”。对需要 API key 的 provider如gemini需要GEMINI_API_KEY必须显式设置仅当 provider 本身有原生凭证来源OpenAI OAuth、Copilot、Anthropic OAuth时可以省略。启动期即校验绝不留下“潜伏的故障转移”设计文档强调loader 校验每个 profile 并一次性解析全部凭证材料凭证缺失、provider 非法或 profile 畸形都应在启动时失败而不是等故障发生时才发现备份不可用。这一点在 crates/ai-memory-cli/src/config.rs 的Config::load中有完整实现遍历每个llm_fallbacks条目读取api_key_env指向的环境变量缺失即bail经fallback_provider_config校验后立即调用build_provider构造并丢弃只为让畸形配置在启动阶段暴露。尤其值得注意的细节fallback_provider_auth与主 provider 的provider_auth刻意区分。主 provider 在未显式指定 key 时可以回退到其固定的环境变量如ANTHROPIC_API_KEY而 fallback profile 绝不隐式继承主 provider 的凭证——否则一个漏写api_key_env的 profile 会悄悄复用主 provider 的 key从而绕过“启动即失败”的校验。原生凭证源OAuth/Copilot天然是进程级的仍与主 provider 共享。语义append-only 有序链暂无环境变量简写初始语义是 append-only主 provider 先跑随后按声明顺序尝试 fallbacks。设计文档明确不新增环境变量简写直到能无歧义地编码 profile 边界与凭证名——源码中也注释了 figment 无法从AI_MEMORY_*环境变量往返VecStruct且把多个 profile 的凭证名编码进单一环境变量会产生歧义。因此[[llm_fallbacks]]只能写在config.toml中。实现边界FallbackLlmProvider与薄 CLI设计文档给出的数据流为Config::load - primary ProviderConfig fallback ProviderConfig values - build_provider for each value - FallbackLlmProvider(VecCandidate) - existing Arcdyn LlmProvider consumers实现位于 crates/ai-memory-llm/src/fallback.rsCandidate链上的单个候选只保存providerstatic str、modelString与已构造的Arcdyn LlmProvider——绝不持有原始配置或凭证。build_provider定义在 crates/ai-memory-llm/src/factory.rs依旧是每个候选唯一且仅有的构造路径。FallbackLlmProvider持有VecCandidateEntry每个 entry 额外带一个MutexCandidateState记录成功/失败时间戳、错误类别与熔断截止并实现LlmProvider的全部四个入口点。每个方法都按声明顺序遍历候选circuit_open(i)为真则跳过调用candidate.inner.same_method成功则record_success返回失败则记录错误若是瞬时错误继续下一个否则立即返回。结构化调用把未改动的 schema 传给每个候选operation-aware 调用把同一个LlmOperationId传给每个候选——保持调用方 retry 与幂等语义不变。测试 fallback.rs 中的all_four_methods_preserve_request_schema_and_operation_id用ScriptedLlm双桩验证主候选瞬时失败后次候选必须看到完全相同的请求含max_tokens42、schema 与None / Some(op_id) / None / Some(op_id)的操作 id 序列。CLI 保持薄层Config::llm_provider_chain()config.rs返回None未配置 LLM、单个 provider无 fallback保持既有单 provider 行为或包装了[primary, fallbacks...]的FallbackLlmProvider。调用方拿到的仍然是OptionArcdyn LlmProvider。零-LLM 路径与既有单 provider 路径完全不受影响。在serve中装配点位于 crates/ai-memory-cli/src/commands/serve.rsllm_provider_chain()构造出 provider 后再由ProviderHealth::wrap_llm_provider包上被动健康记录器。而llm-test命令crates/ai-memory-cli/src/commands/llm_test.rs走build_provider直接构造单个 provider用于手动验证端点行为——这也呼应了文档交付序列第 4 步“针对先返回瞬时失败、再返回合法结构化补全的本地 OpenAI 兼容端点手动跑llm-test”。失败策略与熔断状态设计文档的失败分类表在实现中由LlmError::is_transient()crates/ai-memory-llm/src/error.rs精确承载失败类型切换下一候选理由429是既有瞬时策略5xx是既有瞬时策略超时 / 连接错误是既有瞬时策略400 / 401 / 403 / 404 / 422否通常是请求、能力、模型或凭证配置问题schema / 响应形状 / 反序列化错误否相同输入必然再次确定性失败源码中is_transient()的实现为Provider { status, .. }且status 429 || (500..599).contains(status)或Http(e)且e.is_timeout() || e.is_connect()其余Auth、Schema、Serde、UnexpectedShape、NotConfigured、AllCandidatesFailed一律非瞬时。注意它覆盖了 Cloudflare 风格的52x520/524在测试中显式验证为瞬时。设计文档与错误类型注释都强调调用方必须保持 retry短且有界几次尝试、间隔数秒这不是 tenacity 式 8–128 秒退避的许可——源码注释明确引用了 cognee #2840 的教训。熔断语义源码CandidateState.circuit_open_until候选被调用前先检查进程内熔断以(provider, model)为键瞬时错误将该候选的熔断打开一个有界冷却期CIRCUIT_COOLDOWN Duration::from_secs(30)30 秒其他候选不受影响一次成功响应立即关闭该候选的熔断测试a_success_closes_an_open_circuit_immediately验证即使把circuit_open_until人为推到 600 秒后一次record_success也会立刻清空冷却期自然过期也会关闭熔断无需等到下一次调用测试an_elapsed_cooldown_closes_the_circuit_without_an_explicit_success重启服务端清空所有熔断初始实现没有持久化熔断状态也没有独立的强制请求截止时间。测试an_open_circuit_skips_only_its_candidate精确验证主候选第一次瞬时失败后熔断打开冷却期内第二次调用主候选的调用计数保持 1完全跳过、不再尝试次候选计数递增。当所有合格候选都失败或所有候选的熔断都打开时wrapper 返回有界的聚合错误LlmError::AllCandidatesFailed { attempted, summary }只含 provider/model 标签与错误类别可附带 HTTP 状态绝不包含响应体或秘密。测试all_candidates_failing_returns_a_bounded_aggregate_error断言 summary 包含primary/m1、secondary/m2、500、503同时!summary.contains(secret body)。attempted计数排除被跳过的熔断候选。被动健康上报故障转移结果可观测设计文档要求“让选中的候选与故障转移结果通过被动 provider-health 上报可观测”同时强调 status 只记录服务端已经发起过的调用不去探测 fallback也不触发后台恢复流量。实现分两层ProviderHealthcrates/ai-memory-llm/src/health.rs保留了顶层llm/embedding两个 role 的既有快照字段兼容旧字段新增llm_candidates: VecCandidateHealth#[serde(default)]旧服务端响应也能被新 CLI 反序列化。ProviderHealth额外持有一个ArcMutexOptionArcdyn LlmProvider仅在snapshot()时读取candidate_health()——普通 provider 返回空FallbackLlmProvider返回有序候选列表。CandidateHealth每个候选包含provider 与 model 标签last_selected是否应答了最近一次完成的调用由last_selected: MutexOptionusize提供last_success_at/last_error_at时间戳脱敏的last_error_statusHTTP 状态与last_error_class来自LlmError::class()的稳定短标签如provider、http、auth、schema、serde、unexpected-shape、all-candidates-failedcircuit_open_until仅当冷却期确实未过期才上报避免把过期未重置的时间戳误读为“仍打开”。健康快照中的脱敏是彻底的只记录标签、时间戳、HTTP 状态与短错误类别永不记录响应体或凭证。candidate_health的测试断言format!({health:?})不包含must not leak。在status命令crates/ai-memory-cli/src/commands/status.rs中当report.providers.llm_candidates非空时会按声明顺序逐行渲染每个候选标签、是否应答了最近一次调用、最近成功/失败时间、脱敏错误类别与状态、熔断打开截止时间。运维人员无需主动探测即可从一次 status 调用中看到“当前实际由哪个候选在服务”“哪个候选最近失败过”“哪个候选正处于熔断冷却中”。测试策略从配置校验到链路语义设计文档列出了 7 类测试仓库中均已落地链序chain_tries_candidates_in_order_and_stops_at_first_success验证按声明顺序尝试并在首个成功处停止。四入口语义保持all_four_methods_preserve_request_schema_and_operation_id见上文。瞬时/确定性分流transient_statuses_advance_to_the_next_candidate对[429, 500, 502, 503, 504]逐一断言会推进deterministic_errors_stop_on_the_first_candidate对400/401/403/404/422/Schema/Serde/UnexpectedShape断言次候选调用数为 0 且错误类别原样传播。熔断an_open_circuit_skips_only_its_candidate、a_success_closes_an_open_circuit_immediately、an_elapsed_cooldown_closes_the_circuit_without_an_explicit_success。配置校验在 crates/ai-memory-cli/src/config.rs 的测试模块中通过load_with_toml直接喂 TOML 字符串不读真实主目录验证空 provider / 空 model / 未知 provider 均被拒openai-compat无 base_url 被拒api_key_env指向的环境变量缺失被拒fallback 不隐式继承主 provider 凭证load_rejects_a_fallback_that_would_otherwise_inherit_the_primary_credential以及无主 provider 时 fallback 仍合法llm_provider_chain返回None因为链只通过它被消费。健康快照candidate_health_reports_labels_last_selected_and_redacted_errors断言候选标签、last_selected、脱敏错误类别与熔断字段并验证渲染结果不含秘密。单 provider 兼容llm_provider_chain_is_the_plain_provider_when_no_fallback_is_configured验证无 fallback 时返回普通 provider 且candidate_health()为空llm_fallbacks_default_to_empty_and_no_behavior_change保证默认配置字节级无行为变化llm_provider_chain_wraps_the_primary_and_every_fallback_in_order则端到端验证Config::load - llm_provider_chain按序包装主 provider 与全部 fallbacks。交付序列与落地现状设计文档给出的交付序列新增 profile 解析、校验与测试——无 fallback profile 时零行为变化已由llm_fallbacks_default_to_empty_and_no_behavior_change覆盖新增FallbackLlmProviderwrapper 与聚焦的 fake-provider 测试fallback.rs 完整实现并附ScriptedLlm测试双桩接入serve、扩展被动健康并补充配置/状态文档与CHANGELOG.md条目serve装配点见 serve.rs跑工作区 gates并针对“先瞬时失败、后返回合法结构化补全”的本地 OpenAI 兼容端点手动执行llm-test。需要说明的是设计文档本身是一份proposalissue #648而当前仓库已经完整实现了其中描述的机制fallback.rs模块文档明确注明“Seedocs/llm-provider-fallback.md(issue #648) for the full design”——因此本文以上内容均以落地后的真实源码行为为准。实战建议对自托管聚合端点Ollama / vLLM / LM Studio / 本地路由器将openai-compat作为 fallback 时务必同时给出base_url与api_key_envapi_key_env指向的环境变量在服务启动时就必须已设置否则进程直接拒绝启动。不要把需要鉴权的 provider 与主 provider 混用同一环境变量fallback 的凭证是隔离解析的漏配会在启动时报错而非宕机时才暴露。30 秒熔断冷却期是进程内、按(provider, model)粒度的一次成功即可立即恢复该候选无需等待冷却结束。观测入口是ai-memory status中的llm_candidates段关注last_selected是否长期停留在 fallback 上、last_error_class/last_error_status是否反复出现、circuit_open_until是否持续非空——这些是被动采集的真实调用结果不产生任何额外探测流量。若所有候选持续失败错误为有界的llm fallback chain exhausted after N candidate(s): provider/model: class [status]; ...聚合信息其中只含标签与错误类别不含任何响应体或秘密。【免费下载链接】ai-memorySolution for long term memory for agent coding CLIs and to facilitate handoff between different agent vendors项目地址: https://gitcode.com/GitHub_Trending/ai/ai-memory创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表