Skip to content
This page has been auto-translated and may contain errors.View in English

LLM 是如何工作的 ​

docs.scrimba.com

先说名字。LLM 是 large language model(大语言模型)的缩写,就是 ChatGPT 这类工具背后的 AI:你发一个提示词过去,它给你返回一段文字。这一章要建立的,是"模型到底是什么"这件事的实际认知框架。

你输入一个问题,几秒钟后一段流畅、自信的回答就出现了。读起来就像模型理解了你,然后查到了答案。这种印象很有用,但在关键的地方是错的,而这两者之间的落差,正是大多数新手犯错的根源。

这一章会让你在使用这些工具的整个过程中持续受益。你不需要懂训练模型背后的数学才能拿它来搭东西。但你确实需要搞清楚它回答问题时到底发生了什么,因为这个认知能解释后面几乎所有的事情:为什么提示词有效、为什么同一个问题会得到不同的答案、为什么模型会编造事实、为什么它的成本是这样的、为什么有些看起来应该很简单的功能其实很难做。这本手册后面的所有内容,都是这里这些想法的自然延伸。

这一章要讲的是这些系统背后的心智模型:模型是什么,它运行的那个唯一循环,以及能解释这本手册其余部分的几个关键特性。这里面没有数学,但恰恰是这些内容决定了你是在对着一个黑箱写代码,还是清楚知道它为什么会这样表现。

后面章节里的每一种技巧——提示词、结构化输出、检索、agent——都是你即将读到的内容的自然结果。把这个认知框架建立对了,后面的东西就不再像一堆零散的技巧,而会变成一台你能预判其行为的机器。

这些模型会以特定的、可重复的方式出错,这一章要讲的就是原因。剥开界面看,一个语言模型就是一个冻结的函数,一次预测一小段文本,几乎所有生产环境中的失败模式——不确定性、成本失控、上下文丢失、自信地给出错误答案——都源于这一个事实。

这里没有新的数学知识,这正是重点所在:持久的工程优势在于机制本身,而不是当月最热的那个模型。把这一章当作后面每一个决策都要依赖的底层基础来读。

模型到底是什么 ​

剥开聊天界面,一个大语言模型其实就是一件东西:一个庞大的数学函数,一组固定的、多达数十亿个的数字,叫作**参数**(你也会听到人们叫它"权重")。这就是模型的全部。不是数据库,不是搜索引擎,也不是箱子里藏着的小人。它就是一大堆数字,输入文本,输出文本。

这些数字不是程序员一个个打出来的。它们是在训练过程中确定的。提供方喂给模型海量的文本——书籍、代码、文章、网页,然后反复做一件简单的练习:把下一个词藏起来,让模型去猜,猜错了就微调一下数字。把这个过程重复无数次,这些数字就慢慢收敛到一组善于预测文本的数值。这就是训练的本质:调整数十亿个旋钮,直到预测效果变好。

由此可以得出两件事,它们能解释你之后会遇到的很多行为。第一,训练是提前一次性完成的,而且代价极其高昂。等你调用模型的时候,那些数字早已冻结。模型不会从你的对话中学习。你说的任何话都不会改变它的参数。它看起来像是在记忆和适应,但那其实是模型周围的软件在做这件事,而不是模型本身——这一点在后面讲上下文窗口的部分会再次被重重提起。

第二,模型内部没有一张事实表。所谓的"知识",是以模式的形式烙印在那些数字里的,就像一个做过一万个面包的师傅,手上就有了这门手艺,不用去翻食谱。模型学到的是"正确、合理的文本长什么样"。这就是为什么它能在同一句话里既流畅又出错:它是在复现一个好答案的"样子",而不是从架子上取下一条存好的事实。

Juno模型到底是什么 所以模型就是一大堆固定不变的数字,把文本变成文本,这些数字在训练时调好一次就冻结了。它不会在你聊天的时候学习你,内部也没有一张整齐的事实表,只有模式。这就是为什么它能听起来很有把握却依然出错,所以任何重要的内容都要自己核实一下。

模型是一个**纯函数**:给它 (权重, 输入),它就算出一个输出,仅此而已。权重是训练过程中学到并固定下来的数十亿个数字;输入是你发送的文本。因为它是一个函数,同样的输入在同样的设置下,每次都会产生同样的底层分数。你在不同运行之间看到的任何差异,都来自从这些分数里挑词的采样步骤,而不是模型改变了主意。

这个函数所携带的知识是以有损的方式存储的——被打散、压缩地混合在整套权重里,而不是像可检索的记录那样归档存放。你找不到某一行数据可以读取,也没有单元格可以编辑。模型"知道"的一个事实,实质上是权重按需重建出来的一个强模式,而且有时候重建出来会稍微出点错。你无法指出某个事实存在于哪里,也无法不重新训练就修补某一个事实。

务实的做法是把模型当作一个你调用的固定函数,而不是一个你查询的知识库。靠它来做语言组织、对文本进行推理、遵循指令这些事。当你需要具体、时效性强、或需要可核实的信息时,自己把这些信息放进输入里,而不是指望权重会记住它。这一个习惯——提供事实,而不是指望模型记得——就能避免大量"自信但错误"的回答。

Juno模型到底是什么 把模型想象成一个关于权重和输入的纯函数:同样的输入、同样的设置、同样的底层分数,运行间的差异来自采样。它的知识以有损的方式压缩在权重里,所以你没办法在里面查找或编辑某一个具体事实。据此来搭建系统:把它当作你调用的固定函数,把具体或时效性的事实通过输入喂进去,而不是指望权重会记住。

一个部署好的模型,本质上是 API 背后一组冻结的权重:数十亿个训练时学到并固定下来的数字,计算的是关于 (权重, 输入) 的纯函数。它行为真正发生变化的唯一时刻,是提供方发布了一个不同的模型。

这里有一个很尖锐的运维风险。同一个接口下面悄悄换了模型,可能在你一行代码都没改的情况下让输出发生变化,所以要锁定一个具体的模型版本,把这个版本当作你合约的一部分,并在采用新版本之前重新跑一遍你的评估。凭着信任去用"最新版本",正是一个正常运转的系统悄悄退化的常见原因。

权重是压缩的参数化记忆,它既是有损的,也是无法审计的。你没法在不同版本之间对比模型"知道"什么,没法列举它的事实清单,也没法证明某个具体事实是否存在于其中或是否正确。所以永远不要把关键决策所依赖的事实正确性,压在参数记忆上。如果答错了会让你付出代价,就从一个你能控制、能引用的来源把事实放进输入里,让模型只负责在这个来源之上进行表达和推理,而不是充当来源本身。

微调(fine-tuning)所做的正是刻意改变权重,值得说清楚它到底能换来什么。微调能可靠地改变行为和形式、语气、格式、任务习惯,但它给不了你一个可靠、可查询的事实库。用它来塑造模型如何回应,而不是用来灌入你需要信赖的知识。把事实放在输入里,把风格放在权重里,这样两边都不会给你意外。

Juno模型到底是什么 权重是冻结的,所以行为只有在提供方发布不同模型时才会变化:锁定版本、把它写进你的合约,在升级前重新跑评估,否则一次悄悄的换模型会在你没注意的时候让效果退化。参数记忆是有损且无法审计的;你没法对比它知道什么,所以把关键事实放在你能控制来源的输入里,而不是靠它去回忆。微调改变的是行为和形式,不是一个可靠的事实库,所以用它来调整模型怎么回应,而不是它该知道什么。

预测下一个 token ​

有了这一大堆数字,模型要做的事情其实只有一件:反复预测下一小段文本。

我们把这个循环放慢来看。你给它一段文本。它把这段文本跑过所有那些参数,然后为每一个可能的下一小段文本产出一个概率,一次性给出数万个选项。在"法国的首都是"这句话之后,"巴黎"这个片段可能得分 97%,"一"这个片段得分 1%,其他的几乎都是 0。

模型接着挑一个片段,把它加到文本后面,然后把整段文本重新跑一遍来挑下一个片段。这个过程一直重复,直到答案完成。

这就是整个引擎。你读到的一段浑然一体的回答,其实是一小段一小段拼出来的,每一段都是从一次全新的概率排名里选出来的,除了"接下来最可能是什么"之外,模型对这句话最终要走向哪里根本没有计划。你会用到的每一种能力——聊天、写代码、翻译、调用工具、推理——底层跑的都是同一个循环。

手机键盘猜下一个词其实就是这个想法的缩小版。区别在于这个猜测背后有着数十亿个参数支撑。用足够多的文本训练之后,"猜下一段最可能的文本"这件事,居然就足够好到能写出能跑的代码、能给出清晰的解释。但底层的动作从来没变:它是预测,不是理解,也不是去查一个存好的答案。

Juno预测下一个 token 整个引擎其实就是一个小循环:读到目前为止的文本,给每一个可能的下一段文本打分,挑一个,然后再来一遍,直到答案完成。模型对这句话除了"接下来最可能是什么"之外没有任何计划,所以你读到的一段浑然一体的回答,其实是一段一段拼出来的。这是预测,不是理解,也不是在查一个存好的答案。

模型反复只做一件事:每一步都读取到目前为止的所有文本,然后为词表里的每一个 token(token 是一段文本,常常是一个词的一部分)给出一个分数,一次性给出数万个。这些分数会变成一个概率分布,模型从中采样出一个 token。然后它把这个 token 接到文本后面,重新跑一遍整个过程来挑下一个。在"法国的首都是"之后,"巴黎"可能占据几乎全部的概率,其他所有选项几乎都是零。

这种不断把结果喂回自己的做法,就是它被称为**自回归(autoregressive)**的原因:每一个新 token 都依赖于到目前为止的所有文本,包括它这一轮已经写出来的那些 token。这里没有全局计划,也没有草稿可以修改。模型一次只能落定一个 token,而且一旦落定就无法收回,这就可能把自己逼进死角:前面一个自信落下的词,可能会把句子后面的部分逼向一条尴尬的路,而模型还得把它写完。

这一个事实能解释你实践中会用到的几件事。让模型在给出最终答案之前先把推理过程写出来是有帮助的,因为更多的 token 意味着更多的步骤、更多花在这个问题上的计算量,然后才落定答案。输出的结构是严格从左到右决定的,所以一个回答的形态是走一步定一步,而不是提前规划好的。同一个循环也驱动着工具调用:模型预测出组成一次调用的 token,你执行它,然后把结果作为更多文本喂回去,供下一次预测使用。

Juno预测下一个 token 每一步模型都为词表里的每个 token 打分,采样出一个,喂回去,再重复:这个反馈循环就是"自回归"的意思。这里没有计划,也没有撤销,所以它是一个 token 一个 token 地落定,可能把自己逼进死角。这就是为什么在给答案之前先写推理会有帮助,也是为什么工具调用其实是同一个循环加上结果反馈。

生成过程从结构上就是顺序的:模型产出一个 token(一段文本),把它喂回去,再预测下一个,所以每一个输出 token 都是网络的一次单独前向传播。读取你的输入相对便宜,是一次性处理完的;真正慢、真正贵的部分是模型的书写过程。延迟和成本跟着输出长度扩展,而不是输入长度。由此可以推出实际可用的杠杆:让输出保持简短,用流式传输 token 让用户看到进度而不是等完整回复,也别为你最终会丢掉的冗余格式付费。

另一个结果是关于正确性的。因为没有全局计划,一次生成过程是不会自我纠正的:模型从左到右落定,一个 token 一旦写出来就无法修改。这就是为什么任何需要保证正确的东西都想要一次验证或者对初稿再过一遍的原因,也是为什么一个先写推理后写答案的模型往往表现更好——你是在为它多买几步来落定答案。把第一次生成的结果当作草稿,而不是最终判决。

值得接入的一个杠杆是:很多环境会开放逐 token 的概率(logprobs,模型对自己挑出的每个 token 给出的置信分数)。你可以读取这些分数来标记低置信度的片段,拦住模型自己也不确定的答案,或者把不确定的情况转到第二次处理或人工审核上。这是一个你手上本就有的廉价信号,能把"模型这里可能错了"这种猜测,变成可以程序化处理的东西。

Juno预测下一个 token 每一个输出 token 都是一次独立的前向传播,所以成本和延迟跟着输出长度走,不是输入长度:让回复保持简短、用流式传输,别再为你会丢掉的格式付费。这里没有计划、没有撤销,所以一次生成不会自我纠正;把第一次生成当草稿,对任何必须正确的东西加一道验证。如果你能读到逐 token 的 logprobs,用它来标记或转交模型最不确定的片段,这是你已经拥有却很少用到的置信度信号。

随机性旋钮 ​

如果模型总是抓取得分最高的那一个片段,它对同一个提示词每次都会给你同样的答案,而这个答案往往显得单调、重复。所以大多数时候,挑选的过程里会带一点随机性:通常会选一个高概率的片段,但不总是排名第一的那个。这就是为什么你问两次完全相同的问题会得到两个不同的回答。这是系统按设计运行的结果,不是故障。

你可以通过一个叫**温度(temperature)**的设置来控制随机性的多少,你会在讲如何从代码中调用模型那一章里设置它。现在先建立一个直觉很有用。温度低时,模型会紧贴自己最靠前的选择:专注、一致、可预测,这正是从发票里提取一个数字或对文本分类时你想要的效果。温度高时,低概率的片段会更常被选中:更多变、更有创造力,也更容易走偏。头脑风暴和创意写作想要偏高的一端;任何有正确答案的任务都想要偏低的一端。

所以一个旋钮,就能让模型在"谨慎且可复现"和"意外且富有创造力"之间滑动。无论哪种情况都是同一个预测循环;温度只是改变了模型敢在多大胆子上跳出它最保险的猜测。

Juno随机性旋钮 温度就是你的随机性旋钮。对于有正确答案的任务,比如抽取或分类,把它调低,你会得到专注、可复现的回答。对于头脑风暴和创意工作,把它调高,你会得到更多变化,同时也有更多跑偏的机会。同一个预测循环,只是伸手的胆量大一点或小一点。

在模型挑选下一个片段之前,它对每一个候选项都有一个概率。温度会在模型从这个分布中采样之前,重新塑造这个分布。低温度会把分布向那个概率最高的单一 token 收紧(更接近于总是选排名第一的那个),所以输出会变得专注、可复现。高温度会把分布压平,给低概率的 token 一个真正的机会,所以输出会更多变、也更容易跑偏。这就是为什么同一个提示词会返回不同回答:这里发生了一次抽签,温度决定了这场抽签的偏斜程度。

温度不是这里唯一的旋钮。Top-p(只从概率累加起来达到 p 的那个最小 token 集合里采样)和 top-k(只从最可能的 k 个 token 里采样)都是在采样之前先修剪候选池。它们在效果上和温度是重叠的,所以最好把它们当作一个统一的随机性设置来理解,而不是三个互不相关的独立设置。

务实的做法是针对每个任务显式设置温度,而不是相信默认值。做抽取、分类,以及任何会生成结构化输出、之后要被解析的任务时,把温度调低;做创意构思和草稿这类以多样性为目标的任务时,把温度调高。留意各参数之间的相互作用:把高温度和 top-p=1 叠在一起会让整条概率尾部都参与竞争,当你需要可靠、可解析的结果时,这会对你不利。

Juno随机性旋钮 温度会在采样之前重新塑造概率分布:调低让分布向最高分的 token 收紧,输出更专注;调高把分布压平,得到更多变化。把 temperature、top_p 和 top_k 当作一个统一的随机性设置,而不是三个各自独立的旋钮。按任务设置:抽取、分类和结构化输出用低值;创意构思用高值。需要可解析结果时,别把高温度和 top_p=1 搭在一起用。

温度是在采样之前对分数分布做缩放,一个诱人的捷径是把它设为 0,然后就把输出称为"确定性的"。这个说法很接近事实,但并非绝对保证。浮点数运算、服务端的批处理、以及大型模型内部的路由机制,即便在温度为 0 时也会引入微小的不确定性。所以不要构建假设"同一个提示词重复请求会得到逐字节完全相同的输出"的精确匹配缓存或精确匹配测试;这个假设会间歇性地失效,而间歇性失败正是最难排查、代价最高的那种问题。

这决定了你该如何测试采样出来的行为。单次运行几乎无法告诉你关于一个**随机(stochastic)**系统(输出会随每次运行变化的系统)的任何信息,所以评估一个提示词的方法是把它跑很多次,观察输出的分布:多少比例落在规格范围内,方差有多大,尾部会跑偏到哪里。这正是你的评估体系(evals)要做的事,它们需要足够多的样本来看清分布的散布程度,而不是靠一次幸运的通过。

把温度、top-p(只从概率累计达到 p 的那个最小 token 集合中采样)和 top-k(只保留最可能的 k 个 token)当作一个统一的采样预算,一起调整,而不是三个凭本能设置的旋钮。选择那个仍能给任务带来可接受多样性的最低随机性水平:把这三者一起收紧,可能会让输出坍缩成重复、脆弱的东西;而把它们全部放开,又会带来你之后必须专门去验证的方差。两边的代价都是真实存在的,所以要有意识地去花它。

Juno随机性旋钮 温度为 0 是接近确定性,而不是绝对保证:浮点运算、批处理和内部路由都会泄漏一点不确定性,所以别做精确匹配的缓存或精确匹配测试。要用多次运行来评估采样出来的行为,看分布而不是单次结果;这正是你的评估体系存在的意义。把 temperature、top_p 和 top_k 当作一个统一的采样预算来调,选那个仍能给出可接受多样性的最低随机性。

Token ​

模型预测的这些"片段"有个名字:token。一个 token 是一段文本,常常是一整个词,有时只是一个词的一部分。常见的词通常是一个单独的 token,而较长或较少见的词会被拆成好几个。

"tokenization"  ->  "token" + "ization"
"unbelievable"  ->  "un" + "believ" + "able"
"cat"           ->  "cat"

为什么要把文本切成这些奇怪的小片段,而不是用整个词或单个字母呢?这是一种权衡。单个字母会让每一段序列变得极其漫长,而长度正是模型要计算的对象。整个词则需要一个数百万条目的词表,而且一旦有人造了个新词或打错了字,这套体系立刻就会失效。Token 是中间道路:一个固定的、只有几万个常见片段的词表,可以拼接起来拼出任何东西,包括模型从未见过的词。

这里有一个让人意外的底层结果。模型从来看不到字母。它看到的是 token,而对它来说,这些 token 其实是这些片段对应的 ID 数字。所以对我们来说轻而易举的任务,对它可能很难:数一个词里有几个字母、把一个字符串反过来、注意到"strawberry"里有三个 r。

它不是笨。它根本感知不到单个字符,就像你在一块烤好的面包里尝不出单独的面粉颗粒一样。当一个模型在拼写或数字符这类任务上出错时,原因就在这里,而通常的解决办法是把这部分工作交给普通代码去做。

Token 还是你会一直在意的两件事的计量单位:

  • 费用:提供方按 token 计费,包括你发送的 token 和模型写回的 token,所以输入的长文档和输出的长回答都会花更多钱。中文的一个粗略估算是大约一到两个字对应一个 token(英文大约是每四个字符对应一个 token,或者每 75 个词对应 100 个 token);代码和其他语言常常会被拆成更多 token,这也是它们成本更高的部分原因。等你开始从代码中调用模型时会直接遇到这个问题。
  • 限制:模型一次能处理的 token 数量是有上限的,这就是下一节要讲的内容。
JunoToken 一个 token 是一段文本,通常是一个词或词的一部分,模型读到的是这些片段对应的 ID 数字,不是字母。这就是为什么它会在数数或拼写上出错,所以把这类工作交给普通代码。而且既然你按两个方向的 token 数付费,一次能塞进去的 token 又有上限,token 越少就意味着成本越低、空间越多。

模型预测的是token,常常是一整个词、有时是一个片段的文本单元。负责拆分的部件是**分词器(tokenizer)**,它依据一套学习得到的词表来工作,这套词表是通过反复合并最常见的字符对,直到得到一个固定的、只有几万个片段的集合而构建出来的(这种方案通常叫作字节对编码,即 BPE)。正是这段历史决定了常见词最终变成单个 token,而罕见词会拆成若干部分:常见模式在训练中被合并了,罕见模式从来没有被合并过。

由此得出两个实用的事实。第一,模型从来不处理字符,只处理 token ID,所以任何字符级别的操作(数字母、反转字符串、在精确位置切片)放在提示词里都不牢靠,放进你自己的代码里就靠得住。把这类工作推给代码,让模型只负责语言相关的部分。第二,分词是与具体模型绑定的,所以 token 数量是不可移植的。同一句话在不同的分词器上可能消耗不同数量的 token,这一点在你估算成本或者往窗口里塞内容的那一刻就很重要。

对搭建系统的启示是:不要用字符长度去目测 token 数量。用模型实际使用的分词器去计数,因为这才是计费和限制所依据的数字。等你开始从代码中调用模型时,你会具体地遇到这一切——你发出去的数量加上你收回来的数量,就是账单。

JunoToken 分词器依据一套学习得到的词表(BPE)来拆分文本,这就是为什么常见词是一个 token,罕见词会被拆散。分词是与具体模型绑定的,所以要用模型自己的分词器去计数,而不是靠字符数猜测。字符级别的工作(数数、反转、精确切片)留在你的代码里做,别放进提示词里。

Token 是你所构建一切东西上的计量表。Token 是分词器学习到的词表中的一个文本片段,按 token 计费加上固定的上下文上限,是塑造你的成本和设计的两个数字。让人安心的"大约四个字符对应一个 token"这条经验法则,对纯英文文章成立,但恰好在生产环境真正发生的地方失效:代码、JSON、深度嵌套的格式,以及非拉丁文字,都会被拆成远比这条经验法则预测的更多的 token。所以要用真实的分词器来做预算,而不是靠这个估算值,否则你的成本和容量估算恰恰会在你实际发送的那些负载上出错。

这种碎片化的程度分布并不均匀,这在规模化场景下是一个公平性和成本问题。同样的意思用非拉丁文字表达,可能要花上其英文版本好几倍的 token,所以同一个功能,对你的某些用户来说悄悄变得更贵、也更慢。在你向任何人承诺统一定价或固定延迟之前,值得按语言分别测量一下。

再补充两个来自实战的习惯。格式本身就是开销:空白字符、冗余的分隔符、装饰性的 markdown,全都要花 token,却没有增加任何意义,所以喂给模型的结构要保持精简,展示层面的东西留给你的代码之后再加。还要喂给它干净、正常的文本,因为罕见或畸形的 token(乱码、杂散的控制字符、奇怪的编码)落在词表里那些训练不足的边缘角落,可能引发奇怪的输出。这些都不是什么冷门知识;区别就在于你是拥有一个可信的 token 预算,还是一个会在账单上给你惊喜的预算。

JunoToken "四字符一个 token"的规则在代码、JSON 和非拉丁文字上会失效,而这些恰恰是你真实流量所在的地方,所以要用实际的分词器来做预算。非拉丁语言表达同样的意思,可能要花好几倍的 token,这是一个值得按语言分别测量的真实成本和公平性差距。去掉装饰性格式来省 token,喂给它干净的文本,别让罕见或畸形的 token 给你带来奇怪的行为。

上下文窗口 ​

**上下文窗口**指的是模型一次能接收多少文本,以 token 计量。把它想象成模型这张"办公桌"的大小。一次请求所涉及的所有东西都必须同时摊在这张桌子上:你的指令、目前为止的对话、你粘贴进去的任何文档,以及模型正在写的回复。

为什么会有这个限制呢?这来自预测机制本身的运作方式。为了选出下一个 token,模型要权衡输入里每一个 token 和其他每一个 token 之间的关系,这样它才能知道"它"指的是三句话之前提到的那只猫。这种"两两全对比"正是让模型能够跨越一段文字追踪意义的机制,也正是让长输入处理起来很昂贵的原因。提供方设定了一个上限,也就是上下文窗口,来让每次请求的处理量保持可控。

当桌子被塞满时,总得有东西被挪下去。如果一段对话长到超出窗口容量,最早的部分就会被挤掉,模型也就再也看不到它们了。这就是为什么一段很长的对话看起来会"忘记"你一开始说的话。它不是以人类那种方式忘记的;那些 token 只是不在桌子上了。还有一点值得知道:模型往往对桌上内容的开头和结尾关注得最仔细,容易忽略埋在中间的东西,所以在填满窗口时,把重要文本放在哪个位置是有讲究的。

现在说到几乎每个新手都会踩的那个点,它比前面说的所有东西都更重要。模型在不同的请求之间没有记忆。记住,它的数字是冻结的。每一次调用都是从一张空桌子开始的。模型不记得你上一个问题、你的名字,或者一分钟前发生的任何事。

一个聊天应用感觉像是一段连续流动的对话,仅仅是因为这个应用在每一条新消息发出时,都悄悄地把全部历史重新发送了一遍。这种连续性是模型周围的软件构建出来的,不是模型自己拥有的。

这一个事实决定了你从这里往后要怎么搭建一切。如果你希望模型知道某件事,你就必须在那次请求里把它放到桌子上:到目前为止的对话、用户的信息、相关文档,全部内容,每一次都要放。这本手册的很大一部分,其实就是在讲怎么把这一件事做好——把正确的文本放到桌子上。

Juno上下文窗口 上下文窗口就是模型的办公桌:每一次请求都必须摊在这张桌上,一旦超载,最旧的文本就会被挤掉。模型在不同调用之间不保留任何记忆,所以应用每次都重新发送全部历史,来模拟一场连续的对话。你从这里往后的工作,就是在每一次请求里都把正确的文本放到那张桌子上。

把上下文窗口想象成一次请求能花的固定 token 预算,涵盖你的指令、正在进行的对话、任何粘贴进去的文档,以及正在生成的答案。这个窗口之所以存在,是因为模型读取输入的方式:一种叫**注意力(attention)**的机制,会把每一个 token 和其他每一个 token 相互比较,来判断谁和谁有关联。正是这种比较,让模型能够把一个"它"解析回它所指代的那个名词,而它的计算代价大致随输入长度的平方增长。输入 token 翻一倍,计算量增长得远不止两倍,这才是长输入运行慢、成本高的真正原因。

能塞进窗口不代表模型会用到窗口里的所有内容。有一个被广泛记录的效应叫**迷失在中间(lost in the middle)**:模型对输入开头和结尾的关注最可靠,而对停放在中间位置的材料可能会一扫而过。所以要把你最重要的指令和数据放在提示词的靠前或靠后位置,而不是埋在一整墙粘贴文本的中间。

这件事的另一半是记忆,关键事实是模型是**无状态(stateless)**的:它在不同请求之间什么都不记得。每次调用都从一个空窗口开始,所以模型唯一能知道到目前为止对话内容的方式,就是你每次都重新把它发过去。重新发送并整理这段历史是你的工作,不是模型的。当对话长度超出窗口时,自己动手管理它,修剪掉旧的对话轮次,或者用一段简短的摘要来代替它们,这样你保留下来的 token 才是真正还有用的那些。

这就把"构建上下文"这件事放到了用这些模型搭建系统的核心位置。决定窗口里放什么、按什么顺序放、以及怎么压缩之前的内容,占了大部分工作量,而提示词工程正是你要学会如何刻意塑造它的地方。

Juno上下文窗口 窗口是一个涵盖指令、历史、文档和回复的 token 预算;注意力机制让长输入的成本大致随其长度的平方增长。模型存在"迷失在中间"效应,所以要把重要内容放在靠前或靠后的位置。模型是无状态的,所以重新发送并修剪对话历史,是你每次调用都要做的事,不是模型的工作。

把上下文窗口当作一个上限,而不是一个目标。它是单次请求能拿到的 token 预算,涵盖你的指令、正在进行的历史、检索到的文档,以及输出。

你放进去的每一个 token 都要花钱、花时间,因为**注意力(attention)**这一步(模型为读取输入而做的那种两两全比较)的计算量大致随长度的平方增长。更糟的是,有效的召回能力在你还没到达上限之前就已经开始退化:迷失在中间效应意味着,即便某段内容技术上能塞进窗口,埋在中间的材料的准确率也会下降。所以要精心策划上下文,而不是把什么都一股脑塞进去;当源数据很大时,用RAG只检索相关的片段,而不是把整个语料库都粘贴进去。

顺序是一个可以利用的杠杆,不只是个细节。提供方可以通过**提示词缓存(prompt caching)**来复用未改变前缀的计算结果,这里被缓存的内部状态叫作 KV 缓存,也就是模型对它已经读过的 token 所保存下来的计算结果。把稳定的内容(系统指令、固定的参考材料)放在前面,可变的内容放在最后,重复调用就会因为提示词的前段是从缓存里取用的而变得更便宜、更快。如果你随意重排这个前缀,就会让缓存失效,重新按全价付费。

跨轮次的记忆不是模型自带的功能,而是你要做的一个工程选择,而每一种方案都有自己独特的失败方式:

  • 单纯的窗口化(windowing),只保留最近的几轮对话,成本低,但会悄悄丢掉用户仍然期望你记得的早期事实。
  • **摘要(summarisation)**能用更少的 token 保留更多历史,但会丢失细节,还可能悄悄把自己的错误也编码进去。
  • **检索(retrieval)**只取回相关的内容,但完全依赖你的索引能返回正确的片段。

按具体使用场景来选择,并假设你选的任何一种最终都会在某个时刻捅出错误的东西;给它加上监测手段,而不是盲目信任它。

Juno上下文窗口 窗口是一个上限,不是一个目标:每个 token 都会推高成本和延迟,而召回能力在你还没触及上限之前就已经在腐坏,所以对内容要精心筛选,遇到大规模的内容就靠检索。稳定内容放前面,可变内容放后面,这样提示词缓存(KV 缓存)才能真正发挥作用。跨轮次记忆是你在窗口化、摘要和检索这几个方案里做的设计选择,我在这三种方案上都栽过跟头,所以对你选的那个方案要加上监测。

模型不知道什么 ​

模型的全部知识都是在训练时烙印进去的,所以它只知道训练数据里出现过的东西,而那些数据是在过去某个截止时间点之前收集的。由此直接产生两个限制。

第一,它不知道截止日期之后发生的任何事情。问它上周的新闻,或者这个月刚发布的某个库,它其实没有真实的答案,不过它可能仍然会兴高采烈地给你编出一个听起来靠谱的答案。第二,它从来不知道任何私密的信息。你公司的内部文档、某个用户的订单历史、你数据库里的内容:这些都不在它训练用的公开文本里,所以无论你问得多客气,它都不可能知道。

还有一个更隐蔽的限制。模型通常说不出它"知道"的某件事究竟是从哪里来的,因为这些知识是混合在一起烙印在它内部的,而不是像可溯源的独立事实那样分门别类地存档。它可以给你编一个引用出处,但那是在预测一个引用出处"应该长什么样",不是真的去查一个真实存在的出处。

这正是你之后会遇到的一种叫检索,或者RAG的技术存在的原因。如果你想让模型回答关于近期事件或者你自己私有数据的问题,你不需要重新训练它。你自己去取回相关的文本,在请求时把它放到桌面上,这样模型手头就有了真实的事实,而不需要去猜。模型更像是一个基于你给它的文本进行推理的推理者,而远不是一口可靠的、装满具体事实的井。

Juno模型不知道什么 模型只知道训练数据里有的东西,而这些数据止步于过去某个截止时间,所以它会错过近期事件和任何私密信息,也没办法可靠地说出它的事实来自哪里。当你需要时效性或私有信息时,不要去重新训练:把相关文本取回来,在请求里交给模型。把模型当作一个基于你给它的东西来推理的工具,而不是一个值得信赖的记忆库。

模型内置的知识,也就是它的**参数化知识(parametric knowledge)**(编码在权重里的事实,权重是训练时确定下来的那些可调数字),是在训练截止时刻就被冻结的。那个日期之后发生的任何事情都不在里面,任何私密的信息也从来都不在里面,因为权重是由公开文本塑造出来的。所以记忆上有两个可以预料的缺口:时效性缺口,也就是截止日期之后的任何东西;隐私缺口,也就是从未出现在公开训练数据里的任何东西。

这两个缺口其实是同一个缺口的两面,也用同样的方式来弥补:把需要的文本放进请求里。你自己去取来那篇最新文章或那条相关数据库记录,随问题一起提供出去,这样模型是在读取这个事实,而不是在回忆它。这正是RAG(检索增强生成)背后的核心想法,任何答案必须反映时效性或专有信息的场景,你都会用到它。

参数化知识也没有附带来源。事实是混合分布在权重里的,而不是作为带有溯源信息的独立条目存储的,所以模型无法可靠地告诉你某个信息来自哪里。它生成的一个引用出处,是被生成得"看起来对",而不是从某条记录里检索出来的,这正是为什么把答案扎根在你自己提供的文本上是值得花的功夫。

搭建系统时的实用规则是:不要指望模型从记忆里回忆时效性或专有事实。如果一个事实必须是对的,就去取回它、提供它,让模型在这个事实之上进行推理。

Juno模型不知道什么 参数化知识,也就是编码在权重里的事实,在训练截止时刻就被冻结,也不附带来源,所以模型会错过近期和私密信息,也没法可靠地引用任何出处。两个缺口用同一种方式弥补:把相关文本取回来放进请求里,这正是 RAG 背后的想法。作为一条规则,不要让模型从记忆里回忆时效性或专有事实;提供这些事实,让它在这些事实上进行推理。

模型的参数化记忆,也就是保存在权重(训练时固定下来的那些可调数字)里的知识,是没有版本号、也无法审计的。你没法对它做版本差异比较,没法指出某个说法存在于哪里,也没法确认它没有发生偏移。所以对任何必须正确、时效性强或需要合规的场景,不要相信模型自己的说法:把答案扎根在一个检索到的来源上,并引用那个来源。把内置知识当作一个流畅的先验假设,而不是一份系统记录。

这个截止日期的边界也比一个单一日期所暗示的要模糊得多。训练数据并不是均匀分布到那个日期为止的:近期材料比早期、被充分消化过的材料要薄弱、消化得也不够充分,所以"截止日期前不久"的事实,被了解的程度是不均衡的。一个模型可能对截止日期前一年发生的事情自信地说错,却能精准地说出十年前的一个事实,这就是为什么"这是在截止日期之前吗?"这个问题给出的保证,比它看起来要弱得多,也是为什么任何必须正确的东西都要靠检索。

针对不同的缺口选对合适的工具,因为这两种常见方案并不能互相替代。检索在请求时提供事实,并随着你的信息源更新而保持时效性。微调(fine-tuning)——用你自己的样本继续训练——改变的是形式和行为、语气、格式、模型遵循你的约定的方式,用它来注入知识是一种糟糕、陈旧的手段:训练进去的事实会再次被冻结、难以更新,而且依然没有来源。用检索来提供"什么是真的",用微调来决定"应该听起来是什么样"。

Juno模型不知道什么 参数化记忆没有版本号、也无法审计,所以对任何必须正确、时效性强或需要合规的场景,把答案扎根在一个检索到的来源上并引用它,而不是相信模型自己的说法。记住截止日期是模糊的:"截止日期前不久"的事实被了解的程度不均衡,所以任何必须正确的东西都要靠检索。让工具匹配缺口:检索提供事实,而微调改变的是形式和行为,用它来注入知识是一种陈旧的手段。

模型为什么会编造内容 ​

有时候模型会以十足的自信陈述一件虚假的事:一句没人说过的引言、一个不存在的函数、一篇从来没被写出来过的研究引用。这叫作**幻觉(hallucination)**,读完前面几节之后,你应该会觉得这几乎是不可避免的,而不是什么神秘现象。

原因在于模型内部根本没有"真"与"假"的概念。它只有"可能出现的文本"与"不太可能出现的文本"这两种概念。大多数时候这两者是对齐的,因为在它学习用的文本里,真实的陈述远比虚假的陈述常见得多,所以最可能的续写往往也就是正确的那个。

但当模型碰到一个空白,也就是它从来没在训练中见过的东西,或者是它根本不可能见过的你的私有数据时,它不会停下来标记出这个空白。它没办法可靠地判断自己不知道什么。它只会做它一直在做的唯一一件事:产出听起来最可能的续写。

一个自信、语句工整、却错误的答案,往往恰恰就是听起来最可能的那个答案。让它变得流畅的那套机制,在它犯错的时候依然让它显得流畅。

这一点值得你好好消化:幻觉不是一个可以靠提示词彻底消除掉的小故障。你可以减少它,后面的章节会讲怎么做。告诉模型它可以说"我不知道"会有一点帮助。让它只根据你提供的文本来回答,帮助会大得多,因为这样就没有空白需要它从冻结的训练记忆里去填补了。但这种倾向来自模型本质上是什么——一台没有内置事实核查机制的概率机器,所以持久有效的解决方案是结构性的:给它需要的事实去处理,而不是指望这些事实曾经出现在它的训练数据里,并且对任何重要的答案进行核实,而不是盲目相信。

带着这一条规则离开这一章,比其他任何东西都重要:模型的自信程度,无法告诉你它是否正确。流畅度和正确性来自同一个过程,而它们经常会各走各路。搭建系统时要假设任何单一的答案都可能是错的,因为它确实可能是错的。

Juno模型为什么会编造内容 幻觉就是模型自信地用一段听起来对、但其实不对的文本填补了一个空白,因为它只知道"可能",不知道"真实"。你可以降低它发生的频率,但没法靠提示词把它完全消除掉,所以安全的做法是把模型需要的事实交给它,并核实那些重要的答案。还要记住最重要的一条:自信的语气不是证据,永远不要单独凭确信的语气去信任它。

当模型编造出一句引言、一个函数或一个引用出处,并且以十足的自信陈述出来时,这就是幻觉,它直接源于模型运作的方式。模型没有任何真假信号。它靠"可能出现的文本"和"不太可能出现的文本"来给续写打分,大多数时候"可能"和"真实"是重合的,因为真实的陈述在它的训练数据里占据主导。

幻觉恰恰在这种重合关系失效的地方激增:在它知识的边缘、在罕见或小众话题上,以及在它从未训练过的你的私有数据上。在那些地方,模型依然会产出它自己觉得最可能的续写,而一个流畅、语句工整、却错误的答案,往往正是打分最高的那个。

所以你没法只靠措辞来解决这个问题,但你可以按实际效果给你的应对措施排个优先级。目前为止最大的杠杆是"扎根":把真实的事实放进提示词里,告诉模型只能根据这份提供的文本来回答,最好还能直接引用原文。这样它就没有空白需要从冻结的训练记忆里去填补了。检索出正确的文本来提供,是它自己的一门技术,在RAG那一章讲。

在这之下,给模型一个明确的退路,允许它回答"我不知道"而不是去猜,这能减轻它编造内容的一部分压力。并且在做事实性任务时把温度(控制 token 选择随机性的旋钮)调低一些,因为高随机性会打开更多不太可能、跑偏答案的门。

实际的结论是:把模型约束在你提供的上下文里,并核实那些重要的输出。把提示词措辞上的技巧当作这些结构性措施之上的一个小调整,永远不要把它当作唯一的防线。一个听起来很确信的模型,并没有告诉你任何关于它是否正确的信息,因为流畅度和正确性来自同一个过程,也经常会分道而行。

Juno模型为什么会编造内容 幻觉就是模型给出流畅、看起来可能的文本,却没有任何内在的真实性检查,而这种情况在它知识的边缘、以及它从未见过的数据上会变得更严重。按效果给你的应对措施排序:把它扎根在你提供的文本上(最大的杠杆)、给它留出说"我不知道"的空间、告诉它只根据提供的上下文回答并直接引用、在做事实性任务时把 temperature 调低一些。约束在提供的上下文里,核实那些重要的输出;别只靠措辞上的技巧。

幻觉是模型返回的一段自信、语句工整、但恰好是虚假的文本,这是架构本身的特性,不是一个能靠打补丁修复的 bug。模型按可能性对续写打分,不附带任何真实性信号,所以在它训练的边缘——罕见话题、新近事件、任何私有信息——它依然会吐出它自己最可能的猜测。你无法消除这一点。你的工作是设计整个系统,让一个错误的答案在到达任何重要环节之前就被拦截或被遏制住。

这意味着三个结构性的举措,都跟提示词措辞没关系。核验每一个答案的形态:让模型按一个模式(schema)返回数据,这样畸形或超出范围的输出会大声报错,而不是继续流向下游,这正是结构化输出带给你的东西。用评估体系(evals)——一组带有已知正确答案的留出输入,自动打分——来实际测量你的幻觉率,而不是靠抽查几个提示词就说"没问题"。在错误答案代价高的地方,保留人工介入环节,因为没有任何自动化的防护网能抓住所有情况。

在这一切之下潜藏的一个陷阱是"校准"问题。模型的置信度校准得很差:陈述出的确信程度不是正确性的证据,一个答案的语气不携带任何关于其真实性的信息。所以永远不要因为模型听起来很有把握,就把一个决策押在这上面。

建立并优先使用一条"弃权"路径,也就是一种被认可的"根据给我的信息我无法回答这个问题",并让弃权的代价保持低廉,这样系统会倾向于选择弃权,而不是去编造。这种结构性的思维方式——假设任何单一答案都可能是错的,并遏制这种损害——正是整个安全与局限那一章的基础。

Juno模型为什么会编造内容 你没办法删除幻觉,所以要按遏制的思路来做工程:用模式(schema)来验证结构,用评估体系来测量幻觉率,而不是靠抽查几个输出来目测,在犯错代价高的地方保留人工介入。模型的置信度校准得很差,所以确信的语气不是证据,永远不要因为它而放行一个决策。给系统一条低成本、被认可的弃权路径,让它每次都优先于一个自信的瞎猜。

把这些拼起来 ​

这一章讲的所有内容,其实是从不同角度看同一个想法。模型是一堆冻结的数字,用来预测下一个 token,你会遇到的几乎所有行为,都是从这一点自然推出来的:

  • 它是从一个概率排名里一次预测一个 token,所以它是即兴发挥而不是回忆,一点随机性(温度)就意味着答案会有变化。
  • Token 是它工作的单位:是你账单的计量单位,是你限额的计量单位,也是它数不准字母的原因。
  • 上下文窗口是一次请求共用的一张桌子,模型在不同请求之间什么都不记得,所以连续性是你的工作。
  • 它的知识在训练截止日期就被冻结了,也从来没有包含你的私有数据,这就是为什么你要去取来事实,把它们放进提示词里。
  • 幻觉是一个系统预测可能出现的文本、却没有真假概念所要付出的代价,只能靠扎根和核实来减少,绝不能只靠聪明的措辞。

把这些放在一起看,你会发现,相当一部分 AI 工程工作,其实是同一件事换了不同的外衣:在正确的时刻,用正确的形态,把正确的文本放到桌子上,而且永远不要盲目信任输出。下一章开放与封闭模型会讲有哪些种类的模型、以及你怎么拿到一个,然后提示词工程是你开始把所有这些预测能力引导向你真正想要的东西的地方。

Juno把这些拼起来 关于模型,几乎所有的一切都源于一个事实:它是一组冻结的数字,用来预测下一个 token。由此产生了变化多样的答案、基于 token 的成本和限制、请求之间没有记忆、训练截止日期,以及幻觉。用这些模型搭建系统的很大一部分工作,就是把正确的文本放到桌子上,并且永远不要盲目信任输出。

一个事实生成了整整一章:模型是一个冻结的函数,根据概率分布来预测下一个 token。梳理它的连锁后果,你就得到了你的心智模型:

  • 预测加上采样,意味着输出会有变化;温度决定变化的幅度。
  • Token 是成本的单位、限额的单位,也是字符级任务应该放进代码里的原因。
  • 上下文窗口是有限的、无状态的,所以你要每次都重新发送并整理模型需要的一切。
  • 知识在某个截止日期被冻结,也不附带来源,所以时效性和私有的事实要靠检索,不能靠回忆。
  • 幻觉是结构性的,只能靠扎根和验证来降低,靠措辞是降不下去的。

贯穿所有这些的模式是一样的:在正确的时间,用正确的形态,把正确的文本放进上下文,然后核实返回的结果。这正是这本手册接下来要搭建的东西,从开放与封闭模型的全景图开始,然后是提示词工程。

Juno把这些拼起来 整章内容就是一个事实和它的连锁后果:一个冻结的下一 token 预测器,带来了变化多样的输出、基于 token 的成本和限制、无状态、知识截止日期,以及幻觉。接下来一直要做的工作是不变的:在正确的形态下把正确的文本放进上下文,然后核实返回的结果。提示词工程就是你开始动手做这件事的地方。

这里的一切都可以归结为一句话:一个冻结的函数,在你提供的文本之上预测下一个 token,没有状态、没有真实性信号、也没有计划。你要处理的每一个生产环境特性,都是它的推论:

  • 输出是随机的,所以你测试的是分布,而不是单次运行。
  • 成本和延迟跟着 token 走,尤其是输出 token,所以你要做预算、用流式传输。
  • 上下文是有限的、无状态的,所以记忆是一个带有各种失败模式的工程选择。
  • 参数化知识是冻结的、有损的、不附带来源的,所以任何承重的事实都要扎根并附上引用。
  • 置信度是没有校准过的,所以你要核实,而不是信任。

这一切带来的好处是,当模型发生变化时,这些原则不会跟着变。锁定一个版本,围绕这些失败模式搭建好防护体系,那么下一次发布就是一次你可以评估的升级,而不是一个你必须硬吞下去的意外。这本手册剩下的内容,正是在搭建这套防护体系,从全景图开放与封闭模型开始,然后是提示词工程。

Juno把这些拼起来 一句话贯穿整章:一个冻结的、无状态的下一 token 预测器,没有真实性信号。随机的输出、由 token 驱动的成本、有限的上下文、冻结的知识、未经校准的置信度,全都是它的推论,你的防护体系存在的意义就是把每一个都遏制住。好处在于,模型变化时这些原则不会跟着变,所以要锁定版本,围绕这些失败模式来搭建系统。