掌握RAG,让大模型输出有据可依!向量数据库+LangChain实战,轻松构建知识问答系统
什么是 RAG
在[上一节课中],我们介绍了一种最简单的微调大模型的方法——提示词工程。我们通过一些原则和技术让模型的输出更加符合我们的需求。
那除了使用提示词技术以外,还有一种在不改变大模型参数情况下还能影响大模型输出的技术 —— RAG(Retrieval-Augmented Generation,检索增强生成),其本质是一种让大模型在回答前先去“查资料”(从你的文档库/数据库/网页/知识库里检索相关内容),再基于检索到的内容生成回答的方案。

那这些找到的内容本质上是放到提示词中,结合提示词工程来对提示词进行设计后,统一展示给大模型并获取回复。所以在某种程度上,很多人认为 RAG 的方案也算是提示词工程中的一部分。
本节课我们将介绍RAG和向量数据库的相关理论,以及基于 Langchain 的 RAG 实战教学。已经有 RAG 理论基础的同学可以直接往下翻到实战教学部分。
本节课内容非常厚实,建议先转发收藏~
为什么需要 RAG
之所以我们不能仅仅依靠提示词来完成所有工作,主要是因为大模型天然有两个很大的问题:
- 知识可能过时:训练数据有时间截止,新的信息它不一定知道。
- 容易幻觉:它会“编得像真的一样”,尤其在细节、数字、条款、具体事实方面。
比如 ChatGPT 5.2 的通用知识只截止到 2024 年的 6 月,那在此之后的内容假如不能额外获取信息的话,要不就会回答它不知道,要不然就会出现幻觉在一本正经的胡说八道。

所以 RAG 存在的核心价值在于用外部可信资料把回答“拴住”,减少幻觉。比如我询问 ChatGPT 让其给我 5 篇 2025 年关于 AI Infra 方面的论文,那其并不会直接给我输出答案,而是先通过网络检索,然后再生成回复,并且把对应的链接发给我让我审查,即便我觉得 AI 回答不够靠谱,我们也可以自己去看原文学习:

所以可以看出, 对于只会根据上下文预测下一个 token 概率分布的模型而言,RAG 能够限制模型发散性的思维,通过真实的数据使其输出收敛。
除此之外,RAG 的一个很核心的价值在于让模型使用企业内部的私有化数据(公司文档、课程资料、论文、规章制度等)。这些数据很多时候都是公司的核心价值,那经过前期的整理后,就能快速的开发形成一个内部的知识问答系统。相比于微调动辄几百张显卡训练几十个小时的方案,RAG 能够立竿见影的看到实际的效果。同时设置的数据库可以随时进行更新,在灵活性上其实是远高于继续训练模型。
RAG 的核心组成
其实我们通过名字就能看出 RAG(Retrieval-Augmented Generation,检索增强生成) 主要由三部分组成:检索(Retrieval)→ 增强(Augmentation)→ 生成(Generation)。
它的核心思想很简单:先把资料找对,再让模型基于资料回答,从而把“凭记忆生成”升级为“有依据生成”。
(1)检索(Retrieval)
所谓检索,其实是从知识库里找与问题最相关的内容,这个是 RAG 中最核心的部分。这里的知识库其实不单单指的是最近比较火的向量数据库,其实还有包括很多传统的数据来源,比如传统的搜索引擎、关系数据库、知识图谱以及内部的文档数据。而针对不同种类的知识库,其使用的检索方法其实都会不一样。
比如对于搜索引擎而言,其信息来源非常广泛,包括公开网页、新闻、博客、论坛、开放文档等。但其检索方式很传统,主要就是通过关键词进行检索,同时还可以添加一些时间、文件类型、网站类型等限定进行搜索。

但是与传统信息系统中以关键词为中心的查询方式不同,大模型时代的用户更倾向于使用自然语言对话来表达需求,其提问往往是完整的句子,甚至是带有抽象语义和上下文背景的问题。
在这种情况下,传统基于关键词匹配的检索系统逐渐暴露出局限性:
- 如果查询语句中未显式包含文档中的关键词,即使语义高度相关,也可能无法被检索到;
- 当用户使用同义词、近义表达或不同说法进行提问时,检索结果往往不稳定,甚至偏离最相关内容;
- 对于概括性、总结性或跨多个概念的问题,关键词检索很难准确刻画用户的真实意图。
因此,“是否包含相同词项”已不再是判断相关性的可靠标准,而“是否在语义层面表达相同或相近含义”才是大模型时代检索系统真正需要解决的问题。
基于这一背景,研究者开始引入向量化表示(Embedding)的思想,将文本从“字符与词项”的形式,映射到一个高维的语义空间中。
在该空间内,每一段文本都被表示为一个向量,其位置反映了文本的语义含义。语义相近的文本在向量空间中距离更近,而语义无关的文本则相距较远。

在此基础上,向量数据库被提出并广泛应用于大模型系统中,用于存储文本对应的语义向量,并支持高效的相似度检索。当用户以自然语言提出问题时,系统同样将该问题转化为向量表示,通过计算向量之间的相似度,检索出与问题语义最接近的若干文本片段。

这些被检索到的片段随后作为上下文信息提供给大模型,从而使模型能够在真实、相关的外部知识基础上进行生成。这也是为什么在大模型时代下,人们一提起 RAG 的第一反应的就是使用向量数据库作为知识库进行检索。那在下一章节里,我们将基于向量数据库技术来带领大家快速搭建起一个 RAG 系统。
当然,传统的搜索引擎并非是完全不可用了,只不过是需要先再加一个意图识别模型,将问题转化为关键词以及相关的限制信息,然后再进行检索。这种方式很适合对一些精确问题的检索,比如时间、机票、酒店的查询。所以最好的方法并不是使用单一的知识库来源,而是根据使用场景选择最合适的知识库并对内部进行检索。
(2)增强(Augmentation)
当检索到候选片段后,增强要做的是把这些片段整理成模型可用的“上下文证据包”,并尽可能提升其可用性与可信度。常见操作包括:
- 去重:避免重复片段占用上下文窗口
- 裁剪:保留关键句,去掉无关段落(控制 token 成本)
- 重排(Rerank):把最关键证据放在最前面
- 补充源信息:文档名/章节/页码/URL,方便追溯与引用
- 冲突处理:多个来源不一致时,按“时间新旧/权威等级”选择或并列呈现
增强的目标是让 LLM 读到的内容更少但更关键、更有结构、更可追溯。
(3)生成(Generation)
最后一步是生成:大模型基于“问题 + 证据上下文”生成最终答案。为了尽量减少幻觉影响,工程上通常会加两类要求:
- 约束生成:只允许依据给定上下文回答;上下文没有明确依据就说明“不确定/缺少资料”
- 输出带引用:在答案中标注来源片段(或文档/页码),让用户能回到原文核对
理想状态下,RAG 输出不仅“像是对的”,而是能说明它为什么对。
RAG 的优缺点
总结上面的内容,我们可以看出,使用上 RAG 技术有以下的优点:
- 显著降低幻觉,答案更“有证据”:模型不再凭记忆编,而是基于检索到的材料回答,并可附引用。
- 知识可更新:对于新加入的知识内容,我们只需要更新文档库就约等于更新系统能力,这样就不再需要频繁训练/微调模型。
- 支持私有知识与垂直知识:公司内网、课程资料、项目文档这些“长尾知识”,RAG 能直接用而不仅仅依赖网络检索的内容。
- 更可解释、更可审计:能给出“依据片段/出处”,便于复核、纠错和责任界定。
- **成本往往更可控:**相比把海量资料塞进上下文窗口,RAG 只取“最相关的少量片段”,省 token、延迟更低。
但同样,RAG 也有一些实际工程应用上的缺点与局限,比如:
- 检索错了,生成就必然错:假如检索载入的内容并不是需要的,那么模型就可能会一本正经地基于错误证据回答。
- 对复杂推理/跨文档整合要求高:当问题需要跨多个文档拼出完整链条(条件、例外、边界),单纯召回部分文档内容可能漏关键段。此时可能需要使用多跳检索(multi-hop)、图谱引导、先抽取再回答、按结构递归检索等方法解决。
- 工程复杂度高:一个可靠的 RAG 系统并不是简单构建一个知识库就足够了,其需要文档处理与增量更新、索引构建、权限控制与过滤以及评测体系(命中率、正确率、引用一致性)等全套的内容。这一系列的内容可以被称为是一个系统工程。
因此这里建议假如是要实现企业/组织知识库问答、客服与技术支持以及合规、制度、风控类问答,这种场景非常建议使用 RAG 技术实现。而对于不需要事实证据(比如纯创作、脑暴、风格写作)、问题不依赖外部知识且本身资料来源混乱不可信的场景,其实根本没有必要使用 RAG 技术。
总的来说,RAG 适用于“答案必须有依据、知识会更新、且存在私有资料”的场景;不适用于纯创作或不依赖外部事实的任务。
基于向量数据库的 RAG 应用实战
向量数据库原理
那在前面提到了,向量数据库作为最适配 RAG 系统的数据常常被使用。那这里我们先简单带大家了解一下到底是什么是向量数据库以及其和传统的数据库有什么差别。
(1)向量数据库 vs 传统数据库
那对于传统的关系数据库,如 MySQL 和 PostgreSQL,其内部是一张张二维表格组成的,那这种类型的数据库擅长处理的是精确匹配(=)、范围查询(> <)等问题。

但是在大模型时代下,我们的问题不再是精确性的匹配,而是语义相似的检索,即哪一部分的内容是和用户的问题是最相关的。举几个简单的例子,比如下面有两句话:
- “猫喜欢吃什么?”
- “家养猫咪饮食习惯?”
这两个句子词不同,但语义相近。而传统数据库无法理解“语义相似”,它只能按关键词匹配,所以两者检索出来的结果将天差地别。
(2)文本向量化
基于这种情况,为了能够实现语义理解,向量数据库引入了文本向量化技术(embedding),将所有的知识库资料转变为一个高维度的向量,利用数学方式计算“相似度”,从而实现语义的精确匹配。

文本向量化技术(Text Embedding)本质上是利用专门的 embedding 模型,将一段自然语言文本转换为一个固定维度的高维浮点向量,用于表示文本的语义信息。
例如,qwen3-0.6B-embedding 模型会将输入文本映射为一个 1024 维向量(更大的模型比如 qwen3-8B-embedding 模型会映射为 4096 维的向量),这个向量可以理解为文本在语义空间中的数学表示,维度越高的话所保存的语义信息将更多。假如要实际体验的话我们可以先下载该模型:
import osfrom huggingface_hub import snapshot_downloados.environ["HF_ENDPOINT"] = "https://hf-mirror.com"snapshot_download( repo_id="Qwen/Qwen3-Embedding-0.6B", local_dir="qwen-embedding", local_dir_use_symlinks=False, # Windows 推荐 False resume_download=True)
假如网络不允许的话可以通过 ModelScope 进行下载:
# 模型下载from modelscope import snapshot_downloadmodel_dir = snapshot_download('Qwen/Qwen3-Embedding-0.6B', local_dir="qwen-embedding")
然后通过 pipeline 对 embedding 模型进行调用后就能获取到结果:
from transformers import pipelinepipe = pipeline("feature-extraction", model=r"qwen-embedding")messages = "你是谁"result = pipe(messages) print(result)
此时就会返回 1024 维的向量:
Loading weights: 100%|██████████| 310/310 [00:00<00:00, 780.00it/s, Materializing param=norm.weight] [[[0.021, -0.882, 0.193, ..., 0.442]]] (假设 1024 维)
换句话说,embedding 模型做的事情是:
- 接收一段文本;
- 通过神经网络计算;
- 输出一个固定长度的向量;
- 这个向量能够在数学空间中表达语义关系。
在这个向量空间中:
- 语义相似的文本,其向量之间的距离会更近;
- 语义差异较大的文本,其向量之间的距离会更远。

正因为这种表示方式能够将语言映射为可计算的连续空间结构,我们才能利用余弦相似度等数学方法进行语义检索、相似度匹配、向量数据库存储以及 RAG 检索增强等应用。因此,文本向量化的核心意义在于,把人类语言转化为可以进行数值计算和空间度量的语义表达形式。
(3)文本向量化训练流程
那相信大家可能会好奇,embedding 模型到底是怎么训练出来的?为什么它可以把一段文本变成一个高维向量,并且这个向量还能反映文本语义?
那对于训练这样一个 embedding 模型而言,一般要经过三个阶段:
- 预训练:学会“读懂语言的大概规律”
- 对比训练:学会“哪些句子意思像、哪些不像”
- 有监督微调:学会“更符合真实任务、更听指令、更稳定”
1. 预训练
那对于预训练而言,其实和前面大模型的预训练是一样的,就是把海量文本丢给模型,让它做一件事:猜下一个词。那经过这样的训练后,模型就能够学习到很多语言中的规律,比如词和词的搭配(“上课”“吃饭”“看电影”)以及句子结构(主谓宾、转折、因果)等。训练完后就可以获得像上节课测试的 qwen3-0.6B-base 这样的模型。
2. 对比训练
但是此时的模型只是擅长生成下一个 token,并不擅长“把输入的内容变成向量”,因此我们需要将模型进行部分的改造,从而能够实现向量的获取。那这里改造的重点并不是说把整个骨干都更换掉,Qwen3-Embedding 仍然是基于 Qwen3 的 Transformer 主干(decoder-only),层数/注意力/FFN 这些核心结构不需要为了做 embedding 重新设计。只不过是取最后一个 token(如 EOS)对应的 hidden state 作为整段文本的表示,然后再进行归一化等处理从而让余弦相似度更稳定。这样处理后,模型就不再输出下一个 token 的概率图,而是输出向量信息了。
但是此时由于并没有针对该任务进行训练,所以文本向量化的能力相对较弱,因此我们需要使用大量的对比数据来进行对比训练,让其能够知道什么样的内容应该在向量空间上更加的接近。那这里的数据主要是以“成对”的方式构造,比如一问一答的内容、一个问题对应一段能回答它的文档内容,从而让模型知道对于什么样的问题,它应该把“真正相关的内容”放得更近,而把“不相关或相关性弱的内容”放得更远。
具体来讲,我们会为每条训练样本准备三类内容:
- Query(问题):用户可能会问的一句话,比如“苹果手机怎么截屏?”
- Positive(正例文档/答案):确实能回答这个问题的内容,比如一段 iPhone 截屏操作说明
- Negative(负例文档/答案):不能回答这个问题的内容,或者看起来很像但其实不对的内容,比如“安卓怎么截屏”或者“iPad 怎么截屏”
然后训练的时候,模型会分别把 query 和这些文本都变成向量,接着计算它们之间的相似度(一般就是余弦相似度)。对比训练的目标非常直接:让 query 与正例的相似度更高,让 query 与负例的相似度更低。这样反复训练以后,模型就会逐步学会一件事:“语义相似”应该在向量空间里表现为“距离更近/相似度更高”。
这里你可能会问:负例到底从哪里来?其实负例有很多种构造方式:
- 随机负例:随便抽一段完全不相关的文档(这种负例太简单,更多是用于打底)
- 批内负例(in-batch negatives):同一个 batch 里其他的正例,那就是天然就是负例(训练效率很高)
- 困难负例(hard negatives):和问题非常像、甚至关键词也很接近,但答案方向不对(这种最能逼着模型学“真正的语义差别”)
也正是因为困难负例的存在,embedding 才不会只停留在“关键词匹配”这种浅层能力上,而是必须学会区分更细的语义边界。比如“iPhone 截屏”和“iPhone 录屏”,它们都包含“iPhone”和“屏”,但一个是截图一个是录制,真正相关的文档必须更近,否则检索结果就会乱。
在这个阶段训练完以后,模型的能力会有一个明显的变化:它不再只是“会读懂语言”,而是开始真正具备“把语义组织到向量空间里”的能力。换句话说,向量空间不再是随机的,而是被训练成了一个“语义地图”。
3. 有监督微调
不过只做对比训练通常还不够,因为对比数据往往规模很大,但质量参差不齐,里面会有一些噪声,比如:
- 问题生成得不够贴近文档内容
- 正例其实只部分相关
- 有些负例其实也挺相关(属于“假负例”),会干扰训练
因此接下来我们就需要进入第三阶段:有监督微调(SFT)。这一步你可以理解为“精修”和“校准”。
SFT 的核心思想是:用更高质量、更贴近真实任务的数据,把向量空间再打磨一遍。这些数据通常来自两类来源:
- 筛选后的高质量合成数据:比如先用一个模型跑一遍相似度,只保留那些“问题和文档确实高度匹配”的样本
- 人工标注或高可信数据:比如真实搜索日志、人工标注的相关性数据、成熟评测集中的样本等
有了这些更干净的数据,模型会进一步学会:
- 相关性边界更清晰(非常相关 / 部分相关 / 不相关)
- 对指令更敏感(比如“给我找操作步骤” vs “给我找原理解释”,它会倾向不同的文档)
- 输出更稳定(同义表达、不同问法不会导致检索结果大幅抖动)
也就是说,SFT 不是推翻前面的对比训练,而是在它的基础上进一步把“语义地图”画得更精细、更可靠。
最后我们再回头看这三个阶段,你就会发现 embedding 模型之所以“看起来很神奇”,其实背后逻辑非常朴素:
- 预训练:让模型具备理解语言的基础能力(能读懂)
- 对比训练:让模型学会把“相似与不相似”映射成向量距离(会摆放)
- 有监督微调:用高质量数据校准,让它更像一个可靠的检索工具(摆得更准、更稳)
当这套流程走完之后,我们就得到了一个真正可用的 embedding 模型:你输入任意文本,它就能输出一个稳定的向量;你拿这个向量去做检索、聚类、去重、推荐时,相似度就能在很大程度上反映语义关系。
(4)向量数据库构建
在将文本内容转化为向量后,我们就能够将其放入到向量数据库当中保存了。那比较出名的向量数据库包括 FAISS、Chroma、Pinecone 等,其主要的提供的功能就是把「向量 + 对应文本/ID + 元数据」保存下来,并支持后续进行检索。当然不同向量数据库的检索方法会有所不同,但差异主要体现在索引与近邻搜索实现、元数据过滤与混合检索能力、持久化与服务化、以及规模化运维能力。

(5)向量数据库检索
那在将向量存入到向量数据库中后,我们的向量数据库也就搭建完成了。但是在实际应用中,我们怎么知道哪一个片段的内容和我们提问的问题最相关最接近呢?在实际操作过程中,这里就需要先将提问的内容也进行向量化,通过计算问题与其他点之间的距离来进行确认是否最相关。

常见的距离函数包括:
- **余弦相似度(Cosine Similarity):**最常使用的方案,用于衡量两个向量方向是否一致。
- **欧氏距离(L2):**衡量空间中实际几何距离。
通过这样的方式,我们确实可以找到与问题最相关的文本片段。但当向量库规模变大(比如百万级 chunk),且每条向量维度很高(常见 768/1024/1536 甚至更高)时,如果还用“逐条计算精确距离”的方式做检索,延迟会迅速不可接受。
所以真正的核心问题会转化为:如何在高维空间里“快速、近似地找到最近邻”(ANN, Approximate Nearest Neighbor)——也就是用可接受的精度损失,换取数量级的速度提升。
那这里常见的解决方案大概有几类:
- 倒排 + 聚类路由(IVF:Inverted File Index)
核心思路是先把向量空间做“粗分桶”:
- 先用聚类(例如 k-means)得到很多个“中心点”(centroids)
- 每个向量只挂到离它最近的中心点桶里
- 查询时先找最接近的几个桶,再只在桶内做距离计算
这样就把原本“全库扫描”变成“只扫一小部分候选集合”,速度会明显提升。

- 图结构近邻搜索(HNSW:Hierarchical Navigable Small World)
这是目前非常常用、综合表现很强的一类 ANN 方法。思路可以理解为:
- 把所有向量连成一个“近邻图”
- 查询时从图的高层快速跳转到接近区域,再在低层做局部精细搜索
- 整体像“先粗定位、再细爬坡”,通常延迟很低、召回率也不错
工程上很多向量库默认就会用 HNSW 或提供类似选项。

- 两阶段检索(粗召回 → 精重排)
即便你用 ANN 召回了候选,也不代表最终就用它直接生成答案。常见做法是:
1)ANN 快速召回 topN(比如 50~200)
2)再用更精确但更慢的方法做重排(rerank),得到 topK(比如 5~10)
这样既保证速度,也能把最终质量拉回来。
总之,大规模向量检索几乎都遵循一个共同策略:
用“近似 + 分层/分桶 + 候选集缩小”来换取速度,然后用重排把质量补回来。
在 qwen 系列模型中也提供了 Reranker 模型,比如 Qwen3-Reranker-0.6B,我们也可以尝试通过该模型提升检索精度。

经过以上内容,我们对整个向量数据库中的关键技术都有了一定的了解。下面我们就来看看在实际操作中该如何实现基于向量数据库的 RAG 技术吧!
LangChain RAG 实战指南
(1) 思路简介
从实际操作的角度来看,一个向量数据库的工作流程通常可以拆成两大部分:向量库的构建(生成)与向量库的使用(检索)。也就是说,我们首先要把知识库资料加工成“可检索的向量索引”,随后在用户提问时,再利用这些索引快速找到最相关的文本片段。

因此,在向量数据库的生成与使用过程中,我们往往需要把整个任务拆解为以下几个步骤:
- 准备数据源:找到知识库的原始资料(网页、PDF、Word、Markdown、数据库等),并将内容加载进程序中
- 文本切分:将载入的内容切分为更小的片段(chunk),以确保后续检索得到的片段能够放进模型上下文,同时也提升检索精度
- 向量化与入库:使用 embedding 模型把每个片段转换为向量,并将“向量 + 原文片段 + 元数据”写入向量数据库中,形成可查询的索引
- 向量检索:当用户提出问题时,先把问题同样向量化,然后在向量数据库中检索最相近的片段,作为后续回答的依据
为了更清晰地演示这条完整链路,下面我们将使用 LangChain 框架来实现。LangChain 是一个基于 Python 与 JavaScript 的开源框架,专门用于简化大模型应用的开发流程,尤其适合快速搭建智能体(Agent)或检索增强生成(RAG)这类典型场景。
那这里为什么要用框架,而不是从零开始自己把每一步都手写出来呢?核心原因就是:框架本身已经帮我们把很多“通用且重复”的部分预先封装好了。比如在向量库构建这条链路里,LangChain 提供了大量现成的组件与标准接口,包括:
- 各类数据源的 Loader(网页、PDF、文本、Markdown 等)
- 多种文本切分策略的 Text Splitter(按字符、按递归规则、按 token 等)
- 统一的 Embedding 接口(可以很方便地切换不同的向量模型)
- 各类向量数据库的 VectorStore 封装(FAISS、Chroma、Milvus 等)
也就是说,我们不需要花大量时间去处理一些“工程上的粘合工作”,而是可以把注意力更多放在数据怎么准备、切分策略怎么选、检索参数怎么调、以及最终效果怎么评估上。这样不仅开发速度更快,代码结构也更清晰,更适合后续扩展。那下面,我们就正式来看看具体操作吧!
(2) 前期准备
在正式开始前,请确保电脑显卡是 cuda 12.1 以上版本且有 6 GB以上显存(我使用的是 R9000P 笔记本的 RTX3060 显卡),并且按下面指令安装相关库(若无显卡可前往云平台租借使用):
conda create -n rag python=3.12 -yconda activate ragpip install torch torchvision --index-url https://download.pytorch.org/whl/cu121Looking in indexes: https://download.pytorch.org/whl/cu121pip install langchain-community langchain langchain-huggingface text-generation sentence_transformers langchain_chroma beautifulsoup4 accelerate pymupdf jqpip install transformers==5.1.0
由于 langchain_huggingface 还没对最新的 huggingface_hub 库进行更新,所以安装时会出现一些版本冲突的问题(最新版本 1.2.1 已解决)。但是由于我们全程都使用本地模型,不需要前往 Huggingface 拉取模型,因此并没有太大的影响,可以忽略这个问题。

另外还需要下载相关模型,包括下载 Qwen/Qwen3-0.6B 模型:
import osfrom huggingface_hub import snapshot_downloados.environ["HF_ENDPOINT"] = "https://hf-mirror.com"snapshot_download( repo_id="Qwen/Qwen3-0.6B", local_dir="qwen", local_dir_use_symlinks=False, # Windows 推荐 False resume_download=True)print("Download finished: qwen")
确保将必要的内容进行下载:

最后要将上一节课模型调用的代码准备好,并且运行后能够使用(model_path 需要更换为自己的下载好的 qwen3-0.6B 才能使用,具体可以查看上一节课的内容):
import torchfrom transformers import AutoTokenizer, AutoModelForCausalLMdef main(): model_path = r"D:\微调与部署\qwen" tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path=model_path, use_fast=True,local_files_only=True) model = AutoModelForCausalLM.from_pretrained( pretrained_model_name_or_path=model_path, trust_remote_code=True, device_map="auto", dtype=torch.float16, ) user_prompt = '''你是谁?''' messages = [{"role": "user", "content": user_prompt}] # 关键:用 Qwen3 的 chat template 生成“模型真正想要的输入文本” text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, enable_thinking=True, # 先关闭 thinking,输出更直接 ) inputs = tokenizer([text], return_tensors="pt").to(model.device) generated = model.generate( **inputs, max_new_tokens=2000, ) # 关键:只取新生成的部分(不把 prompt 原样打印出来) new_tokens = generated[0][inputs["input_ids"].shape[1]:] answer = tokenizer.decode(new_tokens, skip_special_tokens=True).strip() print("\n=== 模型回答 ===") print(answer)if __name__ == "__main__": main()
(3) 准备数据源
那对于某个主题的知识库而言,其来源可能有很多,包括前面提到的网页、PDF、文本、Markdown 等。

假如我们希望将这部分内容载入到 LangChain 框架中为后续“切分—嵌入—检索”作准备的话,我们需要先统一转成 Documents 格式,例如将 PDF 文档转为 Documents 格式内容:
{ "page_content": "人工智能是指由计算机系统执行的智能任务...", "metadata": {"source": "chapter1.pdf", "page_number": 1}}
那转换成 Documents 格式的方式在 LangChain 中非常简单,我们只需要导入已经写好的组件工具即可,常用的格式包括:
- **.txt 格式:**最基础的 loader 格式,只需要安装 langchain_community 即可。
from langchain_community.document_loaders import TextLoaderloader = TextLoader(file_path="./loaders_example/sample.txt", encoding="utf-8")pages = loader.load()print(pages) # 将文档里所有的内容都打印出来
- **网页:**需要安装 beautifulsoup4 库后可使用。
from langchain_community.document_loaders import WebBaseLoaderloader = WebBaseLoader("https://zh.d2l.ai/chapter_introduction/index.html")docs = loader.load()print(docs[0].page_content[:500]) # 将网页里面前500个字符打印出来
那这里我们都是通过 loader.load() 的方式将文档进行载入,其内部具体处理流程如下:
- 遍历所有文件 / 页面
- 全部读完
- 全部变成 Document
- 一次性返回 list[Document]
虽然这种方法简单好理解,但是有一个很大的问题是非常吃内存。因此对于数据量特别大的场景,langchain 中提供了 .lazy_load() 的载入方式,其不会立刻读数据,而是用完一个,再读下一个,这样的话整体的内存消耗就会被大大降低了:
pages = []for doc in loader.lazy_load(): pages.append(doc)print(pages[0].page_content[:100])print(pages[0].metadata)
但由于这个方式并非所有方法都支持,因此下面还是以最常用的 .load 的方法展示一下其他格式的载入内容。
可以在官网中进行更多了解 https://python.langchain.com/docs/integrations/document_loaders/
还有其他常用的格式包括:
- **.csv 格式:**只需要 langchain-community 即可。
from langchain_community.document_loaders.csv_loader import CSVLoaderloader = CSVLoader(file_path="./loaders_example/sample.csv", encoding="utf-8")pages = loader.load()print(pages)
- **.pdf 格式:**首先需要安装 pymupdf 库。
from langchain_community.document_loaders import PyMuPDFLoaderloader = PyMuPDFLoader(r"./loaders_example/sample.pdf")pages = loader.load()print(pages)
- **.json 格式:**首先需要安装 jq 库。
from langchain_community.document_loaders import JSONLoaderloader = JSONLoader(file_path="./loaders_example/sample.json", jq_schema=".", text_content=False) # "." 表示把整个 JSON 文件当做一个 Documentpages = loader.load()print(pages)
- **.xlsx 格式:**首先需要安装 unstructured 、msoffcrypto-tool、networkx、和 openpyxl 库。
from langchain_community.document_loaders import UnstructuredExcelLoaderloader = UnstructuredExcelLoader(r"./loaders_example/sample.xlsx")pages = loader.load()print(pages)
- **.docx 格式:**首先需要安装 docx2txt 库。
from langchain_community.document_loaders import Docx2txtLoaderloader = Docx2txtLoader(r"./loaders_example/sample.docx")pages = loader.load()print(pages)
- **.ppt 格式:**首先需要安装 unstructured, python-magic, python-pptx 库。
from langchain_community.document_loaders import UnstructuredPowerPointLoaderloader = UnstructuredPowerPointLoader(r"./loaders_example/sample.pptx")pages = loader.load()print(pages)
- **.html 格式:**首先需要安装 beautifulsoup4 和 lxml 库。
from langchain_community.document_loaders import BSHTMLLoaderloader = BSHTMLLoader(r"./loaders_example/sample.html", open_encoding="utf-8")pages = loader.load()print(pages)
- **.md 格式:**首先需要安装 unstructured 和 markdown 库。
from langchain_community.document_loaders import UnstructuredMarkdownLoaderloader = UnstructuredMarkdownLoader(r"./loaders_example/sample.md")pages = loader.load()print(pages)
- **.ipynb 格式:**只需要 langchain-community 即可。
from langchain_community.document_loaders import NotebookLoaderloader = NotebookLoader(r"./loaders_example/sample.ipynb", include_outputs=True, max_output_length=20, remove_newline=True)pages = loader.load()print(pages)
- **.xml 格式:**只需要 langchain-community 即可。
from langchain_community.document_loaders import UnstructuredXMLLoaderloader = UnstructuredXMLLoader(r"./loaders_example/sample.xml")pages = loader.load()print(pages)
除此之外, LangChain 中针对部分外部工具提供了特定的载入器:

比如我们想要获取 B 站上某个视频的字幕作为我们文档数据的话,LangChain 中也有相关支持的 BiliBiliLoader 可以使用(需要安装 bilibili-api-python)。我们需要打开并登录 B 站 。然后打开开发者模式(F12)并找到应用程序:

然后我们可以在筛选器里搜索三部分内容,并把对应的值保留下来:
- SESSDATA = "<your sessdata>"- BUVID3 = "<your buvids>"- BILI_JCT = "<your bili_jct>"
举个搜索的例子:
然后我们就可以创建载入器(loader)来对有字幕的视频进行提取了!
from langchain_community.document_loaders import BiliBiliLoaderloader = BiliBiliLoader( ["https://www.bilibili.com/video/BV1g84y1R7oE/"], sessdata = "<your sessdata>", buvid3 = "<your buvids>", bili_jct = "<your bili_jct>")docs = loader.load()print(docs)
类似的我们也可以获取 YouTube 上视频的字幕,只需要使用 YoutubeLoader 并安装 pytube 和 youtube-transcript-api 库即可(要求外网才可使用):
from langchain_community.document_loaders import YoutubeLoaderloader = YoutubeLoader.from_youtube_url( "https://www.youtube.com/watch?v=QsYGlZkevEg", add_video_info=True, language=["en", "id"], translation="en",)docs = loader.load()print(docs)
以上就是文档载入器部分的全部内容了,里面涵盖了多种的数据源解析方式,相信能够满足大家对于数据源准备的要求了。那在下面的示例中,我将以大家最容易入手的网页来作为示例,来给大家演示一下 RAG 的实际操作吧。这里我们选用的就是维基百科上关于过拟合部分的描述:
from langchain_community.document_loaders import WebBaseLoaderloader = WebBaseLoader(r"https://zh.wikipedia.org/wiki/过拟合")docs = loader.load()print(docs)
此时将 docs 打印出来可以看到其就是一个 Document 类,里面保存了元数据以及具体内容信息:
[Document(metadata={'source': 'https://zh.wikipedia.org/wiki/过拟合', 'title': '過適 - 维基百科,自由的百科全书', 'language': 'zh'}, page_content='\n\n\n\n過適 - 维基百科,自由的百科全书...')]
(4) 文本切分
文档切分是指将一个长文本的 Document 拆成若干个更小的段落(Chunks),每个段落大小适合被大模型理解和向量化处理。
之所以要切分是因为:
- 大模型通常有 Token 长度限制(如 2048 / 4096 tokens),原始文档太长无法直接处理,必须拆分。
- 长文本容易造成语义漂移,而小段文本可以更聚焦地表达一个意思,有利于后续检索和匹配。
即便现在有些技术声称可以处理无限长度的内容,但从经济角度来看,更长的上下文意味着更高的计算成本和费用,因此并不划算。
切分的方法有很多种,这里我们介绍一种最简单的方法——CharacterTextSplitter(基于字数进行划分)。例如, chunk_size 表示每块内容的长度为 4000 个 token(字符),而 chunk_overlap 表示上下文重叠部分的长度为 200 个字符。

除了 CharacterTextSplitter 以外,其实LangChain 里还有很多其他的切分方式,比如 RecursiveCharacterTextSplitter 。该方法会更加细致地分割文档,因为它不仅考虑分割后的文本长度,还会兼顾重叠字符。默认情况下, 其使用 ["\n\n", "\n", " ", ""] 四种特殊符号作为分割文本的标记,并按照优先级顺序进行分割:首先尝试在双换行符( \n\n )处分割,然后是单换行符( \n ),接着是空格,最后在无法找到合适分割点时强制进行分割。
因此,虽然使用 RecursiveCharacterTextSplitter 分割后的文本长度可能与设定的 chunk_size 不完全一致,但它会更倾向于按照句子或段落的形式来分割文本,从而保持更好的可读性和语义连贯性。
我们可以举一个简单的例子来看看两者的差别,对于同样一段文本:
some_text = """When writing documents, writers will use document structure to group content. This can convey to the reader, which idea's are related. For example, closely related ideas are in sentances. Similar ideas are in paragraphs. Paragraphs form a document. \n\n Paragraphs are often delimited with a carriage return or two carriage returns. Carriage returns are the "backslash n" you see embedded in this string. Sentences have a period at the end, but also, have a space. and words are separated by space."""
假如我们使用的是 CharacterTextSplitter 进行切分的话:
from langchain_text_splitters import CharacterTextSplitterc_splitter = CharacterTextSplitter( chunk_size=450, chunk_overlap=0, separator = ' ')print(c_splitter.split_text(some_text))
得到的结果是:
['When writing documents, writers will use document structure to group content. This can convey to the reader, which idea\'s are related. For example, closely related ideas are in sentances. Similar ideas are in paragraphs. Paragraphs form a document. \n\n Paragraphs are often delimited with a carriage return or two carriage returns. Carriage returns are the "backslash n" you see embedded in this string. Sentences have a period at the end, but also,', 'have a space. and words are separated by space.']
可以看出来这个是严格按照字符数量来进行切分的。但是假如使用的是 RecursiveCharacterTextSplitter 的方法:
from langchain_text_splitters import RecursiveCharacterTextSplitterr_splitter = RecursiveCharacterTextSplitter( chunk_size=450, chunk_overlap=0, separators=["\n\n", "\n", " ", ""])print(r_splitter.split_text(some_text))
此时切分出来的结果为:
["When writing documents, writers will use document structure to group content. This can convey to the reader, which idea's are related. For example, closely related ideas are in sentances. Similar ideas are in paragraphs. Paragraphs form a document.", 'Paragraphs are often delimited with a carriage return or two carriage returns. Carriage returns are the "backslash n" you see embedded in this string. Sentences have a period at the end, but also, have a space. and words are separated by space.']
由此我们可以看出,对于自然语言处理的任务来说,一般情况下为了获取每个句子中更多的语义信息,我们通常会使用 RecursiveCharacterTextSplitter 的方式进行文本的分割。
当然文本的分割其实还有很多能够调整的地方,比如说我们可以不仅仅用默认的 separators=["\n\n", "\n", " ", ""] ,我们可以加入一些别的分割符号,并且根据文本的不同来进行调整。
因此对于我们载入的网页内容,我们也尝试使用 RecursiveCharacterTextSplitter 对其进行切分,具体切分的内容就是前面载入的 docs 内容:
from langchain_text_splitters import RecursiveCharacterTextSplittertext_splitter = RecursiveCharacterTextSplitter( chunk_size = 500, chunk_overlap = 150)splits = text_splitter.split_documents(docs)print(splits)
此时打印的结果大致如下(总共被切分为了 15 块):
[ Document( metadata={ "source": "https://zh.wikipedia.org/wiki/过拟合", "title": "過適 - 维基百科,自由的百科全书", "language": "zh", }, page_content="過適 - 维基百科,自由的百科全书\n...\n目录\n...\n序言\n1 过拟合(Overfitting)技术精要\n 1.1 核心定义\n 1.2 与欠拟合对比\n 1.3 产生机制修正\n 1.4 关键技术补充\n2 机器学习\n 2.1 后果\n3 扩展阅读\n4 參考文獻\n5 外部連結\n..." ), Document( metadata={ "source": "https://zh.wikipedia.org/wiki/过拟合", "title": "過適 - 维基百科,自由的百科全书", "language": "zh", }, page_content="1 过拟合(Overfitting)技术精要\n 1.1 核心定义\n 1.2 与欠拟合对比\n 1.3 产生机制修正\n 1.4 关键技术补充\n2 机器学习\n 2.1 后果\n3 扩展阅读\n4 參考文獻\n5 外部連結\n..." ), Document( metadata={ "source": "https://zh.wikipedia.org/wiki/过拟合", "title": "過適 - 维基百科,自由的百科全书", "language": "zh", }, page_content="...(页面工具/外观/编辑等导航内容已省略)..." ), Document( metadata={ "source": "https://zh.wikipedia.org/wiki/过拟合", "title": "過適 - 维基百科,自由的百科全书", "language": "zh", }, page_content="(正文示意)\n...\n过拟合:训练集很好,但新数据泛化差\n欠拟合:模型太简单,抓不住规律\n偏差-方差:过拟合=低偏差高方差;欠拟合=高偏差低方差\n..." ), Document( metadata={ "source": "https://zh.wikipedia.org/wiki/过拟合", "title": "過適 - 维基百科,自由的百科全书", "language": "zh", }, page_content="(避免/减轻过拟合的方法)\n...\n模型选择、交叉验证、提前停止、正则化、剪枝、AIC/BIC、dropout\n..." ), Document( metadata={ "source": "https://zh.wikipedia.org/wiki/过拟合", "title": "過適 - 维基百科,自由的百科全书", "language": "zh", }, page_content="...(其余段落、示例、扩展阅读、参考文献、外部链接等已省略)..." ),]
另外在 LangChain 中针对某些特定格式的文档还给出专门的切分指引,包括代码类文件、Markdown 文件,这都能帮助我们快速完成文档切分工作。那这些的本质都是根据这类文件的具体格式并调整 RecursiveCharacterTextSplitter 的分割符号来进行实现的。
(5) 文本向量化
在切分好了以后,我们就可以将切分好的文档内容通过 embedding 模型转变为向量后存储到向量数据库中了。那这里的 embedding 模型我们可以选择使用大公司开放的 API 接口,比如通过百炼大模型平台来进行向量化(首先要有 DASHSCOPE_API_KEY):
from langchain_community.embeddings import DashScopeEmbeddingsimport os# 设置 embedding 模型(阿里云)embeddings = DashScopeEmbeddings( dashscope_api_key=os.getenv('DASHSCOPE_API_KEY'))# 设置文本内容text_1 = "今天天气不错"# 进行文本向量化query_result = embeddings.embed_query(text_1)print(query_result)
此时就能得到完整的向量信息:
[-0.0075800973, 0.05336676, -0.017781364, 0.035153043, ...]
当然我们也可以选择使用 Huggingface 上开源的 embedding 模型,比如我们前面在介绍 embedding 模型原理时提到的 qwen3-0.6B-embedding 模型,这里我们就需要安装 langchain-huggingface 等相关库来进行使用:
from langchain_huggingface.embeddings import HuggingFaceEmbeddingsembeddings = HuggingFaceEmbeddings(model_name=r"qwen-embedding")# 设置文本内容text_1 = "今天天气不错"# 进行文本向量化query_result = embeddings.embed_query(text_1)print(query_result)
此时也能够得到完整的向量信息:
Loading weights: 100%|██████████| 310/310 [00:00<00:00, 410.73it/s, Materializing param=norm.weight] [-0.023681640625, -0.0172119140625, -0.00860595703125, -0.01953125, ...]
那开源的方案只需要我们有一定的显存即可尝试,因此后续我们都将基于开源的方案进行实现。
(6) 载入向量数据库
在准备好了 embedding 模型后,我们就可以将其载入到向量数据库当中了。在 LangChain 中有很多种不同类型的向量数据库,其中最基础的就是基于内存的 InMemoryVectorStore :
from langchain_huggingface.embeddings import HuggingFaceEmbeddingsfrom langchain_core.vectorstores import InMemoryVectorStoreembeddings = HuggingFaceEmbeddings(model_name=r"qwen-embedding")vector_store = InMemoryVectorStore(embedding=embeddings)
其主要包含三个方法(其他向量数据库也类似):
- 添加文档:
vector_store.add_documents(documents=[doc1], ids=["id1"]) - 删除文档:
vector_store.delete(ids=["id1"]) - 相似度检索:
vector_store.similarity_search("your query here")
但是 InMemoryVectorStore 无法进行长期保存,当程序运行结束后,向量数据库内的内容将自动清除。因此为了能够更长久的保存,Langchain 提供了更专业的向量数据库支持,包括 Chroma 、Pinecone 以及 FAISS 等。那这里我就以 Chroma 为例来展示一下具体的使用方式(需要先安装 langchain-chroma 库):
from langchain_chroma import Chromavectordb = Chroma.from_documents( documents=splits, embedding=embeddings, persist_directory='./chroma')print(vectordb._collection.count())
此时打印一下存储到向量数据库里的文档数量,返回的结果和切分的结果一样都是 15 。

以上就是向量数据库生成的全过程,当然这里我们也可以做成一个输入 url 链接的函数,这样我们就可以通过输入不同链接不断向向量数据库中添加内容:
from langchain_huggingface.embeddings import HuggingFaceEmbeddingsfrom langchain_text_splitters import RecursiveCharacterTextSplitterfrom langchain_community.document_loaders import WebBaseLoaderdef vectorstore(url): loader = WebBaseLoader(url) docs = loader.load() text_splitter = RecursiveCharacterTextSplitter( chunk_size = 500, chunk_overlap = 150) splits = text_splitter.split_documents(docs) embeddings = HuggingFaceEmbeddings(model_name=r"D:\微调与部署\qwen-embedding") vectordb = Chroma.from_documents( documents=splits, embedding=embeddings, persist_directory='./chroma') print(vectordb._collection.count())if __name__ == "__main__": vectorstore(url=r"https://zh.wikipedia.org/wiki/过拟合")
这样输入任意链接都可以进行创建了,但是需要注意的是这个代码只需要运行一次,运行多次的话就会把重复的内容放到向量数据库里了。
(7) 向量数据库检索
在创建好向量数据库后,假如不是要再往里面添加内容的话,其实上面的代码都不用再运行了。接下来我们只需要运行下面的代码传入 embedding 模型以及向量数据库存储的路径就能加载好向量数据库:
from langchain_chroma import Chromafrom langchain_huggingface.embeddings import HuggingFaceEmbeddings# 使用当前创建的向量数据库embeddings = HuggingFaceEmbeddings(model_name=r"D:\微调与部署\qwen-embedding")vectordb = Chroma(persist_directory="./chroma", embedding_function=embeddings)
加载完成后我们就可以基于传入的问题在向量数据库里检索。在 LangChain 中最基础的检索方式就是前面提到的相似度检索,那这里输入的参数 k 代表的是找到最相近的 k 个内容,也就是说在 .invoke 后返回的是一个列表里面有三个返回的 Documents:
# 设置问题question = "过拟合与欠拟合有什么区别?"# 利用相似度搜索检索与问题最相关的3个切片retriever = vectordb.as_retriever( search_type="similarity", search_kwargs={"k": 3})docs = retriever.invoke(question)# 打印第一个最相关的切块内容print(docs[0].page_content)
那我们这里单独获取到第一个最相关 Documents 的文本内容 page_content,这个就是返回来最相近的内容:
与过拟合相对应的概念是欠拟合(英語:underfitting,或稱:擬合不足);它是指相较于数据而言,模型参数过少或者模型结构过于简单,以至于无法捕捉到数据中的规律的现象。发生欠拟合时,模型的偏差大而方差小。在机器学习或人工神經網路中,过拟合与欠拟合有时也被称为「过训练(英語:overtraining)」和「欠训练(英語:undertraining)」。之所以存在过拟合的可能,是因为选择模型的标准和评价模型的标准是不一致的。举例来说,选择模型时往往是选取在训练数据上表现最好的模型;但评价模型时则是观察模型在训练过程中不可见数据上的表现。当模型尝试「记住」训练数据而非从训练数据中学习规律时,就可能发生过拟合。一般来说,當參數的自由度或模型结构的复杂度超過資料所包含資訊內容時,拟合后的模型可能使用任意多的參數,這會降低或破壞模型泛化的能力。
可以看出来这个找出来的答案还是蛮精准的。除了相似度检索以外,一个比较常用的检索方法是最大边际相关性(Maximum Marginal Relevance, MMR)的方法。其是一种在信息检索中用于平衡 相关性 和 多样性 的技术,特别适用于需要避免重复内容并提高信息覆盖面的场景。它在传统的基于相似度的检索方法上进行了扩展,旨在通过同时考虑文档与查询的相关性以及文档之间的多样性来优化检索结果:
- **相关性(Relevance):**文档与查询之间的相似度,反映了文档对查询的相关性。
- **多样性(Diversity):**文档之间的相似度,旨在避免返回重复或冗余的内容。
MMR 通过以下公式进行平衡,MMR 值越高,表示文档既与查询相关又具有较大的多样性:

其中:
- D 是候选文档,Q 是查询问题。
- Relevance 是候选文档与查询问题之间的相关性分数。例如使用余弦相似度等方法。
- Similarity 是候选文档 D 和已选文档 D′之间的相似度,避免获取过于相近的文档。
- R 是已选文档集合。在每次计算新的文档 D 的 MMR 时,我们会查看这个集合中的所有文档 D′,并计算 D 和这些已选文档的相似度。
- α 是一个权重因子,用来平衡相关性和多样性。 α 越小会越倾向于多样性,反之是相关性。
其具体实现流程为:
- 计算所有候选文档的 MMR 值。
- 选择 MMR 值最高的文档。MMR 值越高,文档的相关性和多样性就越好。
- 将选择的文档加入已选文档集合 R 中。
- 更新每个剩余文档的 MMR 值,并选择下一个 MMR 值最高的文档。这个过程通常会继续进行,直到满足某个条件(比如选定的文档数目、满足查询的准确度等)。
我们可以在代码中输入部分参数进行查询:
# 设置问题question = "过拟合与欠拟合有什么区别?"# fetch_k 对应公式里的 R 参数,lambda_mult 对应公式里的 α 权重retriever = vectordb.as_retriever(search_type="mmr", search_kwargs={"k": 3, "fetch_k": 10, "lambda_mult": 0.25})docs = retriever.invoke(question)# 打印第一个最相关的切块内容print(docs[0].page_content)
此时同样能够找出一样的文档内容:
与过拟合相对应的概念是欠拟合(英語:underfitting,或稱:擬合不足);它是指相较于数据而言,模型参数过少或者模型结构过于简单,以至于无法捕捉到数据中的规律的现象。发生欠拟合时,模型的偏差大而方差小。在机器学习或人工神經網路中,过拟合与欠拟合有时也被称为「过训练(英語:overtraining)」和「欠训练(英語:undertraining)」。之所以存在过拟合的可能,是因为选择模型的标准和评价模型的标准是不一致的。举例来说,选择模型时往往是选取在训练数据上表现最好的模型;但评价模型时则是观察模型在训练过程中不可见数据上的表现。当模型尝试「记住」训练数据而非从训练数据中学习规律时,就可能发生过拟合。一般来说,當參數的自由度或模型结构的复杂度超過資料所包含資訊內容時,拟合后的模型可能使用任意多的參數,這會降低或破壞模型泛化的能力。
那找到最相关的文档内容后,我们其实就可以将这部分文档内容载入到模型的提示词当中并进行使用了!那这里我们也可以将这部分内容转化为一个函数,输入是问题(字符串),输出则是找到最相关的文本片段(字符串):
from langchain_huggingface.embeddings import HuggingFaceEmbeddingsfrom langchain_chroma import Chromadef retriever(question): # 使用当前创建的向量数据库 embeddings = HuggingFaceEmbeddings(model_name=r"qwen-embedding") vectordb = Chroma(persist_directory="./chroma", embedding_function=embeddings) # 利用相似度搜索检索与问题最相关的1个切片 retriever = vectordb.as_retriever( search_type="similarity", search_kwargs={"k": 1} ) docs = retriever.invoke(question) context = docs[0].page_content return context
(8) 设计提示词并回复
前面我们也提到了,RAG 虽然能够从知识库中找到相关的内容给到大模型,但其本质还是提示词工程的一部分。因此我们需要合理的设置提示词,将输入的问题和找到的文本片段都放进去后再传给模型。这里我们就用最简单的 f-string 方式来进行内容的传入:
user_prompt = f'''请使用以下上下文信息回答最后的问题。如果您不知道答案,就直接说您不知道,不要试图编造答案。上下文:{context}问题:{question}'''
然后我们就可以将这部分提示词和前面创建好的 retriver 函数一同放入到最开始模型调用的函数中:
import torchfrom transformers import AutoTokenizer, AutoModelForCausalLMfrom langchain_chroma import Chromafrom langchain_huggingface.embeddings import HuggingFaceEmbeddingsdef main(question): model_path = r"qwen" tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path=model_path, use_fast=True,local_files_only=True) model = AutoModelForCausalLM.from_pretrained( pretrained_model_name_or_path=model_path, trust_remote_code=True, device_map="auto", dtype=torch.float16, ) context = retriever(question) user_prompt = f''' 请使用以下上下文信息回答最后的问题。 如果您不知道答案,就直接说您不知道,不要试图编造答案。 上下文:{context} 问题:{question} ''' messages = [{"role": "user", "content": user_prompt}] # 关键:用 Qwen3 的 chat template 生成“模型真正想要的输入文本” text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, enable_thinking=False, # 先关闭 thinking,输出更直接 ) inputs = tokenizer([text], return_tensors="pt").to(model.device) generated = model.generate( **inputs, max_new_tokens=2000 ) # 关键:只取新生成的部分(不把 prompt 原样打印出来) new_tokens = generated[0][inputs["input_ids"].shape[1]:] answer = tokenizer.decode(new_tokens, skip_special_tokens=True).strip() print("\n=== 模型回答 ===") print(answer)if __name__ == "__main__": main(question="过拟合与欠拟合有什么区别?")
此时返回的结果为:
=== 模型回答 ===过拟合和欠拟合是机器学习中模型性能的两种不同状态,它们的区别在于:- **过拟合**:模型参数过少或结构过于简单,导致模型无法捕捉数据中的规律,反而倾向于记住训练数据中的模式,从而出现偏差大、方差高的情况。- **欠拟合**:模型参数过多或结构复杂,导致模型无法有效学习数据中的规律,反而可能因参数过多而引入过拟合的风险。两者的主要区别在于模型的复杂度和泛化能力。过拟合发生在模型过于简单或参数过多时,而欠拟合则是在模型过于复杂时,导致模型无法有效学习数据。
我们可以和直接运行返回的结果进行对比:
import torchfrom transformers import AutoTokenizer, AutoModelForCausalLMdef main(question): model_path = r"qwen" tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path=model_path, use_fast=True,local_files_only=True) model = AutoModelForCausalLM.from_pretrained( pretrained_model_name_or_path=model_path, trust_remote_code=True, device_map="auto", dtype=torch.float16, ) user_prompt = question messages = [{"role": "user", "content": user_prompt}] # 关键:用 Qwen3 的 chat template 生成“模型真正想要的输入文本” text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, enable_thinking=False, # 先关闭 thinking,输出更直接 ) inputs = tokenizer([text], return_tensors="pt").to(model.device) generated = model.generate( **inputs, max_new_tokens=2000, ) # 关键:只取新生成的部分(不把 prompt 原样打印出来) new_tokens = generated[0][inputs["input_ids"].shape[1]:] answer = tokenizer.decode(new_tokens, skip_special_tokens=True).strip() print("\n=== 模型回答 ===") print(answer)if __name__ == "__main__": main(question="过拟合与欠拟合有什么区别?")
此时返回的结果为:
=== 模型回答 ===过拟合(Overfitting)和欠拟合(Underfitting)是机器学习中常见的两种模型性能问题,它们的区别主要体现在模型的**复杂度**、**泛化能力**和**训练效果**上。下面详细解释两者的区别:---### 1. **过拟合(Overfitting)**- **定义**:模型在训练数据上表现很好,但**泛化能力差**,即模型对新数据的预测能力低。- **表现**: - 模型在训练集上表现很好,但测试集或验证集表现差。 - 模型对训练数据的细节(如噪声、细节)非常敏感,容易“记住”训练数据中的特征,而不是泛化到新数据。- **原因**: - 模型过于复杂,导致参数过多,训练数据中包含过多的噪声或细节。 - 模型没有充分学习数据中的模式,只依赖训练数据中的小部分特征。- **例子**: - 一个简单的线性模型(如线性回归)在训练集上表现很好,但在测试集上表现差。 - 模型对训练数据中的某些特征非常敏感,但测试数据中这些特征缺失或不相关。---### 2. **欠拟合(Underfitting)**- **定义**:模型在训练数据上表现差,无法捕捉到数据中的关键模式。- **表现**: - 模型在训练集上表现差,无法准确预测新数据。 - 模型过于简单,无法学习到数据中的有效特征。- **原因**: - 模型太简单,无法学习到数据中的复杂模式。 - 模型参数太多,导致模型无法学习到数据中的有效特征。- **例子**: - 一个简单的线性回归模型(如线性回归)在训练集上表现很好,但在测试集上表现差。 - 模型无法捕捉到数据中的关键特征,导致预测结果不准确。---### 3. **关键区别总结**| 项目 | 过拟合(Overfitting) | 欠拟合(Underfitting) ||--------------|----------------------|------------------------|| **特征** | 模型过于复杂,学习能力强 | 模型太简单,学习能力差 || **泛化能力** | 小,对新数据预测差 | 大,对新数据预测好 || **训练效果** | 训练集表现好,测试集表现差 | 训练集表现差,测试集表现好 || **例子** | 线性回归在训练集表现好,测试集差 | 线性回归在训练集表现差,测试集好 |---### 4. **如何避免过拟合和欠拟合?**- **过拟合**:增加模型复杂度,使用交叉验证、正则化(如L1、L2正则化)。- **欠拟合**:减少模型复杂度,增加模型参数,使用更复杂的模型。---总之,过拟合和欠拟合是模型性能的两种极端情况,需根据数据和任务选择合适的模型结构。
可以看出,有了 RAG 的引导后,模型的输出会更精确也更符合实际的要求了。以上就是 RAG 演示的所有内容了,在了解完基本的概念后,大家也可以借助 AI 工具的能力,根据自己的想法对代码进行进一步的开发和改造了!完整的代码如下所示:
# 向量数据库生成from langchain_huggingface.embeddings import HuggingFaceEmbeddingsfrom langchain_text_splitters import RecursiveCharacterTextSplitterfrom langchain_community.document_loaders import WebBaseLoaderdef vectorstore(url): loader = WebBaseLoader(url) docs = loader.load() text_splitter = RecursiveCharacterTextSplitter( chunk_size = 500, chunk_overlap = 150) splits = text_splitter.split_documents(docs) embeddings = HuggingFaceEmbeddings(model_name=r"D:\微调与部署\qwen-embedding") vectordb = Chroma.from_documents( documents=splits, embedding=embeddings, persist_directory='./chroma') print(vectordb._collection.count()) # 向量数据库检索from langchain_huggingface.embeddings import HuggingFaceEmbeddingsfrom langchain_chroma import Chromadef retriever(question): # 使用当前创建的向量数据库 embeddings = HuggingFaceEmbeddings(model_name=r"qwen-embedding") vectordb = Chroma(persist_directory="./chroma", embedding_function=embeddings) # 利用相似度搜索检索与问题最相关的1个切片 retriever = vectordb.as_retriever( search_type="similarity", search_kwargs={"k": 1} ) docs = retriever.invoke(question) context = docs[0].page_content return context# 模型调用import torchfrom transformers import AutoTokenizer, AutoModelForCausalLMdef main(question): model_path = r"D:\微调与部署\qwen" tokenizer = AutoTokenizer.from_pretrained(pretrained_model_name_or_path=model_path, use_fast=True,local_files_only=True) model = AutoModelForCausalLM.from_pretrained( pretrained_model_name_or_path=model_path, trust_remote_code=True, device_map="auto", dtype=torch.float16, ) context = retriever(question) user_prompt = f''' 请使用以下上下文信息回答最后的问题。 如果您不知道答案,就直接说您不知道,不要试图编造答案。 上下文:{context} 问题:{question} ''' messages = [{"role": "user", "content": user_prompt}] # 关键:用 Qwen3 的 chat template 生成“模型真正想要的输入文本” text = tokenizer.apply_chat_template( messages, tokenize=False, add_generation_prompt=True, enable_thinking=False, # 先关闭 thinking,输出更直接 ) inputs = tokenizer([text], return_tensors="pt").to(model.device) generated = model.generate( **inputs, max_new_tokens=2000, do_sample=True, temperature=0.7, top_p=0.8, top_k=20, ) # 关键:只取新生成的部分(不把 prompt 原样打印出来) new_tokens = generated[0][inputs["input_ids"].shape[1]:] answer = tokenizer.decode(new_tokens, skip_special_tokens=True).strip() print("\n=== 模型回答 ===") print(answer)if __name__ == "__main__": vectorstore(url=r"https://zh.wikipedia.org/wiki/过拟合") main(question="过拟合与欠拟合有什么区别?")
总结
写到这里,这篇关于 RAG 的内容就先收尾了。
你会发现,RAG 本质上是在推理阶段给模型“加拐杖”:把证据塞进上下文,用真实资料把模型输出拴住,特别适合知识会更新、答案必须有出处、以及企业私有数据问答这类场景。但很多时候,我们并不只是希望模型“引用资料回答”,而是希望它在长期稳定的任务里形成一种固定能力,比如:
- 输出格式必须一致(JSON/表格/评分模板)
- 语气风格要统一(客服话术、教学口吻、审稿语气)
- 任务边界要清晰(该拒绝就拒绝、该追问就追问)
- 对特定领域问题能更稳地答对(在你提供的高质量样本分布上)
这些更像是“行为模式”和“任务技能”的问题——这就轮到 SFT 登场了:它不是在提示词里临时塞材料,而是把你认可的输入输出对,直接用于训练,让模型在参数层面学会“应该怎么答”。
所以下一节我们会从三个问题把 SFT 讲透:
- SFT 到底在训练什么:为什么叫“有监督”,loss 是怎么来的
- 数据长什么样才有效:指令数据、对话数据、格式约束数据分别怎么做
- 工程上怎么落地:数据清洗、训练配置、LoRA/全参的选择、以及怎么评估“微调真的变好了吗”
01
什么是AI大模型应用开发工程师?
如果说AI大模型是蕴藏着巨大能量的“后台超级能力”,那么AI大模型应用开发工程师就是将这种能量转化为实用工具的执行者。
AI大模型应用开发工程师是基于AI大模型,设计开发落地业务的应用工程师。
这个职业的核心价值,在于打破技术与用户之间的壁垒,把普通人难以理解的算法逻辑、模型参数,转化为人人都能轻松操作的产品形态。
无论是日常写作时用到的AI文案生成器、修图软件里的智能美化功能,还是办公场景中的自动记账工具、会议记录用的语音转文字APP,这些看似简单的应用背后,都是应用开发工程师在默默搭建技术与需求之间的桥梁。
他们不追求创造全新的大模型,而是专注于让已有的大模型“听懂”业务需求,“学会”解决具体问题,最终形成可落地、可使用的产品。
CSDN粉丝独家福利
给大家整理了一份AI大模型全套学习资料,这份完整版的 AI 大模型学习资料已经上传CSDN,朋友们如果需要可以扫描下方二维码&点击下方CSDN官方认证链接免费领取 【保证100%免费】

02
AI大模型应用开发工程师的核心职责
需求分析与拆解是工作的起点,也是确保开发不偏离方向的关键。
应用开发工程师需要直接对接业务方,深入理解其核心诉求——不仅要明确“要做什么”,更要厘清“为什么要做”以及“做到什么程度算合格”。
在此基础上,他们会将模糊的业务需求拆解为具体的技术任务,明确每个环节的执行标准,并评估技术实现的可行性,同时定义清晰的核心指标,为后续开发、测试提供依据。
这一步就像建筑前的图纸设计,若出现偏差,后续所有工作都可能白费。
技术选型与适配是衔接需求与开发的核心环节。
工程师需要根据业务场景的特点,选择合适的基础大模型、开发框架和工具——不同的业务对模型的响应速度、精度、成本要求不同,选型的合理性直接影响最终产品的表现。
同时,他们还要对行业相关数据进行预处理,通过提示词工程优化模型输出,或在必要时进行轻量化微调,让基础模型更好地适配具体业务。
此外,设计合理的上下文管理规则确保模型理解连贯需求,建立敏感信息过滤机制保障数据安全,也是这一环节的重要内容。
应用开发与对接则是将方案转化为产品的实操阶段。
工程师会利用选定的开发框架构建应用的核心功能,同时联动各类外部系统——比如将AI模型与企业现有的客户管理系统、数据存储系统打通,确保数据流转顺畅。
在这一过程中,他们还需要配合设计团队打磨前端交互界面,让技术功能以简洁易懂的方式呈现给用户,实现从技术方案到产品形态的转化。
测试与优化是保障产品质量的关键步骤。
工程师会开展全面的功能测试,找出并修复开发过程中出现的漏洞,同时针对模型的响应速度、稳定性等性能指标进行优化。
安全合规性也是测试的重点,需要确保应用符合数据保护、隐私安全等相关规定。
此外,他们还会收集用户反馈,通过调整模型参数、优化提示词等方式持续提升产品体验,让应用更贴合用户实际使用需求。
部署运维与迭代则贯穿产品的整个生命周期。
工程师会通过云服务器或私有服务器将应用部署上线,并实时监控运行状态,及时处理突发故障,确保应用稳定运行。
随着业务需求的变化,他们还需要对应用功能进行迭代更新,同时编写完善的开发文档和使用手册,为后续的维护和交接提供支持。
03
薪资情况与职业价值
市场对这一职业的高度认可,直接体现在薪资待遇上。
据猎聘最新在招岗位数据显示,AI大模型应用开发工程师的月薪最高可达60k。

在AI技术加速落地的当下,这种“技术+业务”的复合型能力尤为稀缺,让该职业成为当下极具吸引力的就业选择。
AI大模型应用开发工程师是AI技术落地的关键桥梁。
他们用专业能力将抽象的技术转化为具体的产品,让大模型的价值真正渗透到各行各业。
随着AI场景化应用的不断深化,这一职业的重要性将更加凸显,也必将吸引更多人才投身其中,推动AI技术更好地服务于社会发展。
CSDN粉丝独家福利
给大家整理了一份AI大模型全套学习资料,这份完整版的 AI 大模型学习资料已经上传CSDN,朋友们如果需要可以扫描下方二维码&点击下方CSDN官方认证链接免费领取 【保证100%免费】

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)