DeepSeek-R1 发布后,”深度思考”成了大模型圈最火的关键词。本文带你从零搭建一个本地运行的 DeepSeek-R1 推理助手,支持多轮对话、思维链可视化,全程只需 5 步。
为什么选择 DeepSeek-R1?
2025 年初,DeepSeek-R1 的发布在 AI 圈引发了一场地震。不同于传统的大语言模型,R1 系列模型主打 推理能力——它能像人类一样”思考”,在数学证明、代码调试、逻辑推理等复杂任务上表现惊人。
DeepSeek-R1 vs 其他推理模型对比
| 特性 | DeepSeek-R1 | OpenAI o3 | Claude 3.5 | QwQ-32B |
|---|---|---|---|---|
| 开源 | ✅ Apache 2.0 | ❌ 闭源 | ❌ 闭源 | ✅ 开源 |
| 本地部署 | ✅ 支持 | ❌ | ❌ | ✅ 支持 |
| 推理链可视化 | ✅ 完整 | 部分 | ❌ | ✅ 完整 |
| 最低显存要求 | 8GB(量化版) | – | – | 16GB |
| 数学推理能力 | 顶尖 | 顶尖 | 优秀 | 优秀 |
| 中文能力 | 原生优秀 | 优秀 | 优秀 | 原生优秀 |
| API 价格(输入) | ¥1/M tokens | $4/M tokens | $3/M tokens | ¥2/M tokens |
核心优势:开源可本地部署、推理链完全透明、中文原生支持、成本极低。
第一步:环境准备
硬件要求
根据你的显卡选择合适的模型版本:
| 模型版本 | 参数量 | 量化方式 | 最低显存 | 推荐显存 |
|---|---|---|---|---|
| R1-Distill-Qwen-1.5B | 1.5B | Q4_K_M | 4GB | 6GB |
| R1-Distill-Qwen-7B | 7B | Q4_K_M | 8GB | 12GB |
| R1-Distill-Qwen-14B | 14B | Q4_K_M | 12GB | 16GB |
| R1-Distill-Qwen-32B | 32B | Q4_K_M | 20GB | 24GB |
| R1-Distill-Qwen-70B | 70B | Q4_K_M | 40GB | 48GB |
💡 没有显卡也能跑!CPU 推理虽然慢一些,但 7B 模型在 16GB 内存的机器上完全可行。
安装 Ollama(最简单的部署方式)
# Linux / macOS(一键安装)
curl -fsSL https://ollama.com/install.sh | sh
# Windows:去 https://ollama.com/download 下载安装包
# 安装完成后,Ollama 会自动以系统服务方式运行
安装完成后验证:
ollama --version
# 输出:ollama version is 0.x.x
第二步:下载 DeepSeek-R1 模型
使用 Ollama 一键拉取模型:
# 推荐大多数人使用 7B 版本(平衡性能与速度)
ollama pull deepseek-r1:7b
# 如果显卡较好(RTX 4090 / A100),可以用 14B
ollama pull deepseek-r1:14b
# 轻量级机器(集成显卡 / 8G 显存),用 1.5B
ollama pull deepseek-r1:1.5b
验证模型加载:
ollama run deepseek-r1:7b "你好,请介绍一下自己"
你会看到模型在回答前会先展示一段 <think >...</think > 的思维链过程——这就是 R1 的核心特征。
第三步:用 Python 构建 API 服务
接下来,我们用 FastAPI 包装一个本地推理服务,支持多轮对话和思维链提取。
"""
DeepSeek-R1 本地推理 API 服务
功能:多轮对话、思维链可视化、流式输出
"""
import json
import time
from typing import AsyncGenerator
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
import httpx
app = FastAPI(title="DeepSeek-R1 本地推理服务")
# 跨域配置
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
# Ollama 本地 API 地址
OLLAMA_BASE_URL = "http://localhost:11434"
MODEL_NAME = "deepseek-r1:7b"
# 对话历史存储(简单实现,生产环境建议用 Redis)
chat_histories: dict[str, list[dict]] = {}
class ChatRequest(BaseModel):
"""聊天请求"""
message: str
session_id: str = "default"
temperature: float = 0.6
max_tokens: int = 4096
class ChatResponse(BaseModel):
"""聊天响应"""
thinking: str # 思维链内容
answer: str # 最终回答
duration: float # 耗时(秒)
model: str # 模型名称
def extract_thinking_and_answer(raw_text: str) -> tuple[str, str]:
"""
从原始输出中提取思维链和最终回答
DeepSeek-R1 的输出格式:<think >思维过程</think >最终回答
"""
thinking = ""
answer = raw_text
# 提取 <think >...</think > 中的内容
if "<think >" in raw_text:
parts = raw_text.split("</think >", 1)
think_part = parts[0]
if "<think >" in think_part:
thinking = think_part.split("<think >", 1)[1].strip()
answer = parts[1].strip() if len(parts) > 1 else raw_text
# 兼容没有闭合标签的情况
elif raw_text.startswith("<think >"):
thinking = raw_text[7:].strip()
answer = ""
return thinking, answer
@app.post("/api/chat", response_model=ChatResponse)
async def chat(request: ChatRequest):
"""普通聊天接口(非流式)"""
# 获取或创建对话历史
if request.session_id not in chat_histories:
chat_histories[request.session_id] = []
history = chat_histories[request.session_id]
# 构建消息列表
messages = history + [{"role": "user", "content": request.message}]
start_time = time.time()
try:
async with httpx.AsyncClient(timeout=300.0) as client:
response = await client.post(
f"{OLLAMA_BASE_URL}/api/chat",
json={
"model": MODEL_NAME,
"messages": messages,
"stream": False,
"options": {
"temperature": request.temperature,
"num_predict": request.max_tokens,
}
}
)
response.raise_for_status()
result = response.json()
except httpx.ConnectError:
raise HTTPException(
status_code=503,
detail="无法连接到 Ollama 服务,请确保 Ollama 正在运行"
)
raw_text = result.get("message", {}).get("content", "")
thinking, answer = extract_thinking_and_answer(raw_text)
duration = time.time() - start_time
# 更新对话历史(只保存最终回答,不保存思维链,节省上下文空间)
history.append({"role": "user", "content": request.message})
history.append({"role": "assistant", "content": answer})
# 限制历史长度(防止显存溢出)
if len(history) > 20:
history[:] = history[-20:]
return ChatResponse(
thinking=thinking,
answer=answer,
duration=duration,
model=result.get("model", MODEL_NAME)
)
@app.post("/api/chat/stream")
async def chat_stream(request: ChatRequest):
"""流式聊天接口(SSE)"""
if request.session_id not in chat_histories:
chat_histories[request.session_id] = []
history = chat_histories[request.session_id]
messages = history + [{"role": "user", "content": request.message}]
async def generate() -> AsyncGenerator[str, None]:
full_text = ""
try:
async with httpx.AsyncClient(timeout=300.0) as client:
async with client.stream(
"POST",
f"{OLLAMA_BASE_URL}/api/chat",
json={
"model": MODEL_NAME,
"messages": messages,
"stream": True,
"options": {
"temperature": request.temperature,
"num_predict": request.max_tokens,
}
}
) as response:
response.raise_for_status()
async for line in response.aiter_lines():
if line.strip():
chunk = json.loads(line)
content = chunk.get("message", {}).get("content", "")
full_text += content
# 发送 SSE 格式数据
yield f"data: {json.dumps({'content': content})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'error': str(e)})}\n\n"
# 流结束后更新历史
thinking, answer = extract_thinking_and_answer(full_text)
history.append({"role": "user", "content": request.message})
history.append({"role": "assistant", "content": answer})
if len(history) > 20:
history[:] = history[-20:]
yield "data: [DONE]\n\n"
return StreamingResponse(generate(), media_type="text/event-stream")
@app.get("/api/sessions/{session_id}/clear")
async def clear_session(session_id: str):
"""清空指定会话历史"""
if session_id in chat_histories:
del chat_histories[session_id]
return {"status": "ok", "message": f"会话 {session_id} 已清空"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
安装依赖:
pip install fastapi uvicorn httpx pydantic
启动服务:
python deepseek_r1_server.py
测试接口:
curl -X POST http://localhost:8000/api/chat \
-H "Content-Type: application/json" \
-d '{"message": "请证明根号2是无理数", "session_id": "test"}'
第四步:构建 Web 聊天界面
用一个简单的 HTML 页面即可实现思维链可视化的聊天界面:
"""
DeepSeek-R1 Web 聊天界面
直接运行后访问 http://localhost:8001
"""
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
app = FastAPI()
HTML_PAGE = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>DeepSeek-R1 深度思考助手</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; background: #f5f5f5; }
.container { max-width: 900px; margin: 0 auto; padding: 20px; }
h1 { text-align: center; color: #333; margin-bottom: 20px; font-size: 24px; }
.chat-box {
background: white; border-radius: 12px;
box-shadow: 0 2px 12px rgba(0,0,0,0.1);
height: 500px; overflow-y: auto; padding: 20px; margin-bottom: 20px;
}
.message { margin-bottom: 16px; }
.user-msg {
background: #1890ff; color: white; padding: 12px 16px;
border-radius: 12px 12px 0 12px; max-width: 70%; margin-left: auto;
}
.thinking-box {
background: #fffbe6; border-left: 3px solid #faad14;
padding: 12px 16px; border-radius: 8px; margin-bottom: 8px;
font-size: 13px; color: #666; cursor: pointer;
}
.thinking-box:hover { background: #fff7cc; }
.thinking-content {
max-height: 0; overflow: hidden; transition: max-height 0.3s;
white-space: pre-wrap;
}
.thinking-box.expanded .thinking-content { max-height: 2000px; }
.thinking-label { font-weight: bold; color: #faad14; }
.answer-box {
background: #f6f8fa; padding: 12px 16px;
border-radius: 0 12px 12px 12px; max-width: 85%;
white-space: pre-wrap; line-height: 1.6;
}
.input-area { display: flex; gap: 12px; }
.input-area textarea {
flex: 1; padding: 12px 16px; border: 2px solid #e8e8e8;
border-radius: 12px; font-size: 14px; resize: none;
height: 50px; outline: none; transition: border-color 0.3s;
}
.input-area textarea:focus { border-color: #1890ff; }
.input-area button {
background: #1890ff; color: white; border: none;
padding: 12px 24px; border-radius: 12px; cursor: pointer;
font-size: 14px; white-space: nowrap;
}
.input-area button:hover { background: #40a9ff; }
.input-area button:disabled { background: #d9d9d9; cursor: not-allowed; }
.meta { text-align: center; color: #999; font-size: 12px; margin-top: 8px; }
.loading { display: inline-block; animation: spin 1s linear infinite; }
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="container">
<h1>DeepSeek-R1 深度思考助手</h1>
<div class="chat-box" id="chatBox"></div>
<div class="input-area">
<textarea id="userInput" placeholder="输入你的问题..." onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();send()}"></textarea>
<button id="sendBtn" onclick="send()">发送</button>
</div>
<div class="meta">本地运行 · 数据不离开你的电脑 · DeepSeek-R1</div>
</div>
<script>
const chatBox = document.getElementById('chatBox');
const userInput = document.getElementById('userInput');
const sendBtn = document.getElementById('sendBtn');
const API_URL = 'http://localhost:8000/api/chat';
const sessionId = 'web_' + Date.now();
async function send() {
const message = userInput.value.trim();
if (!message) return;
userInput.value = '';
sendBtn.disabled = true;
// 显示用户消息
addMessage('user', message);
try {
const resp = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message, session_id: sessionId })
});
const data = await resp.json();
// 显示思维链(可折叠)
if (data.thinking) {
addThinking(data.thinking);
}
// 显示回答
addMessage('assistant', data.answer, data.duration);
} catch (e) {
addMessage('error', '请求失败: ' + e.message);
}
sendBtn.disabled = false;
}
function addMessage(role, content, duration) {
const div = document.createElement('div');
div.className = 'message';
if (role === 'user') {
div.innerHTML = '<div class="user-msg">' + escapeHtml(content) + '</div>';
} else if (role === 'error') {
div.innerHTML = '<div style="color:red;padding:8px">' + escapeHtml(content) + '</div>';
} else {
div.innerHTML = '<div class="answer-box">' + escapeHtml(content) +
(duration ? '</div><div class="meta">耗时 ' + duration.toFixed(1) + 's</div>' : '</div>');
}
chatBox.appendChild(div);
chatBox.scrollTop = chatBox.scrollHeight;
}
function addThinking(content) {
const div = document.createElement('div');
div.className = 'message';
div.innerHTML =
'<div class="thinking-box" onclick="this.classList.toggle(\'expanded\')">' +
'<span class="thinking-label">💭 思考过程(点击展开)</span>' +
'<div class="thinking-content">' + escapeHtml(content) + '</div></div>';
chatBox.appendChild(div);
chatBox.scrollTop = chatBox.scrollHeight;
}
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
</script>
</body>
</html>
"""
@app.get("/", response_class=HTMLResponse)
async def index():
return HTML_PAGE
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8001)
第五步:测试与优化
性能测试脚本
"""
DeepSeek-R1 推理性能测试
"""
import time
import httpx
import asyncio
OLLAMA_URL = "http://localhost:11434"
MODEL = "deepseek-r1:7b"
TEST_QUESTIONS = [
"1+1等于几?",
"用Python写一个快速排序",
"解释相对论的核心概念",
"请证明:对于任意正整数n,n³ - n 一定能被6整除",
]
async def test_question(client: httpx.AsyncClient, question: str) -> dict:
"""测试单个问题"""
start = time.time()
response = await client.post(
f"{OLLAMA_URL}/api/chat",
json={"model": MODEL, "messages": [{"role": "user", "content": question}], "stream": False}
)
elapsed = time.time() - start
result = response.json()
content = result.get("message", {}).get("content", "")
# 统计字数
total_chars = len(content)
# 提取思维链和回答
think_start = content.find("<think >")
think_end = content.find("</think >")
think_chars = think_end - think_start - 7 if think_start != -1 and think_end != -1 else 0
return {
"question": question[:20] + "...",
"total_chars": total_chars,
"thinking_chars": think_chars,
"time_seconds": round(elapsed, 2),
"chars_per_sec": round(total_chars / elapsed, 1),
}
async def run_benchmark():
"""运行完整测试"""
print(f"DeepSeek-R1 推理性能测试")
print(f"模型:{MODEL}")
print("=" * 70)
async with httpx.AsyncClient(timeout=300.0) as client:
results = []
for q in TEST_QUESTIONS:
result = await test_question(client, q)
results.append(result)
print(f"问题:{result['question']}")
print(f" 总字数:{result['total_chars']} 思考过程:{result['thinking_chars']}字")
print(f" 耗时:{result['time_seconds']}s 速度:{result['chars_per_sec']} 字/秒")
print()
# 汇总
avg_time = sum(r['time_seconds'] for r in results) / len(results)
avg_speed = sum(r['chars_per_sec'] for r in results) / len(results)
print("=" * 70)
print(f"平均耗时:{avg_time:.2f}s 平均速度:{avg_speed:.1f} 字/秒")
if __name__ == "__main__":
asyncio.run(run_benchmark())
性能优化技巧
| 优化手段 | 效果 | 操作方式 |
|---|---|---|
| 使用 GGUF Q4_K_M 量化 | 速度提升 2-3x,质量损失 < 3% | 默认已量化 |
| 开启 GPU 加速 | 速度提升 5-10x | Ollama 自动检测 GPU |
| 调低 temperature | 减少推理时间 | 设置 temperature=0.3 |
| 限制 num_predict | 避免超长输出 | 设置 max_tokens=2048 |
| 使用 num_ctx 控制上下文 | 降低显存占用 | 根据需要设置 |
# 指定 GPU 和上下文长度运行
ollama run deepseek-r1:7b --num-gpu-layers 99 --num-ctx 2048
生产部署建议
如果你想把推理服务部署到云服务器上对外提供服务:
| 配置项 | 最低配置 | 推荐配置 |
|---|---|---|
| CPU | 4 核 | 8 核+ |
| 内存 | 16GB | 32GB+ |
| GPU | NVIDIA T4 16GB | NVIDIA A10 24GB |
| 系统 | Ubuntu 22.04 | Ubuntu 22.04 |
| 模型 | 7B Q4_K_M | 14B Q4_K_M |
# 在云服务器上设置 Ollama 监听所有网卡
export OLLAMA_HOST=0.0.0.0:11434
# 设置模型并行(多 GPU)
ollama run deepseek-r1:14b --num-gpu 2
总结
DeepSeek-R1 让本地部署”会思考”的 AI 成为现实。相比依赖云端 API,本地部署有三个核心优势:
- 数据安全:敏感数据不离开你的服务器
- 零调用成本:一次部署,无限使用
- 思维透明:完整的推理过程可审计、可优化
从 Ollama 一键安装到 FastAPI 服务封装,再到 Web 界面搭建,整个过程不超过 30 分钟。如果你在云服务器方面有需求,欢迎联系我(腾讯云/阿里云/火山云都有优惠),帮你选到性价比最高的配置。
👤 作者简介
一枚在大中原腹地(河南)卖公有云的从业者,主营腾讯云/阿里云/火山云,曾踩坑无数,现专注AI大模型应用落地。关注公众号「公有云cloud」,围观AI前沿动态~
博客:y1cloud.cn
