(余弦相似度...实战)从 Token 到 Embedding:用 JavaScript 理解大模型怎么“看懂”文本
最近在学 AI Agent发现一个绕不开的概念Token。刚开始我以为 Token 只是“计费单位”比如输入多少 token、输出多少 token、百万 token 多少钱。后来写了一点 demo 才发现Token 不只是计费问题它其实是大模型处理文本的第一步。这篇文章用 JavaScript 做两个小实验用js-tiktoken把文本编码成 token再解码回文本用 embedding 模型把文本变成向量再计算两段文本的语义相似度项目结构大概是这样Token ├── readme.md └── demo ├── index.mjs ├── main.mjs ├── package.json └── .envindex.mjs用来演示 Token 编码和解码。图中Hello, tiktoken! 你好世界被划为13个tokenmain.mjs用来演示 Embedding 和余弦相似度。余弦相似度(cos)越接近1就说明意思越接近。一、学习 LLM为什么要先理解 Token如果只是使用 ChatGPT、Claude、通义千问、豆包这类工具可以不关心 Token。但如果要做 AI 全栈、AI Agent、RAG、知识库、智能客服、代码助手就必须理解 Token。原因很直接用户输入的文本不会被大模型直接理解。大模型真正处理的是数字。文本进入模型之前会先经过 tokenizer被切成一个个 token然后映射成 token ID。可以粗略理解为文本 -token -token ID -embedding 向量 -模型计算 -输出 token -文本完整一点的流程是Prompt 文本 ↓ Tokenizer 分词器 ↓ Token IDs ↓ Embedding 向量化 ↓ Transformer 模型计算 ↓ 输出 Token IDs ↓ Decode 解码成文本所以 Token 不是一个孤立概念它连接了文本、计费、上下文长度、embedding、模型推理。二、Token 到底是什么Token 可以理解成大模型处理文本的最小单位。但注意Token 不一定等于“一个字”也不一定等于“一个单词”。比如英文里一个单词可能被切成一个 token也可能被切成多个 token。中文里一个汉字可能对应一个 token也可能和周围字符组合成 token。所以不要把 Token 简单理解成“字数”。更准确的说法是Token 是 tokenizer 按照特定规则切出来的文本片段。然后每个 token 会被映射成一个数字 ID。大模型真正处理的是这些 ID 背后的向量表示。三、为什么必须分词计算机不能直接理解中文、英文、标点符号这些东西。神经网络处理的是数字通常是向量、矩阵这类数学对象。所以模型不能直接吃这段文本你好大模型它需要先把文本转成数字。大概过程是你好大模型 ↓ 切成 token ↓[token1, token2, token3]↓ 映射成 token ID ↓[57668,53901,3922,...]这些 ID 再进入 embedding 层变成高维向量。这一步之后模型才有办法用数学方式计算文本之间的关系。四、Token 和计费、上下文长度的关系我们平时调用大模型 API经常会看到这些说法输入 token 输出 token 上下文窗口 百万 token 价格这些都和 Token 有关。一次 API 调用的总 token 通常可以理解为总 token输入 token 输出 token输入 token 就是你发给模型的 prompt、历史对话、系统提示词、工具结果等。输出 token 就是模型生成的回答。如果你做 AI Agent这个点尤其重要。因为 Agent 往往会带很多上下文系统提示词 用户问题 历史对话 工具调用结果 RAG 检索片段 代码文件内容 中间思考过程或执行记录这些都会占 token。上下文塞得越多成本越高响应越慢也更容易超过模型上下文限制。五、项目依赖package.json里主要用了三个依赖{dependencies:{dotenv:^17.4.2,js-tiktoken:^1.0.21,openai:^6.44.0}}分别作用如下dotenv读取 .env 环境变量 js-tiktoken在 JavaScript 中计算 token openai调用兼容 OpenAI SDK 的模型 API安装方式pnpminstall或者pnpmi dotenv js-tiktoken openai六、用 js-tiktoken 做 Token 编码和解码先看index.mjs。import{getEncoding}fromjs-tiktoken;const encgetEncoding(cl100k_base);const textHello, tiktoken! 你好世界;const tokensenc.encode(text);console.log(Token IDs:, tokens, tokens.length);const decodedTextenc.decode(tokens);console.log(Decode Text, decodedText);这里最核心的是const encgetEncoding(cl100k_base);cl100k_base是一种常见的 GPT 系列 tokenizer 编码规则。可以把它理解成一张映射表文本片段 -token ID然后这行代码负责编码const tokensenc.encode(text);它会把文本转成 token ID 数组。比如Hello, tiktoken!你好世界会被转成类似这样的数组[9906,11,87272,...]具体数字不重要重要的是理解这个过程文本被 tokenizer 转成了模型能处理的数字 ID接着这行代码负责解码const decodedTextenc.decode(tokens);也就是把 token ID 再还原成文本。所以encode和decode是一对操作encode文本 -token ID decodetoken ID -文本七、为什么要自己统计 Token如果只是聊天可以不用管。但写 AI 应用时自己统计 Token 很有用。比如做 RAG 知识库时不能把整篇文档全部塞给模型。需要先切片长文档 -多个 chunk -每个 chunk 控制 token 数如果不统计 token很容易出现两个问题。第一个问题是超过上下文长度。比如模型最多支持一定上下文你却塞了太多文档进去请求可能直接失败。第二个问题是成本失控。用户问一个简单问题你却把几十页文档全塞进去效果不一定更好但 token 费用一定更高。所以工程里经常需要做这些事统计 prompt token 限制单个 chunk token 控制历史对话长度 按 token 截断文本 预估 API 调用成本js-tiktoken就适合做这类事情。八、Token 之后是什么EmbeddingToken ID 只是离散数字。比如[9906,11,87272]这些数字本身还不直接表示语义。模型还需要把 token ID 转成向量。这个过程就是 embedding。Embedding 可以理解成把文本变成一串高维数字比如一个文本被转成 1024 维向量[0.012, -0.034,0.156,...]这些数字不是随便来的而是模型学到的语义表示。如果两段文本语义相近它们的 embedding 向量在空间中的方向通常也会更接近。九、调用 Embedding 模型再看main.mjs。import{OpenAI}fromopenai;importdotenv fromdotenv;dotenv.config();const clientnew OpenAI({apiKey: process.env.DASHSCOPE_API_KEY, baseURL:https://dashscope.aliyuncs.com/compatible-mode/v1,});这里用了openaiSDK但 baseURL 指向的是阿里云百炼 DashScope 的兼容模式接口baseURL:https://dashscope.aliyuncs.com/compatible-mode/v1API Key 放在.env里DASHSCOPE_API_KEY你的密钥注意密钥不要提交到 GitHub。然后封装一个获取 embedding 的函数asyncfunctiongetEmbedding(text){const resawait client.embeddings.create({model:text-embedding-v4, input: text, dimensions:1024});returnres.data[0].embedding;}这段代码做了三件事传入一段文本 调用 embedding 模型 返回这段文本对应的向量这里指定了dimensions:1024表示希望返回 1024 维向量。十、什么是语义相似度有了 embedding 之后就可以比较两段文本是不是语义接近。比如这两句话Andrej Karpathy LLM Tokenization 分词原理 卡帕西讲解大模型 BPE 字符分词它们表面文字不完全一样。但语义上很接近都在说 Karpathy、大模型、Tokenization、分词。再看第三句话今天天气晴朗适合出门运动这句话和前两句明显不是一个主题。人能看出来这个区别embedding 也可以通过向量相似度体现出来。十一、余弦相似度项目里用了余弦相似度functioncosineSimilarity(vecA, vecB){letdot0, magA0, magB0;for(let i0;ivecA.length;i){dotvecA[i]* vecB[i];magAvecA[i]**2;magBvecB[i]**2;}returndot /(Math.sqrt(magA)* Math.sqrt(magB));}它计算的是两个向量方向是否接近。可以粗略理解为结果越接近1语义越相似 结果越接近0语义越不相关 结果越接近 -1方向越相反在文本 embedding 场景里我们更常用它判断两段文本是否语义接近。余弦相似度公式是cosine(A, B)A · B /(|A|*|B|)代码里的dot是点积dotvecA[i]* vecB[i];magA和magB是两个向量长度的平方累加magAvecA[i]**2;magBvecB[i]**2;最后除以两个向量的模returndot /(Math.sqrt(magA)* Math.sqrt(magB));十二、完整运行逻辑run函数大概是这样asyncfunctionrun(){const text1Andrej Karpathy LLM Tokenization 分词原理;const text2卡帕西讲解大模型 BPE 字符分词;const text3今天天气晴朗适合出门运动;const vec1await getEmbedding(text1);const vec2await getEmbedding(text2);const vec3await getEmbedding(text3);console.log(vec1);console.log(vec1.length);console.log(相似度:, cosineSimilarity(vec1, vec2));console.log(相似度:, cosineSimilarity(vec1, vec3));}run();这里预期会看到vec1.length1024说明 embedding 返回的是 1024 维向量。然后比较text1 和 text2 的相似度 text1 和 text3 的相似度正常情况下text1和text2的相似度应该更高。因为它们都在讨论大模型分词。而text3是天气和运动主题完全不同相似度应该更低。十三、这个 demo 和 RAG 有什么关系这其实已经是 RAG 的基础了。RAG 的核心流程可以简化为文档切片 ↓ 每个切片生成 embedding ↓ 用户问题生成 embedding ↓ 计算问题和文档切片的相似度 ↓ 取最相似的几个切片 ↓ 塞给大模型生成回答也就是说RAG 不是简单地把所有资料都丢给大模型。它会先用 embedding 做语义检索。比如用户问Tokenization 为什么重要系统会把这个问题转成向量。然后去文档库里找语义最接近的片段。找到之后再把这些片段作为上下文交给大模型。所以这个 demo 虽然很小但它已经覆盖了 RAG 里最关键的一块文本 -embedding -相似度计算十四、Token、Embedding、LLM 的关系可以用一条链路串起来文本 ↓ Tokenizer ↓ Token IDs ↓ Embedding ↓ 向量表示 ↓ Transformer ↓ 输出 Token ↓ Decode 成文本Tokenization 解决的是怎么把文本切成模型能处理的基本单位Embedding 解决的是怎么把 token 或文本变成带有语义信息的向量LLM 解决的是基于上下文预测接下来应该生成什么这几个概念连起来才是大模型真正工作的样子。十五、我对学习 AI Agent 的理解如果要学 AI Agent不建议一上来就堆框架。比如一开始就冲 LangChain、AutoGen、CrewAI、各种 Agent 框架很容易只会调 API不知道底层发生了什么。更好的路径是先把这些基础概念吃透Prompt 是什么 Token 是什么 上下文窗口是什么 Embedding 是什么 相似度检索是什么 Function Calling / Tool Calling 是什么 RAG 是什么 Agent 为什么需要工具和记忆然后再去做项目。比如个人知识库问答 代码仓库问答助手 简历优化 Agent AI 学习笔记助手 客服知识库机器人 网页内容总结工具这些项目都离不开 Token 和 Embedding。十六、容易踩的坑1. 把 token 当成字数Token 不是字数。中文、英文、标点、空格、代码都会影响 token 数。所以要用 tokenizer 工具实际统计。2. 忽略输入 token很多人只关注模型输出花了多少 token。但实际成本里输入 token 也很重要。尤其是 RAG 和 Agent输入上下文经常很长。3. 把 embedding 当成模型回答Embedding 不负责生成回答。它负责把文本转成向量方便做检索、聚类、推荐、相似度计算。真正生成自然语言回答的是聊天模型或文本生成模型。4. 直接把密钥写进代码不要这样写apiKey:sk-xxxx应该放到.envDASHSCOPE_API_KEY你的密钥然后代码里读取apiKey: process.env.DASHSCOPE_API_KEY5. 只看文字是否相同不看语义是否相近Embedding 的价值就在这里。两段文本可以字面不同但语义相近。比如Tokenization 分词原理 BPE 字符分词机制字面不完全一样但主题很接近。十七、总结这次 demo 其实就做了两件事。第一用js-tiktoken理解 Token文本 -token ID -文本第二用 embedding 理解语义相似度文本 -向量 -余弦相似度如果把 AI 应用比作一栋楼Token 和 Embedding 就是地基。不理解 Token就很难控制上下文、成本和模型输入。不理解 Embedding就很难理解 RAG、知识库、语义搜索和 Agent 记忆。所以这两个概念值得认真写代码跑一遍。代码不复杂但能帮我们把“大模型到底怎么处理文本”这件事看得更清楚。