24.10-模型配置中心

要点

  • 模型配置中心统一管理 LLM 提供商、模型列表、参数配置
  • 支持多模型提供商:OpenAI、Anthropic、本地模型
  • 租户可以配置模型偏好:默认模型、温度、最大 token 数
  • 模型路由根据租户套餐、请求特征自动选择最优模型

内容

1. 模型配置架构

模型配置中心
├── 提供商管理 (Providers)
│   ├── OpenAI (GPT-4, GPT-3.5)
│   ├── Anthropic (Claude-3)
│   └── 自定义端点
├── 模型注册表 (Models)
│   ├── 模型元数据(名称、能力、价格)
│   ├── 调用配置(API Key、端点)
│   └── 限制配置(上下文长度、速率)
└── 租户配置 (Tenant Configs)
    ├── 默认模型
    ├── 参数偏好(温度、Top-P)
    └── 可用模型列表

2. 数据模型

-- 模型提供商表
CREATE TABLE model_providers (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,                   -- OpenAI, Anthropic
  type TEXT NOT NULL,                   -- openai/anthropic/custom
  api_endpoint TEXT NOT NULL,
  api_key_encrypted TEXT NOT NULL,      -- 加密存储
  is_active INTEGER DEFAULT 1,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL
)
 
-- 模型注册表
CREATE TABLE model_registry (
  id TEXT PRIMARY KEY,
  provider_id TEXT NOT NULL,
  model_id TEXT NOT NULL,               -- gpt-4, claude-3-opus
  name TEXT NOT NULL,                   -- 显示名称
  capabilities TEXT NOT NULL,           -- JSON: chat, vision, function_calling
  context_length INTEGER NOT NULL,
  input_price_per_1k REAL NOT NULL,     -- 每 1K token 价格(美分)
  output_price_per_1k REAL NOT NULL,
  max_output_tokens INTEGER,
  is_available INTEGER DEFAULT 1,
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL,
  FOREIGN KEY (provider_id) REFERENCES model_providers(id)
)
 
-- 租户模型配置表
CREATE TABLE tenant_model_configs (
  id TEXT PRIMARY KEY,
  tenant_id TEXT NOT NULL UNIQUE,
  default_model TEXT NOT NULL,
  temperature REAL DEFAULT 0.7,
  top_p REAL DEFAULT 1.0,
  max_tokens INTEGER DEFAULT 4000,
  allowed_models TEXT,                  -- JSON 数组,空表示使用套餐默认
  created_at INTEGER NOT NULL,
  updated_at INTEGER NOT NULL,
  FOREIGN KEY (tenant_id) REFERENCES tenants(id)
)

TypeScript 类型:

// src/types/model.ts
export interface ModelProvider {
  id: string
  name: string
  type: 'openai' | 'anthropic' | 'custom'
  apiEndpoint: string
  isActive: boolean
}
 
export interface ModelDefinition {
  id: string
  providerId: string
  modelId: string
  name: string
  capabilities: {
    chat: boolean
    vision?: boolean
    functionCalling?: boolean
  }
  contextLength: number
  inputPricePer1k: number
  outputPricePer1k: number
  maxOutputTokens?: number
  isAvailable: boolean
}
 
export interface TenantModelConfig {
  id: string
  tenantId: string
  defaultModel: string
  temperature: number
  topP: number
  maxTokens: number
  allowedModels?: string[]
}

3. 提供商管理

// src/routes/admin/models.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
import { requireRole } from '../../middleware/permission'
import { encrypt, decrypt } from '../../lib/crypto'
 
const app = new Hono()
 
// 添加提供商
app.post('/providers', requireRole('admin'), zValidator('json', z.object({
  name: z.string(),
  type: z.enum(['openai', 'anthropic', 'custom']),
  apiEndpoint: z.string().url(),
  apiKey: z.string(),
})), async (c) => {
  const { name, type, apiEndpoint, apiKey } = c.req.valid('json')
 
  const id = crypto.randomUUID()
  const now = Date.now()
  const encryptedKey = encrypt(apiKey, c.env.ENCRYPTION_KEY)
 
  await c.env.DB.prepare(`
    INSERT INTO model_providers (id, name, type, api_endpoint, api_key_encrypted, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?)
  `).bind(id, name, type, apiEndpoint, encryptedKey, now, now).run()
 
  return c.json({ id, name, type }, 201)
})
 
// 获取提供商列表
app.get('/providers', requireRole('admin'), async (c) => {
  const providers = await c.env.DB.prepare(`
    SELECT id, name, type, api_endpoint, is_active, created_at
    FROM model_providers
    ORDER BY created_at DESC
  `).all()
 
  return c.json({
    providers: providers.results.map(p => ({
      id: p.id,
      name: p.name,
      type: p.type,
      apiEndpoint: p.api_endpoint,
      isActive: p.is_active === 1,
    })),
  })
})

4. 模型注册

// 注册模型
app.post('/models', requireRole('admin'), zValidator('json', z.object({
  providerId: z.string(),
  modelId: z.string(),
  name: z.string(),
  capabilities: z.object({
    chat: z.boolean(),
    vision: z.boolean().optional(),
    functionCalling: z.boolean().optional(),
  }),
  contextLength: z.number(),
  inputPricePer1k: z.number(),
  outputPricePer1k: z.number(),
  maxOutputTokens: z.number().optional(),
})), async (c) => {
  const data = c.req.valid('json')
  const id = crypto.randomUUID()
  const now = Date.now()
 
  await c.env.DB.prepare(`
    INSERT INTO model_registry (id, provider_id, model_id, name, capabilities, context_length, input_price_per_1k, output_price_per_1k, max_output_tokens, created_at, updated_at)
    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
  `).bind(
    id,
    data.providerId,
    data.modelId,
    data.name,
    JSON.stringify(data.capabilities),
    data.contextLength,
    data.inputPricePer1k,
    data.outputPricePer1k,
    data.maxOutputTokens || null,
    now,
    now
  ).run()
 
  return c.json({ id, modelId: data.modelId, name: data.name }, 201)
})
 
// 获取可用模型列表
app.get('/models', async (c) => {
  const tenantId = c.get('tenantId')
 
  // 获取租户配置
  const config = await c.env.DB.prepare(`
    SELECT * FROM tenant_model_configs WHERE tenant_id = ?
  `).bind(tenantId).first()
 
  // 获取套餐可用模型
  const plan = c.get('plan')
 
  let models
  if (config?.allowed_models) {
    // 使用租户自定义配置
    const allowedIds = JSON.parse(config.allowed_models)
    const placeholders = allowedIds.map(() => '?').join(',')
    models = await c.env.DB.prepare(`
      SELECT * FROM model_registry
      WHERE model_id IN (${placeholders}) AND is_available = 1
    `).bind(...allowedIds).all()
  } else {
    // 使用套餐默认
    const placeholders = plan.allowedModels.map(() => '?').join(',')
    models = await c.env.DB.prepare(`
      SELECT * FROM model_registry
      WHERE model_id IN (${placeholders}) AND is_available = 1
    `).bind(...plan.allowedModels).all()
  }
 
  return c.json({
    models: models.results.map(m => ({
      id: m.id,
      modelId: m.model_id,
      name: m.name,
      capabilities: JSON.parse(m.capabilities),
      contextLength: m.context_length,
      maxOutputTokens: m.max_output_tokens,
    })),
  })
})

5. 租户模型配置

// src/routes/model-config.ts
import { Hono } from 'hono'
import { zValidator } from '@hono/zod-validator'
import { z } from 'zod'
 
const app = new Hono()
 
// 获取租户模型配置
app.get('/', async (c) => {
  const tenantId = c.get('tenantId')
 
  let config = await c.env.DB.prepare(`
    SELECT * FROM tenant_model_configs WHERE tenant_id = ?
  `).bind(tenantId).first()
 
  if (!config) {
    // 返回默认配置
    const plan = c.get('plan')
    return c.json({
      defaultModel: plan.allowedModels[0],
      temperature: 0.7,
      topP: 1.0,
      maxTokens: 4000,
      allowedModels: plan.allowedModels,
    })
  }
 
  return c.json({
    defaultModel: config.default_model,
    temperature: config.temperature,
    topP: config.top_p,
    maxTokens: config.max_tokens,
    allowedModels: config.allowed_models ? JSON.parse(config.allowed_models) : null,
  })
})
 
// 更新租户模型配置
app.put('/', zValidator('json', z.object({
  defaultModel: z.string().optional(),
  temperature: z.number().min(0).max(2).optional(),
  topP: z.number().min(0).max(1).optional(),
  maxTokens: z.number().min(100).max(100000).optional(),
  allowedModels: z.array(z.string()).optional(),
})), async (c) => {
  const tenantId = c.get('tenantId')
  const updates = c.req.valid('json')
 
  // 验证默认模型在允许列表中
  if (updates.defaultModel) {
    const plan = c.get('plan')
    const allowed = updates.allowedModels || plan.allowedModels
    if (!allowed.includes(updates.defaultModel)) {
      return c.json({ error: '默认模型不在允许列表中' }, 400)
    }
  }
 
  const existing = await c.env.DB.prepare(`
    SELECT id FROM tenant_model_configs WHERE tenant_id = ?
  `).bind(tenantId).first()
 
  const now = Date.now()
 
  if (existing) {
    // 更新
    const setClauses = []
    const values: any[] = []
 
    if (updates.defaultModel) {
      setClauses.push('default_model = ?')
      values.push(updates.defaultModel)
    }
    if (updates.temperature !== undefined) {
      setClauses.push('temperature = ?')
      values.push(updates.temperature)
    }
    if (updates.topP !== undefined) {
      setClauses.push('top_p = ?')
      values.push(updates.topP)
    }
    if (updates.maxTokens !== undefined) {
      setClauses.push('max_tokens = ?')
      values.push(updates.maxTokens)
    }
    if (updates.allowedModels !== undefined) {
      setClauses.push('allowed_models = ?')
      values.push(JSON.stringify(updates.allowedModels))
    }
 
    setClauses.push('updated_at = ?')
    values.push(now)
    values.push(tenantId)
 
    await c.env.DB.prepare(`
      UPDATE tenant_model_configs SET ${setClauses.join(', ')} WHERE tenant_id = ?
    `).bind(...values).run()
  } else {
    // 创建
    const plan = c.get('plan')
    const id = crypto.randomUUID()
 
    await c.env.DB.prepare(`
      INSERT INTO tenant_model_configs (id, tenant_id, default_model, temperature, top_p, max_tokens, allowed_models, created_at, updated_at)
      VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
    `).bind(
      id,
      tenantId,
      updates.defaultModel || plan.allowedModels[0],
      updates.temperature ?? 0.7,
      updates.topP ?? 1.0,
      updates.maxTokens ?? 4000,
      updates.allowedModels ? JSON.stringify(updates.allowedModels) : null,
      now,
      now
    ).run()
  }
 
  return c.json({ success: true })
})

6. 模型路由

// src/lib/model-router.ts
import { ModelDefinition, TenantModelConfig } from '../types/model'
 
export class ModelRouter {
  constructor(private env: Env) {}
 
  /**
   * 根据请求选择最优模型
   */
  async selectModel(params: {
    tenantId: string
    requestedModel?: string
    requiredCapabilities?: string[]
    messageCount?: number
    estimatedTokens?: number
  }): Promise<{ model: ModelDefinition; config: TenantModelConfig }> {
    const { tenantId, requestedModel, requiredCapabilities, messageCount, estimatedTokens } = params
 
    // 1. 获取租户配置
    const config = await this.getTenantConfig(tenantId)
 
    // 2. 获取可用模型
    const availableModels = await this.getAvailableModels(tenantId)
 
    // 3. 选择模型
    let selectedModel: ModelDefinition
 
    if (requestedModel) {
      // 使用请求的模型(如果在可用列表中)
      selectedModel = availableModels.find(m => m.modelId === requestedModel)!
      if (!selectedModel) {
        throw new Error(`Model ${requestedModel} is not available`)
      }
    } else {
      // 自动选择
      selectedModel = this.autoSelectModel({
        models: availableModels,
        defaultModel: config.defaultModel,
        requiredCapabilities,
        messageCount,
        estimatedTokens,
      })
    }
 
    return { model: selectedModel, config }
  }
 
  /**
   * 自动选择模型
   */
  private autoSelectModel(params: {
    models: ModelDefinition[]
    defaultModel: string
    requiredCapabilities?: string[]
    messageCount?: number
    estimatedTokens?: number
  }): ModelDefinition {
    const { models, defaultModel, requiredCapabilities, messageCount, estimatedTokens } = params
 
    // 过滤满足能力要求的模型
    let candidates = models
 
    if (requiredCapabilities?.length) {
      candidates = candidates.filter(m => {
        return requiredCapabilities.every(cap => {
          return m.capabilities[cap as keyof typeof m.capabilities]
        })
      })
    }
 
    // 如果对话很长或 token 很多,使用大模型
    if ((messageCount && messageCount > 20) || (estimatedTokens && estimatedTokens > 8000)) {
      const largeModel = candidates.find(m =>
        m.modelId.includes('gpt-4') || m.modelId.includes('claude-3-opus')
      )
      if (largeModel) return largeModel
    }
 
    // 使用默认模型
    const defaultModelDef = candidates.find(m => m.modelId === defaultModel)
    if (defaultModelDef) return defaultModelDef
 
    // 返回第一个可用模型
    return candidates[0]
  }
 
  private async getTenantConfig(tenantId: string): Promise<TenantModelConfig> {
    const config = await this.env.DB.prepare(`
      SELECT * FROM tenant_model_configs WHERE tenant_id = ?
    `).bind(tenantId).first()
 
    if (!config) {
      // 返回默认配置
      return {
        id: '',
        tenantId,
        defaultModel: 'gpt-3.5-turbo',
        temperature: 0.7,
        topP: 1.0,
        maxTokens: 4000,
      }
    }
 
    return {
      id: config.id,
      tenantId: config.tenant_id,
      defaultModel: config.default_model,
      temperature: config.temperature,
      topP: config.top_p,
      maxTokens: config.max_tokens,
      allowedModels: config.allowed_models ? JSON.parse(config.allowed_models) : undefined,
    }
  }
 
  private async getAvailableModels(tenantId: string): Promise<ModelDefinition[]> {
    const tenant = await this.env.DB.prepare(`
      SELECT plan FROM tenants WHERE id = ?
    `).bind(tenantId).first()
 
    const plan = getPlan(tenant.plan)
 
    const placeholders = plan.allowedModels.map(() => '?').join(',')
    const models = await this.env.DB.prepare(`
      SELECT * FROM model_registry
      WHERE model_id IN (${placeholders}) AND is_available = 1
    `).bind(...plan.allowedModels).all()
 
    return models.results.map(m => ({
      id: m.id,
      providerId: m.provider_id,
      modelId: m.model_id,
      name: m.name,
      capabilities: JSON.parse(m.capabilities),
      contextLength: m.context_length,
      inputPricePer1k: m.input_price_per_1k,
      outputPricePer1k: m.output_price_per_1k,
      maxOutputTokens: m.max_output_tokens,
      isAvailable: m.is_available === 1,
    }))
  }
}

7. 调用 LLM

// src/lib/llm.ts
import { ModelDefinition, TenantModelConfig } from '../types/model'
import { decrypt } from './crypto'
 
export async function callLLM(
  messages: any[],
  model: ModelDefinition,
  config: TenantModelConfig,
  env: Env
): Promise<{
  content: string
  usage: { promptTokens: number; completionTokens: number }
}> {
  // 获取提供商信息
  const provider = await env.DB.prepare(`
    SELECT * FROM model_providers WHERE id = ?
  `).bind(model.providerId).first()
 
  if (!provider) {
    throw new Error('Provider not found')
  }
 
  const apiKey = decrypt(provider.api_key_encrypted, env.ENCRYPTION_KEY)
 
  // 根据提供商类型调用
  switch (provider.type) {
    case 'openai':
      return callOpenAI(messages, model, config, apiKey, provider.api_endpoint)
    case 'anthropic':
      return callAnthropic(messages, model, config, apiKey, provider.api_endpoint)
    default:
      throw new Error('Unsupported provider type')
  }
}
 
async function callOpenAI(
  messages: any[],
  model: ModelDefinition,
  config: TenantModelConfig,
  apiKey: string,
  endpoint: string
) {
  const response = await fetch(`${endpoint}/chat/completions`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${apiKey}`,
    },
    body: JSON.stringify({
      model: model.modelId,
      messages,
      temperature: config.temperature,
      top_p: config.topP,
      max_tokens: config.maxTokens,
    }),
  })
 
  const data = await response.json()
 
  return {
    content: data.choices[0].message.content,
    usage: {
      promptTokens: data.usage.prompt_tokens,
      completionTokens: data.usage.completion_tokens,
    },
  }
}
 
async function callAnthropic(
  messages: any[],
  model: ModelDefinition,
  config: TenantModelConfig,
  apiKey: string,
  endpoint: string
) {
  const response = await fetch(`${endpoint}/messages`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-api-key': apiKey,
      'anthropic-version': '2023-06-01',
    },
    body: JSON.stringify({
      model: model.modelId,
      messages,
      temperature: config.temperature,
      top_p: config.topP,
      max_tokens: config.maxTokens,
    }),
  })
 
  const data = await response.json()
 
  return {
    content: data.content[0].text,
    usage: {
      promptTokens: data.usage.input_tokens,
      completionTokens: data.usage.output_tokens,
    },
  }
}

8. 小结

模型配置中心的关键点:

  1. 提供商管理:统一管理 OpenAI、Anthropic 等提供商的 API Key
  2. 模型注册:记录模型的能力、价格、限制
  3. 租户配置:每个租户可以配置默认模型、参数
  4. 模型路由:根据请求特征自动选择最优模型
  5. 统一调用:封装不同提供商的 API 差异

一句话带走:

模型配置中心是「提供商 + 模型 + 租户配置」的三层架构。提供商管理 API Key,模型注册表记录能力,租户配置控制偏好。模型路由根据请求特征自动选择,统一调用接口屏蔽差异。