AI工程师第五课 - 大语言模型基础

学习代码记录仓库

第一课中使用OpenAI调用模型DeepSeek 代码,作为基准测试代码:

py 复制代码
from openai import OpenAI
from dotenv import load_dotenv
import os

# 初始化创建 OpenAI 对象,从.env 中获取 API KEY
load_dotenv(override=True)

api_key = os.getenv("API_KEY")
base_url = os.getenv("BASE_URL")

client = OpenAI(api_key=api_key, base_url=base_url)

def main():
    prompt = "Hi"
    print(call_model([{"role": "user", "content": prompt}]))
        
# 调用大模型
def call_model(messages):
    response = client.chat.completions.create(
        model="deepseek-v4-pro",
        messages=messages,
        stream=False,
        reasoning_effort="high",
        extra_body={"thinking": {"type": "enabled"}}
    )
    
    return response.choices[0].message.content

if __name__ == "__main__":
    main()

GPT 模型原理

GPT(Generative Pre-trained Transformer)就是上一课讲的 Decoder-Only Transformer

一个 GPT 模型从出生到能用,经过三个阶段:

text 复制代码
预训练(Pre-training)  → 微调(Fine-tuning) → 对齐(RLHF)
"学语言"                "学任务"              "学做人"

类比:
上完所有学校的课         学某个专业技能         学会什么该说什么不该说

1. 预训练:学语言

把互联网上的海量文本(维基百科、书籍、代码、论坛)喂给模型,任务很简单------给定前面的词,预测下一个词

text 复制代码
输入:  "今天天气真"
预测:  "好"     ← 模型猜下一个 token

输入:  "今天天气真好,我们出去"
预测:  "玩"     ← 再猜下一个

几万亿个 token 反复猜下来,模型学会了语法、常识、推理、代码......一切人类写下来的东西。但这阶段模型只会"续写",不懂"对话"。

2. 微调:学任务

收集大量"人类觉得好的问答对",教模型:

text 复制代码
训练数据示例:

{"input": "什么是重力?", "output": "重力是地球对物体的吸引力,方向竖直向下......"}
{"input": "写一首关于春天的诗", "output": "春风又绿江南岸......"}

预训练模型学会了续写,微调让它学会了回答问题遵循指令

3. RLHF:学做人

RLHF = Reinforcement Learning from Human Feedback(基于人类反馈的强化学习)。

微调后的模型可能还是会胡说八道、输出有害内容。RLHF 让人类标注员给模型的多个回答打分:

text 复制代码
问:"怎么制作炸弹?"
答1:"我无法提供这类信息......"        ← 好(+1)
答2:"首先你需要......"                ← 差(-1)

模型学会:安全 > 道德 > 有用 > 准确。这就是为什么 ChatGPT 不会教你怎么做坏事。

为什么 Decoder-Only 赢了

上一课讲了 Encoder-DecoderDecoder-Only 两种 Transformer。2023 年后所有主流大模型(GPT-4、Claude、Llama、Qwen、DeepSeek)都是 Decoder-Only。

Encoder-Decoder(BERT、T5) Decoder-Only(GPT 系列)
原理 先理解再生成,两段式 直接自回归生成
擅长 理解、分类、翻译 生成、对话、推理
扩展性 差,加参数成本高 好,Scaling Law 友好
代表 BERT(Google,2018) GPT-4、Claude、Llama

核心原因:Scaling Law。Decoder-Only 模型参数量和数据量翻倍,能力稳定提升。Encoder-Decoder 架构扩展起来没那么顺。

为什么流程一样,模型差距却很大?三个阶段是公开的,但每一步的"用料"天差地别:

  1. 预训练数据 --- 精心清洗的高质量数据 vs 互联网随便爬的噪声数据。垃圾进,垃圾出。
  2. 算力 --- Scaling Law 决定了参数越多、数据越多、能力越强,但这背后是万卡 GPU 集群 vs 几百张卡的差距,训练成本差几个数量级。
  3. 指令微调(SFT) --- 高质量问答对是关键。用博士级标注员写的数据 vs 用 GPT 生成的"二手数据",模型上限完全不同。
  4. RLHF 标注质量 --- 专业团队 + 持续 A/B 迭代 vs 外包标注标准不一。标注员的水平决定模型"品味"的上限。
  5. 训练细节 --- 分词策略、学习率调度、数据配比、过拟合控制、优化器选择......全是踩坑踩出来的经验,论文里不会写全。

需要不断的去试错、去踩坑,才能有更富的经验,实现架构创新、策略创新。

TemperatureTop-p:控制"创造力"

模型预测下一个 token 时,会给每个候选词一个概率。TemperatureTop-p 控制从这些候选词中怎么选。

text 复制代码
模型预测下一个词的概率分布:
"好"  → 60%
"棒"  → 20%
"不错" → 10%
"差"  → 5%
"烂"  → 5%

Temperature = 0(确定性最高):永远选概率最高的 → "好"
Temperature = 1(默认):按原始概率随机选 → 大概率"好",偶尔"棒"
Temperature = 2(疯狂):概率被拉平 → "好"和"烂"被选中的概率差不多
参数 作用 类比
temperature=0 每次输出几乎一样,最确定 前端 Math.floor(),结果固定
temperature=0.7 适度随机,推荐日常使用 有变化但不离谱
temperature=1.5+ 非常随机,可能胡说八道 Math.random() 全开
top_p=0.9 只从累计概率前 90% 的词中选 裁掉长尾低概率词

Temperature 调整概率分布的"陡峭程度",Top-p 则是直接裁剪候选词数量。它从概率最高的词开始累加,累加到 p 就截断,排在后面的词直接丢弃。

text 复制代码
原始概率分布: "好"60%  "棒"20%  "不错"10%  "差"5%  "烂"5%

top_p = 0.8:
  累加:60% + 20% = 80% → 够了,截断
  候选只剩"好"和"棒",后面三个词被丢弃
  → 不管 Temperature 设多高,都选不到"烂"

top_p = 0.5:
  累加:60% → 不够 → +20% = 80% → 超额,截断
  候选还是"好"和"棒"(因为"好"自己不够 50%)

top_p = 1.0:
  不截断,所有词保留

要怎么配置呢:

text 复制代码
Temperature:调整每个候选词的"中奖概率"(改分布形状)
Top-p:      直接淘汰排名靠后的词(改候选数量)

类比:Temperature 是调骰子的重量分布,Top-p 是直接扔掉几个面

一般不要两个都设------OpenAI 建议调 Temperature,Top-p 保持默认 1:

  • 写代码、翻译、JSON 提取 → temperature=0(要确定性)
  • 写文案、讲故事、头脑风暴 → temperature=0.7~1.0(要多样性)
  • 绝大多数场景不要超过 1.0
  • Top-p 通常不动,除非你明确想限制候选词范围(比如只让模型在"常见词"里选)

Prompt Engineering(提示词工程)

Prompt 就是给模型的输入消息。Prompt Engineering 就是设计这条消息让模型输出你想要的结果。

基础原则

1. 说清楚要什么

text 复制代码
❌ "写一个函数"
✅ "写一个 Python 函数,输入一个列表,返回去重后排序的新列表。包含 docstring 和示例。"

2. 给角色设定(System Prompt)

text 复制代码
System: "你是一个资深 Python 后端工程师,擅长写可读性强、有测试的代码。"
User:   "写一个二分查找函数"

模型就会以工程师的口吻回复,而不是助教或文档生成器。

3. 给示例(Few-shot)

text 复制代码
User: "英文 → 中文:
hello → 你好
goodbye → 再见
apple → 苹果
cat → ?"

模型看到前三个例子,就知道你想要的翻译格式,直接输出 "猫"。

进阶模式

Chain-of-Thought(思维链)

让模型先想再答,推理能力显著提升:

text 复制代码
❌ "买 3 个苹果(每个 2 元)和 5 个橘子(每个 3 元),给 50 元,找回多少?"

✅ "买 3 个苹果(每个 2 元)和 5 个橘子(每个 3 元),给 50 元,找回多少?请一步步思考。"

模型输出:
第1步:苹果 3×2 = 6 元
第2步:橘子 5×3 = 15 元
第3步:总共 6+15 = 21 元
第4步:50-21 = 29 元
答案:找回 29 元

对复杂推理(数学、逻辑、编程)效果显著。现代模型(Claude、GPT)内部已融合 CoT 能力,但对小模型加"一步步思考"仍然管用。

ReAct(推理 + 行动)

让模型边想边做,适合需要调用工具的 Agent 场景:

text 复制代码
Thought: 用户想知道今天北京天气,我需要查天气 API
Action: 调用 get_weather(city="北京")
Observation: 北京 26°C,晴
Thought: 信息拿到了,直接回答用户
Answer: 今天北京晴天,26°C

Structured Output / JSON Mode

让 LLM 输出合法 JSON,而不是自由文本。这是 LLM 和前端的 API 契约------前端拿到结构化数据直接渲染,不用正则解析。

修改调用函数传递参数response_format={ 'type': 'json_object' },而且提示词里必须出现json字样。

py 复制代码
# 修改调用参数,增加结构化输出参数
def call_model(messages):
    # 加系统提示词,并且添加 JSON 提示
    system_prompt="All result must be a json object"
    
    messages.insert(0, {"role": "system", "content": system_prompt})

    response = client.chat.completions.create(
        model="deepseek-v4-pro",
        messages=messages,
        stream=False,
        reasoning_effort="high",
        # 结构化输出
        response_format={
            'type': 'json_object'
        },
        extra_body={"thinking": {"type": "enabled"}}
    )
    
    return response.choices[0].message.content

虽然这样可以输出结构化的结果,但是并不知道输出的结果字段、类型是什么。需要去做验证、判断。

字段名本身就是 Prompt

你给 JSON Mode 写输出要求时,不用把所有字段解释一遍------字段名起得好,模型自己就能猜对。

text 复制代码
字段名自解释时,只写字段名就够了:
  ✅ "字段:name, phone, address"                 → 模型知道这是个人信息
  ✅ "字段:severity, line, issue, suggestion"     → 模型知道这是代码审查

字段名模糊时,必须加一句说明:
  ❌ "字段:type, status, level, value"            → type 是什么类型?status 有几个状态?
  ✅ "字段:type(bug | performance | style),status(open | fixed | wontfix)"

修改系统提示词,增加输出字段说明:

py 复制代码
# 加系统提示词,并且添加 JSON 提示
# system_prompt="All result must be a json object"
# 提示词定义结构化输出字段
system_prompt = """
All result must be a json object
{
    "answer": "The answer to the question",
    "question": "The question asked"
}
"""

可以对比输出,输出的结构化字段固定了。经验法则:字段名说不清楚的地方,加说明;吃不准,加一个示例(Few-shot,成本最低)。

更进一步的 Structured Output(结构化输出) 可以精确约束字段和类型------类比前端这层就是 LLM 的 TypeScript 类型定义。

安装pydantic,API调用时需要使用client.chat.completions.parse();解析结果是需要使用.parsed

sh 复制代码
pip install pydantic

定义结构化数据模型:

py 复制代码
from pydantic import BaseModel

class Result(BaseModel):
    answer: str
    question: str  

def call_model(messages):
    response = client.chat.completions.parse(
        model="deepseek-v4-pro",
        messages=messages,
        reasoning_effort="high",
        response_format=Result,
        extra_body={"thinking": {"type": "enabled"}}
    )
    
    return response.choices[0].message.parsed

目前国内模型基本都不支持该模式,只有火山方舟的模型支持。但是还在beta中,不稳定。常用的提示词输出结构就是第二种方式了,定义输出结构字段,并说明输出字段表示什么。

通用问答按照输出展示就行。但是需要对数据进行二次分析汇总,我觉得就需要结构化输出,可预知。程序里可以直接 result.answer,跟调本地函数一样,不需要从自由文本里抠字段。

Prompt 模板化与版本管理

Prompt 和前端代码一样需要版本管理和 A/B 测试。不是"写一次就完了"。

模板化:

py 复制代码
# 不好的做法:每次拼接字符串
prompt = f"你是一个{role},请分析以下文本:{text},按照{format}格式输出"

# 好的做法:用模板引擎
from string import Template

# 提示词模板
prompt_template = Template("""
你是一个 $role,擅长 $skills。

任务:$task

输入:
$input

输出格式要求:
$output_format
""")

# 不同场景传入不同参数
code_review_prompt = prompt_template.substitute(
    role="Python 代码审查专家",
    skills="性能优化、安全审计、代码规范",
    task="审查以下代码,输出问题列表和改进建议",
    input=user_code,
    output_format="JSON,字段:severity, line, issue, suggestion",
)

为什么使用Template而不用f-stringTemplate可以提前定义,使用时再定义参数。而f-string是在执行时的字符串拼接。

版本管理(存 Git + 数据库):

text 复制代码
prompts/
├── code_review/
│   ├── v1.0.md          # 基础版
│   ├── v1.1.md          # 加了安全检查维度
│   └── v2.0.md          # 改用 Structured Output 格式
├── translation/
│   └── v1.0.md

跟前端 feature flag 一样管理 Prompt 变体。不需要装包,就几个函数自己写:

py 复制代码
import os

# 版本配置(改这里就能切版本)
PROMPT_VERSIONS = {
    "code_review": "v2.0",    # 90% 用户走 v1.1,10% 走 v2.0
    "translation": "v1.0",
}

def get_prompt_version(feature_name, user_id=None):
    """根据用户 ID 决定走哪个 Prompt 版本(A/B 测试)"""
    version = PROMPT_VERSIONS.get(feature_name, "v1.0")
    # 10% 用户走新版,90% 走旧版
    if user_id and feature_name == "code_review":
        if hash(user_id) % 10 == 0:
            return "v2.0"
    return version

def load_prompt(path):
    """读取 Prompt 文件"""
    with open(f"prompts/{path}") as f:
        return f.read()

# 使用
prompt = load_prompt(f"code_review/{get_prompt_version('code_review', request.user_id)}.md")

这里可以自定义版本加载逻辑,根据实际业务规则。

Token 与上下文窗口

LLM 不是按"汉字"或"单词"来处理文本的,是按 Token。不同模型的分词器不同,同一个文本切出来的 Token 数可能差不少。

字符和 Token 的参考换算(不同模型有差异):

text 复制代码
1 个英文字符 ≈ 0.3 token       →  1000 英文 ≈ 300 token
1 个中文字符 ≈ 0.6 token       →  1000 中文 ≈ 600 token

中文的 Token 消耗大约是英文的两倍

精确值以 API 返回的 response.usage.prompt_tokens 为准。不同模型实际 Token 数会有偏差。

计费按 Token 算------输入和输出都计费,输出通常比输入贵 3-4 倍。不同模型价格不同,查最新 API 文档。

使用tiktoken来估算费用, tiktoken是 OpenAI 的 Token 计数器,写代码前先学会数 Token,才能控制成本。

安装:

sh 复制代码
pip install tiktoken
py 复制代码
import tiktoken

# 用通用编码器(大多数模型兼容此格式)
enc = tiktoken.get_encoding("cl100k_base")

text = "你好世界"
tokens = enc.encode(text)
print(len(tokens))  

上下文窗口

模型一次能"看到"的 token 上限就叫上下文窗口

text 复制代码
发展历程:
早期模型     → 1K ~ 4K tokens      几段话到几页纸
现在主流     → 128K ~ 200K tokens   一本到两本书的体量
最新模型     → 1M tokens 起步       可以一次塞进整个代码仓库

为什么重要? 窗口越大,一次能喂进去的上下文越多------整个项目代码、长文档、多轮对话历史。超过窗口就会被截断,模型"忘记"之前的内容。

text 复制代码
对话进行中:
第 1 轮:1K tokens
第 5 轮:5K tokens
第 20 轮:20K tokens  ← 早期对话可能被挤出窗口

所以做长对话应用时,超过窗口需要做摘要滑动窗口来管理上下文。

Streaming 流式响应

LLM 生成文本很慢------一个几百字的回答可能要好几秒。如果不做流式,用户盯着空白等死。

text 复制代码
非流式(同步):
前端发请求 → 服务器等 → 模型一个字一个字生成完 → 拼成完整回复 → 一次性返回
用户体验:点击发送 → 白屏 5 秒 → 突然冒出全文

流式(Streaming):
前端发请求 → 服务器每生成几个字就推给前端 → 前端逐个字渲染
用户体验:点击发送 → 文字一个一个往外蹦,像打字

对前端来说,流式就是 Server-Sent Events(SSE) ,跟 WebSocket 类似但更简单------服务器单向推送,浏览器自动重连。

修改参数stream=True,使大模型流式结果响应:

py 复制代码
def main():
    call_model([{"role": "user", "content": "你是谁,详细介绍?"}])
    
# 调用大模型
def call_model(messages):
    # 加系统提示词,并且添加 JSON 提示
    system_prompt="All result must be a json object"
    
    messages.insert(0, {"role": "system", "content": system_prompt})

    response = client.chat.completions.create(
        model="deepseek-v4-pro",
        messages=messages,
        stream=True,
        reasoning_effort="high",
        response_format={"type": "json_object"},
        extra_body={"thinking": {"type": "enabled"}}
    )
    
    result = ""
    for chunk in response:
        content = chunk.choices[0].delta.content
        if content:
            # 打字机效果
            print(content,end="", flush=True)
            result += content
    
    return result

执行后可以看到终端类似打字机输出响应结果。

实现前后端流式响应

创建调用API接口,后端使用FastAPI创建服务和sse_starlette创建sse响应:

安装:

sh 复制代码
pip install fastapi sse_starlette uvicorn

接口服务部分代码,要启动服务,需要安装uvicorn,然后通过uvicorn main:app --reload启动开发服务:

py 复制代码
from fastapi import FastAPI
from sse_starlette.sse import EventSourceResponse

app = FastAPI()

class ChatRequest(BaseModel):
    messages: list[dict]

@app.post("/chat")
async def chat(req: ChatRequest):
    return EventSourceResponse(call_model(req.messages))

修改大模型调用函数call_model,函数体必须为异步函数,返回一个迭代器。

py 复制代码
# 调用大模型
async def call_model(messages):
    # 加系统提示词,并且添加 JSON 提示
    system_prompt="All result must be a json object"
    
    messages.insert(0, {"role": "system", "content": system_prompt})

    response = client.chat.completions.create(
        model="deepseek-v4-pro",
        messages=messages,
        stream=True,
        reasoning_effort="high",
        response_format={"type": "json_object"},
        extra_body={"thinking": {"type": "enabled"}}
    )
    
    for chunk in response:
        content = chunk.choices[0].delta.content
        if content:
            yield {"data": content} 

前端部分通过fetch请求获取sse响应

js 复制代码
const response = await fetch("http://127.0.0.1:8000/chat", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    messages: [{ role: "user", content: "解释一下机器学习" }],
  }),
});

const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";

while (true) {
  const { done, value } = await reader.read();
  if (done) break;

  buffer += decoder.decode(value, { stream: true });
  // SSE 用 \r\n\r\n 分隔每条消息
  const parts = buffer.split("\r\n\r\n");
  buffer = parts.pop();

  for (const part of parts) {
    const line = part.trim();
    if (!line.startsWith("data: ")) continue;
    const text = line.slice(6).trim();
    if (text) {
      document.getElementById("output").textContent += text;
    }
  }
}

httpx 读取流式响应

httpx 是一个 Python 的 HTTP 客户端,可以用来发送 HTTP 请求,并自动处理响应。

安装:

sh 复制代码
pip install httpx

也可以不用OpenAI SDK,直接通过httpx转发调用大模型API接口。

py 复制代码
import httpx

async def stream_api(messages):
    # 加系统提示词,并且添加 JSON 提示
    system_prompt = """
    All result must be a json object
    {
        "answer": "The answer to the question",
        "question": "The question asked"
    }
    """

    messages.insert(0, {"role": "system", "content": system_prompt})

    async with httpx.AsyncClient() as client:
        async with client.stream(
            "POST",
            "https://api.deepseek.com/chat/completions",
            headers={
                "Authorization": f"Bearer {api_key}",
                "Content-Type": "application/json",
            },
            json={
                "model": "deepseek-v4-pro",
                "messages": messages,
                "stream": True,
                "thinking": {"type": "enabled"},
                "reasoning_effort": "high",
            },
        ) as response:
            async for line in response.aiter_lines():
                if line.startswith("data: "):
                    chunk = line[6:]  # 去掉 "data: " 前缀
                    if chunk != "[DONE]":
                        yield chunk

@app.post("/chat")
async def chat(req: ChatRequest):
    # return EventSourceResponse(call_model(req.messages))
    return EventSourceResponse(stream_api(req.messages))

两种方式的区别:

OpenAI SDK 流式 httpx 直接请求
适用场景 只调 OpenAI 协议支持的 调任何 LLM API(自部署、Claude、DeepSeek)
复杂度 低,自动处理 chunk 中,手动解析 SSE 协议
灵活性 高,完全控制请求/响应

做 LLM 应用,流式不是优化项,是基本功能。

模型选型

三类选择

text 复制代码
云端闭源(付钱省心)  → OpenAI / Anthropic / 国内大厂
云端开源(性价比)    → DeepSeek 等
本地部署(数据不出域)→ Ollama / llama.cpp

选型原则不是"哪个模型最好",是"你的场景需要什么"。

  1. 看任务复杂度

    • 简单任务(分类、提取、翻译)→ 便宜模型足够,没必要上最贵的
    • 复杂推理(代码生成、数学、长文档分析)→ 用能力最强的
  2. 看上下文窗口

    • 单轮问答几 KB → 随便选
    • 分析整个项目代码/长文档 → 需要大窗口(128K 起步)
  3. 看数据安全

    • 非敏感数据 → 调 API,省运维
    • 敏感数据/内部代码 → 本地部署,数据不出域
  4. 看成本

    • 每天几千次调用 → 选便宜的,便宜模型做简单任务
    • 偶尔用 → 选最强的,贵一点但省人工调试
  5. 看延迟

    • 实时对话 → 小模型/流式输出
    • 后台批处理 → 大模型慢慢跑

本地部署

不想把数据发云端,就本地跑。两款工具:

Ollama --- 极简、易用

sh 复制代码
brew install ollama
ollama pull qwen2.5:1.7b   # 选当前最新的轻量模型
ollama run qwen2.5:1.7b     # 终端直接对话
  • 好处:离线可用、数据不出域、免费、接口兼容 OpenAI 格式
  • 限制:本地模型能力远不如云端,适合原型开发和敏感数据处理

llama.cpp --- 极致控制

Ollama 底层用的就是 llama.cpp。需要自定义量化、精确控制显存时用。

sh 复制代码
brew install llama.cpp
llama-cli --model model.gguf --prompt "你好" --temp 0.7 --n-predict 200
text 复制代码
Ollama ≈ 用 Vercel 部署    (封装好,一键启动)
llama.cpp ≈ 自己配 Nginx   (裸机操作,精细控制)

日常学习用 Ollama 就够了。

相关推荐
AOwhisky5 小时前
Python 学习笔记(第一期与第二期)——基础语法——核心知识点自测与详解
开发语言·笔记·python·学习·云原生·运维开发
STLearner6 小时前
ICML 2026 | LLM×Graph论文总结[1]【图基础模型,文本属性图,多模态属性图,图对齐,图提示学习,关系深度学习
论文阅读·人工智能·python·深度学习·学习·机器学习·数据挖掘
冬奇Lab6 小时前
MCP 系列(05):Resources 和 Prompts 进阶——动态数据、参数化 URI 与多轮模板
人工智能·llm·mcp
习明然7 小时前
我的本地化AI项目(三)
人工智能·python·electron·c#·avalonia
喜欢的名字被抢了8 小时前
Python实战:SQLAlchemy ORM与FastAPI项目集成
开发语言·python·sql·教程·fastapi
KaneLogger9 小时前
防止 AI Agent 误删文件,最简单的办法就是加一个 Hook
llm·agent·trae
夏季疯9 小时前
读论文:STARS 是什么结构?一个统一的歌声自动标注框架
python
先吃饱再说10 小时前
组合多个远程 MCP Server:让 Agent 同时调用高德地图、Chrome DevTools 和文件系统
langchain·llm·mcp
码流怪侠10 小时前
StreamingBench:首个流式视频理解基准——多模态大模型离人类实时感知还有多远?
llm·github·音视频开发