LobeChat 自托管部署指南 — Docker 一条命令接 DeepSeek
LobeChat 是当前最流行的开源 AI 聊天前端之一,界面漂亮、功能齐全,支持聚合多家大模型供应商。自己部署一台的好处很实在:对话数据不出自己的服务器,模型按需切换,还省了订阅费。本文给出一条命令的 Docker 部署方案,直接接 DeepSeek——国内直连、便宜、够用。
DeepSeek 相关
查看所有标签LobeChat 是当前最流行的开源 AI 聊天前端之一,界面漂亮、功能齐全,支持聚合多家大模型供应商。自己部署一台的好处很实在:对话数据不出自己的服务器,模型按需切换,还省了订阅费。本文给出一条命令的 Docker 部署方案,直接接 DeepSeek——国内直连、便宜、够用。
DeepSeek-R1 发布后,GRPO(Group Relative Policy Optimization)成了对齐训练领域最火的关键词。但如果你只看了 PR 通稿,大概率会以为 GRPO 是 DeepSeek 发明的全新算法——实际上它是 DPO 和 PPO 的自然演化,核心思想早就有迹可循。
这篇文章从工程落地的角度,把 RLHF → DPO → GRPO 这条线讲清楚。有代码、有坑、有 benchmark。
RLHF(Reinforcement Learning from Human Feedback)的标准流程:
┌─────────┐ ┌──────────┐ ┌──────────┐
│ SFT │───▶│ Reward │───▶│ PPO │
│ Model │ │ Model │ │ Update │
└─────────┘ └──────────┘ └──────────┘
工程上维护这个流程的痛点:
DPO 的出现解决了 reward model 的问题,但引入了新问题。
DPO(Direct Preference Optimization) 的核心洞察:PPO 的优化目标可以重参数化,直接消掉 reward model。
DPO 的 loss:
def dpo_loss(
policy_chosen_logp, # π_θ(y_w|x)
policy_rejected_logp, # π_θ(y_l|x)
ref_chosen_logp, # π_ref(y_w|x)
ref_rejected_logp, # π_ref(y_l|x)
beta=0.1, # temperature
):
"""
DPO loss: maximize the gap between chosen and rejected responses.
β controls how far the policy can deviate from the reference.
"""
import torch
policy_log_ratio = policy_chosen_logp - policy_rejected_logp
ref_log_ratio = ref_chosen_logp - ref_rejected_logp
logits = policy_log_ratio - ref_log_ratio
loss = -torch.nn.functional.logsigmoid(beta * logits)
return loss.mean()
不需要 reward model,只需要偏好对 (chosen, rejected)。训练速度比 RLHF 快 2-4 倍。
但 DPO 的硬伤:
这就是为什么 2024 年底开始,工业界开始探索 在线对齐 方案。
GRPO 是 DeepSeek 在 DeepSeekMath 论文中提出的,后来被 DeepSeek-R1 采用。
传统 PPO 需要四个模型:
PPO: Policy + Reference + Reward + Value (4 个模型)
GRPO: Policy + Reference + Reward (3 个模型)
GRPO 去掉了 Value 模型(critic),用组内相对比较替代 advantage 估计:
对每个 prompt x,采样 G 个响应 {y1, y2, ..., yG}
→ 用 reward model 打分得到 {r1, r2, ..., rG}
→ 组内归一化:advantage_i = (ri - mean(r)) / std(r)
→ 只更新使 advantage > 0 的 token
def grpo_loss(
policy_model,
reference_model,
prompts, # batch of prompts
reward_func, # callable: (prompt, response) -> float
group_size=4, # G: responses per prompt
beta=0.04, # KL penalty coefficient
epsilon=0.2, # clipping parameter
):
"""
GRPO loss as used in DeepSeek-R1.
Key difference from PPO:
- No critic/value model
- Advantage computed via group normalization
- Clipping on both sides like PPO
"""
import torch
import torch.nn.functional as F
total_loss = []
total_kl = []
for prompt in prompts:
# Step 1: Sample G responses from current policy
responses = []
rewards = []
with torch.no_grad():
for _ in range(group_size):
resp_ids, logps = policy_model.generate(
prompt, return_logprobs=True, do_sample=True
)
responses.append((resp_ids, logps))
# Get reward (rule-based or model-based)
resp_text = policy_model.tokenizer.decode(resp_ids[0])
r = reward_func(prompt, resp_text)
rewards.append(r)
rewards = torch.tensor(rewards)
# Step 2: Group normalization for advantage
advantages = (rewards - rewards.mean()) / (rewards.std() + 1e-8)
# Step 3: Compute loss for each positive-advantage response
for i, (resp_ids, old_logps) in enumerate(responses):
if advantages[i] <= 0:
continue
# Current policy logprobs
new_logps = policy_model.get_logprobs(prompt, resp_ids)
# PPO-style ratio
ratio = torch.exp(new_logps - old_logps)
# Clipped objective
clipped_ratio = torch.clamp(
ratio, 1 - epsilon, 1 + epsilon
)
policy_loss = -torch.min(
ratio * advantages[i],
clipped_ratio * advantages[i]
).mean()
# KL penalty to reference model
ref_logps = reference_model.get_logprobs(prompt, resp_ids)
kl = (new_logps - ref_logps).mean()
loss = policy_loss + beta * kl
total_loss.append(loss)
total_kl.append(kl.item())
if total_loss:
return torch.stack(total_loss).mean(), sum(total_kl) / len(total_kl)
return torch.tensor(0.0), 0.0
| 维度 | PPO | DPO | GRPO |
|---|---|---|---|
| 在线采样 | ✅ | ❌ | ✅ |
| 需要 Value Model | ✅ | N/A | ❌ |
| 需要 Reward Model | ✅ | ❌ | ✅ |
| 偏好数据需求 | 低 | 高 | 低 |
| 显存占用 | 高 | 中 | 中 |
| 训练稳定性 | 低 | 高 | 中 |
Benchmark 数据(来自 DeepSeekMath 论文):
| 方法 | MATH 500 | GSM8K | HumanEval |
|---|---|---|---|
| Base (DeepSeek-Coder 7B) | 51.7% | 78.2% | 48.2% |
| + SFT | 56.2% | 82.4% | 52.4% |
| + DPO | 58.4% | 83.1% | 54.3% |
| + GRPO | 64.2% | 86.7% | 58.5% |
GRPO 在数学和代码推理任务上比 DPO 显著提升,尤其在 MATH 上差了近 6 个点。
截至 2026 年 8 月,TRL(Transformer Reinforcement Learning)已原生支持 GRPO:
from datasets import load_dataset
from trl import GRPOConfig, GRPOTrainer
from transformers import AutoModelForCausalLM, AutoTokenizer
# 1. 加载模型
model_name = "Qwen/Qwen2.5-7B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto",
attn_implementation="flash_attention_2",
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
# 2. 准备数据集(只需要 prompts)
dataset = load_dataset("openai/gsm8k", "main", split="train[:2000]")
def format_prompt(example):
return {
"prompt": f"Question: {example['question']}\nAnswer: "
}
dataset = dataset.map(format_prompt)
# 3. 定义 reward function
def math_reward(prompt, completion):
"""
Rule-based reward for math reasoning.
简单粗暴:提取 \boxed{} 中的答案,跟标准答案比对。
"""
import re
# Extract answer from completion
match = re.search(r'\\boxed\{([^}]+)\}', completion)
if not match:
return 0.0
predicted = match.group(1).strip()
# Extract ground truth (GSM8K format: "#### 42")
# We need the ground truth from somewhere — in practice
# you'd pass it alongside the prompt
# Simplified here for illustration
return 1.0 if predicted # placeholder
# 实际使用时,用 GRPOTrainer 自带的 reward 机制:
# 可以传 reward_funcs 列表,支持 rule-based + model-based 混合
# 4. 配置 GRPO
grpo_config = GRPOConfig(
output_dir="./grpo-qwen-math",
num_train_epochs=1,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
# GRPO 特定参数
num_generation_per_prompt=4, # G (group size)
max_prompt_length=512,
max_completion_length=256,
temperature=0.9,
# KL 控制
beta=0.04, # KL penalty coefficient
# PPO-style clipping
epsilon=0.2,
# 学习率
learning_rate=1e-6,
lr_scheduler_type="cosine",
warmup_ratio=0.1,
# 日志
logging_steps=10,
save_steps=500,
bf16=True,
gradient_checkpointing=True,
)
# 5. 启动训练
trainer = GRPOTrainer(
model=model,
tokenizer=tokenizer,
args=grpo_config,
train_dataset=dataset,
reward_funcs=[math_reward],
)
trainer.train()
trainer.save_model("./grpo-qwen-math-final")
Unsloth 对 GRPO 有专门优化,显存和速度都好不少:
from unsloth import FastLanguageModel, is_bfloat16_supported
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="Qwen/Qwen2.5-7B-Instruct",
max_seq_length=2048,
load_in_4bit=True, # QLoRA
fast_inference=True,
)
model = FastLanguageModel.get_peft_model(
model,
r=64,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj",
],
lora_alpha=128,
use_gradient_checkpointing="unsloth",
)
# 后续跟标准 TRL GRPOTrainer 一样使用
单张 A100 80GB,Qwen2.5-7B QLoRA + GRPO(G=4),batch_size=4,episode ~20s,比标准 PPO 快 1.5x。
GRPO 去掉 critic 后,reward 信号的质量变成了唯一指标。很多团队花大量精力调 GRPO 的超参,但 reward function 本身设计有问题。
对于数学推理,DeepSeek-R1 实际用了混合 reward:
final_reward = accuracy_reward × 0.8 + format_reward × 0.2
format_reward 强制模型把思考过程放在 标签里,答案放在 里。这个简单的格式约束极大地稳定了训练。
G(group size per prompt)越大,advantage 估计越准确,但计算量线性增长:
| G | 单步耗时 | MATH 500 提升 |
|---|---|---|
| 2 | 12s | +2.1% |
| 4 | 20s | +5.8% |
| 8 | 38s | +7.2% |
| 16 | 72s | +7.8% |
G=4 是性价比最高的选择。G=8 之后提升递减明显。
GRPO 的 KL 惩罚系数 β 非常敏感。β 太小时 policy 会快速偏离 reference,生成一堆 reward hacking 的样本;太大又基本等于没训练。
一个实用技巧是自适应 β:
def adaptive_beta(current_kl, target_kl=0.01, beta_min=0.01, beta_max=0.5):
"""
Adjust β to keep KL divergence near target.
当 KL 过大时增大 β,过小时减小 β。
"""
if current_kl > target_kl * 2:
return min(beta_max, beta * 2)
elif current_kl < target_kl / 2:
return max(beta_min, beta / 2)
return beta
GRPO 的在线采样在 generation 阶段会产生显存峰值。每次 sample G 个 response,长序列(比如数学推理 1K+ tokens)时 decoder 的 KV cache 占显存很夸张。
解决方案:
max_completion_length 不要设太大,实验中 256-512 足够GRPO 不是银弹。一个实用的决策树:
实际上,DeepSeek-R1 的训练流程是 SFT → 冷启动数据 → RL(GRPO)→ 拒绝采样 → SFT → 全场景 RL,GRPO 只是其中一环。
GRPO 解决了 PPO 的显存问题和 DPO 的离线局限,在代码/数学推理任务上效果显著。但它的核心假设——组内相对比较能替代 critic 的 advantage 估计——在 reward 信号噪声大的场景下可能不成立。
如果你是后端工程师想做模型对齐,我的建议:
DPOTrainer,半天就能出结果)本文是模型评测系列的一篇。前作《GPT-5.6 价格-性能革命》拆解了闭源侧的价格战,《Kimi K3 架构分析》解读了开源侧的 2.8T 巨兽,本篇聚焦 DeepSeek 在 7 月 31 日放出的新变体:DeepSeek V4 Flash 0731。
2026 年 7 月 31 日,Artificial Analysis 发布了 DeepSeek V4 Flash 0731 的独立评测(Reasoning / Max Effort 版本),随后在 Hacker News 上引发热议(500+ 分)。在 GPT-5.6 宣布 Luna 降价 80%、Kimi K3 主打自托管性价比的混战周里,DeepSeek 用一个 284B 总参数的 MoE 模型把「智能/价格」的比值又推高了一截。本文基于 Artificial Analysis 的公开评测数据,拆解这份答卷背后的模型参数、定价结构与工程启示。
2024 年底,DeepSeek 发布了 DeepSeek-V2 和 DeepSeek-R1 系列模型,并选择完全开源。这一举动在全球 AI 社区引发了巨大反响。本文试图从多个维度分析 DeepSeek 开源战略的真正价值。
需要从一份 Word 简历中提取结构化信息(姓名、专业技能、工作年限),用 RAG 方案比直接写正则解析灵活得多。
| 组件 | 选型 |
|---|---|
| 文档加载 | UnstructuredLoader |
| 向量库 | Qdrant (Docker) |
| 嵌入模型 | 通义千问 text-embedding-v1 |
| LLM | DeepSeek v4 Flash |
| 编排框架 | LangChain |
| 运行环境 | Python 3.13 + uv |
rag/
├── main.py # 主流程
├── llm.py # LLM + 向量库封装
├── prompt.py # 本地 prompt 模板
├── sensenova.py # 备用 LLM 客户端
├── pyproject.toml # 依赖管理
└── uv.lock
word = UnstructuredLoader("简历.docx")
docs = word.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=20)
chunks = splitter.split_documents(docs)
UnstructuredLoader 支持 .docx/.pdf/.pptx 等格式,自动提取文本。分片用 50 字符 + 20 字符重叠,对简历这种短文档粒度合适。
embeddings = DashScopeEmbeddings(
dashscope_api_key=os.environ.get("dashscope"),
model="text-embedding-v1",
)
vectordb = QdrantVectorStore.from_documents(
chunks, embeddings,
collection_name="resume",
url="http://localhost:6333",
)
嵌入用通义千问的 text-embedding-v1,向量库用 Qdrant(Docker 运行),collection 名 resume。
chain = {
"context": vec_store.as_retriever() | format_docs,
"question": RunnablePassthrough(),
} | prompt | llm | StrOutputParser()
用 LangChain Expression Language 串联:检索 → 格式化上下文 → 填充 prompt → LLM 推理 → 解析输出。
chain.invoke("请输入姓名. 格式如下\n姓名: ?")
chain.invoke("总结专业技能情况. 格式如下\n专业技能: ?")
chain.invoke("根据工作年份总结工作经验. 格式如下\n工作经验: ?年")
三次调用分别提取姓名、技能、工作年限,prompt 里约束了输出格式。
姓名: XX
专业技能: Go, Python, C/C++, C#, Java, Lua
工作经验: 约16年
hub.pull("rlm/rag-prompt") 默认不允许拉取公开 prompt,需要:
from langsmith import Client as LangSmithClient
client = LangSmithClient()
prompt = client.pull_prompt("rlm/rag-prompt", dangerously_pull_public_prompt=True)
langchain-text-splitters、langchain-community、dashscope 需要在 pyproject.toml 中显式声明。
Qdrant 通过 Docker 启动,确保 localhost:6333 可访问。
import os
from langchain_openai import OpenAIEmbeddings
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_unstructured import UnstructuredLoader
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough
from langsmith import Client as LangSmithClient
from llm import DeepSeek, QdrantVecStoreFromDocs
def clearstr(s: str) -> str:
filter_chars = ['\n', '\r', '\t', '\u3000', ' ']
for char in filter_chars:
s = s.replace(char, '')
return s
def format_docs(docs) -> str:
return "\n\n".join(clearstr(doc.page_content) for doc in docs)
def load_doc():
word = UnstructuredLoader("C:\\Users\\Administrator\\Downloads\\xxx.docx")
docs = word.load()
splitter = RecursiveCharacterTextSplitter(chunk_size=50, chunk_overlap=20)
chunks = splitter.split_documents(docs)
vec_store = QdrantVecStoreFromDocs(chunks, collection_name="resume")
llm = DeepSeek()
client = LangSmithClient()
prompt = client.pull_prompt("rlm/rag-prompt", dangerously_pull_public_prompt=True)
chain = {
"context": vec_store.as_retriever() | format_docs,
"question": RunnablePassthrough(),
} | prompt | llm | StrOutputParser()
ret = chain.invoke("请输入姓名. 格式如下\n姓名: ?")
print(ret)
ret = chain.invoke("总结专业技能情况,内容可能包含golang、AI Agent、python、rag等.格式如下\n专业技能: ?")
print(ret)
ret = chain.invoke("根据各大公司工作过的年份总结工作经验有多少年.格式如下\n工作经验: ?年")
print(ret)
def main():
print("Hello from rag!")
load_doc()
if __name__ == "__main__":
main()
import os
from typing import List, Optional
from langchain_community.embeddings import DashScopeEmbeddings
from langchain_core.documents import Document
from langchain_openai import ChatOpenAI
from langchain_qdrant import QdrantVectorStore
def DeepSeek():
return ChatOpenAI(
model="deepseek-v4-flash",
api_key=os.environ.get("DEEPSEEK_API_KEY"),
base_url="https://api.deepseek.com/v1",
)
def get_embeddings():
return DashScopeEmbeddings(
dashscope_api_key=os.environ.get("dashscope"),
model="text-embedding-v1",
)
def QdrantVecStoreFromDocs(docs: List[Document], collection_name: str,
qdrant_url: Optional[str] = None,
qdrant_api_key: Optional[str] = None):
if qdrant_url is None:
qdrant_url = "http://localhost:6333"
embeddings = get_embeddings()
vectordb = QdrantVectorStore.from_documents(
docs, embeddings, collection_name=collection_name,
url=qdrant_url, api_key=qdrant_api_key,
)
return vectordb
RAGPrompt = """
You are an assistant for question-answering tasks.
Use the following pieces of retrieved context to answer the question.
If you don't know the answer, just say that you don't know,
don't try to make up an answer.
Use three sentences maximum and keep the answer concise.
Question: {question}
Context: {context}
Answer:
"""
[project]
name = "rag"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.13"
dependencies = [
"langchain>=1.3.11",
"langchain-unstructured>=0.1.0",
"langchain-openai>=1.3.3",
"langchain-text-splitters>=0.3.0",
"langchain-community>=0.3.0",
"nltk>=3.9.4",
"python-docx>=1.2.0",
"unstructured>=0.23.1",
"langchain-qdrant>=1.1.0",
"dashscope>=1.0.0",
]
LangChain 的 LCEL 写 RAG pipeline 非常简洁,配合 Docker 运行的 Qdrant 和 DeepSeek 的 API,从文档到结构化输出只要几十行代码。这套方案不仅适用于简历解析,合同审核、知识库 QA 等场景也能直接复用。