企业私有化大模型后训练:统一基座+LoRA覆盖多场景

发布时间:2026/10/28 10:39:04
企业私有化大模型后训练:统一基座+LoRA覆盖多场景

企业私有化大模型落地最容易遇到的困难,通常不是买不到显卡,而是模型不懂业务语言。通用开源模型在内网一跑,写周报还行,一旦接入合同审查、工单分类、代码补全、客服话术这些具体场景,回答就开始答非所问。想给每个场景单独微调一个模型,200 多个应用就是 200 多份训练数据、200 多次评测、200 多套部署,运维成本立刻失控。这次要拆解的思路是反过来的:不按应用做模型,而是把生产流量收上来,做一次通用的后训练,用一个自托管基座 + 少量适配层去覆盖长尾场景。

这套方案的核心卖点很明确。第一,训练数据来自真实生产流量,不是人工攒的题库,分布天然贴近业务;第二,一次后训练产出通用能力底座,应用侧通过提示词、RAG、工具调用、LoRA 适配层解决差异,避免一场景一模型;第三,全链路离线自托管,模型权重、日志、训练数据都不出内网;第四,对外统一暴露 OpenAI 兼容接口,200+ 应用改个 base_url 就能接;第五,训练、评测、灰度、回流形成闭环,模型能力可以按周迭代而不是按项目迭代。

门槛也要说清楚。这不是“下载一个整合包双击启动”的事情,它是一条工程链路:接入网关要能埋点、日志要能落盘且脱敏、数据要能清洗去重、训练框架要能跑得动、推理服务要能扛住并发、评测集要能反映真实分布。缺任何一环,后训练都会变成“训完不知道好没好”的黑箱。本文会按“架构设计 → 数据采集治理 → 样本构建 → 后训练实操 → 服务化与 API → 评测 → 性能观察 → 排错”的顺序展开,给出可以直接套用的配置模板和验证步骤。

适合谁看:正在做企业级 LLM 平台的技术负责人、算法工程师、后端架构师,以及需要把大模型从 Demo 推到生产环境的团队。如果你只是想本地跑个模型聊天,这篇的工程密度会偏高;如果你的痛点是“几十个业务方都在提需求,但只有一个小团队在支撑”,那正好对得上。

1. 核心能力速览

先给规格。注意下表描述的是这类“生产流量驱动的企业自托管后训练方案”的通用能力画像,具体参数量、显存占用、吞吐指标必须按你实际选的基座模型、精度和推理框架实测,本文不编造任何未经验证的数字。

能力项说明
方案类型企业级 LLM 后训练 + 自托管推理的一体化工程方案
数据来源生产流量(网关日志、应用侧反馈、人工修正记录)
主要功能SFT 监督微调、偏好优化(DPO 类)、大模型蒸馏、领域适配
覆盖方式统一基座 + 提示词模板 + RAG + 工具调用 + LoRA 适配层
推荐硬件训练与推理建议分离;训练侧按基座规模和数据量评估,推理侧按并发与上下文长度评估,需实测
显存占用不确定,需按实际模型版本、精度(FP16/BF16/INT8/INT4)、上下文长度和批大小测试
支持平台Linux 为主,容器化部署;训练框架与推理框架均需 GPU 环境
启动方式命令行启动推理服务(如 vLLM 类框架)+ 容器化编排,非一键包形态
是否支持 API是,建议统一为 OpenAI 兼容协议,便于 200+ 应用零改造接入
是否支持批量任务是,离线批推理、批量评测、批量数据清洗都属于必备能力
适合场景内网知识问答、工单与客服、文档处理、代码辅助、结构化抽取、内容审核辅助
不适合场景强实时性毫秒级响应、超高并发公网 C 端、对事实准确性零容错的场景

这张表里最容易被忽略的是“覆盖方式”这一行。很多团队失败的根源,是把它当成“训一个万能模型”,指望一个权重解决所有问题。更稳妥的判断是:把后训练定位成“提升基座在业务语料上的表达习惯、格式遵循和领域常识”,而把事实性知识交给 RAG,把确定性逻辑交给工具调用,把风格差异交给 LoRA。职责分清,200+ 应用才可能收敛到一套体系里。

2. 适用场景与不适用场景

2.1 什么情况下值得做

判断标准其实很朴素:当你的业务方开始批量提“能不能让模型按我们的格式输出”“能不能听懂我们的行业黑话”“能不能直接调我们的内部接口”这类需求时,通用模型的天花板就到了。这时候有三条路:继续堆提示词、给每个场景上 RAG、做后训练。

堆提示词的成本最低,但边际收益衰减很快,尤其是输出格式复杂、需要多步推理、需要遵循内部规范的场景,提示词会膨胀到几百行,维护难度反而超过训一个模型。RAG 解决的是“知识不知道”,解决不了“说话方式不对”和“流程不会走”。后训练解决的正是后两者:格式遵循、语气风格、领域表达、工具调用习惯、拒答边界。

所以最值得做的场景通常长这样:调用量大(有足够流量可采)、任务相对收敛(输入输出形态稳定)、错误代价可控(有兜底和人工复核)、有明确评测口径(能说清什么叫好)。反过来说,一天几十次调用、需求还在频繁变的场景,先别训,先做提示词工程和 RAG。

2.2 哪些场景不要硬上

第一类是强事实性场景,比如报价、法条原文、医疗剂量。后训练会提升流畅度,但也会放大幻觉,这类必须靠检索和规则兜底。第二类是超低延迟场景,自托管推理有网络和调度开销,需要按实际压测结论决定。第三类是高并发公网流量,成本和容量规划要单独算。第四类是数据拿不到授权的场景,这一条最致命。

2.3 合规与边界

生产流量里天然包含客户信息、员工对话、合同条款、内部工单。用于训练前必须完成三件事:一是脱敏,手机号、身份证、银行卡、地址、邮箱等结构化 PII 要在落盘前或入库前替换;二是授权确认,客户合同、员工告知、隐私政策里是否覆盖“用于模型改进”,需要法务确认,未覆盖的流量要么剔除要么只做统计不做训练;三是可删除,训练样本要能按租户、按用户、按时间回溯删除,模型重训要能剔除已撤回的数据。

另外,涉及人脸、声音、数字人、内容生成的能力,必须强调合法授权和肖像权、声音权确认,不做未授权的克隆和仿冒。模型输出侧建议保留审核链路,尤其是对外发布的场景。这不是保守,是生产环境的底线。

3. 整体架构:从生产流量到自托管模型的闭环

把链路拆开看,一共七段,每段都有明确的输入输出,这样才可运维。

第一段是接入层。所有应用不直连模型,统一走一个 LLM Gateway。网关负责鉴权、限流、路由、日志埋点、成本归集。这一步是整个方案的地基,没有统一网关,后面的流量采集根本无从谈起。

第二段是日志层。请求和响应落盘到对象存储或数据湖,字段要够用:trace_id、app_id、租户、模型版本、完整 messages、输出内容、token 用量、时延、用户反馈、时间戳。

第三段是治理层。脱敏、去重、质量过滤、语言识别、长度截断、敏感内容剔除,输出干净的候选样本池。

第四段是构建层。按任务类型聚类分桶,把原始对话转成 SFT 样本、偏好对样本、工具调用样本,做配比和采样。

第五段是训练层。SFT 打底,偏好优化调风格与安全性,蒸馏补长尾能力。

第六段是评测层。离线评测集 + 回归集 + 线上灰度指标,三层把关。

第七段是发布层。模型权重发布到推理集群,网关按 app_id 灰度切流,指标异常自动回退。

这七段串起来就是一个数据飞轮:线上流量 → 训练数据 → 新模型 → 灰度上线 → 新流量。飞轮转起来之后,模型迭代周期可以从“一个项目三个月”压缩到“两周一个版本”。但要提醒一句,飞轮不是自动转的,每一段都需要有人维护指标,否则数据池会迅速腐化。

4. 环境准备与前置条件

4.1 基础检查清单

下面这套清单不绑定具体版本号,因为不同基座模型对 CUDA、PyTorch、推理框架的版本要求差异很大,照抄别人的版本号是最容易踩的坑。正确做法是先确定基座模型,再去查该模型官方仓库的 requirements。

检查项要求验证方式
操作系统Linux(Ubuntu 系居多),内核与驱动匹配uname -a、nvidia-smi
GPU 驱动与 CUDA 版本匹配,容器内可见 GPUnvidia-smi能列出设备
Python建议独立虚拟环境或 conda 环境隔离python -V、pip list
训练框架支持 LoRA/QLoRA 的微调框架(如 LLaMA-Factory 类)跑通官方最小示例
推理框架支持连续批处理与 OpenAI 兼容协议的框架(如 vLLM 类)curl /v1/models返回列表
存储模型权重、训练数据、日志分目录,容量按实际规模预留df -h
网络内网可达,模型下载走内网镜像源离线环境提前准备权重
端口推理服务、网关、评测服务端口不冲突ss -lntp检查

4.2 目录规划

工程上最容易被低估的是目录规范。建议一开始就定死结构,后面所有脚本都按这个路径走,避免“数据在 A 机器、权重在 B 机器、配置在某人本地”的混乱。

# 目录结构示例,实际路径按团队规范替换 /data/llm/ ├── models/ # 基座权重与训练产出的 checkpoint │ ├── base/ │ └── sft-v1/ ├── datasets/ # 训练数据 │ ├── raw/ # 原始导出(含敏感信息,权限收紧) │ ├── clean/ # 脱敏去重后 │ └── sft/ # 最终训练格式 ├── configs/ # 训练与推理配置 ├── eval/ # 评测集与评测脚本 ├── logs/ # 训练日志、推理日志 └── outputs/ # 推理产出、批量任务结果

raw目录要单独做权限控制,最好只允许数据治理任务的服务账号读取,其他角色一律不可见。这条在很多公司是靠自觉,出问题时才发现原始日志被同步到了测试机。

5. 生产流量采集与数据治理

5.1 网关埋点:字段设计决定后续一切

日志 schema 是整个方案里最不该省事的地方。字段少了,后面想补就得等下一个版本周期。建议最小集合如下:

{ "trace_id": "req-20250101-000001", "app_id": "contract-review", "tenant_id": "t-001", "model_version": "base-7b-instruct-v3", "messages": [ {"role": "system", "content": "你是合同审查助手……"}, {"role": "user", "content": "请检查这份条款的风险点……"} ], "output": "1. 付款周期过长……", "usage": {"prompt_tokens": 812, "completion_tokens": 233}, "latency_ms": 1840, "feedback": {"thumb": "up", "copied": true, "edited": false}, "created_at": "2025-01-01T10:00:00+08:00" }

这里有两个细节值得强调。app_id必须有,否则你无法做分应用的效果归因;feedback里的edited字段价值最高——用户把模型输出改了,说明原始输出不合格,这就是天然的偏好对数据来源。很多团队只记了点赞点踩,其实“用户改写”比点踩信号更密集、更可靠。

5.2 脱敏与清洗

脱敏要放在入库前,不能等训练前才做。下面是一段示意脚本,正则规则需要按自己业务补充,不要照抄就当生产可用。

import re import hashlib PII_PATTERNS = [ (re.compile(r"1[3-9]\d{9}"), "[PHONE]"), (re.compile(r"\d{17}[\dXx]"), "[ID_CARD]"), (re.compile(r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}"), "[EMAIL]"), (re.compile(r"\d{16,19}"), "[BANK_CARD]"), ] def mask_pii(text: str) -> str: if not text: return "" for pattern, placeholder in PII_PATTERNS: text = pattern.sub(placeholder, text) return text def sample_key(messages) -> str: # 用内容哈希做精确去重,实际可叠加 SimHash / MinHash 做近似去重 content = "".join(m.get("content", "") for m in messages) return hashlib.md5(content.encode("utf-8")).hexdigest() def is_valid(sample, min_chars=20, max_chars=8000) -> bool: out = sample.get("output", "") if not (min_chars <= len(out) <= max_chars): return False # 过滤明显截断、纯报错、空回复 bad_markers = ["系统繁忙", "请求超时", "Internal Server Error"] return not any(m in out for m in bad_markers)

清洗的重点是过滤掉“坏样本”:超时截断的、报错的、空回复的、重复刷屏的、纯寒暄的。这些样本如果混进训练集,模型会学到一堆没用的模式。去重则建议两级:精确哈希去重去掉完全重复,近似去重(SimHash、MinHash、n-gram Jaccard)去掉模板化改写的重复。企业场景里模板化重复极为常见,同一份表单被不同用户提交上千次,不去重会严重拉偏数据分布。

5.3 采样策略

不是所有流量都值得拿来训练。建议按三个维度做分层采样:按 app_id 保证长尾应用有最低配额,避免大应用吃掉全部预算;按时间窗口保证新鲜度,比如最近 90 天权重更高;按反馈信号加权,有正向反馈和被采纳的输出权重更高,有负向反馈的进入偏好对池而非 SFT 池。

采样比例没有万能公式,稳妥做法是先算总量,再定“每个应用最低样本数”和“单应用最大占比上限”,用配额约束代替拍脑袋。经验上单应用占比过高会让模型过度偏向某个业务的语言风格,表现是别的应用接入后回答带着那股味。

6. 训练样本构建:把日志变成 SFT / DPO 数据

6.1 三种样本形态

SFT 样本是标准的 messages 格式,直接由高质量日志转换而来,要求输出准确、格式规范、无敏感信息。偏好对样本由“用户改写前后”或“点赞与点踩”构造,用于 DPO 类算法,专门调语气、格式遵循和安全性。工具调用样本需要额外的函数签名和调用结果,用于让模型学会走内部流程。

{"messages": [{"role": "system", "content": "你是工单分类助手"}, {"role": "user", "content": "客户反馈:登录后页面一直转圈"}, {"role": "assistant", "content": "{\"category\": \"登录异常\", \"priority\": \"P1\", \"need_human\": true}"}]} {"messages": [{"role": "system", "content": "你是合同条款抽取助手"}, {"role": "user", "content": "……"}, {"role": "assistant", "content": "{\"payment_days\": 90, \"penalty_rate\": \"0.05%/日\"}"}]}

注意第二个例子:结构化抽取类任务,输出必须是合法 JSON,这类样本的训练价值很高,因为通用模型经常在 JSON 里加注释、加中文引号。一个应用把这个能力练稳了,同类应用的 JSON 输出都会跟着稳。

6.2 数据配比

配比是后训练里最像“手艺”的部分。下面是建议的起始比例,实际要按评测结果调整,不要当成固定公式。

数据类型建议占比作用备注
通用能力保持数据20%–30%防止灾难性遗忘可用公开指令数据集或历史通用样本
业务 SFT 样本40%–50%学格式、学风格、学领域表达按 app_id 分桶配额
结构化输出样本10%–15%JSON、表格、字段抽取强约束场景优先
工具调用样本5%–10%学会调内部接口需要真实函数签名
安全与拒答样本3%–5%边界控制覆盖越权、敏感、超出范围
偏好对样本按需偏好优化阶段使用与 SFT 分开训练

通用数据绝不能省。很多团队一上来全用业务数据,训完发现模型连基本常识问答都退化了,这叫灾难性遗忘。保留一定比例的通用数据,是最便宜的保险。

6.3 数据质检

样本进训练前建议做三件事:一是抽检,每个 app_id 至少人工看若干条,确认格式和内容没问题;二是统计,看长度分布、token 分布、重复率、语言占比,异常分布往往意味着采集逻辑有 bug;三是泄漏检查,评测集里的样本绝对不能出现在训练集里,用哈希比对即可,这一步不做,评测分数会虚高得离谱。

7. 后训练实操:SFT、偏好优化与蒸馏

7.1 SFT 阶段

以常见的 LoRA 微调框架为例,配置文件大致长这样。所有路径、模型名、参数都需要按你的实际环境替换,下面只是结构示例。

# configs/sft_v1.yaml —— 结构示例,参数需按实际环境调整 model_name_or_path: /data/llm/models/base stage: sft do_train: true finetuning_type: lora lora_target: all lora_rank: 16 lora_alpha: 32 dataset_dir: /data/llm/datasets/sft dataset: business_mix_v1 template: default cutoff_len: 4096 max_samples: 100000 overwrite_cache: true preprocessing_num_workers: 8 per_device_train_batch_size: 1 gradient_accumulation_steps: 8 learning_rate: 1.0e-4 num_train_epochs: 3 lr_scheduler_type: cosine warmup_ratio: 0.1 bf16: true gradient_checkpointing: true logging_steps: 10 save_steps: 500 output_dir: /data/llm/models/sft-v1

几个实操要点。第一,先跑max_samples很小的冒烟测试,确认数据格式、模板、tokenizer 都对得上,再上全量,否则可能浪费一整晚。第二,cutoff_len要按业务实际上下文长度定,定太大显存吃紧且大部分样本被 padding 浪费,定太小会把长文档样本截断成废样本。第三,学习率、epoch 数不要一上来就调大,业务数据量不大时很容易过拟合,表现是训练 loss 一路降但评测分数不动甚至下降。

7.2 偏好优化阶段

偏好优化在 SFT 之后做,目的是让模型更倾向于“被采纳的回答”。数据形态是 chosen/rejected 对,chosen 来自用户改写后的版本或被采纳的版本,rejected 来自原始输出或被点踩的版本。

# configs/dpo_v1.yaml —— 结构示例 model_name_or_path: /data/llm/models/sft-v1 stage: dpo do_train: true finetuning_type: lora pref_beta: 0.1 pref_loss: sigmoid dataset_dir: /data/llm/datasets/dpo dataset: pref_pairs_v1 cutoff_len: 4096 learning_rate: 5.0e-5 num_train_epochs: 1 bf16: true output_dir: /data/llm/models/dpo-v1

偏好优化的坑在于“偏好对质量”。如果 chosen 和 rejected 差别极小(只是措辞不同),模型学不到有效信号;如果差别极大(一个是完整回答一个是报错),模型学到的是“别报错”这种低级特征。构造偏好对时要控制差异维度:格式差异、语气差异、是否遵循指令,一次只强化一个维度,效果更可控。

7.3 蒸馏补长尾

200+ 应用里总有一些样本量极少的场景,几十条、上百条,微调不动。这时候可以用能力更强的教师模型对这些场景做批量生成,人工筛一遍后混入训练集。蒸馏的价值在于把大模型的“解题过程”迁移到小模型,成本比人工标注低得多。但要注意两点:教师模型的输出必须经过人工抽检,错误会被学生模型完整继承;蒸馏数据占比不宜过高,否则学生模型会过度模仿教师的表达风格,丢掉自己的稳定性。

8. 自托管推理服务与接口 API

8.1 启动推理服务

训练产出的是 LoRA 权重或合并后的全量权重,推理侧建议用支持连续批处理和 OpenAI 兼容协议的框架。下面是启动命令的结构示例,模型路径、端口、并行度都要按实际环境改。

# 启动 OpenAI 兼容推理服务,示例参数需按实际环境调整 python -m vllm.entrypoints.openai.api_server \ --model /data/llm/models/sft-v1 \ --served-model-name enterprise-llm-v1 \ --host 0.0.0.0 \ --port 8000 \ --max-model-len 8192 \ --gpu-memory-utilization 0.85 \ --tensor-parallel-size 1 \ --enable-lora \ --lora-modules contract=/data/llm/models/lora-contract \ --api-key ${INTERNAL_API_KEY}

--gpu-memory-utilization是最需要现场调参的一项,设太高容易 OOM,设太低浪费显存。建议从 0.85 起步,压测后再调。--enable-lora是多应用覆盖的关键:一个基座常驻显存,多个 LoRA 按需挂载,切换成本远低于起多个独立模型实例。

8.2 接口调用示例

服务起来之后,先验证/v1/models,确认模型列表里有你启动时指定的名字,再发起一次真实请求。

import os from openai import OpenAI client = OpenAI( base_url="http://127.0.0.1:8000/v1", api_key=os.environ.get("INTERNAL_API_KEY", "EMPTY"), ) resp = client.chat.completions.create( model="contract", # 指定挂载的 LoRA 名称,或使用基座名 messages=[ {"role": "system", "content": "你是合同条款抽取助手,只输出 JSON。"}, {"role": "user", "content": "请抽取付款周期与违约金比例。"}, ], temperature=0.1, max_tokens=512, extra_body={"top_k": 20}, ) print(resp.choices[0].message.content) print(resp.usage)

这段代码的价值在于,200+ 应用接入时只需要改base_url和model两个字段,业务代码几乎零改造。这也是为什么强烈建议统一 OpenAI 兼容协议,而不是每个团队自己定一套 HTTP 接口——后者会让网关的复杂度指数级上升。

8.3 网关侧的路由与灰度

网关要承担四件事。路由:按 app_id 把请求打到对应模型版本或 LoRA。灰度:新模型先接 5% 流量,观察指标再逐步放量。兜底:模型服务不可用时降级到旧版本或规则回复。计量:按 app_id 统计 token 消耗,用于成本归集和预算控制。

批量任务则单独走离线队列,不要和在线请求抢资源。典型场景是历史工单批量重分类、合同批量预审、知识库批量清洗。做法是目录进目录出,一条样本一个任务,失败重试有上限,结果落盘保留 trace_id 便于回溯。

{ "input_dir": "/data/llm/outputs/batch/input", "output_dir": "/data/llm/outputs/batch/output", "model": "enterprise-llm-v1", "batch_size": 1, "max_retry": 2, "concurrency": 4, "timeout_seconds": 120, "log_file": "/data/llm/logs/batch.log" }

批量任务一定要有幂等设计:同一份输入重跑不应产生重复输出,输出文件名带 trace_id 或内容哈希。并发数不要设太大,在线服务优先,离线任务让路。

9. 评测体系:怎么证明能覆盖 200+ 应用

9.1 三层评测

第一层是通用能力回归集,检测有没有灾难性遗忘,覆盖常识问答、指令遵循、长文本理解、代码基础题。第二层是业务分域评测集,按 app_id 或任务类型分桶,每桶至少几十条,用规则加人工双评。第三层是线上灰度指标,包括采纳率、改写率、点踩率、平均时延、超时率。

评测层数据来源判定方式上线门槛(示例,需按业务定)
通用回归公开集 + 历史通用样本自动打分不低于基线模型的既定阈值
业务分域人工标注 + 高质量日志规则校验 + 人工抽检各桶均不低于基线,无显著回退
结构化输出JSON 抽取样本解析成功率解析成功率需达到业务要求
安全边界越权、敏感、超范围样本人工判定不得出现越权或违规输出
线上灰度真实流量指标对比关键指标不劣于旧版本

这里说明一点:表中没有写具体百分比,因为不同业务对“不低于”的定义差异太大,硬写一个数字反而是误导。稳妥做法是先测出基线模型在各桶上的分数,把“不低于基线”作为硬门槛,再对重点应用设更高的期望值。

9.2 自动评测脚本骨架

import json from collections import defaultdict def load_jsonl(path): with open(path, "r", encoding="utf-8") as f: return [json.loads(line) for line in f if line.strip()] def check_json_output(text: str) -> bool: try: json.loads(text) return True except Exception: return False def run_eval(pred_path: str, ref_path: str): preds = {s["id"]: s for s in load_jsonl(pred_path)} refs = load_jsonl(ref_path) stats = defaultdict(lambda: {"total": 0, "ok": 0}) for ref in refs: bucket = ref.get("app_id", "unknown") pred = preds.get(ref["id"]) stats[bucket]["total"] += 1 if pred is None: continue # 示例判据:结构化任务看能否解析,开放任务看关键词覆盖 if ref.get("expect_json"): if check_json_output(pred["output"]): stats[bucket]["ok"] += 1 else: hit = all(k in pred["output"] for k in ref.get("must_contain", [])) if hit: stats[bucket]["ok"] += 1 for bucket, s in stats.items(): rate = s["ok"] / s["total"] if s["total"] else 0 print(f"{bucket}: {s['ok']}/{s['total']} = {rate:.2%}") if __name__ == "__main__": run_eval("/data/llm/outputs/eval/pred.jsonl", "/data/llm/eval/ref.jsonl")

这个骨架故意写得很朴素,因为评测脚本最重要的是“分桶统计”和“判据可解释”。真正的生产评测会复杂得多,但结构一致:按桶算通过率,和基线对比,输出可读报告。最忌讳的是只报一个总分,总分涨了但某个关键业务桶掉了,上线就是事故。

9.3 评测集维护

评测集要版本化管理,和训练数据物理隔离。建议每个季度补一次新样本,尤其是线上出现的新任务类型。还有个小技巧:保留一批“历史难点样本”,就是过去模型一直答不好的那些,专门用来看迭代是否真的在进步。这类样本往往比随机抽的样本更能反映能力变化。

10. 资源占用与性能观察

10.1 训练侧看什么

训练时重点盯三个量:显存占用、每秒处理的 token 数、梯度范数。显存占用决定你能开多大 batch,token 吞吐决定训练时长,梯度范数异常波动通常意味着学习率过大或数据里有异常样本。观察方式很直接,nvidia-smi定时采样或者用框架自带日志都行。

降显存的手段按性价比排序:开梯度检查点、降 cutoff_len、用 QLoRA 量化训练、减小 per_device_batch 并增大梯度累积、换更小的基座。注意梯度累积能缓解显存但不减少单步时间,它换的是空间不是时间。

10.2 推理侧看什么

推理侧的核心指标是首 token 时延(TTFT)、每 token 输出时延(TPOT)、吞吐(tokens/s)、并发承载数、显存水位。TTFT 主要受输入长度和排队影响,TPOT 主要受模型规模和批处理影响。上下文从 4K 涨到 16K,KV cache 占用会显著上升,并发能力会明显下降,这个非线性关系必须压测出来,不能拍脑袋。

降低推理成本的手段:限制 max_tokens、做提示词精简(system prompt 太长是隐形消耗)、开前缀缓存复用相同 system prompt、按业务分级(简单任务走小模型或 LoRA)、离线任务攒批处理。

10.3 进程与端口

自托管最容易出的运维问题是进程残留和端口占用。服务重启前先确认旧进程真的退出了,否则会出现“新进程起不来但日志没报错”的诡异现象,实际是端口被占。建议在启动脚本里加端口检查,容器化部署则用健康检查探针,连续失败自动重启。

11. 常见问题与排查方法

问题现象可能原因排查方式解决方案
训练 loss 不下降数据格式或对话模板不匹配、学习率过小打印 tokenizer 编码后的样本,人工核对模板改用与基座匹配的 template,调大学习率
loss 下降但评测不涨过拟合、训练集与评测集分布不一致对比训练集与评测集的 app_id 分布增加通用数据、减少 epoch、补评测桶样本
模型输出格式退化结构化样本占比过低统计训练集中 JSON 样本比例提高结构化样本占比,加入解析失败惩罚样本
通用能力明显下降灾难性遗忘跑通用回归集提高通用数据比例,降低训练强度
推理服务起不来端口占用、显存不足、模型路径错误看启动日志、nvidia-smi、ss -lntp换端口、降低显存水位、核对路径
请求超时率高上下文过长、并发超限、批处理排队看 TTFT 与队列长度限制 max_tokens,扩容或加限流
LoRA 之间“串味”多 LoRA 共享基座时挂载或路由错误检查请求里的 model 字段与网关路由修正路由表,避免同名覆盖
批量任务卡住单条样本超时未设上限、并发过高看批量日志最后一个成功 trace_id加 timeout 与重试上限,降低并发
评测分数异常高评测集泄漏进训练集哈希比对训练集与评测集剔除泄漏样本,重建评测集
上线后效果回退灰度样本偏差、流量结构变化对比灰度与全量的 app_id 分布扩大灰度范围,按应用分批放量
日志缺字段网关版本不一致、埋点未覆盖抽样检查日志 schema统一网关版本,补埋点并回溯校验

排查的通用心法是“先缩小范围再定位”。先确认是数据问题、训练问题还是服务问题:把同一批输入分别喂给基座模型和新模型,如果基座正常、新模型异常,问题在训练;如果两者都异常,问题在服务或提示词。这一步能省掉大量瞎猜。

另一个高频坑是“以为在训模型,其实在训格式”。很多团队反馈效果不明显,最后发现是样本里 assistant 内容带有大量模板套话(比如每条都以“您好,很高兴为您服务”开头),模型学到的全是这套客套话。清理掉这些模板前缀,效果往往立刻改善。

12. 最佳实践与落地路线

第一阶段,先把网关和日志做扎实。目标不是训模型,而是拿到干净、有结构、可归因的流量。这个阶段建议跑两周到一个月,同时把脱敏链路和权限控制建起来。没有这一步,后面全是空中楼阁。

第二阶段,选一个高价值、任务收敛的应用做试点。不要一上来就想着覆盖 200 个,先做一个能拿出对比数据的。做的时候把评测集一起建好,哪怕只有几十条。试点成功的标志不是“模型能聊”,而是“评测分数超过基线且线上采纳率有提升”。

第三阶段,扩到 5 到 10 个应用,验证“一套基座 + 多 LoRA”的架构能不能撑住差异化的需求。这个阶段最容易暴露路由、配额、数据配比的问题,也最能积累可复用的流程。

第四阶段,形成版本节奏。每两周或每月出一版,固定流程:拉数据、清洗、构建、训练、评测、灰度、回流。流程固定之后,人力投入会显著下降。

几条具体建议。第一,永远保留一套最小可运行配置,包括一份小样本数据、一份能跑通的训练配置、一份能起服务的推理命令,出问题时用它快速复现。第二,模型权重、训练数据、输出结果严格分目录,权限分级。第三,批量任务必须带日志和重试上限,否则半夜挂掉没人知道。第四,接口服务限制访问范围,内网加鉴权,不要图省事裸奔。第五,涉及客户数据、人脸、声音、版权素材的训练和使用,逐一确认授权范围,拿不准的样本宁可不训。第六,发布前做效果复核,尤其是面向外部用户的场景,抽检不能省。

还有一点容易被忽略:给模型留“退路”。任何一次上线都要能在分钟级回退到上一版本,回退开关要放在网关而不是应用侧。这样即使新模型在某个长尾应用上翻车,影响面也可控。

13. 最后:先做哪一步

如果现在就要动手,顺序建议是这样:今天先把网关的日志字段补齐,尤其是 app_id 和用户改写标记;这周把脱敏脚本跑通,确认原始日志不会流出受控环境;下周挑一个应用,人工标几十条评测样本,测出基线分数。这三件事做完,你才具备判断“后训练到底有没有用”的能力。

最值得先验证的功能不是模型多聪明,而是结构化输出稳定性——让模型对同一类输入连续输出上百次合法 JSON,看解析成功率。这个指标最能反映后训练是否真的把格式约束学进去了,也最容易在业务侧换来信任。

最容易踩的坑有三个:一是数据不脱敏就进训练集,后面要么重训要么删模型;二是评测集和训练集交叉污染,分数好看但上线就崩;三是把所有应用都塞进一个模型不管差异,最后谁都不满意。

往后可以扩展的方向不少:把工具调用做成统一 schema,让 200+ 应用共享一套函数注册表;把 RAG 的检索质量和模型回答质量分开评测,避免互相背锅;把 LoRA 的生命周期管理起来,哪个应用用哪个版本、什么时候该合并、什么时候该下线,都需要台账。这些做完,自托管大模型才算从“能跑”走到“能养”。

建议把这篇收藏备用,尤其是第 5 章的数据字段设计和第 11 章的排查表,真正上手时翻出来对照,比临时搜索省时间。