从零理解 LLM 与 RAG:大模型不是万能的,检索增强帮你弥补盲点

84 阅读10分钟

通过这篇文章我们能学会

  1. 市面上常见的AI产品本质是什么
  2. RAG是什么,如何去使用

LLM初识

LLM:Large Language Models,大规模语言模型

很多刚接触前端,或是有一定基础的前端开发者,对于LLM的认识还只是停留在询问豆包,询问ChatGPT这一阶段。所以我们想要接触AI,接触Agent开发时,首先要明确LLM到底是什么?

官方定义:

大型语言模型(large language model,LLM) ,也称大语言模型,是由具有大量参数(通常数十亿个权重或更多)的人工神经网络组成的一类语言模型,使用自监督学习或半监督学习对大量未标记文本进行训练,大语言模型在2018年左右出现,并在各种任务中表现出色。[来自维基百科的官方定义]

看着很多专业词语堆叠一起很复杂,实际上LLM的本质就是一个条件概率分布模型: 即给定上下文(输入+已生成的token),预测下一个token的概率。

🌰

我向LLM输入了这样一段话:

今天肚子好饿,我

此时,LLM就会根据你的这句话计算下一个token(词)的概率

  1. 我想吃饭。 概率:70%
  2. 我想运动。 概率:30%
  3. 我想玩王者荣耀。 概率:20%

LLM发现我想吃饭这个token概率比较高,就拼接到回答上。

最后回答:

今天肚子好饿,我想吃饭

这种看上去合理的答案。

当然上面的例子是很简化的那种。具体操作流程可以看看这张图片

他会多次调用LLM,继续在后面填充token,直到一个满意的答案出现。


了解完LLM,我们需要明确一个概念:LLM能为我们做到的事情:通过不断调用LLM概率计算,帮助我们输出一个合理的答案

小登们可能就会有些问题:

LLM 能力只有这个,那:

  1. 豆包为什么输出的内容能记忆之前的回答?
  2. cursor为什么可以帮助我们更改代码
  3. ............

原因就在于,这些AI产品都进行了一次应用层的封装,这些封装扩展了 LLM 能够使用的能力。让用户觉得AI就是有那么强,无所不能。

比如说:

  • 由于LLM本身是不具备任何记忆能力,其API调用是无状态的,你传什么上下文,他只负责太预测下一个token,然后返回结果。所以对话AI产品中都会针对记忆这一块进行引用层封装

  • 面对五花八门的用户输入的prompt,可能会导致LLM输出的回答不受控或是不尽人意,所以在用户提问后,在AI产品里要对prompt进行处理成一个成熟的prompt交给LLM,这就是prompt工程

  • 如果仅仅只是来回答用户五花八门的问题,那AI也太掉价了吧,我们需要AI能帮助我们做很多事情,比如点菜,点外卖,帮我们搜索什么内容。这都需要给LLM封装一个TOOL,给予它操控工具的能力

总结:

LLM不等于AI。LLM只是AI产品的大脑,一个成熟的AI产品是经过很多应用层封装后的结果。LLM的本质只是一个概率预测器


RAG初识

在我正式讲解RAG时,我想先交代一下前面讲述的LLM会有哪些缺点:

image.png

痛点:

  1. 信息实效性

现代社会发展变更很快,信息的更替也会更快,但是现有的LLM都是依赖训练过的数据进行输出,所以说一些很新的知识,LLM的回答可能会有问题

  1. 数据的安全性

对于企业来说,数据安全至关重要,没有企业愿意承担数据泄露的风险,尤其是大公司,没有人将私域数据上传第三方平台进行训练会推理


  1. 幻觉的不可靠性

因为 LLM 本身是基于从大量数据中训练出来的概率模型来一个个生成 token,也就是它并没有逻辑和事实基线。

所以我们说 LLM 的智能是涌现性的智能,是基于概率产生的 “伪智能” ,而不是底层基于逻辑和推理能力“真智能”。

🌰

我们给猴子一个打字机,让它随便打字,如果这个 实验拉长到时间是无限 的。

有没有一种可能,总有一天他会打出一部完整的莎士比亚的小说?

答案是肯定的,因为时间是无限的,而且它是随机打字,那就一定会在某个时间点所有概率都碰巧了,成了一本莎士比亚的小说。

那一个问题来了,猴子到底懂不懂莎士比亚?

那肯定是完全不懂的,它不具备逻辑,只是一切概率性的巧合凑到一起了罢了。

LLM 可以理解成为一个更大概率打出莎士比亚的猴子。

它不理解输出文本的逻辑,更不理解内容背后的逻辑。

但因为它的模型足够大,训练数据集足够大,他输出正确内容的概率也足够大。

所以,从外界看来,他就像真正理解内容一样,也就像具有真正的逻辑和推理能力。

image.png

为了解决痛点:RAG应运而生

RAG 是 Retrieval-Augmented Generation 的缩写,也就 检索增强生成 的意思。

它主要是为了解决 大模型 本身知识匮乏的问题,主要流程包括索引、检索和生成。

使用RAG能够将大模型“记忆式回答”转变成“查完文档在回答”

从而解决:

  1. 信息实效性:通过查找前沿资料
  2. 数据安全性:通过查找公司文档
  3. 幻觉不可靠性:通过查找资料,减少幻觉产生

具体操作流程

RAG的核心思想是:先检索,再生成

  1. 检索阶段
  • 从外部知识库(如文档、数据库、代码库)中动态查找与用户输入相关的信息。
  • 解决生成模型依赖静态训练数据的问题
  1. 生成阶段
  • 将检索到的内容作为上下文,指导 LLM 生成更准确的回答。
  • 解决纯检索系统无法灵活组织语言的问题。

  1. 检索阶段

知识库构建

  • 数据源:文档,代码库,数据库
  • 预处理:数据源切片,分块,清洗

之所以要分块切片,是因为:

GPT 3.5 的上下文窗口是 16k,GPT 4 上下文窗口是 128k,

而我们很多数据源都很容易比这个大,而且用户的提问经常涉及多个数据源。

所以我们需要对数据集进行语意化的切分

根据内容的特点和目标 大模型 的特点、上下文窗口等,对 数据源 进行合适的切分。

文本向量化

即embedding阶段

使用 Embedding 模型 将文本转换为 向量,目的是为了让计算机能够用数学方式理解和比较语义的相似度。

这个就是RAG比较抽象的点,小白一下子会比较难听懂

🌰

向量这个概念比较抽象,我们先从二维向量来说起。

  • 向量 A:[1, 2] 表示一个点
  • 向量 B:[2, 2] 是另一个点

这两个点在二维平面中靠得近,就表示它们意义接近。

语言中的向量:是高维向量,其实就是高维的数字数组。也就是切出来的文本段所转换的向量

LLM 会把一句话变成一个 512维 / 768维 的向量:

  • “我想休假” → [0.1, -0.3, 0.6, ..., 0.2] (共 768 个数字)

高维其实是为了更加精确的区分不同的向量,维度越高,语义表达就越丰富。

此时用户输入一个问题:

我现在很累,我能怎么办?

我们将用户问题通过embedding阶段,转换成一个语言向量,在通过一个他们的余弦值进行对比,选择最合适的文档提出来交给LLM

向量存储

我们将文档内容切片后,再通过embedding阶段转换成向量后,就需要存储到向量数据库中方便后面检索

向量在存哪儿呢? → 向量数据库!

  • FAISS(本地,开源)
  • Milvus(国产,支持 分布式
  • Weaviate、Pinecone(商用 SaaS)

查询检索

  • 将用户输入 Query 同样向量化,从知识库中召回 Top-K 相关文档。

向量检索 最常见的指标: ****余弦相似度 cosine similarity

它衡量两个向量之间夹角有多小 —— 角度越小,语义越接近

计算出cos值

范围:-1~1

1 → 完全相同方向(语义几乎相同)

0 → 无关

-1 → 相反意思

  1. 生成阶段

构造prompt

将搜索到的文档内容,和用户问题包装成一个结构化pormpt

请基于以下信息回答问题:
* [检索到的文档1]
* [检索到的文档2]
用户问题:{Query}

调用 LLM 生成回答

  • 调用 LLM Chat 模型 生成回答。
  • 参数控制:temperature=0.3(降低随机性)、max_tokens=500 等

代码框架(使用langchain)


 // 最小 RAG Demo(LangChain JS 组件化)
// 使用前请在环境变量或 .env 中配置 OPENAI_API_KEY

import 'dotenv/config';
import fs from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import readline from 'node:readline/promises';
import { stdin as input, stdout as output } from 'node:process';

import { TextLoader } from '@langchain/community/document_loaders/fs/text';
import { MemoryVectorStore } from '@langchain/community/vectorstores/memory';
import { ChatOpenAI, OpenAIEmbeddings } from '@langchain/openai';
import { ChatPromptTemplate } from '@langchain/core/prompts';
import { RunnableSequence, RunnablePassthrough } from '@langchain/core/runnables';
import { StringOutputParser } from '@langchain/core/output_parsers';
import { RecursiveCharacterTextSplitter } from '@langchain/textsplitters';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const PROJECT_ROOT = __dirname;
const DATA_DIR = path.join(PROJECT_ROOT, 'data');

/**
* 读取 data 目录中的 .txt 文本为 LangChain 文档列表
*/
async function loadDocuments() {
  const files = await fs.readdir(DATA_DIR);
  const txtFiles = files.filter((f) => f.toLowerCase().endsWith('.txt'));
  const allDocs = [];
  for (const name of txtFiles) {
    const full = path.join(DATA_DIR, name);
    const loader = new TextLoader(full, { encoding: 'utf-8' });
    const docs = await loader.load();
    allDocs.push(...docs);
  }
  return allDocs;
}

/**
* 将长文档切分为更小的片段,便于向量化与召回
*/
async function splitDocuments(documents, chunkSize = 800, chunkOverlap = 100) {
  const splitter = new RecursiveCharacterTextSplitter({
    chunkSize,
    chunkOverlap,
    separators: ['\n\n', '\n', '。', '!', '?', ',', ' ', ''],
  });
  return splitter.splitDocuments(documents);
}

/**
* 将检索到的文档片段格式化为字符串,插入提示词上下文
*/
function formatDocs(docs) {
  return docs
    .map((d, i) => `【片段${i + 1}】\n${d.pageContent}`)
    .join('\n\n');
}

/**
* 构建最小 RAG 链:
* - 检查 API Key
* - 初始化 LLM
* - 加载并切分文档
* - 构建向量库与检索器
* - 组装提示模板 + 模型 + 输出解析(LCEL 串联)
*/
async function buildChain() {
  if (!process.env.OPENAI_API_KEY) {
    throw new Error('缺少 OPENAI_API_KEY,请在环境变量或 .env 中配置。');
  }

  const llm = new ChatOpenAI({
    model: 'gpt-4o-mini',
    temperature: 0,
  });

  // 加载知识库 -> 切分为片段
  const documents = await loadDocuments();
  const splits = await splitDocuments(documents);

  // 文本向量化 + 构建内存向量库(示例用内存;生产可替换为 Chroma/FAISS 持久化)
  const embeddings = new OpenAIEmbeddings();
  const vectorstore = await MemoryVectorStore.fromDocuments(splits, embeddings);
  const retriever = vectorstore.asRetriever({ k: 4 });

  // 提示模板:严格要求仅基于上下文回答,不确定则显式说明
  const prompt = ChatPromptTemplate.fromMessages([
    [
      'system',
      '你是一个严谨的知识助手。仅使用给定的检索上下文回答。' +
        '若上下文不足以回答,请回答“我不确定”。\n\n上下文:\n{context}',
    ],
    ['human', '{question}'],
  ]);

  // LCEL 串联:{检索->格式化, 透传问题} -> 提示 -> LLM -> 字符串解析
  const chain = RunnableSequence.from([
    {
      context: retriever.pipe(formatDocs),
      question: new RunnablePassthrough(),
    },
    prompt,
    llm,
    new StringOutputParser(),
  ]);

  return chain;
}

/**
* 交互式 CLI 主程序:循环读取用户问题并输出 RAG 回答
*/
async function main() {
  const rl = readline.createInterface({ input, output });
  console.log('RAG JS Demo 就绪。输入你的问题(Ctrl+C 退出):');

  try {
    const chain = await buildChain();
    // eslint-disable-next-line no-constant-condition
    while (true) {
      const q = (await rl.question('> ')).trim();
      if (!q) continue;
      const answer = await chain.invoke(q);
      console.log('\n=== 回答 ===');
      console.log(answer);
      console.log('\n');
    }
  } catch (err) {
    console.error(err?.message || err);
  } finally {
    rl.close();
  }
}

if (import.meta.url === `file://${__filename}`) {
  // eslint-disable-next-line unicorn/prefer-top-level-await
  main();
}