首页 / 文章 / 下一个标记循环:在接触智能体之前了解大语言模型的思维模型

下一个标记循环:在接触智能体之前了解大语言模型的思维模型

通过简单的离线Python示例,了解令牌、上下文窗口、采样以及生成循环的工作原理,进而理解为何会有RAG、ReAct和LangGraph这些技术存在。

1143 词

通过 LangChain 等框架接触大语言模型的工程师往往在系统出问题时无法判断是框架、其封装的 API,还是模型本身存在故障。解决之道在于建立清晰的思维模型:大语言模型本质上是一个在循环中反复调用的下一个标记预测函数。有了这个认知后,RAG 是用于选择输入该函数的素材的方式,ReAct 是用于解析函数输出结果的方式,而 LangGraph 则是用于协调多次调用的工具。以下示例可在本地使用 Python 及一个小型库运行,无需 API 密钥。

生成过程即循环中的下一个标记预测

给定一系列标记后,模型会为每个可能的下一个标记返回概率值。系统会从中选择一个标记并将其添加到序列中,然后将更长的序列重新输入模型。这一过程会持续进行,直到遇到停止标记或达到长度限制。流式输出只是将这个循环中的标记逐个展示出来而已。

由此会产生两个后果:

  • 没有规划性。每个标记仅依赖于其前的标记。
  • 调用之间没有记忆功能。每个请求都从你发送的标记开始,这就是需要检索增强生成技术的原因。

标记是子词单元,而非完整单词

模型看到的是整数而非文本。分词器负责将字符串映射为标记ID,然后再反向转换。常见单词通常会被合并为一个标记,而罕见单词和代码则会拆分成多个标记。一个简单的估算:一个标记大约相当于一个单词的四分之三。

该示例通过tiktoken加载gpt-4o系列模型的编码o200k_base,对句子进行编码,并输出每个标记ID及其对应的文本片段。注意“LangGraph”被拆成了两个标记,而开头的空格则属于下一个标记。

import tiktoken

# tokenizer of the gpt-4o model family
enc = tiktoken.get_encoding("o200k_base")

ids = enc.encode("LangGraph orchestrates agents.")
print(ids)
# [30741, 9922, 109873, 1381, 19297, 13]

for i in ids:
    print(i, repr(enc.decode([i])))
# 'Lang' 'Graph' ' orchestr' 'ates' ' agents' '.'

训练与推理是两个独立的阶段

训练是通过在大量文本中预测下一个标记来调整权重。而推理则是使用已冻结的模型进行推导。用户的提示语不会改变模型权重,因此模型所缺乏的信息必须在推理时通过提示语提供。这正是RAG的运作方式。

上下文窗口即模型的整个世界

单次调用只能处理有限数量的标记,这个限制也就是上下文窗口,它同时涵盖了提示语和生成的输出内容。超出此范围的任何信息在当前调用中都不存在。聊天界面之所以显得连贯,是因为应用每次都会重新发送之前的消息;而RAG的存在则是因为完整文档集无法容纳所有内容,因此只能检索其中若干相关的片段。

预算计算其实只是简单的算术题。辅助工具会使用上述分词器统计提示词的数量,然后从128,000个令牌的窗口中减去这些数量以及用于生成答案的预留量,这一数值是针对gpt-4o-mini设定的。不同模型和版本的限制各不相同,因此请查阅您的服务提供商的最新文档。

def count_tokens(text: str) -> int:
    return len(enc.encode(text))

prompt = "Summarize the attached design doc."
window = 128_000     # e.g. gpt-4o-mini
reserve = 1_000      # room for the answer
used = count_tokens(prompt)
print("left:", window - reserve - used)

温度参数控制下一个令牌的选取方式

采样功能是从模型的概率分布中选择一个令牌,而温度参数则用于调整该分布。当温度接近零时,概率最高的令牌几乎总会被选中;而在1.0及以上时,那些概率较低的令牌也有机会被选中。在需要进行信息提取、结构化输出或调用工具时,建议将温度参数设置为0;而在起草内容或进行头脑风暴时,建议将其设置在0.7到1.0之间。我们的关于温度参数、top-k和top-p的实用指南介绍了其他相关的采样设置。

微型循环的构建

下一个示例将真实的分词器与基于查找表生成的虚拟模型结合使用。虽然规模很小,但它遵循与实际大型语言模型相同的流程:编码、预测、追加、停止。

首先,对小型语料库进行分词,并统计每对相邻的标识符出现次数。对于每个标记,表格仅保留出现频率最高的后续标记,这使得该预测器属于贪心型预测器,相当于温度值为零的情况。

CORPUS = (
    "the agent calls the model. "
    "the model returns a token. "
    "the agent calls the tool. "
    "the tool returns a result."
)

ids = enc.encode(CORPUS)
counts = {}
for a, b in zip(ids, ids[1:]):
    counts.setdefault(a, {})
    counts[a][b] = counts[a].get(b, 0) + 1

# greedy "model": token -> likeliest successor
table = {
    a: max(s, key=s.get)
    for a, s in counts.items()
}

循环会对提示语进行编码,查找最后一个标记最可能的后续标记,将其追加后再输出解码后的文本。若找不到合适的后续标记,则返回None,以此作为停止标记。追加操作是核心步骤:输出内容会变成新的输入。

seq = enc.encode("the agent calls")
for _ in range(3):
    nxt = table.get(seq[-1])
    if nxt is None:    # our toy "stop token"
        break
    seq.append(nxt)    # output becomes input
    print(enc.decode(seq))

序列每一步都会增加一个标记:

the agent calls the
the agent calls the model
the agent calls the model.

有一个细节很有启发性。在语料库中,“the”后面出现“model”和“tool”的频率相当,而max函数会保留它最先遇到的那个词。真实的模型会不断面临这样的接近情况,这也正是采样设置如此重要的原因。

运行示例

可以将这些代码片段收集到一个脚本中,例如examples/part01_tokens.py,同时准备一个列出tiktokenrequirements.txt文件。该脚本会输出分词结果,进行预算计算,并在缓存了分词器数据后大约一秒内完成模拟循环。之后更改语料库,观察输出的变化。

pip install -r requirements.txt
python examples/part01_tokens.py

关键要点

  • 大型语言模型本质上是一个从词元序列到下一个词元分布的固定函数,通过反馈循环来运行。
  • 它仅知晓自身的权重及上下文窗口,永远不会从你的提示中学习。
  • 多样性源自采样,由温度参数控制。
  • 智能体技术决定了哪些标记会被输入,以及输出后的标记将如何处理。
  • 相关阅读