一,技术迭代背景
传统编码时代 :规则驱动
这是最早期的方法。想让计算机处理语言,就由语言学家和工程师手工编写大量规则,形成专家系统。
- 特点:如果输入
A,就执行B。 - 局限:语言是无穷无尽的,充满歧义和例外。规则的维护成本极高,根本无法覆盖真实世界的千变万化,很快达到了天花板。
NLP 时代: 让机器理解语言;这标志着从“规则驱动”向“数据驱动”的转变,分为三个关键阶段:
- 统计NLP:核心思想是不再死记规则,而是用概率建模。
-
语言模型初现:一个句子是否合理,用概率
P(今天天气真好) > P(今天天气很坏)来判断。 -
核心瓶颈:严重依赖人工设计的“特征工程”,且无法真正理解语义。比如“苹果很好吃”和“苹果很好用”,对统计模型来说,“苹果”只是一个词。
-
- 词嵌入:词的数字化革命;这是里程碑式的突破,解决了**“如何让计算机理解词义”**的问题。
-
核心思想:用一个稠密的实数向量(比如
[0.2, -0.5, 0.8, ...])来表示一个词。 -
神奇效果:语义相近的词,其向量在空间中的距离也近。它甚至能学到语义关系:
国王 - 男人 + 女人 ≈ 女王。 -
代表模型:Word2Vec、GloVe。
-
巨大局限:给每个词一个固定的向量,无法处理一词多义。同一个“苹果”,在水果和手机两个语境下,向量完全相同。
-
- 预训练语言模型:一词多义的终结;这个阶段开始训练深层模型,并让词义随上下文动态改变。
-
代表模型:ELMo, GPT-1, BERT。
-
核心突破:不再是“查字典”式地找一个固定向量,而是让模型读完整个句子后,为当前词动态生成一个向量。至此,水果“苹果”和科技“苹果”终于有了不同的表示。
-
关键范式:“预训练 + 微调”。先在海量数据上训练一个通用模型,再针对具体任务(如情感分析)进行小范围调整,效果远超从前。
神经网络时代:Transformer 一统天下
- 革命性架构:Transformer 的自注意力机制,让模型处理一个词时,能直接看到并权衡句子中所有词与它的关系,高效解决长距离依赖。
- 并行计算能力:相比RNN(循环神经网络)必须按顺序一步步处理,Transformer能同时处理整个序列,使得在GPU上训练海量数据成为可能。
大模型时代 : 量变到质变;当 Transformer 架构与不断堆叠的规模(参数、数据、算力)相遇,涌现能力发生了。
- GPT-3 的飞跃:它证明了,当模型大到一定程度,不需要任何微调,只需在提示词里给它几个例子(上下文学习),它就能完成翻译、写作等全新任务。这与第二阶段“每个任务都要精调一个模型”的范式有了本质不同。
- ChatGPT 的质变:它在上一步基础上,通过指令微调学会了“理解指令”,通过人类反馈强化学习让回答更符合人类偏好。这让模型从“能力强大的文字接龙工具”,进化成了“与人类价值观对齐的实用助手”
二,工作原理
2.1 大模型的核心任务:预测下一个词
本质就是一个猜词游戏
如果用一句话概括大模型在做什么,那就是:给它一串文字,它来猜下一个最可能出现的词是什么。
举个例子,输入 "今天的天气真",大模型的任务是猜下一个词。它内部会计算所有候选词的概率:
"好" → 概率 62%
"不错" → 概率 18%
"差" → 概率 8%
"热" → 概率 5%
"糟糕" → 概率 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(值):每本书的内容——真正有用的信息
在自注意力中:
- 每个词都会生成自己的 Q、K、V 三个向量
- 某个词的 Q 会和所有词的 K 做匹配,计算相关性分数
- 根据分数对所有词的 V 做加权求和,得到最终的输出
用公式表示(简化版):
注意力分数 = softmax(Q × K的转置 / √d)
输出 = 注意力分数 × V
其中 √d 是一个缩放因子,防止数值过大。softmax 把分数转换成概率(总和为 1)。
为什么自注意力这么强大
自注意力有几个关键优势:
-
能处理长距离依赖
在 RNN 中,如果两个相关的词隔得很远,中间的信息会逐渐丢失。但在自注意力中,任意两个词之间都有直接的连接,不管它们隔多远。
比如:"小明昨天在超市买了一个苹果,回家后他把苹果洗干净吃了。"
最后的 "苹果" 要和开头的 "苹果" 联系起来,在 RNN 中可能因为距离太远而丢失关联。但在自注意力中,它们可以直接建立联系。
-
可以并行计算
因为是同时处理所有词,不需要按顺序,所以可以充分利用 GPU 的并行能力。这让训练大规模模型成为可能。
-
关系建模灵活
每个词都会和所有词建立联系,这种灵活性让模型能学到各种复杂的语言模式。
2.3.5 多头注意力:从不同角度看问题
在实际的 Transformer 中,不是只做一次自注意力,而是同时做多次,这叫多头注意力(Multi-Head Attention)。
为什么需要多头
词和词之间的关系是多维度的。一次注意力可能只能捕捉到一种关系,但语言的复杂性需要同时捕捉多种关系。
比如这个句子:"程序员小王用 Python 写了一个爬虫程序。"
从不同角度看,词之间有不同的关系:
- 语法角度:"小王"是主语,"写"是谓语,"程序"是宾语
- 职业角度:"程序员"和"Python"、"爬虫程序"相关
- 工具角度:"用"连接了"Python"和"写"
- 指代角度:如果后文有"他",应该指"小王"
多头注意力就是让模型同时从多个角度分析词之间的关系。每个 "头" 专注于一种关系,最后把所有头的结果综合起来。
具体是怎么做的
假设我们用 8 个头(这是常见的设置):
- 把原始的 Q、K、V 向量分成 8 份
- 每份独立做一次自注意力计算
- 把 8 个头的输出拼接起来
- 通过一个线性变换,得到最终输出
多头的实际效果
研究者发现,不同的头确实学会了关注不同的模式:
- 有的头关注语法结构(主谓宾关系)
- 有的头关注位置关系(相邻的词)
- 有的头关注语义相似性(同类词)
- 有的头关注指代关系(代词指向)
这种分工让模型的理解更加全面和深入。
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) 两部分组成:

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 World | Hello / ␣World |
| programming | program / ming |
| ChatGPT | Chat / G / PT |
| 人工智能 | 人工 / 智能 |
| 今天天气不错 | 今天 / 天气 / 不错 |
| function | function |
| beautiful | beaut / 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,字节对编码)。它的基本思想是:
- 从最小单位(字符或字节)开始
- 统计训练语料中哪两个相邻单位最常一起出现
- 把最常见的组合合并成一个新单位
- 重复步骤 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 s 和 s t 出现次数最多。假设我们先合并 e s → es:
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 步:重复直到达到目标词表大小
继续合并高频对:
lo→low→low</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%+
实践建议:
- 不要因为有大上下文就一股脑塞满。精选最相关的内容,控制在合理的长度
- 把最重要的信息放在开头或结尾
- 如果必须提供很长的上下文,可以在最后重复强调关键点
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.2 | JSON 等结构化输出需要确定性 |
| 翻译 | 0.3 - 0.5 | 需要准确但也要自然流畅 |
| 问答/RAG | 0.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-P 和 Top-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% 的词中选
)
一般建议:
- 如果只调一个,优先用 Temperature(最直观)或 Top-P(更灵活)
- 不建议同时调 Temperature 和 Top-P,容易产生意想不到的效果
- OpenAI 官方建议:调整其中一个时,把另一个设为默认值(Temperature=1 或 Top-P=1)
3.5 MoE
3.5.1 什么是 MoE
MoE(Mixture of Experts,混合专家) 是一种模型架构,核心思想是:不是每次推理都使用全部参数,而是动态选择部分 "专家" 来处理当前输入。
打个比方。传统的大模型像一个综合医院的全员会诊。不管病人来看什么病,所有科室的医生都要参与。感冒患者?内科、外科、骨科、眼科……全部到场。虽然诊断可能很全面,但效率低、成本高。
MoE 模型像一个智能分诊系统。病人来了,前台先判断是什么情况,然后只叫相关科室的专家。感冒?叫呼吸内科就行了。骨折?叫骨科。不需要所有医生都参与。
3.5.2 MoE 的工作原理
MoE 模型的核心组件是:
- 多个专家(Experts):每个专家是一个独立的小型神经网络(通常是前馈网络 FFN)
- 门控网络(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 8x7B | 46.7B | 12.9B | 8 | 2 |
| Mixtral 8x22B | 141B | 39B | 8 | 2 |
| DeepSeek-V2 | 236B | 21B | 160 | 6 |
| DeepSeek-V3 | 671B | 37B | 256 | 8 |
| 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 参数的模型:
| 精度 | 每参数字节 | 模型大小 | 运行时显存估算 |
|---|---|---|---|
| FP16 | 2 | 14 GB | ~18-20 GB |
| INT8 | 1 | 7 GB | ~10-12 GB |
| INT4 | 0.5 | 3.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_0 | 8 bit | ~50% | 几乎无损 |
| Q6_K | 6 bit | ~42% | 很小的损失 |
| Q5_K_M | 5 bit | ~35% | 小损失,推荐 |
| Q5_K_S | 5 bit | ~33% | 稍大损失 |
| Q4_K_M | 4 bit | ~28% | 明显但可接受 |
| Q4_K_S | 4 bit | ~26% | 更明显损失 |
| Q3_K_M | 3 bit | ~22% | 显著损失 |
| Q2_K | 2 bit | ~15% | 严重损失,不推荐 |
3.7.5 如何选择量化级别
选择建议:
- 显存充足:用 Q8_0 或 Q6_K,质量损失极小
- 显存一般:用 Q5_K_M(最佳平衡点),或 Q4_K_M
- 显存紧张:用 Q4_K_S 或 Q4_K_M
- 极端情况: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 Maverick | 17B 激活 / 400B 总参数 | 1M | 当前主力多模态旗舰,性能和部署成本最平衡 |
| Llama 4 Scout | 17B 激活 / 109B 总参数 | 10M | 超长上下文,单张 H100 就能运行 |
| Llama 4 Behemoth(预告) | 288B 激活 / 2T 总参数 | - | 教师模型,Meta 主要用于蒸馏和对齐,暂未开放下载 |
4.2 国内主流模型
DeepSeek 系列
深度求索(DeepSeek)依然是国内最有代表性的高性价比模型厂商之一。
核心产品线
| 模型 | 版本/形态 | 上下文 | 特点 |
|---|---|---|---|
| deepseek-chat | DeepSeek-V3.2(非思考模式) | 128K | 日常主力,工具调用、JSON、FIM 都很完整 |
| deepseek-reasoner | DeepSeek-V3.2(思考模式) | 128K | 深度推理、复杂代码、工具内思考能力更强 |
| DeepSeek-V3.2-Exp(开源) | 开源部署版 | 128K | 适合私有化、自建服务和二次微调 |
通义千问 Qwen 系列
阿里云出品,是国内开源生态最完善的模型系列。
核心产品线
| 模型 | 参数量 | 上下文 | 特点 |
|---|---|---|---|
| Qwen2.5-72B | 72B | 128K | 旗舰版,能力最强 |
| Qwen2.5-32B | 32B | 128K | 主力版,平衡性能和成本 |
| Qwen2.5-14B | 14B | 128K | 中等版,适合多数场景 |
| Qwen2.5-7B | 7B | 128K | 轻量版,部署门槛低 |
| Qwen2.5-3B | 3B | 32K | 超轻量,端侧部署 |
| Qwen2.5-1.5B | 1.5B | 32K | 极轻量,嵌入式场景 |
| Qwen2.5-0.5B | 0.5B | 32K | 最小版,资源极度受限 |
| 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:明确要求模型承认不确定性
在 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 年的获奖者"
- 或者错误地猜测一个答案(幻觉)
范围边界:私有信息不可及
训练数据主要来自公开的互联网内容。以下信息是模型不可能知道的:
企业内部信息:
- 公司的组织架构
- 内部规章制度
- 产品技术文档
- 客户数据
个人私有信息:
- 你的个人笔记
- 未公开的研究资料
- 私人通信内容
付费 / 受限内容:
- 付费论文的全文
- 订阅制服务的内容
- 需要登录才能访问的资料
示例:
用户:我们公司的年假政策是什么?
模型的真实情况:
- 它不知道你在哪家公司
- 你们公司的员工手册不在它的训练数据里
- 它只能给出一个通用的、基于常见做法的回答
- 这个回答可能和你们公司的实际政策完全不同
知识边界的解决方案
-
RAG(检索增强生成)
这是解决知识边界问题的主流方案:
用户提问 ↓ 从私有知识库检索相关文档片段 ↓ 把片段 + 问题一起发给大模型 ↓ 模型基于片段生成回答通过这种方式,模型可以回答训练数据里没有的问题。
-
知识库定期更新
对于时效性要求高的场景,定期更新知识库的内容。
-
联网搜索
对于实时信息,让模型调用搜索引擎获取最新内容。
-
模型微调
把领域知识 "植入" 模型,让它内化这些知识。但成本较高,且知识更新不灵活。
5.3 常见解决方案
5.3.1 技术一:Prompt 工程
Prompt 工程是优化大模型输出的第一道防线,零成本,立即生效。
什么是 Prompt 工程
Prompt 工程是通过精心设计输入的提示词,引导模型更好地完成任务的技术。
好的 Prompt 可以:
- 减少幻觉
- 提高回答的准确性
- 控制输出格式
- 引导模型的思考方式
核心技巧
-
技巧 1:明确角色和背景
你是一个专业的法律顾问,专注于劳动法领域,有 10 年执业经验。 你的回答应该准确、专业,同时用普通人能理解的语言表达。 -
技巧 2:约束回答范围
请仅基于以下提供的资料回答问题,不要添加资料中没有的信息。 如果资料中没有相关内容,请说"提供的资料中没有这方面的信息"。 -
技巧 3:要求承认不确定性
如果你不确定某个信息的准确性,请明确说明这是你的推测,而不是确定的事实。 对于你不知道的问题,请直接说"我不知道",不要编造答案。 -
技巧 4:提供示例(Few-shot)
请按以下格式提取文本中的实体: 示例输入:苹果公司发布了iPhone 15,CEO库克出席了发布会。 示例输出: - 公司:苹果公司 - 产品:iPhone 15 - 人物:库克 现在请处理以下文本: ... -
技巧 5:分步引导(CoT)
请按以下步骤分析这个问题: 1. 首先,识别问题的核心是什么 2. 然后,列出解决这个问题需要的信息 3. 接着,逐步分析每个相关因素 4. 最后,给出你的结论和理由 -
技巧 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 的核心组件
-
文档处理
把原始文档切成小块(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 -
向量化(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 的优势
- 解决知识边界:可以回答训练数据里没有的问题
- 减少幻觉:回答基于提供的资料,而不是模型的"记忆"
- 可追溯:可以显示答案的来源,便于验证
- 低成本更新:更新知识只需更新文档库,不需要重新训练模型
RAG 的挑战
- 检索质量:检索不到相关文档,回答就没法准确
- 上下文长度:检索到的内容可能超过上下文窗口
- 多跳推理:需要综合多个文档才能回答的问题比较难处理
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 的核心能力
-
任务规划(Planning)
把复杂任务分解成可执行的步骤:
用户:帮我分析一下最近一周苹果股票的走势,并给出投资建议 Agent 的规划: 1. 获取苹果股票最近一周的历史数据 2. 分析价格走势(涨跌、波动) 3. 查找相关新闻和市场分析 4. 综合以上信息给出投资建议 -
工具调用(Tool Use)
选择合适的工具执行每个步骤:
步骤 1 → 调用股票 API 获取历史数据 步骤 2 → 调用数据分析工具计算指标 步骤 3 → 调用搜索引擎获取新闻 步骤 4 → 由大模型综合分析 -
执行和观察(Action & Observation)
执行工具调用,观察结果,根据结果决定下一步:
执行步骤 1:调用股票 API 观察结果:获得了 7 天的 OHLC 数据 判断:数据获取成功,进入步骤 2 执行步骤 2:分析数据 观察结果:周涨幅 +3.2%,波动率较低 判断:分析完成,进入步骤 3 ... -
记忆(Memory)
记住执行过程中的信息,便于后续步骤使用:
- 短期记忆:当前任务的上下文
- 长期记忆:跨任务的知识积累
-
反思和修正(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.com | • Qwen首发:最新版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
最简单、最推荐的本地部署方案。
核心优势 :
- 极简部署 :一条命令安装,一条命令运行模型
- 跨平台 :支持 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
}
字段详解 :
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| model | string | 是 | 模型 ID,不同平台格式不同 |
| messages | array | 是 | 对话消息数组 |
| temperature | float | 否 | 随机性,0-2,默认 1 |
| top_p | float | 否 | 核采样,0-1,默认 1 |
| max_tokens | int | 否 | 最大输出 Token 数 |
| stream | bool | 否 | 是否流式返回,默认 false |
| stop | array | 否 | 停止生成的字符串列表 |
| presence_penalty | float | 否 | 存在惩罚,-2 到 2 |
| frequency_penalty | float | 否 | 频率惩罚,-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 准备工作
-
注册获取 API Key
-
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 协议的好处。