Files
archived-gitea-ai-assistant/docs/design/pluggable-llm-providers.md

35 KiB
Raw Blame History

技术设计文档:可插拔 LLM Provider 架构

状态: Draft
作者: AI Architect
日期: 2026-03-04
相关 Issue: N/A


目录


0. 设计原则

原则 说明
UI-Only 配置 所有业务配置仅通过 Web 管理后台设置,不再有环境变量覆盖层(仅保留极少数启动参数如 PORTWEBHOOK_SECRETDATABASE_PATH
4 Provider 并存 openai_compatible(现有兼容格式)、openai_responsesResponses APIanthropicMessages APIgeminigenerateContent API
SQLite 持久化 使用 bun:sqlite 零依赖,单文件 data/assistant.db
密钥应用层加密 API Key 使用 AES-256-GCM 加密后存 DB主密钥通过环境变量 ENCRYPTION_KEY 传入hex 编码64 字符 = 32 字节),未设置则拒绝启动
不做向前兼容 旧 JSON 配置文件方案直接废弃,新版本仅支持数据库配置

开源参考

借鉴点 参考项目 具体模式
接口先行 + provider registry Vercel AI SDK LanguageModelV3 spec-first adapter版本化接口
Provider transformation 差异映射 LiteLLM 每个 provider 独立 transformation.py,标准化 error/usage
Runtime factory + 多 provider 配置 LobeChat createOpenAICompatibleRuntime 工厂 + 动态 model list
配置驱动多 endpoint LibreChat librechat.yaml Custom Endpoints 模式
能力声明/能力检测 Continue supportsTools / supportsImages 声明式 capability

1. 目录结构(新增/改动部分)

src/
├── db/
│   ├── database.ts              # bun:sqlite 初始化
│   ├── migrations/
│   │   └── 001_init.ts          # 建表 DDL
│   └── repositories/
│       ├── provider-repo.ts     # llm_providers CRUD
│       ├── model-role-repo.ts   # model_role_assignments CRUD
│       ├── secret-repo.ts       # 加密 read/write
│       └── settings-repo.ts     # system_settings KV
│
├── llm/
│   ├── types.ts                 # 统一内部请求/响应类型
│   ├── capabilities.ts          # 能力声明枚举
│   ├── errors.ts                # LLM 层标准化错误
│   ├── gateway.ts               # LLM Gateway 入口(按 provider 路由)
│   ├── tool-converter.ts        # 工具定义 → 各 provider 格式转换
│   └── providers/
│       ├── base.ts              # LLMProvider 抽象接口
│       ├── openai-compatible.ts # 现有兼容格式 adapter
│       ├── openai-responses.ts  # OpenAI Responses API adapter
│       ├── anthropic.ts         # Anthropic Messages API adapter
│       └── gemini.ts            # Gemini generateContent adapter
│
├── crypto/
│   └── secrets.ts               # AES-256-GCM 加解密 + master key 管理
│
├── controllers/
│   └── llm-config.ts            # 新 REST API替代 config.ts 中 LLM 部分)
│
└── config/
    ├── config-manager.ts        # 精简:只管非 LLM 配置gitea/feishu/app/admin/review 非模型部分)
    └── config-schema.ts         # 移除 openai groupLLM 配置全部走 DB

2. 数据库表结构SQLite DDL

2.1 ER 关系

llm_providers  1 ←──── 1  llm_secrets              (每个 provider 一个加密 key)
llm_providers  1 ←──── N  model_role_assignments    (一个 provider 可服务多个角色)

2.2 完整 DDL

-- ============================================================
-- 表1: llm_providers — Provider 实例配置
-- ============================================================
CREATE TABLE llm_providers (
  id            TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(8)))),
  name          TEXT NOT NULL,                    -- 用户自定义显示名,如 "公司内部 OpenAI 代理"
  type          TEXT NOT NULL CHECK (type IN (
                  'openai_compatible',             -- 现有兼容格式(自定义 baseUrl
                  'openai_responses',              -- OpenAI 标准 Responses API
                  'anthropic',                     -- Anthropic Messages API
                  'gemini'                         -- Google Gemini generateContent
                )),
  base_url      TEXT,                             -- 可选自定义 endpointopenai_compatible 必填)
  default_model TEXT NOT NULL,                    -- 此 provider 的默认模型 ID
  is_enabled    INTEGER NOT NULL DEFAULT 1,       -- 0=禁用, 1=启用
  extra_config  TEXT DEFAULT '{}',                -- JSON: provider 特有配置(如 api_version, project_id 等)
  created_at    TEXT NOT NULL DEFAULT (datetime('now')),
  updated_at    TEXT NOT NULL DEFAULT (datetime('now'))
);

-- ============================================================
-- 表2: llm_secrets — 加密存储的 API Key
-- ============================================================
CREATE TABLE llm_secrets (
  provider_id   TEXT PRIMARY KEY REFERENCES llm_providers(id) ON DELETE CASCADE,
  ciphertext    BLOB NOT NULL,                    -- AES-256-GCM 密文
  iv            BLOB NOT NULL,                    -- 12 bytes nonce
  auth_tag      BLOB NOT NULL,                    -- 16 bytes GCM tag
  key_version   INTEGER NOT NULL DEFAULT 1,       -- 主密钥版本号(便于密钥轮换)
  updated_at    TEXT NOT NULL DEFAULT (datetime('now'))
);

-- ============================================================
-- 表3: model_role_assignments — 场景 → 模型映射
-- ============================================================
-- 每个业务场景(如 planner/specialist/judge/embedding绑定到
-- 一个 provider + 具体 model支持不同场景用不同 provider。
CREATE TABLE model_role_assignments (
  role          TEXT PRIMARY KEY CHECK (role IN (
                  'planner',         -- Agent 审查 planner
                  'specialist',      -- Agent 审查 specialist
                  'judge',           -- Agent 审查 judge
                  'embedding'        -- 向量嵌入Qdrant
                )),
  provider_id   TEXT NOT NULL REFERENCES llm_providers(id),
  model         TEXT NOT NULL,        -- 该场景使用的具体模型 ID
  updated_at    TEXT NOT NULL DEFAULT (datetime('now'))
);

-- ============================================================
-- 表4: system_settings — 通用 KV 设置
-- ============================================================
-- 存放非 LLM 的业务配置(由 UI 直接写入 DB
CREATE TABLE system_settings (
  key           TEXT PRIMARY KEY,
  value         TEXT NOT NULL,
  is_sensitive  INTEGER NOT NULL DEFAULT 0,       -- 1=加密存储(复用 crypto 模块)
  updated_at    TEXT NOT NULL DEFAULT (datetime('now'))
);

-- 索引
CREATE INDEX idx_providers_type ON llm_providers(type);
CREATE INDEX idx_providers_enabled ON llm_providers(is_enabled);

2.3 字段说明补充

表.字段 说明
llm_providers.type 决定使用哪个 adapter 实现
llm_providers.base_url openai_compatible 类型必填(用户自建代理地址);其他类型可选覆盖官方默认 endpoint
llm_providers.extra_config JSON 字段,存放 provider 特有参数。例如 Gemini 的 projectId、OpenAI 的 organization、Anthropic 的 anthropic-version header 等
llm_secrets.key_version 用于密钥轮换:当 ENCRYPTION_KEY 更新后,启动时批量重加密所有 key_version < current 的记录
model_role_assignments.role 业务角色枚举,对应代码中不同调用场景
system_settings.is_sensitive 为 1 时 value 字段存密文(复用 crypto/secrets.tsGET API 返回 masked

3. LLM Gateway 核心 TypeScript 接口

3.1 统一消息与请求/响应类型

// ── src/llm/types.ts ────────────────────────────────────────

/** 模型角色枚举 */
export type ModelRole = 'planner' | 'specialist' | 'judge' | 'embedding';

/** 统一消息格式(内部表达,不暴露 provider 差异) */
export interface LLMMessage {
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string;
  toolCallId?: string;        // role=tool 时关联的 tool call ID
  toolCalls?: LLMToolCall[];  // role=assistant 时返回的 tool calls
}

export interface LLMToolCall {
  id: string;
  name: string;
  arguments: string; // JSON string
}

/** 工具定义(内部通用格式,由 tool-converter.ts 转为各 provider 格式) */
export interface LLMToolDefinition {
  name: string;
  description: string;
  parameters: Record<string, unknown>; // JSON Schema
}

/** 统一请求 */
export interface LLMChatRequest {
  messages: LLMMessage[];
  model: string;
  temperature?: number;
  maxTokens?: number;
  responseFormat?: 'text' | 'json';     // 抽象 JSON mode
  tools?: LLMToolDefinition[];
  /** provider 透传配置(如 Anthropic thinking、Gemini safetySettings */
  providerOptions?: Record<string, unknown>;
}

/** 统一响应 */
export interface LLMChatResponse {
  content: string | null;
  toolCalls: LLMToolCall[];
  finishReason: 'stop' | 'tool_calls' | 'length' | 'content_filter' | 'error';
  usage: {
    promptTokens: number;
    completionTokens: number;
    totalTokens: number;
  };
  raw?: unknown; // 保留原始响应供调试
}

3.2 能力模型

// ── src/llm/capabilities.ts ─────────────────────────────────

export interface ProviderCapabilities {
  /** 是否支持 tool/function calling */
  supportsTools: boolean;
  /** 是否支持原生 JSON modevs 需要 prompt 指令 + 手动解析) */
  supportsJsonMode: boolean;
  /** 是否支持 SSE streaming */
  supportsStreaming: boolean;
  /** 是否支持 embedding 接口 */
  supportsEmbeddings: boolean;
  /** 是否支持图片/多模态输入 */
  supportsMultimodal: boolean;
  /** 最大输入 token 数(用于预校验,避免无效调用) */
  maxInputTokens?: number;
}

/** 各 provider 默认能力声明 */
export const DEFAULT_CAPABILITIES: Record<string, ProviderCapabilities> = {
  openai_compatible: {
    supportsTools: true,
    supportsJsonMode: true,
    supportsStreaming: true,
    supportsEmbeddings: true,
    supportsMultimodal: false, // 取决于具体模型
  },
  openai_responses: {
    supportsTools: true,
    supportsJsonMode: true,
    supportsStreaming: true,
    supportsEmbeddings: true,
    supportsMultimodal: true,
  },
  anthropic: {
    supportsTools: true,
    supportsJsonMode: false, // 无原生 JSON mode需 prompt 指令
    supportsStreaming: true,
    supportsEmbeddings: false,
    supportsMultimodal: true,
  },
  gemini: {
    supportsTools: true,
    supportsJsonMode: true, // responseMimeType: 'application/json'
    supportsStreaming: true,
    supportsEmbeddings: true,
    supportsMultimodal: true,
  },
};

3.3 Provider 抽象接口

// ── src/llm/providers/base.ts ───────────────────────────────

import type { ProviderCapabilities } from '../capabilities';
import type { LLMChatRequest, LLMChatResponse } from '../types';

export interface LLMProvider {
  /** Provider 类型标识 */
  readonly type: string;

  /** 能力声明 */
  readonly capabilities: ProviderCapabilities;

  /**
   * 核心调用方法。Gateway 只调用此方法。
   * 各 adapter 负责:
   *   1. 将 LLMChatRequest 转为 provider 原生格式
   *   2. 发 HTTP / SDK 调用
   *   3. 将原生响应转为 LLMChatResponse
   */
  chat(request: LLMChatRequest): Promise<LLMChatResponse>;

  /** 可选:嵌入接口 */
  embed?(texts: string[]): Promise<number[][]>;
}

/** Provider 工厂函数签名:从 DB 配置 + 解密后 apiKey 创建实例 */
export type ProviderFactory = (config: {
  baseUrl?: string;
  apiKey: string;
  defaultModel: string;
  extraConfig: Record<string, unknown>;
}) => LLMProvider;

3.4 Gateway 入口

// ── src/llm/gateway.ts ──────────────────────────────────────

import type { ModelRole, LLMChatRequest, LLMChatResponse } from './types';
import type { LLMProvider } from './providers/base';

/**
 * LLM Gateway — 业务层唯一入口
 *
 * 职责:
 *   1. 根据 role 查询 model_role_assignments → provider_id + model
 *   2. 从 provider 缓存获取或按需创建LLMProvider 实例
 *   3. 调用 provider.chat() 并返回统一响应
 *   4. 如果 provider 配置变更UI 保存时invalidate 缓存
 */
export class LLMGateway {
  /** provider 实例缓存provider_id → LLMProvider */
  private cache = new Map<string, LLMProvider>();

  /**
   * 按业务角色调用 LLM
 * @param role 业务角色planner/specialist/judge/embedding
   * @param request 请求(不含 model由角色映射决定
   */
  async chatForRole(
    role: ModelRole,
    request: Omit<LLMChatRequest, 'model'>
  ): Promise<LLMChatResponse>;

  /**
   * 用指定 provider 直接调用(连通性测试用)
   */
  async chatDirect(
    providerId: string,
    request: LLMChatRequest
  ): Promise<LLMChatResponse>;

  /**
   * 获取指定 provider 的 embedding 接口
   */
  async embedForRole(
    role: 'embedding',
    texts: string[]
  ): Promise<number[][]>;

  /** 配置变更时清除单个 provider 缓存 */
  invalidateProvider(providerId: string): void;

  /** 清除全部缓存(全局配置变更时) */
  invalidateAll(): void;
}

4. 四个 Provider Adapter 核心差异映射

4.1 总览对照表

特性 openai_compatible openai_responses anthropic gemini
SDK/HTTP openai npm (chat.completions) openai npm (responses.create) @anthropic-ai/sdk @google/generative-ai 或 REST
系统指令 messages[0].role='system' instructions 参数 system 顶层参数 systemInstruction 参数
JSON mode response_format: {type:'json_object'} text.format: {type:'json_object'} 无原生支持 → prompt 指令 + JSON.parse responseMimeType: 'application/json' + responseSchema
工具调用请求 tools[].type='function' + function.{name,description,parameters} tools[].type='function' + function.{name,description,parameters} tools[].name + input_schema tools[].functionDeclarations[].{name,description,parameters}
工具结果返回 role: 'tool' + tool_call_id type: 'function_call_output' + call_id role: 'user' + content: [{type:'tool_result', tool_use_id}] role: 'function' + parts: [{functionResponse}]
finish_reason stop / tool_calls / length stop / tool_calls / ... end_turn / tool_use / max_tokens STOP / FUNCTION_CALL / MAX_TOKENS
Token 用量 usage.{prompt,completion}_tokens usage.{input,output}_tokens usage.{input,output}_tokens usageMetadata.{prompt,candidates}TokenCount

4.2 各 Adapter 核心转换逻辑

4.2.1 openai_compatible现有兼容格式

// 请求转换:几乎直通(这就是现有代码逻辑的抽象)
// - LLMMessage → OpenAI ChatCompletionMessage (直接映射)
// - responseFormat='json' → { type: 'json_object' }
// - tools → tools[].function (直接映射)
//
// 响应转换:
// - choices[0].message.content → content
// - choices[0].message.tool_calls → toolCalls
// - choices[0].finish_reason → finishReason (直接映射)
// - usage.{prompt,completion}_tokens → usage

4.2.2 openai_responsesResponses API

// 请求转换:
// - system message 提取为 instructions 参数
// - 非 system messages 转为 input items
// - responseFormat='json' → text: { format: { type: 'json_object' } }
// - tools → tools[].function
//
// 响应转换:
// - output items 中 type='message' → content
// - output items 中 type='function_call' → toolCalls
// - status → finishReason 映射
// - usage.{input,output}_tokens → usage

4.2.3 anthropicMessages API

// 请求转换:
// - system message 提取为 system 顶层参数
// - 非 system messages → messagesrole 直接映射)
// - responseFormat='json' → 无原生支持,在 system prompt 末尾追加:
//   "You MUST respond with valid JSON only. No other text."
// - tools → tools[].{ name, description, input_schema }
// - tool results → role='user', content: [{ type: 'tool_result', tool_use_id, content }]
//
// 响应转换:
// - content blocks: type='text' → content
// - content blocks: type='tool_use' → toolCalls (id, name, JSON.stringify(input))
// - stop_reason: 'end_turn' → 'stop', 'tool_use' → 'tool_calls', 'max_tokens' → 'length'
// - usage.{input,output}_tokens → usage
//
// JSON mode 容错:
// - JSON.parse(content) 失败时,尝试正则提取 ```json...``` 块

4.2.4 geminigenerateContent API

// 请求转换:
// - system message 提取为 systemInstruction: { parts: [{ text }] }
// - messages → contents[].{ role: 'user'|'model', parts: [{ text }] }
//   注意Gemini 用 'model' 而非 'assistant'
// - responseFormat='json' → generationConfig: {
//     responseMimeType: 'application/json',
//     responseSchema: <如果有的话>
//   }
// - tools → tools: [{ functionDeclarations: [...] }]
// - tool results → role: 'function', parts: [{ functionResponse: { name, response } }]
//
// 响应转换:
// - candidates[0].content.parts: type='text' → content
// - candidates[0].content.parts: functionCall → toolCalls
// - candidates[0].finishReason: 'STOP' → 'stop', 'FUNCTION_CALL' → 'tool_calls', 'MAX_TOKENS' → 'length'
// - usageMetadata.{promptTokenCount, candidatesTokenCount} → usage

4.3 tool-converter.ts 接口

// ── src/llm/tool-converter.ts ───────────────────────────────

import type { LLMToolDefinition } from './types';

/**
 * 将内部通用 LLMToolDefinition 转为各 provider 原生格式。
 * 由各 adapter 在 chat() 中调用。
 */

/** → OpenAI / OpenAI Compatible 格式 */
export function toOpenAITools(tools: LLMToolDefinition[]): object[];

/** → Anthropic 格式 */
export function toAnthropicTools(tools: LLMToolDefinition[]): object[];

/** → Gemini functionDeclarations 格式 */
export function toGeminiTools(tools: LLMToolDefinition[]): object[];

5. 后端 REST API 契约

所有新 API 挂在 /admin/api/llm/ 下,复用现有 JWT 鉴权中间件。

5.1 Provider 管理

Method Path 说明
GET /admin/api/llm/providers 列出所有 providerhasKey 布尔,不含明文 key
POST /admin/api/llm/providers 创建 provider + 设置 API Key
GET /admin/api/llm/providers/:id 获取单个详情
PUT /admin/api/llm/providers/:id 更新name/base_url/default_model/extra_config/is_enabled
DELETE /admin/api/llm/providers/:id 删除(级联删 secret + role assignments

5.2 API Key仅 set/clear不回显

Method Path 说明
PUT /admin/api/llm/providers/:id/key 设置/更新 API Key
DELETE /admin/api/llm/providers/:id/key 清除 API Key

5.3 角色 → 模型映射

Method Path 说明
GET /admin/api/llm/roles 列出所有角色及当前绑定
PUT /admin/api/llm/roles/:role 设置角色绑定

5.4 连通性测试

Method Path 说明
POST /admin/api/llm/providers/:id/test 发送简单 prompt 验证 provider 可达

5.5 通用设置(非 LLM

Method Path 说明
GET /admin/api/settings 列出所有sensitive 字段 masked
PUT /admin/api/settings 批量更新

5.6 请求/响应示例

创建 Provider

// POST /admin/api/llm/providers
// Request:
{
  "name": "Anthropic Claude",
  "type": "anthropic",
  "baseUrl": null,
  "defaultModel": "claude-sonnet-4-20250514",
  "apiKey": "sk-ant-xxxx",
  "extraConfig": {}
}

// Response 201:
{
  "id": "a1b2c3d4",
  "name": "Anthropic Claude",
  "type": "anthropic",
  "baseUrl": null,
  "defaultModel": "claude-sonnet-4-20250514",
  "isEnabled": true,
  "hasKey": true,
  "extraConfig": {},
  "createdAt": "2026-03-04T12:00:00Z"
}

设置角色绑定

// PUT /admin/api/llm/roles/specialist
// Request:
{
  "providerId": "a1b2c3d4",
  "model": "claude-sonnet-4-20250514"
}

// Response 200:
{
  "role": "specialist",
  "providerId": "a1b2c3d4",
  "providerName": "Anthropic Claude",
  "providerType": "anthropic",
  "model": "claude-sonnet-4-20250514"
}

连通性测试

// POST /admin/api/llm/providers/a1b2c3d4/test
// Response 200:
{
  "success": true,
  "latencyMs": 823,
  "model": "claude-sonnet-4-20250514",
  "message": "Hello! I'm Claude, an AI assistant."
}

// Response 200 (失败):
{
  "success": false,
  "latencyMs": 5012,
  "error": "401 Unauthorized: Invalid API key"
}

6. 密钥安全设计

6.1 Master Key 管理

启动流程:
  1. 读取环境变量 ENCRYPTION_KEYhex 编码64 字符)
     ├── 未设置或为空 → 抛出错误,拒绝启动
     ├── 长度不正确   → 抛出错误,提示需要 64 个十六进制字符
     └── 正确         → 解码为 32 字节 Buffer
  2. 主密钥常驻内存(进程生命周期)
  3. 绝对不写入日志、不暴露给 API

6.2 加密流程(写 API Key

输入: plaintext apiKey (string)
  1. 生成 12 bytes IV: crypto.randomFillSync(new Uint8Array(12))
  2. 创建 cipher: crypto.createCipheriv('aes-256-gcm', masterKey, iv)
  3. 加密: ciphertext = Buffer.concat([cipher.update(apiKey, 'utf8'), cipher.final()])
  4. 获取 auth tag: authTag = cipher.getAuthTag()  // 16 bytes
  5. 写 DB: INSERT INTO llm_secrets (provider_id, ciphertext, iv, auth_tag, key_version)

6.3 解密流程Gateway 需要调 provider

输入: provider_id
  1. 从 DB 读取: { ciphertext, iv, auth_tag, key_version }
  2. 创建 decipher: crypto.createDecipheriv('aes-256-gcm', masterKey, iv)
  3. 设置 auth tag: decipher.setAuthTag(authTag)
  4. 解密: plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()])
  5. 返回明文 API Key → 传给 provider factory
  6. 解密后的 key 随 provider 实例缓存在 Gateway.cache 中

6.4 密钥轮换

场景: 管理员更换 ENCRYPTION_KEY
  1. 启动时读取新的 ENCRYPTION_KEY 环境变量
  2. 查询所有 llm_secrets WHERE key_version < current_version
  3. 逐条: 用旧 key 解密 → 用新 key 重加密 → 更新 key_version
  4. 如果旧 key 不可用(环境变量缺失)→ 启动报错,要求重新设置所有 API Key

7. 前端配置页设计

7.1 页面结构

Settings 页面
├── 🔌 LLM ProvidersTab 或独立 Card
│   │
│   ├── Provider 列表表格
│   │   ┌──────────────────────────────────────────────────────────────┐
│   │   │ 名称              │ 类型          │ 默认模型      │ Key │ 状态   │ 操作 │
│   │   ├───────────────────┼───────────────┼───────────────┼─────┼────────┼──────┤
│   │   │ 公司 OpenAI 代理  │ OpenAI Compat │ gpt-4o-mini   │ ●   │ [开关] │ [⋮] │
│   │   │ Anthropic Claude  │ Anthropic     │ claude-sonnet  │ ●   │ [开关] │ [⋮] │
│   │   │ Google Gemini     │ Gemini        │ gemini-2.5-pro│ ○   │ [开关] │ [⋮] │
│   │   └──────────────────────────────────────────────────────────────┘
│   │   + 添加 Provider 按钮
│   │
│   ├── 添加/编辑 Provider 对话框
│   │   ├── 名称 (text input)
│   │   ├── 类型 (select dropdown)
│   │   │   ├── OpenAI Compatible   — 兼容 OpenAI 接口的第三方服务
│   │   │   ├── OpenAI Responses    — OpenAI 官方 Responses API
│   │   │   ├── Anthropic           — Anthropic Messages API
│   │   │   └── Gemini              — Google Gemini API
│   │   ├── Base URL (text, 条件显示openai_compatible 必填, 其他可选)
│   │   ├── 默认模型 (text + autocomplete suggestions)
│   │   ├── API Key (password input, 已有时显示 ••••••••)
│   │   ├── ▸ 高级配置 (collapsible, JSON key-value editor)
│   │   └── [测试连接] [保存] [取消]
│   │
│   └── 🧩 角色分配与分级审查映射 区域
│       ┌──────────────────────────────────────────────────────────────┐
│       │ 角色/阶段            │ Provider 下拉        │ 模型 ID              │
│       ├──────────────┼──────────────────────┼──────────────────────┤
│       │ Planner(Triage)      │ [公司 OpenAI 代理 ▾] │ [gpt-4o-mini      ] │
│       │ Specialist(任务执行) │ [Anthropic Claude ▾] │ [claude-sonnet-4   ] │
│       │ Judge(汇总裁决)      │ [公司 OpenAI 代理 ▾] │ [gpt-4o           ] │
│       │ Embedding(记忆检索)  │ [公司 OpenAI 代理 ▾] │ [text-embedding-3  ] │
│       └──────────────────────────────────────────────────────────────┘
│       [保存角色分配]
│
├── ⚙️ 通用设置(现有 Gitea / 飞书 / App / Review 参数)
│   ├── Agent 分级审查参数small/medium 阈值、token budget、triage 开关
│   └── (复用现有 ConfigManager 组件,数据源统一为 DB)

7.2 交互规则

交互 行为
添加 Provider 弹出对话框;类型选择后动态显示/隐藏 base_url 字段
API Key 输入 已有 key 时展示 ••••••••readonly 占位);清空内容后保存 = 删除 key输入新值 = 替换(调用 PUT /key);未修改 = 不发请求
测试连接 点击后调 POST /providers/:id/test;显示 spinner → 成功绿色 toast延迟+模型)/ 失败红色 toast错误信息
角色分配下拉 仅显示 is_enabled=truehasKey=true 的 provider选择后自动填充该 provider 的 default_model(用户可修改)
禁用 Provider 如果有角色绑定到此 provider → 弹确认对话框:"此 Provider 正被以下角色使用:[...],禁用后这些角色将无法调用 LLM。确定禁用"
删除 Provider 同上,级联影响提示更强烈
模型建议 根据 provider type 显示常见模型建议列表(硬编码在前端,仅作参考,不限制输入)

7.3 模型建议列表(前端硬编码参考)

const MODEL_SUGGESTIONS: Record<string, string[]> = {
  openai_compatible: ['gpt-4o', 'gpt-4o-mini', 'gpt-4-turbo', 'deepseek-chat', 'qwen-plus'],
  openai_responses: ['gpt-4o', 'gpt-4o-mini', 'gpt-4.1', 'gpt-4.1-mini', 'o3-mini'],
  anthropic: ['claude-sonnet-4-20250514', 'claude-3-5-haiku-20241022', 'claude-opus-4-20250514'],
  gemini: ['gemini-2.5-pro', 'gemini-2.5-flash', 'gemini-2.0-flash'],
};

8. 现有调用点改造清单

8.1 后端代码改造

# 文件 当前代码 改造为 影响范围
1 src/index.ts:69-71 const openaiClient = new OpenAI({baseURL, apiKey}) 删除;初始化 LLMGateway 单例并传入业务层 入口
2 src/controllers/review.ts 旧版 webhook 存在回退分支 删除回退分支,仅保留 agent / codex 入队逻辑 审查主入口
3 src/review/orchestrator.ts:45,61-63 private readonly openai: OpenAI + this.openai = new OpenAI(...) 构造函数改接收 LLMGateway;传给各 agent任务化分级编排skip/light/full Agent 编排
4 src/review/agents/specialist-agent.ts:93 protected readonly openai: OpenAI protected readonly gateway: LLMGatewayreviewWithOptions() 与 ReAct 调用改为 this.gateway.chatForRole('specialist', ...) 核心 agent
5 src/review/agents/critic-agent.ts:23 private openai: OpenAI 同上 评审 agent
6 src/review/agents/reflexion-agent.ts:24 constructor(openai: OpenAI, ...) 构造传 gateway 反思 agent
7 src/review/agents/debate-orchestrator.ts:17 private openai: OpenAI 同上 辩论 agent
8 src/review/tools/registry.ts:22 toOpenAIFunctions() toToolDefinitions(): LLMToolDefinition[](返回内部格式),由 adapter 的 tool-converter.ts 负责转换 工具注册
9 src/config/config-schema.ts:120-138 OPENAI_BASE_URL/API_KEY/MODEL 字段定义 删除这些字段;group: 'openai' → 整个 group 移除 配置 schema
10 src/config/config-manager.ts:44-46 OPENAI_* Zod schema 条目 删除 配置验证
11 src/config/config-manager.ts:271-273 openai: { baseUrl, apiKey, model } 映射 删除整个 openai 配置输出

8.2 前端代码改造

# 文件 改造内容
1 frontend/src/services/configService.ts 新增 llmProviderService.tsProvider CRUD + Key 管理 + Role 管理 + Test
2 frontend/src/components/ConfigManager.tsx 添加 "LLM Providers" Tab/Card引入新组件
3 新增 frontend/src/components/llm/ProviderList.tsx — Provider 列表表格
4 新增 frontend/src/components/llm/ProviderDialog.tsx — 添加/编辑对话框
5 新增 frontend/src/components/llm/RoleAssignment.tsx — 角色分配面板

8.3 配置层改造

变更 说明
config-manager.ts 精简为只管非 LLM 配置;数据源统一为 system_settings
config-schema.ts 移除 openai group 及其字段;保留 gitea/feishu/app/admin/review非模型字段
controllers/config.ts LLM 相关接口迁到 controllers/llm-config.ts;通用配置接口改读写 DB
.env.example 移除 OPENAI_*REVIEW_MODEL_*;仅保留启动参数

9. 实施阶段建议

阶段 内容 依赖 估时
Phase 1: 基础设施 DB 层 (bun:sqlite 初始化 + DDL) + crypto 模块 1d
Phase 2: LLM 抽象层 src/llm/ 全部types + capabilities + errors + gateway + 4 adapters + tool-converter Phase 1 2d
Phase 3: 后端 API + 调用点替换 controllers/llm-config.ts + 替换 11 个现有 OpenAI 调用点 + 测试 Phase 2 1.5d
Phase 4: 前端改造 Provider 管理 + 角色分配 + 连接测试 UI + 通用设置切 DB Phase 3 1.5d
Phase 5: 清理与验收 删除旧代码 + 更新文档 + E2E 测试 + .env.example 精简 Phase 4 0.5d

总计约 6.5 人天。

关键里程碑

Day 1:    DB + crypto 就绪,配置写入链路打通
Day 3:    LLM Gateway 可用4 个 adapter 通过单元测试
Day 4.5:  后端 API 完成,所有调用点已替换,`bun test` 全绿
Day 6:    前端配置页可用,可通过 UI 添加/测试 Provider
Day 6.5:  旧代码清理完毕文档更新Ready for review

10. 风险与缓解

风险 影响 缓解措施
Anthropic 无原生 JSON mode response_format: json_object 不可用JSON 解析可能失败 Adapter 内 prompt 注入 JSON 指令 + JSON.parse() 容错(正则提取 ```json``` 块 → 重试 parse
Gemini function calling 格式差异大 functionDeclarations 包装层级不同;functionResponse 嵌套在 parts tool-converter.ts 单独处理finish reason 映射表全覆盖测试
Embedding 维度变化导致 Qdrant 不兼容 src/review/memory/vector-store.ts 硬编码 1536 维 model_role_assignments.role='embedding' 变更时UI 提示用户需重建 collection或自动检测维度创建新 collection
ENCRYPTION_KEY 丢失 所有加密的 API Key 不可恢复 启动时检测密钥版本不匹配 → 报错并要求重新设置所有 API Keytrade-off安全性 > 便利性)
SQLite 并发写 多请求同时写入可能 SQLITE_BUSY bun:sqlite 开启 WAL mode写操作走单连接序列化读可并行
Provider SDK 版本冲突 openai@anthropic-ai/sdk@google/generative-ai 三个 SDK 共存 各 adapter 独立 import无交叉依赖package.json 锁定主版本
配置热更新 UI 修改 provider 配置后,正在进行的审查仍用旧配置 Gateway 缓存按 provider_id 粒度 invalidate正在执行的请求不受影响用的是已创建的实例下次请求用新实例

附录 A: 新增依赖

// package.json 新增
{
  "dependencies": {
    // bun:sqlite 是 Bun 内置,无需安装
    "@anthropic-ai/sdk": "^0.39.0",         // Anthropic adapter
    "@google/generative-ai": "^0.24.0"      // Gemini adapter
    // "openai" 已存在: "^4.87.3"           // OpenAI compatible + Responses adapter
  }
}

附录 B: 环境变量精简

# .env.example仅保留启动参数

# 应用启动参数(不可通过 UI 设置)
PORT=5174
WEBHOOK_SECRET=your_webhook_secret
DATABASE_PATH=./data/assistant.db    # SQLite 文件路径

# 以下配置已迁入数据库,通过 Web UI 管理:
# - LLM Provider 配置API Key / Base URL / Model
# - Gitea 配置API URL / Token
# - 飞书配置Webhook URL / Secret
# - Review 引擎配置
# - 记忆系统配置