文章

从 C++ 到 LLM 实战:本地部署 DeepSeek 并用私有数据做一次 Python 微调

面向传统 C++ 工程师,讲解如何在单机上部署开源 DeepSeek 模型、准备指令数据集,并使用 Python + LoRA 完成一次可运行的轻量微调。

从 C++ 到 LLM 实战:本地部署 DeepSeek 并用私有数据做一次 Python 微调

如果你是传统 C++ 程序员,第一次接触大模型工程,最常见的疑问通常是:

  1. 我能不能像部署一个服务那样,把开源模型先跑起来?
  2. 我能不能不用“重新训练整个模型”,只做一个轻量微调?
  3. 全流程能不能尽量工程化,而不是只会跑 Colab Notebook?

这篇文章给你一条 “先跑通、再优化” 的路径:

  • 用 Python 在本地部署一个 DeepSeek 开源模型;
  • 用自己的小数据集做一次 LoRA 微调;
  • 最后验证微调前后输出差异。

目标读者:有 C++ 工程经验,了解基本 Linux 命令,但对 Hugging Face / PEFT / SFT 还不熟。


0. 先建立正确预期(非常重要)

0.1 “部署”和“微调”是两件事

  • 部署(Inference):加载模型并提供推理服务。
  • 微调(Fine-tuning):在你自己的数据上更新一小部分参数(如 LoRA adapter)。

你可以先部署,再微调;微调完成后再部署“底座模型 + adapter”。

0.2 硬件建议

为了降低门槛,这里按“能跑通”为目标:

  • 最低可尝试:单张 24GB 显存(例如 4090)+ 4bit 量化 + LoRA
  • 更舒服:40GB+ 显存,或者多卡
  • 仅 CPU:理论可推理,训练体验通常较差

0.3 模型与许可

DeepSeek 有多个版本和许可协议。请在下载前确认:

  • 模型是否允许商用;
  • 你的数据是否允许用于训练;
  • 产出是否需要额外合规审查。

1. 环境准备:像搭 C++ 构建环境一样搭 Python 训练环境

建议使用 venv 隔离依赖。

1
2
3
python3 -m venv .venv
source .venv/bin/activate
pip install -U pip

安装核心依赖:

1
pip install torch transformers datasets peft trl accelerate bitsandbytes sentencepiece

可选检查(类似你在 C++ 里看编译器/链接器版本):

1
2
python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
nvidia-smi

2. 先做“部署”:本地最小推理脚本

先不管微调,先确认模型能正确加载和回答。

新建 infer_deepseek.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
from transformers import AutoTokenizer, AutoModelForCausalLM
import torch

# 根据你要使用的具体 DeepSeek 开源模型替换
MODEL_NAME = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"


def main():
    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)

    model = AutoModelForCausalLM.from_pretrained(
        MODEL_NAME,
        torch_dtype=torch.float16,
        device_map="auto",
        trust_remote_code=True,
    )

    prompt = "请用 5 句话解释 C++ RAII 的核心价值。"

    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=256,
            do_sample=True,
            temperature=0.7,
            top_p=0.9,
        )

    text = tokenizer.decode(outputs[0], skip_special_tokens=True)
    print(text)


if __name__ == "__main__":
    main()

运行:

1
python infer_deepseek.py

如果这里就报错,通常是:

  • 显存不足(先换更小模型/开量化);
  • transformers 版本不匹配;
  • 模型名写错或未登录 Hugging Face。

3. 准备你自己的微调数据:先用“指令-回答”结构

对传统工程师,最稳妥的第一步是 SFT(监督微调),数据格式尽量简单。

新建 data/train.jsonl(每行一个样本):

1
2
3
{"instruction":"解释什么是线程安全的单例模式","output":"线程安全单例模式保证在多线程环境中实例只被初始化一次..."}
{"instruction":"给出一个 C++17 的最小线程池骨架","output":"下面给出一个简化实现,包含任务队列、工作线程与停止机制..."}
{"instruction":"如何排查 Linux 下 C++ 服务内存泄漏","output":"建议先用 ASan/LSan 在测试环境复现,再结合 valgrind 或 heaptrack..."}

数据建议

  • 先准备 200~2000 条高质量样本,宁少勿脏;
  • 输出风格尽量一致(例如都偏工程文档风格);
  • 避免包含密钥、隐私、受版权限制内容。

4. 用 LoRA 做一次轻量微调(Python)

新建 train_lora.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import torch
from datasets import load_dataset
from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
    BitsAndBytesConfig,
    TrainingArguments,
)
from peft import LoraConfig
from trl import SFTTrainer

MODEL_NAME = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"
DATA_PATH = "data/train.jsonl"
OUT_DIR = "outputs/deepseek-lora-cpp"


def format_example(example):
    instruction = example["instruction"].strip()
    output = example["output"].strip()
    return {
        "text": (
            "你是资深 C++ 架构师,请给出工程可落地的回答。\n\n"
            f"### 问题\n{instruction}\n\n"
            f"### 回答\n{output}"
        )
    }


def main():
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.float16,
        bnb_4bit_use_double_quant=True,
    )

    tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME, trust_remote_code=True)
    if tokenizer.pad_token is None:
        tokenizer.pad_token = tokenizer.eos_token

    model = AutoModelForCausalLM.from_pretrained(
        MODEL_NAME,
        quantization_config=bnb_config,
        device_map="auto",
        trust_remote_code=True,
    )

    dataset = load_dataset("json", data_files=DATA_PATH, split="train")
    dataset = dataset.map(format_example, remove_columns=dataset.column_names)

    peft_config = LoraConfig(
        r=8,
        lora_alpha=16,
        lora_dropout=0.05,
        bias="none",
        task_type="CAUSAL_LM",
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj"],
    )

    training_args = TrainingArguments(
        output_dir=OUT_DIR,
        per_device_train_batch_size=1,
        gradient_accumulation_steps=8,
        learning_rate=2e-4,
        num_train_epochs=2,
        logging_steps=10,
        save_steps=100,
        save_total_limit=2,
        fp16=True,
        report_to="none",
    )

    trainer = SFTTrainer(
        model=model,
        train_dataset=dataset,
        peft_config=peft_config,
        args=training_args,
        tokenizer=tokenizer,
        dataset_text_field="text",
        max_seq_length=1024,
    )

    trainer.train()
    trainer.model.save_pretrained(OUT_DIR)
    tokenizer.save_pretrained(OUT_DIR)


if __name__ == "__main__":
    main()

运行:

1
python train_lora.py

训练完成后,outputs/deepseek-lora-cpp/ 里是 adapter 权重(不是完整底座模型)。


5. 加载微调后的 LoRA adapter 做验证

新建 infer_lora.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel

BASE_MODEL = "deepseek-ai/DeepSeek-R1-Distill-Qwen-7B"
ADAPTER_PATH = "outputs/deepseek-lora-cpp"


def main():
    tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True)
    base_model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL,
        torch_dtype=torch.float16,
        device_map="auto",
        trust_remote_code=True,
    )

    model = PeftModel.from_pretrained(base_model, ADAPTER_PATH)

    prompt = "如何设计一个支持优雅停机的 C++ 网络服务框架?"
    inputs = tokenizer(prompt, return_tensors="pt").to(model.device)

    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=256,
            temperature=0.7,
            top_p=0.9,
            do_sample=True,
        )

    print(tokenizer.decode(outputs[0], skip_special_tokens=True))


if __name__ == "__main__":
    main()

你可以用同一个 prompt 分别跑:

  1. 底座模型输出;
  2. 底座 + LoRA 输出;

观察是否更符合你的目标风格(例如更偏 C++ 工程落地、术语更统一)。


6. 给 C++ 程序员的迁移类比

把这套流程类比成你熟悉的 C++ 工程:

  • 底座模型 ≈ 你依赖的成熟基础库(如 Boost / LLVM)
  • LoRA adapter ≈ 你的项目补丁层(不改库本体,按需叠加)
  • SFT 数据集 ≈ 领域测试样例 + 规范文档
  • 评测集 ≈ 回归测试集(升级后要看是否“修复问题同时不引入倒退”)

这就是为什么 LoRA 对工程团队很友好:

  • 迭代快;
  • 成本可控;
  • 易于版本化管理。

7. 常见问题与排障

7.1 OOM(显存不够)怎么办?

优先顺序:

  1. 降低 max_seq_length
  2. 减小 batch size;
  3. 增加 gradient_accumulation_steps
  4. 改更小模型;
  5. 启用更激进量化。

7.2 Loss 降了但效果没变?

  • 数据过于同质或太少;
  • 训练轮数不够,或学习率不合适;
  • 评测问题与训练分布不一致。

建议做一个 50~200 条的小评测集,分主题统计命中率。

7.3 需要全量微调吗?

多数业务早期不需要。先用 LoRA 验证价值,再决定是否进入更重的训练方案。


8. 下一步怎么工程化

当你跑通这篇最小闭环后,可以逐步升级:

  1. 数据工程化:增加清洗、去重、版本管理;
  2. 评测工程化:固定 benchmark + 自动评分;
  3. 服务化部署:封装成 API(如 FastAPI/vLLM);
  4. 观察性:记录 prompt、延迟、命中率、用户反馈。

结语

对于传统 C++ 程序员,切入 LLM 最好的方式不是“先学所有论文”,而是先拿一条最小工程链路跑通:

本地部署 → 小数据集 LoRA 微调 → 对比验证 → 迭代数据与评测。

当你把这个闭环跑过两三次,就会发现它和你熟悉的系统工程并没有本质差异:

  • 都强调可复现;
  • 都依赖迭代和回归;
  • 都是“先正确,再更快”。

祝你从 C++ 工程顺滑迁移到 LLM 工程。下一篇如果你愿意,我可以继续写:

  • 如何把这个流程接入你现有 C++ 后端(Python 推理服务 + C++ RPC 调用);
  • 如何设计一套面向 C++ 代码库问答的 RAG + 微调混合方案。
本文由作者按照 CC BY 4.0 进行授权