LLM大模型基础知识点

LLM大模型基础知识点

_

一,技术迭代背景

传统编码时代 :规则驱动

这是最早期的方法。想让计算机处理语言,就由语言学家和工程师手工编写大量规则,形成专家系统。

  • 特点:如果输入 A,就执行 B
  • 局限:语言是无穷无尽的,充满歧义和例外。规则的维护成本极高,根本无法覆盖真实世界的千变万化,很快达到了天花板。

NLP 时代: 让机器理解语言;这标志着从“规则驱动”向“数据驱动”的转变,分为三个关键阶段:

  1. 统计NLP:核心思想是不再死记规则,而是用概率建模。
    • 语言模型初现:一个句子是否合理,用概率 P(今天天气真好) > P(今天天气很坏) 来判断。

    • 核心瓶颈:严重依赖人工设计的“特征工程”,且无法真正理解语义。比如“苹果很好吃”和“苹果很好用”,对统计模型来说,“苹果”只是一个词。

  2. 词嵌入:词的数字化革命;这是里程碑式的突破,解决了**“如何让计算机理解词义”**的问题。
    • 核心思想:用一个稠密的实数向量(比如 [0.2, -0.5, 0.8, ...])来表示一个词。

    • 神奇效果:语义相近的词,其向量在空间中的距离也近。它甚至能学到语义关系:国王 - 男人 + 女人 ≈ 女王

    • 代表模型:Word2Vec、GloVe。

    • 巨大局限:给每个词一个固定的向量,无法处理一词多义。同一个“苹果”,在水果和手机两个语境下,向量完全相同。

  3. 预训练语言模型:一词多义的终结;这个阶段开始训练深层模型,并让词义随上下文动态改变。
  • 代表模型:ELMo, GPT-1, BERT。

  • 核心突破:不再是“查字典”式地找一个固定向量,而是让模型读完整个句子后,为当前词动态生成一个向量。至此,水果“苹果”和科技“苹果”终于有了不同的表示。

  • 关键范式:“预训练 + 微调”。先在海量数据上训练一个通用模型,再针对具体任务(如情感分析)进行小范围调整,效果远超从前。


神经网络时代:Transformer 一统天下

  • 革命性架构:Transformer 的自注意力机制,让模型处理一个词时,能直接看到并权衡句子中所有词与它的关系,高效解决长距离依赖。
  • 并行计算能力:相比RNN(循环神经网络)必须按顺序一步步处理,Transformer能同时处理整个序列,使得在GPU上训练海量数据成为可能。

大模型时代 : 量变到质变;当 Transformer 架构与不断堆叠的规模(参数、数据、算力)相遇,涌现能力发生了。

  • GPT-3 的飞跃:它证明了,当模型大到一定程度,不需要任何微调,只需在提示词里给它几个例子(上下文学习),它就能完成翻译、写作等全新任务。这与第二阶段“每个任务都要精调一个模型”的范式有了本质不同。
  • ChatGPT 的质变:它在上一步基础上,通过指令微调学会了“理解指令”,通过人类反馈强化学习让回答更符合人类偏好。这让模型从“能力强大的文字接龙工具”,进化成了“与人类价值观对齐的实用助手”

二,工作原理

2.1 大模型的核心任务:预测下一个词

本质就是一个猜词游戏

如果用一句话概括大模型在做什么,那就是:给它一串文字,它来猜下一个最可能出现的词是什么

举个例子,输入 "今天的天气真",大模型的任务是猜下一个词。它内部会计算所有候选词的概率:

"好"    → 概率 62%
"不错"  → 概率 18%
"差"    → 概率 8%
"热"    → 概率 5%
"糟糕"  → 概率 3%
其他    → 概率 4%

然后它会根据这些概率选择一个词输出。选完之后,"今天的天气真好" 变成新的输入,它再猜下一个词……如此循环,直到生成完整的句子或达到设定的长度上限。


为什么预测下一个词这么有用

你可能觉得奇怪:只是预测下一个词,怎么就能回答复杂的问题?关键在于:要准确预测下一个词,模型必须理解上下文的含义

比如你问:"中国的首都是哪个城市?答案是"

要预测 "答案是" 后面的词,模型必须:

  1. 理解这是一个问答格式
  2. 理解问题问的是"中国的首都"
  3. 知道中国的首都是北京
  4. 理解应该输出城市名称作为答案

只有真正 "理解"了这些,模型才能预测出" 北京 "这个词。如果模型不理解问题,它可能会输出" 一个 "、" 在 " 这种语法上说得通但语义上答非所问的词。

所以,预测下一个词只是表面任务,理解语言才是底层能力。正是因为要做好预测,模型被迫学会了理解。


生成过程的完整示例

让我们完整走一遍大模型生成回答的过程。

输入 Prompt

用户:请用一句话介绍一下 Python 这门编程语言。
助手:

生成过程

第 1 步:模型看到上下文,预测 "助手:" 后面的第一个词

  • 候选词概率:"Python"(35%)、"它"(25%)、"这"(15%)、"一"(10%)...
  • 选择:"Python"

第 2 步:上下文变成 "... 助手:Python",预测下一个词

  • 候选词概率:"是"(60%)、"这"(15%)、"作为"(10%)...
  • 选择:"是"

第 3 步:上下文变成 "... 助手:Python 是",预测下一个词

  • 候选词概率:"一"(40%)、"一种"(30%)、"一门"(20%)...
  • 选择:"一门"

... 继续这个过程 ...

最终输出

Python是一门简洁易读、功能强大的高级编程语言,广泛应用于Web开发、数据分析、人工智能等领域。

整个过程就是一个词一个词地生成,每一步都是在做 "预测下一个词" 这个任务。但最终的输出是连贯、有意义的句子。


这不是搜索,是生成

很多人有个误解:以为大模型回答问题时,是去训练数据里 "搜索" 答案。

这是错误的理解。

大模型生成回答时,不是在做检索,而是在做生成。它根据学到的语言规律,一个词一个词地 "创造" 回答。这些回答可能和训练数据中的某些句子相似,但绝不是复制粘贴。

这就是为什么:

  • 同样的问题,问两次可能得到不同的回答(因为选词有随机性)
  • 模型可以回答训练数据中从未出现过的问题(因为它学会了举一反三)
  • 模型有时候会"胡说八道"(因为它只是在预测"看起来合理"的词,不保证事实正确)

2.2 它是怎么学会预测的

训练的本质:完形填空

大模型是怎么学会预测下一个词的?答案是通过海量的 "完形填空" 练习。

训练过程大致是这样的:

第 1 步:准备数据

从互联网上收集海量文本:书籍、网页、代码、论文、对话……

第 2 步:制造训练样本

从文本中截取片段,遮住一部分,让模型预测被遮住的内容。

原文:人工智能正在改变我们的生活方式
训练样本:人工智能正在____我们的生活方式
正确答案:改变

第 3 步:训练和调整

模型一开始是 "瞎猜" 的,预测结果大概率是错的。但每次猜完,它会收到反馈:"正确答案是 xxx,你猜的 yyy 偏差了多少"。

根据这个反馈,模型调整自己内部的参数(那几百亿个数字),让下次遇到类似情况时猜得更准一点。

第 4 步:重复亿万次

这个过程在超级计算机上,用海量的文本片段,重复进行数十亿甚至上万亿次。每次重复,模型的预测能力都会提升一点点。

经过这样的训练,模型逐渐学会了:

  • 在"今天天气"后面,"真好"出现的概率比"汽车"高
  • 在"def function():"后面,应该是 Python 代码
  • 在"苹果公司"后面,更可能是科技相关的词而不是水果相关的词

它不是记住了每个具体的句子,而是学会了语言的统计规律


一个简化的数学视角

如果你想从数学角度理解,训练的目标可以表示为:

最大化 P(下一个词 | 上下文)

也就是说,给定上下文(前面的词),最大化模型预测出正确的下一个词的概率。模型的参数(几百亿个数字)就是通过不断调整,来让这个概率尽可能高。训练完成后,这些参数就固定下来,代表了模型学到的所有 "知识"。

2.3 Transformer架构

2.3.1 背景

2017 年,Google 发表了一篇划时代的论文《Attention Is All You Need》(注意力就是你所需要的一切),提出了 Transformer 架构。这篇论文彻底改变了自然语言处理的格局。

在 Transformer 之前,处理语言主要用 RNN(循环神经网络)。RNN 的工作方式是一个词一个词地按顺序处理,就像人阅读一样从左到右。

这种方式有两个大问题:

  • 问题 1:容易 "健忘"

    想象你在读一本很长的书。读到第 500 页时,你还能清晰记得第 1 页的内容吗?大概率记不清了。

    RNN 也有这个问题。处理长文本时,开头的信息会逐渐被 "遗忘"。这在语言理解中是致命的——有时候理解句子后半部分的意思,必须要知道前面说了什么。

  • 问题 2:没法并行

    因为必须按顺序一个词一个词处理,RNN 无法利用现代 GPU 的并行计算能力。这导致训练速度很慢,很难扩展到大规模。

    Transformer 用一种全新的思路解决了这两个问题。

2.3.2 Transformer 的核心思想

Transformer 的核心思想可以概括为:不按顺序读,而是同时看所有词,然后计算每个词和其他词之间的关系

打个比方来说明两种方式的区别:

  • RNN 方式(像流水线工人):

    想象你是一个流水线工人,产品一个一个传过来。你处理完当前这个,才能处理下一个。处理到后面的产品时,你只能靠记忆想起前面的产品是什么样的。记忆力好的时候还行,产品多了就容易记混。

  • Transformer 方式(像会议协调员):

    想象你是一个会议协调员,所有参会者同时在你面前。你可以随时看任何一个人,可以同时比较任意两个人的观点,可以直接建立任意两个人之间的联系。不用按顺序,不用靠记忆。

    这个 "同时看所有词、建立任意词之间联系" 的能力,就来自于 Transformer 的核心机制——自注意力(Self-Attention)

2.3.3 自注意力机制

自注意力机制:找出谁和谁有关系

什么是注意力

在深入自注意力之前,先理解 "注意力" 这个概念。

想象你在一个嘈杂的酒吧里,周围有几十个人在聊天。虽然所有声音都传到你的耳朵里,但你能 "专注" 于和你聊天的朋友的声音,把其他声音当作背景噪音。

这就是注意力:在众多信息中,有选择地关注最重要的部分

在语言处理中,注意力机制让模型能够在处理某个词时,有选择地 "关注" 句子中最相关的其他词。

自注意力做了什么

自注意力(Self-Attention)的 "自" 是指句子对自己的注意力——句子中的每个词,都去 "关注" 句子中的其他词。

具体来说,对于句子中的每一个词,自注意力机制会回答这个问题:

"在当前的上下文中,句子里的其他词对理解这个词有多重要?"

然后给每个词打一个 "重要性分数"。分数高的词,对理解当前词的影响更大。

举例 :假设我们有这样一个句子:

"小明把作业交给了老师,因为他要求必须今天交。"

当模型处理 "他" 这个词时,需要弄清楚 "他" 指的是谁——是 "小明" 还是 "老师"?

自注意力机制会计算 "他" 和句子中每个词的关联分数:

"他" 与各词的关联分数(示意):
小明  → 0.15
把    → 0.02
作业  → 0.08
交给了 → 0.05
老师  → 0.35  ← 最高!
因为  → 0.03
他    → 0.10
要求  → 0.12
必须  → 0.04
今天  → 0.03
交    → 0.03

可以看到,"老师" 的分数最高。这是因为在训练数据中,模型学到了 "要求必须 xxx" 这种表达通常是上级或权威人物说的话,而在学生和老师的关系中,老师更可能是 "要求" 的发出者。

所以模型能判断出 "他" 指的是 "老师",而不是 "小明"。

2.3.4 自注意力的计算过程

从技术角度,自注意力的计算涉及三个关键概念:Query(查询)Key(键)Value(值)

可以用一个图书馆查书的比喻来理解:

  • Query(查询):你想找什么?——你的搜索关键词
  • Key(键):每本书的标签——用来匹配搜索
  • Value(值):每本书的内容——真正有用的信息

在自注意力中:

  1. 每个词都会生成自己的 Q、K、V 三个向量
  2. 某个词的 Q 会和所有词的 K 做匹配,计算相关性分数
  3. 根据分数对所有词的 V 做加权求和,得到最终的输出

用公式表示(简化版):

注意力分数 = softmax(Q × K的转置 / √d)
输出 = 注意力分数 × V

其中 √d 是一个缩放因子,防止数值过大。softmax 把分数转换成概率(总和为 1)。


为什么自注意力这么强大

自注意力有几个关键优势:

  1. 能处理长距离依赖

    在 RNN 中,如果两个相关的词隔得很远,中间的信息会逐渐丢失。但在自注意力中,任意两个词之间都有直接的连接,不管它们隔多远。

    比如:"小明昨天在超市买了一个苹果,回家后他把苹果洗干净吃了。"

    最后的 "苹果" 要和开头的 "苹果" 联系起来,在 RNN 中可能因为距离太远而丢失关联。但在自注意力中,它们可以直接建立联系。

  2. 可以并行计算

    因为是同时处理所有词,不需要按顺序,所以可以充分利用 GPU 的并行能力。这让训练大规模模型成为可能。

  3. 关系建模灵活

    每个词都会和所有词建立联系,这种灵活性让模型能学到各种复杂的语言模式。

2.3.5 多头注意力:从不同角度看问题

在实际的 Transformer 中,不是只做一次自注意力,而是同时做多次,这叫多头注意力(Multi-Head Attention)

为什么需要多头

词和词之间的关系是多维度的。一次注意力可能只能捕捉到一种关系,但语言的复杂性需要同时捕捉多种关系。

比如这个句子:"程序员小王用 Python 写了一个爬虫程序。"

从不同角度看,词之间有不同的关系:

  • 语法角度:"小王"是主语,"写"是谓语,"程序"是宾语
  • 职业角度:"程序员"和"Python"、"爬虫程序"相关
  • 工具角度:"用"连接了"Python"和"写"
  • 指代角度:如果后文有"他",应该指"小王"

多头注意力就是让模型同时从多个角度分析词之间的关系。每个 "头" 专注于一种关系,最后把所有头的结果综合起来。

具体是怎么做的

假设我们用 8 个头(这是常见的设置):

  1. 把原始的 Q、K、V 向量分成 8 份
  2. 每份独立做一次自注意力计算
  3. 把 8 个头的输出拼接起来
  4. 通过一个线性变换,得到最终输出

多头的实际效果

研究者发现,不同的头确实学会了关注不同的模式:

  • 有的头关注语法结构(主谓宾关系)
  • 有的头关注位置关系(相邻的词)
  • 有的头关注语义相似性(同类词)
  • 有的头关注指代关系(代词指向)

这种分工让模型的理解更加全面和深入。

2.3.6 位置编码:告诉模型词的顺序

为什么需要位置信息

自注意力机制有一个问题:它只看词和词的关系,不考虑词的位置。

对于它来说,"猫追老鼠" 和 "老鼠追猫" 可能没有区别——都是 "猫"、"追"、"老鼠" 三个词。但这两句话的意思完全不同!

词的顺序对语言理解至关重要,所以 Transformer 需要一种方式来记录位置信息。

位置编码是怎么做的

Transformer 的解决方案是位置编码(Positional Encoding):给每个位置生成一个独特的向量,然后加到词的向量上。

原始论文使用的是正弦 / 余弦函数来生成位置编码:

PE(pos, 2i) = sin(pos / 10000^(2i/d))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d))

其中 pos 是位置,i 是维度索引,d 是向量维度。

这种编码有几个巧妙的性质:

  • 每个位置的编码都是唯一的
  • 编码是有界的(-1 到 1 之间)
  • 相对位置可以通过线性变换得到

现代方法:旋转位置编码

现代大模型(如 Llama、Qwen)更多使用旋转位置编码(RoPE)。它的核心思想是把位置信息编码到向量的旋转角度中

RoPE 的优势:

  • 更好地表达相对位置关系
  • 可以外推到更长的序列
  • 计算效率更高

作为应用开发者,不需要深入理解位置编码的数学细节,只需要知道:模型是通过位置编码来理解词序的

2.4 Transformer 的完整架构

把上面讲的组件组合起来,就是完整的 Transformer 架构。

2.4.1 整体结构

Transformer 由 编码器(Encoder)解码器(Decoder) 两部分组成:

2953321-20260525155053387-1777289909.png

2.4.2 各组件的作用

词嵌入(Embedding)

把文字转换成数字向量。计算机不认识文字,所以要把每个词(或子词)映射成一个固定长度的向量。相似的词,向量也会比较接近。

位置编码(Positional Encoding)

前面讲过,用来告诉模型每个词的位置。

多头自注意力(Multi-Head Self-Attention)

前面详细讲过,用来捕捉词与词之间的关系。

前馈神经网络(Feed-Forward Network,FFN)

一个简单的两层神经网络,对每个位置的向量做进一步处理。可以理解为 "消化" 注意力层捕捉到的信息。

FFN(x) = max(0, xW1 + b1)W2 + b2

中间的 max(0, ...) 是 ReLU 激活函数(现代模型更多用 GELU 或 SwiGLU)。

残差连接(Residual Connection)

把输入直接加到输出上:output = layer(x) + x。这让信息可以 "绕过" 某些层直接传递,有助于训练深层网络。

层归一化(Layer Normalization)

对向量做标准化处理,让数值分布更稳定,有助于训练。

2.4.3 编码器 vs 解码器

编码器的任务是理解输入。它把输入文本转换成一系列向量,这些向量编码了输入的语义信息。

解码器的任务是生成输出。它一个词一个词地生成,每生成一个词都会参考编码器的输出(通过编码器 - 解码器注意力)。

在大语言模型(如 GPT)中,通常只使用解码器,因为它的任务就是根据上文生成下文,不需要单独的编码器。

2.5 GPT 的架构

GPT(Generative Pre-trained Transformer)只使用 Transformer 的解码器部分,但做了一个关键修改:掩码自注意力(Masked Self-Attention)

在生成第 n 个词时,模型只能看到前 n-1 个词,不能 "偷看" 后面的词。这是通过在注意力计算时加一个掩码(mask)来实现的,把未来位置的注意力分数设为负无穷(经过 softmax 后变成 0)。

GPT 架构(简化):

输入 tokens
    ↓
词嵌入 + 位置编码
    ↓
┌─────────────────────┐
│  掩码自注意力层      │
│  前馈神经网络        │  × N 层(GPT-3 是 96 层)
│  残差连接 + 层归一化  │
└─────────────────────┘
    ↓
线性层 + Softmax
    ↓
下一个词的概率分布

2.6 从输入到输出:完整的推理过程

输入

用户:什么是机器学习?
助手:

第 1 步:分词(Tokenization)

首先,把文本切分成 token。大模型通常使用子词分词(如 BPE),一个汉字可能是一个 token,也可能和其他字合成一个 token。

["用户", ":", "什么", "是", "机器", "学习", "?", "\n", "助手", ":"]

每个 token 都有一个唯一的 ID:

[1234, 567, 8901, 234, 5678, 9012, 345, 678, 9012, 567]

第 2 步:词嵌入

根据 token ID,查找嵌入矩阵,得到每个 token 的向量。假设向量维度是 4096,那就得到一个 10×4096 的矩阵。

第 3 步:加上位置编码

给每个位置加上对应的位置编码向量,让模型知道每个 token 的位置。

第 4 步:通过 N 层 Transformer 层

经过每一层,向量都会被更新:

  • 自注意力让每个位置"吸收"其他位置的信息
  • 前馈网络进一步处理信息
  • 残差连接保留原始信息
  • 层归一化稳定数值

经过几十层处理后,最后一个位置(":" 后面)的向量,就编码了 "应该输出什么" 的信息。

第 5 步:生成第一个词

最后一个位置的向量,通过一个线性层映射到词表大小(比如 32000),再经过 softmax 得到概率分布。

"机器"   → 0.35
"它"     → 0.20
"简单"   → 0.10
"一种"   → 0.08
...

根据概率采样(或选概率最高的),得到第一个输出词:"机器"

第 6 步:继续生成

把 "机器" 加到输入末尾:

["用户", ":", "什么", "是", "机器", "学习", "?", "\n", "助手", ":", "机器"]

重复第 2-5 步,生成下一个词:"学习"

继续重复,直到生成完整的回答:

机器学习是人工智能的一个分支,它使计算机能够通过数据和经验自动学习和改进,而无需明确编程。

三,核心概念

3.1 Token

3.1.1 什么是 Token

Token 是大模型处理文本的最小单位。

你可能以为大模型是一个字一个字、或者一个词一个词处理的。实际上不是。大模型处理的是 Token——一种介于字和词之间的单位。

举个例子:

原文分词结果(Token)
你好世界你好 / 世界
Hello WorldHello / ␣World
programmingprogram / ming
ChatGPTChat / G / PT
人工智能人工 / 智能
今天天气不错今天 / 天气 / 不错
functionfunction
beautifulbeaut / iful

可以看到:

  • 常见的词往往是一个 Token
  • 不常见的词可能被拆成多个 Token
  • 英文单词可能被拆成词根和后缀
  • 空格通常会被算进 Token 里

3.1.2 为什么要用 Token 而不是字/词

为什么不用 "字" 作为单位?

如果用字符作为单位,英文就是 26 个字母加一些标点。词表太小,模型需要很长的序列才能表达一个词,效率太低。

比如 "programming" 需要 11 个字符,而作为 Token 只需要 2 个(program + ming)。

对于中文,如果用单字作为单位,词表就是几千个汉字。虽然可行,但会丢失很多词语级别的语义信息。"人工智能" 被拆 \ 成 "人"、"工"、"智"、"能" 四个字,模型需要花额外的努力去理解这是一个整体。

为什么不用 "词" 作为单位?

如果用词作为单位,英文可能需要几十万个词(加上各种变体、专有名词),词表太大,而且会遇到大量 "未登录词"(词表里没有的词)。

遇到新词怎么办?比如 "ChatGPT" 这个词,2022 年之前根本不存在。如果用词表,就只能当作未知词处理。

Token 是一个折中方案

Token 采用 子词分词(Subword Tokenization) 的方式,把常见词保持原样,把不常见的词拆成更小的单位。

最常用的子词分词算法是 BPE(Byte Pair Encoding,字节对编码)。它的基本思想是:

  1. 从最小单位(字符或字节)开始
  2. 统计训练语料中哪两个相邻单位最常一起出现
  3. 把最常见的组合合并成一个新单位
  4. 重复步骤 2-3,直到词表达到指定大小

通过这种方式,BPE 能自动学会:

  • 高频词保持完整(如 "the"、"is"、"今天")
  • 低频词被拆分(如 "programming" → "program" + "ming")
  • 新词也能处理(通过组合已有的子词)

3.1.3 BPE 算法详解

假设我们的训练语料只有这些词及其出现频率:

low: 5次
lower: 2次
newest: 6次
widest: 3次

第 1 步:初始化

把每个词拆成字符,加上词尾标记 </w>

l o w </w>: 5
l o w e r </w>: 2
n e w e s t </w>: 6
w i d e s t </w>: 3

初始词表是所有字符:{l, o, w, e, r, n, s, t, i, d, </w>}

第 2 步:统计相邻字符对

统计所有相邻字符对的出现频率:

  • e s: 出现 6+3=9 次(newest 和 widest 都有)
  • s t: 出现 6+3=9 次
  • lo: 出现 5+2=7 次
  • ow: 出现 5+2=7 次
  • ...

第 3 步:合并最频繁的对

e ss t 出现次数最多。假设我们先合并 e ses

l o w </w>: 5
l o w e r </w>: 2
n e w es t </w>: 6
w i d es t </w>: 3

词表变成:{l, o, w, e, r, n, s, t, i, d, </w>, es}

第 4 步:继续合并

现在 es t 出现 9 次,合并 → est

l o w </w>: 5
l o w e r </w>: 2
n e w est </w>: 6
w i d est </w>: 3

词表变成:{l, o, w, e, r, n, s, t, i, d, </w>, es, est}

第 5 步:重复直到达到目标词表大小

继续合并高频对:

  • lolowlow</w>
  • est </w>est</w>
  • ...

最终可能得到这样的词表:

{l, o, w, e, r, n, s, t, i, d, </w>, es, est, est</w>, lo, low, low</w>, ...}

BPE 的精妙之处

通过这种方式:

  • "low" 变成了一个完整的 Token
  • "newest" 被切成 "new" + "est"
  • 即使遇到训练时没见过的词,比如 "lowest",也能切成 "low" + "est"

这就是 BPE 能处理未登录词的原因。

3.1.4 Token 和字符数的关系

一个非常实用的问题:1 个 Token 大约等于多少字 / 字符?

这取决于语言和具体的分词器,但有一些经验值:

语言大约比例
英文1 Token ≈ 4 个字符 ≈ 0.75 个单词
中文1 Token ≈ 1.5-2 个汉字
代码1 Token ≈ 3-4 个字符

所以:

  • 一篇 1000 字的中文文章,大约是 500-700 Token
  • 一篇 1000 词的英文文章,大约是 1300-1500 Token
  • 一段 100 行的代码,可能是 500-1000 Token

3.1.5 Token 为什么重要

Token 直接关系到两件事:能力边界使用成本

能力边界

大模型有一个上下文窗口(后面会详细讲),限制了一次能处理的 Token 数量。比如 GPT-4 Turbo 的上下文窗口是 128K Token,如果你的输入超过这个限制,要么会报错,要么早期的内容会被 "遗忘"。

使用成本

API 调用按 Token 计费。输入 Token 和输出 Token 通常分开计价,输出 Token 往往更贵。

以 GPT-4 为例(价格可能已变化,仅作参考):

  • 输入:$0.03 / 1K Token
  • 输出:$0.06 / 1K Token

如果你的应用每天处理 100 万 Token,光 API 费用就要几十美元。所以优化 Token 使用量是降低成本的重要手段。

3.2 上下文窗口

3.2.1 什么是上下文窗口

上下文窗口(Context Window)是大模型一次能 "看到" 的最大 Token 数量。

你可以把它理解为模型的 "工作记忆"。就像人类的短期记忆有容量限制,你没法同时记住一本书的所有内容。大模型也一样,它能同时处理的信息是有限的。

上下文窗口包括了输入输出的总和。比如一个 8K 上下文的模型:

  • 如果你输入了 6K Token,那最多只能输出 2K Token
  • 如果你想要 4K 的输出,输入就只能有 4K

3.2.2 上下文窗口的深入理解

上下文窗口包含什么?

一次完整的对话请求中,占用上下文窗口的内容包括:

[系统提示词 System Prompt] → 几十到几百 Token
[历史对话记录] → 根据对话长度,可能占大头
  - 第1轮:用户问题 + 助手回答
  - 第2轮:用户问题 + 助手回答
  - ...
[当前用户问题] → 几十到几百 Token
[模型生成的回答] → 这也算在窗口内!

一个常见的误区是以为上下文窗口只包含输入,实际上输出也算

多轮对话的累积效应

大模型本身是没有记忆的。每次 API 调用都是独立的。所谓的 "多轮对话",是在每次请求时把之前的对话历史都带上。

{
  "messages": [
    {"role": "system", "content": "你是一个编程助手"},
    {"role": "user", "content": "什么是冒泡排序?"},
    {"role": "assistant", "content": "冒泡排序是一种简单的排序算法...(500字)"},
    {"role": "user", "content": "能给我写个例子吗?"},
    {"role": "assistant", "content": "当然,这是一个Python实现...(300字)"},
    {"role": "user", "content": "能改成Java吗?"}
  ]
}

看到问题了吗?每次请求都要带上所有历史。对话越长,每次请求消耗的 Token 越多,而且是重复付费的。

假设对话了 20 轮,每轮平均 500 Token,那第 20 轮的请求就要带上 10000 Token 的历史——光是 "上下文" 就要花不少钱。

3.2.3 超出上下文窗口怎么办

方案 1:截断(Truncation)

最简单的方法,超出部分直接丢掉。但可能丢失重要信息。

def truncate_messages(messages, max_tokens=4000, encoder=None):
    """截断历史消息,保留最新的"""
    if encoder is None:
        encoder = tiktoken.encoding_for_model("gpt-4")
    
    total_tokens = 0
    truncated = []
    
    # 倒序遍历,保留最新的消息
    for msg in reversed(messages):
        msg_tokens = len(encoder.encode(str(msg)))
        if total_tokens + msg_tokens > max_tokens:
            break
        truncated.insert(0, msg)
        total_tokens += msg_tokens
    
    return truncated

方案 2:分块处理(Chunking + Map-Reduce)

把长文本分成多个块,每块单独处理,最后汇总结果。

长文档(50000 字)
    ↓
分成 10 个块(每块 5000 字)
    ↓
每块单独让模型处理(提取要点/生成摘要)
    ↓
把 10 个结果汇总
    ↓
再让模型对汇总结果做最终处理

这种方法叫 Map-Reduce,适合摘要、提取关键信息、分析长文档等任务。

def summarize_long_document(document, chunk_size=3000):
    """对长文档进行分块摘要"""
    # 分块
    chunks = split_into_chunks(document, chunk_size)
    
    # Map: 对每个块生成摘要
    summaries = []
    for chunk in chunks:
        summary = call_llm(f"请用100字总结以下内容:\n{chunk}")
        summaries.append(summary)
    
    # Reduce: 汇总所有摘要
    combined = "\n".join(summaries)
    final_summary = call_llm(f"请综合以下摘要,给出最终总结:\n{combined}")
    
    return final_summary

方案 3:滑动窗口 + 摘要

保留最近的完整对话,对早期对话做摘要。

def manage_conversation_history(messages, max_recent=5, max_tokens=6000):
    """管理对话历史:保留最近N轮 + 早期摘要"""
    if len(messages) <= max_recent * 2:  # 用户+助手算两条
        return messages
    
    # 分离早期和最近的消息
    recent = messages[-(max_recent * 2):]
    early = messages[:-(max_recent * 2)]
    
    # 对早期消息做摘要
    early_text = format_messages_as_text(early)
    summary = call_llm(f"请简要概括以下对话的主要内容:\n{early_text}")
    
    # 返回:摘要 + 最近消息
    return [
        {"role": "system", "content": f"之前对话摘要:{summary}"},
        *recent
    ]

方案 4:选择更大上下文的模型

如果你的场景确实需要处理长文本,考虑选择上下文窗口更大的模型。虽然可能更贵,但省去了复杂的工程处理。

注意:大上下文 ≠ 用满了也很准

有一个常见的误解:觉得模型支持 128K 上下文,就能完美处理 128K 的内容。

实际上,上下文越长,模型的表现可能越差

"Lost in the Middle" 问题

有研究发现,大模型对上下文中间部分的注意力会下降。如果关键信息藏在一长段文本的中间,模型可能会忽略它。

[开头的内容] → 注意力较高
[中间的内容] → 注意力下降 ← 关键信息可能被忽略
[结尾的内容] → 注意力较高

实验验证

给模型一个 10 万 Token 的上下文,在不同位置放一个关键信息(比如一个密码),然后问模型这个信息是什么。结果发现:

  • 放在开头:准确率 90%+
  • 放在中间:准确率可能降到 50%
  • 放在结尾:准确率 85%+

实践建议:

  1. 不要因为有大上下文就一股脑塞满。精选最相关的内容,控制在合理的长度
  2. 把最重要的信息放在开头或结尾
  3. 如果必须提供很长的上下文,可以在最后重复强调关键点

3.3 Temperature

3.3.1 什么是 Temperature

Temperature 是一个控制模型输出随机性的参数,取值范围通常是 0 到 2(不同模型可能不同)。

还记得大模型是怎么生成文本的吗?它会计算每个候选词的概率,然后选择一个词输出。Temperature 影响的就是这个选择过程

  • Temperature = 0:总是选择概率最高的词,输出最确定、最"保守"
  • Temperature = 1:按照原始概率分布采样,比较均衡
  • Temperature > 1:放大低概率词的机会,输出更随机、更"疯狂"

3.3.2 直观理解 Temperature

用一个例子来说明。假设模型在预测 "今天天气真 ____" 的下一个词,概率分布是:

候选词原始概率
60%
不错25%
10%
糟糕5%

Temperature = 0 时

直接选概率最高的 "好"。问 100 次,100 次都选 "好"。

Temperature = 0.7 时

概率分布会被调整,高概率词更占优势:

候选词调整后概率
~75%
不错~18%
~5%
糟糕~2%

大多数情况选 "好",偶尔选 "不错"。

Temperature = 1.5 时

概率分布变得更平均:

候选词调整后概率
~45%
不错~28%
~17%
糟糕~10%

低概率词也有更多机会被选中。输出更加多样化,但也更可能出现 "奇怪" 的回答。

3.3.3 Temperature 的数学原理

如果你想了解技术细节,Temperature 是这样工作的。

标准的 Softmax 函数:

P(wi) = exp(zi) / Σexp(zj)

带 Temperature 的 Softmax:

P(wi) = exp(zi/T) / Σexp(zj/T)

其中 T 就是 Temperature。

  • 当 T < 1 时,指数函数的差异被放大,高概率词更占优势(分布更"尖锐")
  • 当 T > 1 时,指数函数的差异被缩小,概率分布更平均(分布更"平坦")
  • 当 T → 0 时,趋近于总是选概率最高的词(贪婪解码,Greedy Decoding)
  • 当 T → ∞ 时,趋近于均匀随机选择

举个具体的数学例子

假设两个候选词的原始 logit 是 z1=2, z2=1

T=1 时

  • P1 = exp(2) / (exp(2)+exp(1)) = 7.39 / (7.39+2.72) = 73%
  • P2 = exp(1) / (exp(2)+exp(1)) = 2.72 / (7.39+2.72) = 27%

T=0.5 时

  • P1 = exp(4) / (exp(4)+exp(2)) = 54.6 / (54.6+7.39) = 88%
  • P2 = 12%

T=2 时

  • P1 = exp(1) / (exp(1)+exp(0.5)) = 2.72 / (2.72+1.65) = 62%
  • P2 = 38%

可以看到,T 越小,概率差距越大;T 越大,概率差距越小。

3.3.4 不同场景的 Temperature 建议

场景推荐 Temperature原因
代码生成0 - 0.3代码需要精确,不能有太多随机性
数据提取/结构化输出0 - 0.2JSON 等结构化输出需要确定性
翻译0.3 - 0.5需要准确但也要自然流畅
问答/RAG0.3 - 0.7准确性重要,但希望表达有变化
文章写作0.7 - 1.0需要一定的创意和文采
头脑风暴1.0 - 1.5需要更多发散思维
创意故事/诗歌1.2 - 1.8鼓励意想不到的内容

常见误区

误区 1:Temperature 高 = 模型更聪明

很多人以为调高 Temperature 能让模型更 "聪明"。其实不是的。

Temperature 只影响选词的随机性,不影响模型的 "智商"。调高只是让它更敢冒险选择低概率的词,不是让它更有能力。

如果模型回答的内容本来就不对,调高 Temperature 只会让它更随机地胡说八道。

误区 2:Temperature 总是要调

对于大多数应用场景,默认的 Temperature(通常是 0.7 或 1.0)就够用了。没必要过度调参。

先跑通功能,再根据实际效果微调。

3.4 Top-P 和 Top-K

除了 Temperature,还有两个常见的参数也控制采样过程:Top-PTop-K

3.4.1 Top-K 采样

Top-K 是指只从概率最高的 K 个候选词中选择。

比如设置 K=3,原始概率分布:

候选词概率
50%
不错25%
15%
糟糕5%
3%
其他2%

Top-K=3 后,只考虑前 3 个词,重新归一化:

候选词调整后概率
55.6%
不错27.8%
16.6%

低概率的 "糟糕"、"坏" 等词被完全排除,永远不会被选中。

Top-K 的问题:K 是固定的,但不同上下文的概率分布差异很大。

比如在有些上下文中,只有 1-2 个合理选项;在另一些上下文中,可能有 10 个都合理。固定的 K 不够灵活:

  • 如果 K 太小,可能排除了合理的选项
  • 如果 K 太大,可能包含了太多不合理的选项

3.4.2 Top-P 采样

Top-P 是指选择累积概率达到 P 的最小候选词集合。

比如设置 P=0.9,按概率从高到低累加:

候选词概率累积概率是否保留
50%50%
不错25%75%
15%90%
糟糕5%95%
其他5%100%

前 3 个词的累积概率正好达到 90%,所以只从这 3 个词中选择。

Top-P 的优势:它是自适应的。

  • 如果概率很集中(一个词就占 95%),候选集可能只有 1 个词
  • 如果概率分散(前 10 个词每个都占 9%),候选集可能有 10 个词

这样就能根据实际的概率分布自动调整候选集大小。

3.4.3 如何组合使用

这三个参数可以单独使用,也可以组合使用:

response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "..."}],
    temperature=0.8,  # 先调整概率分布
    top_p=0.9,        # 再从累积概率 90% 的词中选
)

一般建议

  1. 如果只调一个,优先用 Temperature(最直观)或 Top-P(更灵活)
  2. 不建议同时调 Temperature 和 Top-P,容易产生意想不到的效果
  3. OpenAI 官方建议:调整其中一个时,把另一个设为默认值(Temperature=1 或 Top-P=1)

3.5 MoE

3.5.1 什么是 MoE

MoE(Mixture of Experts,混合专家) 是一种模型架构,核心思想是:不是每次推理都使用全部参数,而是动态选择部分 "专家" 来处理当前输入。

打个比方。传统的大模型像一个综合医院的全员会诊。不管病人来看什么病,所有科室的医生都要参与。感冒患者?内科、外科、骨科、眼科……全部到场。虽然诊断可能很全面,但效率低、成本高。

MoE 模型像一个智能分诊系统。病人来了,前台先判断是什么情况,然后只叫相关科室的专家。感冒?叫呼吸内科就行了。骨折?叫骨科。不需要所有医生都参与。

3.5.2 MoE 的工作原理

MoE 模型的核心组件是:

  1. 多个专家(Experts):每个专家是一个独立的小型神经网络(通常是前馈网络 FFN)
  2. 门控网络(Router/Gating Network):决定当前输入应该使用哪些专家

处理流程

输入 Token
    ↓
门控网络(Router)分析输入
    ↓
选择 Top-K 个最相关的专家(通常 K=2)
    ↓
只激活被选中的专家进行计算
    ↓
合并专家的输出
    ↓
输出结果

具体例子

假设一个 MoE 模型有 8 个专家,门控网络设置为每次选 2 个:

输入: "写一段Python代码"
    ↓
Router 分析: 这是编程任务
    ↓
选择专家: Expert3(代码专家)和 Expert7(Python专家)
    ↓
只有 Expert3 和 Expert7 参与计算
    ↓
其他 6 个专家"休息"

再来一个输入:

输入: "翻译这段文字到英文"
    ↓
Router 分析: 这是翻译任务
    ↓
选择专家: Expert1(语言理解)和 Expert5(英语生成)
    ↓
只有 Expert1 和 Expert5 参与计算

3.5.3 MoE 的关键指标

看 MoE 模型时,有两个关键数字:

  • 总参数(Total Parameters):所有专家加起来的参数量
  • 激活参数(Active Parameters):每次推理实际使用的参数量

以几个典型 MoE 模型为例:

模型总参数激活参数专家数每次激活
Mixtral 8x7B46.7B12.9B82
Mixtral 8x22B141B39B82
DeepSeek-V2236B21B1606
DeepSeek-V3671B37B2568
GPT-4(传闻)~1.8T~220B~16~2

DeepSeek-V3 的总参数是 671B,看起来巨大无比。但每次推理只激活 37B,实际的计算成本和一个 37B 的普通模型差不多。

3.5.4 Moe优劣势

MoE 的优势

  • 优势 1:性价比极高

    用较少的计算量获得大模型的效果。

    DeepSeek-V3:

    • 总参数 671B 的知识容量(学得多、知道得多)
    • 实际推理成本约等于 37B 的模型(跑得快、花得少)
    • 效果接近甚至超越 GPT-4

    这就是为什么 DeepSeek 的 API 价格可以比 GPT-4 便宜 10-20 倍。

  • 优势 2:可扩展性强

    传统模型要变大,计算量会等比例增加。参数翻倍,计算量也翻倍。

    MoE 可以只增加专家数量,而保持每次激活的专家数量不变。从 8 个专家增加到 256 个,但每次还是只激活 8 个,计算量基本不变。

    理论上可以扩展到任意大的参数量,而计算成本增长有限。

  • 优势 3:专家专精(可能)

    研究者发现,不同的专家可能会自然地学会处理不同类型的任务:

    • 有的专家更擅长代码
    • 有的专家更擅长数学
    • 有的专家更擅长某种语言
    • 有的专家更擅长逻辑推理

    门控网络会自动把相关的输入路由到对应的专家,实现某种程度的 "专业分工"。

MoE 的局限性

  • 局限 1:显存占用依然很大

    虽然每次只激活部分专家,但所有专家的参数都要加载到显存中。

    DeepSeek-V3 激活参数只有 37B,但部署时需要加载全部 671B 参数。以 FP16 精度,显存需求超过 1.3TB——需要 16 张 80GB 的 A100 才能装下。

    对于本地部署来说,MoE 模型的显存需求比同等激活参数的普通模型大得多。

  • 局限 2:负载均衡问题

    如果门控网络总是把输入路由到少数几个专家,其他专家就被 "冷落" 了,造成:

    • 热门专家过载,成为瓶颈
    • 冷门专家参数浪费
    • 训练不均衡

    需要专门的损失函数(如负载均衡损失)来约束,让专家之间的负载尽量均匀。

  • 局限 3:训练复杂度高

    MoE 模型的训练比普通模型更复杂:

    • 需要处理专家负载均衡
    • 分布式训练时的通信开销更大
    • 调参空间更大(专家数、激活数、路由策略……)

3.6 深度思考与 CoT

3.6.1 什么是深度思考

一些模型(如 DeepSeek-R1、OpenAI o1)引入了 "深度思考" 模式。开启这个模式后,模型会在回答之前先进行一番 "内心独白",把问题分析清楚再给出答案。

这种方式能显著提升复杂推理任务的准确率,尤其是数学、逻辑、代码等需要多步推理的任务。

3.6.2 CoT(思维链)的本质

CoT(Chain of Thought,思维链) 是实现深度思考的一种技术。核心思想是:让模型像人一样,一步一步地推理,把中间过程写出来,而不是直接跳到答案。

为什么这样有效?

想象你做一道复杂的数学题。如果直接心算,很容易出错。但如果在草稿纸上一步步写出来,每一步都是简单的计算,最后把结果串起来,正确率就高很多。

大模型也是一样。直接生成最终答案,中间的推理是 "隐式" 的,容易出错。把推理过程 "显式" 地写出来,相当于强迫模型一步一步想清楚。

3.6.3 具体例子对比

问题:一个水池有两个进水管,A 管单独注满需要 4 小时,B 管单独注满需要 6 小时。两管同时开,多久能注满?

不用 CoT(直接回答)

答案是 2.4 小时。

答案是对的,但如果题目稍微复杂一点,模型可能就算错了。

用 CoT(思维链回答)

让我一步步分析这个问题。

1. 首先确定每个管的效率:
   - A 管每小时注入水池的 1/4
   - B 管每小时注入水池的 1/6

2. 两管同时开时,每小时注入量:
   1/4 + 1/6 = 3/12 + 2/12 = 5/12

3. 注满整个水池需要的时间:
   1 ÷ (5/12) = 12/5 = 2.4 小时

所以两管同时开,需要 2.4 小时注满水池。

通过显式地写出每一步,模型犯错的概率大大降低。

3.6.4 如何触发 CoT

方式 1:在 Prompt 中明确要求

请一步一步地思考这个问题,写出你的推理过程,然后给出最终答案。

问题:xxx

或者:

Let's think step by step.

这是最简单的触发方式,称为 Zero-shot CoT

方式 2:提供 Few-shot 示例

给模型几个带思维链的例子,它会学着用同样的方式回答:

问题:一辆车以 60 公里/小时的速度行驶,2 小时能走多远?

思考过程:
1. 已知速度是 60 公里/小时
2. 已知时间是 2 小时
3. 根据公式:距离 = 速度 × 时间
4. 距离 = 60 × 2 = 120 公里

答案:120 公里

---

问题:小明有 100 元,买了 3 本书,每本 25 元,还剩多少钱?

思考过程:

模型会自动续写思考过程。

方式 3:使用支持深度思考的模型

一些模型内置了深度思考能力:

  • OpenAI o1 / o1-mini:专门优化的推理模型
  • DeepSeek-R1:开源的深度思考模型
  • Claude 3.5 extended thinking:支持扩展思考模式

这些模型会自动进行内部推理。有的会展示思考过程,有的只展示最终答案。

3.6.5 什么时候用 CoT

CoT 不是银弹,要根据任务类型选择:

适合用 CoT 的场景

  • 数学计算和推理
  • 逻辑推理和分析
  • 复杂的代码生成(需要先设计再编码)
  • 多步骤的分析任务
  • 需要综合多个因素做决策

不适合用 CoT 的场景

  • 简单的事实问答("中国的首都是哪里?")
  • 日常闲聊
  • RAG 场景(答案已经在检索结果中)
  • 创意写作(思考过程可能反而限制创意)
  • 翻译任务(直接翻译通常更好)

CoT 的代价

  • 增加输出 Token(思考过程也要付费)
  • 增加延迟(生成思考过程需要时间)
  • 有时候"想太多"反而出错

3.7 模型量化

3.7.1 什么是量化

量化(Quantization)是一种模型压缩技术,通过降低参数的数值精度来减少模型大小和计算量。

大模型的参数通常是 32 位浮点数(FP32)或 16 位浮点数(FP16/BF16)。每个参数用 2-4 个字节存储。

量化就是把这些参数转换成更低精度的表示,比如 8 位整数(INT8)、4 位整数(INT4),甚至 2 位(INT2)。

原始参数 (FP16):   0.0012345678...    [16 bits = 2 bytes]
INT8 量化后:       3                   [8 bits = 1 byte]    → 压缩 2 倍
INT4 量化后:       1                   [4 bits = 0.5 byte]  → 压缩 4 倍

3.7.2 量化带来的好处

好处 1:显存占用减少

一个 7B 参数的模型:

精度每参数字节模型大小运行时显存估算
FP16214 GB~18-20 GB
INT817 GB~10-12 GB
INT40.53.5 GB~5-6 GB

量化后,原本需要 24GB 显存才能跑的模型,用 8GB 的消费级显卡也能跑了。

好处 2:推理速度提升

  • 低精度计算更快(硬件对 INT8 计算有优化)
  • 内存带宽需求降低(数据搬运更快)
  • 缓存利用率更高

量化后的模型推理速度可能提升 2-4 倍。

好处 3:部署成本降低

  • 可以用更便宜的 GPU
  • 或者在同样的 GPU 上部署更大的模型
  • 甚至可以用 CPU 运行(GGUF 格式)

3.7.3 常见的量化方法

PTQ(Post-Training Quantization)训练后量化

在模型训练完成后进行量化,不需要重新训练。

代表方法:

  • GPTQ:分析权重分布,选择最优的量化策略
  • AWQ:考虑激活值分布,保护重要权重
  • GGUF:llama.cpp 项目推广的格式

QAT(Quantization-Aware Training)量化感知训练

在训练过程中就考虑量化,让模型 "学会" 适应低精度表示。效果通常比 PTQ 更好,但需要重新训练。

3.7.4 量化格式详解

GPTQ

  • 需要 GPU 运行
  • 量化过程需要校准数据集
  • 精度损失较小
  • 社区支持好,HuggingFace 上有大量预量化模型
# 使用 GPTQ 模型(通过 transformers)
from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "TheBloke/Llama-2-7B-GPTQ",
    device_map="auto"
)

AWQ(Activation-aware Weight Quantization)

  • 需要 GPU 运行
  • 考虑激活值的分布,保护对输出影响大的权重
  • 量化质量可能比 GPTQ 更好
  • 推理速度也更快

GGUF(GPT-Generated Unified Format)

  • 支持 CPU 运行,不需要显卡
  • 由 llama.cpp 项目推广
  • 支持多种量化级别
  • 通过 Ollama 等工具很容易使用

GGUF 的量化级别命名约定:

格式量化位数大小(相对 FP16)质量
Q8_08 bit~50%几乎无损
Q6_K6 bit~42%很小的损失
Q5_K_M5 bit~35%小损失,推荐
Q5_K_S5 bit~33%稍大损失
Q4_K_M4 bit~28%明显但可接受
Q4_K_S4 bit~26%更明显损失
Q3_K_M3 bit~22%显著损失
Q2_K2 bit~15%严重损失,不推荐

3.7.5 如何选择量化级别

选择建议

  1. 显存充足:用 Q8_0 或 Q6_K,质量损失极小
  2. 显存一般:用 Q5_K_M(最佳平衡点),或 Q4_K_M
  3. 显存紧张:用 Q4_K_S 或 Q4_K_M
  4. 极端情况:Q3 及以下不推荐,质量损失太大

经验法则

  • 如果可以接受的话,优先选 Q5_K_M
  • Q4 是底线,再低效果会明显下降
  • 宁可选稍小的模型用高精度,也不要选大模型用过低精度

3.7.6 量化的代价

量化不是免费的午餐,会有一定的质量损失:

精度下降的表现

  • 复杂推理能力下降
  • 数学计算更容易出错
  • 知识召回可能不准确
  • 小语种能力下降更明显

量化敏感的任务

  • 数学计算
  • 代码生成
  • 精确的事实问答
  • 需要精细区分的分类

量化不敏感的任务

  • 日常对话
  • 创意写作
  • 情感分析
  • 简单的问答

好消息:对于大多数应用场景,Q4 或 Q5 量化的模型和原始模型的差异不容易察觉。只有在对质量要求极高的场景,才需要考虑更高精度。

四,主流模型介绍

4.1 国际主流模型

OpenAI GPT 系列

GPT 系列依然是目前公认的大模型综合能力天花板,也是整个行业的 "标杆"。不过截至 2026 年 3 月,OpenAI 的主力产品线已经从 GPT-4o / o1 演进到了 GPT-5.x 家族。

核心产品线

模型定位上下文特点
GPT-5.4旗舰通用模型1M复杂推理、代码、Agent 工作流综合最强
GPT-5.4 pro极致性能版1M更稳更强,适合关键链路和最复杂任务
GPT-5 mini轻量主力400K低延迟、高吞吐,适合大多数生产任务
GPT-5 nano超轻量版400K分类、抽取、路由、批处理最省钱

Anthropic Claude 系列

Claude 是由 Anthropic 开发的大模型,Anthropic 的创始人是前 OpenAI 核心成员。截至 2026 年 3 月,Claude 的主力产品线已经来到 4.6 / 4.5 这一代,在代码、Agent 工具使用和长任务稳定性上依然非常强,是 GPT-5 系列最直接的竞争对手之一。

核心产品线

模型定位上下文特点
Claude Opus 4.6顶级旗舰1M复杂推理、长任务、代码质量最强
Claude Sonnet 4.6主力旗舰1M速度/能力最平衡,代码与 Agent 任务首选
Claude Haiku 4.5轻量版1M延迟低、成本低,适合高频生产调用

Google Gemini 系列

Google 的大模型,主打多模态、搜索增强和超长上下文。截至 2026 年 3 月,Gemini 的主力线已经从 1.5/2.0 过渡到 3.1 / 2.5 并行,官方预览版迭代也非常快。

核心产品线

模型定位上下文特点
Gemini 3.1 Pro Preview前沿旗舰1M多模态、代码、Agent、Search Grounding 最强
Gemini 2.5 Pro稳定旗舰1M推理强、生产更稳,适合正式上线
Gemini 2.5 Flash平衡版1M速度快、成本低,适合高频 API 调用
Gemini 3.1 Flash-Lite Preview极致轻量1M批处理、抽取、分类最省钱

Meta Llama 系列

Llama 依然是开放权重模型里的标杆,但主线已经从 Llama 3.x 进入 Llama 4 时代,重点从纯文本模型转向了多模态和 MoE 架构。

核心产品线

模型参数量上下文特点
Llama 4 Maverick17B 激活 / 400B 总参数1M当前主力多模态旗舰,性能和部署成本最平衡
Llama 4 Scout17B 激活 / 109B 总参数10M超长上下文,单张 H100 就能运行
Llama 4 Behemoth(预告)288B 激活 / 2T 总参数-教师模型,Meta 主要用于蒸馏和对齐,暂未开放下载

4.2 国内主流模型

DeepSeek 系列

深度求索(DeepSeek)依然是国内最有代表性的高性价比模型厂商之一。

核心产品线

模型版本/形态上下文特点
deepseek-chatDeepSeek-V3.2(非思考模式)128K日常主力,工具调用、JSON、FIM 都很完整
deepseek-reasonerDeepSeek-V3.2(思考模式)128K深度推理、复杂代码、工具内思考能力更强
DeepSeek-V3.2-Exp(开源)开源部署版128K适合私有化、自建服务和二次微调

通义千问 Qwen 系列

阿里云出品,是国内开源生态最完善的模型系列。

核心产品线

模型参数量上下文特点
Qwen2.5-72B72B128K旗舰版,能力最强
Qwen2.5-32B32B128K主力版,平衡性能和成本
Qwen2.5-14B14B128K中等版,适合多数场景
Qwen2.5-7B7B128K轻量版,部署门槛低
Qwen2.5-3B3B32K超轻量,端侧部署
Qwen2.5-1.5B1.5B32K极轻量,嵌入式场景
Qwen2.5-0.5B0.5B32K最小版,资源极度受限
Qwen2.5-Coder多种-代码专用系列
Qwen2.5-Math多种-数学专用系列

智谱 GLM 系列

清华系背景,在复杂工程任务和 Agent 场景有特色。

核心产品线

模型特点
GLM-4-Plus旗舰版,综合能力强
GLM-4标准版
GLM-4-Flash轻量版,速度快
GLM-4V多模态版,支持图片

其他值得关注的国产模型

Kimi(月之暗面)

  • 核心特点:超长上下文处理(最高 200K)
  • 擅长场景:长文档分析、论文阅读、合同审查
  • 使用方式:网页版免费使用,有 API 服务

MiniMax

  • 核心特点:超长上下文、角色扮演能力强
  • 擅长场景:虚拟角色、游戏 NPC、内容创作
  • 使用方式:API 服务

讯飞星火

  • 核心特点:语音技术结合
  • 擅长场景:语音交互、教育场景
  • 使用方式:讯飞开放平台

五,大模型能力边界与解决方案

5.1 大模型擅长什么

能力领域具体能力原理简述对应训练阶段
文本理解与生成阅读理解、摘要、创作、改写、信息提取核心是“文字接龙”与“完形填空”。模型基于对上下文的理解(自注意力机制),预测下一个最可能的词元(Token)或填补空缺,从而生成连贯文本。预训练(获得语言和知识)、指令微调(学会遵循指令完成任务)
代码能力生成、解释、调试、重构、补全代码是一种结构化的语言。模型在海量代码库上预训练后,掌握了编程语言的语法和模式,其生成和调试过程同样是序列预测问题。预训练(大量代码数据)、指令微调(对齐代码任务)
语言处理翻译、语法纠错、语气转换翻译等任务在模型看来,本质是序列到序列的转换。预训练让它学到了不同语言间的语义对齐关系,使其能将一种语言序列转换为等义的另一种序列。预训练(多语言数据)
推理与分析逻辑推理、因果分析、方案比较属于涌现能力。当模型规模突破阈值后,它学会在输出答案前生成中间推理步骤(如思维链提示),将复杂问题拆解为一系列简单预测。这是复杂模式匹配的体现。预训练(规模是关键)、指令微调RLHF(人类反馈强化学习,使推理更有序)
对话交互多轮对话、意图识别、情感理解这是与人类偏好对齐的结果。模型把整个对话历史作为长文本,持续预测下一句回答。它通过人类反馈学会了识别情感、保持角色一致等交互模式。指令微调RLHF(学习“有用且无害”的交互方式)

5.2 三大核心局限

5.2.1 局限一:幻觉问题

这是大模型最广为人知的问题,也是最危险的问题。大模型有时候会一本正经地编造根本不存在的事实。它不会坦然说 "我不知道",而是会用流畅的语言、自信的语气说出看起来很像那么回事、但完全是假的内容。

真实案例:AI 写的合同

假设你让大模型帮你起草一份租房合同:

用户:请帮我写一份房屋租赁合同,要符合《民法典》相关规定。

大模型可能会给你一份格式工整、条款完整的合同,里面引用的法律条文看起来也很专业:

根据《中华人民共和国民法典》第七百零三条规定,租赁合同是出租人将租赁物
交付承租人使用、收益,承租人支付租金的合同。

...

第五条 违约责任
根据《民法典》第五百八十四条的规定,当事人一方不履行合同义务或者履行
合同义务不符合约定,造成对方损失的,损失赔偿额应当相当于因违约所造成
的损失...

如果你不是法律专业人士,很可能觉得这份合同很专业,直接就用了。但实际核对后可能会发现:

  • 某个引用的条款编号和实际内容不匹配
  • 某个法律概念的表述有偏差
  • 某些格式要求和当地法院的实践不符
  • 遗漏了某些重要的保护性条款

等到真出了纠纷,才发现这份 "专业" 的合同有漏洞——这时候代价可就大了。


为什么会有幻觉

理解幻觉的成因,需要回顾大模型的工作原理:

大模型的核心任务是 "预测下一个最可能出现的词"。

它不是在知识库里查找答案,而是根据语言模式生成 "看起来合理" 的内容。它不知道自己说的对不对,只知道这样说 "读起来很通顺"。

具体来说:

  1. **训练目标不是"正确",而是"流畅"**模型被训练的目标是生成语法正确、语义连贯的文本,不是生成事实正确的文本。它没有"这是真的"和"这是假的"的区分机制。
  2. 没有内置的事实检验模型内部没有知识库,也没有能力去验证自己说的是否正确。它只是在做概率预测。
  3. 宁编不缺的倾向大模型被训练成要给出完整、有帮助的回答。当它不确定答案时,与其说"我不知道",它更倾向于根据相关主题的语言模式"编"一个答案。
  4. 语言模式的误导如果训练数据中某种模式出现频繁,模型可能会在不适用的场景中也套用这个模式。

幻觉的几种表现形式

类型表现示例
事实错误说出不存在的事实编造历史事件、虚构人物履历
引用错误引用不存在的文献/法条编造论文、法律条款
数据错误给出错误的数字错误的统计数据、日期
逻辑错误推理过程有漏洞自相矛盾的论证
过度推断把不确定的说成确定的把可能性说成事实

应对幻觉的策略

策略 1:明确要求模型承认不确定性

在 Prompt 中明确告诉模型:

如果你不确定答案,请明确说 "我不确定" 或 "我没有这方面的信息",
不要猜测或编造。

策略 2:要求提供来源

请回答以下问题,并为你的每个关键观点提供来源出处。如果无法提供来源,请说明这是你的推测。

策略 3:使用 RAG 技术

把相关的参考资料通过检索系统找出来,放在 Prompt 里,让模型基于这些资料回答。这样可以大幅减少幻觉。

策略 4:人工审核关键内容

对于法律文件、医疗建议、财务分析等高风险内容,必须有专业人士审核。

策略 5:交叉验证

重要信息通过其他渠道(搜索引擎、专业数据库)验证。

5.2.2 局限二:精确计算是短板

大模型不擅长数学计算,这个问题很多人都遇到过。

经典问题:3.9 和 3.11 哪个大

这是小学生都能答对的问题:

用户:3.9 和 3.11 哪个大?

某些模型的回答:3.11 更大,因为 11 > 9。

这个回答是错的。3.9 = 3.90,是大于 3.11 的。


为什么会算错

理解这个问题,需要知道模型是怎么处理数字的。

数字被切成 Token

大模型在处理文本时会把内容切成 Token。数字也会被切分:

"3.11" → ["3", ".", "11"] 或 ["3.11"]

取决于分词器的实现。

模型看到的是这些 Token 的组合,而不是 "3.11" 这个数值本身。它不是在做数学运算,而是在做语言模式匹配

语言模式的干扰

模型在训练数据里见过很多 "版本号" 的比较,比如:

  • "Python 3.11 比 3.9 新"
  • "iOS 15 比 iOS 14 更新"

这些场景中,后面的数字确实代表 "更新 / 更大"。模型学到了这个模式,但在比较小数值时错误地套用了这个模式。

没有真正的计算能力

大模型内部没有计算器。当你让它算 17 × 24 时,它不是在执行乘法运算,而是在 "猜测" 一个看起来合理的答案。

用户:17 × 24 = ?

模型的内部过程:
- 见过类似的乘法题
- 17 × 24 大约是 17 × 25 = 425,减一点
- 猜一个看起来合理的数字,比如 408

实际答案:17 × 24 = 408(碰巧对了)

但稍微复杂一点的计算就容易出错:

用户:17 × 24 + 89 ÷ 3 = ?

正确答案:408 + 29.67 = 437.67
模型可能回答:412(随便猜的)

计算短板的解决方案

  • 最佳方案:让模型调用计算工具

    这就是 Function Call / Tool Use 技术的用武之地。

    # 定义一个计算器工具
    tools = [
        {
            "type": "function",
            "function": {
                "name": "calculate",
                "description": "执行数学计算",
                "parameters": {
                    "type": "object",
                    "properties": {
                        "expression": {
                            "type": "string",
                            "description": "数学表达式,如 '17 * 24 + 89 / 3'"
                        }
                    }
                }
            }
        }
    ]
    
    # 模型会识别需要计算的场景,输出调用请求
    # 系统执行计算,把结果返回给模型
    # 模型基于正确的计算结果生成回答
    
  • 代码执行环境

    让模型生成代码来计算,然后执行代码获取结果:

    # 模型生成的代码
    result = 17 * 24 + 89 / 3
    print(result)  # 437.6666...
    
    # 然后基于执行结果生成回答
    
  • 提示模型一步步计算

    通过 CoT(思维链)让模型分步计算,可以减少错误:

    请一步一步计算:
    第一步:17 × 24 = ?
    第二步:89 ÷ 3 = ?
    第三步:将两个结果相加
    

    但这仍然不能完全避免计算错误,只是降低概率。关键计算还是要用工具。

5.2.3 局限三:知识有边界

大模型的知识来自训练数据。训练数据没有的内容,它就不知道。

时间边界:知识截止日期

每个模型都有一个训练数据截止日期(Knowledge Cutoff)。这个日期之后发生的事情,模型是不知道的。

示例

用户:2025 年的诺贝尔物理学奖得主是谁?

如果模型的截止日期是 2024 年,它会:
- 诚实地说"我的知识截止到 2024 年,不知道 2025 年的获奖者"
- 或者错误地猜测一个答案(幻觉)

范围边界:私有信息不可及

训练数据主要来自公开的互联网内容。以下信息是模型不可能知道的:

企业内部信息

  • 公司的组织架构
  • 内部规章制度
  • 产品技术文档
  • 客户数据

个人私有信息

  • 你的个人笔记
  • 未公开的研究资料
  • 私人通信内容

付费 / 受限内容

  • 付费论文的全文
  • 订阅制服务的内容
  • 需要登录才能访问的资料

示例

用户:我们公司的年假政策是什么?

模型的真实情况:
- 它不知道你在哪家公司
- 你们公司的员工手册不在它的训练数据里
- 它只能给出一个通用的、基于常见做法的回答
- 这个回答可能和你们公司的实际政策完全不同

知识边界的解决方案

  1. RAG(检索增强生成)

    这是解决知识边界问题的主流方案:

    用户提问
        ↓
    从私有知识库检索相关文档片段
        ↓
    把片段 + 问题一起发给大模型
        ↓
    模型基于片段生成回答
    

    通过这种方式,模型可以回答训练数据里没有的问题。

  2. 知识库定期更新

    对于时效性要求高的场景,定期更新知识库的内容。

  3. 联网搜索

    对于实时信息,让模型调用搜索引擎获取最新内容。

  4. 模型微调

    把领域知识 "植入" 模型,让它内化这些知识。但成本较高,且知识更新不灵活。

5.3 常见解决方案

5.3.1 技术一:Prompt 工程

Prompt 工程是优化大模型输出的第一道防线,零成本,立即生效。

什么是 Prompt 工程

Prompt 工程是通过精心设计输入的提示词,引导模型更好地完成任务的技术。

好的 Prompt 可以:

  • 减少幻觉
  • 提高回答的准确性
  • 控制输出格式
  • 引导模型的思考方式

核心技巧

  1. 技巧 1:明确角色和背景

    你是一个专业的法律顾问,专注于劳动法领域,有 10 年执业经验。
    你的回答应该准确、专业,同时用普通人能理解的语言表达。
    
  2. 技巧 2:约束回答范围

    请仅基于以下提供的资料回答问题,不要添加资料中没有的信息。
    如果资料中没有相关内容,请说"提供的资料中没有这方面的信息"。
    
  3. 技巧 3:要求承认不确定性

    如果你不确定某个信息的准确性,请明确说明这是你的推测,而不是确定的事实。
    对于你不知道的问题,请直接说"我不知道",不要编造答案。
    
  4. 技巧 4:提供示例(Few-shot)

    请按以下格式提取文本中的实体:
    
    示例输入:苹果公司发布了iPhone 15,CEO库克出席了发布会。
    示例输出:
    - 公司:苹果公司
    - 产品:iPhone 15
    - 人物:库克
    
    现在请处理以下文本:
    ...
    
  5. 技巧 5:分步引导(CoT)

    请按以下步骤分析这个问题:
    1. 首先,识别问题的核心是什么
    2. 然后,列出解决这个问题需要的信息
    3. 接着,逐步分析每个相关因素
    4. 最后,给出你的结论和理由
    
  6. 技巧 6:限定输出格式

    请以 JSON 格式返回结果,格式如下:
    {
      "answer": "你的回答",
      "confidence": "高/中/低",
      "sources": ["来源1", "来源2"]
    }
    

Prompt 工程的局限

Prompt 工程能解决很多问题,但有边界:

  • 无法让模型获得它不知道的知识
  • 无法让模型真正进行数学计算
  • 无法获取实时信息
  • 复杂任务可能需要其他技术配合

5.3.2 技术二:RAG(检索增强生成)

RAG 是解决知识边界和减少幻觉的核心技术。

什么是 RAG

RAG = Retrieval-Augmented Generation,检索增强生成。

核心思想:不指望模型什么都知道,而是在需要的时候给它 "喂" 相关资料

RAG 的工作流程

用户提问:"我们公司的年假政策是什么?"
    ↓
【检索阶段】
从公司知识库中搜索"年假政策"相关的文档
    ↓
找到《员工手册》第 5 章"休假制度"相关段落
    ↓
【增强阶段】
构建 Prompt:
"根据以下公司资料回答员工的问题:

【参考资料】
员工入职满一年可享受 5 天年假,每增加一年工龄增加 1 天,
上限为 15 天。年假需提前 3 个工作日申请...

【问题】
我们公司的年假政策是什么?"
    ↓
【生成阶段】
大模型基于提供的资料生成回答
    ↓
回答:"根据公司规定,员工入职满一年后可享受 5 天年假..."

RAG 的核心组件

  1. 文档处理

    把原始文档切成小块(chunks),便于检索和塞进上下文:

    # 文档切分示例
    def split_document(doc, chunk_size=500, overlap=50):
        chunks = []
        start = 0
        while start < len(doc):
            end = start + chunk_size
            chunk = doc[start:end]
            chunks.append(chunk)
            start = end - overlap  # 重叠部分保持上下文连贯
        return chunks
    
  2. 向量化(Embedding)

 把文本转换成向量,便于语义搜索:
 ```python
 from sentence_transformers import SentenceTransformer
 
 model = SentenceTransformer('BAAI/bge-base-zh')  # 中文 Embedding 模型
 
 # 把文档块转换成向量
 doc_embeddings = model.encode(chunks)
 
 # 把用户问题转换成向量
 query_embedding = model.encode(query)
 ```

3. 向量数据库

 存储和检索向量,常用的有:

 -   Milvus
 -   Pinecone
 -   Chroma
 -   Qdrant
 -   FAISS

4. 检索策略

 决定如何找到最相关的文档块:

 -   **语义检索**:基于向量相似度
 -   **关键词检索**:基于 BM25 等传统方法
 -   **混合检索**:两者结合

5. 生成回答

 把检索到的文档和问题一起发给大模型:

 ```python
 def rag_answer(query, retrieved_docs):
     context = "\n\n".join(retrieved_docs)
     
     prompt = f"""请根据以下参考资料回答问题。
 如果资料中没有相关信息,请说明无法回答。
 
 【参考资料】
 {context}
 
 【问题】
 {query}
 
 【回答】"""
     
     return llm.generate(prompt)
 ```

RAG 的优势

  1. 解决知识边界:可以回答训练数据里没有的问题
  2. 减少幻觉:回答基于提供的资料,而不是模型的"记忆"
  3. 可追溯:可以显示答案的来源,便于验证
  4. 低成本更新:更新知识只需更新文档库,不需要重新训练模型

RAG 的挑战

  1. 检索质量:检索不到相关文档,回答就没法准确
  2. 上下文长度:检索到的内容可能超过上下文窗口
  3. 多跳推理:需要综合多个文档才能回答的问题比较难处理

5.3.3 技术三:Function Call / Tool Use

Function Call 让大模型能够调用外部工具,弥补能力短板。

什么是 Function Call

Function Call(函数调用)或 Tool Use(工具使用)是让大模型在需要时调用外部工具 /API 的能力。

大模型本身不执行工具,它只是判断需要调用什么工具、传什么参数,然后由外部系统执行。

工作流程

用户:北京今天天气怎么样?
    ↓
【大模型判断】
这个问题需要实时天气数据,我应该调用天气 API
    ↓
【输出工具调用请求】
{
  "tool": "get_weather",
  "arguments": {"city": "北京"}
}
    ↓
【系统执行工具】
调用天气 API,获得结果:
{"temp": 25, "weather": "晴", "humidity": 40}
    ↓
【把结果返回给模型】
    ↓
【大模型生成回答】
北京今天天气晴朗,气温 25°C,湿度 40%,是个适合外出的好天气。

代码示例

import openai

# 定义可用的工具
tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "获取指定城市的实时天气信息",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {
                        "type": "string",
                        "description": "城市名称,如'北京'、'上海'"
                    }
                },
                "required": ["city"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "calculate",
            "description": "执行数学计算",
            "parameters": {
                "type": "object",
                "properties": {
                    "expression": {
                        "type": "string",
                        "description": "数学表达式,如 '17 * 24 + 89 / 3'"
                    }
                },
                "required": ["expression"]
            }
        }
    }
]

# 调用模型
response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "北京今天天气怎么样?"}],
    tools=tools,
    tool_choice="auto"  # 让模型自己决定是否调用工具
)

# 检查是否需要调用工具
if response.choices[0].message.tool_calls:
    tool_call = response.choices[0].message.tool_calls[0]
    
    # 执行工具(这里需要你自己实现)
    if tool_call.function.name == "get_weather":
        args = json.loads(tool_call.function.arguments)
        result = get_weather_api(args["city"])
        
        # 把结果返回给模型
        messages.append(response.choices[0].message)
        messages.append({
            "role": "tool",
            "tool_call_id": tool_call.id,
            "content": json.dumps(result)
        })
        
        # 模型基于工具结果生成最终回答
        final_response = client.chat.completions.create(
            model="gpt-4",
            messages=messages
        )

常见工具类型

工具类型用途示例
计算工具数学计算计算器、代码执行器
搜索工具获取信息搜索引擎 API、知识库检索
数据 API获取实时数据天气、股票、汇率
操作工具执行动作发邮件、创建日程、下单
数据库查询数据SQL 查询、图数据库查询

5.3.4 技术四:Agent 架构

Agent 是把各种技术组合起来,让大模型能够自主完成复杂任务的架构。

什么是 Agent

Agent(智能体)是一个能够自主感知环境、做出决策、执行行动的系统。

在大模型语境下,Agent 通常是以大模型为 "大脑",配合各种工具和记忆系统,能够自主完成复杂任务的应用。

Agent 的核心能力

  1. 任务规划(Planning)

    把复杂任务分解成可执行的步骤:

    用户:帮我分析一下最近一周苹果股票的走势,并给出投资建议
    
    Agent 的规划:
    1. 获取苹果股票最近一周的历史数据
    2. 分析价格走势(涨跌、波动)
    3. 查找相关新闻和市场分析
    4. 综合以上信息给出投资建议
    
  2. 工具调用(Tool Use)

    选择合适的工具执行每个步骤:

    步骤 1 → 调用股票 API 获取历史数据
    步骤 2 → 调用数据分析工具计算指标
    步骤 3 → 调用搜索引擎获取新闻
    步骤 4 → 由大模型综合分析
    
  3. 执行和观察(Action & Observation)

    执行工具调用,观察结果,根据结果决定下一步:

    执行步骤 1:调用股票 API
    观察结果:获得了 7 天的 OHLC 数据
    判断:数据获取成功,进入步骤 2
    
    执行步骤 2:分析数据
    观察结果:周涨幅 +3.2%,波动率较低
    判断:分析完成,进入步骤 3
    ...
    
  4. 记忆(Memory)

    记住执行过程中的信息,便于后续步骤使用:

    • 短期记忆:当前任务的上下文
    • 长期记忆:跨任务的知识积累
  5. 反思和修正(Reflection)

    如果某一步失败或结果不理想,能够调整策略重试:

    执行步骤 3:搜索新闻
    观察结果:搜索 API 返回错误
    反思:可能是关键词不够具体
    修正:调整搜索关键词为 "Apple stock AAPL news this week"
    重试步骤 3
    

Agent 的适用场景

  • 复杂的多步骤任务
  • 需要多种工具协作的场景
  • 自动化工作流
  • 智能助手/副驾驶

5.3.5 技术五:模型微调(Fine-tuning)

微调是在基础模型上进行额外训练,让模型更适合特定任务。

什么是微调

微调是用特定领域或任务的数据,在已经训练好的基础模型上继续训练,让模型 **"学会" 新的知识或能力。**

类比:

  • 基础模型 = 一个大学毕业生,有广泛的通识知识
  • 微调 = 让他接受专业培训,成为某个领域的专家

微调的类型

  • 全参数微调(Full Fine-tuning)

    对模型的所有参数都进行训练。效果最好,但成本最高。

  • LoRA / QLoRA

    只训练一小部分参数(通过低秩分解),效果接近全参数微调,成本大幅降低。

  • Prompt Tuning

    只训练一组可学习的提示词向量,模型参数不变。成本最低,但效果有限。

什么时候需要微调

  • 特定领域深度应用(如医疗、法律)
  • 需要特定的输出格式或风格
  • Prompt 工程和 RAG 效果不够
  • 需要内化大量领域知识

不需要微调的场景

  • 通用对话和问答

  • 知识可以通过 RAG 注入

  • 没有高质量的训练数据

  • 预算有限


微调的成本

项目说明
数据准备需要高质量的训练数据,通常几千到几万条
计算资源需要 GPU 服务器,7B 模型约需 24GB+ 显存
时间成本数据准备 + 训练 + 评估,可能需要数周
人力成本需要有经验的工程师

微调 vs RAG 怎么选

维度RAG微调
成本
实施难度较低较高
知识更新灵活(更新文档即可)需要重新训练
效果上限依赖检索质量可能更高
适用场景知识密集型问答特定任务/风格

一般建议:先尝试 Prompt 工程和 RAG,如果效果不满足再考虑微调。

5.4 实战建议:分层应对策略

面对具体项目,建议按这个优先级来应对大模型的局限:

第一层:Prompt 工程(零成本)

任何项目都应该首先优化 Prompt:

  • 写好 System Prompt,明确角色和约束
  • 要求模型不确定时承认不知道
  • 提供示例引导输出格式

投入:0 成本,几小时

第二层:RAG(低成本)

如果涉及私有知识或需要引用来源:

  • 搭建向量数据库
  • 实现检索 Pipeline
  • 优化检索策略

投入:几天开发,云服务费用

第三层:Function Call(中等成本)

如果需要精确计算、实时数据、执行操作:

  • 设计工具集
  • 实现工具调用流程
  • 处理错误和边界情况

投入:一两周开发,API 费用

第四层:Agent(较高成本)

如果任务复杂、需要多步骤自动化:

  • 设计 Agent 架构
  • 实现规划、执行、记忆模块
  • 大量测试和调优

投入:数周开发,持续优化

第五层:微调(高成本)

只有当以上方案都不满足需求,且有充足预算和数据时:

  • 准备高质量训练数据
  • 执行微调训练
  • 评估和迭代

投入:数周到数月,计算资源费用

大多数应用场景,前两三层就够了。

六,大模型调用

6.1 API平台

大模型的使用方式主要分两种:调用云端 API本地部署

6.1.1 云端 API 平台详解

平台定位与地址核心优势主要限制/注意事项适用场景
硅基流动 (SiliconFlow)国内领先的API聚合平台 siliconflow.cn模型丰富:聚合Qwen、DeepSeek、Llama等主流模型 • 协议统一:兼容OpenAI协议,换模型只需改模型名 • 价格较低:定价有竞争力 • 访问稳定:国内服务,无需特殊网络 • 新用户福利:有免费额度供学习实验作为聚合平台,最新模型的更新速度可能略晚于官方学习实验、快速原型开发、中小项目、需灵活切换多模型的场景
阿里云百炼阿里云官方平台,Qwen系列第一方 aliyun.comQwen首发:最新版Qwen最先上线 • 企业级支持:提供SLA保障、技术支持和合规认证 • 阿里生态集成:与OSS、ECS等云服务无缝对接 • 安全合规:适合对数据安全有要求的企业主要面向企业用户,可能有一定使用门槛企业级应用、需要Qwen最新版本、对服务可用性(SLA)有严格要求、已使用阿里云生态
DeepSeek 官方DeepSeek官方API服务 deepseek.com价格最低:官方直连,性价比高 • 模型齐全:提供V3、R1、Coder等全系列 • 性能保障:直接对接官方服务器集群模型选择单一,仅限于DeepSeek系列确定使用DeepSeek模型、对成本敏感的推理和应用场景
OpenAI全球领先的模型服务商 openai.com综合能力最强:GPT-4o仍是标杆 • 生态最完善:框架、工具支持最广 • 新功能首发:Function Calling、Vision等功能通常最先支持网络限制:国内无法直接访问 • 注册/支付门槛:需海外手机号和信用卡 • 价格较高:使用成本相对更高需要使用GPT系列最强模型、能解决网络和支付问题、紧跟最新技术前沿的场景

6.1.2 本地部署工具

Ollama

最简单、最推荐的本地部署方案。

地址:https://ollama.com/

核心优势

  • 极简部署 :一条命令安装,一条命令运行模型
  • 跨平台 :支持 Mac、Linux、Windows
  • 兼容OpenAI协议 :代码几乎不用改
  • 模型库丰富 :主流开源模型基本都有
  • 自动资源管理 :自动加载/卸载模型,管理显存

适用场景 :学习实验、离线使用、数据敏感不能出网、快速原型

6.2 OpenAI 协议详解

写代码之前,需要深入理解 OpenAI 的 Chat Completions API 协议。这是业界事实标准,几乎所有平台都兼容。

6.2.1 请求格式完整解析

{
  "model": "Qwen/Qwen3.5-plus",
  "messages": [
    {"role": "system", "content": "你是一个专业的技术顾问"},
    {"role": "user", "content": "什么是微服务架构?"},
    {"role": "assistant", "content": "微服务架构是一种将应用程序..."},
    {"role": "user", "content": "它和单体架构有什么区别?"}
  ],
  "temperature": 0.7,
  "top_p": 1.0,
  "max_tokens": 1000,
  "stream": false,
  "stop": ["\n\n"],
  "presence_penalty": 0,
  "frequency_penalty": 0
}

字段详解

字段类型必填说明
modelstring模型 ID,不同平台格式不同
messagesarray对话消息数组
temperaturefloat随机性,0-2,默认 1
top_pfloat核采样,0-1,默认 1
max_tokensint最大输出 Token 数
streambool是否流式返回,默认 false
stoparray停止生成的字符串列表
presence_penaltyfloat存在惩罚,-2 到 2
frequency_penaltyfloat频率惩罚,-2 到 2

messages 数组的角色类型

role说明用途
system系统消息定义模型的角色、行为、约束
user用户消息用户的输入、问题
assistant助手消息模型之前的回复(多轮对话时用)

6.2.2 响应格式完整解析

非流式响应

{
  "id": "chatcmpl-abc123",
  "object": "chat.completion",
  "created": 1699000000,
  "model": "Qwen/Qwen3.5-plus",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "微服务架构和单体架构的主要区别在于..."
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 156,
    "completion_tokens": 423,
    "total_tokens": 579
  }
}

关键字段

  • choices[0].message.content:模型的回答内容
  • choices[0].finish_reason:结束原因
    • stop:正常结束
    • length:达到 max_tokens 限制被截断
    • content_filter:内容过滤
  • usage:Token 使用统计

流式响应
流式响应采用 SSE(Server-Sent Events)格式,每个数据块是一行:

data: {"id":"chatcmpl-abc123","choices":[{"delta":{"content":"微"},"index":0}]}

data: {"id":"chatcmpl-abc123","choices":[{"delta":{"content":"服务"},"index":0}]}

data: {"id":"chatcmpl-abc123","choices":[{"delta":{"content":"架构"},"index":0}]}

data: [DONE]

注意:

  • 每行以 data: 开头
  • 内容在 choices[0].delta.content 里(不是 message
  • 最后一行是 data: [DONE]
  • 中间可能有空行

6.3 硅基流动 API 调用样例

6.3.1 准备工作

  1. 注册获取 API Key

  2. Maven 依赖配置

    创建一个 Maven 项目,添加以下依赖:

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
      <modelVersion>4.0.0</modelVersion>
    
      <parent>
        <groupId>org.javaup</groupId>
        <artifactId>ai-example</artifactId>
        <version>${revision}</version>
      </parent>
    
      <artifactId>ai-example-one</artifactId>
    
      <name>ai-example-one</name>
      <description>ai示例1</description>
    
      <dependencies>
        <!-- macOS DNS 原生解析库(解决 Netty 在 macOS 上的 DNS 解析警告) -->
        <dependency>
          <groupId>io.netty</groupId>
          <artifactId>netty-resolver-dns-native-macos</artifactId>
          <classifier>osx-aarch_64</classifier>
        </dependency>
        <!-- OkHttp:HTTP 客户端 -->
        <dependency>
          <groupId>com.squareup.okhttp3</groupId>
          <artifactId>okhttp</artifactId>
          <version>4.12.0</version>
        </dependency>
    
        <!-- OkHttp SSE 支持(流式调用需要)-->
        <dependency>
          <groupId>com.squareup.okhttp3</groupId>
          <artifactId>okhttp-sse</artifactId>
          <version>4.12.0</version>
        </dependency>
    
        <!-- Gson:JSON 处理 -->
        <dependency>
          <groupId>com.google.code.gson</groupId>
          <artifactId>gson</artifactId>
          <version>2.10.1</version>
        </dependency>
    
        <!-- SLF4J + Logback:日志 -->
        <dependency>
          <groupId>ch.qos.logback</groupId>
          <artifactId>logback-classic</artifactId>
          <version>1.4.14</version>
        </dependency>
      </dependencies>
    </project>
    

6.3.2 非流式调用代码示例

非流式调用最直观:发请求 → 等待 → 拿到完整结果。

package com.normaling.aidemo;

/**
 * 大模型 API 非流式调用示例
 * 演示如何调用硅基流动的 API 进行对话
 */
public class LLMClient {

    private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");

    /**
     * API 配置
     * */
    private static final String API_URL = "https://api.siliconflow.cn/v1/chat/completions";
    /**
     * 替换成你的 API Key
     * */
    private static final String API_KEY = "设置成你的apiKey";

    private static final String MODEL = "Qwen/Qwen3.5-122B-A10B";
    /**
     * 硅基流动文档中该模型支持 thinking 模式,默认开启时非流式调用可能等待较久。
     * 这里示例默认关闭,以减少直接运行 demo 时的超时概率。
     */
    private static final boolean ENABLE_THINKING = false;
    private static final int MAX_TOKENS = 512;
    private static final int CONNECT_TIMEOUT_SECONDS = 30;
    private static final int READ_TIMEOUT_SECONDS = 180;
    private static final int WRITE_TIMEOUT_SECONDS = 30;
    private static final int CALL_TIMEOUT_SECONDS = 180;

    /**
     * HTTP 客户端(复用以提高性能)
     * */
    private final OkHttpClient httpClient;
    private final Gson gson;

    public LLMClient() {
        // 配置 HTTP 客户端
        this.httpClient = new OkHttpClient.Builder()
                .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
                .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
                .writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
                .callTimeout(CALL_TIMEOUT_SECONDS, TimeUnit.SECONDS)
                .retryOnConnectionFailure(true)
                .build();

        this.gson = new GsonBuilder()
                .setPrettyPrinting()
                .create();
    }

    /**
     * 发送对话请求
     *
     * @param systemPrompt 系统提示词,定义模型角色
     * @param userMessage  用户消息
     * @return 模型的回复
     */
    public String chat(String systemPrompt, String userMessage) throws IOException {
        // 1. 构建请求体
        JsonObject requestBody = new JsonObject();
        requestBody.addProperty("model", MODEL);
        requestBody.addProperty("temperature", 0.7);
        requestBody.addProperty("max_tokens", MAX_TOKENS);
        requestBody.addProperty("stream", false);
        requestBody.addProperty("enable_thinking", ENABLE_THINKING);

        // 构建 messages 数组
        JsonArray messages = new JsonArray();

        // 添加 system 消息
        if (systemPrompt != null && !systemPrompt.isEmpty()) {
            JsonObject systemMsg = new JsonObject();
            systemMsg.addProperty("role", "system");
            systemMsg.addProperty("content", systemPrompt);
            messages.add(systemMsg);
        }

        // 添加 user 消息
        JsonObject userMsg = new JsonObject();
        userMsg.addProperty("role", "user");
        userMsg.addProperty("content", userMessage);
        messages.add(userMsg);

        requestBody.add("messages", messages);

        // 2. 构建 HTTP 请求
        Request request = new Request.Builder()
                .url(API_URL)
                .addHeader("Authorization", "Bearer " + API_KEY)
                .addHeader("Content-Type", "application/json")
                .post(RequestBody.create(
                        requestBody.toString(),
                        JSON
                ))
                .build();

        // 3. 发送请求并处理响应
        long requestStart = System.nanoTime();
        try (Response response = httpClient.newCall(request).execute()) {
            String traceId = response.header("x-siliconcloud-trace-id");
            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - requestStart);

            // 检查响应状态
            if (!response.isSuccessful()) {
                String errorBody = response.body() != null ? response.body().string() : "No error body";
                throw new IOException(String.format(
                        "API 请求失败: %d, traceId=%s, body=%s",
                        response.code(),
                        traceId != null ? traceId : "N/A",
                        errorBody
                ));
            }

            // 解析响应 JSON
            if (response.body() == null) {
                throw new IOException("API 响应体为空");
            }
            String responseBody = response.body().string();
            JsonObject json = gson.fromJson(responseBody, JsonObject.class);

            // 提取回答内容
            JsonArray choices = json.getAsJsonArray("choices");
            if (choices == null || choices.size() == 0) {
                throw new IOException("响应中没有 choices");
            }

            if (traceId != null) {
                System.out.printf("[请求耗时] %d ms, traceId=%s%n", elapsedMs, traceId);
            } else {
                System.out.printf("[请求耗时] %d ms%n", elapsedMs);
            }

            String answer = choices.get(0).getAsJsonObject()
                    .getAsJsonObject("message")
                    .get("content").getAsString();

            // 打印 Token 使用情况
            if (json.has("usage")) {
                JsonObject usage = json.getAsJsonObject("usage");
                System.out.printf("[Token 使用] 输入: %d, 输出: %d, 总计: %d%n",
                        usage.get("prompt_tokens").getAsInt(),
                        usage.get("completion_tokens").getAsInt(),
                        usage.get("total_tokens").getAsInt());
            }

            if (choices.get(0).getAsJsonObject().has("finish_reason")) {
                String finishReason = choices.get(0).getAsJsonObject()
                        .get("finish_reason").getAsString();
                if ("length".equals(finishReason)) {
                    System.out.printf("[提示] 输出触发 max_tokens=%d 上限,如需更完整回答可调大该值,同时建议同步调大 readTimeout/callTimeout。%n",
                            MAX_TOKENS);
                }
            }

            return answer;
        } catch (SocketTimeoutException e) {
            throw new SocketTimeoutException(String.format(
                    "调用硅基流动超时(%d 秒)。当前示例已默认关闭 enable_thinking;如果你改回 true 或更换为更慢的推理模型,建议使用 stream=true,或继续增大 readTimeout/callTimeout。原始错误: %s",
                    CALL_TIMEOUT_SECONDS,
                    e.getMessage()
            ));
        }
    }

    public static void main(String[] args) {
        LLMClient client = new LLMClient();

        String systemPrompt = """
            你是一个专业的 Java 技术顾问,擅长解答 Java 相关的技术问题。
            回答要准确、简洁,如果涉及代码,请给出示例。
            如果不确定答案,请明确说明。
            """;

        String userMessage = "请解释一下 Java 中的 volatile 关键字有什么作用?";

        try {
            System.out.println("发送问题: " + userMessage);
            System.out.printf("等待回复... [model=%s, enableThinking=%s]%n%n", MODEL, ENABLE_THINKING);

            String answer = client.chat(systemPrompt, userMessage);

            System.out.println("=== 模型回答 ===");
            System.out.println(answer);
        } catch (IOException e) {
            System.err.println("调用失败: " + e.getMessage());
        }
    }
}

运行结果示例

发送问题: 请解释一下 Java 中的 volatile 关键字有什么作用?
等待回复... [model=Qwen/Qwen3.5-122B-A10B, enableThinking=false]

[请求耗时] 61716 ms, traceId=ti_gowxb2k8nuoapfql24
[Token 使用] 输入: 67, 输出: 512, 总计: 579
[提示] 输出触发 max_tokens=512 上限,如需更完整回答可调大该值,同时建议同步调大 readTimeout/callTimeout。
=== 模型回答 ===
`volatile` 是 Java 中用于**保证变量的可见性** 和**禁止指令重排序** 的关键字,但它**不保证原子性** 。

### 核心作用

1. **保证可见性(Visibility)** 
   - 当一个线程修改了 `volatile` 变量,新值会立即刷新到主内存中。
   - 其他线程读取该变量时,会直接从主内存中获取最新值,而不是使用 CPU 缓存中的旧值。
   - 解决了多线程环境下,线程工作内存(CPU缓存)与主内存数据不一致的问题。

2. **禁止指令重排序(Ordering)** 
   - 编译器和处理器为了优化性能,可能会重排指令执行顺序。
   - `volatile` 通过插入**内存屏障(MemoryBarrier)** ,禁止重排序操作,确保代码按照程序规定的顺序执行。
   - 典型应用是**双重检查锁(DCL)单例模式** 中的 `instance` 变量。

### 关键限制:不保证原子性

`volatile` 只能保证“读”和“写”操作本身的可见性,无法保证复合操作的原子性(例如 `i++`)。

6.3.3 流式调用代码示例

流式调用可以让用户实时看到模型的输出,体验更好。

/**
 * 大模型 API 流式调用示例
 * 实现类似 ChatGPT 的打字机效果
 */
public class LLMStreamClient {

    private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
    private static final String API_URL = "https://api.siliconflow.cn/v1/chat/completions";
    private static final String API_KEY = "替换成你的apiKey";
    private static final String MODEL = "Qwen/Qwen3.5-122B-A10B";
    /**
     * 该模型支持 thinking 模式;流式返回时 reasoning_content 可能出现,而 content 为 null。
     * 示例默认关闭 thinking,避免首次体验时既慢又需要额外解析思考流。
     */
    private static final boolean ENABLE_THINKING = false;
    private static final int MAX_TOKENS = 512;
    private static final int CONNECT_TIMEOUT_SECONDS = 30;
    private static final int READ_TIMEOUT_SECONDS = 300;
    private static final int WRITE_TIMEOUT_SECONDS = 30;
    private static final int CALL_TIMEOUT_SECONDS = 300;

    private final OkHttpClient httpClient;
    private final Gson gson;

    public LLMStreamClient() {
        this.httpClient = new OkHttpClient.Builder()
            .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .writeTimeout(WRITE_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .callTimeout(CALL_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .build();

        this.gson = new Gson();
    }

    /**
     * 回调接口,用于处理流式数据
     */
    public interface StreamCallback {
        /**
         * 收到新的文本片段
         */
        void onContent(String content);

        /**
         * 流式传输完成
         */
        void onComplete(String fullContent);

        /**
         * 发生错误
         */
        void onError(Exception e);
    }

    /**
     * 流式对话
     */
    public void chatStream(String systemPrompt, String userMessage, StreamCallback callback) {
        // 1. 构建请求体
        JsonObject requestBody = new JsonObject();
        requestBody.addProperty("model", MODEL);
        requestBody.addProperty("temperature", 0.7);
        requestBody.addProperty("max_tokens", MAX_TOKENS);
        requestBody.addProperty("stream", true);
        requestBody.addProperty("enable_thinking", ENABLE_THINKING);

        JsonArray messages = new JsonArray();

        if (systemPrompt != null && !systemPrompt.isEmpty()) {
            JsonObject systemMsg = new JsonObject();
            systemMsg.addProperty("role", "system");
            systemMsg.addProperty("content", systemPrompt);
            messages.add(systemMsg);
        }

        JsonObject userMsg = new JsonObject();
        userMsg.addProperty("role", "user");
        userMsg.addProperty("content", userMessage);
        messages.add(userMsg);

        requestBody.add("messages", messages);

        // 2. 构建请求
        Request request = new Request.Builder()
            .url(API_URL)
            .addHeader("Authorization", "Bearer " + API_KEY)
            .addHeader("Content-Type", "application/json")
            .addHeader("Accept", "text/event-stream")
            .post(RequestBody.create(
                requestBody.toString(),
                JSON
            ))
            .build();

        // 3. 异步执行请求
        httpClient.newCall(request).enqueue(new Callback() {
            @Override
            public void onFailure(Call call, IOException e) {
                callback.onError(e);
            }

            @Override
            public void onResponse(Call call, Response response) {
                long requestStart = System.nanoTime();
                try (response) {
                    String traceId = response.header("x-siliconcloud-trace-id");
                    if (!response.isSuccessful()) {
                        String errorBody = response.body() != null ? response.body().string() : "No error body";
                        callback.onError(new IOException(String.format(
                            "API 请求失败: %d, traceId=%s, body=%s",
                            response.code(),
                            traceId != null ? traceId : "N/A",
                            errorBody
                        )));
                        return;
                    }

                    if (response.body() == null) {
                        callback.onError(new IOException("API 响应体为空"));
                        return;
                    }

                    StringBuilder fullContent = new StringBuilder();
                    String finishReason = null;

                    try (BufferedReader reader = new BufferedReader(
                            new InputStreamReader(response.body().byteStream()))) {

                        String line;
                        while ((line = reader.readLine()) != null) {
                            if (line.isBlank() || !line.startsWith("data:")) {
                                continue;
                            }

                            String data = line.substring(5).trim();
                            if ("[DONE]".equals(data)) {
                                break;
                            }

                            try {
                                JsonObject chunk = gson.fromJson(data, JsonObject.class);
                                JsonArray choices = chunk.getAsJsonArray("choices");
                                if (choices == null || choices.size() == 0) {
                                    continue;
                                }

                                JsonObject choice = choices.get(0).getAsJsonObject();
                                JsonObject delta = getAsJsonObject(choice, "delta");
                                if (delta == null) {
                                    continue;
                                }

                                String content = getNullableString(delta, "content");
                                if (content != null && !content.isEmpty()) {
                                    fullContent.append(content);
                                    callback.onContent(content);
                                }

                                String chunkFinishReason = getNullableString(choice, "finish_reason");
                                if (chunkFinishReason != null) {
                                    finishReason = chunkFinishReason;
                                }
                            } catch (JsonParseException | IllegalStateException e) {
                                // SSE 中可能夹杂非 JSON 行,或字段结构与普通 completion 不同,跳过即可。
                            }
                        }

                        long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - requestStart);
                        System.out.printf("%n[流式请求耗时] %d ms, traceId=%s%n",
                            elapsedMs,
                            traceId != null ? traceId : "N/A");
                        if ("length".equals(finishReason)) {
                            System.out.printf("[提示] 输出触发 max_tokens=%d 上限,如需更完整回答可调大该值。%n",
                                MAX_TOKENS);
                        }
                        callback.onComplete(fullContent.toString());
                    } catch (SocketTimeoutException e) {
                        callback.onError(new SocketTimeoutException(String.format(
                            "流式调用超时(%d 秒)。如果切换回 thinking 模式或增大 max_tokens,建议同步调大 readTimeout/callTimeout。原始错误: %s",
                            CALL_TIMEOUT_SECONDS,
                            e.getMessage()
                        )));
                    }
                } catch (IOException e) {
                    callback.onError(e);
                } catch (RuntimeException e) {
                    callback.onError(new IOException("解析流式响应失败: " + e.getMessage(), e));
                }
            }
        });
    }

    private static JsonObject getAsJsonObject(JsonObject parent, String memberName) {
        if (parent == null || !parent.has(memberName)) {
            return null;
        }
        JsonElement element = parent.get(memberName);
        if (element == null || element.isJsonNull() || !element.isJsonObject()) {
            return null;
        }
        return element.getAsJsonObject();
    }

    private static String getNullableString(JsonObject parent, String memberName) {
        if (parent == null || !parent.has(memberName)) {
            return null;
        }
        JsonElement element = parent.get(memberName);
        if (element == null || element.isJsonNull()) {
            return null;
        }
        return element.getAsString();
    }

    public static void main(String[] args) throws InterruptedException {
        LLMStreamClient client = new LLMStreamClient();

        String systemPrompt = "你是一个编程助手,用简洁清晰的方式解答问题。";
        String userMessage = "用 Java 写一个单例模式的示例";

        System.out.println("发送问题: " + userMessage);
        System.out.printf("模型: %s, enableThinking=%s%n", MODEL, ENABLE_THINKING);
        System.out.println("\n=== 模型回答(流式)===\n");

        // 用于等待异步调用完成
        Object lock = new Object();

        client.chatStream(systemPrompt, userMessage, new LLMStreamClient.StreamCallback() {
            @Override
            public void onContent(String content) {
                // 实时打印收到的内容(打字机效果)
                System.out.print(content);
                System.out.flush();
            }

            @Override
            public void onComplete(String fullContent) {
                System.out.println("\n\n=== 回答完成 ===");
                System.out.println("总长度: " + fullContent.length() + " 字符");
                
                synchronized (lock) {
                    lock.notify();
                }
            }

            @Override
            public void onError(Exception e) {
                System.err.println("\n发生错误: " + e.getMessage());
                
                synchronized (lock) {
                    lock.notify();
                }
            }
        });

        // 等待异步调用完成
        synchronized (lock) {
            // 最多等待 5 分钟
            lock.wait(300000);  
        }
    }
}

运行效果

你会看到文字一个一个地 "打" 出来,而不是等很久后突然出现一大段。这就是 ChatGPT 那种打字机效果。

6.3.4 多轮对话实现

真实的聊天应用需要支持多轮对话。核心是把历史对话都带在请求里

public class ConversationClient {

    private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
    private static final String API_URL = "https://api.siliconflow.cn/v1/chat/completions";
    private static final String API_KEY = "设置你的apiKey";
    private static final String MODEL = "Qwen/Qwen3.5-122B-A10B";
    private static final boolean ENABLE_THINKING = false;
    private static final int MAX_TOKENS = 256;
    private static final int CONNECT_TIMEOUT_SECONDS = 30;
    private static final int READ_TIMEOUT_SECONDS = 180;
    private static final int CALL_TIMEOUT_SECONDS = 180;

    /**
     * 最大历史消息数(防止上下文过长)
     * */
    private static final int MAX_HISTORY_SIZE = 20;

    private final OkHttpClient httpClient;
    private final Gson gson;
    private final String systemPrompt;
    private final List<Message> conversationHistory;

    /**
     * 消息类
     */
    public static class Message {
        public String role;
        public String content;

        public Message(String role, String content) {
            this.role = role;
            this.content = content;
        }
    }

    public ConversationClient(String systemPrompt) {
        this.httpClient = new OkHttpClient.Builder()
            .connectTimeout(CONNECT_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .readTimeout(READ_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .callTimeout(CALL_TIMEOUT_SECONDS, TimeUnit.SECONDS)
            .retryOnConnectionFailure(true)
            .build();

        this.gson = new Gson();
        this.systemPrompt = systemPrompt;
        this.conversationHistory = new ArrayList<>();
    }

    /**
     * 发送消息并获取回复
     */
    public String send(String userMessage) throws IOException {
        List<Message> requestMessages = buildRequestMessages(userMessage);

        // 1. 构建请求
        JsonObject requestBody = new JsonObject();
        requestBody.addProperty("model", MODEL);
        requestBody.addProperty("temperature", 0.7);
        requestBody.addProperty("max_tokens", MAX_TOKENS);
        requestBody.addProperty("stream", false);
        requestBody.addProperty("enable_thinking", ENABLE_THINKING);

        // 构建 messages:system + 历史对话
        JsonArray messages = new JsonArray();

        // 添加 system 消息
        JsonObject systemMsg = new JsonObject();
        systemMsg.addProperty("role", "system");
        systemMsg.addProperty("content", systemPrompt);
        messages.add(systemMsg);

        // 添加历史对话(最近的 MAX_HISTORY_SIZE 条)
        for (Message msg : requestMessages) {
            JsonObject msgJson = new JsonObject();
            msgJson.addProperty("role", msg.role);
            msgJson.addProperty("content", msg.content);
            messages.add(msgJson);
        }

        requestBody.add("messages", messages);

        // 3. 发送请求
        Request request = new Request.Builder()
            .url(API_URL)
            .addHeader("Authorization", "Bearer " + API_KEY)
            .addHeader("Content-Type", "application/json")
            .post(RequestBody.create(
                requestBody.toString(),
                JSON
            ))
            .build();

        long requestStart = System.nanoTime();
        try (Response response = httpClient.newCall(request).execute()) {
            String traceId = response.header("x-siliconcloud-trace-id");
            if (!response.isSuccessful()) {
                String errorBody = response.body() != null ? response.body().string() : "No error body";
                throw new IOException(String.format(
                    "请求失败: %d, traceId=%s, body=%s",
                    response.code(),
                    traceId != null ? traceId : "N/A",
                    errorBody
                ));
            }

            if (response.body() == null) {
                throw new IOException("API 响应体为空");
            }

            String body = response.body().string();
            JsonObject json = gson.fromJson(body, JsonObject.class);

            JsonArray choices = json.getAsJsonArray("choices");
            if (choices == null || choices.size() == 0) {
                throw new IOException("响应中没有 choices");
            }

            String answer = json.getAsJsonArray("choices")
                .get(0).getAsJsonObject()
                .getAsJsonObject("message")
                .get("content").getAsString();

            // 4. 请求成功后再提交本轮历史,避免失败重试造成重复上下文
            conversationHistory.add(new Message("user", userMessage));
            conversationHistory.add(new Message("assistant", answer));

            long elapsedMs = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - requestStart);
            System.out.printf("[请求耗时] %d ms, traceId=%s%n",
                elapsedMs,
                traceId != null ? traceId : "N/A");

            // 打印 Token 使用情况
            if (json.has("usage")) {
                JsonObject usage = json.getAsJsonObject("usage");
                System.out.printf("[Token] 本轮: %d, 历史消息数: %d%n",
                    usage.get("total_tokens").getAsInt(),
                    conversationHistory.size());
            }

            String finishReason = getNullableString(choices.get(0).getAsJsonObject(), "finish_reason");
            if ("length".equals(finishReason)) {
                System.out.printf("[提示] 输出触发 max_tokens=%d 上限,如需更完整回答可调大该值,同时建议同步调大 readTimeout/callTimeout。%n",
                    MAX_TOKENS);
            }

            return answer;
        } catch (SocketTimeoutException e) {
            throw new SocketTimeoutException(String.format(
                "多轮对话调用超时(%d 秒)。当前示例已默认关闭 enable_thinking;如果改回 true 或调大 max_tokens,建议同步调大 readTimeout/callTimeout,或直接改成流式输出。原始错误: %s",
                CALL_TIMEOUT_SECONDS,
                e.getMessage()
            ));
        }
    }

    private List<Message> buildRequestMessages(String userMessage) {
        int startIndex = Math.max(0, conversationHistory.size() - MAX_HISTORY_SIZE);
        List<Message> requestMessages = new ArrayList<>(conversationHistory.subList(startIndex, conversationHistory.size()));
        requestMessages.add(new Message("user", userMessage));
        return requestMessages;
    }

    private static String getNullableString(JsonObject parent, String memberName) {
        if (parent == null || !parent.has(memberName)) {
            return null;
        }
        JsonElement element = parent.get(memberName);
        if (element == null || element.isJsonNull()) {
            return null;
        }
        return element.getAsString();
    }

    /**
     * 清空对话历史
     */
    public void clearHistory() {
        conversationHistory.clear();
        System.out.println("对话历史已清空");
    }

    /**
     * 获取对话历史
     */
    public List<Message> getHistory() {
        return new ArrayList<>(conversationHistory);
    }

    public static void main(String[] args) throws IOException {
        String systemPrompt = """
            你是一个友好的编程助手。
            记住用户之前说过的话,在对话中保持连贯性。
            回答要简洁明了。
            """;

        ConversationClient client = new ConversationClient(systemPrompt);
        Scanner scanner = new Scanner(System.in);

        System.out.println("=== 多轮对话演示 ===");
        System.out.println("输入 'quit' 退出,输入 'clear' 清空历史\n");

        while (true) {
            System.out.print("你: ");
            String input = scanner.nextLine().trim();

            if ("quit".equalsIgnoreCase(input)) {
                System.out.println("再见!");
                break;
            }

            if ("clear".equalsIgnoreCase(input)) {
                client.clearHistory();
                continue;
            }

            if (input.isEmpty()) {
                continue;
            }

            try {
                System.out.printf("助手: 正在思考... [model=%s, enableThinking=%s]%n",
                    MODEL,
                    ENABLE_THINKING);
                String answer = client.send(input);
                System.out.println("\n助手: " + answer + "\n");
            } catch (IOException e) {
                System.err.println("发生错误: " + e.getMessage());
            }
        }

        scanner.close();
    }
}

对话示例

=== 多轮对话演示 ===
输入 'quit' 退出,输入 'clear' 清空历史

你: 你好,我叫小明
助手: 正在思考... [model=Qwen/Qwen3.5-122B-A10B, enableThinking=false]
[请求耗时] 2513 ms, traceId=ti_9e853c5mjbmufqudfl
[Token] 本轮: 64, 历史消息数: 2

助手: 你好,小明!很高兴认识你。有什么编程问题需要我帮忙吗?

你: 我想学习 Spring Boot
助手: 正在思考... [model=Qwen/Qwen3.5-122B-A10B, enableThinking=false]
[请求耗时] 13881 ms, traceId=ti_sxx4ah96dm296l9lai
[Token] 本轮: 194, 历史消息数: 4

助手: 太棒了,Spring Boot 是 Java 开发中最流行的框架之一,上手快且功能强大!

我们可以从以下几个步骤开始:
1. **基础准备** :确保你熟悉 Java 基础(如注解、接口)和 Maven/Gradle。
2. **第一个项目** :用 Spring Initializr 快速创建一个"Hello World"项目。
3. **核心概念** :了解自动配置、依赖注入和 RESTful API 开发。

你想先了解**如何创建第一个项目** ,还是想直接聊聊**核心概念** ?

你: 从基础开始吧
助手: 正在思考... [model=Qwen/Qwen3.5-122B-A10B, enableThinking=false]
[请求耗时] 23357 ms, traceId=ti_o2vojopycyiyp7bk8e
[Token] 本轮: 408, 历史消息数: 6

助手: 没问题,小明!我们一步步来。

首先,**创建第一个项目** 是最快的上手方式:

1.  **访问官网工具** :打开 [Spring Initializr](https://start.spring.io)。
2.  **填写配置** :
    *   **Project** : Maven
    *   **Language** : Java
    *   **SpringBoot** : 选最新稳定版(如 3.x)
    *   **Dependencies** : 点击"Add dependencies",搜索并添加 **SpringWeb** (这是开发网页和 API 的核心)。
3.  **生成代码** :点击"Generate"下载压缩包,解压后用 IDE(如 IntelliJ IDEA 或 VS Code)打开。

**下一步** :
项目里通常会有一个 `DemoApplication.java` 文件和一个 `Controller.java`(或者你需要自己新建一个)。

你需要我演示一下**如何写第一个"HelloWorld"接口** ,让你能在浏览器里看到结果吗?

6.4 Ollama 本地部署

Ollama 是在本地运行大模型的最简单方案。

6.4.1 安装 Ollama

方式一:官网下载(推荐)

访问 ollama.com/download,下载对应系统的安装包:

  • Mac:下载 .dmg 文件,拖到应用程序文件夹
  • Windows:下载 .exe 安装程序
  • Linux:运行安装脚本

方式二:命令行安装(Mac/Linux)

curl -fsSL https://ollama.com/install.sh | sh

方式三:Docker 安装

# 基础安装(仅 CPU)
docker run -d -p 11434:11434 --name ollama ollama/ollama

# 启用 GPU(需要 nvidia-container-toolkit)
docker run -d --gpus=all -p 11434:11434 --name ollama ollama/ollama

验证安装

ollama --version
# 输出类似:ollama version 0.1.xxx

6.4.2 下载和运行模型

Ollama 的模型库在 ollama.com/library

拉取模型

# 拉取 Qwen 2.5 7B 模型(约 4.5GB)
ollama pull qwen2.5:7b

# 拉取更小的模型(适合内存有限的情况)
ollama pull qwen2.5:3b

# 拉取更大的模型(效果更好)
ollama pull qwen2.5:14b

直接运行(交互模式)

ollama run qwen2.5:7b

进入交互模式后直接对话:

>>> 你好,介绍一下你自己
我是通义千问,一个由阿里云开发的大语言模型...

>>> 用 Python 写个快速排序
好的,这是一个 Python 实现的快速排序算法:

def quicksort(arr):
    if len(arr) <= 1:
        return arr
    pivot = arr[len(arr) // 2]
    left = [x for x in arr if x < pivot]
    middle = [x for x in arr if x == pivot]
    right = [x for x in arr if x > pivot]
    return quicksort(left) + middle + quicksort(right)

>>> /bye  (退出)

常用命令

# 查看已下载的模型
ollama list

# 查看模型信息
ollama show qwen2.5:7b

# 删除模型
ollama rm qwen2.5:7b

# 停止 Ollama 服务
ollama stop

6.4.3 通过 API 调用本地模型

Ollama 启动后会在 localhost:11434 提供 API 服务,兼容 OpenAI 协议

命令行测试

curl http://localhost:11434/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "qwen2.5:7b",
    "messages": [
      {"role": "user", "content": "你好"}
    ]
  }'

Java 代码调用
只需要把之前代码中的 API_URL 改成本地地址:

// 云端调用
private static final String API_URL = "https://api.siliconflow.cn/v1/chat/completions";
private static final String API_KEY = "YOUR_API_KEY";

// 改成本地调用
private static final String API_URL = "http://localhost:11434/v1/chat/completions";
private static final String API_KEY = "ollama";  // 随便填,本地不校验

其他代码完全不用改!这就是兼容 OpenAI 协议的好处。

项目学习目录 2026-05-25
提示词工程 2026-05-28

© 2026 苏叶的belog