专栏 AI 学习笔记 子专栏 AI 学习笔记 31 篇

Day 28|第 4 周复盘与 LLM 应用项目:RAG 端到端实战(AI 学习笔记 · 大模型与生成式AI周 · 第 28 篇)

本周七天(Day 22-27)从 GPT/BERT 的预训练范式差异讲起,到 LLM 的 Scaling Laws、Prompt Engineering、CoT 思维链、微调技术 LoRA / QLoRA、RAG 检索增强生成、多模态 CLIP / 扩散模型 / VLM,本周结束应该能把这一轮 LLM 的核心概念串成一条线。今天的复盘目标:把这一周的知识点压成一张可索引的表,再把它们用 LangChain + OpenAI + Chroma 拼出一个 100 行内的「企业文档问答助手」完整项目,把 load → split → embed → retrieve → generate 全流程跑通,并指出生产化前最常踩的 5 类工程坑。


1. Week 4 七天全景回顾

1.1 知识脉络

flowchart LR
  A[Day22 GPT vs BERT 预训练范式] --> B[Day23 Scaling Laws + LLM 推理]
  B --> C[Day24 Prompt Engineering + CoT]
  C --> D[Day25 LoRA / QLoRA 微调]
  D --> E[Day26 RAG + 向量数据库]
  E --> F[Day27 多模态 CLIP / DDPM / SD / VLM]
  F --> G[Day28 复盘 + 完整项目]

1.2 七天关键技能表

Day 主题 核心技能(1 行) 关键概念 / 论文
Day 22 GPT vs BERT 理解 decoder-only vs encoder-only 范式 GPT(2018)、BERT(2018)、T5
Day 23 Scaling Laws & LLM 推理 训练计算量 vs Loss 幂律;KV Cache 加速 Chinchilla(2022)、Scaling Law(Hoffmann)
Day 24 Prompt Engineering + CoT Zero/Few-shot + Chain-of-Thought CoT(Wei 2022)、Self-Consistency(2022)
Day 25 LoRA / QLoRA 微调 低秩适配 + 4-bit 量化 LoRA(Hu 2021)、QLoRA(Dettmers 2023)
Day 26 RAG + 向量数据库 Embedding + 检索 + 生成 Lewis 2020(2005.11401)、GraphRAG(2404.16130)
Day 27 多模态 CLIP / DDPM / SD / VLM CLIP(2103.00020)、DDPM(2006.11239)、LDM(2112.10752)
Day 28 第 4 周复盘 + 完整项目 把上述串成 100 行 RAG 项目 全栈串联

(注:Day 22-25 在本系列之前的文章中已固化,本周内容从 Day 26 起聚焦 RAG 与多模态;Day 28 是工程化收口。)

1.3 知识盲点清单(自查)

  • RAG 评估指标:RAGAS(arXiv:2309.15217)的四项指标没人能脱稿讲清。
  • Hybrid Retrieval 权重 α/β:真实工程取值 0.3-0.5 是经验值,没有公开 benchmark 给出最优。
  • LoRA rank 选择:r=8 还是 r=64 没有理论答案,主流默认 r=16 / 32。
  • CFG scale w:7-12 是经验区间,本质是「条件 vs 无条件流形的距离」。
  • Reranker 必要性:Embedding + Reranker 比纯 Embedding 好 5-15%,但具体任务差异大。
  • VLM 输入图像分辨率:LLaVA 早期 336×336,Qwen2-VL 用 dynamic resolution,InternVL2 用 1024×1024,这是一个隐含成本。
  • SDXL 是否需要 refiner:refiner 用 1024×1024 后再精修效果,但 gen + ref 总推理 2× 时长。
  • GraphRAG 调用次数:每篇文档多花 5-15× LLM 调用,真实成本不是小数。

2. 本周产出物 / 实战项目

2.1 必交(Week 4 收尾作业)

  • 企业文档问答助手(RAG 端到端):可拷就跑的 Python 项目,把 PDF / Markdown / HTML 文档加载、切片、Embedding、入库 Chroma,并对查询返回带引用的答案。
  • RAGAS 评估:对上述问答助手准备 30-100 个 QA 对,跑四项指标。
  • Simple WebUI:Gradio 或 Streamlit 起一个最小 UI,输入查询展示答案 + 引用。

2.2 进阶(加分项)

  • 加 Hybrid Retrieval(BM25 + Embedding)。
  • 加 BGE-reranker-v2-m3 重排。
  • 接入 Anthropic Claude / 本地 Ollama 做 LLM 后端。
  • 用 LangSmith / TruLens 做生产 trace 与评估。

2.3 评估目标(评估指标 + 阈值)

  • RAGAS Faithfulness ≥ 0.80:答案忠于 context 的比例,生产级基线。
  • RAGAS Answer Relevancy ≥ 0.85:答案与问题相关性。
  • RAGAS Context Recall ≥ 0.75:Top-K 覆盖金标答案的比例。
  • RAGAS Context Precision ≥ 0.80:Top-K 中相关文档的比例。
  • 端到端延迟:从 query 提交到答案展示 ≤ 4 秒(单文档 ≤ 1000 chunk)。
  • 引用准确率:人工 spot check ≥ 90%。

3. 完整 LLM 应用项目:RAG 端到端

3.1 项目目标

输入:一份企业知识库(PDF / Markdown / HTML / DOCX,本项目用两份示例 PDF)。 输出:Web UI 输入查询 → 返回「答案 + 引用 + 引用所在 chunk」。

技术栈:

组件 选型 理由
加载 LangChain PyPDFLoader / UnstructuredMarkdownLoader 主流、文档化全
切片 RecursiveCharacterTextSplitter 通用稳定
Embedding HuggingFace BAAI/bge-small-zh-v1.5 中文 + Apache-2.0 + 512 维
向量库 Chroma(本地持久化) 单文件,原型最合适
LLM OpenAI gpt-4o-mini 性价比,中英文都行
UI Gradio 最小可运行,30 行
评估 RAGAS 工业事实标准,2024

3.2 项目结构

day28_rag_project/
├── data/                     # 放你的 PDF / MD
│   └── handbook.pdf
├── ingest.py                 # 加载 + 切片 + Embedding + 入库
├── qa_chain.py               # 检索 + Prompt + 生成
├── app.py                    # Gradio UI
├── evaluate.py               # RAGAS 评估
├── requirements.txt
└── README.md

3.3 requirements.txt

langchain>=0.3
langchain-huggingface>=0.1
langchain-chroma>=0.1
langchain-openai>=0.2
chromadb>=0.5
sentence-transformers>=3.0
gradio>=4.40
pypdf>=4.0
ragas>=0.2
datasets>=2.20
openai>=1.40
huggingface-hub>=0.25

pip install -r requirements.txt。

3.4 ingest.py — 加载、切片、入库

"""Load documents, split chunks, embed, and persist in Chroma."""
import os
import glob
from langchain_community.document_loaders import PyPDFLoader, UnstructuredMarkdownLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma

DATA_DIR      = "./data"
PERSIST_DIR   = "./chroma_db"
COLLECTION    = "handbook_v1"
EMBED_MODEL   = "BAAI/bge-small-zh-v1.5"

def load_docs(data_dir: str):
    docs = []
    for fp in glob.glob(os.path.join(data_dir, "**/*.pdf"), recursive=True):
        docs.extend(PyPDFLoader(fp).load())
    for fp in glob.glob(os.path.join(data_dir, "**/*.md"), recursive=True):
        docs.extend(UnstructuredMarkdownLoader(fp).load())
    return docs

def split_docs(docs, chunk_size=500, chunk_overlap=80):
    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size, chunk_overlap=chunk_overlap,
        separators=["\n\n", "\n", "。", ".", " ", ""],
    )
    return splitter.split_documents(docs)

def build_vectorstore(docs):
    embeddings = HuggingFaceEmbeddings(
        model_name=EMBED_MODEL,
        model_kwargs={"device": "cuda"},   # 没 GPU 改 "cpu"
        encode_kwargs={"normalize_embeddings": True, "batch_size": 32},
    )
    vs = Chroma.from_documents(
        documents=docs, embedding=embeddings,
        persist_directory=PERSIST_DIR, collection_name=COLLECTION,
    )
    return vs

if __name__ == "__main__":
    raw = load_docs(DATA_DIR)
    chunks = split_docs(raw)
    print(f"loaded {len(raw)} docs -> {len(chunks)} chunks")
    vs = build_vectorstore(chunks)
    print(f"persisted to {PERSIST_DIR}, collection={COLLECTION}")

预期输出:

loaded 12 docs -> 487 chunks
persisted to ./chroma_db, collection=handbook_v1

3.5 qa_chain.py — 检索 + Prompt + 生成

"""Retrieve top-K chunks, build prompt, call OpenAI LLM, return answer."""
import os
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_core.runnables import RunnablePassthrough

PERSIST_DIR = "./chroma_db"
COLLECTION  = "handbook_v1"
EMBED_MODEL = "BAAI/bge-small-zh-v1.5"
TOP_K       = 6

PROMPT = ChatPromptTemplate.from_messages([
    ("system",
     "你是企业知识助手,只根据 <context> 中的内容回答用户问题,"
     "无法回答时请说'我不知道',不要编造。"
     "回答时引用编号 [1] / [2] / ... 标注信息来自哪一条。"),
    ("user",
     "<context>\n{context}\n</context>\n\n问题:{question}\n\n回答(中文,带引用编号):"),
])

def _format(docs):
    out = []
    for i, d in enumerate(docs, 1):
        src = d.metadata.get("source", "?")
        page = d.metadata.get("page", "")
        out.append(f"[{i}] (来源:{os.path.basename(src)}"
                   + (f" p{page+1}" if page != "" else "") + ")\n"
                   + d.page_content.strip())
    return "\n\n---\n\n".join(out)

def get_chain():
    embeddings = HuggingFaceEmbeddings(
        model_name=EMBED_MODEL,
        model_kwargs={"device": "cuda"},
        encode_kwargs={"normalize_embeddings": True},
    )
    vs = Chroma(persist_directory=PERSIST_DIR,
                collection_name=COLLECTION,
                embedding_function=embeddings)
    retriever = vs.as_retriever(search_type="similarity",
                                search_kwargs={"k": TOP_K})
    llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)

    chain = (
        {"context": retriever | _format, "question": RunnablePassthrough()}
        | PROMPT | llm | StrOutputParser()
    )
    return chain, retriever

def answer(question: str):
    chain, retriever = get_chain()
    response = chain.invoke(question)
    hits = retriever.invoke(question)
    return response, hits

if __name__ == "__main__":
    q = "年假是几天?"
    ans, hits = answer(q)
    print("Q:", q)
    print("A:", ans)
    print(f"({len(hits)} chunks retrieved)")

预期输出:

Q: 年假是几天?
A: 根据公司手册,工作满 1 年的员工享受 5 天年假,满 5 年享受 10 天,以此类推。
    [1] / [3] 中提到此规定。
(6 chunks retrieved)

3.6 app.py — Gradio 最小 UI

import gradio as gr
from qa_chain import answer

def ask(q, history):
    if not q.strip():
        return "请输入问题。"
    response, hits = answer(q)
    cite = "\n\n---\n引用:\n" + "\n".join(
        f"[{i+1}] {h.metadata.get('source','?')}: {h.page_content[:120]}..."
        for i, h in enumerate(hits)
    )
    return response + cite

demo = gr.ChatInterface(
    fn=ask, type="messages",
    title="企业文档问答助手 (Day 28 RAG demo)",
    description="基于 LangChain + BGE + Chroma + GPT-4o-mini 的端到端 RAG。",
)
demo.launch(server_name="0.0.0.0", server_port=7860)

python app.py 起 UI,浏览器开 http://localhost:7860 问答。

3.7 bis 增量更新:加新文档而不重建

# add_more.py —— 已有持久化时只追加
from langchain_huggingface import HuggingFaceEmbeddings
from langchain_chroma import Chroma
from langchain_community.document_loaders import PyPDFLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter

emb = HuggingFaceEmbeddings(model_name="BAAI/bge-small-zh-v1.5",
                            model_kwargs={"device": "cuda"},
                            encode_kwargs={"normalize_embeddings": True})
vs = Chroma(persist_directory="./chroma_db",
            collection_name="handbook_v1",
            embedding_function=emb)

# 加载 + 切片 + 追加
new = PyPDFLoader("./data/handbook_v2.pdf").load()
chunks = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=80).split_documents(new)
vs.add_documents(chunks, embedding=emb)
print(f"added {len(chunks)} chunks")

生产常识:chroma collection 体积超过 100 万 chunk 时 rebuild 极慢,务必从 Day 1 就按 collection 拆分(比如一个 collection = 一个客户 / 一个项目)。

3.7 evaluate.py — RAGAS 评估

"""Evaluate the RAG pipeline with RAGAS four metrics."""
import json
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import (faithfulness, answer_relevancy,
                           context_precision, context_recall)
from qa_chain import answer

# 1) 准备 QA 对(完整生产建议 ≥ 30 个)
qa_pairs = [
    {"question": "年假是几天?",                       "ground_truth": "满 1 年 5 天,满 5 年 10 天"},
    {"question": "试用期多长?",                       "ground_truth": "试用期最长不超过 6 个月"},
    {"question": "病假如何申请?",                     "ground_truth": "提前 1 天书面或邮件申请"},
]

records = []
for qa in qa_pairs:
    ans, hits = answer(qa["question"])
    records.append({
        "question":    qa["question"],
        "answer":      ans,
        "contexts":    [h.page_content for h in hits],
        "ground_truth": qa["ground_truth"],
    })

ds = Dataset.from_list(records)
result = evaluate(ds, metrics=[faithfulness, answer_relevancy,
                               context_precision, context_recall])
print(result.to_pandas().to_markdown())
result.to_pandas().to_csv("ragas_result.csv", index=False)

预期输出(表格):

question faithfulness answer_relevancy context_precision context_recall
年假是几天? 0.93 0.91 0.86 0.80
试用期多长? 0.88 0.84 0.79 0.74
病假如何申请? 0.82 0.78 0.71 0.68

Faithfulness < 0.7 的样本要进失败案例分析,逐条定位是检索还是 Prompt 问题。


4. Week 4 vs Week 5 难度对比

4.1 工程复杂度

维度 Week 4(LLM 应用) Week 5(AI Agent)
数据流 「文档 → 向量库」静态 实时调 API / DB / 代码解释器
失败模式 检索召回差 → 答非所问 无限循环 / 工具错 / token 耗尽
评估难度 RAGAS 四项可比 AgentBench + 端到端成功率,评测成本 10×
工程化成本 1-2 周可上线 4-8 周才能稳态
典型框架 LangChain / LlamaIndex AutoGen / LangGraph / CrewAI

4.2 主要差异点

  • 可观测性:RAG 只看 retrieve + generate 两点;Agent 还要看每步工具调用、token 消耗、循环深度。
  • 容错能力:RAG 答错可以人工补充「我不知道」分支;Agent 答错可能执行了不可逆操作(删库、发邮件、转账),必须人审 / 高风险 API 拦截。
  • 评估基准:RAG 有 RAGAS / TruLens;Agent 主要靠 AgentBench(清华 2023-08 arXiv:2310.06770)、SWE-bench(arXiv:2310.06770)、τ-bench 等。

5. 7 条常见坑

5.1 Embedding 模型与向量库维度不匹配

症状:检索时 ValueError: dimension mismatch,以为 chroma 拒服务。 原因:入库用 bge-small-zh 512 维,查询时换了 text-embedding-3-large 3072 维。 修法:显式在 README 写明 embedding 规格,改名 collection(handbook_v1_bge_512)。

5.2 metadata 字段名错

症状:filter={"src": "handbook.pdf"} 返回 0 条。 原因:LangChain 写入时 key 是 source,查询时写成了 src。 修法:用统一的 metadata_extractor.py 包一个 LLM 抽取文件名 / 章节,key 集中到 config。

5.3 没设 OpenAI API Key 环境变量

症状:LangChain 第一次调 ChatOpenAI 报 openai.AuthenticationError。 原因:OPENAI_API_KEY 环境变量没设,或者 .env 没加载。 修法:from dotenv import load_dotenv; load_dotenv() 后用 os.getenv('OPENAI_API_KEY') 显式传 api_key=...。

5.4 切片粒度太小导致上下文断裂

症状:RAGAS Faithfulness 低,答案 disjoint。 原因:chunk_size=100, chunk_overlap=0,句子被从中切断,语义断片。 修法:中文用 300-500,overlap 50-80;Markdown 优先按 H1/H2 切。

5.5 top-K 太大撑爆 context

症状:OpenAI API 拒答或 token 超限报错。 原因:K=20 + chunk 500 = 10K tokens,中文 token 化后更大。 修法:K=5-10 + Reranker 压到 5。

5.6 LLM 没用 streaming

症状:用户体感「白屏」5-10 秒。 原因:LangChain 默认 non-streaming,等全部生成完才返回。 修法:Gradio 用 stream=True,LangChain chain.stream(question) 用 RunnablePassthrough.assign(text=...)。

5.7 没做成本控制

症状:一个查询花掉 $0.10,日活 1 万时成本爆炸。 原因:TopK=20 + gpt-4o,中文长答案。 修法:把 LLM 默认设为 gpt-4o-mini / claude-haiku,关键任务再用 gpt-4o;加 prompt cache(Anthropic 2024-08 prompt caching 90% 折扣)。


6. 自检三问

A. RAG 与微调(LoRA / QLoRA)各自适合什么场景?为什么很多生产系统两者都用?

要点:RAG 适合「外部动态知识 / 私有文档 / 实时事实」的检索型问答;LoRA/QLoRA 适合「让模型学会特定输出格式 / 特定风格 / 特定领域术语」。两者并不冲突:LoRA 把基础模型对齐到业务语言,RAG 解决事实性问题。生产上常见做法是先 SFT / DPO 让模型「听话」,再用 RAG 提升准确率。Day 25 已展开 LoRA 微调,Day 26 展开 RAG,Day 28 的端到端项目把它们与 LLM 调用三段串起来。

B. Week 4 的 RAG 项目如何升级为生产系统?最少要做哪三件事?

要点:1) 可观测性——接 LangSmith / TruLens,记录 query → retrieved → answer 全链路,失败率、延迟、token 消耗看板化;2) 评估 CI—— 每次改 prompt / 切片策略 / embedding 都跑 RAGAS,把退化拦在合并前;3) 权限与审计——对私有向量库做 RBAC,谁加了什么文档、谁查了什么,有审计日志。三件事之外再考虑 A/B、缓存、自动重排。

C. 第 4 周的核心概念如何在 Day 29(AI Agent)中复用?

要点:RAG 是 Agent 的「长期记忆」(Read-Only),Tool Use 是 Agent 的「工具手」,Prompt Engineering 是 Agent 的「思维链骨架」,LLM API 是 Agent 的「大脑」。ReAct 把 Thought → Action → Observation 三段拼成循环,而 RAG 的 retrieve → generate 正好是 Action 中的工具调用。Week 4 是 Week 5 的具体组件库。


7. 推荐资源

视频

  • LangChain「Build a RAG Application」(2024-04,YouTube)—— 官方 90 分钟实战课。
  • Databricks「Building Production-Ready RAG」(2024-09)—— Lakehouse + RAG 模式。
  • DeepLearning.AI《LangChain for LLM Application Development》(2023-2024)。

教科书 / 文档

  • LangChain RAG Tutorial:https://python.langchain.com/docs/tutorials/rag/ —— Day 28 项目代码基于此。
  • Chroma Docs:https://docs.trychroma.com —— 持久化与 query DSL。
  • OpenAI Cookbook / rag.ipynb:https://cookbook.openai.com —— 官方 RAG 模板。
  • RAGAS Docs:https://docs.ragas.io —— 评估四指标定义。

实战项目

  • LangChain RAG Template:https://github.com/langchain-ai/rag-from-scratch(16 段 Notebook)。
  • chromadb/chroma:https://github.com/chroma-core/chroma。
  • explodinggradients/ragas:https://github.com/explodinggradients/ragas。
  • gradio-app/gradio:https://github.com/gradio-app/gradio。
  • microsoft/graphrag:https://github.com/microsoft/graphrag —— Week 4 提到的进阶方案。

论文

  • Lewis et al.”Retrieval-Augmented Generation for Knowledge-Intensive NLP Tasks” arXiv:2005.11401。
  • Edge et al.”From Local to Global: A Graph RAG Approach…” arXiv:2404.16130。
  • Es et al.”RAGAS: Automated Evaluation of Retrieval Augmented Generation” arXiv:2309.15217。
  • Wang et al.”Searching for Best Practices in Retrieval-Augmented Generation” arXiv:2407.16833。

8. 本节要点

  • 要 1:Week 4 七天串成 GPT/BERT 范式 → Scaling → Prompt → LoRA → RAG → 多模态一条线;Day 28 把它们打包成可跑项目。
  • 要 2:完整 RAG 项目六步 — 加载(PDFLoader)、切片(RecursiveSplitter)、Embedding(BGE-small-zh)、入库(Chroma)、检索(top-K=6)、生成(GPT-4o-mini 带引用)。
  • 要 3:RAGAS 四项指标(arXiv:2309.15217)是 2024 工业事实标准:faithfulness / answer_relevancy / context_precision / context_recall。
  • 要 4:Week 4 RAG 项目升级生产要三件事 —— LangSmith 可观测性 + 评估 CI + 权限审计。
  • 要 5:Hybrid(BM25 + Embedding)+ Reranker(BGE-reranker-v2-m3)+ 元数据过滤是工业级 RAG 的标配三件套。
  • 要 6:Gradio / Streamlit 是最小可用 UI;LangChain chain.stream() 解决白屏体感;OpenAI prompt caching 把成本压到 1/10。
  • 要 7:Week 5 的 AI Agent 把 Week 4 的 RAG 当作「长期记忆」组件,ReAct 把 Thought / Action / Observation 串成循环,Day 29 起展开。

9. 下一节:Day 29 · AI Agent 基础 — ReAct、Tool Use、Planning

主题:从「被动答题」到「主动解题」,LLM 加上 ReAct / Tool Use / Planning 后的能力跃迁。 覆盖:

  • AI Agent 的四大模块:感知(Perception)、规划(Planning)、行动(Action)、反思(Reflection)。
  • ReAct 论文(arXiv:2210.03629)与「Thought → Action → Observation」三段循环。
  • OpenAI Function Calling(2023-06)/ Claude Tool Use(2024-03)的 API 形态。
  • Toolformer(arXiv:2302.11461)与自监督调用工具。

产出物:80 行 OpenAI Function Calling + LangChain Agent 实战,从「告诉 LLM 工具列表」到「Agent 自主决定调用 + 解析结果」全流程。

说明 · 本站内容均为学习笔记与经验总结,所有菜谱与技法请结合实际食材、季节与个人口味灵活调整。涉及生食、营养与健康的内容仅供参考,特殊体质或疾病请咨询专业营养师/医生。