AI学习 - 大模型基础入门

从零开始:Ollama 安装 → 本地模型运行 → Python 代码接入 → 理解核心概念


摘要

本文记录了在 Windows 上使用 Ollama 部署本地大模型、并通过 Python 代码接入调用的完整过程。内容涵盖:Ollama 安装与模型拉取、大模型基础概念科普(Token / Context / Context Window / Temperature / Role)、三种调用方式对比,以及多轮对话的完整实现代码。

跟着做完,你能在自己电脑上跑起一个本地大模型,并用代码与它对话。


一、环境准备

1.1 硬件建议

配置 能跑的模型 体验
8GB 显存独显 + 16GB 内存 7b-9b 模型 流畅
无独显 / 核显 3b 以下模型 慢,但能跑
16GB 显存 14b-30b 模型 良好

模型文件大小 ≈ 显存需求。qwen3.5:9b 文件 6.6GB,需要约 7GB 显存。

1.2 安装 Ollama

推荐安装官方版本:

# 方式一:winget 安装(推荐)
winget install Ollama.Ollama

# 方式二:官网下载安装包
# https://ollama.com/download 选 Windows

安装完成后验证:

ollama --version
# 应输出类似:ollama version 0.9.x

二、拉取并运行本地模型

2.1 推荐的入门模型

模型 大小 显存需求 特点
qwen3.5:9b 6.6GB ~7GB 中文友好,速度快,推荐入门
deepseek-r1:7b 4.7GB ~5GB 有思考链,适合观察推理过程
qwen3-coder:30b 18GB ~20GB 代码能力强,需要大显存

2.2 拉取模型

# 拉取(第一次会下载,之后直接用缓存)
ollama pull qwen3.5:9b

# 查看已安装的模型
ollama list

# 查看当前运行中的模型
ollama ps

2.3 命令行直接对话(验证是否正常)

ollama run qwen3.5:9b
# 进入交互模式,直接输入问题
# 输入 /bye 退出

2.4 常见问题

问题:Error: 500 Internal Server Error: memory layout cannot be allocated

排查顺序:

# 1. 查看显存占用
nvidia-smi

# 2. 查看内存占用
Get-CimInstance Win32_OperatingSystem | Select-Object `
  @{N='总内存(GB)';E={[math]::Round($_.TotalVisibleMemorySize/1MB,1)}}, `
  @{N='可用内存(GB)';E={[math]::Round($_.FreePhysicalMemory/1MB,1)}}

# 3. 停止已加载的模型释放显存
ollama stop deepseek-r1:7b

# 4. 版本太旧导致不兼容,尝试升级
winget upgrade Ollama.Ollama

# 5. 模型文件损坏,重新下载
ollama rm qwen3.5:9b
ollama pull qwen3.5:9b

三、大模型基础概念

在写代码之前,先把几个核心概念搞清楚,后面看代码会顺很多。

3.1 Token:模型处理文字的最小单位

模型不认识"字"或"词",只认识 token。可以理解为把文字切成碎片:

"你好北京"  →  ["你", "好", "北", "京"]  →  4 个 token
"hello"     →  ["hello"]                →  1 个 token
"unhappy"   →  ["un", "happy"]          →  2 个 token

为什么要了解 token:

  • API 按 token 计费(调云端模型时)
  • 模型有最大处理量限制(context window)
  • max_tokens 参数控制的就是最多生成多少 token

经验换算:1000 个中文字符 ≈ 500-700 token。


3.2 Context:模型这次推理能看到的全部内容

模型没有记忆,每次调用都是全新的。它能"记住"上下文,是因为你把历史对话都传进去了。

一次 API 调用时,context 包含:

┌─────────────────────────────┐
│  system prompt               │  → 角色设定
│  第1轮用户消息               │  → 历史
│  第1轮模型回复               │  → 历史
│  第2轮用户消息               │  → 历史
│  第2轮模型回复               │  → 历史
│  当前用户消息(最新)         │  → 当前问题
└─────────────────────────────┘
         模型基于以上全部内容生成回复

思考题: 如果对话进行了 100 轮,每次都要把 100 轮历史传进去,token 消耗会越来越大。这就是为什么长对话会越来越慢、越来越贵。


3.3 Context Window:模型一次能处理的 token 上限

Context Window = 128,000 token(以 Claude 为例)

意思是:system prompt + 所有历史对话 + 当前问题
        加起来不能超过 128,000 token

超出的部分模型直接看不到,就像被裁掉了

这也是 RAG(检索增强生成)存在的核心原因:当你有一本 500 页的书,塞不进 context window,就只能先检索最相关的几段塞进去,而不是全文。


3.4 Temperature:控制输出的随机程度

第一步:模型输出的是 logits,不是概率

模型最后一层输出的是原始分数,叫 logits,数值没有限制,可以是任意实数,不能直接当概率用:

# 模型对"下一个词是什么"给出的原始分数
logits = [2.1, 1.8, 1.2, 0.9]
#          "9"  "5"  "3"  "7"
第二步:softmax 把 logits 转成概率
import math

def softmax(logits):
    exp_values = [math.exp(x) for x in logits]  # 对每个值取 e^x
    total = sum(exp_values)
    return [x / total for x in exp_values]       # 归一化,加起来 = 1

softmax([2.1, 1.8, 1.2, 0.9])
# → [0.38, 0.28, 0.16, 0.12]   加起来 = 1.0
第三步:temperature 在 softmax 之前介入

操作很简单,就是把 logits 除以 T,再做 softmax:

def softmax_with_temperature(logits, T):
    scaled = [x / T for x in logits]   # 先除以 temperature
    return softmax(scaled)              # 再转概率
为什么除法能控制尖锐程度

关键在 e^x 这个函数对数值差距极其敏感

# 原始 logits,"9" 和 "5" 差距是 0.3
logits = [2.1, 1.8]

# 直接做 e^x,比值 = 1.35
math.exp(2.1) = 8.17
math.exp(1.8) = 6.05

# ── temperature = 0.1,差距放大 10 倍 ──
scaled = [21.0, 18.0]
math.exp(21.0) = 1,318,815,734
math.exp(18.0) =    65,659,969
# 比值 = 20.1,差距被指数级拉开 → 分布尖锐

# ── temperature = 2.0,差距压缩到一半 ──
scaled = [1.05, 0.90]
math.exp(1.05) = 2.86
math.exp(0.90) = 2.46
# 比值 = 1.16,差距被压缩 → 分布平坦
完整数字对比
原始 logits:[2.1,  1.8,  1.2,  0.9]
token:       "9"   "5"   "3"   "7"

T = 1.0(不变):
  scaled:  [2.1,  1.8,  1.2,  0.9]
  概率:    [38%,  28%,  16%,  12%]   有差距,其他词仍有机会

T = 0.1(极低,差距放大 10 倍):
  scaled:  [21.0, 18.0, 12.0,  9.0]
  概率:    [95%,   4%,  ~0%,  ~0%]   "9" 几乎必然被选,输出稳定

T = 0.5(适中,差距放大 2 倍):
  scaled:  [4.2,  3.6,  2.4,  1.8]
  概率:    [63%,  27%,   7%,   3%]   "9" 概率提升,"5" 还有机会

T = 2.0(极高,差距压缩一半):
  scaled:  [1.05, 0.90, 0.60, 0.45]
  概率:    [30%,  27%,  23%,  20%]   接近均匀,什么都可能选

T → 0(趋近于零):
  → 等价于直接取最大值(greedy decoding)
  → [100%, 0%, 0%, 0%],必然选 "9"
用图形理解
概率分布的"山峰"形状:

temperature 低(0.1):         temperature 高(2.0):
        █                              █ █ █ █
        █                            █ █ █ █ █
        █ █                        █ █ █ █ █ █
        █ █ █ █                  █ █ █ █ █ █ █
   9    5   3   7            9   5   3   7

   尖锐,"9" 一枝独秀           平坦,每个词机会差不多
   → 输出稳定                  → 输出多样
一句话总结
temperature 通过缩放 logits 的数值差距
利用 e^x 对差距极其敏感的特性
在 softmax 之后产生尖锐或平坦的概率分布

T 小 → 差距放大 → e^x 把差距指数级拉开 → 分布尖锐 → 输出稳定
T 大 → 差距压缩 → e^x 的放大效果减弱   → 分布平坦 → 输出多样
T→0  → 差距无限大 → 最高分词概率→100%  → greedy decoding
实际使用建议
场景 temperature 推荐值
分类、判断、提取(需要稳定) 0 - 0.2
问答、总结(平衡) 0.3 - 0.7
写作、头脑风暴(需要创意) 0.7 - 1.0
deepseek-r1 推理模型 0.6(官方推荐)

注意: deepseek-r1 这类推理模型即使 temperature=0,输出也可能不固定。原因是它先生成 <think> 思考链(几百次采样),思考路径的微小差异会影响最终答案的措辞。


3.5 Role:对话里的角色标识

发给模型的消息必须标明是谁说的,三个固定角色:

{"role": "system",    "content": "..."}  # 系统指令,定角色和规则
{"role": "user",      "content": "..."}  # 人类说的话
{"role": "assistant", "content": "..."}  # 模型自己说过的话

这三个值是训练时就固化的,不能随意更改。模型内部会把消息拼接成:

<|system|>你是一个助手
<|user|>我叫张三
<|assistant|>你好张三!
<|user|>我叫什么名字?
<|assistant|>        ← 模型从这里续写

assistant 历史为什么不能省: 没有 assistant 的历史,对话上下文就断了,模型不知道自己之前说过什么。


3.6 API Key:身份验证凭证,不是模型的一部分

云端服务(Claude/GPT):
  请求 → [业务层验证 Key → 计费 → 限速] → 模型推理 → 返回

本地 Ollama:
  请求 → 模型推理 → 返回
  (没有业务层,不需要 Key)

用 openai 库调本地 ollama 时,api_key 填任意非空字符串都行,因为 openai 库在代码层面要求这个字段不能为空,但 ollama 收到请求后直接忽略它。

# 这三种写法效果完全相同
OpenAI(api_key="ollama",      base_url="http://localhost:11500/v1")
OpenAI(api_key="任意字符串",   base_url="http://localhost:11500/v1")
OpenAI(api_key="helloworld",  base_url="http://localhost:11500/v1")

如果要对外提供服务并做鉴权,需要在 ollama 前面加反向代理(如 FastAPI 或 one-api),自己实现 Key 的生成和验证逻辑。


四、三种调用方式

方式对比

anthropic 库 openai 库(调GPT) openai 库(调ollama)
目标模型 Claude GPT 系列 本地模型
需要 Key 是(付费) 是(付费) 不需要
认证 Header X-Api-Key Authorization: Bearer 忽略
额外 Header anthropic-version
费用 按 token 计费 按 token 计费 免费
数据隐私 发到云端 发到云端 本地,不出网

学习阶段推荐方式三(openai 库 + 本地 ollama):不花钱、数据不出网、openai 的接口格式是行业标准,学会后换 GPT/Claude 只改两行代码。


五、完整项目代码

"""
本地大模型接入示例
依赖:pip install openai
运行前确保:ollama 已启动,且已拉取对应模型
"""

import re
from openai import OpenAI

# ==================== 初始化客户端 ====================

client_ollama = OpenAI(
    api_key="ollama",                        # 本地不验证,随便填但不能为空
    base_url="http://localhost:11500/v1"     # 改成你的 ollama 端口
)

DEFAULT_MODEL = "qwen3.5:9b"                 # 默认使用的模型


# ==================== 工具函数 ====================

def parse_think(content: str, show_think: bool = True) -> str:
    """
    解析 deepseek-r1 的思考链输出
    deepseek-r1 会在回答前输出 <think>...</think> 思考过程
    这个函数把思考过程和最终答案分离
    """
    if "<think>" not in content:
        return content

    think_match = re.search(r"<think>(.*?)</think>", content, re.DOTALL)
    answer = re.sub(r"<think>.*?</think>", "", content, flags=re.DOTALL).strip()

    if show_think and think_match:
        print(f"💭 思考过程:\n{'-' * 40}")
        print(think_match.group(1).strip())
        print(f"{'-' * 40}\n")

    return answer


# ==================== 单轮对话 ====================

def ask(
    question: str,
    system: str = "你是一个助手,请用中文回答",
    temperature: float = 0.7,
    model: str = DEFAULT_MODEL,
    show_think: bool = True
) -> str:
    """
    单轮问答
    每次调用都是独立的,不保留上下文

    Args:
        question:    用户问题
        system:      系统提示词,定义模型角色和行为规则
        temperature: 随机程度,0=稳定,1=多样,推理模型建议0.6
        model:       使用的模型名称
        show_think:  是否打印思考过程(deepseek-r1 专用)
    """
    resp = client_ollama.chat.completions.create(
        model=model,
        temperature=temperature,
        messages=[
            {"role": "system", "content": system},
            {"role": "user",   "content": question},
        ]
    )
    content = resp.choices[0].message.content
    return parse_think(content, show_think)


# ==================== 多轮对话 ====================

class ChatSession:
    """
    多轮对话会话
    维护对话历史,实现上下文记忆

    模型本身没有记忆,"记住"上下文的原因是:
    每次调用都把完整的历史对话传给模型(history 列表)
    随着对话轮数增加,传入的 token 也越来越多
    """

    def __init__(
        self,
        system: str = "你是一个助手,请用中文回答",
        model: str = DEFAULT_MODEL,
        temperature: float = 0.7,
        show_think: bool = False
    ):
        self.model = model
        self.temperature = temperature
        self.show_think = show_think
        self.history = [
            {"role": "system", "content": system}
        ]

    def chat(self, user_input: str) -> str:
        """
        发送一条消息,返回模型回复
        自动维护 history,实现多轮对话
        """
        # 把用户消息加入历史
        self.history.append({"role": "user", "content": user_input})

        # 把完整历史传给模型
        resp = client_ollama.chat.completions.create(
            model=self.model,
            temperature=self.temperature,
            messages=self.history      # 关键:传完整历史
        )
        reply = resp.choices[0].message.content
        reply = parse_think(reply, self.show_think)

        # 把模型回复也加入历史(下一轮需要用到)
        self.history.append({"role": "assistant", "content": reply})

        return reply

    def clear(self):
        """清空对话历史,保留 system prompt"""
        system_msg = self.history[0]
        self.history = [system_msg]

    def show_history(self):
        """打印当前对话历史"""
        print("\n📋 当前对话历史:")
        for i, msg in enumerate(self.history):
            role_icon = {"system": "⚙️", "user": "👤", "assistant": "🤖"}.get(msg["role"], "?")
            print(f"{role_icon} [{msg['role']}]: {msg['content'][:80]}...")
        print(f"共 {len(self.history)} 条消息\n")


# ==================== 演示代码 ====================

if __name__ == "__main__":

    # ---------- 演示1:temperature 对比 ----------
    print("=" * 60)
    print("演示1:temperature 对比(使用 qwen3.5:9b)")
    print("=" * 60)

    print("\n--- temperature=0(稳定,每次结果应该相同)---")
    print(ask("随机给我一个1-10之间的数字,只回答数字",
              temperature=0, model="qwen3.5:9b", show_think=False))
    print(ask("随机给我一个1-10之间的数字,只回答数字",
              temperature=0, model="qwen3.5:9b", show_think=False))

    print("\n--- temperature=1(多样,每次结果可能不同)---")
    print(ask("随机给我一个1-10之间的数字,只回答数字",
              temperature=1, model="qwen3.5:9b", show_think=False))
    print(ask("随机给我一个1-10之间的数字,只回答数字",
              temperature=1, model="qwen3.5:9b", show_think=False))

    # ---------- 演示2:system prompt 的效果 ----------
    print("\n" + "=" * 60)
    print("演示2:system prompt 角色限定")
    print("=" * 60)

    print("\n--- 没有角色限定 ---")
    print(ask("帮我写首诗", model="qwen3.5:9b", show_think=False))

    print("\n--- 限定为客服角色 ---")
    print(ask(
        "帮我写首诗",
        system="你是一家叫「星河科技」的公司的客服,只回答产品相关问题,其他问题礼貌拒绝",
        model="qwen3.5:9b",
        show_think=False
    ))

    # ---------- 演示3:deepseek-r1 思考链 ----------
    print("\n" + "=" * 60)
    print("演示3:deepseek-r1 思考链(观察推理过程)")
    print("=" * 60)

    print(ask(
        "9.9 和 9.11 哪个数字更大?",
        model="deepseek-r1:7b",
        temperature=0.6,
        show_think=True
    ))

    # ---------- 演示4:多轮对话记忆 ----------
    print("\n" + "=" * 60)
    print("演示4:多轮对话(验证上下文记忆)")
    print("=" * 60)

    session = ChatSession(
        system="你是一个友好的助手",
        model="qwen3.5:9b",
        show_think=False
    )

    print("👤 我叫张三")
    print(f"🤖 {session.chat('我叫张三')}\n")

    print("👤 我在学习 Python 和大模型开发")
    print(f"🤖 {session.chat('我在学习 Python 和大模型开发')}\n")

    print("👤 我叫什么名字?在学什么?")
    print(f"🤖 {session.chat('我叫什么名字?在学什么?')}\n")

    # 查看传给模型的完整历史
    session.show_history()

    # ---------- 演示5:交互式对话 ----------
    print("\n" + "=" * 60)
    print("演示5:交互式对话(输入 quit 退出)")
    print("=" * 60)

    interactive_session = ChatSession(
        system="你是一个耐心的 AI 学习助手,用简单易懂的方式解释概念",
        model="qwen3.5:9b",
        show_think=False
    )

    while True:
        user_input = input("\n👤 你:").strip()
        if user_input.lower() in ["quit", "exit", "退出", "q"]:
            print("对话结束")
            break
        if not user_input:
            continue
        reply = interactive_session.chat(user_input)
        print(f"🤖 助手:{reply}")

六、思考与延伸

Q:模型越大越好吗?

不一定。9b 的模型在日常问答、写作、代码辅助上已经够用,而且速度更快。30b+ 的模型在复杂推理、长文档理解上有明显优势,但需要更强的硬件。实际项目里先用小模型跑通,再按需升级。

Q:本地模型和云端模型的选择?

本地模型:数据不出网(适合隐私数据)、免费、受硬件限制
云端模型:能力更强、随用随取、按量计费、数据上传云端

学习阶段用本地,生产环境根据数据敏感程度和能力需求决定。

Q:history 一直增长怎么办?

对话很长时,history 会越来越大,超出 context window 后模型就会"失忆"。实际项目里的处理方式:

  • 只保留最近 N 轮对话
  • 把早期对话摘要压缩后放入 system prompt
  • 用向量数据库存历史,按需检索(RAG 思路)

Q:为什么 deepseek-r1 的 temperature=0 仍然不稳定?

因为推理模型先生成 <think> 思考链(几百次采样),GPU 浮点运算的微小误差会在思考链中累积,导致最终答案措辞不同。对于需要稳定输出的场景,推理模型反而不如普通模型合适。


七、参考资料

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐