搞懂 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,打印出来看看数组长啥样、相似度数值是多少,比看十篇概念文章都管用。跑通了记得回来留个言,我也想看看你是怎么理解这俩概念的。

相关推荐
冬奇Lab4 小时前
每日一个开源项目(第139篇):Voicebox - 本地运行的开源 ElevenLabs 替代品
人工智能·开源·资讯
冬奇Lab4 小时前
Skill 系列(03):Skill 设计范式——5 个模式让输出从混沌到可预测
人工智能·开源·agent
Hyyy5 小时前
Temperature 与 Top-p:控制模型输出的两个参数
llm·ai编程
IT_陈寒6 小时前
Python搞不定字符串编码?这破玩意坑我两小时!
前端·人工智能·后端
Darling噜啦啦7 小时前
LLM 无状态本质与上下文工程:从 Prompt 到 Context 的进化——为什么 AI 总是"失忆"?
llm
星始流年7 小时前
从 Tool 到 Skill——基于 LangChain 的服务端Skill实现
前端·langchain·agent
凌奕7 小时前
让你的 AI 编程助手「偷懒」:50k Star 的 Ponytail,让 Agent 少写一半代码
chatgpt·agent·claude
大模型真好玩7 小时前
什么是Loop Engineering?最通俗易懂的Loop Engineering核心概念
人工智能·agent·deepseek