最近不少读者在准备大模型应用落地时,都会遇到同一类问题:模型微调怎么跑通?Prompt 怎么写才稳定?RAG 知识库为什么总答非所问?Agent 一接工具就报错?网上资料很多,但大多是零散片段,串不起一条完整链路。这篇文章把“大模型入门到项目落地”最关心的六块内容——Prompt 工程、ReAct 模式、RAG 检索增强、Agent 开发、QAT 量化、Harness 评估——整合成一份微调实战过程笔记。无论你是刚接触大模型的新手,还是已经在做应用层开发的工程师,都能按章节找到可直接复用的代码和排查思路。
为了便于理解,这里以 Qwen3.5 开源模型为主线,结合 LoRA/QLoRA 微调、Ollama 推理、向量检索、Agent 工具调用几大模块,演示一条从模型微调到应用部署的完整闭环。需要说明的是,Qwen3.5 在不同版本的官方仓库、第三方 GGUF 仓库中可能存在差异,文中示例以常见开源环境为主,具体路径和参数要按你实际拉到的模型版本做调整。
1. 背景与核心概念
1.1 为什么突然都在聊“微调 + RAG + Agent”
过去一年里,大模型的能力边界不断被扩展,但企业项目落地时很快会发现两个问题。
第一个问题是通用模型不够“懂业务”。基座模型虽然在预训练时学习了海量语料,但你问它“我们公司内部审批流程是什么”“某个私有协议怎么解析”,它大概率只能给出泛泛而谈的答案。要让模型贴合特定领域术语、输出格式、交互风格,就需要在开源基座模型上做微调。
第二个问题是模型“记忆”不够新、不够准。基座模型的训练数据有截止时间,面对实时数据、私有文档、最新规范,模型无法给出可靠答案。于是 RAG(检索增强生成)成为主流方案:先从知识库中检索出相关片段,再把这些片段作为上下文交给模型回答。微调负责改变模型的“行为习惯”,RAG 负责补足“最新事实”。
而 Agent 解决的是“大模型不能主动做事”的问题。语言模型本身只能生成文本,但通过 ReAct 模式,模型可以输出“思考过程 + 工具调用”,由代码解析这些输出并真正执行函数调用,再把结果回传给模型继续推理。这样大模型就从“聊天机器人”变成了“能操作的助手”。
1.2 本文涉及的技术栈关系
用一条链路来概括:
原始数据 -> 文本清洗 -> Prompt 构造 -> Qwen3.5 基座模型 -> LoRA/QLoRA 微调 -> 导出/合并权重 -> Ollama 或 vLLM 部署 -> 接入 RAG 知识库(向量检索) -> 构建 Agent(ReAct 模式 + 工具调用) -> Harness 评估 -> QAT 量化压缩 -> 上线每个环节都有独立的知识体系,但生产项目里它们是环环相扣的。所谓“三天速通”,不是三天把所有细节都挖透,而是先建立起这条主线的完整认知,每个环节先跑通最小可运行版本,再逐步深入。
1.3 常见概念区分
- Prompt 工程:通过设计输入文本,引导模型输出符合预期的结果。它不改变模型权重。
- 微调(Fine-tuning):在基座模型基础上,用标注数据继续训练,改变模型权重,让模型学会特定风格或能力。
- RAG:在推理阶段引入外部检索结果,让模型基于检索到的内容回答。不改变模型权重。
- Agent:让模型能够调用外部工具,获取新信息或执行操作,核心是“模型决策 + 工具执行”的循环。
- QAT(量化感知训练):在训练阶段模拟量化误差,让模型权重适应低比特表示,从而减少推理时精度损失。
- Harness:这里指大模型评估框架(如 lm-evaluation-harness),用于在标准数据集上评估模型的准确率、困惑度等指标。
明白这些区别之后,后面每章的实操就不会混淆了。
2. 环境准备与版本说明
2.1 硬件与运行环境
大模型微调和推理对硬件要求较高,尤其显存是关键瓶颈。以 Qwen3.5 系列的 7B~9B 量级模型为例:
- 纯 CPU 推理可以跑,但速度较慢,适合验证流程。
- 4bit 量化推理建议 8GB 以上显存。
- QLoRA 微调 7B~9B 模型,建议 16GB 以上显存;如果使用 LoRA 并且开启梯度检查点,有条件可以压到更低。
- 全参数微调 7B 以上模型,建议多卡 A100 或 H100,普通开发者不推荐轻易尝试。
整体环境版本如下,具体以你的实际项目为准:
| 组件 | 版本建议 | 说明 |
|---|---|---|
| 操作系统 | Ubuntu 20.04 / 22.04,Windows WSL2 也可 | Linux 下 CUDA 生态最顺 |
| Python | 3.10 / 3.11 | 兼容最新 PyTorch 与 Transformers |
| CUDA | 11.8 / 12.1 | 根据显卡驱动选择 |
| PyTorch | 2.1 及以上 | 量化、训练算子更稳 |
| Transformers | 4.40 及以上 | 具体版本看模型官方要求 |
| PEFT | 0.10 及以上 | LoRA/QLoRA 训练 |
| TRL | 0.8 及以上 | SFT 训练器 |
| Datasets | 2.16 及以上 | 数据集加载处理 |
| vLLM / Ollama | 最新稳定版 | 推理服务部署 |
| LanceDB / Chroma / FAISS | 按需选择 | 向量检索 |
这里特别提醒:不要盲目追最新版本。很多报错的根源是 transformers、peft、trl 三者版本不匹配。建议先固定一套组合,跑通后再升级。
2.2 推荐项目结构
为了后期维护方便,建议按下面结构组织项目目录。
qwen3-finetune-project/ ├── data/ # 原始数据与处理脚本 │ ├── raw/ # 原始 JSON/JSONL 数据 │ └── processed/ # 处理后的训练集、验证集 ├── scripts/ │ ├── prepare_data.py # 数据清洗与格式化 │ ├── train_lora.py # LoRA/QLoRA 微调 │ ├── merge_weights.py # 合并 LoRA 权重 │ ├── build_rag.py # 构建 RAG 向量库 │ ├── run_agent.py # Agent 主程序 │ └── evaluate_harness.py # 模型评估 ├── models/ # 模型权重 ├── logs/ # 训练日志与评估结果 └── configs/ # 配置文件2.3 拉取模型与国内访问优化
使用 Ollama 拉取 Qwen3.5 模型时,常见的命令是:
ollama pull qwen3.5:9b如果遇到网络下载慢或超时,可以先检查网络连接,再考虑配置环境变量或切换为国内可访问的镜像源。需要提醒的是,这里只讨论网络源配置,不涉及任何代理工具。部分第三方用户上传的 GGUF 模型(例如社区命名的 uncensored 版本)来源不明,可能存在数据投毒或安全后门,不建议在生产环境使用。请优先从模型官方仓库或可信渠道下载。
下载完成后,通过 Ollama 快速验证模型是否可用:
ollama run qwen3.5:9b "你好,请介绍一下你自己"能看到正常回复,说明模型基础推理没问题。
3. Prompt 工程与 ReAct 模式
3.1 Prompt 的基本结构
Prompt 是与模型交互的输入文本,它的质量直接决定输出质量。一个工程化的 Prompt 通常包含四部分:
- 系统角色:告诉模型你是谁、你的职责、输出风格。
- 任务描述:明确要完成的任务。
- 上下文信息:背景资料、文档片段、历史对话。
- 输出约束:格式要求、长度限制、禁止事项。
下面是一个典型的系统 Prompt 示例:
system_prompt = """你是一名专业的技术文档助手,擅长把复杂技术概念讲清楚。 你的任务要求: 1. 根据用户提供的资料回答问题。 2. 如果资料中没有相关信息,直接回答“资料库中没有找到相关内容”,不要编造。 3. 你的回答要分点列出,每个要点不超过 50 字。 4. 使用中文回答,不要使用英文缩写(除专业名词外)。 用户资料: {context} 用户问题: {question} """这里的关键在于“如果资料中没有相关信息,直接回答没有”——这能显著减少模型的幻觉。
3.2 ReAct:让模型具备推理与行动能力
ReAct 的核心思路是让模型在推理时交替输出 Thought(思考)、Action(动作)、Observation(观察)三个字段。
一个典型的 ReAct Prompt 长这样:
你可以使用以下工具: - search_web(query: str): 搜索互联网信息 - get_weather(city: str): 查询城市天气 请按照以下格式回答: Thought: 分析用户问题,决定下一步动作。 Action: 工具名称(参数) Observation: 工具返回结果 最后根据 Observation 给出最终回答。模型输出如下:
Thought: 用户想知道北京今天的天气,我需要调用天气查询工具。 Action: get_weather("北京") Observation: 晴,25℃,空气质量良。 Thought: 已获取天气信息,可以直接回答。 Answer: 北京今天晴天,气温 25℃,空气质量良。注意:当前很多开源模型在输出 Action 时会出现格式漂移,比如漏掉括号、把工具名写错。解决办法是在指令数据集中加入大量 ReAct 格式的样本并进行微调,或者在后端解析时做格式化修正。
3.3 Prompt 常见报错与处理
使用 Prompt 时会遇到几种典型的报错,其中与内容安全策略相关的报错尤其常见。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
invalid prompt: your prompt was flagged as potentially violating our usage policy | 提示词触发了服务端安全策略 | 检查提示词是否包含暴力、违法、违规内容,尝试改写为更中性的表达 |
prompt is too long ... automatic compaction failed: api error: 400 | 输入 token 数超过了模型上下文窗口,或上下文压缩失败 | 截断历史消息、缩短 Prompt、使用更大上下文窗口模型 |
| Prompt 闪退 | 某些客户端对超长 Prompt 处理崩溃 | 分批输入或限制 Prompt 最大长度 |
| Agent 执行时报错 | 工具参数格式错误、工具返回异常 | 工具注册时做参数校验,异常捕获后返回可读信息 |
尤其是“invalid prompt”这类报错,很多开发者会误以为是网络问题,实际是提示词触发了安全过滤。你需要先自查提示词内容,而不是反复重试。官方建议参考平台文档中关于 prompt 引导的最佳实践。
4. 基于 Qwen3.5 的 LoRA 微调实战
4.1 为什么要用 LoRA / QLoRA
全参数微调对整个模型所有参数做梯度更新,显存开销巨大。LoRA(Low-Rank Adaptation)的思路是冻结原模型参数,只在 attention 层的权重旁边插入低秩矩阵进行训练。训练完成后,把低秩矩阵与原模型合并,或者单独保存 LoRA 权重。
QLoRA 则进一步引入 4bit 量化基座模型,显著降低显存占用,让普通显卡也能微调大模型。我们的实战选用 QLoRA 方案,数据量控制在几千条,单卡 16GB 显存即可运行。
4.2 准备微调数据
微调数据一般使用对话格式。以 JSONL 为例,每行是一个完整的对话样本:
{"messages": [{"role": "user", "content": "什么是 RAG?"}, {"role": "assistant", "content": "RAG 是检索增强生成,先检索知识库再生成答案。"}]}训练前要处理数据:去除重复样本、过滤过短样本、检查特殊字符。这里给出一个简单的数据处理脚本:
# 文件路径:scripts/prepare_data.py import json def load_and_filter(input_path, output_path, min_len=10): samples = [] with open(input_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue obj = json.loads(line) messages = obj.get("messages", []) if len(messages) < 2: continue # 过滤过短样本 content_all = "".join(m["content"] for m in messages) if len(content_all) < min_len: continue samples.append(obj) with open(output_path, "w", encoding="utf-8") as f: for s in samples: f.write(json.dumps(s, ensure_ascii=False) + "\n") print(f"过滤完成, 原始数据 -> 有效数据: {len(samples)} 条")4.3 编写 QLoRA 训练脚本
下面给出一个基于 Transformers、PEFT、TRL 的 QLoRA 训练核心示例。
# 文件路径:scripts/train_lora.py import torch from transformers import ( AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig, TrainingArguments, ) from trl import SFTTrainer from peft import LoraConfig # 模型路径按实际环境修改 model_name = "Qwen/Qwen3.5-9B" # 以官方实际名称/路径为准 # 4bit 量化配置 bnb_config = BitsAndBytesConfig( load_in_4bit=True, bnb_4bit_quant_type="nf4", bnb_4bit_compute_dtype=torch.bfloat16, bnb_4bit_use_double_quant=True, ) model = AutoModelForCausalLM.from_pretrained( model_name, quantization_config=bnb_config, device_map="auto", torch_dtype=torch.bfloat16, ) tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) tokenizer.pad_token = tokenizer.eos_token # LoRA 配置 lora_config = LoraConfig( r=8, lora_alpha=16, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], lora_dropout=0.05, bias="none", task_type="CAUSAL_LM", ) # 训练参数 training_args = TrainingArguments( output_dir="./logs/qwen3_lora", per_device_train_batch_size=1, per_device_eval_batch_size=1, gradient_accumulation_steps=16, learning_rate=2e-4, num_train_epochs=3, logging_steps=50, save_steps=500, eval_strategy="epoch", save_strategy="epoch", bf16=True, gradient_checkpointing=True, ) trainer = SFTTrainer( model=model, args=training_args, train_dataset=train_dataset, eval_dataset=eval_dataset, tokenizer=tokenizer, peft_config=lora_config, dataset_text_field="text", max_seq_length=2048, ) trainer.train() trainer.save_model("./logs/qwen3_lora/final")几个关键参数说明:
gradient_accumulation_steps=16:相当于累积 16 个 batch 后再更新一次参数,可以弥补小 batch 导致的训练不稳定。gradient_checkpointing=True:用计算换显存,训练时能省下不少显存。target_modules需要和模型结构匹配,不同版本的 Qwen 模块名可能不同,如果报错请检查模型 config 中实际存在的层名称。
4.4 合并权重并部署推理
训练完成后,LoRA 权重单独保存在输出目录。为了方便 vLLM、Ollama 这类推理框架直接加载,需要把 LoRA 权重合并回原模型。
# 文件路径:scripts/merge_weights.py from peft import PeftModel from transformers import AutoModelForCausalLM, AutoTokenizer base_model_name = "Qwen/Qwen3.5-9B" # 按实际路径调整 lora_path = "./logs/qwen3_lora/final" output_path = "./models/qwen3.5-9b-lora-merged" model = AutoModelForCausalLM.from_pretrained( base_model_name, torch_dtype="auto", device_map="auto", ) model = PeftModel.from_pretrained(model, lora_path) model = model.merge_and_unload() model.save_pretrained(output_path) tokenizer = AutoTokenizer.from_pretrained(base_model_name) tokenizer.save_pretrained(output_path)合并完成后,可以先用 Transformers 快速验证微调效果:
from transformers import AutoModelForCausalLM, AutoTokenizer model_path = "./models/qwen3.5-9b-lora-merged" tokenizer = AutoTokenizer.from_pretrained(model_path, trust_remote_code=True) model = AutoModelForCausalLM.from_pretrained(model_path, device_map="auto", torch_dtype="auto") prompt = "用户:什么是检索增强生成?\n助手:" inputs = tokenizer(prompt, return_tensors="pt").to(model.device) outputs = model.generate(**inputs, max_new_tokens=256) print(tokenizer.decode(outputs[0], skip_special_tokens=True))如果回复风格、术语明显符合预期,说明微调成功。接下来可以接入 Ollama:
ollama create qwen3.5-finetuned -f ./configs/ModelfileModelfile 内容大致如下:
FROM ./models/qwen3.5-9b-lora-merged SYSTEM "你是一个专注于企业内部知识问答的助手,回答要简洁、准确。"然后启动推理:
ollama run qwen3.5-finetuned "请介绍一下公司的差旅报销流程"5. RAG 知识库构建与切块策略
5.1 RAG 原理与流程
RAG 并不是一个复杂的模型,而是一条数据流水线:
- 把文档切分为多个文本块(chunk)。
- 为每个文本块生成向量表示(embedding)并存入向量数据库。
- 用户提问时,把问题编码为向量,在向量库中做相似度检索。
- 取 Top-K 个相关文本块,连同问题一起组成新的 Prompt。
- 模型基于上下文生成最终回答。
一个常见误区是“切块越大越好”。切块太大会引入大量无关信息,降低检索准确率;切块太小会导致语义不完整。切块策略需要根据文档类型和下游任务调优。
5.2 文档切块策略
| 切块方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 固定长度切块 | 通用文本 | 实现简单、速度最快 | 可能切断完整句子或段落 |
| 按段落切块 | 结构清晰的 Markdown/Word 文档 | 语义相对完整 | 长段落仍会超长 |
| 滑动窗口切块 | 长文本、需要上下文衔接 | 能保留相邻句子的上下文 | 会产生较多重复块 |
| 语义切块 | 新闻、论文、合同 | 语义边界相对合理 | 计算成本较高 |
实际项目中我常用的策略是:固定长度为主(例如 512 token),重叠 64 token。这样既能控制向量库大小,又能避免句子被硬生生切断。
# 文件路径:scripts/chunk_text.py def chunk_text(text, chunk_size=512, overlap=64): """按 token 近似长度切块,保留重叠区域。""" if len(text) <= chunk_size: return [text] chunks = [] start = 0 while start < len(text): end = start + chunk_size chunk = text[start:end] chunks.append(chunk) if end >= len(text): break start = end - overlap return chunks5.3 构建向量库并在问答中接入
这里选用轻量级向量数据库,方便本地验证。完整示例使用 LangChain 风格组装。
# 文件路径:scripts/build_rag.py from sentence_transformers import SentenceTransformer import lancedb # 1. 加载 embedding 模型 embed_model = SentenceTransformer("BAAI/bge-small-zh-v1.5") # 2. 准备文本块 texts = [ "公司差旅报销需要提前在系统提交申请。", "报销发票需为合规增值税发票。", "差旅标准:高铁二等座,住宿标准每晚 400 元。", ] embeddings = embed_model.encode(texts).tolist() # 3. 写入向量库 db = lancedb.connect("./data/vector_db") table = db.create_table("company_kb", data=[ {"id": i, "text": t, "vector": embeddings[i]} for i, t in enumerate(texts) ]) print(f"向量库写入完成,共 {table.count()} 条记录")查询时,把用户问题也变成向量,然后查相似度最高的片段。
query = "高铁能坐几等座?" query_vec = embed_model.encode([query])[0] results = table.search(query_vec).limit(3).to_list() for r in results: print(f"相似度: {r['_distance']:.4f} 文本: {r['text']}")检索出相关片段后,再把它拼进 Prompt。这与第 3 章的 Prompt 结构衔接起来:
retrieved_text = "\n".join(r["text"] for r in results) final_prompt = f"""基于以下资料回答问题。如果资料中没有答案,请直接说明“资料中未找到”。 资料: {retrieved_text} 问题: {query} """5.4 RAG 常见的“答非所问”问题
RAG 效果好坏的瓶颈通常不在模型,而在检索环节。遇到“答非所问”时,优先按下面顺序排查:
- Embedding 模型是否匹配领域:通用 embedding 对专业术语效果差,可换领域微调的 embedding 模型。
- 切块是否切断语义:检查 Top-K 结果,看检索到的片段是不是真正回答了问题。
- 向量检索是否需要混合检索:单纯向量检索可能漏掉关键词精确匹配,可以结合 BM25 做混合召回。
- Prompt 中上下文是否过多:5 条不相关片段比 1 条相关片段干扰更大,适当降低 Top-K 数值。
6. Agent 开发与工具调用实战
6.1 Agent 的核心循环
Agent 的本质是一个循环过程:
- 接收用户任务。
- 模型根据 Prompt 和现有信息决定调用哪个工具。
- 代码执行工具函数并返回结果。
- 模型根据结果决定下一步动作或生成最终答案。
这个循环会在模型输出“最终答案”或达到最大迭代次数时停止。ReAct 模式是其中最常见的实现方式。
6.2 一个最小可用的 ReAct Agent
下面给出一个简化版 Python 实现,它把工具函数注册到字典中,解析模型输出的 Action 字段并执行。
# 文件路径:scripts/run_agent.py import re import json def get_weather(city: str) -> str: """模拟天气查询工具。""" weather_map = {"北京": "晴, 25℃", "上海": "多云, 28℃", "广州": "小雨, 26℃"} return weather_map.get(city, "暂无数据,请检查城市名称。") def search_web(query: str) -> str: """模拟搜索工具。""" return f"关于「{query}」的搜索结果为:示例数据一、示例数据二。" TOOLS = { "get_weather": get_weather, "search_web": search_web, } TOOL_DESCRIPTION = """ 可用工具: - get_weather(city: str): 查询城市天气 - search_web(query: str): 搜索互联网信息 输出格式: Thought: 你的思考 Action: 工具名(参数) Observation: 你观察到的结果 ...(可多轮) Thought: 我已经得到最终答案 Answer: 最终回答 """ def parse_action(text: str): """从模型输出中解析 Action 字段。""" match = re.search(r"Action:\s*(\w+)\((.+)\)", text) if not match: return None, None name, raw_args = match.group(1), match.group(2) try: args = json.loads(f"[{raw_args}]") except json.JSONDecodeError: args = [raw_args.strip().strip("'\"")] return name, args def run_agent(prompt): messages = [ {"role": "system", "content": TOOL_DESCRIPTION}, {"role": "user", "content": prompt}, ] for step in range(5): # 防止死循环,最多迭代 5 轮 response = chat_with_model(messages) # 调用本地模型生成回复 print("模型输出:", response) if "Answer:" in response: return response.split("Answer:")[-1].strip() action_name, args = parse_action(response) if action_name and action_name in TOOLS: result = TOOLS[action_name](*args) messages.append({"role": "assistant", "content": response}) messages.append({"role": "user", "content": f"Observation: {result}"}) else: messages.append({"role": "assistant", "content": response}) messages.append({"role": "user", "content": "请重新输出 Action 或直接给出 Answer。"}) return "达到最大迭代次数,任务结束。"这里的chat_with_model是你自己对接模型推理的函数,可以调用前面的 Ollama 接口或 vLLM 接口。最关键的是设计“Action 解析失败后如何重试”——这是 Agent 工程中最容易疏漏的地方。
6.3 Agent 常见报错与调试
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
agent terminated due to error | 工具执行抛异常,Agent 循环没有捕获 | 在工具调用外加 try-except,把错误信息作为 Observation 返回 |
| Action 格式一直解析失败 | 模型没有严格遵循 ReAct 格式 | 微调数据中加入大量 ReAct 示例,或后端对输出做正则校正 |
| 工具参数总传错 | 模型不知道工具参数类型 | 在工具描述中明确参数类型和示例 |
| 进入死循环 | 模型不断重复相同 Action | 设置最大迭代次数,并检测重复 Action |
7. QAT 量化与 Harness 评估
7.1 量化感知训练(QAT)
微调后的模型权重通常占用较大显存,部署到生产环境前需要压缩。量化是常用手段。按量化发生的时间点,可以分成两类:
- 训练后量化(PTQ):直接对训练好的模型做 INT8/INT4 量化,速度快但精度损失可能较大。
- 量化感知训练(QAT):在训练或继续训练阶段就模拟量化误差,让模型逐步适应低比特表示,量化后精度损失更小。
QAT 的流程可以概括为:
原模型 -> 插入伪量化节点 -> 使用带量化的前向/反向训练 -> 导出低比特模型现实中 Qwen 系的 QAT 工作通常依赖专门的量化工具链,不同框架的 API 差异较大,这里不贴死代码。建议在项目中使用前先对照官方量化文档,在验证集上对比量化前后效果。不要盲目追求 4bit,如果任务本身对数字推理、代码生成敏感,精度损失可能不可接受。
7.2 用 Harness 评估模型效果
评估是容易被忽略却至关重要的环节。没有评估,你就无法判断微调到底是变好还是变坏。开源社区常用的评估框架是lm-evaluation-harness。
安装方式:
pip install lm-eval评估命令示例:
lm_eval \ --model hf \ --model_args pretrained=./models/qwen3.5-9b-lora-merged \ --tasks mmlu,ceval-valid \ --batch_size auto \ --output_path ./logs/eval_results评估时需要注意:
- 先测基座模型再做对比:不要只评估微调后的模型,要和基座模型在同一批数据集上对比,才能看出提升或退化。
- 任务要贴合业务:通用榜单(如 MMLU)只反映通用能力,你的场景如果是法律问答,需要额外构造业务评测集。
- 评估不代表一切:客观指标提升,不代表用户体验变好,建议结合人工抽检。
7.3 线上部署建议
压缩后的模型可以用 vLLM 或者 Ollama 提供服务。Ollama 适合中小规模内部使用,vLLM 适合高并发生产环境。下面是 vLLM 启动示例:
vllm serve ./models/qwen3.5-9b-lora-merged \ --served-model-name qwen3.5-ft \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9启动后可以通过 OpenAI 兼容接口调用,方便接入 Agent 框架。
8. 常见问题排查清单
最后整理一份高频问题排查表,建议收藏备查。
| 问题现象 | 常见原因 | 优先排查步骤 |
|---|---|---|
| 训练时报显存不足 | batch 太大、未开梯度检查点 | 调小 batch、开启 gradient_checkpointing、用 QLoRA |
| 训练损失不下降 | 学习率过大或数据噪声过强 | 先用小批次拟合少量样本,再调整学习率 |
| 微调后模型变“笨”了 | 数据分布偏斜或过拟合 | 增加通用数据比例,控制训练轮数 |
| 合并权重后推理出错 | 基座模型与 LoRA 版本不匹配 | 确认 LoRA 基座路径完全一致 |
| RAG 检索结果不相关 | Embedding 模型与领域不匹配 | 换领域 Embedding 模型,或混合检索 |
| Agent 工具调用失败 | 工具解析逻辑太脆弱 | 增加格式校正、异常捕获、重试机制 |
| invalid prompt 报错 | 触发了安全策略 | 改写提示词为中立表达 |
| prompt too long | 上下文超限 | 减少历史轮数、缩短系统 Prompt |
| 推理速度慢 | 未量化、并发配置低 | 使用 vLLM 并发、考虑量化压缩 |
| 评估指标比基座差 | 评测集与微调方向不一致 | 重新审视微调目标,增加通用能力数据 |
9. 最佳实践与工程建议
9.1 数据质量比模型参数更重要
微调的效果上限往往由数据质量决定。一个几千条高质量样本的 QLoRA,效果可能好过几万条低质量样本的全参数微调。在准备数据时,重点关注:
- 去重、清洗、格式统一。
- 多轮对话中要控制 assistant 回答长度,避免过长的样板回答。
- 覆盖边界场景,例如“我不知道”“资料不足”等拒绝回答的样本。
9.2 安全与合规边界
生产环境里,大模型应用无法回避安全问题。
- 涉及用户个人数据时,要注意数据合规与脱敏,不要随意把内部文档送入公有云 API。
- 微调数据中不得包含敏感信息,模型可能通过记忆泄露训练数据。
- 在 Agent 工具设计中,涉及删除、更新、转账等高风险操作,必须加人工审批或二次确认。
- 外部工具调用要限制白名单,防止 Prompt 注入导致恶意工具执行。
这里特别想强调:Agent 面临的安全风险比单纯对话更大。如果模型的输入中存在恶意注入指令,它可能调用不在预期范围内的工具。解决办法是在工具注册时做严格的参数校验,并且不要给模型提供“万能执行”类工具。
9.3 版本管理和可复现性
大模型项目迭代很快,但工程上必须讲究可复现:
- 固定依赖版本,使用
requirements.txt或conda env export保存环境。 - 每次训练记录模型路径、数据版本、训练参数、评估结果。
- 建议使用 WanDB 或本地日志记录训练曲线。
- LoRA 权重和基座模型分开存储,方便回溯和增量训练。
9.4 推荐的上手路线
如果你是从零开始,不建议一上来就追全参数微调或复杂分布式训练。推荐按下面节奏推进:
- 先用 Ollama 跑通 Qwen3.5 的对话。
- 在本地写好 Prompt,熟悉输出格式。
- 用 QLoRA 做一次小规模微调,掌握训练流程。
- 接入 RAG,跑通知识库问答。
- 实现一个工具调用 Agent,体验 ReAct 模式。
- 最后再考虑 QAT 量化和 Harness 评估。
按这个顺序,每一步都在上一步的基础上扩展,排查问题时也能快速定位到具体环节。
10. 总结
这次实战过程其实可以用一句话概括:聊模型,不只看生成,更要看“训练 — 检索 — 工具 — 评估”整条链路。基于 Qwen3.5 的微调、Prompt 调优、RAG 知识库、Agent 开发、QAT 量化和 Harness 评估,六个环节互相独立又有紧密关联。微调负责优化模型的行为习惯,RAG 补充最新事实,Agent 让模型从“会说话”变成“会办事”,量化降低部署成本,评估保证每次迭代方向正确。建议你先从最小闭环开始:一条数据、一次 QLoRA 微调、一个 RAG 查询、一次工具调用,跑通之后再逐步扩大数据规模和业务复杂度。做任何生产变更前,记得先在测试环境验证,做好备份,守住安全边界。希望这份实战笔记能帮你省去一些摸索时间,少踩几个坑。