通过这篇文章我们能学会
- 市面上常见的AI产品本质是什么
- RAG是什么,如何去使用
LLM初识
LLM:Large Language Models,大规模语言模型
很多刚接触前端,或是有一定基础的前端开发者,对于LLM的认识还只是停留在询问豆包,询问ChatGPT这一阶段。所以我们想要接触AI,接触Agent开发时,首先要明确LLM到底是什么?
官方定义:
大型语言模型(large language model,LLM) ,也称大语言模型,是由具有大量参数(通常数十亿个权重或更多)的人工神经网络组成的一类语言模型,使用自监督学习或半监督学习对大量未标记文本进行训练,大语言模型在2018年左右出现,并在各种任务中表现出色。[来自维基百科的官方定义]
看着很多专业词语堆叠一起很复杂,实际上LLM的本质就是一个条件概率分布模型: 即给定上下文(输入+已生成的token),预测下一个token的概率。
🌰
我向LLM输入了这样一段话:
今天肚子好饿,我
此时,LLM就会根据你的这句话计算下一个token(词)的概率
- 我想吃饭。 概率:70%
- 我想运动。 概率:30%
- 我想玩王者荣耀。 概率:20%
LLM发现我想吃饭这个token概率比较高,就拼接到回答上。
最后回答:
今天肚子好饿,我想吃饭
这种看上去合理的答案。
当然上面的例子是很简化的那种。具体操作流程可以看看这张图片
他会多次调用LLM,继续在后面填充token,直到一个满意的答案出现。
了解完LLM,我们需要明确一个概念:LLM能为我们做到的事情:通过不断调用LLM概率计算,帮助我们输出一个合理的答案。
小登们可能就会有些问题:
LLM 能力只有这个,那:
- 豆包为什么输出的内容能记忆之前的回答?
- cursor为什么可以帮助我们更改代码
- ............
原因就在于,这些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会有哪些缺点:
痛点:
- 信息实效性
现代社会发展变更很快,信息的更替也会更快,但是现有的LLM都是依赖训练过的数据进行输出,所以说一些很新的知识,LLM的回答可能会有问题
- 数据的安全性
对于企业来说,数据安全至关重要,没有企业愿意承担数据泄露的风险,尤其是大公司,没有人将私域数据上传第三方平台进行训练会推理
- 幻觉的不可靠性
因为 LLM 本身是基于从大量数据中训练出来的概率模型来一个个生成 token,也就是它并没有逻辑和事实基线。
所以我们说 LLM 的智能是涌现性的智能,是基于概率产生的 “伪智能” ,而不是底层基于逻辑和推理能力“真智能”。
🌰
我们给猴子一个打字机,让它随便打字,如果这个 实验拉长到时间是无限 的。
有没有一种可能,总有一天他会打出一部完整的莎士比亚的小说?
答案是肯定的,因为时间是无限的,而且它是随机打字,那就一定会在某个时间点所有概率都碰巧了,成了一本莎士比亚的小说。
那一个问题来了,猴子到底懂不懂莎士比亚?
那肯定是完全不懂的,它不具备逻辑,只是一切概率性的巧合凑到一起了罢了。
而 LLM 可以理解成为一个更大概率打出莎士比亚的猴子。
它不理解输出文本的逻辑,更不理解内容背后的逻辑。
但因为它的模型足够大,训练数据集足够大,他输出正确内容的概率也足够大。
所以,从外界看来,他就像真正理解内容一样,也就像具有真正的逻辑和推理能力。
为了解决痛点:RAG应运而生
RAG 是 Retrieval-Augmented Generation 的缩写,也就 检索增强生成 的意思。
它主要是为了解决 大模型 本身知识匮乏的问题,主要流程包括索引、检索和生成。
使用RAG能够将大模型“记忆式回答”转变成“查完文档在回答”
从而解决:
- 信息实效性:通过查找前沿资料
- 数据安全性:通过查找公司文档
- 幻觉不可靠性:通过查找资料,减少幻觉产生
具体操作流程
RAG的核心思想是:先检索,再生成
- 检索阶段
- 从外部知识库(如文档、数据库、代码库)中动态查找与用户输入相关的信息。
- 解决生成模型依赖静态训练数据的问题
- 生成阶段
- 将检索到的内容作为上下文,指导 LLM 生成更准确的回答。
- 解决纯检索系统无法灵活组织语言的问题。
-
检索阶段
知识库构建
- 数据源:文档,代码库,数据库
- 预处理:数据源切片,分块,清洗
之所以要分块切片,是因为:
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 → 相反意思
-
生成阶段
构造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();
}