week4

RAG = Retrieval‑Augmented Generation,检索增强生成

它是大模型外挂知识库的主流方案,解决原生大模型两大痛点:

  1. 知识截止时间问题:大模型训练完之后,不会自动知道最新资料
  2. 幻觉问题 (Hallucination):模型一本正经编造不存在的事实

简单一句话:先查资料,再让大模型基于查到的资料来回答,而不是只靠模型内部记忆生成答案。

Day1·⽂档加载+切分

Day开始前·环境准备

python 复制代码
cd ~/projects/llm-Ktongxue 
source venv/bin/activate ls 
# 检查 W3 的依赖是否还在 
python -c "import langchain; print('langchain', langchain.__version__)" 
# W4 Day 1 新增依赖(4 个 loader 必备) 
pip install --only-binary=:all: pypdf pymupdf markdownify unstructured

Step0:W3之后为什么需要RAG

W3你学了LangChain+LCEL――能⽤5⾏代码搭⼀条LLMchain。但你⻢上会发现⼀个新问题:0.1LLM存在知识盲区LLM的训练数据截⽌到某个时间------你的私域知识(你的论⽂/你公司内部⽂档/你实验室的实验记录)它没看过。W3之前的解决⽅案的将内容放在代码中喂给模型,但是•论⽂30⻚=⼏万字→超过LLM上下⽂窗⼝(DeepSeek64K/GPT-4128K)•整篇塞进去→每次都要传⼏万字,贵+慢+浪费token•论⽂越多越没法搞

RAG 的优缺点

优点

  1. 不用微调大模型,低成本接入私有数据
  2. 答案来源可追溯,可以给出引用文档出处
  3. 随时更新知识库,不需要重新训练模型
  4. 大幅降低幻觉,回答可控
  5. 支持企业内部文档、论文、知识库、手册问答

缺点

  1. 检索质量决定答案上限:检索错了,回答一定错
  2. 长文档切分策略、相似度匹配容易漏掉关键信息
  3. 有上下文窗口限制,检索太多片段会超出 LLM 最大 token
    RAG的核⼼思想
powershell 复制代码
1. 加载    Loader → Document 列表(每份⽂档 = 1 个 Document) 
2. 切分    Splitter → chunk 列表(每个 Document 切成 N 个 chunk,每块 ~500 字) 3. 嵌⼊    OpenAIEmbeddings → 1536 维向量 
4. 存库    Embedding + Vector DB → 向量库 
5. 检索    query → 找最相关的 3 个 chunk → 拼 prompt → LLM 回答

Step1:Document对象+第⼀个loader

什么是Document对象

Document=LangChain⾥所有"⽂档"的统⼀数据格式

powershell 复制代码
from langchain_core.documents import Document 
doc = Document( 
page_content="这是⽂档的⽂本内容...", 
metadata={"source": "paper.pdf", "page": 3} )

第⼀个loader:TextLoader

powershell 复制代码
from langchain_community.document_loaders import TextLoader 
loader = TextLoader("outputs/week04/papers/sample_text.txt", encoding="utf-8") docs = loader.load() 
print(f"读出 {len(docs)} 个 Document") 
print(f"第⼀个 Document 的 page_content ⻓度:{len(docs[0].page_content)}") print(f"metadata:{docs[0].metadata}") 

预期输出

powershell 复制代码
读出 1 个 Document 
第⼀个 Document 的 page_content ⻓度:4609 
metadata:{'source': 'outputs/week04/papers/sample_text.txt'}

预期输出

Step2:PyPDFLoader读PDF

powershell 复制代码
from langchain_community.document_loaders import PyPDFLoader 
# 假设你有 1 篇 PDF:outputs/week04/papers/sample_paper.pdf loader = PyPDFLoader("outputs/week04/papers/sample_paper.pdf") docs = loader.load() 
print(f"读出 {len(docs)} 个 Document(每⻚ 1 个)") print(f"第 1 ⻚前 200 字:\n{docs[0].page_content[:200]}") print(f"第 1 ⻚ metadata:\n{docs[0].metadata}") 
powershell 复制代码
读出 12 个 Document(每⻚ 1 个) 
第 1 ⻚前 200 字: 
Attention Is All You Need 
Ashish Vaswani et al. 
... 
第 1 ⻚ metadata: 
{'source': 'outputs/week04/papers/sample_paper.pdf', 'page': 0}

4个loader对⽐

loader_demo.py ⼀次性对⽐4种loader:

powershell 复制代码
from langchain_community.document_loaders import ( 
PyPDFLoader, UnstructuredMarkdownLoader, 
TextLoader 
) 
from langchain_community.document_loaders.generic import GenericLoader from langchain_community.document_loaders.parsers import LanguageParser 
# 1. PyPDFLoader:读 PDF(每⻚ 1 个 Document) 
print("=== PyPDFLoader ===") 
pdf_loader = PyPDFLoader("outputs/week04/papers/sample_paper.pdf") pdf_docs = pdf_loader.load() 
print(f"读出 {len(pdf_docs)} 个 Document(每⻚ 1 个)") print(f"metadata ⽰例:{pdf_docs[0].metadata}") 
# 2. UnstructuredMarkdownLoader:读 Markdown(按 header 切分) 
print("\n=== UnstructuredMarkdownLoader ===") 
md_loader = UnstructuredMarkdownLoader("outputs/week04/papers/sample_paper.md") md_docs = md_loader.load() 
print(f"读出 {len(md_docs)} 个 Document(按 # ## ### header 切分)") print(f"metadata ⽰例:{md_docs[0].metadata}") 
# 3. LanguageParser:读代码⽂件(按函数/类切分) 
print("\n=== LanguageParser(读 Python 代码)===") 
py_loader = GenericLoader.from_filesystem( "outputs/week04/papers/sample_code.py", parser=LanguageParser(language="python"), 
).load() 
print(f"读出 {len(py_loader)} 个 Document(每个函数/类 1 个)") print(f"metadata ⽰例:{py_loader[0].metadata}") 
# 4. TextLoader:读纯⽂本(1 ⽂件 1 Document) 
print("\n=== TextLoader ===") 
text_loader = TextLoader("outputs/week04/papers/sample_text.txt", encoding="utf-8") 
text_docs = text_loader.load() 
print(f"读出 {len(text_docs)} 个 Document(整个⽂件 1 个)") print(f"metadata ⽰例:{text_docs[0].metadata}") 

Step4:paper_loader.py

powershell 复制代码
from pathlib import Path 
from langchain_community.document_loaders import ( 
PyPDFLoader, UnstructuredMarkdownLoader, TextLoader 
) 
from langchain_community.document_loaders.generic import GenericLoader from langchain_community.document_loaders.parsers import LanguageParser from langchain_core.documents import Document 
def load_paper(file_path: str) -> list[Document]: """根据⽂件类型⾃动选 loader,返回 Document 列表""" 
path = Path(file_path) 
if path.suffix == ".pdf": 
loader = PyPDFLoader(str(path)) 
elif path.suffix == ".md": 
loader = UnstructuredMarkdownLoader(str(path)) 
elif path.suffix in (".py", ".js", ".ts", ".java", ".go"): loader = GenericLoader.from_filesystem( 
str(path), parser=LanguageParser(language=path.suffix[1:]), 
) 
else:  # .txt / .log / .csv 
loader = TextLoader(str(path), encoding="utf-8") 
return loader.load() 
# 演⽰ 
if __name__ == "__main__": 
import sys 
file_path = sys.argv[1] if len(sys.argv) > 1 else "outputs/week04/papers/sample_text.txt" 
docs = load_paper(file_path) 
print(f"读出 {len(docs)} 个 Document") 
print(f"第⼀个 Document 前 200 字:\n{docs[0].page_content[:200]}") print(f"metadata:{docs[0].metadata}") 

Step5:chunker.py(切分⼯具)

写chunker.py

powershell 复制代码
# chunker.py 作者:K同学啊 
""" 
Day 1 ⼯具:把 Document 列表切成 chunk 
""" 
from langchain_text_splitters import RecursiveCharacterTextSplitter from langchain_core.documents import Document 
def chunk_documents(docs: list[Document], chunk_size: int = 500, chunk_overlap: int = 50) -> list[Document]: 
"""把 Document 列表切成 chunk 
Args: 
docs: 来⾃ loader 的 Document 列表 
chunk_size: 每个 chunk 的⽬标字符数 
chunk_overlap: chunk 之间重叠的字符数(防⽌切到段落中间) 
Returns: 
切好的 chunk 列表(每个还是 Document 对象,metadata 跟着⾛) 
""" 
splitter = RecursiveCharacterTextSplitter( chunk_size=chunk_size, 
chunk_overlap=chunk_overlap, 
# 优先级:段落 > 句⼦ > 词 
separators=["\n\n", "\n", "。", "!", "?", ".", "!", "?", " ", ""], length_function=len, 
) 
return splitter.split_documents(docs) 
# 演⽰ 
if __name__ == "__main__": 
from paper_loader import load_paper 
docs = load_paper("outputs/week04/papers/sample_text.txt") print(f"原始 {len(docs)} 个 Document(每⻚ 1 个)") 
chunks = chunk_documents(docs, chunk_size=500, chunk_overlap=50) print(f"切分后 {len(chunks)} 个 chunk") 
# 看第⼀个 chunk 
print(f"\n=== 第 1 个 chunk ===") 
print(f"⻓度:{len(chunks[0].page_content)} 字") 
print(f"内容前 200 字:\n{chunks[0].page_content[:200]}") print(f"metadata:{chunks[0].metadata}") 
# 看 chunk ⻓度分布 
lengths = [len(c.page_content) for c in chunks] 
print(f"\nchunk ⻓度分布:min={min(lengths)}, max={max(lengths)}, avg=
{sum(lengths)//len(lengths)}") 

Day2·Embedding+cosine相似度

Step0:什么是Embedding

Embedding=把⽂本转成⼀串数字(向量)------"语义相近的⽂本,向量也相近"

Step1:第⼀个embedder

embedder.py

powershell 复制代码
# embedder.py 
""" 
Day 2 ⼯具:把 chunk 转成向量 
⽀持多后端:OpenAI 官⽅(1536 维)/ 硅基流动 BAAI/bge-m3(1024 维,⾃动切换) 
""" 
import os 
from langchain_openai import OpenAIEmbeddings 
from dotenv import load_dotenv 
load_dotenv() 
def get_embeddings() -> OpenAIEmbeddings: 
"""⾃动选 Embedding 后端:优先 OpenAI,否则硅基流动""" 
if os.getenv("OPENAI_API_KEY"): 
# ⽅案 A:OpenAI 官⽅(1536 维) 
return OpenAIEmbeddings( model="text-embedding-3-small", openai_api_key=os.getenv("OPENAI_API_KEY"), 
) 
elif os.getenv("SILICONFLOW_API_KEY"): 
# ⽅案 B:硅基流动 BAAI/bge-m3(1024 维,中英双语,免费) 
return OpenAIEmbeddings( 
model="BAAI/bge-m3", openai_api_key=os.getenv("SILICONFLOW_API_KEY"), openai_api_base="https://api.siliconflow.cn/v1", 
) 
else: 
raise ValueError( 
"❌ 没有可⽤的 Embedding API key!\n" 
"请在 .env 加 OPENAI_API_KEY 或 SILICONFLOW_API_KEY\n" "免费申请:https://cloud.siliconflow.cn/account/ak" 
) 
embeddings = get_embeddings() 
def embed_chunks(chunks: list) -> list[list[float]]: """批量把 chunk 转成向量""" 
texts = [c.page_content for c in chunks] 
return embeddings.embed_documents(texts) 
def embed_query(query: str) -> list[float]: """把单条查询转成向量""" 
return embeddings.embed_query(query) 
# 演⽰ 
if __name__ == "__main__": 
from chunker import chunk_documents from paper_loader import load_paper 
docs = load_paper("outputs/week04/papers/sample_text.txt") chunks = chunk_documents(docs, chunk_size=500) 
print(f"切了 {len(chunks)} 个 chunk") 
# 只 embed 前 3 个(节省 API 费⽤) sample_chunks = chunks[:3] 
vectors = embed_chunks(sample_chunks) 
print(f"\n第 1 个 chunk 前 100 字:\n{sample_chunks[0].page_content[:100]}") print(f"\n第 1 个向量维度:{len(vectors[0])}  ← 1024 维(硅基流动)或 1536 维(OpenAI)") 
print(f"第 1 个向量前 10 维:\n{vectors[0][:10]}") 
跑之前,确认 .env 有Embeddingkey
代码块
# W1 Day 1 你已经建过 .env(DEEPSEEK_API_KEY) 
# Day 2 起需要再加 Embedding key(⼆选⼀) 
echo "SILICONFLOW_API_KEY=sk-你的key" >> .env   # 推荐:免费 # 或 
echo "OPENAI_API_KEY=sk-你的key" >> .env        # 备选:海外

预期输出(⽤硅基流动BAAI/bge-m3)

powershell 复制代码
切了 12 个 chunk 
第 1 个 chunk 前 100 字: Attention Is All You Need 
Ashish Vaswani, Noam Shazeer, Niki Parmar, Jakob Uszkoreit, Llion Jones, Aidan N. Gomez, Łukasz Kaiser, Illia Polosukhin 
Google Brain, 2017 
Abstract 
The dominant sequence 
第 1 个向量维度:1024 
第 1 个向量前 10 维: 
[-0.00757195, 0.01940517, 0.04772623, -0.02714102, 0.02268307, 0.02438758, 0.02976334, 0.02530539, 0.00442517, -0.03435240]

Step2:CosineSimilarity计算两个chunk的相似度

什么是余弦相似度Cosinesimilarity=衡量两个向量的"⽅向接近度",范围-1到1。1=完全相同⽅向(语义最像)/0=正交(⽆关)/-1=完全相反。

powershell 复制代码
A · B 
cos(θ) = ───── |A| |B|

similarity_demo.py

powershell 复制代码
# similarity_demo.py 
""" 
Day 2 ⼯具:计算两个向量的余弦相似度 
""" 
import numpy as np 
from embedder import embed_chunks, embed_query from chunker import chunk_documents 
from paper_loader import load_paper 
def cosine_similarity(a: list[float], b: list[float]) -> float: 
"""计算两个向量的余弦相似度""" 
a = np.array(a) 
b = np.array(b) 
return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) 
# 演⽰ 
if __name__ == "__main__": 
docs = load_paper("outputs/week04/papers/sample_text.txt") chunks = chunk_documents(docs, chunk_size=500) 
print(f"切了 {len(chunks)} 个 chunk") 
# embed 前 10 个 chunk 
sample = chunks[:10] 
vectors = embed_chunks(sample) 

print(f"embed {len(vectors)} 个向量(每个 {len(vectors[0])} 维)\n") 
# 3 个查询,对每个 chunk 算相似度 
queries = [ 
"What is the Transformer architecture?", "What dataset did they use?", 
"How many parameters does the model have?", ] 
for q in queries: 
q_vec = embed_query(q) 
print(f"=== 查询:{q} ===") 
scores = [(cosine_similarity(q_vec, v), i) for i, v in enumerate(vectors)] 
scores.sort(reverse=True) 
# 打印 top 3 
for score, idx in scores[:3]: 
print(f"  相似度 {score:.4f} | chunk {idx} | 
{sample[idx].page_content[:80]}...") 
print() 

Step3:完整的"切分→Embedding→检索"流程

powershell 复制代码
# rag_minimal.py(Day 2 阶段项⽬雏形) 
""" 
RAG 最⼩版:切分 + Embedding + ⼿⼯ Top-K 检索 
(明天 Day 3 ⽤ Chroma 替代⼿⼯检索) 
""" 
import numpy as np 
from paper_loader import load_paper 
from chunker import chunk_documents 
from embedder import embed_chunks, embed_query 
def rag_search(query: str, top_k: int = 3) -> list[dict]: 
"""最简 RAG:query → top_k 个最相关的 chunk""" 
docs = load_paper("outputs/week04/papers/sample_text.txt") chunks = chunk_documents(docs, chunk_size=500) 
print(f"[RAG] 切分 {len(chunks)} 个 chunk") 
# 1. embed 所有 chunk(实际项⽬只 embed ⼀次存库) chunk_vectors = embed_chunks(chunks) 
print(f"[RAG] embed {len(chunk_vectors)} 个向量") 
# 2. embed query 
q_vec = embed_query(query) 
print(f"[RAG] embed query 向量") 

scores = [] 
for i, v in enumerate(chunk_vectors): 
sim = float(np.dot(q_vec, v) / (np.linalg.norm(q_vec) * np.linalg.norm(v))) 
scores.append((sim, i)) 
scores.sort(reverse=True) 
top = scores[:top_k] 
print(f"[RAG] 取 top {top_k} 相似度最⾼的 chunk\n") 
# 4. 返回结果 
return [ 
{ 
"score": s, 
"chunk": chunks[i], 
"source": chunks[i].metadata.get("source", "?"), "page": chunks[i].metadata.get("page", "?"), 
} 
for s, i in top 
] 
# 演⽰ 
if __name__ == "__main__": 
query = "What optimizer did the paper use?" results = rag_search(query, top_k=3) 
print(f"=== 查询:{query} ===\n") 
for r in results: 
print(f"[{r['score']:.4f}] {r['source']} 第 {r['page']} ⻚") 
print(f"  {r['chunk'].page_content[:200]}...") 
print() 

Day3·向量数据库Chroma

为什么不能直接用普通数据库(MySQL)存向量?

MySQL 可以存向量数组,但是它没有专门做相似度计算的索引

问题:

  1. 你存了 10 万条文档向量。来了一个问题向量。
  2. MySQL 只能暴力遍历全部 10 万个向量,逐条计算余弦距离。
  3. 文档一多,速度极慢,延迟爆炸,完全没法线上使用。

暴力搜索时间复杂度:O (N),N 越大越卡

向量数据库专门解决什么问题

向量数据库核心能力:近似最近邻搜索 ANN (Approximate Nearest Neighbor)

它会建立特殊索引(HNSW、IVF‑FLAT),不需要遍历全部数据。

  • 搜索速度大幅提升,毫秒级返回 Top‑k 最相似片段
  • 牺牲一点点精度换取极高检索速度,RAG 场景完全够用

向量数据库提供四大核心功能

  1. 向量存储:保存文档 chunk 对应的 embedding 向量 + 原始文本
  2. 快速语义检索:高维向量相似度查询(余弦相似度、欧氏距离)
  3. 增删改查:随时新增、删除知识库文档,不需要重新全量训练
  4. 元数据过滤:检索时筛选条件,比如:只查 2025 年后的 PDF 文档

chroma_init.py

powershell 复制代码
""" 
Day 3 ⼯具:把 1 篇 PDF 的 chunk 存进 Chroma 向量库(持久化) 
对应教程:W4 Day 3 Step 1 """ 
import os 
from langchain_community.vectorstores import Chroma 
from paper_loader import load_paper 
from chunker import chunk_documents 
# 复⽤ embedder 的多后端(OpenAI / 硅基流动 ⾃动切换) # get_embeddings() 内部已经处理 .env key 查找 
from embedder import get_embeddings 
CHROMA_DIR = "outputs/week04/chroma_db" PAPER_DIR = "outputs/week04/papers" 
def build_vectorstore(persist_dir: str = CHROMA_DIR) -> Chroma: """把 sample_text.txt 存进 Chroma(⾸次运⾏会建库)""" 
docs = load_paper(f"{PAPER_DIR}/sample_text.txt") 
print(f"[1/3] 加载 {len(docs)} 个 Document") 
chunks = chunk_documents(docs, chunk_size=500) print(f"[2/3] 切分 {len(chunks)} 个 chunk") 
embeddings = get_embeddings() 
vectorstore = Chroma.from_documents( 
documents=chunks, 
embedding=embeddings, 
persist_directory=persist_dir, 
collection_name="papers", 
) 
print(f"[3/3] 存进 Chroma: {persist_dir}") 
print(f"      集合名: papers, 共 {vectorstore._collection.count()} 条") 
return vectorstore 
def load_vectorstore(persist_dir: str = CHROMA_DIR) -> Chroma: """从磁盘加载 Chroma(已有库时⽤)""" 
embeddings = get_embeddings() 
return Chroma( 
persist_directory=persist_dir, embedding_function=embeddings, collection_name="papers", 
) 
if __name__ == "__main__": 
if os.path.exists(f"{CHROMA_DIR}/chroma.sqlite3"): 
print(f"Chroma 已存在,直接加载...") 
vs = load_vectorstore() 
else: 
print(f"Chroma 不存在,开始建库(⾸次需要 30 秒)...") 
vs = build_vectorstore() 
print(f"\n向量库统计:{vs._collection.count()} 条") 

Step2:基础检索

chroma_query.py

powershell 复制代码
# src/week04/chroma_query.py 
""" 
Day 3 ⼯具:从 Chroma 检索 top-K 最相关的 chunk 
""" 
from pathlib import Path 
import sys 
sys.path.insert(0, str(Path(__file__).parent)) 
from chroma_init import load_vectorstore 
def search(query: str, top_k: int = 3) -> list[dict]: """基础检索:返回 top-K 个最相关的 chunk""" 
vs = load_vectorstore() 
docs = vs.similarity_search(query, k=top_k) 
return [ 
{ 
"content": d.page_content, 
"source": d.metadata.get("source", "?"), "page": d.metadata.get("page", "?"), 
} 
for d in docs 
] 
def search_with_score(query: str, top_k: int = 3) -> list[dict]: """带相似度分数的检索""" 
vs = load_vectorstore() 
results = vs.similarity_search_with_score(query, k=top_k) 
return [ 
{ 
"content": d.page_content, 
"source": d.metadata.get("source", "?"), 
"page": d.metadata.get("page", "?"), 
"score": float(score),  # Chroma ⽤ L2 距离,越⼩越像 
} 
for d, score in results 
] 
# 演⽰ 
if __name__ == "__main__": 
query = "What optimizer did the paper use?" 
print(f"=== 基础检索(top_k=3)===") 
print(f"查询:{query}\n") 
results = search(query, top_k=3) 
for i, r in enumerate(results, 1): 
print(f"[{i}] {r['source']} 第 {r['page']} ⻚") 
print(f"    {r['content'][:150]}...") 
print() 
print(f"\n=== 带分数检索 ===") 
results = search_with_score(query, top_k=3) 
for i, r in enumerate(results, 1): 
print(f"[{i}] 距离 {r['score']:.4f}(越⼩越像)| 第 {r['page']} ⻚") 
print(f"    {r['content'][:150]}...") 
print()

预期输出

powershell 复制代码
=== 基础检索(top_k=3)=== 
查询:What optimizer did the paper use? 
[1] outputs/week04/papers/sample_text.txt 第 4 ⻚ 
...We trained the model using the Adam optimizer (Kingma & Ba, 2014)... 
[2] outputs/week04/papers/sample_text.txt 第 7 ⻚ ...we used Adam with β1 = 0.9, β2 = 0.98... 
[3] outputs/week04/papers/sample_text.txt 第 2 ⻚ 
...The base model architecture follows the encoder-decoder structure... 
=== 带分数检索 === 
[1] 距离 0.8234(越⼩越像)| 第 4 ⻚ 
...We trained the model using the Adam optimizer... 
[2] 距离 1.0123 | 第 7 ⻚ 
...we used Adam with β1 = 0.9, β2 = 0.98... 
[3] 距离 1.4567 | 第 2 ⻚ 
...The base model architecture follows... 

Day4·朴素RAGChain

prompt完整模板

powershell 复制代码
# rag_prompt.py 
""" 
Day 4:RAG 的 4 段 prompt 模板 
""" 
from langchain_core.prompts import ChatPromptTemplate 
RAG_PROMPT = ChatPromptTemplate.from_messages([ # === 第 1 段:System === 
("system", """你是⼀个严谨的论⽂问答助⼿。 
回答要求: 
1. 只根据 <context> 标签内的内容回答 2. 不知道就说"根据提供的内容⽆法回答" 3. 不要编造信息 
4. 回答简洁(不超过 100 字)"""), 
# === 第 2 段:Context(动态注⼊)=== 
("system", "<context>\n{context}\n</context>"), 
# === 第 3 段:Question(⽤⼾问题)=== ("human", "{question}"), 
# === 第 4 段:Format(可选,要求带引⽤)=== 
# ("system", "回答后请标注引⽤来源,格式:'(⻅第 X ⻚)'"), 
]) 

rag_chain.py

powershell 复制代码
""" 
Day 4:完整 RAG chain(基础版 + 流式版 + 带引⽤版) 
对应教程:W4 Day 4 Step 2 """ 
from pathlib import Path 
from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser from langchain_openai import ChatOpenAI 
from langchain_community.vectorstores import Chroma 
from dotenv import load_dotenv 
import os 
from rag_prompt import RAG_PROMPT 
# 复⽤ embedder 的多后端(OpenAI / 硅基流动 ⾃动切换) 
from embedder import get_embeddings 
load_dotenv() 
def format_docs(docs) -> str: 
"""把 Document 列表转成字符串(每段前⾯标⻚码)""" 
formatted = [] 
for i, doc in enumerate(docs, 1): 
page = doc.metadata.get("page", "?") 
source = Path(doc.metadata.get("source", "?")).name 
formatted.append(f"[{i}] (来源: {source}, 第 {page} ⻚)\n{doc.page_content}") 
return "\n\n---\n\n".join(formatted) 
def build_rag_chain(): 
"""拼出 RAG chain(基础版)""" 
embeddings = get_embeddings() 
vectorstore = Chroma( persist_directory="outputs/week04/chroma_db", embedding_function=embeddings, 
collection_name="papers", 
) 
retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) 
llm = ChatOpenAI( 
model="deepseek-chat", 
temperature=0, openai_api_key=os.getenv("DEEPSEEK_API_KEY"), openai_api_base="https://api.deepseek.com/v1", ) 
rag_chain = ( 
{ 
"context": retriever | format_docs, "question": RunnablePassthrough(), } 
| RAG_PROMPT 
| llm 
| StrOutputParser() 
) 
return rag_chain 
if __name__ == "__main__": print("=== 构建 RAG chain ===") 
chain = build_rag_chain() print("OK\n") 
question = "What optimizer did the paper use?" print(f"Q: {question}\n") 
answer = chain.invoke(question) 
print(f"A: {answer}") 

4种带引⽤的⽅式

rag_with_citation.py

powershell 复制代码
# rag_with_citation.py 
""" 
Day 4:带引⽤的 RAG(答案后追加"(⻅第 X ⻚)") 
""" 
from pathlib import Path 
import sys 
sys.path.insert(0, str(Path(__file__).parent)) 
from langchain_core.runnables import RunnablePassthrough from langchain_core.output_parsers import StrOutputParser from langchain_openai import ChatOpenAI, OpenAIEmbeddings from langchain_community.vectorstores import Chroma 
from dotenv import load_dotenv import os 
from rag_prompt import RAG_PROMPT from rag_chain import format_docs from embedder import get_embeddings 
load_dotenv() 
def build_rag_with_citation(): 
"""带引⽤的 RAG chain------返回 (answer, citations)""" 
embeddings = get_embeddings() 
vectorstore = Chroma( persist_directory="outputs/week04/chroma_db", embedding_function=embeddings, 
collection_name="papers", 
) 
retriever = vectorstore.as_retriever(search_kwargs={"k": 3}) 
llm = ChatOpenAI( 
model="deepseek-chat", 
temperature=0, openai_api_key=os.getenv("DEEPSEEK_API_KEY"), openai_api_base="https://api.deepseek.com/v1", ) 
# === Step 1: 先检索(拿 sources)=== 
def rag_with_sources(question: str) -> dict: sources = retriever.invoke(question) 
return { 
"context": format_docs(sources), "question": question, 
"sources": sources,  # ← 多塞⼀个 sources 
} 
# === Step 2: LLM 回答 === answer_chain = ( RAG_PROMPT 
| llm 
| StrOutputParser() 
) 
return rag_with_sources, answer_chain 
def answer_with_citation(question: str) -> str: """回答 + 引⽤合并""" 
rag_with_sources, answer_chain = build_rag_with_citation() 
# 1. 拿 sources + 准备 context data = rag_with_sources(question) 
# 2. LLM 回答 
answer = answer_chain.invoke({ "context": data["context"], "question": data["question"], }) 
# 3. 后处理:拼接引⽤ 
citations = [] 
for i, s in enumerate(data["sources"], 1): 
page = s.metadata.get("page", "?") 
source_name = Path(s.metadata.get("source", "?")).name 
citations.append(f"[{i}] {source_name} 第 {page} ⻚") 
citation_text = "\n".join(citations) 
return f"{answer}\n\n---\n来源:\n{citation_text}" 
# 演⽰ 
if __name__ == "__main__": 
question = "What is the Transformer architecture?" print(f"Q: {question}\n") 
result = answer_with_citation(question) print(result) 

把3个RAG模式拼成⼯具函数

写rag.py

powershell 复制代码
# rag.py 
""" 
Day 4 统⼀⼊⼝:3 种 RAG 模式(基础 / 流式 / 引⽤) 
""" 
from pathlib import Path 
import sys 
sys.path.insert(0, str(Path(__file__).parent)) 
from dotenv import load_dotenv import os 
load_dotenv() 
# === 模式 1:基础 RAG === 
def ask(question: str) -> str: 
"""基础问答(等 LLM 回答完⼀次性返回)""" 
from rag_chain import build_rag_chain chain = build_rag_chain() 
return chain.invoke(question) 
# === 模式 2:流式 RAG === 
def ask_stream(question: str): 
"""流式问答(逐字返回)""" 
from rag_chain import build_rag_chain chain = build_rag_chain() 
for chunk in chain.stream(question): yield chunk 
# === 模式 3:带引⽤ RAG === 
def ask_with_citation(question: str) -> str: 
"""带引⽤的问答(答案后追加"⻅第 X ⻚")""" 
from rag_with_citation import answer_with_citation return answer_with_citation(question) 
# 演⽰ 
if __name__ == "__main__": 
print("=" * 60) 
print("模式 1:基础 RAG") 
print("=" * 60) 
print(ask("What optimizer did the paper use?")) 
print("\n" + "=" * 60) 
print("模式 2:流式 RAG") 
print("=" * 60) 
for chunk in ask_stream("What is multi-head attention?"): print(chunk, end="", flush=True) 
print() 
print("\n" + "=" * 60) 
print("模式 3:带引⽤ RAG") 
print("=" * 60) 
print(ask_with_citation("What dataset did they use?")) 

预期输出

powershell 复制代码
============================================================ 
模式 1:基础 RAG 
============================================================ 
论⽂使⽤了 Adam 优化器(Adam optimizer),β1 = 0.9,β2 = 0.98。 
============================================================ 
模式 2:流式 RAG 
============================================================ 
Multi-head attention 是 Transformer 的核⼼机制,它将输⼊投影到多个⼦空间并⾏计算 
attention... 
============================================================ 模式 3:带引⽤ RAG 
============================================================ 
论⽂使⽤了 WMT 2014 English-German 数据集进⾏训练。 
(⻅ sample_text.txt 第 5 ⻚) 
(⻅ sample_text.txt 第 6 ⻚)
相关推荐
2601_9499506317 分钟前
在线刷题用什么小程序?认识一下能导资料、AI出题的练题簿
人工智能·学习·小程序·刷题·小程序推荐
TMT星球18 分钟前
美团2026年Q2财报:收入1046亿元,同比增长14.4%
大数据·人工智能
xfan_me19 分钟前
手机在网状态接口-空号查询-空号过滤API
数据库·人工智能·python·智能手机
laotiemen66627 分钟前
亲测好用的家居MES,实践经验分享!
大数据·人工智能·云计算·软件需求
镭封27 分钟前
2026免费AI配音5款实测:哪些真正无水印可导出音频
人工智能·音视频·媒体
V哥AI增长29 分钟前
AI搜索中用户评价的可引用性机制与结构化改造实证
人工智能
东方佑29 分钟前
超越参数记忆:构建“调度中枢“式语言模型
人工智能·语言模型·自然语言处理
青山科技分享31 分钟前
跨境电商AI Agent哪个比较好用?剖析自动化运营工具的落地价值
运维·人工智能·自动化·ai智能体
JienDa32 分钟前
我做了一款不依赖 AI 的离线传统术数排盘工具:Electron、Vue3 与 Java 17 的完整实践
java·人工智能·electron