title: "AI 聊天机器人中,从按下回车到第一个单词之间发生了什么?"
source_url: "https://blog.bytebytego.com/p/what-happens-inside-an-ai-chatbot"
author: "ByteByteGo Newsletter"
excerpt: "公共早报 本文剖析了用户消息在 AI 聊天机器人中的多阶段旅程,揭示了在生成第一个单词之前,上下文工程、安全检查、分词以及并行处理之间的复杂相互作用。"
当你向 AI 聊天输入一个后续问题并按下回车时,会有那么一两秒钟什么都没发生。然后答案就以连续的词语形式迅速出现。这看起来比最初那个暂停暗示的要快得多。
这个暂停并非停滞时间。在一个典型的 LLM 中,一条消息在回复开始出现之前要经历大约十几个不同的阶段。两种截然不同的计算工作在后台同时进行,使这一切成为可能。以下是这段旅程的一些关键点:
模型从未收到过你输入的原话。
它对对话没有记忆。屏幕上的历史记录在每次交互中都是从零重新构建的。
它与其他陌生人共享一台机器,而它所归属的那一组会影响回复内容。
一旦一个词被发送出去,模型就无法收回。
在本文中,我们将详细审视这段完整的旅程。以下是我们将涵盖的内容:
模型的输入是如何组装的?
为什么模型收到的每条输入消息都是独立的?
对输入进行安全检查
模型如何理解文字?
模型如何在多个对话之间共享?
预填充和解码步骤
缓存现有计算结果
流式输出与安全护栏
模型如何运行各种工具?
免责声明:本文基于各种来源公开分享的详细信息撰写。参考文献见末尾。如发现任何不准确之处,请留言评论。
模型的输入是如何组装的?
首先要理解的关键点是,你在输入框中输入的句子并不是到达模型的原样内容。在请求发送之前,围绕该句子组装的一份文档才是真正到达模型的东西。
这份文档包含几个要素,如下:
一个系统提示词(system prompt),这是由 LLM 提供商编写的一组指令。这些指令告诉模型在对话期间如何表现。
任何可用工具的定义,描述每个工具的功能及其接受的输入。
早期会话中存储为记忆的任何内容。
在需要检索时从知识库中拉取的文档。
迄今为止的完整对话。
最后是新的消息。
决定将哪些内容以什么顺序放入这份文档、哪些内容被排除在外,这本身是一门学科,称为上下文工程(context engineering)。这并不是简单地向容器里填充内容那么简单。模型的注意力预算有限,每增加一个 token 都会消耗它。
随着输入变长,准确率会下降,即使在相当简单的任务上也是如此。这种下降是渐进的。更长的提示词不会直接破坏任何东西,但底层的精确度会逐渐流失。
这种处理上下文工程学科的方式会导致一种现象:两个基于相同底层模型的产品,即使被问完全相同的问题,也会返回不同的答案。模型可能是一样的,但包裹在问题周围的文档却不同。
不同的提供商在何时收集文档材料方面也有所不同。有些一次性检索所有内容。其他一些则给模型提供轻量级引用、文件路径或存储的查询,让模型在工作时按需拉取。前者更快,后者则减少了对可能无关材料的 token 消耗。

为什么模型收到的每条输入消息都是独立的?
模型在设计上是无状态的(stateless),这意味着它们不会在消息之间保留任何东西。每个请求到达时都不记得之前发生了什么。换句话说,我们在屏幕上看到的对话每次都被完整地重新构建并重新发送。
相关的数学计算相当惊人。假设一个产品有 1000 个 token 的系统提示词,每条消息和回复大约运行 100 个 token。
第一轮处理大约 1100 个 token。
第二轮处理大约 1300 个。
到第三轮,我们有大约 1500 个。
到第二十轮,我们可能有接近 4900 个。
尽管输出 token 比输入 token 更贵,但输入量在每轮交互中都会累积,而输出量基本保持不变。这就是为什么在任何对话产品中,输入通常主导总开销,尽管它是两者中较便宜的那个。
朴素的做法是重新发送所有内容,但它只在短对话中效果良好。一旦对话超出上下文窗口,这就变得不可行了。有几种改进方法可以帮助:
最简单的方法是丢弃最早的轮次。这不会增加太多延迟,但会丢失被丢弃的内容。
更谨慎的版本会总结对话,然后用总结重新开始。这有助于保留决策和悬而未决的问题,同时丢弃没有人需要第二次看到的原始工具输出之类的东西。
第三种方法是将材料完全存储在上下文窗口之外,在相关时才检索。
长对话会变得更慢、更贵,因为每轮都必须重新处理之前的所有内容。助手最终会丢失细节,因为一些细节被修剪或压缩了。此外,编辑早期的消息会重写模型认为已发生的事情。
对输入进行安全检查
在开始生成答案之前,组装好的文档会通过一个单独的、较小的模型来评判该请求是否应该继续。你可以把它想象成一道安全层。
这里需要注意的是设计上的分离。这道安全层是一个独立于助手的系统,这意味着它可以按照自己的节奏进行重新训练、调整和监控,而不会对主模型产生任何影响。它不仅可以允许或阻止请求,还可以将请求路由到其他地方、记录日志,或提交审查。
这种分离需要时间,这体现在答案之前的那个暂停中。一个生产系统的公开数据显示,早一代分类器的计算量增加了约 24%,同时对无害请求的拒绝率上升了 0.38 个百分点。这两个数字都高到限制了这种方法可以部署的范围。
替代方案使用级联(cascade),即用非常便宜的验证来筛选所有流量。只有被标记的对话才会被发送到昂贵的分类器。这将开销降低到约 1%,对无害查询的拒绝率降低到 0.05%。
模型如何理解文字?
下一步,文档被转换成模型工作的单元。这些单元称为 token,它们是文本块,通常比单词小但比字母大。常用词通常用一个片段表示,而较罕见的词会拆分成碎片。
大多数现代系统从原始字节而不是字符开始构建这些块,这保证了任何书写系统的文本都可以被表示。作为一个参考数字,一个 token 平均大约相当于四分之三个英语单词。
这产生了两个后果:
第一个是成本和容量因语言而异。在一个主要机器学习会议上发表的研究测量了同一文本在不同翻译中的 token 数量,发现差异高达 15 倍。这不仅仅是一个计费问题。更高的 token 数量意味着更高的成本、更慢的处理速度,以及更少的内容能塞进相同的上下文窗口。因此,说某些语言的人对于相同的文档获得了更少可用的空间。
第二个是字符级的问题可能会变得尴尬。数一个特定字母在单词中出现的次数意味着要查看块内部的字符。
模型如何在多个对话之间共享?
没有模型会在等待请求到来时闲着。在典型设置中,token 序列加入一个队列,然后与在同一硬件上运行的其他人的请求批次一起处理。
为什么需要这种批处理?
这是因为,虽然现代加速器拥有巨大的计算能力,但其大量内存带宽花在了加载模型参数上,而不是在任何单个请求上执行有用的工作。加载这些参数一次,然后同时将它们应用到多个请求上,这才是使服务变得可负担的原因。
朴素的批处理版本是收集一组请求,一起运行它们,然后等待整个组完成后再开始下一个。当每个响应长度相似时,这是有效的。然而聊天响应长度并不相似。有些可能在 10 个词内完成,而另一些可能运行 1000 个词。因此,硬件可能在等待最长的那个时部分闲置。
更好的方法是在单个生成步骤的级别上调度请求。一旦一个响应完成,一个新请求就接替它的位置,而不是等待整组清空。基准测试已经证明,与朴素方法相比,吞吐量提高了多达 23 倍。此外,这种方法也改善了中位响应时间。
共享资源生成答案还有另一个奇怪的后果。即使关闭了随机性,相同的请求也可能返回不同的答案。发生这种情况是因为所涉及的数值运算对一起处理的请求数量敏感。换句话说,将相同的提示发送一千次给一个大模型,最终可能得到 80 种不同的完成结果。
预填充和解码
这正是暂停和输入分离开来的时刻。
生成回复分两个阶段进行,每个阶段有相反的特性:
第一个阶段在一轮中读取整个组装好的文档。每个输入 token 都与所有其他 token 一起处理。因此,这项工作并行运行,并推动硬件进行原始计算。这个阶段就是那个暂停。
第二个阶段一次生成一个输出 token。每个 token 依赖于它前面的那个,所以没有任何部分可以并行化。这里的限制因素是内存速度而不是计算能力,因为每一步都要读取到目前为止已计算的所有内容。这个阶段就是稳定的输入过程。
缓存现有计算结果
如果你在过去一周内一直在进行一个长对话,你可能已经注意到回复开始前的暂停似乎在变短。
这很可能是因为对话中重复的模式已经被缓存了。
当你发送一条新消息时,你并不只是发送最新的一轮。你发送的是整个对话,包括系统提示、工具定义,以及你所有的早期互动。
每次模型处理这个组装好的文档时,它都会计算每个 token 及其与所有其他 token 的关系。组装文档的前面部分——系统提示词、历史对话——在每轮中都是相同的。
缓存将这种重复计算的结果存储在 GPU 内存中。当新请求到来时,模型只需将其与缓存进行比较,并从中断的地方继续,而不是从头开始重新计算每条消息的每个部分。
这解释了为什么长对话的响应速度有时会快得多——这不仅仅是推理服务变得更忙或更空闲的问题。
你可能已经注意到的另一个效果是,缓存使服务能够同时为多个用户提供服务。如果两个用户有相同的系统提示词(比如他们都在使用同一个应用程序),则可以共享缓存结果。这意味着不必为每个用户重复相同的计算。
缓存现有计算结果意味着这些计算只需完成一次,然后就可以重用。模型不需要重新计算那些已经计算过的部分。
流式输出
这是你看到答案逐字出现的原因。
在第二个阶段——解码阶段——token 被一个一个地生成。第一个 token 是在预填充阶段完成后立即生成的。然后每生成一个 token,它就会被发送到你的设备。
这意味着你在模型完成整个回复之前就开始看到输出。这让你感觉响应更快,即使底层计算没有加速。
这是另一种节省资源的方式。通过在每个 token 生成后立即发送它,服务器和你的设备都不必等待整个完成的消息。
模型还可以在生成过程中进行额外的检查。一旦生成了足够的 token 来形成一个看起来像有害请求的东西——即使完整请求还没有完成——模型可以提前停止。
这是一种更聪明的方式来处理一个否则会很尴尬的问题:等待完整的回复来检查它是否安全,然后在发现问题时丢弃它。
安全护栏在流式输出过程中仍在运行。如果模型开始偏离安全轨道,它可以停止生成,即使那意味着给用户一个不完整的句子。
模型如何运行各种工具?
这是现代 LLM 与早期模型真正不同的一个领域。
当你要求模型使用工具时——比如网络搜索、代码执行或访问数据库——会发生以下情况:
首先,模型决定它需要什么信息以及以什么顺序。它制定一个"计划",分解复杂的任务。
然后,当模型认为它需要使用工具时,它不会只是"调用"该工具。取而代之的是,它生成一条特殊消息——通常称为工具调用或操作——指定它想要使用的工具及其参数。
这个工具调用然后被发送回系统,系统执行实际的工作,执行计算或从外部来源获取数据。然后将结果插入对话中,模型继续处理。
这就是上下文工程变得重要的地方。模型需要在正确的上下文中才能知道何时使用工具、 使用哪个工具,以及如何处理结果。如果上下文组装不正确,模型可能会使用错误的工具或误解结果。
一个 AI 请求的完整生命周期
让我们一步步走过整个旅程:
你输入你的消息 并按回车。
上下文被组装:你的消息与系统提示词、工具定义、对话历史和任何相关记忆被组合在一起。
安全检查:文档通过一个单独的安全模型进行检查——这需要一点时间。
分词:文本被分解成 token。这对非英语语言的影响不同。
批处理:你的请求与其他人的请求分组在一起,以共享 GPU 资源。
预填充:模型并行处理整个输入文档。
缓存检查:重复的模式被重用,而不是重新计算。
解码开始:第一个 token 被生成并发送。在你看到任何东西之前,预填充完成了。
流式输出开始:第一个 token 被发送到你的设备。你开始看到输出。
工具调用:如果模型需要外部数据,它会暂停生成、调用工具、获取结果,然后继续。
安全护栏:内容在流式输出过程中仍在被检查。
完成:完整的回复被生成——或者在安全护栏提前停止时没有完成。
整个过程通常只需要几秒钟,但我们现在知道在那段时间里发生了相当多的事情。
理解这些有什么帮助?
了解这些内容可能不会改变你使用聊天机器人的方式,但它可能有助于解释一些常见的问题:
为什么回复有时会在中间停顿? 这可能是因为模型正在调用工具或等待外部数据。
为什么非英语语言有时响应较慢或成本较高? token 化对不同语言的影响不同,某些语言的 token 消耗量是其他语言的好几倍。
为什么在长对话中我会丢失细节? 随着对话变长,上下文窗口会被耗尽,历史记录会被压缩。
为什么有时候相同的问题会得到不同的答案? 批处理、缓存命中的变化,以及模型对共享硬件的依赖都会影响结果。
参考文献
本文基于以下来源构建:
OpenAI 关于 GPT 模型工作原理的公开文档
Anthropic 关于 Claude 对话原理的博客文章
各种 LLM 提供商关于分词、缓存和推理优化的技术文档
机器学习社区关于 transformer 架构和推理优化的公开研究
这是 ByteByteGo Newsletter 的公共早报系列文章之一。








