第一次打开 stablyai/orca 这个仓库时,如果只把它当作一个普通的微调项目,很容易在数据格式、训练脚本和显存占用上绕很多圈子。Orca 的关键不是模型结构,而是一整套训练方法:由强大的教师模型生成带解释轨迹的指令数据,让学生模型在渐进式学习中逐步掌握推理过程。这里不假设仓库已经附带完整权重和全部训练数据,而是从原理、环境、最小复现、验证和排错这条完整链路,把 Orca 类方法跑通并真正理解。读完后,你可以在自建数据集上复现同一条训练流程,也能判断哪些配置会明显影响最终效果。
1. Orca 是什么:解释轨迹与渐进式学习
1.1 从普通指令微调到解释轨迹
在大模型应用中,指令微调(SFT)的标准做法是构造(指令, 期望输出)这样的训练样本,让模型根据输入指令生成正确答案。这种做法能教会模型“说什么”,但很难教会模型“为什么这么说”。遇到需要多步推理的数学题、逻辑题或复杂业务问题,模型很容易记住题面与答案之间的表面映射,一旦题目换一种说法,推理就断掉了。
Orca 类方法改掉了这个训练样本结构。除了最终答案,训练数据里还包含完整的解释轨迹(explanation traces),也就是模型在给出答案前一步步展开的推理过程。训练时,学生模型不仅要生成正确的最终答案,还要学会生成中间推理:先回忆什么公式,再代入什么条件,最后得到什么结论。这样做的好处在于,损失函数不再只监督结果,而是监督整个推理路径。
解释轨迹可以理解为“带步骤的思维过程”。它比单句答案携带更多可学习的信号。学生模型在训练时看到的不是一个黑箱结论,而是一个可模仿、可复用的推理范式。这也是 Orca 与普通指令微调最关键的区别。
1.2 渐进式学习为什么有效
Orca 方法里还有一个容易被忽略的设计:渐进式学习。它不是把所有难度的题目一次性混在一起训练,而是让模型先从相对简单的任务开始,再逐步接触更复杂、需要更多推理步骤的样本。
这样做与人类学习规律一致。模型在简单任务上先学会基础模式,比如“调用公式”“提取条件”“分步计算”,然后这些基础能力被复用到复杂任务上。如果一开始就上高难度题目,模型可能连中间步骤该怎么组织都学不会,梯度信号也容易混乱。
渐进式学习的另一个作用与教师模型相关。教师模型越强,生成的解释轨迹质量越高。但高质量解释往往也更长、更复杂。学生模型能力不足时,面对过长的解释轨迹反而会学到噪音。先用难度适中的解释轨迹把基础推理能力建立起来,再引入更长、更复杂的轨迹,学习效率会明显更高。
1.3 复现时最容易误解的三个点
理解 Orca 方法时,有三个误区需要先澄清。
第一个误区是认为 Orca 是一种新的模型架构。实际上,Orca 是训练方法,不限定底层模型。只要是有监督微调能力的因果语言模型,都可以套用这套数据组织和训练流程。
第二个误区是认为解释轨迹就是随便让教师模型多输出几句话。解释轨迹的质量直接决定训练上限。生成时需要在 prompt 里明确要求教师模型按步骤推理,还要对明显逻辑断裂、答案错误的轨迹做过滤。垃圾解释轨迹喂进去,模型学到的就是垃圾推理模式。
第三个误区是认为数据量越大越好。Orca 类方法里,数据质量远重要于数量。几千条解释清晰、难度递进的数据,比几十万条只含最终答案的数据更有训练价值。实际项目里如果数据管道跟不上,优先保质量,不要盲目扩量。
2. 拿到仓库后,先做环境与依赖对齐
2.1 学习环境和生产环境的差别
在 GitHub 上拿到 stablyai/orca 这类仓库后,第一件事不是直接跑训练,而是确认自己处在什么环境。学习复现和生产训练的环境要求差别非常大,配置错了,后面每一步都会出问题。
| 维度 | 学习复现环境 | 生产训练环境 |
|---|---|---|
| 单卡显存 | 24GB 以上较稳妥,最低 16GB 配合 4bit 量化 | A100 / H100,多卡集群 |
| 模型规模 | 1B 到 3B 的小模型 | 7B 到 70B 的大模型 |
| 精度策略 | fp16 或 4bit LoRA | bf16 + DeepSpeed ZeRO-2/3 |
| 数据规模 | 几千条即可验证流程 | 几十万条以上 |
| 检查点保存 | 本地目录 | 远程对象存储加版本管理 |
| 监控能力 | 简单日志 | 训练指标平台、告警、断点续训 |
学习环境的目标是“跑通”,生产环境的目标是“稳定、可回溯、可回滚”。如果你的卡只有 16GB,建议先跑 1B 级别的小模型,不要直接尝试 7B 全参数微调。全参数训练对显存、数据管线和容错能力的要求都远高于 LoRA。
2.2 Python 环境和依赖安装
Orca 类训练仓库通常基于 PyTorch 生态。依赖安装前先确认 CUDA 版本,因为 PyTorch 的安装命令要对应本地显卡驱动支持的 CUDA 版本。下面的命令用于说明思路,实际版本要先通过nvidia-smi确认。
conda create -n orca python=3.10 -y conda activate orca pip install torch --index-url https://download.pytorch.org/whl/cu121 pip install transformers peft bitsandbytes accelerate datasets deepspeed如果仓库里有requirements.txt,安装完基础依赖后要再执行一次:
pip install -r requirements.txt这里要注意版本一致性。transformers的新版本经常会调整 API,peft和bitsandbytes的版本如果偏旧,加载 4bit 量化模型时可能直接报错。仓库没有明确版本时,落地前先确认当前生态的稳定版本组合,不要盲目装最新版。
2.3 克隆仓库并确认目录结构
克隆仓库命令如下,实际地址以仓库为准:
git clone https://github.com/stablyai/orca.git cd orca进入目录后,先不要急着看训练脚本,而是把仓库结构整体浏览一遍:
orca/ ├── README.md ├── requirements.txt ├── data/ │ └── orca_train.jsonl ├── scripts/ │ └── train_lora.sh ├── src/ │ ├── data.py │ ├── model.py │ └── train.py ├── configs/ │ └── lora_config.yaml └── outputs/重点看四个东西:README 里关于启动方式的说明、requirements.txt里的依赖版本、data/目录下是否真的有训练数据、scripts/里的启动脚本是否依赖特定路径。很多复现失败不是代码问题,而是数据文件不存在或路径写死。
如果仓库里没有提供数据文件,就需要自己准备或从公开数据集转换。这一点很常见,因为完整训练数据通常会单独发布,不会所有仓库都直接带上几万条 JSONL。
3. 用最小配置复现一次 Orca 类训练
3.1 准备训练数据:JSONL 格式与字段说明
Orca 类训练数据可以组织成 JSONL 格式,每行一条样本。核心字段包括指令、可选输入、最终答案和解释轨迹。下面是一条数学样例:
{ "instruction": "一个长方形的长是 8 厘米,宽是 5 厘米,求它的面积。", "input": "", "output": "40 平方厘米", "explanation": "先回忆长方形面积公式:面积等于长乘以宽。题目给出长 8 厘米、宽 5 厘米,代入公式得到 8 × 5 = 40,单位是平方厘米。" }训练目标是让模型同时学会推理和作答。可以把explanation与output拼接成完整 target:
推理过程:先回忆长方形面积公式:面积等于长乘以宽。题目给出长 8 厘米、宽 5 厘米,代入公式得到 8 × 5 = 40,单位是平方厘米。 最终答案:40 平方厘米如果input字段不为空,说明指令里还要附加一段背景材料,比如表格数据或多轮上下文。此时格式函数里要额外拼一段补充信息。
3.2 数据加载与 tokenize 的核心代码
数据加载可以用 HuggingFacedatasets库。关键是 tokenize 时要把 prompt 部分和 target 部分区分开:训练时只对 target 计算损失,prompt 不参与损失计算,否则模型会学着把题目也背出来。
import json from datasets import load_dataset from transformers import AutoTokenizer model_name = "TinyLlama/TinyLlama-1.1B-Chat-v1.0" tokenizer = AutoTokenizer.from_pretrained(model_name) tokenizer.pad_token = tokenizer.eos_token def format_example(sample): prompt = f"题目:{sample['instruction']}\n请给出推理过程和最终答案。\n" if sample.get("input"): prompt += f"补充信息:{sample['input']}\n" target = ( f"推理过程:{sample['explanation']}\n" f"最终答案:{sample['output']}" ) return prompt, target def tokenize_train(examples): inputs = [] labels = [] for i in range(len(examples["instruction"])): prompt, target = format_example({ "instruction": examples["instruction"][i], "input": examples["input"][i], "output": examples["output"][i], "explanation": examples["explanation"][i], }) prompt_ids = tokenizer(prompt, truncation=True, max_length=512).input_ids target_ids = tokenizer(target, truncation=True, max_length=512).input_ids input_ids = prompt_ids + target_ids + [tokenizer.eos_token_id] label_ids = [-100] * len(prompt_ids) + target_ids + [tokenizer.eos_token_id] if len(input_ids) > 1024: input_ids = input_ids[:1024] label_ids = label_ids[:1024] inputs.append(input_ids) labels.append(label_ids) return {"input_ids": inputs, "labels": labels} ds = load_dataset("json", data_files="data/orca_train.jsonl", split="train") ds = ds.map(tokenize_train, batched=True, remove_columns=ds.column_names)这里有几个关键点。第一,-100是 PyTorch 交叉熵损失里的忽略索引,被标成-100的位置不会参与损失计算。第二,把 prompt 和 target 拼在一起输入,但 labels 里 prompt 部分全部用-100屏蔽。第三,max_length和截断要配合好,避免 prompt 过长导致 target 被截掉。
3.3 模型加载与 LoRA 配置
模型部分建议先用 LoRA 做参数高效微调。LoRA 只训练低秩矩阵,不修改原始权重,显存占用小,也方便后续合并保存。
from transformers import AutoModelForCausalLM, TrainingArguments, Trainer from peft import LoraConfig, get_peft_model import torch base_model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype=torch.float16, device_map="auto", ) 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", ) model = get_peft_model(base_model, lora_config) model.print_trainable_parameters()target_modules需要与底层模型的模块名一致。上面给出的模块名适用于 LLaMA 系结构,如果你用的基础模型是 Qwen、Mistral 或百川,模块名可能不同。最稳妥的方法是把模型结构打印出来,确认注意力层和 MLP 层的命名规律后再配置。
3.4 训练循环与启动命令
训练可以直接用transformers的Trainer,减少手写训练循环的出错概率。
training_args = TrainingArguments( output_dir="outputs/orca-lora", per_device_train_batch_size=1, gradient_accumulation_steps=8, learning_rate=2e-4, num_train_epochs=1, logging_steps=10, save_steps=100, fp16=True, dataloader_num_workers=2, remove_unused_columns=False, ) trainer = Trainer( model=model, args=training_args, train_dataset=ds, ) trainer.train()启动命令用一条脚本记录下来,方便后续调整参数重跑:
#!/usr/bin/env bash set -e CUDA_VISIBLE_DEVICES=0 python src/train.py \ --model_name TinyLlama/TinyLlama-1.1B-Chat-v1.0 \ --data_path data/orca_train.jsonl \ --output_dir outputs/orca-lora \ --num_train_epochs 1 \ --gradient_accumulation_steps 8batch_size=1加上gradient_accumulation_steps=8相当于实际批次大小是 8,同时保持单张卡不会一次吃下太多样本。显存紧张时,这是最常用的组合。
3.5 训练完成后的模型合并
LoRA 权重训练完保存在 checkpoint 里,推理时可以直接加载 LoRA 权重,也可以合并回基础模型再保存。合并后的模型使用更方便,部署时不需要额外依赖 PEFT 逻辑。
from peft import PeftModel import torch from transformers import AutoModelForCausalLM, AutoTokenizer base_model = AutoModelForCausalLM.from_pretrained( model_name, torch_dtype=torch.float16, device_map="auto", ) model = PeftModel.from_pretrained(base_model, "outputs/orca-lora/checkpoint-100") merged_model = model.merge_and_unload() merged_model.save_pretrained("outputs/orca-merged") tokenizer.save_pretrained("outputs/orca-merged")合并后建议跑一遍推理,确认输出正常再进入评估。合并失败或模块名不对时,模型输出会出现明显的乱码或重复,这一步能提前暴露问题。
4. 关键参数说明:从快速出结果到稳定收敛
4.1 数据侧参数
数据侧最影响结果的是max_length、截断策略和 prompt 模板。
max_length决定一条样本最长能保留多少 token。设置过短,长推理步骤会被截断,模型学不到完整逻辑;设置过长,显存占用和训练时间同步上升。常见做法是先统计数据集的 token 长度分布,再取 90 分位作为默认值,而不是随便填一个 2048。
prompt 模板要稳定统一。训练时用什么模板,推理时就用什么模板。模板不一致是“训练时 loss 正常、推理时效果拉垮”的常见原因。
4.2 训练侧参数
训练侧参数决定了收敛速度、稳定性和最终效果。下面是一张常用参数速查表:
| 参数 | 含义 | 常见值 | 偏大影响 | 偏小影响 | 建议 |
|---|---|---|---|---|---|
| learning_rate | 学习率 | 1e-4 到 3e-4 | 不收敛,loss 震荡或变 NaN | 收敛慢,效果不足 | LoRA 微调通常从 1e-4 起步 |
| per_device_train_batch_size | 单卡批次大小 | 1 到 4 | 显存溢出 | 训练慢 | 显存紧张时先保 batch_size=1 |
| gradient_accumulation_steps | 梯度累积步数 | 4 到 16 | 等效 batch 过大,收敛不稳 | 等效 batch 过小 | 与 batch_size 配合,控制等效 batch |
| num_train_epochs | 训练轮数 | 1 到 3 | 过拟合,记住训练题 | 学不透 | 小数据集先试 1 到 2 轮 |
| warmup_ratio | 预热比例 | 0.03 到 0.1 | 前期浪费步数 | 前期震荡 | 数据量大时取较小值 |
| lora_r | LoRA 秩 | 8 到 32 | 可学习参数多,显存涨 | 表达能力不足 | 先 8,效果不够再上调 |
| lora_alpha | LoRA 缩放系数 | 16 到 32 | 权重贡献大,易震荡 | 权重贡献小 | 一般设为 lora_r 的 2 倍 |
| fp16 / bf16 | 混合精度 | 视显卡而定 | fp16 可能溢出 | 无 | 当前主流显卡优先 bf16 |
| gradient_checkpointing | 梯度检查点 | True/False | 换显存省计算 | — | 显存不足时打开,训练变慢 |
不要一次性把所有参数都调到位。正确做法是先固定数据样例和评估方法,只调学习率和训练轮数,等模型能稳定收敛后,再动 LoRA 秩和批次大小。
4.3 三种典型配置组合
根据不同目标,可以直接套用下面三种组合:
| 目标 | 模型 | 数据量 | 学习率 | LoRA 配置 | 注意事项 |
|---|---|---|---|---|---|
| 流程验证 | 1B 左右 | 200 到 1000 条 | 2e-4 | r=8, alpha=16 | 只看能不能跑通,不评估效果 |
| 常规业务微调 | 7B 左右 | 1 万到 10 万条 | 1e-4 | r=16, alpha=32 | 关注 loss 下降和评估指标 |
| 生产级训练 | 13B 以上 | 几十万条 | 按规模衰减 | 按需要调 | 必须配 DeepSpeed 和监控 |
流程验证和常规业务微调的配置可以互相切换,但不要在流程验证阶段就上大规模数据和长训练时间。先花半小时跑通最小闭环,再讨论如何把效果调好。
5. 验证效果:不能只看 loss 降没降
5.1 用推理脚本检查解释轨迹
训练结束后,先用一条没见过的题验证模型能否生成完整推理过程。下面是一段最小推理代码:
import torch from transformers import AutoModelForCausalLM, AutoTokenizer model_path = "outputs/orca-merged" tokenizer = AutoTokenizer.from_pretrained(model_path) model = AutoModelForCausalLM.from_pretrained( model_path, torch_dtype=torch.float16, device_map="auto", ) prompt = "题目:一个三角形三个内角分别是 60 度和 70 度,求第三个角。\n请给出推理过程和最终答案。\n" input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to(model.device) output_ids = model.generate( input_ids, max_new_tokens=256, do_sample=False, ) print(tokenizer.decode(output_ids[0][input_ids.shape[-1]:], skip_special_tokens=True))正常输出应该包含两个部分:一段说明内角和定理的推理过程,以及最终答案 50 度。如果输出直接跳到答案,没有中间推理,说明训练数据里的解释轨迹可能没有起作用,或者损失屏蔽把 target 里的推理部分误伤了。
5.2 评估维度与人工检查
Orca 类模型评估不能只看一个分数。至少要从三个维度分别检查:
| 维度 | 检查内容 | 判断方式 |
|---|---|---|
| 答案正确性 | 最终答案是否为正确答案 | 与标准答案比对 |
| 推理一致性 | 中间推理是否能推出最终答案 | 人工阅读推理步骤 |
| 过程规范性 | 是否先解释定理、再代入条件、最后给结论 | 检查步骤顺序和结构 |
| 幻觉程度 | 是否引用了不存在的公式或条件 | 人工核对引用 |
机器评估可以算最终答案的准确率,但推理过程质量很难用自动化指标完全替代。建议在小规模验证集上做抽样人工评估,至少检查 50 到 100 条输出。
5.3 常见评估指标及其局限
BLEU、ROUGE 这类文本相似度指标不适合评估推理过程。推理过程可以表达正确但措辞完全不同,使用相似度指标会被严重误判。准确率适合计算 final answer 是否字符串相等,但字符串相等对数字和格式容错低。更实用的做法是准备一份带标准答案的验证集,用程序比对最终答案,再配合人工抽查推理过程。
6. 常见问题与排查路径
6.1 CUDA out of memory
这是复现 Orca 类训练时最常见的问题。
现象:训练刚开始就报CUDA out of memory。
可能原因:per_device_train_batch_size太大、max_length过长、多个模型同时占用显存、没有启用梯度检查点。
检查方式:先看nvidia-smi,确认是否还有其他进程占卡;再逐步降低 batch_size 到 1,把max_length从 2048 降到 1024,打开gradient_checkpointing=True。
解决方案:优先开梯度检查点,再考虑 4bit 量化加载基础模型。量化加载方式可以大幅减少显存占用,但训练速度会变慢。
6.2 加载模型或 LoRA 权重时报错
现象:from_pretrained时提示找不到权重文件,或者加载 LoRA 时提示模块名不匹配。
可能原因:基础模型没有先从 HuggingFace 下载成功,checkpoint 路径写错,target_modules与模型真实模块名不一致。
检查方式:确认~/.cache/huggingface下是否有模型缓存;用print(model)查看真实模块名;确认 checkpoint 目录里存在adapter_config.json。
解决方案:先单独加载基础模型,再加载 LoRA,逐步缩小问题范围。不要为了省事跳过基础模型加载这一步。
6.3 训练 loss 不下降或直接变成 NaN
现象:训练几轮后 loss 没有明显下降,或者某一步直接从正常值跳到 NaN。
可能原因:学习率过高、数据里有脏字符、fp16 溢出、labels 构造错误导致模型只在预测 $-100$ 位置。
检查方式:看训练日志里 loss 的变化曲线;检查数据文件是否包含空字符串或异常编码;把 fp16 换成 bf16 试一次。
解决方案:先降低学习率到 1e-5 测试模型是否能拟合一条数据,再从数据里排除空样本和超长样本。如果单条数据能拟合,说明数据和代码没问题,再逐步恢复学习率。
6.4 LoRA 合并后推理输出乱码或重复
现象:训练时没报错,合并后推理输出全是重复 token。
可能原因:合并前没有调用merge_and_unload,保存的权重不完整;tokenizer 与模型不一致;推理时没有正确添加 pad token 或 eos token。
检查方式:先不合并,直接用PeftModel.from_pretrained加载 LoRA 推理,确认是否是合并带来的问题。再对比保存目录里tokenizer_config.json是否存在。
解决方案:重新用merge_and_unload保存,推理时设置tokenizer.pad_token = tokenizer.eos_token,并把skip_special_tokens=True。
6.5 排查链路汇总
遇到问题不要盲目改参数,按下面的顺序定位:
- 检查输入数据:字段是否完整,是否为空,长度是否异常。
- 检查文件路径:数据路径、模型路径、checkpoint 路径是否正确。
- 检查依赖版本:transformers、peft、bitsandbytes 是否兼容。
- 检查配置是否生效:修改的配置有没有真正传进训练脚本。
- 检查资源占用:显存、内存、CPU 进程是否被其他任务抢占。
- 检查日志关键字:是 OOM、NaN 还是路径错误,日志里的 stack trace 能给出直接线索。
- 检查框架版本限制:某些 API 在最新版可能已经改名,要按当前版本调整。
| 问题现象 | 可能原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| CUDA out of memory | batch 过大、max_length 过长 | nvidia-smi 查看显存占用 | batch_size=1,开 gradient checkpointing |
| 加载权重失败 | 路径错误,模型未下载成功 | 看缓存目录、确认 adapter_config.json | 先单独加载基础模型 |
| loss 为 NaN | 学习率过高,fp16 溢出 | 看 loss 曲线,换 bf16 | 降低学习率,清理异常数据 |
| 合并后输出乱码 | 未 merge_and_unload,tokenizer 不匹配 | 先加载 LoRA 推理对比 | 重新合并保存,设置 pad_token |
7. 最佳实践与扩展方向
7.1 数据质量是 Orca 类方法的天花板
Orca 类方法的效果上限,由解释轨迹质量决定。生成解释轨迹时,教师模型的 prompt 要显式要求分步推理,最好给出固定格式,比如“第一步……第二步……最后……”。生成后要做过滤,至少排除三类数据:最终答案错误、推理与答案不一致、推理步骤明显跳跃。
训练数据的难度分布也要控制。不要全是简单题,也不要全是复杂题。一种可行做法是先把数据按推理步数分桶,简单样本占一部分,中等难度占大部分,极难样本只保留少量。这样的分布更符合渐进式学习的思路。
7.2 从复现到自建教师-学生蒸馏流程
复现仓库只是第一步。实际项目里往往需要自己搭建教师-学生蒸馏流程。典型流程如下:
- 收集一批种子题目,覆盖目标领域的主要题型。
- 用教师模型生成逐步解释和最终答案。
- 用规则校验答案正确性,抽样检查推理质量。
- 清洗数据并转换成统一的 JSONL 格式。
- 用小模型做一轮快速训练,确认数据管道没有污染。
- 全量训练学生模型,在验证集上抽样评估。
这个流程里最容易被忽略的是步骤 3。没有校验的教师输出直接进训练集,等于把错误推理当成标准答案教给学生。生成解释时可以多用几条独立 prompt 重复采样,再选择答案一致且推理完整的样本。
7.3 训练前检查清单
每次开始训练前,按下面这个清单过一遍,能省掉大量返工时间:
- 确认基础模型名称和下载状态,磁盘空间足够。
- 确认数据文件存在,字段名与加载代码一致。
- 确认 tokenize 后样本没有空内容,推理部分没有被截断。
- 先跑 1 到 5 步的 smoke run,确认 loss 能正常打印。
- 确认 checkpoint 保存路径存在且有写权限。
- 记录当前训练参数,方便复现和对比。
- 确认显存没有其他进程占用。
- 评估过程固定为同一套 prompt 模板,不能训练和推理用不同模板。
7.4 扩展方向:推理技巧、多轮对话与对齐
Orca 类方法的方向还在不断演进。后续工作开始关注模型如何在推理前选择策略:是逐步推导,还是先回忆相关知识点,再生成答案。这是从“会分步推理”走向“会选择合适的推理方式”。
实际项目里还有两个值得投入的方向。第一个是多轮对话中的推理,模型在长上下文里要能区分历史信息、当前问题和中间结论。第二个是与 RLHF、DPO 对齐结合,先通过解释轨迹学推理,再通过偏好优化让输出更符合业务规范。
对新手来说,最有价值的练习不是追求一次跑赢某个榜单,而是把“一条训练样本从原始题目到最终的推理输出”这条链路完全掌握。能说清数据格式为什么这样设计、损失为什么只算在 target 上、LoRA 参数为什么这样设置,才算真正理解 Orca 类方法。先跑通最小闭环,再逐步加数据、加评估、加部署,这条路比直接套大模型训练框架要扎实得多。