Skip to content

模型管理 ​

Yuheng 不绑定任何一家模型厂商:对话、向量化、重排、图片理解、语音转写这五类能力都抽象成统一的「模型」,你在「设置 → 模型管理」里添加,然后在知识库和问答上按需选用。本地 Ollama 和 25 家远程厂商(OpenAI、DeepSeek、通义、智谱、混元、Gemini、硅基流动等)都可以混着用,比如用本地小模型做向量化、用远程大模型做回答。

添加模型时注意两点:

  • 向量模型选定后别再换。它决定索引里向量的含义与维度,换了之后老数据检索不到,必须重建索引;
  • 保存前点一下测试。连不通的模型保存后只会在提问时报错,排查更费劲。

谁能管模型:添加、修改、删除、调试模型需要工作区 Admin 或 Owner;系统设置 governance.centralized_infra 打开后,只有系统管理员能写,工作区成员只能选用。查看与选用模型对所有成员开放。系统管理员还可以把一个模型设为平台共享(在代码里即把它标记为 is_builtin),所有工作区都能看到并使用它,但看不到它的凭据。

下面梳理模型类型、Provider 抽象、配置字段、内置模型、并发限流、连通性测试与用量统计。

模型类型与用途 ​

模型类型定义在 internal/types/model.go:

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
)
类型前端标识客户端包接口用途
KnowledgeQAchatinternal/models/chatChat / ChatStream(支持 Tools、Thinking、多模态消息)知识问答、摘要 / 问题生成 / 图谱抽取等一切 LLM 调用
Embeddingembeddinginternal/models/embeddingEmbed / BatchEmbed(含 GetDimensions)文本向量化,供向量检索索引与查询
Rerankrerankinternal/models/rerankRerank(query, documents) 返回 RankResult检索结果精排
VLLMvllminternal/models/vlmPredict(imgBytes, prompt)视觉语言模型(VLM),文档图片理解 / 多模态解析
ASRasrinternal/models/asrTranscribe(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 定义了多厂商适配的统一注册表:

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 标识名称说明
genericGeneric任意 OpenAI 兼容 / 自定义部署(默认兜底)
aliyun阿里云 DashScope
zhipu智谱 AI(GLM 系列)
volcengine火山引擎 Ark
hunyuan腾讯混元
siliconflow硅基流动
deepseekDeepSeek
minimaxMiniMax
moonshot月之暗面 Moonshot (Kimi)
modelscope魔搭 ModelScope
qianfan百度千帆
qiniu七牛云
openaiOpenAI五种模型类型全支持
anthropicAnthropic Claude独立 Messages 协议实现
geminiGoogle GeminiEmbedding 走专用 API
openrouterOpenRouter
requestyRequesty
jinaJina AIEmbedding 与 Rerank
mimo小米 MiMo
longcat美团 LongCat AI
lkeap腾讯云 LKEAP(知识引擎原子能力)提供专用 Rerank 实现
gpustackGPUStack(私有化部署)
nvidiaNVIDIA专用 Embedding / Rerank 实现
novitaNovita AI
azure_openaiAzure 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:

go
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/v1 OpenAI 兼容端点)、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 这类阈值配置在该厂商下完全失效。
  • 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):

go
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_urlstring空(可用 Provider 的 DefaultURLs)模型 API 地址,创建/更新时经过 SSRF 校验(ValidateURLForSSRF)
api_keystring空API 密钥,AES-256-GCM 加密落库(ModelParameters.Value/Scan),仅通过 PUT /models/:id/credentials 子资源修改
interface_typestring空(VLM:local 默认 ollama,remote 默认 openai)接口协议类型
embedding_parameters.dimensionint0向量维度
embedding_parameters.truncate_prompt_tokensint0输入截断 token 数
embedding_parameters.supports_dimension_overrideboolfalse是否支持请求级维度覆盖(dimensions 参数)
parameter_sizestring空Ollama 模型参数规模(如 "7B"),后端维护、前端不可改
providerstring空(按 BaseURL 自动检测)厂商标识
extra_configmap[string]stringnil厂商专属配置(如 Azure 的 api_version)
custom_headersmap[string]stringnil附加自定义 HTTP 请求头(类似 OpenAI SDK extra_headers;Authorization、api-key 等保留头在运行期被忽略)
supports_visionboolfalseChat 模型是否接受图片多模态输入
max_concurrencyint0(回落到全局 model.max_concurrency)该模型后台任务并发上限(仅 chat/vlm/embedding 生效)
app_id / app_secretstring空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 声明内置模型 ​

  1. 复制 config/builtin_models.yaml.example 为 config/builtin_models.yaml(或用环境变量 BUILTIN_MODELS_CONFIG 指向别的路径);
  2. 在 docker-compose.yml 的 app 服务里取消 - ./config/builtin_models.yaml:/app/config/builtin_models.yaml:ro 这一行挂载的注释;
  3. 把文件里引用的变量写进项目根目录的 .env。app 服务通过 env_file: .env 读取整个文件,不需要在 environment: 里逐个列出(这里用的是数组写法,.env 不存在时 Compose 会报错,scripts/start_all.sh 在缺失时会从 .env.example 复制一份);
  4. 重启 app,看日志确认:
bash
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 一致):

yaml
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: 的条目不应被当成「已从文件删除」而软删掉仍在使用的模型。文件改正后的第一次启动会补做清理;
  • 每个条目按 id UPSERT,写入 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/statusOllama 服务可用性
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 为租约到期时间;acquireScript Lua 脚本原子地清理过期租约、计数、在限额内准入。租约 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 为进程本地)。

模型健康检查 / 连通性测试 ​

两套机制,均在服务端持有凭证、不回传明文密钥:

  1. 测试连接(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 映射。

  2. 模型调试器(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 输出统一的结构化日志行:

    go
    logger.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}/
Ollamainternal/models/utils/ollama/、internal/handler/initialization.go
后台并发闸门internal/models/limiter/、internal/container/container.go(resolveModelMaxConcurrency)
模型服务internal/application/service/model.go
Handlerinternal/handler/model.go、model_credentials.go
路由与守卫internal/router/routes_infra.go、internal/router/rbac.go(PlatformManaged)

基于 Yuheng v0.1.0 源码整理 · MIT License