模型管理
Yuheng 不绑定任何一家模型厂商:对话、向量化、重排、图片理解、语音转写这五类能力都抽象成统一的「模型」,你在「设置 → 模型管理」里添加,然后在知识库和问答上按需选用。本地 Ollama 和 25 家远程厂商(OpenAI、DeepSeek、通义、智谱、混元、Gemini、硅基流动等)都可以混着用,比如用本地小模型做向量化、用远程大模型做回答。
添加模型时注意两点:
- 向量模型选定后别再换。它决定索引里向量的含义与维度,换了之后老数据检索不到,必须重建索引;
- 保存前点一下测试。连不通的模型保存后只会在提问时报错,排查更费劲。
谁能管模型:添加、修改、删除、调试模型需要工作区 Admin 或 Owner;系统设置 governance.centralized_infra 打开后,只有系统管理员能写,工作区成员只能选用。查看与选用模型对所有成员开放。系统管理员还可以把一个模型设为平台共享(在代码里即把它标记为 is_builtin),所有工作区都能看到并使用它,但看不到它的凭据。
下面梳理模型类型、Provider 抽象、配置字段、内置模型、并发限流、连通性测试与用量统计。
模型类型与用途
模型类型定义在 internal/types/model.go:
const (
ModelTypeEmbedding ModelType = "Embedding" // Embedding model
ModelTypeRerank ModelType = "Rerank" // Rerank model
ModelTypeKnowledgeQA ModelType = "KnowledgeQA" // KnowledgeQA model
ModelTypeVLLM ModelType = "VLLM" // VLLM model
ModelTypeASR ModelType = "ASR" // ASR model
)| 类型 | 前端标识 | 客户端包 | 接口 | 用途 |
|---|---|---|---|---|
KnowledgeQA | chat | internal/models/chat | Chat / ChatStream(支持 Tools、Thinking、多模态消息) | 知识问答、摘要 / 问题生成 / 图谱抽取等一切 LLM 调用 |
Embedding | embedding | internal/models/embedding | Embed / BatchEmbed(含 GetDimensions) | 文本向量化,供向量检索索引与查询 |
Rerank | rerank | internal/models/rerank | Rerank(query, documents) 返回 RankResult | 检索结果精排 |
VLLM | vllm | internal/models/vlm | Predict(imgBytes, prompt) | 视觉语言模型(VLM),文档图片理解 / 多模态解析 |
ASR | asr | internal/models/asr | Transcribe(audioBytes, fileName) 返回文本与分段时间戳 | 音频转写(自动语音识别) |
前后端类型映射见 internal/handler/model.go 的 modelTypeToFrontend()(KnowledgeQA -> chat 等)。
模型来源(ModelSource)核心取值为两个:local(本地 Ollama 拉起)与 remote(远程 API);其余历史值(aliyun、zhipu、openai 等)为兼容保留,路由行为等同 remote + 对应 provider。
Provider 抽象
internal/models/provider/provider.go 定义了多厂商适配的统一注册表:
type Provider interface {
// Info 返回服务商的元数据
Info() ProviderInfo
// ValidateConfig 验证服务商的配置
ValidateConfig(config *Config) error
}每个厂商在自己的文件(如 provider/openai.go、provider/aliyun.go)中通过 init() 调用 Register() 注册自身,ProviderInfo 携带 DisplayName、Description、按模型类型区分的 DefaultURLs、支持的 ModelTypes、RequiresAuth 以及可选的 ExtraFields(例如 Azure OpenAI 声明了 api_version 额外字段,默认 2024-10-21)。
支持的厂商清单
AllProviders()(provider/provider.go)返回的完整列表(共 25 个,每个厂商在自己的文件里 init() 注册)。表格最后一行的 Ollama 不在其中,它走 source=local 这条独立路径,列在这里只为方便对照:
| Provider 标识 | 名称 | 说明 |
|---|---|---|
generic | Generic | 任意 OpenAI 兼容 / 自定义部署(默认兜底) |
aliyun | 阿里云 DashScope | |
zhipu | 智谱 AI(GLM 系列) | |
volcengine | 火山引擎 Ark | |
hunyuan | 腾讯混元 | |
siliconflow | 硅基流动 | |
deepseek | DeepSeek | |
minimax | MiniMax | |
moonshot | 月之暗面 Moonshot (Kimi) | |
modelscope | 魔搭 ModelScope | |
qianfan | 百度千帆 | |
qiniu | 七牛云 | |
openai | OpenAI | 五种模型类型全支持 |
anthropic | Anthropic Claude | 独立 Messages 协议实现 |
gemini | Google Gemini | Embedding 走专用 API |
openrouter | OpenRouter | |
requesty | Requesty | |
jina | Jina AI | Embedding 与 Rerank |
mimo | 小米 MiMo | |
longcat | 美团 LongCat AI | |
lkeap | 腾讯云 LKEAP(知识引擎原子能力) | 提供专用 Rerank 实现 |
gpustack | GPUStack(私有化部署) | |
nvidia | NVIDIA | 专用 Embedding / Rerank 实现 |
novita | Novita AI | |
azure_openai | Azure OpenAI | 额外字段 api_version |
ollama(source=local) | Ollama 本地模型 | 非 Provider 注册表成员,由 ModelSourceLocal 路由 |
当模型未显式指定 provider 时,DetectProvider(baseURL) 会按 BaseURL 域名特征自动识别(如 dashscope.aliyuncs.com -> aliyun、api.anthropic.com -> anthropic),识别失败回落为 generic。
协议路由
internal/models/chat/chat.go 的 NewRemoteChat:
func NewRemoteChat(config *ChatConfig) (Chat, error) {
providerName := provider.ProviderName(config.Provider)
if providerName == "" {
providerName = provider.DetectProvider(config.BaseURL)
}
if providerName == provider.ProviderAnthropic {
return NewAnthropicChat(config) // 独立 Messages 协议
}
return NewRemoteAPIChat(config) // 统一 OpenAI 兼容协议 + providerAdapter
}- Ollama(
source=local):chat/ollama.go、embedding/ollama.go、vlm/ollama.go通过internal/models/utils/ollama的OllamaService直连本机 Ollama。 - Anthropic:
chat/anthropic.go实现 Messages 协议。 - 其余远程厂商:统一走
chat/remote_api.go的 OpenAI 兼容 Chat Completions 实现,厂商差异(thinking 编码、参数兼容等)由构造时解析的providerAdapter处理。 - Embedding 有更多专用实现:阿里云多模态(
tongyi-embedding-vision-*走 DashScope 专用端点,纯文本模型自动改写为/compatible-mode/v1OpenAI 兼容端点)、Volcengine 多模态、Jina、Azure OpenAI、NVIDIA、Gemini、Zhipu,其余为 OpenAI 兼容(embedding/openai.go)。 - Rerank 专用实现:Aliyun、Zhipu、Jina、NVIDIA、LKEAP、Volcengine,默认
NewOpenAIReranker(通用/rerank风格接口)。两个厂商有额外适配:- LKEAP:腾讯云
RunRerank限制单次最多 60 篇文档、Query 与 Docs 合计不超过 2000 字符。lkeapRerankBatches按这两个上限自动切批并回填全局下标,调用方不用感知分批;单篇文档自身就超限时直接报错并指出下标。 - Volcengine:候选集超过接口单次文档上限时自动切成多批并发打分再合并(并发上限见
volcengineRerankMaxConcurrency),不会静默截断候选。 - NVIDIA:接口返回的是原始 logit 而非 [0,1] 概率。
normalizeNvidiaLogit用数值稳定的 sigmoid 归一化(负数走e^x/(1+e^x)分支避免溢出),否则RerankThreshold这类阈值配置在该厂商下完全失效。
- LKEAP:腾讯云
- ASR:阿里云百炼(DashScope)没有 OpenAI 兼容的转写端点,provider 为
aliyun、来源为aliyun或 BaseURL 识别为 DashScope 时走asr/aliyun.go:通过多模态生成接口调用 Qwen-ASR(如qwen3-asr-flash),音频以 base64 内联,返回不带时间戳的纯文本,视频的时间线靠分段转写得到(见文档解析服务 §3.14)。其余厂商使用 OpenAI 兼容/v1/audio/transcriptions(asr/openai.go),返回文本与分段时间戳。
模型调用链
工厂函数在真实客户端外层依次套上三个装饰器(见 chat.NewChat / embedding.NewEmbedder / vlm.NewVLM):
c, err = wrapChatDebug(c, err)
c, err = wrapChatLangfuse(c, err)
// Outermost: hold the per-model concurrency slot only around the real
// provider round-trip, so the wait is excluded from debug/langfuse timing.
return wrapChatConcurrency(c, config.MaxConcurrency, err)模型配置字段
模型实体 types.Model 的 Parameters(internal/types/model.go 的 ModelParameters):
| 名称 | 类型 | 默认值 | 说明 |
|---|---|---|---|
base_url | string | 空(可用 Provider 的 DefaultURLs) | 模型 API 地址,创建/更新时经过 SSRF 校验(ValidateURLForSSRF) |
api_key | string | 空 | API 密钥,AES-256-GCM 加密落库(ModelParameters.Value/Scan),仅通过 PUT /models/:id/credentials 子资源修改 |
interface_type | string | 空(VLM:local 默认 ollama,remote 默认 openai) | 接口协议类型 |
embedding_parameters.dimension | int | 0 | 向量维度 |
embedding_parameters.truncate_prompt_tokens | int | 0 | 输入截断 token 数 |
embedding_parameters.supports_dimension_override | bool | false | 是否支持请求级维度覆盖(dimensions 参数) |
parameter_size | string | 空 | Ollama 模型参数规模(如 "7B"),后端维护、前端不可改 |
provider | string | 空(按 BaseURL 自动检测) | 厂商标识 |
extra_config | map[string]string | nil | 厂商专属配置(如 Azure 的 api_version) |
custom_headers | map[string]string | nil | 附加自定义 HTTP 请求头(类似 OpenAI SDK extra_headers;Authorization、api-key 等保留头在运行期被忽略) |
supports_vision | bool | false | Chat 模型是否接受图片多模态输入 |
max_concurrency | int | 0(回落到全局 model.max_concurrency) | 该模型后台任务并发上限(仅 chat/vlm/embedding 生效) |
app_id / app_secret | string | 空 | app-id + 签名密钥类 provider 的凭证,app_secret AES 加密存储 |
模型级字段还包括 name(运行期实际调用的模型名)、display_name、type、source、is_default(同一 (tenant_id, type) 桶内唯一默认)、is_builtin、managed_by、status(active / downloading / download_failed)。
管理 API(internal/router/routes_infra.go)
| 方法 & 路径 | 说明 | 权限 |
|---|---|---|
GET /models/providers | 按 model_type 查询支持的厂商列表(ListModelProviders) | Viewer+ |
GET /models / GET /models/:id | 列表 / 详情(响应不含明文密钥) | Viewer+ |
POST /models / PUT /models/:id / DELETE /models/:id | 创建 / 修改 / 删除 | PlatformManaged |
PUT /models/:id/credentials、DELETE /models/:id/credentials/:field | 凭证子资源;PUT /models/:id 请求体中的 api_key 会被强制忽略并告警 | PlatformManaged |
POST /models/:id/debug | 模型调试(见下文),会发起真实上游调用并产生费用 | PlatformManaged |
PUT /models/:id/sharing | 设为 / 取消平台共享;取消时若还有任何工作区的知识库在用该模型则拒绝 | 仅系统管理员(服务层强制) |
PlatformManaged 守卫:governance.centralized_infra 关闭(默认)时为工作区 Admin+(系统管理员也可),打开后仅系统管理员。内置模型(is_builtin)的凭据只有系统管理员能改。API Key 调用这些接口需要 manage_models 能力或完全访问。
内置模型
内置模型是对部署里所有工作区可见、可选用的模型,用来给每个工作区提供统一的默认模型服务。工作区成员(包括工作区 Admin / Owner)只能看到模型名、类型这些能力信息,看不到 Base URL 和凭据,也不能修改;只有系统管理员能编辑它的配置和凭据。数据上它就是 models 表里 is_builtin=true 的行,默认挂在租户 10000(DefaultBuiltinModelTenantID)下。
有三种方式产生内置模型:
| 方式 | 适合 | 行的 managed_by | 怎么下线 |
|---|---|---|---|
| 界面:「设置 → 模型管理」,模型卡片菜单里「设为平台共享」 | 最直接,随时开关 | 空(运行时托管) | 同一菜单「取消平台共享」,或在界面删除 |
config/builtin_models.yaml 声明 | 需要版本化、随部署脚本下发 | yaml | 从文件里删掉条目后重启 |
直接写 SQL(INSERT … is_builtin = true,或把已有模型 UPDATE … SET is_builtin = true) | 兼容旧做法 | 空 | 用 SQL 改回 is_builtin = false 或删除 |
几条护栏:
- 取消共享或删除前检查引用:只要还有任何工作区的知识库绑定该模型,操作被拒绝并给出引用数。
knowledge_bases.embedding_model_id这类列没有外键,强行撤回会让那些工作区的检索悄悄失效。 - YAML 托管的行不能在界面删除:删了下次启动会被写回,接口直接返回错误,要去 YAML 里删。
- 界面保存即接管:系统管理员在界面上保存过某个 YAML 托管的模型(改参数或凭据)后,该行
managed_by被清空,之后启动不再用 YAML 覆盖它,界面上的修改重启后仍然有效。想交还给 YAML 管理,需要在数据库里把managed_by改回yaml。 - 凭据按
SYSTEM_AES_KEY加密存储;界面对非系统管理员隐藏 API Key 与 Base URL,但数据库里仍有原始(加密后的)数据,数据库访问权限要妥善控制。
用 YAML 声明内置模型
- 复制
config/builtin_models.yaml.example为config/builtin_models.yaml(或用环境变量BUILTIN_MODELS_CONFIG指向别的路径); - 在
docker-compose.yml的app服务里取消- ./config/builtin_models.yaml:/app/config/builtin_models.yaml:ro这一行挂载的注释; - 把文件里引用的变量写进项目根目录的
.env。app服务通过env_file: .env读取整个文件,不需要在environment:里逐个列出(这里用的是数组写法,.env不存在时 Compose 会报错,scripts/start_all.sh在缺失时会从.env.example复制一份); - 重启
app,看日志确认:
docker compose logs app | grep builtin-models
# [builtin-models] upserted: id=builtin-llm-default name=gpt-4o-mini type=KnowledgeQA
# [builtin-models] applied: 1 upserted, 0 pruned from /app/config/builtin_models.yaml条目格式(字段含义与 types.Model 一致):
builtin_models:
- id: builtin-llm-default # 必填,稳定 ID,按它幂等 UPSERT;建议 builtin-{类型}-{名称}
tenant_id: 10000 # 可省,默认 10000
type: KnowledgeQA # KnowledgeQA | Embedding | Rerank | VLLM | ASR
source: remote # 可省,默认 remote
status: active # 可省,默认 active
is_default: true # 设为该类型默认模型
name: ${LLM_MODEL_NAME}
description: 平台默认对话模型
parameters:
base_url: ${LLM_BASE_URL}
api_key: ${LLM_API_KEY}
provider: ${LLM_PROVIDER} # openai | generic | aliyun | …
# embedding_parameters: # 仅 Embedding 类型
# dimension: 1536
# truncate_prompt_tokens: 0${NAME} 插值规则:字符串字段里的 ${NAME} 替换为环境变量的值;变量未设置或为空时保留字面量 ${NAME},让上游返回的 401 一眼能看出是哪个变量没配。不支持 ${VAR:-default} 这类 shell 扩展。非字符串字段(type、is_default、dimension 等)必须写字面值。
加载与对账的实现
internal/types/builtin_models_config.go 的 LoadBuiltinModelsConfig 在每次启动时执行:
- 文件不存在(或是个目录——Docker 绑定挂载一个不存在的源文件时会替换成目录)时记一行日志跳过,不报错;
- 读取失败或 YAML 解析失败只打 WARN 并跳过整次对账,不执行漂移清理,一次手抖的改动不会成批软删已有模型;
- 单个条目校验不通过(id 为空或超过 64 字符
ModelIDMaxLen、type 非法、status 非法),或查询 / UPSERT 失败时,跳过该条、保留它在库里的现有行,并且本次不做漂移清理(日志drift sweep skipped):没能完整应用的文件不足以说明「哪些模型该保留」,写错 type 或丢了id:的条目不应被当成「已从文件删除」而软删掉仍在使用的模型。文件改正后的第一次启动会补做清理; - 每个条目按
idUPSERT,写入is_builtin=true、managed_by="yaml",并把deleted_at重置为 NULL(从文件里拿掉再加回来等于恢复);已被运行时接管(managed_by为空)的同 ID 行跳过,日志preserving runtime override; is_default: true的条目先清掉同(tenant_id, type)里其他行的默认标记,保持「每类一个默认」;- 漂移清理(仅在所有条目都应用成功时执行):
managed_by='yaml'且 id 不在本次文件里的行被软删除。手工 SQL 插入或界面共享的行(managed_by为空)永远不受影响。
需要临时停用 YAML 接管又不想改文件时,把 BUILTIN_MODELS_CONFIG 指向一个不存在的路径并重启:加载器看到文件缺失直接跳过,也不做漂移清理,已写入的行原样保留。
本地模型下载(Ollama)
本地模型的生命周期由 internal/models/utils/ollama/ollama.go 的 OllamaService 管理(IsModelAvailable / PullModel / EnsureModelAvailable / ListModelsDetailed / DeleteModel 等),HTTP 入口在 internal/handler/initialization.go:
| 路径 | 说明 |
|---|---|
GET /initialization/ollama/status | Ollama 服务可用性 |
GET /initialization/ollama/models | 列出本地已有模型 |
POST /initialization/ollama/models/check | 批量检查模型是否已下载 |
POST /initialization/ollama/models/download | 异步下载(downloadModelAsync + pullModelWithProgress,写入模型 status=downloading) |
GET /initialization/ollama/download/progress/:taskId、GET /initialization/ollama/download/tasks | 下载进度 / 任务列表 |
注意:
cmd/download/duckdb/duckdb.go与模型无关——它在构建镜像时预下载 DuckDB 的spatial、excel扩展,供入库时为 CSV / Excel 生成表格摘要分块使用(internal/application/service/extract.go的DataTableSummaryService)。模型权重下载只发生在 Ollama 路径。
Ollama 服务地址由环境变量 OLLAMA_BASE_URL 指定;下载、检查模型属于写操作,同样受 PlatformManaged 守卫约束。
并发与限流(limiter)
internal/models/limiter 提供按模型 ID 的分布式后台并发闸门,核心设计(limiter.go 包注释):共享的稀缺资源是模型厂商的请求预算,因此在模型客户端层(唯一能看到所有任务类型的位置)限流,而不是在 asynq 队列层。
- Redis 后端(
NewRedisLimiter):自愈式分布式信号量。每个持有的槽位是 ZSET 成员(唯一 token),score 为租约到期时间;acquireScriptLua 脚本原子地清理过期租约、计数、在限额内准入。租约 TTL 30s,持有方每 TTL/3 心跳续租(同时续 ZSET key 自身的 TTL),进程崩溃后租约自然过期回收。任何后端错误都 fail-open——限流器故障绝不能阻断模型流量。 - Local 后端(
NewLocalLimiter):无 Redis(单进程)部署下的进程内计数信号量。 - 仅后台任务被限流:
GateNamedN(governor.go)只在types.IsBackgroundTask(ctx)为真(asynq worker:摘要、问题生成、图谱抽取、多模态增强等)时排队;交互式用户请求永不被闸门阻塞。 - 限额优先取模型自身
parameters.max_concurrency,为 0 时回落全局默认:系统设置model.max_concurrency> 环境变量YUHENG_MODEL_MAX_CONCURRENCY> 32。全局值在运行时修改后通过SetGlobalLimit热更新;设为 0 或负数即关闭闸门。 - 运行时观测:
GET /system/admin/runtime/queues(internal/handler/system.go)返回limiter.RuntimeStats()的每模型active / waiting / limit(Redis 后端 active 为集群级,waiting 为进程本地)。
模型健康检查 / 连通性测试
两套机制,均在服务端持有凭证、不回传明文密钥:
测试连接(
internal/handler/initialization.go,供模型创建/编辑表单的 "Test connection" 按钮):POST /initialization/remote/check— Chat 模型(CheckRemoteModel/checkChatModelConnection)POST /initialization/embedding/test— Embedding(TestEmbeddingModel)POST /initialization/rerank/check— Rerank(CheckRerankModel)POST /initialization/asr/check— ASR(CheckASRModel)POST /initialization/multimodal/test— VLM 多模态解析(TestMultimodalFunction)
请求体
ModelTestRequest可携带modelId:fillSecretsFromStoredModel会把请求中缺失的APIKey/AppSecret从已存模型(解密后)补齐,实现"改 BaseURL 用旧密钥一键验证",前端无需也无法拿到明文密钥。buildTestModel把请求转换为不落库的临时*types.Model,与生产路径共享同一套ConfigFromModel映射。模型调试器(
POST /models/:id/debug,ModelHandler.DebugModel):对已保存模型按类型发起真实调用并返回完整归一化响应——Chat 走流式并聚合stream_events/ thinking 观测项;Embedding 返回向量与维度;Rerank 返回打分结果;VLM / ASR 接受上传文件。响应含elapsed_ms、脱敏后的请求预览(redactedDebugConfig隐去 secret/token/api_key 类字段)与observations。
自托管 Rerank 服务的协议
任何私有 Rerank 服务只要实现如下最小协议,就可以以 generic provider 接入 Yuheng:暴露 POST /rerank,请求体 {query, documents},返回 {"results": [{index, document: {text}, score}]}。
Go 客户端对返回格式做了宽松兼容(internal/models/rerank/reranker.go):RankResult.UnmarshalJSON 优先读取 relevance_score,缺失时回退到 score;DocumentInfo.UnmarshalJSON 同时兼容字符串与 {text} 对象两种格式。
模型用量统计
Token 用量:
types.TokenUsage(internal/types/chat.go)记录prompt_tokens / completion_tokens / total_tokens及 prompt cache 细分(cache_read_tokens / cache_write_tokens / cache_miss_tokens / cache_status)。每个 Chat 实现通过internal/models/chat/usage.go的logUsage输出统一的结构化日志行:gologger.Infof(ctx, "[LLM Usage] model=%s, purpose=%s, prompt_prefix=%s, prompt_tokens=%d, completion_tokens=%d, ...", ...)其中
purpose来自types.WithLLMCallMetadata(如document_summary、question_generation、entity_extraction、document_auto_tag、query_rewrite),可按用途聚合。链路追踪:启用 Langfuse 时,每类模型都有
langfuse_wrapper.go装饰器把调用(含 usage)上报为 trace/span。流式响应:usage 随最后的
StreamResponse事件返回(模型调试器会将其聚合进usage字段)。并发水位:如上节所述,
GET /system/admin/runtime/queues暴露每模型实时active / waiting / limit。
实现参考
想读源码时按下表定位(路径相对仓库根目录):
| 层 | 文件 |
|---|---|
| 模型实体与参数 | internal/types/model.go |
| 内置模型加载 | internal/types/builtin_models_config.go、config/builtin_models.yaml.example |
| 厂商注册表 | internal/models/provider/ |
| 各类模型客户端 | internal/models/{chat,embedding,rerank,vlm,asr}/ |
| Ollama | internal/models/utils/ollama/、internal/handler/initialization.go |
| 后台并发闸门 | internal/models/limiter/、internal/container/container.go(resolveModelMaxConcurrency) |
| 模型服务 | internal/application/service/model.go |
| Handler | internal/handler/model.go、model_credentials.go |
| 路由与守卫 | internal/router/routes_infra.go、internal/router/rbac.go(PlatformManaged) |