RAG与Agent_体系

RAG全链路(数据清洗、切分、Embedding、向量库、召回、重排、答案生成)的设计逻辑

离线阶段:文档 → 清洗 → 切分 → Embedding → 向量库

1)文档加载

python 复制代码
import os
import fitz  # PyMuPDF
from loguru import logger
from tqdm import tqdm
log_dir = "log"
if not os.path.exists(log_dir):
    os.makedirs(log_dir)
logger.add(
    f"{log_dir}/pdf_load.log",
    rotation="200 MB",
    retention=1,
    level="INFO",
    format="{time:YYYY-MM-DD HH:mm:ss} | {level} | {message}",
    encoding="utf-8")

2)数据清洗

python 复制代码
import re

#页级清洗:过滤版权/图片授权页
NOISE_KEYWORDS = [
    "unsplash license",
    "creative commons",
    "attribution",
    "photo by",
    "image by",
    "reused",
    "license",
    "copyright",
    "all rights reserved",
]

def is_noise_page(text):
    lower_text = text.lower()
    hit_count = sum(keyword in lower_text for keyword in NOISE_KEYWORDS)

    # 如果一页里多个版权/授权关键词同时出现,大概率是噪声页
    if hit_count >= 3:
        return True

    # URL 很多的页通常是引用/授权页
    url_count = len(re.findall(r"https?://|www\.", lower_text))
    if url_count >= 5:
        return True

    return False


#过滤空格等特殊字符,合并多个换行符
def clean_text(text):
    text = text.replace("\xa0", " ")
    text = re.sub(r"[ \t]+", " ", text)
    text = re.sub(r"(?<!\n)\n(?!\n)", " ", text)
    text = re.sub(r"\n{3,}", "\n\n", text)
    return text.strip()

def load_pdf(pdf_path):
    try:
        pages = []
        with fitz.open(pdf_path) as doc:
            logger.info(f"开始加载PDF: {pdf_path}, 总页数: {doc.page_count}")

            for page_index, page in tqdm(enumerate(doc), total=doc.page_count):
                text = clean_text(page.get_text())
                

                if not text:
                    logger.info(f"第{page_index}页为空,跳过")
                    continue
                if is_noise_page(text):
                    logger.info(f"第{page_index}页为噪声页,跳过")
                    continue

                pages.append({
                    "page": page_index,
                    "text": text
                })

        logger.info(f"PDF加载完成,有效页数: {len(pages)}")
        return pages

    except Exception as error:
        logger.exception(f"加载PDF失败: {pdf_path}, error: {error}")
        raise
pages = load_pdf("rag/simple-local-rag/human-nutrition-text.pdf")

3)文本切分

python 复制代码
方法一:
from sentence_transformers import SentenceTransformer

def split_text(text, chunk_size=300):
    # 简单按字符数切分,可用更智能的分句/分段
    return [text[i:i+chunk_size] for i in range(0, len(text), chunk_size)]

chunks = []
for page in pages:
    for chunk in split_text(page["text"]):
        if len(chunk.strip()) > 50:  # 过滤过短片段
            chunks.append({"page": page["page"], "text": chunk})
chunks

方法二:
from langchain_text_splitters import CharacterTextSplitter
text_splitter = CharacterTextSplitter(
    chunk_size=300,#切分长度
    chunk_overlap=5 #相邻两个chunks之间的重叠token数量
)
all_text = "\n".join([p["text"] for p in pages])
chunks = text_splitter.split_text(all_text)
chunks

4)向量化与存储

python 复制代码
import numpy as np
from sentence_transformers import SentenceTransformer
#文档向量化
model = SentenceTransformer("all-mpnet-base-v2")
doc_vectors = model.encode([c["text"] for c in chunks], normalize_embeddings=True)
doc_vectors = np.array(doc_vectors)


# 构建 FAISS 索引
import faiss
dim = doc_vectors.shape[1]
index = faiss.IndexFlatIP(dim)   # 精确搜索,内积
index.add(doc_vectors)

#保存索引
faiss.write_index(index, "/opt/cyc/rag/simple-local-rag/docs.index")

在线阶段:问题 → Embedding → 召回 → 重排 → 拼上下文 → LLM 生成答案

1)问题 → Embedding → 召回

python 复制代码
# 查询
query = "怎么保持饮食健康?"
query_vector = model.encode([query], normalize_embeddings=True)
query_vector = np.asarray(query_vector, dtype="float32")
texts = [d["text"] for d in pages]
# 搜索 top-k
k = 3
scores, ids = index.search(query_vector, k)

print(f"Query: {query}\n")
for score, idx in zip(scores[0], ids[0]):
    print(f"score={score:.4f}, doc={texts[idx]}")

2)重排 → 拼上下文 → LLM 生成答案

python 复制代码
from anthropic import Anthropic
import numpy as np

client = Anthropic()
def bge_reranker_matched(text1, text2, threshold=0.01, ratio_threshold=0.01):
    """接口仅支持 1 对多:返回得分>=threshold 的候选占比>=ratio_threshold 的 text1 元素。"""
    bge_url=bge_url
    bge_token=bge_token
    headers = {
        "Content-Type": "application/json",
        "Authorization": bge_token
    }
    matched = []
    for d in text2:
        data = {
            "model": "bge-reranker-v2-m3",
            "text_1": text1,
            "text_2": d
        }
        response = requests.post(bge_url, json=data, headers=headers)
        res = response.json().get("data") or []
        if not res:
            continue
        hit_count = sum(item.get("score", 0) >= threshold for item in res)
        if hit_count / len(res) >= ratio_threshold:
            matched.append(d)
    return matched

def answer_with_rag(query, model, index, pages, k=3):
    # 1. query -> vector
    records = chunks
    texts = [d["text"] for d in records]

    doc_vectors = model.encode(
        texts,
        normalize_embeddings=True,
        show_progress_bar=True
    )
    doc_vectors = np.asarray(doc_vectors, dtype="float32")

    query_vector = model.encode([query], normalize_embeddings=True)
    query_vector = np.asarray(query_vector, dtype="float32")


    # 2. retrieve
    k = 3
    scores, ids = index.search(query_vector, k)

    retrieved = []
    for score, idx in zip(scores[0], ids[0]):
        retrieved.append({
            "score": float(score),
            "page": pages[idx]["page"],
            "text": pages[idx]["text"]
        })
    #3.精排
    context = bge_reranker_matched(query, retrieved)
    # 4. generate
    prompt = f"""
    你是一个基于资料回答问题的助手。
    请只根据"参考资料"回答,不要编造。
    如果参考资料不足以回答,就明确说"参考资料不足"。

    用户问题:
    {query}

    参考资料:
    {context}

    请输出:
    1. 简洁答案
    """

    resp = client.messages.create(
        model="claude-sonnet-5",
        max_tokens=500,
        messages=[{"role": "user", "content": prompt}]
    )

    return {
        "answer": resp.content[0].text,
        "retrieved": retrieved
    }
answer_with_rag("蛋白质的主要功能是什么?", model, index, pages)

了解SFT、LoRA、RLHF等技术的适用场景,对比RAG与微调的成本、效果、维护难度差异。

SFT(Supervised Fine-Tuning) :一种训练方式(有监督微调)

  • 用"输入 → 标准输出"的标注数据去教模型

LoRA :一种参数微调方法

  • 不直接全量改模型参数,只训练少量"适配层/低秩矩阵"

微调(Fine-Tuning)

├── SFT

├── DPO

├── PPO(RLHF 里常用的一种优化算法,注:RLHF 是一整套对齐流程)

└── ORPO

SFT

问题:什么是ROI?

标准答案:ROI是投资回报率,用于衡量...
DPO

问题:什么商品适合圣诞节促销?

回答A:应该选择具有明显圣诞节消费场景的商品,并结合历史销量...

回答B:所有商品都应该在圣诞节促销。

人工标注:A > B
RLHF

  1. SFT 先做监督微调
  2. 训练奖励模型(Reward Model)
  3. 用强化学习优化策略 ,常见就是 PPO
    PPO(强化学习算法)
  • 根据奖励模型的分数
  • 继续优化模型输出

参数更新方式

├── Full Fine-Tuning

├── LoRA

├── QLoRA

├── AdaLoRA

└── DoRA

适用场景
技术 主要作用 适合场景 不适合场景
SFT(监督微调) 让模型学会特定任务格式、风格、流程 客服话术、结构化输出、领域问答、工具调用格式、固定写作风格 频繁变化的知识库、强实时性事实
LoRA 低成本做微调 预算有限、数据量中小、要快速试验、要多个版本并存 需要极致性能且愿意全量训练时
RLHF 对齐人类偏好,改善"好不好用" 语气、礼貌、安全性、减少胡说、提升回答偏好一致性 单纯补知识、快速迭代事实内容
DPO/ORPO(常见替代) 更简单的偏好优化 想要类似 RLHF 的效果,但训练更简化 复杂奖励建模场景
对比差异
维度 RAG 微调(SFT/LoRA)
成本 通常较低到中等 中等到高
数据准备 文档清洗、切分、向量化 需要高质量指令数据/对话数据
训练成本 无训练或很少训练 要训练,LoRA 比全量便宜很多
推理成本 通常更高一些(检索 + 生成) 通常更低、更稳定
效果 最新知识和可追溯性以及幻觉减少更强、复杂行为学习一般 幻觉取决于训练质量、复杂行为学习更好
维护难度 改知识库、文档/索引版本管理、容易定位是检索还是生成问题、加文档即可扩展 需要重新训练、新能力需要加数据重训
相关推荐
飞凌嵌入式1 小时前
工业控制+AI视觉齐发,飞凌嵌入式将亮相国际物联网展·深圳站
人工智能·物联网
AI工具测评家1 小时前
硕博论文怎么降低AI检测率?从深层语义改写到专业术语保留全面解析
人工智能·降重·ai检测·查重·降ai
Harm灬小海1 小时前
2026 年主流 AI+DevOps 平台调研分析报告
运维·人工智能·devops
Csvn1 小时前
🐍 Day 5: Python 函数详解 — 参数、作用域与一等公民
人工智能·后端
陆枫Larry1 小时前
英伟达 H100 是啥,到底好在哪?
人工智能
老郑聊AI业财智造1 小时前
Spring AI 技术架构与源码分析
java·人工智能·后端·spring·架构·软件工程
一点一木1 小时前
豆包工作发布:飞书,才是它真正的底牌
人工智能·ai编程·产品
Csvn1 小时前
第 1 章 AI Agent 是什么
人工智能·aigc
阿里云大数据AI技术1 小时前
知衣科技 × 阿里云:以MaxCompute 向量检索打通商品与海外社媒内容,让跨境选品看见真实热度
人工智能·agent