停止符(Stop Token)是 LLM 词表中的一个特殊标记,表示文本到此结束。当模型生成它时,生成循环就会停止输出。

在这篇博客中,我们将学习 LLM 中的停止符。我们还会了解为什么需要告诉 LLM 何时停止、模型在训练过程中如何学会生成停止符、它与我们自己设置的停止序列有何不同、当模型永不停止时会发生什么、聊天模型如何使用它,以及我们在使用它时常犯的错误。

我们将涵盖以下内容:

  • 什么是 Token?
  • LLM 是如何生成文本的?
  • 为什么 LLM 需要停止?
  • 什么是停止符(Stop Token)?
  • 模型如何学会生成停止符?
  • 停止符与停止序列
  • 如果模型永不停止会怎样?
  • 聊天模型中的停止符
  • 使用停止符的常见错误

我是 Amit Shekhar,Outcome School 的创始人。我曾指导和培养过许多开发者,他们的努力帮助他们获得了高薪技术工作,帮助许多科技公司解决了他们的独特问题,并创建了许多被顶级公司使用的开源库。我热衷于通过开源、博客和视频分享知识。

我在 Outcome School 教授 AI 和机器学习。

让我们开始吧。

什么是 Token?

在深入探讨停止符之前,我们必须先了解什么是 token。

Token 是 LLM(大语言模型) 读取和写入的一小段文本。

LLM 不会逐字母地读取文本,也不会逐词地读取文本。它以称为 token 的小片段来读取文本。

一个 token 可以是一个完整的单词、单词的一部分、单个字符、一个空格或一个标点符号。

假设我们有如下句子:

I love learning.

LLM 会将其拆分为如下 token:

["I", " love", " learning", "."]

在这里,我们可以看到每个片段都是一个 token。"love" 前面的空格是 token 本身的一部分。模型就是这样来追踪一个单词在哪里结束、下一个单词从哪里开始的。

每个 token 都有一个对应的编号。模型并不直接处理文本,而是处理这些编号。模型所知道的所有 token 的完整列表称为它的词表。

为了便于理解,可以把词表想象成一本大词典,其中每个词条都有一个序列号。模型只看到这些序列号。

我们有一篇关于 LLM 中的分词(Tokenization) 的详细博客,逐步讲解了文本是如何被拆分为 token 的。

现在我们知道了什么是 token,接下来看看 LLM 如何使用 token 来书写文本。

LLM 是如何生成文本的?

LLM 一次生成一个 token 来书写文本。 这种书写过程称为生成。

它不会一次性写出完整的答案。它会挑选一个 token,将其添加到文本中,然后根据目前已生成的所有内容挑选下一个 token。

学习这一点的最好方法是通过一个例子。

我们提供给模型的文本称为 prompt(提示词)。假设我们的 prompt 如下:

The capital of India is

模型的工作方式如下:

第 1 步: 它查看 "The capital of India is",并挑选下一个 token:" New"

第 2 步: 它查看 "The capital of India is New",并挑选下一个 token:" Delhi"

第 3 步: 它查看 "The capital of India is New Delhi",并挑选下一个 token:"."

在每一步中,模型做的事情都一样。它会查看目前为止的完整文本,并为其词表中的每个 token 打分。得分高的 token 更可能成为下一个 token。然后,根据这些分数挑选出一个 token,并将其添加到文本中。

这个循环有一个名字,自回归生成。

自回归 = Auto + Regressive

Auto 意为自身。Regressive 在这里指回到已经生成的内容。也就是说,模型将自己的输出反复作为输入反馈给自己。

计划转型进入 AI 领域?

无论你今天在哪个技术领域工作,只要付出努力,你都可以转型到以下这些角色:

  • AI 工程师
  • 前沿部署工程师
  • AI Agent 工程师
  • Harness 工程师
  • 语音 AI 工程师
  • AI 架构师
  • LLM 工程师
  • LLM 推理工程师
  • 生成式 AI 工程师
  • MLOps 工程师
  • 机器学习工程师
  • 深度学习工程师

我们在 Outcome School 的 AI 与机器学习课程能带你实现这一目标。

我们可以把这个循环写成如下形式:

tokens = tokenize(prompt)

while True:
    next_token = model.predict_next(tokens)
    tokens.append(next_token)

这里,

tokenize

将 prompt 拆分为 token。

while True

表示循环不断运行,没有终点。在循环内部,我们向模型请求下一个 token,将其添加到列表中,然后再次请求。

仔细看看这个循环。它什么时候结束?

为什么 LLM 需要停止?

我们刚才看到的循环没有出口。其中没有任何东西表示“你完成了”。

如果没有任何东西让它停下来,模型就会一直生成下去。在 "New Delhi." 之后,它会挑选另一个 token,再一个,再一个。它会开始写德里的历史,然后写天气,然后写别的东西。它永远不会自行停止。

所以,我们需要一种方法来告诉模型答案何时完成。

有两种方法可以停止这个循环:

  • 我们设定一个固定的上限。比如说,在 100 个 token 之后停止。
  • 我们让模型自己说“我完成了”。

第一种方法很简单,但仅靠它是不够的。如果答案只需要 5 个 token,我们就会浪费时间和金钱去多生成 95 个无用的 token。而如果答案需要 200 个 token,我们就会在中间把它截断。

第二种方法好得多。模型知道答案何时完成。它只需要一种方式来表达这一点。

于是,Stop Token(停止 token)应运而生。

什么是停止符(Stop Token)?

停止标记(Stop Token)是模型词表中的一个特殊标记,意思是“文本到此结束”。

它不是一个普通的词,也不是我们读到的答案的一部分。它是一个隐藏的标记,当模型认为答案已经完成时就会生成它。

当生成循环看到这个标记时,就会停止,不再生成任何新的标记。

普通的句号"."结束一个句子,而停止标记结束整个回复。

不同的模型对这个标记有不同的命名,常见的有:

  • <|endoftext|>
    used by GPT-2 and many older models
  • </s>
    used by Llama 2 and many models built on top of it
  • <|eot_id|>
    used by Llama 3
  • <|im_end|>
    used by Qwen and many other chat models
  • <end_of_turn>
    used by Gemma

标记的名称并不重要,重要的是它是词表中的一个标记,有自己的编号,而生成循环会把它当作停止的信号。

很多人也把它称为 EOS token。EOS 是 End Of Sequence(序列结束)的缩写。序列就是完整的文本,所以 End Of Sequence 意味着文本结束。Stop Token 和 EOS token 通常指同一个东西。

我们更新后的带有停止标记的循环:

tokens = tokenize(prompt)

while True:
    next_token = model.predict_next(tokens)
    if next_token == STOP_TOKEN:
        break
    tokens.append(next_token)

print(detokenize(tokens))

在这里,我们增加了一个检查。在把标记加入列表之前,先看它是不是停止标记。如果是,我们就

break

,也就是跳出循环并停止。停止标记永远不会被加入列表,所以它不会出现在最终文本中。最后,

detokenize

把这些标记重新拼接成文本。

让我们再看一遍带有停止标记的示例。

第 1 步: " New"

第 2 步: " Delhi"

第 3 步: "."

第 4 步:

<|endoftext|>

循环停止。

展示给我们的输出:"The capital of India is New Delhi."

完美运行。

在这里我们可以注意到,停止标记和其他标记一样是由模型生成的。它在每一步都会得到一个分数。在第 1 步,停止标记的分数非常低,因为"The capital of India is"显然还不完整。在第 4 步,它得到了最高分,因为答案已经完整了。

要学习 Tokenization、LLM 基础知识和 LLM 内部原理,并从零开始构建一个大语言模型(LLM),请查看 Outcome School 的 AI 与机器学习课程。

现在,下一个重要的问题是:模型如何知道什么时候该生成这个标记?

模型如何学会生成停止符?

模型并没有被赋予“回答完问题后停止”这样的规则。它是从数据中学到的,就像它学习其他一切东西一样。

让我们来了解一下。

LLM 是在海量文本上训练的。这些文本由许多独立的文档组成,比如文章、书籍、网页、代码文件、对话等等。

在训练之前,每个文档的末尾都会附加一个停止标记。所以,训练数据看起来如下:

This is the full text of document one. <|endoftext|>
This is the full text of document two. <|endoftext|>
This is the full text of document three. <|endoftext|>

在训练过程中,模型只有一个任务。它观察目前为止的文本,并尝试猜测下一个 token。当它猜错时,会被稍微纠正一下。这个过程会发生数十亿次。

现在,想想每个文档结尾处会发生什么。目前为止的文本是完整的文档,而正确的下一个 token 就是停止 token。每当模型在这里没能猜出停止 token 时,它就会被纠正。

停止 token 在这里还承担另一项工作。它标记一个文档在哪里结束、下一个文档从哪里开始。因此,模型学到停止 token 之后的文本是一个全新的开始。

慢慢地,模型学会了一种模式。当一段文本感觉已经完整时——也就是说故事讲完了、问题回答完了、或者代码文件结束了——下一个 token 很可能就是停止 token。

这就是模型能够自行停止的原因。它见过数百万个结尾,并且学会了结尾长什么样。

为了便于理解,可以想象一个听过数千个故事的孩子。过了一段时间,孩子就知道“从此他们幸福地生活在一起”之后就是合上书本。没有人直接教过这条规则,孩子是通过多次见到它而学会的。

停止 token 的工作方式与此相同。它是学出来的,而不是写死的固定规则。

对于聊天模型来说,还有一轮额外的训练,其中数据看起来像用户和助手之间的对话。每条助手回复都以停止 token 结尾。因此,模型学会在回复结束时生成停止 token,从而把发言权交还给用户。我们将在后面的章节中详细讨论这一点。

给你的一个快速提示

无论你在哪个技术领域工作,都要熟悉以下这些主题:

  • LLM
  • RAG
  • MCP
  • Agent
  • 微调
  • 量化

我们把它们全部整合在一个视频中:

AI 工程详解:LLM、RAG、MCP、Agent、微调与量化

不必停下阅读——把它加入书签,等有空时再看。未来的你会感谢现在的你。

现在,让我们回到主题。

既然我们已经知道模型是如何学会生成停止 token 的,接下来看看一个经常与它混淆的概念。

停止符与停止序列

很多时候,人们把这两个术语当作同一个东西来使用。其实不然。

停止 Token 是内置于模型词表中的一个特殊 token。模型在训练过程中学会生成它。

停止序列是我们作为模型的使用者要求系统监视的任意一段文本。当该文本出现在输出中时,生成就会停止。

假设我们正在通过 API 使用一个 LLM。大多数 LLM API 都允许我们传入一个停止序列列表,如下所示:

response = client.completions.create(
    model="some-model",
    prompt="Q: What is 2 + 2?\nA:",
    stop=["\n", "Q:"]
)

这里,

\n

表示换行。我们传入了

stop=["\n", "Q:"]

。这告诉系统:如果模型写了换行或写了 "Q:",就立即停止。

模型完全不知道我们的停止序列。它照常继续生成 token。模型周围的系统会在每个 token 生成后检查输出,一旦发现匹配,就中断生成。

为什么需要这个?因为有时模型不会在我们想要的地方生成停止 token。

在上面的例子中,如果没有停止序列,模型通常会回答"4"然后继续往下写。它会自己开始写"Q: What is 3 + 3?",因为它只是在延续它所看到的模式。在它的训练数据中,一列问答通常会接着更多的问题。所以,停止 token 在这里得不到高分。

停止序列"Q:"捕捉到这一点并将其停止。

我们可以把这个检查加到循环中,如下所示:

tokens = tokenize(prompt)

while True:
    next_token = model.predict_next(tokens)
    if next_token == STOP_TOKEN:
        break
    tokens.append(next_token)
    text = detokenize(tokens)
    if ends_with_any(text, stop_sequences):
        break

这里,在添加每个 token 之后,我们把 token 转换回文本。然后

ends_with_any

检查文本是否以我们的任何一个停止序列结尾。如果是,我们就跳出循环。

为了便于理解,我把停止 token 和停止序列的区别整理成表格。

Stop Token Stop Sequence
它是什么?词表中的一个特殊 token我们选择的任意一段文本
谁决定它?在模型训练时决定由我们在使用时决定
谁生成它?模型本身模型并不知道它
谁检查它?生成循环模型周围的代码
会出现在输出中吗?不会,它会被移除通常会从输出中移除
Example
</s>
,
<end_of_turn>
"\n"
,
"Q:"
,
"###"

在实践中,两者协同工作。停止 token 始终生效。停止序列是我们根据使用场景额外添加的检查。

如果模型永不停止会怎样?

现在,让我们考虑一个糟糕的情况。如果模型从不生成停止 token,也没有出现任何停止序列,会怎样?

这种情况是可能发生的。有时模型会陷入循环,一遍又一遍地重复同一个短语。有时是因为模型训练得不好。有时提示词太不寻常,以至于模型从不觉得文本已经完整。

如果我们只依赖停止 token,生成就会永远运行下去。它会浪费时间、内存和金钱。

还记得我们之前预留的固定上限吗?现在它派上用场了,但只是作为备用手段,而不是主要的停止方式。

这就是为什么每个系统还会设置一个最大 token 上限。它通常被称为

max_tokens

或

max_new_tokens

.

我们更新后的循环包含全部三项检查:

tokens = tokenize(prompt)
generated = 0

while generated < max_tokens:
    next_token = model.predict_next(tokens)
    if next_token == STOP_TOKEN:
        break
    tokens.append(next_token)
    generated += 1
    text = detokenize(tokens)
    if ends_with_any(text, stop_sequences):
        break

这里,循环只在已生成的 token 数量小于

max_tokens

时运行。因此,当以下任一情况最先发生时,生成就会停止:

  • 模型生成了停止 token。
  • 输出中出现了停止序列。
  • 达到了最大 token 上限。

所以,生成循环有三个刹车。停止 token 是自然刹车。停止序列是我们添加的刹车。最大 token 上限是紧急刹车。

大多数 API 还会告诉我们使用了哪种停止方式。返回值是

finish_reason

或

stop_reason

。像

stop

或

end_turn

这样的值表示模型自行以停止标记结束,或者找到了停止序列。像

length

或

max_tokens

这样的值表示是限制截断了输出。

如果我们经常看到

length

,说明我们的限制太小,回答被中途截断了。我们必须增大限制。

我们有一套关于 LLM 基础、微调和 LLM 推理工程的完整课程——欢迎查看 Outcome School 的 AI 与机器学习课程,深入学习掌握这些内容。

聊天模型中的停止符

现在,让我们来看一个真实的使用场景。当我们与 AI 助手聊天时,它是如何知道自己该结束发言的呢?

聊天内容是用特殊标记来格式化的,这些标记标明谁在说话。一个简化版本如下所示:

<|user|>
What is the capital of India?
<|end|>
<|assistant|>
The capital of India is New Delhi.
<|end|>

这里,

<|user|>

和

<|assistant|>

标明谁在说话,

<|end|>

标记一轮发言的结束。这三者都是词表中的特殊标记,就像我们之前看到的停止标记一样。

在训练过程中,模型会看到许多这种格式的对话。每条助手回复都以

<|end|>

结尾。因此,当模型在回答时,一旦它觉得回复已经完整,就会生成

<|end|>

。系统看到这个标记后,停止生成,移除该标记,并把回复展示给我们。然后等待我们的下一条消息。

这里,

<|end|>

就是这个聊天模型的停止标记。在 Llama 3 中,它是

<|eot_id|>

。在 Qwen 中,它是

<|im_end|>

。原理是一样的。

注意: 有些模型有不止一个停止标记。Llama 3 使用

<|end_of_text|>

标记文档结束,使用

<|eot_id|>

标记聊天中一轮发言的结束。两者都被视为停止信号。

聊天模型就是这样知道自己该结束发言的。它不会一直说个没完,也不会替我们写下一条问题。它会停下来,因为它已经学会了自己的发言以这个标记结束。

保持更新: 订阅我们的 新闻通讯,第一时间将最新的 AI 与机器学习博客送到您的邮箱。

使用停止符的常见错误

在使用停止标记时,我们经常会犯错。让我们看看常见的几种。

错误 1:为模型使用了错误的停止标记。

当我们下载一个模型并在自己的机器上运行时,必须使用该模型训练时所用的停止标记。假设该模型被训练为以

<|eot_id|>

结束发言,但我们的设置却在等待

</s>

。模型会生成

<|eot_id|>

,我们的设置无法识别它,模型就会继续生成下去。它会开始自行编写用户的下一条消息。

Llama 3 发布时,很多人都遇到了这个问题。解决办法是在生成设置中设置正确的停止标记。

错误 2:把最大 token 限制设置得太小。

如果限制太小,回答会在句子中间被截断。模型永远没有机会生成停止标记。我们必须始终检查

finish_reason

。如果显示

length

,我们就必须提高上限。

错误 3:在微调数据中忘记停止标记。

微调 是指在一个已经训练好的模型上,用我们自己的数据再进行一点训练。当我们微调一个模型时,每个训练样本都必须以停止标记结尾。如果我们忘记了,模型就永远学不会在我们的数据类型上何时停止。微调之后,它会在每个回答之后不停地继续输出。

错误 4:把停止标记展示给用户。

有时,把 token 转换回文本的代码没有移除特殊标记。这样用户就会在每个回答的末尾看到

<|endoftext|>

或

</s>

。大多数工具都有一个类似

skip_special_tokens=True

的选项来处理这个问题。我们必须确保它已开启。

现在,我们应该已经理解了 LLM 中的停止标记是什么、模型如何学会生成它们,以及它们如何与停止序列和最大 token 上限协同工作。

常见问题

最终答案中会显示停止标记吗?

不会。停止标记是一个隐藏的标记,不是我们所读答案的一部分。生成循环在看到该标记时就会停止,并且不会把它添加到文本中。如果把 token 转换回文本的代码没有移除特殊标记,用户可能会看到它,因此必须开启类似

skip_special_tokens=True

的选项。

停止标记和 EOS token 是同一个东西吗?

是的。停止标记和 EOS token 通常指同一个东西。EOS 代表 End Of Sequence(序列结束),而序列就是完整的文本。不同的模型给这个标记起了不同的名字,比如

<|endoftext|>

,

</s>

或

<|eot_id|>

,但名字并不重要。

为什么 LLM 会自己继续写用户的下一条消息?

这通常发生在我们的设置等待了错误的停止标记时。如果模型以

<|eot_id|>

结束它的回合,但我们的设置在等待

</s>

,那么模型的停止标记就不会被识别,它会继续输出。Llama 3 发布时很多人都遇到了这个问题。解决办法是在生成设置中设置正确的停止标记。

一个模型可以有多个停止标记吗?

可以。有些模型有不止一个停止标记。Llama 3 使用

<|end_of_text|>

标记文档结束,使用

<|eot_id|>

来标记聊天中一个回合的结束。两者都被视为停止生成的信号。

finish_reason 为 length 是什么意思?

它意味着是最大 token 上限截断了答案,而不是模型。模型根本没有机会生成停止标记,所以答案可能在句子中间就结束了。如果我们太频繁地看到

length

,说明我们的上限太小了,必须提高它。

今天就到这里。

谢谢

Amit Shekhar Outcome School 创始人

你可以通过以下方式与我联系:

关注 Outcome School: