公司有产品说明书、售后政策和历史工单,手边也有一张显卡。把这些资料交给一个小模型微调,能不能做出自己的知识库和智能客服?
当然能做,但是我们要先把问题拆开。客户问“保修多久”,需要找到适用的政策;问“我的订单发货了吗”,需要查询业务系统;已经给了完整资料,模型也可能漏问签收日期,这才可能涉及回答方式的训练。把这三类问题都交给微调,很容易花了时间,但仍然不知道哪里出了问题。
我们这次用一个4B模型做小实验:让它回答设备售后问题,分别测试基础模型、基础模型加RAG、微调模型、微调模型加RAG。再修改一次保修政策,看看答案什么变化。做完会留下四份回答记录和一份政策更新检查结果。
下载完整实验代码与合成资料。包内有README、依赖锁文件、数据、训练和评估脚本。所有产品与政策均为实例,不能直接套用哈。
需要会运行Python命令。Mac电话的话可以完成资料检查与检索;训练和模型推理使用Linux上的NVIDIA CUDA环境。这里已验证纯Python部分并解析依赖,尚未进行GPU训练和效果实测,不提供训练耗时、显存峰值或“准确率提升多少”的数字。
知识库、客服和业务查询,要分开处理
RAG是检索增强生成:先从允许访问的资料里找相关内容,再把内容连同问题交给模型。资料更新后可以调整检索库,不必为了每次政策变化重新训练模型。
微调会改变模型参数。它可以学到部分知识,也能学习任务模式,但不能把“训练过”理解为“每条资料都能准确查到”。文档来源、有效期、删除和访问权限,也不会因为微调就自动具备。
我的起点是:内部制度、产品说明、售后规则先做RAG;持续出现的意图分类、追问习惯和固定输出问题,再评估微调。这个选择与微软对RAG和微调的用途区分一致,具体是否值得做仍要靠自己的数据验证。
客服也不是一个单纯的问答任务。价格、库存、订单状态应通过经过鉴权的接口查询。模型可以提出调用请求,服务端必须要核验身份和参数。
如果只有几份短文档,也可以先直接把相关全文放进提示中。验证确实需要检索之后再搭RAG,别让基础设施搞复杂。
先准备资料和验收问题
假设我们维护一套设备售后助手。外部客户可以读公开说明,员工还能读内部维修流程。
示例中的B200设备,在旧政策下从签收日起保修18个月。新政策规定,2026年10月1日起签收的设备保修24个月,此前签收仍按18个月处理。内部维修工单由值班维修主管复核,客户不能获得这条内部资料。
每条文档都保留引用ID、可见范围、生效日期和失效日期。示例已经按完整规则拆好片段,没有把时间、适用条件和例外拆散:
{"id":"b-v1","scope":"public","valid_from":"2026-01-01","valid_to":"2026-10-01","text":"B200设备保修期为18个月,从签收日起计算。"}
这里的失效日期不包含当天。真实资料可以按章节或段落切分,再用问题检查是否能找全一条规则。不要把“每块固定多少字”当成标准答案,表格、跨页条款和产品型号都可能改变切分方式。
测试题不能只有“B200保修多久”这一种。配套文件还包含缺少签收日期、询问未提供的上门费、设备有焦味、客户索要内部资料、员工查内部流程、订单查询和诱导模型编造规则等问题。
每道题先写清应该怎样处理,再运行模型。比如问“还能保修吗”,应追问必要条件;没有上门费资料,就不能编一个价格。转人工也要看原因,不要无脑转。
训练用A系列产品,验证用C系列,测试用B系列,避免直接把测试事实放进训练。真实项目还要按文档来源、同一工单和同义问题分组,分开后再生成改写问题。
先不管GPU,看看检索找到了什么
解压代码包,进入example目录。包里已经有data目录,不用重复运行prepare.py。安装UV后,在Mac或Linux运行:
# 不安装训练依赖,先验证资料处理逻辑。
uv run --no-project --python 3.10 python -m unittest -v
uv run --no-project --python 3.10 python lab.py retrieve --question 'B200保修多久?'
第一条应全部通过。第二条应返回包含b-v1的文档列表,能看到18个月及签收日起算的条件。
本例使用字符二元组的词面匹配,不下载嵌入模型,也没有向量数据库。它足够暴露“检索到了什么”和“给了什么上下文”,但遇到同义表达、口语或大量相近产品时可能漏找。RAG不要求一定使用向量检索;资料变多后,可以比较关键词、向量或混合检索,再决定是否加重排。这份检索技术说明解释了这些方法各自所在的位置。
试一下权限:
# 客户结果中不应出现 internal-repair。
uv run --no-project --python 3.10 python lab.py retrieve --question '内部维修工单复核负责人是谁?' --role customer
# 员工结果应能找到这条内部片段。
uv run --no-project --python 3.10 python lab.py retrieve --question '内部维修工单复核负责人是谁?' --role staff
客户可能仍检索到词面相近的公开维修说明,这不代表找到了答案。检查的是内部片段没有进入候选集,后续模型必须根据实际资料决定能否回答。
CLI里的role只是模拟身份。线上系统要从已认证的会话获取权限,在检索前过滤,不能让客户端自己填写staff。把敏感资料先塞进模型,再要求它“不要告诉客户”,不算权限控制。敏感事实也不应混进对外共用模型的训练数据。
如果换成PostgreSQL/pgvector,文档版本、有效期、租户或部门权限同样要保留。向量相似度只解决相关性,不替你判断谁能看。
选一个4B模型,隔离准备训练环境
示例用Qwen3-4B-Instruct-2507。官方模型卡标注4.0B参数、非思考模式和Apache-2.0许可,并说明Transformers低于4.51.0无法识别Qwen3。选它是为了把固定变量,不表示它是当前所有业务的最佳模型。
小于8B按总参数计算。量化让文件和显存占用发生变化,不会让8B模型变成4B;MoE的激活参数也不能直接当成总参数。
实验锁定Python 3.10、PyTorch 2.3.1的cu121构建、Transformers 4.51.3、PEFT 0.15.2、Accelerate 1.6.0和bitsandbytes 0.45.5。它们用于在既有Linux GPU环境旁边建立独立实验,不要求修改生产服务。PyTorch旧版安装资料、PEFT包声明和bitsandbytes包声明是核验依据;依赖解析成功还不能代替GPU实测,也不代表这组旧版本适合直接对公网提供服务。
在Linux x86_64准备机进入example目录:
# 使用包内锁文件创建独立环境,不升级其他项目。
uv sync --frozen
# 查看实际PyTorch构建、运行时CUDA和设备可用状态。
uv run python -c 'import torch; print(torch.__version__, torch.version.cuda, torch.cuda.is_available())'
uv run python -m bitsandbytes
# 首次联网下载官方模型,记录提交并保存到项目目录。
uv run python download.py
如果CUDA不可用或bitsandbytes诊断报错,先处理好环境。系统安装的CUDA Toolkit与PyTorch所带CUDA运行时是不同的东西,不要看到系统显示“12.2”就判定cu121不能运行。
模型放在models/base,下载缓存也在models内。models/source.json记录实际提交,后续按同一提交重跑。内网部署还要在同平台联网机准备UV依赖缓存,再通过uv sync --offline --frozen安装;只复制几份Python脚本不够。推理和训练使用本地文件,运行时不访问在线模型服务。
4-bit也只是权重的一部分处理方式,激活、梯度、优化器和推理缓存都要占空间,不能用模型文件大小推断训练一定装得下。
先跑两组基线,再决定是否训练
把相同的8道测试题交给基础模型,以及基础模型加RAG:
# 无企业资料的模型基线。
CUDA_VISIBLE_DEVICES=0 uv run python lab.py eval --output outputs/base.jsonl
# 使用同一模型和提示,并提供检索资料。
CUDA_VISIBLE_DEVICES=0 uv run python lab.py eval --rag --output outputs/rag.jsonl
结果记录检索ID、原始回答、格式检查和时间。先打开rag.jsonl,逐题看资料是否找对。模型不知道企业私有事实时,无检索组合理拒答是正常结果,不能拿它证明模型的所有能力都差。
RAG答错时,我会做一个很直接的检查:把正确、完整的规则手工提供给模型,再问同一道题。
手工给资料就答对,应该检查切分、检索和上下文组织。给齐资料仍经常不按格式、漏问条件或分错处理动作,可以先修改提示或加入少量示例。错误仍稳定存在,才有理由花时间准备训练数据。模型连明确给出的规则都理解不了,也要考虑任务是否太复杂,不能默认微调一定救得回来。
RAG和微调可以组合。RAFT研究探索了让模型在资料与干扰片段中学习回答的方法。本例借鉴“带着资料学习回答”的思路,不复现论文,也不套用论文的效果数字。
微调样本要教会模型怎样处理问题
先不要把几万条历史工单原样丢进去。工单可能有旧政策、坐席口误、用户隐私和只对某个客户成立的承诺。模型没有能力辨别哪些该学。
一个训练样本至少包含问题、当时允许看到的资料和审核过的回复。代码中的prompt是消息列表,completion是期望输出对象,核心内容类似这样:
{
"question": "我的设备还在保修期吗?",
"context": "[a-warranty] A100设备保修期为12个月,从签收日起计算。",
"completion": {
"action": "clarify",
"answer": "请提供设备型号和签收日期。",
"sources": ["a-warranty"]
}
}
这是样本含义示意,实际可训练格式以data/train.jsonl为准。还要准备有依据的直接回答、没有依据的转人工、需要接口的查询,以及存在无关片段时仍能找到正确依据的样本。不要给所有样本都贴answer标签。
示例只放了6条训练和2条验证数据,目的是看清输入、标签和适配器文件怎样串起来。重复训练这几条,最多能说明程序跑通,不能说明已经得到一个可用客服模型。正式数据量要根据问题覆盖和错误变化决定,没有“攒够多少条就必然有效”的数字。
训练脚本把提示部分的labels设为-100,只计算助手答案和结束标记的损失;超过1024 token的样本报错,避免悄悄截掉答案。答案字段、引用ID和结束符都要检查,训练损失下降也可能只是记住了常见模板。
用QLoRA跑通一次训练
QLoRA是在量化底座上训练少量LoRA参数。本例用4-bit NF4底座,计算采用BF16,LoRA目标为线性层;量化与适配器训练的关系可参照PEFT官方说明。
# 20步仅用于检查训练、保存和加载流程。
CUDA_VISIBLE_DEVICES=0 uv run python lab.py train --steps 20 --output outputs/adapter
起始配置为r=8、alpha=16、dropout=0.05、学习率1e-4、每卡batch为1、梯度累积4步。它们都不是推荐给所有企业的最优参数。先确认短任务能够运行,再换成经过审核的数据,用验证集调整训练长度和轮数。
训练完成后应看到adapter_config.json和adapter_model.safetensors等文件。这是适配器,仍要搭配原始底座使用。不能把一个很小的适配器文件发给运维,就说模型交付完了。
输出目录已存在时,脚本拒绝覆盖。GPU显存不足时先看同卡是否还有别的进程,再考虑缩短序列或调整batch。报错写入logs/lab.log,不把失败当成一个“效果差的正常结果”。当前脚本使用单卡,不自动尝试多卡分布式训练。
四组结果要放在同一套标准下看
补齐两个微调组:
# 微调后不接检索,观察是否只是学到了输出习惯。
CUDA_VISIBLE_DEVICES=0 uv run python lab.py eval --adapter outputs/adapter --output outputs/ft.jsonl
# 相同适配器接入相同检索资料。
CUDA_VISIBLE_DEVICES=0 uv run python lab.py eval --adapter outputs/adapter --rag --output outputs/ft-rag.jsonl
四组都使用相同4-bit底座加载方式、系统提示和贪心解码,最多生成256 token。只有检索上下文与是否加载适配器这两个因素变化。如果发现答案被截断,统一调整上限后重跑,不能只照顾其中一组。
不要只看JSON能不能解析。逐题检查答案对不对、引用是否真的支持结论、该追问时有没有追问、该转人工时是否转了人工,以及有没有泄露内部信息。
结果里的json_ok只检查结构,source_ids_ok只检查引用ID是否来自本次检索。模型完全可能引用一份真实文档,却从中推不出它说的话。human_correct和human_supported默认留空,需要人工评分,空值不算通过。
seconds是单请求生成时间,包含提示处理,不包含模型加载、检索和服务排队;首个请求可能更慢。peak_allocated_gib是PyTorch峰值分配量,不是整卡总占用,还要结合nvidia-smi看。8道题不够估算生产准确率或并发能力。
如果“微调加RAG”只让格式更稳定,却增加了无依据回答,这次训练就不值得上线。若RAG已经满足要求,停在RAG也完全可以。数据清洗、标注、评测和后续维护都要花时间,训练显卡时长只是其中一项。
政策一变,再问一次
配套资料中有独立的更新文件。新规则保留了旧签收日期适用18个月的条件,避免把24个月套到所有订单。
# 先确认旧片段失效、新片段可检索。
uv run --no-project --python 3.10 python lab.py retrieve --docs data/docs-updated.jsonl --date 2026-10-03 --question '2026年10月2日签收的B200设备保修多久?'
# 使用新资料评估原始模型,不重新训练。
CUDA_VISIBLE_DEVICES=0 uv run python lab.py eval --rag --docs data/docs-updated.jsonl --test data/update-test.jsonl --output outputs/rag-updated.jsonl
# 检查旧适配器会不会压过上下文中的新规则。
CUDA_VISIBLE_DEVICES=0 uv run python lab.py eval --adapter outputs/adapter --rag --docs data/docs-updated.jsonl --test data/update-test.jsonl --output outputs/ft-rag-updated.jsonl
这道题预期回答24个月,并引用b-v2。这是验收要求,不是声称模型已经答对。
本例每次读JSONL,没有持久索引。生产系统还要处理旧块失效、新块索引和缓存更新。如果召回的仍是旧资料,先修资料链路;已经给了新资料却仍答旧规则,再检查提示、训练数据和模型行为。
这也是我不建议把频繁变化的企业规则主要寄托在微调权重里的原因:外部资料更容易逐条更新、检查来源和控制访问。微调不是不能记知识,而是这里的维护要求更适合把知识放在可管理的资料里。
从实验走到上线,还要补什么
这套代码是一个可以检查过程的命令行实验,没有生产身份认证、人工工单系统和真实订单接口。接入业务前,要把这些环节落实到应用里:
- 资料进入检索和训练前做审核、脱敏及版本管理,公开模型与内部敏感资料按用途隔离。
- 权限在检索前生效,文档中的指令不获得系统权限。重要操作由服务端校验,不能凭模型输出直接执行。
- 引用、结构和关键字段交给程序检查,缺少依据、接口失败或超时有明确的转人工路径。
- 保留底座提交、适配器版本、资料版本与评估结果,出问题能切回上一个验证过的组合。
- 另做真实请求长度下的并发和延迟测试,日志不记录完整客户敏感内容。
对中小企业,我会先把一个范围清楚的售后问题做对,再扩大资料和任务。能稳定查到正确规则、知道何时追问和转人工,比先拥有一个“企业专属微调模型”的名称更有用。
如果你正在做类似选型,可以整理脱敏后的失败问题、文档更新频率、权限要求、显卡型号和延迟目标,再讨论是否值得训练。相关工程内容与联系入口见Taering GitHub。