搞懂 Token 和 Embedding 后,我终于明白大模型是怎么 "读" 文字的

说实话,之前写了不少调用大模型 API 的代码,但一直停留在 "传 prompt、等结果" 的黑盒阶段。我知道大模型处理的是数字,也听过 token、embedding 这些词,但它们到底在整条链路里扮演什么角色,为什么缺一不可,脑子里始终是一团浆糊。

直到这周啃完相关资料,自己动手跑了几遍 demo,才算把这条链路串了起来。这篇就顺着我当时踩坑的思路,把 token 分词和 embedding 向量化这俩最基础的概念掰扯清楚。

先从一个最朴素的疑问说起

最开始我特别纳闷:神经网络不就是算数字吗,那直接把每个字符转成 ASCII 或者 Unicode 编码喂进去不行吗?为啥非要搞个 token 出来,还得再做一次 embedding?

我第一反应是 ------"应该是性能问题吧",字符太多的话计算量太大。后来去翻了讲分词的资料,才发现事情没这么简单。

你想啊,如果按单个汉字或者字母来处理,"天" 和 "地" 在计算机里就是两个独立的数字,模型根本没法直观感受到它们之间有啥语义关联。更麻烦的是,像 "苹果" 这种词,拆成 "苹" 和 "果" 两个字,语义就碎了。模型得自己一点点学 "苹 + 果" 合在一起是一种水果,效率低得离谱。

所以分词的本质,就是找一个既不太大也不太小的语义单元。比整句话小,比单个字符大,大概对应我们常说的 "词" 或者 "词根"。模型在这个粒度上学语义,效率最高。

动手跑一下 token 编码解码

光说没用,我直接找了个 js 版本的分词库 js-tiktoken 跑了一下。OpenAI 官方用的就是 tiktoken,这个是 JavaScript 实现版,规则完全一致。

安装很简单:

bash 复制代码
pnpm i js-tiktoken

然后写了个最小 demo,就测一句话:

javascript 复制代码
    import { getEncoding } from 'js-tiktoken';

    // 用 gpt 官方的 cl100k_base 分词规则
    const enc = getEncoding('cl100k_base');

    const text = "hello world! 你好,世界!";

    // 编码:文本 → token ID 数组
    const tokens = enc.encode(text);
    console.log("Token IDs:", tokens);
    console.log("tokens.length:", tokens.length);

    // 解码:token ID 数组 → 文本
    const decodedText = enc.decode(tokens);
    console.log("Decoded Text:", decodedText);

跑出来的结果是这样的:

我当时数了一下:英文部分 "hello"、"world"、"!" 占了 3 个,中文 "你好,世界!" 占了 8 个。算下来差不多一个中文字对应 1.3 个 token 左右,和网上说的 "1 个中文字符 ≈ 0.6 个 token" 刚好对上。

冷知识计价也是按 token 算的,输入输出加在一起计费。所以平时写 prompt 别瞎啰嗦,字字都是钱。国产模型百万 token 大概几块钱人民币,听着便宜,但架不住量大。

这里我踩了第一个坑:一开始我以为 token 就是 "单词",跑了几个例子发现根本不是。比如 "world" 前面带了个空格也算一个 token,有些长单词会被拆成词根 + 后缀,中文更是经常一个字就占一个 token。

它不是按我们语言学上的 "词" 来切的,是按 cl100k_base 这套映射规则来的。这套词表里有大概十万个左右的 token ID,从 0 开始编号。文本进来之后,就按最大匹配的方式切成表里已有的片段,每个片段对应一个数字 ID。

说白了,token 就是大模型的 "识字表" 。模型出生的时候就只认识这十万个符号,再多的它就不认得了。你输入任何文字,最终都会被翻译成这张表里的数字 ID 序列,再送进模型。

光有 token ID 还不够,得变成 "有语义的数字"

到这一步,文本变成了一串数字,比如 [15339, 1917, 0, 220, ...]

那直接把这串数字丢进神经网络行不行?

我当时觉得也行吧,反正都是数字。后来才明白,差远了。

这些 ID 本质上就是个编号,跟食堂的取餐号一样,只有 "是不是同一个" 的意义,没有 "距离远近" 的概念。比如编号 123 和 124,虽然数字挨在一起,但语义上可能八竿子打不着;而 "猫" 和 "猫咪" 对应的 ID 可能差了好几万,但语义几乎一样。

模型拿这种纯编号算不动。它需要的是 ------每个 token 都对应一组数字,数字和数字之间的 "距离" 能反映出语义的远近

这就是 embedding 干的事:把每个 token ID 映射成一个高维向量。向量是几百到几千维的浮点数,每一维都代表某种说不清的语义特征。两个向量离得近,就说明语义相似;离得远,就说明语义差得远。

跑个 embedding + 余弦相似度的 demo

我直接调用了通义千问的 embedding 接口,又手写了个余弦相似度计算,测了三句话。

完整代码长这样:

javascript 复制代码
    import OpenAI from 'openai';
    import dotenv from 'dotenv';
    dotenv.config();

    const client = new OpenAI({
      apiKey: process.env.DASHSCOPE_API_KEY,
      baseURL: process.env.DASHSCOPE_API_BASE_URL,
    });

    // 文本 → 高维向量
    async function getEmbedding(text) {
      const res = await client.embeddings.create({
        model: 'text-embedding-v4',
        input: text,
        dimension: 1024, // 向量维度,维度越高语义越精细,计算量也越大
      });
      return res.data[0].embedding;
    }

    // 余弦相似度:值越接近 1,语义越相似
    function cosineSimilarity(vecA, vecB) {
      let dot = 0, magA = 0, magB = 0;
      for (let i = 0; i < vecA.length; i++) {
        dot += vecA[i] * vecB[i];
        magA += vecA[i] ** 2;
        magB += vecB[i] ** 2;
      }
      return dot / (Math.sqrt(magA) * Math.sqrt(magB));
    }

    async function run() {
      const text1 = 'Embedding is a process of converting text into a numerical representation.';
      const text2 = "Embeding 是将文本转换为数值表示的过程。";
      const text3 = "我是一个学生";

      const vec1 = await getEmbedding(text1);
      const vec2 = await getEmbedding(text2);
      const vec3 = await getEmbedding(text3);

      console.log(cosineSimilarity(vec1, vec2)); // 中英文同一个意思,相似度应该很高
      console.log(cosineSimilarity(vec1, vec3)); // 完全不相关,相似度应该很低
    }

    run();

跑出来的结果我截了图:

结果挺有意思的:

  • 一句英文一句中文,说的完全是同一件事,相似度达到了 0.88
  • 一句讲 embedding 和一句 "我是一个学生",相似度只有 0.19

要知道这两句话字面上来讲没有任何一个字是重合的,但 embedding 就是能把 "语义" 给抓出来。这就是向量化最神奇的地方 ------ 它把 "意思" 翻译成了数学上的 "距离"。

我当时盯着这两个数字愣了几秒。以前总听人说 RAG、语义检索,原理其实就这么简单:把知识库所有段落都转成向量存起来,用户提问的时候也转成向量,去库里找距离最近的几个段落,就是最相关的内容。

整条链路串起来看

到这儿基本就通了。我在代码注释里写了一行,现在回头看还挺准确的:

plaintext 复制代码
    prompt(文本输入) 
      → tokens(分词器编码成数字ID) 
        → embedding(向量化,带上语义) 
          → LLM(Transformer 一顿算) 
            → tokens(输出ID) 
              → 解码成文本

画个示意图的话大概是这样:

每一步都有它不可替代的作用:

  • 分词是为了找到合适的语义单元,把无限的文本映射到有限的词表里
  • Embedding 是为了把离散的编号变成连续的向量,让模型能计算语义关系
  • 最后 Transformer 在向量空间里做注意力计算,预测下一个 token 是什么

之前我还疑惑过 "为什么 embedding 之后还要经过那么多层 Transformer",现在想明白了 ------embedding 只是给每个 token 准备了一个初始的语义向量,还带着上下文无关的 "字面意思"。经过多层注意力之后,每个位置的向量才会融合整句话的上下文,变成 "在当前语境下的意思"。

比如 "苹果" 这个词,单独的 embedding 可能更偏向水果;但在 "苹果发布了新手机" 这句话里,经过注意力层之后,它的向量就会往 "科技公司" 那边靠。

我踩过的几个坑

坑一:以为 token 数和字数差不多

最开始估算费用的时候,我按 "一个字算一个 token" 粗算,结果实际跑出来差了不少。中文其实是 1 个字约等于 1.3 个 token,英文反而更省,一个单词可能才 1 个 token。长文本算费用的时候,中文要往多了估。

坑二:embedding 向量直接拿来做精确匹配

我一开始想当然地拿向量去做等值比较,后来才反应过来,浮点数哪有完全相等的。语义相似性看的是相对距离,不是绝对相等。一般余弦相似度在 0.8 以上就算高度相关了,0.5 左右就是弱相关。

坑三:维度越高越好

dimension 参数我一开始无脑选最大的,后来发现很多场景 1024 维完全够用。维度越高精度确实会好一点,但存储和计算成本也线性上涨。做召回的话,几百万条向量差几百维,索引体积差很多。

注意不同模型产出的 embedding 向量不能混着算相似度。A 模型的向量和 B 模型的向量,不在同一个语义空间里,算出来的距离没有意义。

最后说几句

其实搞懂这俩概念之后,再去看什么 RAG、Agent、微调,心里就有底了。它们都是在这条基础链路之上玩出来的花样,底层逻辑没变 ------ 大模型自始至终都在跟向量打交道,从来没真的 "读懂" 过文字。

我觉得最关键的收获有三点:第一,token 是计价和处理的最小单位,不是字也不是词,是词表中的一个编号。写 prompt、做上下文窗口估算都得用它来算。第二,embedding 把离散符号变成了连续语义空间,"语义相似" 这件事才能用数学来计算。这是所有语义检索、聚类、分类的基础。第三,这两步只是前置处理,真正的 "理解" 发生在 Transformer 的注意力计算里。但没有这两步,后面的一切都无从谈起。

当然这东西也不是万能的。embedding 擅长抓整体语义,但对精确的数字、专有名词、格式要求经常抓不准,所以 RAG 才需要配合各种召回策略,不能光靠向量相似度一条路走到黑。

如果你之前也对这块一知半解,建议自己动手跑一遍 tiktoken 和 embedding 的 demo,打印出来看看数组长啥样、相似度数值是多少,比看十篇概念文章都管用。跑通了记得回来留个言,我也想看看你是怎么理解这俩概念的。

相关推荐
fai厅的秃头姐!33 分钟前
OpenCV实战篇
人工智能·opencv·计算机视觉
TMT星球1 小时前
虎鲸文娱春苗编剧计划全面升级,已孵化20个剧本进入前期开发
大数据·人工智能
奔跑中的小象8 小时前
统信UOS + 天数AI卡部署SGLang服务手册
人工智能·uos·sglang·天数智芯
DevSecOps选型指南8 小时前
中国版Mythos,为何是悬镜安全灵脉CodeAI?
人工智能·安全
Shockang8 小时前
LangGraph 状态机实战
人工智能
刹那芳华19928 小时前
循环神经网络的从零开始实现(RNN)
人工智能·rnn·深度学习
阿童木写作8 小时前
跨境图片翻译工具多合一,批量图片视频字幕翻译加智能抠图
人工智能·python·音视频·语音识别
科技之门8 小时前
AI3D从建模到贴图、绑骨和动画的完整流程怎么做?V2Fun完整工作流指南
人工智能·3d·贴图
宇宙第一小趴菜8 小时前
二、机器学习的应用领域和发展史
人工智能·机器学习
前端开发江鸟8 小时前
我写过 MCP Server,却一直以为 MCP 只有 Tool
人工智能