RAG 实战:从一篇掘金文章出发,拆解检索增强生成的全链路

RAG 实战:从一篇掘金文章出发,拆解检索增强生成的全链路

你有没有遇到过这样的场景:让 AI 总结一篇长文章的核心观点,它要么回答得似是而非,要么干脆说"这篇文章超出了我的知识范围"。这不是模型不够聪明,而是它没有见过这篇文章。

大语言模型的训练数据有截止日期,且无法覆盖互联网上的每一篇文章。当你需要 AI 理解一份它从未见过的文档时,你有两个选择:

  • 微调(Fine-tuning):把文章喂给模型重新训练,耗时、烧钱,且每次新增文档都要重来一遍
  • RAG(Retrieval-Augmented Generation,检索增强生成):先把文章存进知识库,提问时检索相关内容,塞进提示词让 AI 现场阅读并回答

显然,RAG 是更务实的选择。下面我们通过一段不到 90 行的真实代码,一步步拆解 RAG 的完整链路。

RAG 六步流水线

整个 RAG 流程可以抽象为六个步骤:

复制代码
加载文档 → 文本切分 → 向量嵌入 → 存入向量库 → 相似检索 → 增强生成

每一步都有其不可替代的作用,缺任何一环,整个链路都会断裂。下面逐一展开。

第一步:文档加载------把外部知识"搬"进来

javascript 复制代码
import { CheerioWebBaseLoader } from "@langchain/community/document_loaders/web/cheerio";

const cheerioLoader = new CheerioWebBaseLoader(
    'https://juejin.cn/post/7233327509919547452',
    { selector: '.main-area p' }
);

const documents = await cheerioLoader.load();

这里做了两件事:抓取网页提取正文

CheerioWebBaseLoader 底层使用了 Cheerio------一个在 Node.js 端运行的类 jQuery 库,让你可以用 CSS 选择器像操作前端 DOM 一样解析 HTML。selector: '.main-area p' 精确锁定了文章正文区域的所有段落标签,滤掉了导航栏、侧边栏、评论区等噪音。

这是 RAG 的第一道关卡:垃圾进,垃圾出。如果加载的内容混杂了大量无关信息,后续的检索质量无从谈起。

LangChain 的 document_loaders 模块提供了数十种加载器:PDF、Markdown、Notion、GitHub、Confluence......不管知识存在哪里,都能"搬"进管道。

第二步:文本切分------把长文"切"成可检索的块

这是整个 RAG 流程中最容易被低估、却最能影响最终效果的环节。

javascript 复制代码
import { RecursiveCharacterTextSplitter } from "@langchain/textsplitters";

const textSplitter = new RecursiveCharacterTextSplitter({
    chunkSize: 400,          // 每个文本块的最大字符数
    chunkOverlap: 50,        // 相邻文本块之间的重叠字符数
    separators: ['。', ',', '!', '?'], // 语义分割符
});

const splitDocuments = await textSplitter.splitDocuments(documents);

这里涉及三个关键参数,每个都有它的"为什么":

chunkSize = 400

文本块不能太大,也不能太小。太大 ,检索精度下降,且容易超出 LLM 的上下文窗口;太小,语义碎片化,一段完整论述被拦腰截断。400 个字符约等于中文的 200 字,刚好容纳一个完整的观点段落。

在实际项目中,chunkSize 的调整往往是 RAG 性能调优的第一步:客服问答场景可能偏短(200-300),论文分析场景可能偏长(800-1000)。

chunkOverlap = 50

这是 RecursiveCharacterTextSplitter 最精妙的设计------相邻块之间不是硬截断,而是有 50 个字符的"重叠带"。

为什么要重叠?想象一句话正好横跨 Chunk 1 和 Chunk 2 的边界:"......父亲的去世让我意识到//(此处是分块边界)人生短暂,应该及时行乐。"如果不重叠,这句完整的因果逻辑就被一分为二,检索时无论命中哪个 Chunk,AI 都看不到完整语境。50 个字符的重叠如同"安全冗余",保证边界处的语义不被割裂。

separators: '。', ',', '!', '?'

这些中文标点定义了切割的优先级。RecursiveCharacterTextSplitter 的"递归"体现在:它先尝试用句号切,切出来的块如果还超过 400 字,再用逗号切,以此类推。这样保证切割总是发生在最自然的语义边界上,而不是生硬地数到第 400 个字符就一刀下去。

为什么叫"Recursive"?

这是常见的面试考点。它先按最高优先级分隔符(句号)切分 → 检查每个块是否超过 chunkSize → 对超标的块,降级到下一个分隔符(逗号)继续切 → 递归直到所有块都合规。这种"分层降级"的策略,比简单按固定长度截断更尊重文档的语义结构。

第三步:向量嵌入------把文字翻译成机器"认识"的数字

javascript 复制代码
import { OpenAIEmbeddings } from "@langchain/openai";

const embeddings = new OpenAIEmbeddings({
    apiKey: process.env.OPENAI_API_KEY,
    model: process.env.EMBEDDING_MODEL_NAME,
    configuration: { baseURL: process.env.OPENAI_BASE_URL },
});

Embedding 模型的工作是:把一段文本映射成一个高维向量(如一串 1536 个浮点数)。语义相近的两段话,它们在向量空间中的距离就相近;语义无关的,距离就远。

这个过程很像给每段文字拍了一张"语义指纹"。你不需要精确匹配关键词,哪怕换了一套说法,只要意思相同,向量距离依然很近------这就是 RAG 比传统关键词搜索高明的地方。

值得注意的是,这里的 Embedding 模型和大语言模型使用了同一个兼容端点(阿里云 DashScope),但它们是两个不同的模型。Embedding 模型专做向量化,轻量、快速、便宜;Chat 模型专做对话推理。两者各司其职,不可混用------这也是面试中的高频考点。

第四步:向量存储------给"语义指纹"建一个可查询的索引

javascript 复制代码
import { MemoryVectorStore } from "@langchain/classic/vectorstores/memory";

const vectorStore = await MemoryVectorStore.fromDocuments(splitDocuments, embeddings);

MemoryVectorStore.fromDocuments() 一次性完成了两件事:

  1. 调用 embeddings 模型,将每个文本块转为向量
  2. 将所有向量存入内存向量数据库

MemoryVectorStore 顾名思义,数据存在内存中,程序关闭即消失。它适合 Demo 和学习场景。生产环境通常会换成 Pinecone(云原生)、Chroma(本地持久化)、Milvus(分布式高性能)等持久化向量数据库。

第五步:相似检索------找到最相关的"知识碎片"

javascript 复制代码
const retriever = await vectorStore.asRetriever({ k: 2 });
const retrievedDocs = await retriever.invoke(question);
const scoreResults = await vectorStore.similaritySearchWithScore(question, 2);

这短短的几行代码,背后发生了什么呢?

  1. 问题向量化 :将用户问题 "父亲的去世对作者的人生态度产生了怎样的根本性逆转?" 传入同一个 Embedding 模型,得到问题向量
  2. 相似度计算:在向量库中计算问题向量与所有文档向量的距离(通常用余弦相似度或欧氏距离)
  3. Top-K 选取 :返回距离最近的 k=2 个文本块

代码还通过 similaritySearchWithScore 拿到了每个检索结果的相似度评分:

javascript 复制代码
retrievedDocs.forEach((doc, i) => {
    const scoreResult = scoreResults.find(
        ([scoredDoc]) => scoredDoc.pageContent === doc.pageContent
    );
    const score = scoreResult ? scoreResult[1] : null;
    const similarity = score ? (1 - score).toFixed(2) : "N/A";
    console.log(`\n文档 ${i+1} 相似度:${similarity}`);
});

这里的 score 是向量距离(越小越相似),1 - score 转换为相似度百分比(越高越相关)。这个评分在生产环境中是重要的诊断工具------如果 Top-1 文档的相似度只有 0.3,说明知识库里可能根本没有相关内容,此时强行生成反而会产生幻觉,应该触发"知识库未覆盖"的兜底逻辑。

第六步:增强生成------让 AI "开卷考试"

javascript 复制代码
const content = retrievedDocs
    .map((doc, i) => `[片段${i+1}]\n ${doc.pageContent}`)
    .join("\n\n----\n\n");

const prompt = `你是一个文章辅助阅读助手,根据文章内容来解答:
文章内容:
${content}

问题:
${question}
回答:`;

const response = await model.invoke(prompt);

这就是 RAG 的"R"(Retrieval)与"G"(Generation)的交接点------把检索到的文档片段拼接成"开卷材料",连同用户问题一起填入提示词模板,交给 LLM 现场作答。

这个提示词模板本身就是一种提示工程

  • 你是一个文章辅助阅读助手:角色设定,锚定 AI 的行为边界------只基于文章回答,不编造
  • ${content}${question} 用明确的标签分隔:结构化输入,减少模型混淆
  • 回答: 结尾:引导模型直接进入回答模式,减少废话

完整链路回顾

把这六步串起来,一次完整的 RAG 查询是这样的:

css 复制代码
用户提问
    │
    ▼
问题向量化 ──────────→ 在向量库中检索 Top-K 相似文档
    │                          │
    │                          ▼
    │                   返回 K 个最相关文本块
    │                          │
    ▼                          ▼
             拼装提示词模板
                 │
                 ▼
           LLM 阅读 + 回答
                 │
                 ▼
             返回最终答案

整段代码不到 90 行,却完整实现了从网页抓取到智能回答的全链路。这就是 LangChain 这类框架的价值------它把每个环节抽象为标准组件,你可以像搭积木一样组合它们,快速验证想法。


深入切割机制:三大 Splitter 横向对比

前面的示例使用了 RecursiveCharacterTextSplitter,但 LangChain 的切割器家族不止这一位成员。CharacterTextSplitterTokenTextSplitterRecursiveCharacterTextSplitter 三者各有所长,选错切割器对 RAG 效果的影响不亚于选错 Embedding 模型。下面结合真实代码逐一拆解。

前置知识:Token 不等同于字符

在深入三种切割器之前,必须先理解一个基础概念------Token 不是字符。大语言模型不是逐字阅读文本,而是将文本转化为 Token(词元)序列后再处理。同一个意思,用不同语言表达,Token 数量可以天差地别:

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

const enc = getEncoding('cl100k_base');

console.log('apple', enc.encode('apple').length);      // → 1 个 Token
console.log('pineapple', enc.encode('pineapple').length); // → 2 个 Token
console.log('苹果', enc.encode('苹果').length);          // → 2 个 Token

apple 是一个高频英文单词,编码器给它分配了独立的 Token ID,1 个 Token 就搞定。pineapple 虽然也是单词,但频率没那么高,被拆成了 2 个 Token。苹果 两个汉字,每个汉字各占 1 个 Token。

这个差异的意义在于:LLM 的上下文窗口是按 Token 计算的 (如 GPT-4 的 128K 窗口指的是 128K Token),而你写代码时看到的是字符数。一个中文字符约等于 1.5-2 个 Token,一段 1000 字的英文可能只有 700 Token------用字符数估算 Token 数,误差可以高达 50%。三种切割器的本质差异,正是源于"按什么单位来计数"。

Tiktoken 是 OpenAI 开源的 Token 计数库,cl100k_base 是 GPT-4 / GPT-3.5-turbo 使用的编码表。js-tiktoken 是其 JavaScript 移植版。它提供的能力很简单:给定一段文本,告诉你这段文本会被模型当成多少个 Token。但这个简单的能力,是整个切割器体系的基石。


一、CharacterTextSplitter:简单粗暴的字符计数器

CharacterTextSplitter 是最基础的切割器------它只做一件事:按字符数切。用一段日志文件来演示:

javascript 复制代码
import { CharacterTextSplitter } from '@langchain/textsplitters';
import { Document } from '@langchain/core/documents';

const logDocument = new Document({
    pageContent: `[2024-01-15 10:00:00] INFO: Application started
[2024-01-15 10:00:05] DEBUG: Loading configuration file
[2024-01-15 10:00:10] INFO: Database connection established
...`
});

const splitter = new CharacterTextSplitter({
    chunkSize: 50,       // 每个块最多 50 个字符
    chunkOverlap: 10,    // 块之间重叠 10 个字符
    separator: '\n',     // 单一分隔符
});

它的切割逻辑非常直白:

arduino 复制代码
if (当前累计字符数 + 下一段字符数 > chunkSize) {
    切割,另起一个新块
} else {
    继续往当前块里追加
}

优点:快,非常快。没有复杂的递归逻辑,不需要加载外部编码表,适合对速度要求极高的流式场景。

缺点 :完全无视语义。如果 chunkSize: 50 正好卡在一句话中间,它会毫不犹豫地切断。对于中文来说伤害稍小------每个汉字本身是一个有意义的单元,切割点即使不理想,信息损失也有限。但对于英文,切断一个单词中间是灾难性的。

适用场景:中文文本、日志文件、代码等字符与语义单元基本一一对应的内容,或者对切割精度要求不高但追求速度的原型验证。


二、TokenTextSplitter:最精准的"窗口对齐器"

TokenTextSplitter 是三种切割器里唯一直接对标 LLM 上下文窗口的。它不问"这段文本有多少字符",只问"这段文本有多少 Token":

javascript 复制代码
import { TokenTextSplitter } from '@langchain/textsplitters';

const logTextSplitter = new TokenTextSplitter({
    chunkSize: 50,               // 每个块最多 50 个 Token(不是字符!)
    chunkOverlap: 10,            // 块之间重叠 10 个 Token
    encodingName: 'cl100k_base', // 使用 GPT-4 的编码表
});

const splitDocuments = await logTextSplitter.splitDocuments([logDocument]);

const enc = getEncoding('cl100k_base');
splitDocuments.forEach(doc => {
    console.log('char length:', doc.pageContent.length);      // 字符长度
    console.log('token length:', enc.encode(doc.pageContent).length); // Token 长度 ≤ 50
});

注释中的 chunkSize: 50 指的是 50 个 Token,不是 50 个字符。这是最容易混淆的地方。同一个块,字符长度可能是 80,但 Token 长度严格不超过 50。

它的切割流程是:

复制代码
加载 tiktoken 编码表 → 将文本 Token 化 → 按 Token 累计计数 → 达到 chunkSize 时切割

这是唯一一个切割后可以直接拿去计算"还剩多少上下文空间"的切割器。 当你需要向 GPT-4 的 128K 窗口中塞入尽可能多的检索结果时,TokenTextSplitter 能精确告诉你每个 Chunk 消耗了多少 Token 配额,不会出现"我以为只用了 3000 Token,实际用了 5000"的估算偏差。

优点:Token 级别的精确控制,与 LLM 窗口天然对齐,跨语言一致性好(中英文在 Token 维度上的表现是统一的)。

缺点 :依赖外部编码表(js-tiktoken),初始化有开销;切割发生在 Token 边界而非字符边界,可能导致文本在奇怪的位置断开;不同模型的编码表不同(GPT-4 用 cl100k_base,GPT-2 用 gpt2),需要根据实际使用的模型配置。

适用场景:需要严格控制 Token 预算的生产环境、多语言混合文档、与 OpenAI 模型对接的 RAG 管道。


三、RecursiveCharacterTextSplitter:最"聪明"的语义感知器

RecursiveCharacterTextSplitter 是三种切割器里策略最复杂的。它用字符数作为上限,但在切割时有一套"分层降级"的分隔符策略:

javascript 复制代码
const logSplitter = new RecursiveCharacterTextSplitter({
    separators: ["\n", "。", ","],  // 分隔符优先级:先换行,再句号,再逗号
    chunkSize: 200,                    // 字符上限
    chunkOverlap: 20,                  // 重叠字符数
});

const logChunks = await logSplitter.splitDocuments([logDocument]);

const enc = getEncoding('cl100k_base');
logChunks.forEach(doc => {
    console.log('character length:', doc.pageContent.length);
    console.log('token length:', enc.encode(doc.pageContent).length);
});

输出会清楚地暴露字符与 Token 之间的"汇率"------同一段中文长文本,character length 可能是 180,但 token length 可能是 280。这也是为什么在生产环境中,很多人会在 RecursiveCharacterTextSplitter 验证通过后,再加一层 Token 数校验。

它的核心算法可以概括为:

perl 复制代码
function split(text, separators, chunkSize):
    // 1. 取当前优先级最高的分隔符
    separator = separators[0]

    // 2. 用该分隔符切分文本
    splits = text.split(separator)

    // 3. 合并切分结果,尽量接近但不超出 chunkSize
    for each split in splits:
        if (currentChunk.length + split.length > chunkSize):
            if (currentChunk.length < chunkSize):
                保存 currentChunk,另起新块
            else:
                // 当前块仍然超标 → 递归降级到下一个分隔符
                subSplits = split(split, separators[1:], chunkSize)
                保存 subSplits

    // 4. 根据 chunkOverlap 为相邻块添加重叠内容
    applyOverlap(allChunks, chunkOverlap)

关键在第三步的 else 分支:当使用最高优先级分隔符切出来的片段仍然超出 chunkSize 时,不会暴力截断,而是递归降级到下一个分隔符重新尝试。比如:

swift 复制代码
separators: ["\n", "。", ","]

一个超长段落
  → 先试 \n 切:只切成 1 段(因为段落内没有换行),仍超标
    → 降级到 。 切:切成 3 句,其中第 2 句还超标
      → 降级到 ,切:切成若干短句,全部合规

这种策略保证切割总是优先发生在最高语义层级------段落边界(换行)优于句子边界(句号),句子边界优于从句边界(逗号)。同样是切成 200 字符的块,Recursive 切出来的块比 CharacterTextSplitter 更"完整",比 TokenTextSplitter 更"语义连贯"。

但它有一个固有矛盾:用字符数限制,却试图在语义边界切割 。当一段话在最近的一个句号处已经 190 字符,再加一句就变成 250 字符时,切割器面临选择------是在句号处切(190 字符,语义完整但浪费了 10 字符配额),还是继续凑到接近 200 字符再切(可能切断语义)?RecursiveCharacterTextSplitter 选择了前者------优先保证语义完整性,即使单个 chunk 略小于 chunkSize。


三者对比一览

维度 CharacterTextSplitter TokenTextSplitter RecursiveCharacterTextSplitter
计量单位 字符数 Token 数 字符数
切割策略 累计字符数到阈值即切 累计 Token 数到阈值即切 分层递归 + 分隔符优先级降级
语义感知 无(但 Token 天然有一定语义) 强------优先在段落/句子/从句边界切割
精确度 低(Token 估算偏差大) 高(直接对标 LLM 窗口) 中(字符数估算,但有语义补偿)
依赖 js-tiktoken + 编码表
速度 最快 慢(需 Token 化) 中等
中文友好度 较好 较好 最好
适用场景 日志、代码、原型 生产环境、Token 预算敏感 文章、文档、对话等自然语言

如何选择?

没有万能切割器,只有合适的选择:

  • 不确定用什么?从 RecursiveCharacterTextSplitter 开始。 它的语义感知能力在大多数自然语言场景下表现最好,是 LangChain 文档的默认推荐。
  • 对接 OpenAI 模型且关注 Token 预算?用 TokenTextSplitter 当每个请求的 Token 消耗直接等于成本时,精确计数不是可选项,是必选项。
  • 处理结构化日志或代码?用 CharacterTextSplitter 这类内容的行本身就是天然的分割单元,不需要复杂的递归策略。
  • 进阶玩法:组合使用。 先用 RecursiveCharacterTextSplitter 做语义切割,再对每个 Chunk 用 TokenTextSplitter 做二次校准------确保既不破坏语义,又不超出 Token 预算。这在高要求的生产 RAG 管道中是最常见的模式。

面试考点总结

1. RAG 的核心流程是什么?每一步的作用分别是什么?

Loading → Splitting → Embedding → Storing → Retrieval → Generation。加载负责获取外部知识,切分保证检索粒度合理,嵌入将文本转为可计算的向量,存储建立索引,检索找出相关内容,生成基于检索结果回答问题。漏掉任何一步都走不通。

2. chunkSize 和 chunkOverlap 分别如何影响 RAG 效果?

chunkSize 决定了检索粒度 :太大则检索不精确且可能超出 LLM 上下文窗口,太小则语义碎片化。chunkOverlap 防止边界语义断裂:保留相邻块之间的重叠内容,确保跨块的关键信息不被截断。两者需要根据文档类型和业务场景联合调优。

3. RecursiveCharacterTextSplitter 的"递归"是什么意思?

它按分隔符优先级递归切分:先用高优先级分隔符(如句号)切,对超出 chunkSize 的块降级到下一个分隔符(如逗号)继续切,直到所有块都符合大小限制。这种方式优先在自然语义边界上切割,而非暴力截断。

4. Embedding 模型和 Chat 模型有什么区别?能否互换?

不能互换。Embedding 模型将文本映射为固定维度的向量,输出是数字数组,用于语义相似度计算;Chat 模型接收文本序列、输出文本,用于对话和推理。它们是两种不同的模型架构,职责完全不同。RAG 中两个模型各司其职:Embedding 负责"找",Chat 负责"答"。

5. 向量检索的相似度评分是怎么算出来的?

将用户问题向量化后,在向量空间中计算问题向量与所有文档向量的距离(常用余弦相似度、欧氏距离或内积),距离越小表示语义越接近。引擎返回距离最近的 K 个文档及其分数。1 - score 可近似转换为相似度百分比。如果最高分仍然很低,说明知识库可能没有覆盖该问题------此时应触发兜底策略而非强行回答。

6. MemoryVectorStore 和持久化向量数据库的区别是什么?

MemoryVectorStore 将向量和文档存在内存中,进程结束即丢失,适合原型验证。生产环境使用 Pinecone、Chroma、Milvus 等持久化方案,支持数据落盘、分布式检索、增量更新和权限控制。面试中能说出至少两种生产级向量数据库是加分项。

7. RAG 和微调(Fine-tuning)的适用场景分别是什么?

RAG 适合 :知识频繁更新、需要可解释性(能溯源到具体文档)、外部知识注入、低成本快速接入。微调适合:需要模型学习特定风格或行为模式、任务定义稳定、对延迟敏感不能加检索环节。两者并不互斥,成熟的工业方案往往是 RAG + Fine-tuning 的组合。

8. 提示词模板在 RAG 中起什么作用?

提示词模板决定了检索到的文档如何与用户问题结合。它至少承担三个职能:角色设定 (限定 AI 的行为边界,如"只基于给定文章回答")、结构化输入 (在文档和问题之间插入明确的分隔标记)、防幻觉引导(隐含地告诉模型"不要编造,不知道就说不知道")。模板本身也是提示工程的一部分,需要针对不同场景迭代优化。

9. Token 和字符有什么区别?为什么这个区别对 RAG 很重要?

Token 是 LLM 处理文本的最小语义单元,不是字符。 一个英文单词可能是 1 个 Token,一个汉字通常是 1-2 个 Token。LLM 的上下文窗口(如 GPT-4 的 128K)是按 Token 计算的,而不是字符。如果按字符数估算 Token 消耗,误差可能高达 50%。在 RAG 中,这意味着你以为塞了 10 个 Chunk 给模型,实际可能已经爆了窗口。js-tiktoken 库的作用就是精确计算任意文本的 Token 数,消除这个估算误差。面试中能说出 cl100k_base(GPT-4 编码表名称)是加分项。

10. CharacterTextSplitter、TokenTextSplitter 和 RecursiveCharacterTextSplitter 三者有什么区别?分别适用什么场景?

这是切割器三兄弟,核心区别在于计量单位切割策略

  • CharacterTextSplitter:按字符数切,简单直接,无外部依赖,速度快。适合日志、代码等结构化文本。缺点是完全无视语义边界。
  • TokenTextSplitter :按 Token 数切,依赖 js-tiktoken 编码表,切割结果直接对标 LLM 的上下文窗口。适合 Token 预算敏感的生产环境。缺点是需要加载编码表、切割点可能在语法上不自然。
  • RecursiveCharacterTextSplitter:按字符数切上限,但用递归分隔符策略优先在语义边界切割(段落 → 句子 → 从句逐级降级)。适合文章、文档等自然语言内容。它用字符数做上限、用语义做约束,是 LangChain 的默认推荐。

三者不是互斥的------生产环境中常见"Recursive 语义切割 + TokenTextSplitter 二次校准"的组合模式。

11. tiktoken 是什么?cl100k_base 又是什么?

tiktoken 是 OpenAI 开源的 Token 计数库(js-tiktoken 是其 JS 移植版),它能精确计算任意文本在特定模型下的 Token 数。cl100k_base 是 GPT-4 和 GPT-3.5-turbo 使用的编码表名称------不同的模型使用不同的编码表(如 GPT-2 用 gpt2),编码表决定了文本如何被切分为 Token。在 RAG 中使用 TokenTextSplitter 时必须指定与目标模型匹配的编码表,否则 Token 计数不准确。

12. 为什么 RecursiveCharacterTextSplitter 用字符数而不用 Token 数做上限?

这是一个设计权衡。如果要精确控制 Token 数,需要在每次切割时实时 Token 化文本------这对长文档来说是巨大的性能开销。用字符数做上限,用递归分隔符做语义补偿,是一种工程上的近似最优解 :速度快、无外部依赖、语义完整性好,代价是 Token 估算不够精确。当 Token 精度不可妥协时(如按 Token 计费的 API),改用 TokenTextSplitter 或对 Recursive 的输出做 Token 二次校验。

相关推荐
fanstuck5 小时前
1M 上下文能怎么用?我用 Seed Evolving 做了一个招标文件版本差异审查器
服务器·人工智能·数据分析·开源·aigc
小满zs5 小时前
Go语言第三章(五谷轮回)
后端·go
墨舟的AI笔记7 小时前
React 组件库的 Tree Shaking:按需加载与副作用的工程化治理
人工智能
qq_589666057 小时前
TypeScript 完整入门教程
前端·javascript·typescript
WoooChi8 小时前
DailyTech-20260724
人工智能·科技·业界资讯
zh路西法8 小时前
【CAM Grad-CAM Grad-CAM++】深度学习模型可解释性:从原理到YOLOv8部署实战
人工智能·深度学习·yolo·yolov8·grad-cam·cam
雨辰AI8 小时前
全集实战:企业级大模型服务化部署全栈指南|FastAPI 封装 + Nginx 负载均衡 + 高可用架构 从单机到生产一步到位
人工智能·ai·负载均衡·fastapi·ai编程
触底反弹8 小时前
Vibe Coding 不写 Git,等于悬崖边飙车
人工智能·git·面试
咖啡屋和酒吧9 小时前
健康管理:现代生活的科学守护
人工智能·生活·精选
星栈独行9 小时前
翻完 Pi 源码:它和 Codex、Claude Code 有何不同
开发语言·javascript·人工智能·程序人生