《互联网产品的 AI 架构·模型接入与能力边界》2.3 多模型并存的现实

多模型并存不是架构失败,而是能力覆盖、质量分层、区域合规与供应关系共同作用的常态。以「回声」的三个模型与四个能力为例,说明能力覆盖矩阵、冗余度与单提供方风险如何计算,给出缺口分析的 TypeScript 实现与测试,并讨论组合治理节奏与降级路径。

一个模型接所有能力,为什么做不到

产品评审里经常出现一个朴素的愿望:既然模型越来越强,为什么不选一个最强的,把所有 AI 能力都接上去?统一模型听起来简单:一份提示词规范、一套监控、一个账单、一次升级。但回声接入第三个能力之后就发现,这个愿望在四个方向上同时碰壁。

第一,任务结构不同。智能回复是短文本生成,审核分类是有限标签判定,搜索重排是对候选文档排序。重排模型需要理解查询与文档的关系,生成模型擅长续写,两者不是同一种能力。要求一个模型全包,通常意味着在某几个任务上接受明显的质量损失。

第二,质量与成本需要分层。审核每天要处理大量内容,全部用前沿模型会把成本推高一个数量级;但只用便宜模型,边界样本的漏放又不可接受。现实中可行的方案是分层:便宜模型做初筛,不确定样本升级。分层本身就意味着至少两个模型。

第三,区域与合规要求不同。某些能力只能在特定区域处理,某些数据不能离开受控环境,某些模型在目标区域没有服务。一个模型即使质量最好,只要不满足区域约束,就不能进入候选。

第四,供应关系会变化。模型会涨价、降速、下线旧版本,提供方会调整配额与条款。把所有能力押在一个模型上,等于把产品的连续性押在一次商务或一次版本发布上。多模型并存是对这种不确定性的正常对冲。

所以问题不是“要不要多模型”,而是“哪些能力需要多模型、冗余到什么程度、缺口由谁承担”。这正是能力覆盖矩阵要回答的。

多模型并存的四个来源

把多模型的原因拆开,能避免把所有复杂度都归为“架构没设计好”。

能力覆盖是最硬的原因。不同模型支持的任务类型不同,重排、嵌入、审核、生成往往需要不同模型。即使都是生成,长文总结与短回复对上下文和输出控制的要求也不同。

质量分层是产品原因。高价值请求走前沿模型,海量低风险请求走经济模型,中间用置信度或规则决定升级。分层不是歧视用户,而是把预算花在错误代价最高的地方。

区域与合规是外部原因。数据驻留、内容策略、可用区域都会限制候选集合。一个在质量上领先的模型,如果不能在目标区域服务,对产品来说就不存在。

供应与议价是组织原因。多提供方可以降低单点故障风险,也能在商务谈判中保留替代方案。但多提供方不是免费的,每增加一个模型,就增加一套评测、监控、提示词适配和故障处理。

能力覆盖矩阵

能力覆盖矩阵的行是产品能力,列是模型,单元格记录三件事:是否声明覆盖、是否健康、是否在目标区域。矩阵不是模型清单,它把“产品承诺”和“模型事实”放在同一张表上,让缺口一眼可见。

矩阵至少回答四个问题。第一,每个能力有几个健康模型覆盖?第二,这些模型来自几个提供方?第三,覆盖的质量档位是否满足产品要求?第四,如果目标区域受限,覆盖会不会瞬间消失?

覆盖率本身不是目标。一个能力有三个模型覆盖,但全部来自同一提供方,故障时仍然一起失效;一个能力只有一个模型,但这个能力是低风险的辅助功能,可能完全可以接受。矩阵的价值在于把“多”与“稳”区分开。

回声的矩阵按能力排序,每个能力记录模型列表、健康模型、提供方和质量档位。报告里还会给出区域内的健康比例与提供方数量,作为组合健康度的两个粗指标。

缺口不是“再买一个模型”

发现缺口之后,最直接的反应是再加一个模型。但缺口有不同类型,对应的动作完全不同。

缺失(missing)表示没有任何模型声明该能力。这时候要么引入新模型,要么承认这个能力暂时不做。不能用一个“相似能力”的模型硬顶,因为输出契约不同,评测也没有覆盖。

全部不健康(all-unhealthy)表示模型存在但当前不可用。动作是排查故障、切换区域或启用降级,而不是立刻采购新模型。采购解决不了当天的故障。

冗余不足(insufficient-redundancy)表示健康模型数量低于要求。动作可以是引入第二家提供方,也可以是降低该能力的冗余要求,但后者必须经过风险评审。冗余要求应该由能力的错误代价决定,而不是统一设成 2。

档位缺失(missing-tier)表示某个质量档位没有健康模型。比如产品要求同时具备前沿与经济两档,但当前只有标准档。这时候要判断是产品要求过高,还是覆盖确实不足。档位不是越多越好,只有真正会被路由使用的档位才需要覆盖。

单提供方(single-provider)表示该能力只由一家提供方支撑。动作不一定是立刻换掉,而是准备应急预案:备用模型是否完成评测、切换需要多久、切换后质量下降是否可接受。没有预案的单提供方,才是真正的单点。

组合策略要写产品要求

如果矩阵只统计“现有模型碰巧覆盖了什么”,它会得出一个自我实现的结论:现有覆盖就是合理覆盖。组合分析必须同时读入产品要求,包括必须支持的能力、目标区域、每个能力需要的质量档位,以及最小冗余数。

这些要求应该来自能力契约与风险评审,而不是来自模型清单。把它们写进策略对象,矩阵才能区分“没有缺口”和“没有检查”。策略变化时,缺口报告会跟着变化,评审能直接看到影响。

最小冗余数需要谨慎设置。把所有能力都设成 2,会迫使团队为低风险能力也维护第二套模型,增加评测与运维负担;全部设成 1,又会让关键能力在故障时没有退路。合理的做法是按能力分级:影响资金、审核、隐私的能力要求更高冗余,辅助性、可降级的能力可以接受单模型。

把组合分析写成代码

下面的实现把策略、覆盖、缺口和矩阵放在一个函数里。它不负责路由,也不负责采购决策,只负责把事实与要求对齐,输出可复审的报告。

// file: src/core/portfolio.ts
// tier: L1
// verify: node --test test/core/portfolio.test.ts

import { AiError } from "./errors.ts";
import type { CapabilityId, ModelProfile, QualityTier } from "./types.ts";

/**
 * 组合策略描述“产品要求”,不是“现有模型碰巧能做什么”。
 * 缺少的能力必须显式出现在缺口里,而不是被静默忽略。
 */
export interface PortfolioPolicy {
  requiredCapabilities?: CapabilityId[];
  region?: string;
  /** 每个能力至少要覆盖的质量档位,用来防止只剩便宜模型。 */
  requiredTiers?: Record<CapabilityId, QualityTier[]>;
  /** 同一能力至少需要的健康模型数,默认 2。 */
  minRedundancy?: number;
}

export type GapKind =
  | "missing"
  | "all-unhealthy"
  | "insufficient-redundancy"
  | "missing-tier"
  | "single-provider";

export interface CoverageGap {
  capability: CapabilityId;
  kind: GapKind;
  detail: string;
}

export interface CapabilityCoverage {
  capability: CapabilityId;
  /** 目标区域内声明了该能力的模型,包含不健康模型。 */
  models: string[];
  healthyModels: string[];
  /** 健康模型背后的提供方,用于判断是否被单一供应商锁死。 */
  providers: string[];
  /** 健康模型实际提供的质量档位。 */
  tiers: QualityTier[];
}

export interface CoverageCell {
  modelId: string;
  provider: string;
  covered: boolean;
  healthy: boolean;
  inRegion: boolean;
}

export interface CoverageMatrixRow {
  capability: CapabilityId;
  cells: CoverageCell[];
}

export interface PortfolioReport {
  capabilities: CapabilityCoverage[];
  gaps: CoverageGap[];
  /** 目标区域内健康模型涉及的提供方数量。 */
  providerCount: number;
  /** 目标区域内健康模型占全部模型的比例,0..1。 */
  healthyShare: number;
  matrix: CoverageMatrixRow[];
}

const TIER_ORDER: readonly QualityTier[] = ["frontier", "standard", "economy"];

function assertPolicy(policy: PortfolioPolicy): number {
  const minRedundancy = policy.minRedundancy ?? 2;
  if (!Number.isSafeInteger(minRedundancy) || minRedundancy < 1) {
    throw new AiError("invalid-request", "minRedundancy 必须是 >= 1 的整数");
  }
  for (const [capability, tiers] of Object.entries(policy.requiredTiers ?? {})) {
    for (const tier of tiers) {
      if (!TIER_ORDER.includes(tier)) {
        throw new AiError("invalid-request", `能力 ${capability} 声明了未知档位 ${String(tier)}`);
      }
    }
  }
  return minRedundancy;
}

/**
 * 组合分析只回答三个问题:每个能力有谁覆盖、哪里有缺口、是否被单一提供方锁死。
 * 它不做路由决策,路由仍然由网关按请求预算实时选择。
 */
export function analyzePortfolio(
  profiles: readonly ModelProfile[],
  policy: PortfolioPolicy = {},
): PortfolioReport {
  const minRedundancy = assertPolicy(policy);
  const region = policy.region;

  const inRegion = (profile: ModelProfile): boolean =>
    region === undefined || profile.regions.includes(region);

  const capabilities = new Set<CapabilityId>(policy.requiredCapabilities ?? []);
  for (const profile of profiles) {
    for (const capability of profile.capabilities) {
      capabilities.add(capability);
    }
  }
  for (const capability of Object.keys(policy.requiredTiers ?? {})) {
    capabilities.add(capability);
  }
  const orderedCapabilities = [...capabilities].sort();

  const gaps: CoverageGap[] = [];
  const coverage: CapabilityCoverage[] = [];
  const matrix: CoverageMatrixRow[] = [];

  for (const capability of orderedCapabilities) {
    const covering = profiles.filter((profile) => profile.capabilities.includes(capability));
    const scoped = covering.filter(inRegion);
    const healthy = scoped.filter((profile) => profile.healthy);
    const providers = [...new Set(healthy.map((profile) => profile.provider))].sort();
    const tiers = TIER_ORDER.filter((tier) => healthy.some((profile) => profile.qualityTier === tier));

    coverage.push({
      capability,
      models: scoped.map((profile) => profile.id).sort(),
      healthyModels: healthy.map((profile) => profile.id).sort(),
      providers,
      tiers,
    });

    matrix.push({
      capability,
      cells: profiles.map((profile) => ({
        modelId: profile.id,
        provider: profile.provider,
        covered: profile.capabilities.includes(capability),
        healthy: profile.healthy,
        inRegion: inRegion(profile),
      })),
    });

    if (scoped.length === 0) {
      gaps.push({
        capability,
        kind: "missing",
        detail: region ? `区域 ${region} 没有任何模型声明该能力` : "没有任何模型声明该能力",
      });
      continue;
    }
    if (healthy.length === 0) {
      gaps.push({
        capability,
        kind: "all-unhealthy",
        detail: `${scoped.length} 个模型覆盖该能力,但全部不健康`,
      });
      continue;
    }
    if (healthy.length < minRedundancy) {
      gaps.push({
        capability,
        kind: "insufficient-redundancy",
        detail: `健康模型 ${healthy.length} 个,低于要求的 ${minRedundancy} 个`,
      });
    }
    if (providers.length === 1) {
      gaps.push({
        capability,
        kind: "single-provider",
        detail: `仅由 ${providers[0]} 提供,故障或涨价时没有替代`,
      });
    }
    const required = policy.requiredTiers?.[capability] ?? [];
    for (const tier of TIER_ORDER) {
      if (required.includes(tier) && !tiers.includes(tier)) {
        gaps.push({
          capability,
          kind: "missing-tier",
          detail: `缺少健康可用的 ${tier} 档模型`,
        });
      }
    }
  }

  const scopedProfiles = profiles.filter(inRegion);
  const healthyProfiles = scopedProfiles.filter((profile) => profile.healthy);

  return {
    capabilities: coverage,
    gaps,
    providerCount: new Set(healthyProfiles.map((profile) => profile.provider)).size,
    healthyShare:
      scopedProfiles.length === 0 ? 0 : healthyProfiles.length / scopedProfiles.length,
    matrix,
  };
}

/** 只取缺口类型,便于在发布门禁里做集合比较。 */
export function gapKinds(report: PortfolioReport): GapKind[] {
  return [...new Set(report.gaps.map((gap) => gap.kind))].sort();
}

实现里有几个刻意的选择。第一,能力集合取模型声明与策略要求的并集,这样“产品要求但没人覆盖”的能力不会因为模型清单里没有就消失。第二,区域过滤在覆盖统计之前执行,因为不在目标区域的模型对产品来说不可用。第三,缺口按固定顺序检查:缺失、全部不健康、冗余不足、单提供方、档位缺失。顺序固定让报告稳定,也避免同一问题被重复描述成多条。

gapKinds 只返回去重后的缺口类型,适合放进发布门禁做集合比较。比如发布要求里可以写“不允许 missing 与 all-unhealthy”,而把 single-provider 作为警告而不是阻断。门禁的严格程度应该由风险决定,而不是由工具决定。

回声的组合:三个模型与四个能力

回声当前维护三个模型档案:前沿档、标准档、经济档。产品侧有四个能力:智能回复、内容审核、会话总结、搜索重排。

智能回复以标准档为主,遇到长对话或低置信度时升级到前沿档。内容审核以经济档做初筛,边界样本升级到标准档。会话总结用标准档,因为输入长但错误代价中等。搜索重排只有前沿档覆盖,而且只有一家提供方。

把这份组合放进矩阵,会看到几个缺口。搜索重排是单提供方且冗余不足;内容审核的经济档虽然覆盖了初筛,但高风险类别缺少前沿档兜底;会话总结只有标准档,没有可降级的经济档。智能回复的覆盖最完整,但也依赖两个档位之间的升级逻辑。

这些缺口不一定都要立刻补。搜索重排的调用量小、错误可回退到规则排序,单提供方的风险可能可以接受,但必须准备降级预案并定期演练。内容审核的高风险类别漏放代价高,即使调用量小,也应该补前沿档兜底。会话总结缺少经济档,则可以先观察成本结构,再决定是否需要。

矩阵的作用不是逼团队把所有格子填满,而是让“不填”成为一个有记录的决定。

冗余不是越多越好

多模型并存的成本经常被低估。每增加一个模型,至少要增加四类工作:离线评测集要覆盖它,提示词与输出解析要适配它,监控要区分它,故障预案要包含它。模型越多,路由组合越多,排查问题时需要排除的变量也越多。

冗余的价值只在故障或涨价时兑现,成本却每天发生。因此冗余应该按能力的关键程度分配,而不是按模型数量考核。一个健康的组合可能只有三个模型,但每个关键能力都有明确的备用路径;另一个组合有十个模型,但关键能力仍然只由一家提供方支撑,后者的“多”是假的多。

还要区分“模型冗余”和“路径冗余”。有时候备用路径不是另一个模型,而是规则引擎、缓存结果、模板回复或人工队列。这些路径的质量可能不如模型,但在故障时能保住核心流程。组合分析应该把降级路径也算进覆盖,而不是只数模型。

组合治理的节奏

组合需要定期治理,否则会随着临时接入逐渐失控。回声把治理分成三个节奏。

季度复审看结构:能力覆盖、提供方分布、冗余要求是否仍然匹配产品风险。复审的输出是组合策略的变更,而不是单个模型的替换。

月度检查看健康:模型可用性、区域覆盖、价格变化、配额使用。发现持续不健康的模型,先走故障流程,再评估是否保留。

每次接入看准入:新模型必须通过离线评测、成本核算、区域检查和降级演练,才能进入矩阵。接入时就要写清楚它覆盖哪些能力、替代谁、失败时切到哪里。没有退出方案的接入不应该被批准。

模型下线同样需要治理。提供方宣布旧版本退役时,产品要有迁移计划:哪些能力受影响、替代模型是否完成评测、切换窗口多长、回滚路径是什么。把下线当成一次发布来管理,而不是等故障发生后再补救。

小结

多模型并存是能力结构、质量分层、区域合规与供应关系共同作用的常态。能力覆盖矩阵把产品要求与模型事实放在一起,缺口分析区分缺失、全部不健康、冗余不足、档位缺失与单提供方五种情况,每种情况对应不同动作。冗余要按能力风险分配,降级路径也应该算进覆盖。

组合治理需要节奏:季度看结构,月度看健康,接入与下线都按发布管理。下一章进入模型网关,讨论当多个模型并存时,网关如何承接统一契约、预算控制、降级与可观测性。

阅读导航:上一节:2.2 选型矩阵:离线评测与在线 A/B · 下一节:3.1 模型网关的职责边界 。

常见错误与边界

  • 把多模型当成架构失败。能力结构、分层、区域与供应都会自然产生多模型,关键是有没有覆盖分析。
  • 只看模型数量,不看提供方分布。三个模型来自同一家,故障时仍然一起失效。
  • 发现缺口就加模型。缺失、故障、冗余不足、档位缺失的动作不同,采购不是万能解。
  • 统一设置冗余要求。低风险辅助能力不必强制双模型,高风险能力也不应只靠一个模型。
  • 只统计现有覆盖,不读产品要求。没有策略输入的矩阵,会把“碰巧有”当成“已经够”。
  • 忽略降级路径。规则、缓存、模板与人工队列也是覆盖,不能只数模型。
  • 接入新模型不写退出方案。没有替代与回滚计划的接入,会在下线时变成事故。
  • 把单提供方当阻断项一刀切。是否接受取决于错误代价与预案,应该评审而不是机械拦截。
  • 组合长期不复审。价格、区域、版本和业务风险都会变,矩阵与策略需要定期重跑。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《互联网产品的 AI 架构·平台化与治理》附录 C 平台化评审清单
  2. 《互联网产品的 AI 架构·平台化与治理》附录 B 能力目录与配额字段速查
  3. 《互联网产品的 AI 架构·平台化与治理》附录 A 治理角色与职责速查