欢迎光临
我们一直在努力

5.5 RAG优化与评估

邓立国Agent开发入门必读书《AI Agent智能体开发实践》全文试读-示例案例丰富且能正确运行_《ai agent 智能体开发实践》在线阅读-CSDN博客

AI Agent智能体开发实践【行情 报价 价格 评测】-京东

RAG(Retrieval-Augmented Generation,检索增强生成)是结合信息检索与生成式AI的技术框架,核心是通过检索外部知识(如文档、数据库)辅助大语言模型生成更准确、可靠的回答。其效果高度依赖“检索质量”和“生成质量”的协同,因此优化与评估是提升RAG系统性能的核心环节。

5.5.1  评估指标

在AI智能体开发中,评估指标是衡量系统性能的核心工具。下面从检索质量、生成质量和人工评估三个维度,结合技术原理与行业实践展开详解。

1. 检索质量(Recall@k、MRR和NDCG)

检索类任务(如RAG问答、搜索引擎)需从海量数据中筛选高价值信息,核心指标围绕相关性排序质量设计,主要包括Recall@k、MRR和NDCG。

1)Recall@k

定义:前k个结果中包含的相关文档数占总相关文档数的比例,用于衡量核心覆盖能力。

公式:Recall@k={前k个结果中的相关文档数}/{总相关文档数}。

案例:若用户需求包含5个相关文档,系统在前10个结果中返回3个,则Recall@10=3/5=0.6。

行业标准:在RAG系统中,通常要求Recall@5≥0.853。

局限性:仅关注覆盖范围,不考虑排序顺序。

2)MRR:首个相关结果的优先级

定义:首个相关结果的排名倒数的平均值。

案例:若三个查询的首个相关结果分别位于第2、3、1位,则MRR=(1/2+1/3+1/1)/3≈0.555。

适用场景:强调首结果质量的场景(如电商推荐),行业标准通常要求MRR≥0.73。

3)NDCG:排序质量的全局优化

定义:通过折扣累计增益(DCG)与理想排序增益(IDCG)的比值,量化排序质量。

技术细节:

  • 相关度分级:支持多级评分(如0~3分)。例如,真实相关度标注为[3,2,1,0,0]时,DCG@3=3+ 2/2+1/3≈4.33335。
  • 标准化:IDCG为完美排序下的DCG值,确保不同查询结果可比。
  • 行业实践:在RAG系统中,NDCG@5≥0.8被视为合格,美团搜索通过NDCG优化排序,单击率提升10%。
  • 【示例5.23】检索质量评估指标的实现。

    import numpy as np

    def recall_at_k(relevant, retrieved, k):

        """

        计算Recall@k指标

        :param relevant: 相关文档集合

        :param retrieved: 检索到的文档集合

        :param k: k个结果

        :return: Recall@k

        """

        retrieved_k = retrieved[:k]

        relevant_set = set(relevant)

        retrieved_set = set(retrieved_k)

        intersection = relevant_set & retrieved_set

        return len(intersection) / len(relevant_set)

    def mean_reciprocal_rank(relevant, retrieved):

        """

        计算平均倒数排名(MRR)

        :param relevant: 相关文档集合

        :param retrieved: 检索到的文档集合

        :return: MRR

        """

        reciprocal_ranks = []

        relevant_set = set(relevant)

        for doc in retrieved:

            if doc in relevant_set:

                rank = retrieved.index(doc) + 1

                reciprocal_ranks.append(1.0 / rank)

                break

        return np.mean(reciprocal_ranks) if reciprocal_ranks else 0.0

    def ndcg(relevant, retrieved, k):

        """

        计算NDCG@k指标

        :param relevant: 相关文档及其相关性分数列表[(doc_id, score)]

        :param retrieved: 检索到的文档集合

        :param k: k个结果

        :return: NDCG@k

        """

        # 构建相关性分数字典

        relevance_scores = {doc_id: score for doc_id, score in relevant}

        # 计算DCG

        dcg = 0.0

        for i, doc_id in enumerate(retrieved[:k], 1):

            rel_score = relevance_scores.get(doc_id, 0)

            dcg += (2 ** rel_score – 1) / np.log2(i + 1)

        # 计算IDCG

        ideal_sorted = sorted(relevant, key=lambda x: x[1], reverse=True)

        idcg = 0.0

        for i, (doc_id, score) in enumerate(ideal_sorted[:k], 1):

            idcg += (2 ** score – 1) / np.log2(i + 1)

        return dcg / idcg if idcg > 0 else 0.0

    # 测试用例

    if __name__ == "__main__":

        relevant_docs = ['doc1', 'doc3', 'doc5']

        retrieved_docs = ['doc3', 'doc5', 'doc1', 'doc7', 'doc9']

        relevant_with_scores = [('doc1', 3), ('doc3', 2), ('doc5', 1)]

        print(f"Recall@3: {recall_at_k(relevant_docs, retrieved_docs, 3)}")

        print(f"MRR: {mean_reciprocal_rank(relevant_docs, retrieved_docs)}")

        print(f"NDCG@3: {ndcg(relevant_with_scores, retrieved_docs, 3)}")

    运行代码,输出如下:

    Recall@3: 1.0

    MRR: 1.0

    NDCG@3: 0.7591919243197319

    2. 生成质量(BLEU、ROUGE、BERTScore):语义与形式的双重校验

    生成类任务(如机器翻译、对话生成)需兼顾内容准确性与表达流畅性,核心指标从表层匹配到深层语义逐步深化。

    1)BLEU:基于n-gram的形式匹配

    定义:通过计算生成文本与参考文本的n-gram重叠率来评估质量。

    技术细节:

  • 修正精度:每个n-gram的匹配次数不超过参考文本中的出现次数,避免重复词虚高。
  • 长度惩罚:短句得分会被BP(Brevity Penalty)因子降低,例如生成句长度不足参考句50%时,BP≈0.607。
  • 局限性:对低频词和语义变化不敏感,例如“手机性能出色”与“这款设备表现优异”的BLEU得分可能较低。
  • 2)ROUGE-L:基于最长公共子序列(LCS)的召回率

    定义:通过计算生成文本与参考文本的n-gram或最长公共子序列(ROUGE−Lrecall)的召回率评估质量,计算最长公共子序列LCS长度(记为LCS_len)。

    应用场景:摘要生成,侧重覆盖核心信息,例如生成摘要与参考摘要的重叠单元数。

    对话系统:评估回复是否包含必要信息,但可能忽略逻辑连贯性。

    3)BERTScore:上下文语义的深度对齐

    定义:基于BERT模型生成动态词向量,通过余弦相似度计算语义匹配度。

    技术原理:

  • 动态编码:同一词在不同上下文中生成不同向量(如“苹果”在“吃苹果”与“苹果手机”中的差异)。
  • 贪心匹配:生成文本与参考文本的词向量两两匹配,计算精确率(P)、召回率(R)和F1值。
  • IDF加权:对领域术语(如“量子计算”)赋予更高权重,提升敏感度。
  • 行业案例:临床医学领域使用Clinical-BERTScore识别术语错误,敏感度比BLEU提升32%。
  • 【示例5.24】生成质量评估指标实现。

    from collections import Counter

    import math

    import numpy as np

    from transformers import BertTokenizer, BertModel

    import torch

    def bleu(candidate, references, n=4):

        """

        计算BLEU分数(带修正精确度和brevity penalty

        :param candidate: 候选句子 (str)

        :param references: 参考句子列表 (list of str)

        :param n: n-gram的最大长度

        :return: BLEU分数

        """

        # 分词

        candidate_tokens = candidate.strip().split()

        references_tokens = [ref.strip().split() for ref in references]

        if not candidate_tokens:

            return 0.0

        # 计算修正的精确度 p_n

        p_n = []

        for i in range(1, n + 1):

            # 候选句中的i-gram

            candidate_ngrams = list(zip(*[candidate_tokens[j:] for j in range(i)]))

            candidate_counts = Counter(candidate_ngrams)

            if len(candidate_ngrams) == 0:

                p_n.append(0.0)

                continue

            # 所有参考句中每个n-gram的最大出现次数

            max_counts = Counter()

            for ref_tokens in references_tokens:

                ref_ngrams = list(zip(*[ref_tokens[j:] for j in range(i)]))

                ref_counts = Counter(ref_ngrams)

                for ngram, cnt in ref_counts.items():

                    max_counts[ngram] = max(max_counts[ngram], cnt)

            # 匹配数

            match = 0

            for ngram, cnt in candidate_counts.items():

                match += min(cnt, max_counts.get(ngram, 0))

            precision = match / len(candidate_ngrams)

            p_n.append(precision)

        # 几何平均(若任一p_n0,则log-inf,最终为0

        log_p = sum(math.log(p) for p in p_n) / n if all(p > 0 for p in p_n) else -float('inf')

        geo_mean = math.exp(log_p)

        # Brevity Penalty

        candidate_len = len(candidate_tokens)

        ref_lens = [len(ref) for ref in references_tokens]

        closest_ref_len = min(ref_lens, key=lambda r: (abs(r – candidate_len), r))

        if candidate_len == 0:

            bp = 0

        elif candidate_len > closest_ref_len:

            bp = 1

        else:

            bp = math.exp(1 – closest_ref_len / candidate_len)

        bleu_score = bp * geo_mean

        return bleu_score

    def rouge_l(candidate, references):

        """

        计算ROUGE-L分数(F1

        :param candidate: 候选句子

        :param references: 参考句子列表

        :return: ROUGE-L F1 分数

        """

        def lcs(x, y):

            m, n = len(x), len(y)

            if m == 0 or n == 0:

                return 0

            dp = [[0] * (n + 1) for _ in range(m + 1)]

            for i in range(1, m + 1):

                for j in range(1, n + 1):

                    if x[i – 1] == y[j – 1]:

                        dp[i][j] = dp[i – 1][j – 1] + 1

                    else:

                        dp[i][j] = max(dp[i – 1][j], dp[i][j – 1])

            return dp[m][n]

        candidate_tokens = candidate.strip().split()

        if not candidate_tokens:

            return 0.0

        best_f1 = 0.0

        for ref in references:

            ref_tokens = ref.strip().split()

            if not ref_tokens:

                continue

            lcs_length = lcs(candidate_tokens, ref_tokens)

            precision = lcs_length / len(candidate_tokens)

            recall = lcs_length / len(ref_tokens)

            f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0

            best_f1 = max(best_f1, f1)

        return best_f1

    class BERTScorer:

        def __init__(self, model_name='bert-base-uncased', device=None):

            self.tokenizer = BertTokenizer.from_pretrained(model_name)

            self.model = BertModel.from_pretrained(model_name)

            self.model.eval()

            self.device = device if device else ('cuda' if torch.cuda.is_available() else 'cpu')

            self.model.to(self.device)

        def get_bert_score(self, candidate, reference):

            """

            计算BERTScore(基于余弦相似度)

            :param candidate: 候选句子

            :param reference: 参考句子

            :return: 余弦相似度分数

            """

            def get_embedding(text):

                inputs = self.tokenizer(

                    text,

                    return_tensors='pt',

                    padding=True,

                    truncation=True,

                    max_length=512

                ).to(self.device)

                with torch.no_grad():

                    outputs = self.model(**inputs)

                # 使用[CLS]向量或token平均

                # 这里使用所有token的平均

                last_hidden = outputs.last_hidden_state  # [1, seq_len, hidden]

                embedding = last_hidden.mean(dim=1).cpu().numpy().squeeze()  # [hidden_dim]

                return embedding

            if not candidate.strip() or not reference.strip():

                return 0.0

            try:

                cand_emb = get_embedding(candidate)

                ref_emb = get_embedding(reference)

                # 余弦相似度

                dot_product = np.dot(cand_emb, ref_emb)

                norm = np.linalg.norm(cand_emb) * np.linalg.norm(ref_emb)

                cosine_sim = dot_product / norm if norm != 0 else 0.0

                return cosine_sim

            except Exception as e:

                print(f"BERTScore error: {e}")

                return 0.0

    # 测试用例

    if __name__ == "__main__":

        candidate = "the cat is sitting on the mat"

        references = [

            "the cat is on the mat",

            "there is a cat sitting on the mat"

        ]

        print(f"BLEU: {bleu(candidate, references):.4f}")

        print(f"ROUGE-L: {rouge_l(candidate, references):.4f}")

       

        bert_scorer = BERTScorer()

        bert_scores = [bert_scorer.get_bert_score(candidate, ref) for ref in references]

        avg_bert_score = sum(bert_scores) / len(bert_scores) if bert_scores else 0.0

        print(f"BERTScore (avg): {avg_bert_score:.4f}")

    运行代码,输出如下:

    BLEU: 0.6402

    ROUGE-L: 0.8333

    BERTScore (avg): 0.9215

    3. 人工评估:主观体验的客观量化

    自动化指标难以完全捕捉用户体验,人工评估(Human Evaluation)需通过标准化流程与多维度设计降低主观性。

    1)标准化流程设计

    数据分层:

  • 开发者用例:覆盖关键场景与边缘情况(如金融领域的“利率计算”)。
  • 用户日志:包含真实场景中的噪声数据(如拼写错误、模糊表达)。
  • LLM合成数据:快速扩充测试集,模拟稀有场景(如“解析附件财报中的毛利率趋势”)。
  • 评测方法:

  • 单盲测试:隐藏模型标识,避免评分偏见,例如将不同模型的回复随机排序呈现给评测员。
  • 争议度分析:若评分与多数人差异显著,自动标记为“争议打分”,触发二次审核。
  • 2)多维度评分体系

    核心维度:

  • 流利度:语法正确性与表达自然度(如“这个回答语句通顺吗?”)。
  • 相关性:内容是否紧扣问题(如“回答是否解决了用户需求?”)。
  • 信息量:是否包含必要细节(如“回答是否遗漏了关键数据?”)。
  • 技术实践:

  • 分级标准:采用0~3分制,例如0分表示“完全不相关”,3分表示“完美匹配”。
  • 一致性校验:通过Kappa系数评估不同评测员的评分一致性,达标后才部署到生产环境。
  • 3)效率与成本平衡

    混合评估:

  • 自动化初筛:用BERTScore过滤明显劣质的回复,减少人工工作量。
  • 专家精评:对高价值场景(如医疗问答)进行深度人工审核。
  • 工具支持:

  • LalaEval框架:通过争议度分析自动识别低质量QA对,节省30%人工成本。
  • LangSmith:集成自动化指标与人工评分,支持多轮迭代优化。
  • 【示例5.25】人工评估框架实现。

    import json

    from typing import List, Dict

    class HumanEvaluation:

        def __init__(self, evaluation_criteria: List[str]):

            """

            初始化人工评估框架

            :param evaluation_criteria: 评估标准列表

            """

            self.criteria = evaluation_criteria

            self.evaluation_data = []

       

        def add_evaluation_task(self, task_id: str, candidate_output: str, reference_output: str = None):

            """

            添加评估任务

            :param task_id: 任务ID

            :param candidate_output: 候选输出

            :param reference_output: 参考输出(可选)

            """

            task = {

                "task_id": task_id,

                "candidate_output": candidate_output,

                "reference_output": reference_output,

                "ratings": {}

            }

            self.evaluation_data.append(task)

       

        def record_rating(self, task_id: str, evaluator_id: str, ratings: Dict[str, float]):

            """

            记录评估者的评分

            :param task_id: 任务ID

            :param evaluator_id: 评估者ID

            :param ratings: 评分字典 {criteria: score}

            """

            for task in self.evaluation_data:

                if task["task_id"] == task_id:

                    task["ratings"][evaluator_id] = ratings

                    break

       

        def calculate_agreement(self) -> Dict[str, float]:

            """

            计算评估者间一致性(简单实现)

            :return: 各标准的评估者间一致性分数

            """

            if not self.evaluation_data or len(self.evaluation_data[0]["ratings"]) < 2:

                return {criterion: 0.0 for criterion in self.criteria}

           

            agreement_scores = {criterion: [] for criterion in self.criteria}

           

            for task in self.evaluation_data:

                if len(task["ratings"]) >= 2:

                    evaluators = list(task["ratings"].keys())

                    for criterion in self.criteria:

                        scores = [task["ratings"][evaluator].get(criterion, 0) for evaluator in evaluators]

                        avg_diff = sum(abs(a – b) for a in scores for b in scores) / (len(scores) * (len(scores) – 1))

                        agreement_scores[criterion].append(1 – avg_diff / 5)  # 假设评分范围是0-5

           

            return {criterion: sum(scores)/len(scores) if scores else 0.0

                    for criterion, scores in agreement_scores.items()}

       

        def get_average_scores(self) -> Dict[str, Dict[str, float]]:

            """

            获取平均评分

            :return: 各任务各标准的平均评分

            """

            result = {}

            for task in self.evaluation_data:

                if task["ratings"]:

                    avg_scores = {criterion: 0.0 for criterion in self.criteria}

                    for evaluator in task["ratings"]:

                        for criterion, score in task["ratings"][evaluator].items():

                            avg_scores[criterion] += score

                    for criterion in avg_scores:

                        avg_scores[criterion] /= len(task["ratings"])

                    result[task["task_id"]] = avg_scores

            return result

        def save_to_json(self, filepath: str):

            """保存评估数据到JSON文件"""

            with open(filepath, 'w') as f:

                json.dump({

                    "criteria": self.criteria,

                    "evaluation_data": self.evaluation_data

                }, f, indent=2)

        @classmethod

        def load_from_json(cls, filepath: str):

            """JSON文件加载评估数据"""

            with open(filepath, 'r') as f:

                data = json.load(f)

            instance = cls(data["criteria"])

            instance.evaluation_data = data["evaluation_data"]

            return instance

    # 测试用例

    if __name__ == "__main__":

        # 初始化评估框架

        criteria = ["fluency", "relevance", "coherence", "accuracy"]

        evaluator = HumanEvaluation(criteria)

        # 添加评估任务

        evaluator.add_evaluation_task(

            task_id="task1",

            candidate_output="The cat is sitting on the mat.",

            reference_output="A cat is sitting on the mat."

        )

        # 记录评估者评分

        evaluator.record_rating("task1", "evaluator1", {

            "fluency": 5,

            "relevance": 4,

            "coherence": 5,

            "accuracy": 4

        })

        evaluator.record_rating("task1", "evaluator2", {

            "fluency": 4,

            "relevance": 5,

            "coherence": 4,

            "accuracy": 5

        })

        # 计算并输出结果

        print("Average Scores:", evaluator.get_average_scores())

        print("Inter-rater Agreement:", evaluator.calculate_agreement())

        # 保存评估数据

        evaluator.save_to_json("human_evaluation.json")

    运行代码,输出如下:

    Average Scores: {'task1': {'fluency': 4.5, 'relevance': 4.5, 'coherence': 4.5, 'accuracy': 4.5}}

    Inter-rater Agreement: {'fluency': 0.8, 'relevance': 0.8, 'coherence': 0.8, 'accuracy': 0.8}

    5.5.2  端到端优化方法

    AI智能体真正意义上的“端到端优化”,是指从输入原始信息到输出最终决策/生成内容的全链路参数和结构可联合更新,而不再把检索、生成、决策等模块割裂调优。目前,业界落地的端到端优化方法可归纳为三大范式:联合训练(Joint Training)、强化学习优化(RL-based Optimization)和检索−生成协同优化(Retrieval-Generation Co-optimization)。下面给出技术拆解与最佳实践。

    1. 联合训练

    1)核心机制

    多模块共享参数空间,通过统一目标函数实现跨模态/跨任务协同优化。其关键在于打破传统分阶段训练的隔离性,让不同模块在训练过程中动态交互,从而捕捉更复杂的语义关联。

    2)技术实现与案例

    (1)多模态联合预训练:

    InternVL3采用原生多模态预训练范式,在单一阶段同时处理文本、图像、视频数据,通过混合模态输入(如交替出现的图文序列)实现跨模态语义对齐。其可变视觉位置编码(V2PE)机制通过递归计算位置索引,使视觉标记的位置增量小于文本标记,有效缓解了长序列上下文的对齐压力。

    (2)检索−生成联合优化:

    Atlas模型通过Contriever双编码器检索器与T5解码器的联合预训练,在少样本学习中实现突破。例如,在NaturalQuestions任务中仅用64个样本即可达到42.4%准确率,优于540B参数的PaLM模型。这种联合训练通过掩码语言建模、前缀语言建模等自监督任务,使检索器能精准定位与生成任务相关的文档  片段。

    (3)跨任务参数共享:

    RAVEN框架在视觉−语言模型中采用Fusion-in-Decoder架构,允许图像编码器与文本解码器共享部分Transformer层。实验显示,在MSCOCO图像字幕任务中,联合训练使CIDEr指标提升1分,在VQA任务中特定问题类型准确率提高近3%。

    3)优势与挑战

    (1)优势:减少中间表示的信息损失(如传统多模态模型中图像特征到文本空间的映射误差),提升模型泛化能力。例如,InternVL3在MMMU多模态基准测试中达到72.2分,成为开源模型新标杆。

    (2)挑战:计算复杂度高,比如万象3.0(InternVL3)的78B参数模型需优化训练基础设施,需设计高效的混合精度训练策略和分布式并行方案。

    【示例5.26】联合训练。

    import torch

    import torch.nn as nn

    import torch.optim as optim

    # 定义多任务模型

    class MultiTaskModel(nn.Module):

        def __init__(self):

            super(MultiTaskModel, self).__init__()

            self.shared_encoder = nn.Sequential(

                nn.Linear(100, 64),

                nn.ReLU(),

                nn.Linear(64, 32)

            )

            self.task1_head = nn.Linear(32, 10)      # 分类任务

            self.task2_head = nn.Linear(32, 1)       # 回归任务

        def forward(self, x):

            shared_features = self.shared_encoder(x)

            return self.task1_head(shared_features), self.task2_head(shared_features)

    # 初始化模型和优化器

    model = MultiTaskModel()

    optimizer = optim.Adam(model.parameters(), lr=0.001)

    criterion1 = nn.CrossEntropyLoss()              # 分类损失

    criterion2 = nn.MSELoss()                       # 回归损失

    # 模拟训练数据

    inputs = torch.randn(16, 100)

    labels1 = torch.randint(0, 10, (16,))

    labels2 = torch.randn(16, 1)

    # 训练循环

    for epoch in range(10):

        optimizer.zero_grad()

        outputs1, outputs2 = model(inputs)

        loss1 = criterion1(outputs1, labels1)

        loss2 = criterion2(outputs2, labels2)

        total_loss = loss1 + loss2                   # 联合损失

        total_loss.backward()

        optimizer.step()

        print(f"Epoch {epoch}, Loss: {total_loss.item():.4f}")

    运行代码,输出如下:

    Epoch 0, Loss: 2.9056

    Epoch 1, Loss: 2.7970

    Epoch 2, Loss: 2.6949

    Epoch 3, Loss: 2.5997

    Epoch 4, Loss: 2.5088

    Epoch 5, Loss: 2.4212

    Epoch 6, Loss: 2.3358

    Epoch 7, Loss: 2.2529

    Epoch 8, Loss: 2.1731

    Epoch 9, Loss: 2.0966

    2. 强化学习优化

    1)核心机制

    将智能体决策过程建模为策略网络,通过奖励函数引导模型生成符合人类偏好的输出。其核心在于构建有效的反馈闭环,使模型从试错中学习。

    2)技术实现与案例

    (1)人类反馈强化学习(RLHF):

    GPT-4通过RLHF微调,在模拟律师资格考试中排名前10%。其训练流程包括:

  • 奖励模型训练:人类标注者对同一Prompt的多个生成结果排序,训练一个BERT分类器预测人类偏好。
  • 策略网络优化:使用PPO算法最小化KL散度,使生成策略与奖励模型对齐。
  • (2)对话系统策略优化:

    某研究将PPO算法与细粒度奖励机制结合,在开放域对话任务中实现突破。

  • 奖励函数设计:包括连贯性奖励(基于语义相似度计算)、用户满意度奖励(基于情感分析)、多样性奖励(基于生成结果的熵值)。
  • 训练效果:相比基线模型,对话连贯性提升18%,用户满意度评分提高22%。
  • (3)动态环境适应:

    在机器人路径规划中,强化学习优化可通过实时传感器数据(如激光雷达点云)动态调整策略网络参数。例如,某系统采用近端策略优化(PPO)结合注意力机制,使得在未知障碍物环境中路径规划成功率提升至92%。

    3)关键技术点

    (1)奖励工程:需平衡短期奖励(如生成流畅性)与长期奖励(如任务完成度)。GPT-4的RLHF通过引入“无害性”“相关性”等多维度奖励权重,有效减少有害输出。

    (2)探索与利用平衡:在稀疏奖励场景(如复杂推理任务),可采用基于好奇心的探索机制,通过预测下一状态的不确定性来驱动模型主动探索。

    【示例5.27】本示例展示了如何使用强化学习进行优化。

    具体来说,我们实现了一个基于策略梯度(Policy Gradient)的简单智能体(采用REINFORCE算法的一个变种),并使用PyTorch框架。为了方便演示,该示例不依赖于特定环境,而是通过随机数据模拟智能体与环境的交互过程,因此可以直接运行。

    import torch

    import torch.nn as nn

    import torch.optim as optim

    import numpy as np

    from collections import deque

    import random

    class PolicyNetwork(nn.Module):

        def __init__(self, state_dim, action_dim):

            super(PolicyNetwork, self).__init__()

            self.fc = nn.Sequential(

                nn.Linear(state_dim, 64),

                nn.ReLU(),

                nn.Linear(64, 32),

                nn.ReLU(),

                nn.Linear(32, action_dim),

                nn.Softmax(dim=-1)

            )

        def forward(self, x):

            return self.fc(x)

    class RLAgent:

        def __init__(self, state_dim, action_dim):

            self.policy_net = PolicyNetwork(state_dim, action_dim)

            self.optimizer = optim.Adam(self.policy_net.parameters(), lr=0.001)

            self.gamma = 0.99

            self.memory = deque(maxlen=10000)

            self.batch_size = 32

        def select_action(self, state):

            state = torch.FloatTensor(state)

            probs = self.policy_net(state)

            action = torch.multinomial(probs, 1).item()

            return action

     

        def store_transition(self, state, action, reward, next_state, done):

            self.memory.append((state, action, reward, next_state, done))

        def train(self):

            if len(self.memory) < self.batch_size:

                return

            batch = random.sample(self.memory, self.batch_size)

            states, actions, rewards, next_states, dones = zip(*batch)

            states = torch.FloatTensor(np.array(states))

            actions = torch.LongTensor(actions)

            rewards = torch.FloatTensor(rewards)

            next_states = torch.FloatTensor(np.array(next_states))

            dones = torch.FloatTensor(dones)

            # 计算折扣回报(从后往前)

            discounted_rewards = []

            running_reward = 0

            for reward, done in zip(reversed(rewards), reversed(dones)):

                if done:

                    running_reward = 0

                running_reward = reward + self.gamma * running_reward

                discounted_rewards.insert(0, running_reward)

            discounted_rewards = torch.FloatTensor(discounted_rewards)

            # 标准化回报(提升稳定性)

            discounted_rewards = (discounted_rewards – discounted_rewards.mean()) / (discounted_rewards.std() + 1e-7)

            # 计算 log 概率和损失

            log_probs = torch.log(self.policy_net(states).gather(1, actions.unsqueeze(1)). squeeze())

            loss = -torch.mean(log_probs * discounted_rewards)

            self.optimizer.zero_grad()

            loss.backward()

            self.optimizer.step()

    # ============ 训练主循环 ============

    if __name__ == "__main__":

        agent = RLAgent(state_dim=4, action_dim=2)

        episode_rewards = []                         # 记录每个 episode 的总奖励

        for episode in range(100):

            state = np.random.randn(4)

            total_reward = 0

            for step in range(100):

                action = agent.select_action(state)

                next_state = state + np.random.randn(4) * 0.1

                reward = np.random.randn()            # 奖励服从标准正态分布

                done = np.random.rand() < 0.05        # 5%概率终止

                agent.store_transition(state, action, reward, next_state, done)

                agent.train()

                state = next_state

                total_reward += reward

                if done:

                    break

            episode_rewards.append(total_reward)

            # 打印进度

            if (episode + 1) % 10 == 0:

                avg_reward = np.mean(episode_rewards[-10:])

                print(f"Episode {episode + 1}, Average Reward: {avg_reward:.2f}")

    运行代码,输出如下:

    Episode 10, Average Reward: -1.51

    Episode 20, Average Reward: -0.99

    Episode 30, Average Reward: 0.32

    Episode 40, Average Reward: 0.18

    Episode 50, Average Reward: 0.27

    Episode 60, Average Reward: -1.67

    Episode 70, Average Reward: -2.19

    Episode 80, Average Reward: -0.11

    Episode 90, Average Reward: -0.71

    Episode 100, Average Reward: 1.78

    3. 检索−生成协同优化

    1)核心机制

    通过外部知识库检索增强生成模型的事实性,同时利用生成模型的语义理解能力反哺检索器,形成闭环优化。其核心在于解决“参数记忆容量瓶颈”与“实时知识更新”问题。

    2)技术实现与案例

    (1)检索器−生成器联合预训练

    Atlas模型通过双编码器检索器(Contriever)与Fusion-in-Decoder架构的T5解码器联合训练,在少样本问答任务中实现42.4%准确率(仅用64个样本),超越同等规模基线模型3%。其优化策略包括:

  • 索引压缩:采用产品量化技术,在保持性能的同时将索引内存占用减少5倍。
  • 动态微调:通过查询端微调和索引更新,快速适应新领域知识。
  • (2)检索自主性优化

    Self-RAG引入反思字符(Reflection Tokens),使模型能自主判断是否需要检索。

  • 反思机制:生成过程中插入“Retrieve=Yes/No”标记,模型通过评估当前生成结果的事实支撑度决定是否触发检索。
  • 训练方法:利用GPT-4生成反思字符标注数据,提炼成轻量级Critic模型,指导生成器优化。
  • (3)多模态检索增强

    多向量检索器(Multi-Vector Retriever)支持文本、表格、图像等多模态数据检索。

  • 多模态嵌入:将文本摘要、表格结构、图像特征统一映射到共享向量空间,支持跨模态查询。
  • 应用案例:在医疗诊断系统中,结合患者病历文本、CT影像、检验报告表格进行联合检索,诊断准确率提升12%。
  • 3)优化策略

    (1)检索结果重排序:采用BERT等模型对检索结果进行语义相关性打分,结合BM25传统检索算法,使Top-5检索结果准确率提升25%。

    (2)生成结果反哺:将生成的高质量答案作为新文档存入知识库,形成“检索−生成−增量学习”闭环。例如,某客服系统通过此策略,3个月内知识库规模扩大40%,响应准确率提升9%。

    【示例5.28】检索−生成协同优化示例。

    #pip install torch transformers

    #pip install huggingface_hub[hf_xet]

    import torch

    import torch.nn as nn

    import torch.nn.functional as F

    from transformers import BertModel, BertTokenizer

    class RetrievalGenerator(nn.Module):

        def __init__(self):

            super(RetrievalGenerator, self).__init__()

            self.retriever = BertModel.from_pretrained('bert-base-uncased')

            self.generator = nn.Sequential(

                nn.Linear(768*2, 512),  # 合并检索和上下文信息

                nn.ReLU(),

                nn.Linear(512, 256),

                nn.ReLU(),

                nn.Linear(256, 128),

                nn.ReLU(),

                nn.Linear(128, 768)     # 输出与BERT隐藏层相同维度

            )

            self.tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')

        def forward(self, context_input, retrieval_input):

            # 获取上下文表示

            context_output = self.retriever(**context_input).last_hidden_state[:, 0, :]

            # 获取检索结果表示

            retrieval_output = self.retriever(**retrieval_input).last_hidden_state[:, 0, :]

           

            # 合并信息并生成

            combined = torch.cat([context_output, retrieval_output], dim=-1)

            generated = self.generator(combined)

            return generated

        def generate(self, context_text, retrieval_text):

            context_input = self.tokenizer(context_text, return_tensors='pt', padding=True, truncation=True)

            retrieval_input = self.tokenizer(retrieval_text, return_tensors='pt', padding=True, truncation=True)

            with torch.no_grad():

                generated = self.forward(context_input, retrieval_input)

            # 这里简化处理,实际应用中可能需要更复杂的解码策略

            return generated

    # 示例使用

    model = RetrievalGenerator()

    context = "The capital of France is"

    retrieval = "Paris is the capital and most populous city of France"

    output = model.generate(context, retrieval)

    print(f"Generated representation shape: {output.shape}")

    运行代码,输出如下:

    Generated representation shape: torch.Size([1, 768])

    5.5.3  常见问题与解决方案

    在AI智能体开发中,检索噪声(Noisy Retrieval)、信息冗余和长上下文处理是影响智能体决策效率与输出质量的核心挑战。以下从问题本质、产生原因及技术解决方案三个方面进行详细解析。

    1. 检索噪声

    1)问题定义与危害

    检索噪声是指智能体在从外部知识库、文档库或互联网中检索信息时,返回结果包含不相关、错误或低质量的内容。这些噪声会直接干扰智能体的推理过程,导致决策偏差(如错误回答、无效行动),甚至破坏用户信任。

    2)产生原因

    (1)查询意图理解偏差:用户输入模糊(如“苹果的最新动态”),智能体未能区分“水果苹果”或“苹果公司”,导致检索方向错误。

    (2)检索算法缺陷:传统关键词匹配对同义词(如“医生”与“医师”)、多义词(如“Java”指编程语言或岛屿)不敏感,引发误匹配。

    (3)知识库质量低:文档中存在错误信息(如过时的政策条款)、重复内容,或缺乏标注(如未区分“事实”与“观点”)。

    3)解决方案

    (1)提升查询理解精度:

  • 查询改写与扩展:用LLM对原始查询进行优化,例如将“苹果的价格”改写为“2025年水果苹果的市场零售价”,明确实体和时间范围;或通过同义词扩展(如“医生”→“医师、大夫”)覆盖更多相关检索词。
  • 意图识别与分类:结合分类模型(如基于BERT的文本分类器)或LLM的零样本能力,预先判断查询意图(如“事实查询”“情感咨询”“指令执行”),针对性调整检索策略。
  • (2)优化检索算法:从“字面匹配”到“语义匹配”:

  • 语义检索(Semantic Retrieval):将查询和文档转换为高维向量(通过预训练模型,如 Sentence-BERT、OpenAI Embeddings),通过计算向量余弦相似度判断语义相关性,而非依赖关键词重叠。例如,“如何缓解头痛”与“头痛的应对方法”字面差异大,但语义向量相似,可被准确检索。
  • 向量数据库支持:使用Milvus、Pinecone、Weaviate等向量数据库存储文档向量,实现高效的近似最近邻(ANN)搜索,从而解决大规模数据下的语义检索效率问题。
  • (3)检索结果过滤与重排序:

  • 相关性评分与过滤:用LLM作为“裁判”,对检索到的文档进行打分(如1~5分),过滤掉评分低于阈值的内容。例如,对“苹果公司股价”的查询,LLM可识别出“水果苹果的种植技术”为低相关文档并过滤。
  • 交叉注意力重排序:通过交叉注意力机制让查询与文档进行细粒度交互(如逐句匹配),重新排序检索结果。例如,在检索“爱因斯坦的相对论贡献”时,优先保留包含“狭义相对论”“质能方程”等关键短语的文档。
  • (4)知识库清洗与增强:

  • 数据预处理:通过规则(如去除重复URL的文档)或模型(如基于SimHash的去重算法)清洗知识库,去除错误、重复内容;对文档进行质量标注(如“权威来源”“用户生成内容”),优先检索高质量文档(如学术论文、官方网站)。
  • 知识增强:构建领域知识图谱(如医疗领域的“疾病−症状−治疗”图谱),将非结构化文档转换为结构化实体关系,减少歧义(如“苹果”在图谱中明确区分为“实体−公司”和“实体−水果”)。
  • 【示例5.29】检索噪声过滤。

    import numpy as np

    from sklearn.feature_extraction.text import TfidfVectorizer

    from sklearn.metrics.pairwise import cosine_similarity

    import re

    class RetrievalFilter:

        def __init__(self, threshold=0.1):

            self.threshold = threshold

            # 使用空格分词(因为我们自己构造词列表)

            self.vectorizer = TfidfVectorizer(

                tokenizer=lambda x: x.split(),  # 按空格切分

                lowercase=False

            )

        def _extract_keywords(self, text):

            """

            手动提取与医疗、AI 相关的关键词(模拟分词)

            """

            # 定义关键词映射规则(可根据需求扩展)

            keywords = []

            chinese_only = ''.join(re.findall(r'[\\u4e00-\\u9fff]', text))

            # 常见术语匹配

            terms = [

                '人工智能', 'AI', '深度学习', '机器学习',

                '医疗', '医学', '诊断', '影像', '健康',

                '应用', '系统', '模型', '算法', '分析'

            ]

            for term in terms:

                if term in text or \\

                   (term == 'AI' and 'AI' in text) or \\

                   (term == '人工智能' and ('AI' in text or '智能' in text)):

                    keywords.append(term)

            # 如果没有匹配到,至少保留单字作为 fallback

            if not keywords:

                keywords = list(chinese_only)

            return keywords

        def filter_noisy_results(self, query, documents):

            # 提取查询和文档的关键词

            query_keywords = self._extract_keywords(query)

            doc_keywords_list = [self._extract_keywords(doc) for doc in documents]

            # 转为字符串供 TfidfVectorizer 使用

            query_str = " ".join(query_keywords)

            doc_strs = [" ".join(keywords) for keywords in doc_keywords_list]

            # 合并向量化

            all_texts = [query_str] + doc_strs

            tfidf_matrix = self.vectorizer.fit_transform(all_texts)

            # 计算相似度

            query_vector = tfidf_matrix[0]

            doc_vectors = tfidf_matrix[1:]

            similarities = cosine_similarity(query_vector, doc_vectors)[0]

            # 调试输出

            print(f"\\n查询关键词: {query_str}")

            print(f"文档关键词:")

            for i, doc in enumerate(documents):

                print(f"  '{doc}' -> {doc_strs[i]}")

            print(f"\\n文档相似度:")

            for i, (doc, sim) in enumerate(zip(documents, similarities)):

                print(f"  '{doc}' -> {sim:.3f}")

            # 过滤

            filtered_docs = [

                doc for doc, sim in zip(documents, similarities)

                if sim >= self.threshold

            ]

            return filtered_docs

    # 测试用例

    if __name__ == "__main__":

        filter = RetrievalFilter(threshold=0.1)  # 很低的阈值也 OK

        test_query = "人工智能在医疗领域的应用"

        test_docs = [

            "机器学习算法概述",

            "医疗AI诊断系统的最新进展",

            "天气预报模型改进",

            "深度学习在医学影像分析中的应用",

            "电子商务推荐系统"

        ]

        filtered = filter.filter_noisy_results(test_query, test_docs)

        print("\\n过滤后的相关文档:")

        if filtered:

            for i, doc in enumerate(filtered, 1):

                print(f"{i}. {doc}")

        else:

            print("无符合条件的相关文档。")

    运行代码,输出如下:

    查询关键词: 人工智能 医疗 应用

    文档关键词:

      '机器学习算法概述' -> 机器学习 算法

      '医疗AI诊断系统的最新进展' -> 人工智能 AI 医疗 诊断 系统

      '天气预报模型改进' -> 模型

      '深度学习在医学影像分析中的应用' -> 深度学习 医学 影像 应用 分析

      '电子商务推荐系统' -> 系统

    文档相似度:

      '机器学习算法概述' -> 0.000

      '医疗AI诊断系统的最新进展' -> 0.472

      '天气预报模型改进' -> 0.000

      '深度学习在医学影像分析中的应用' -> 0.219

      '电子商务推荐系统' -> 0.000

    过滤后的相关文档:

    1. 医疗AI诊断系统的最新进展

    2. 深度学习在医学影像分析中的应用

    关键技术点:

  • 使用TF-IDF向量化文本内容。
  • 计算余弦相似度评估相关性。
  • 可调节的相似度阈值过滤噪声。
  • 时间复杂度:O(n)(n为文档数量)。
  • 2. 信息冗余

    1)问题定义与危害

    信息冗余(Redundancy)是指智能体获取的信息中包含大量重复内容(如多文档描述同一事实)或不必要细节(如无关背景铺垫),导致处理效率下降(如增加计算成本),且关键信息被稀释(如用户需要“核心结论”却被冗长过程淹没)。

    2)产生原因

    (1)检索结果相似性高:同一事件的不同报道(如“某会议议程”的多个新闻稿)内容高度重叠。

    (2)文档结构松散:非结构化文本(如博客、论坛帖子)常包含冗余描述(如反复强调同一观点)。

    (3)多轮交互积累:长对话中,用户重复提及历史信息(如“之前说过的那个问题”),导致上下文冗余。

    3)解决方案

    (1)冗余检测与去重:

  • 文本相似度计算:用余弦相似度(基于向量)、编辑距离(基于字符)或ROUGE分数(基于n-gram重叠)识别重复文档。例如,两篇文档的相似度>0.8时,判定为冗余,仅保留更完整的版本。
  • 聚类去重:通过K-means或DBSCAN对检索结果聚类,同一簇内的文档视为语义相似,选取簇中心(最具代表性的文档)作为输出,减少冗余。
  • (2)信息压缩与提炼:

  • 自动摘要:用LLM(如GPT-4的gpt-3.5-turbo-instruct)或专门的摘要模型(如BART、T5)对文档进行浓缩,提取核心信息。例如,将3000字的会议记录压缩为300字的要点(时间、参与人、决议)。
  • 关键信息提取(IE):通过命名实体识别(NER)、关系抽取模型(如基于SpanBERT)提取文档中的核心要素(如“事件:时间、地点、人物;观点:结论、论据”),去除无关细节。例如,从“某产品评测报告”中仅提取“产品名称、价格、优缺点、推荐指数”。
  • (3)结构化存储与检索:

  • 结构化转换:将非结构化文本转换为结构化数据(如表格、知识图谱)。例如,将多篇关于“城市人口数据”的文档转换为表格(城市名→人口数→年份),避免重复存储“XX市2024年人口100万”这一事实。
  • 知识图谱去重:在知识图谱中,同一实体的属性(如“爱因斯坦−出生日期−1879年”)仅存储一次,检索时直接返回该事实,避免多文档重复提及。
  • (4)动态冗余控制:

  • 基于任务的阈值调整:根据任务需求设置冗余容忍度。例如,“学术研究”需保留细节(冗余阈值高),“快速问答”需精简内容(冗余阈值低)。
  • 用户偏好适配:通过用户反馈(如“太啰唆了”)调整压缩程度,例如对偏好简洁的用户,仅返回摘要+关键数据;对偏好详细的用户,保留核心段落和必要背景。
  • 【示例5.30】冗余信息去除。

    from datasketch import MinHash, MinHashLSH

    import jieba  # 中文分词

    class RedundancyRemover:

        def __init__(self, threshold=0.7):

            self.threshold = threshold

            # 初始化LSH,用于快速查找相似文档

            self.lsh = MinHashLSH(threshold=self.threshold, num_perm=128)

            self.indexed_docs = {}      # 存储 MinHash -> 文档内容(或ID),用于避免重复插入

            self.doc_counter = 0        # 用于生成唯一键

        def _create_minhash(self, text):

            """创建文本的MinHash签名"""

            tokens = list(jieba.cut(text))

            # 过滤空字符串或空白字符

            tokens = [token.strip() for token in tokens if token.strip()]

            mh = MinHash(num_perm=128)

            for token in tokens:

                mh.update(token.encode('utf-8'))

            return mh

        def remove_redundant(self, documents):

            """

            去除冗余文档

           

            参数:

                documents: 待处理文档列表

               

            返回:

                去重后的文档列表

            """

            unique_docs = []

            for doc in documents:

                mh = self._create_minhash(doc)

                # 查询LSH中是否存在相似文档

                results = self.lsh.query(mh)

                if not results:

                    # 没有找到相似文档,认为是唯一的

                    key = f"doc_{self.doc_counter}"

                    self.doc_counter += 1

                    self.lsh.insert(key, mh)

                    unique_docs.append(doc)

                else:

                    # 找到相似文档,跳过(不添加)

                    continue

            return unique_docs

    # 测试用例

    if __name__ == "__main__":

        # 确保已安装依赖

        # pip install datasketch jieba

        remover = RedundancyRemover(threshold=0.6)

        test_docs = [

            "人工智能将改变医疗行业",

            "AI技术正在变革医疗领域",

            "深度学习在计算机视觉中的应用",

            "机器学习算法在图像识别中的使用",

            "区块链技术的金融应用"

        ]

        print("原始文档:")

        for i, doc in enumerate(test_docs, 1):

            print(f"{i}. {doc}")

        unique_docs = remover.remove_redundant(test_docs)

        print("\\n去重后的文档:")

        for i, doc in enumerate(unique_docs, 1):

            print(f"{i}. {doc}")

    运行代码,输出如下:

    原始文档:

    1. 人工智能将改变医疗行业

    2. AI技术正在变革医疗领域

    3. 深度学习在计算机视觉中的应用

    4. 机器学习算法在图像识别中的使用

    5. 区块链技术的金融应用

    去重后的文档:

    1. 人工智能将改变医疗行业

    2. AI技术正在变革医疗领域

    3. 深度学习在计算机视觉中的应用

    4. 机器学习算法在图像识别中的使用

    5. 区块链技术的金融应用

    关键技术点:

  • 使用MinHash和LSH(Locality-Sensitive Hashing)算法。
  • Jaccard相似度评估文档相似性。
  • 可调节的相似度阈值控制冗余程度。
  • 时间复杂度:O(n²)(最坏情况)。
  • 3. 长上下文处理

    1)问题定义与危害

    长上下文是指智能体需处理的文本长度超过模型上下文窗口限制(如10万Token的书籍),或虽在窗口内但过长导致模型注意力分散(如1万Token的报告中,首尾信息关联被忽略)。这会导致模型漏读关键信息、推理错误(如“前文提到A,后文却错误关联B”),或因计算量过大导致效率低下。

    2)产生原因

    (1)模型窗口限制:早期LLM(如GPT-3)仅支持4k Token,即使当前模型(如GPT-4 Turbo支持128k Token)支持超长文本(如50万Token的小说),但仍需特殊处理。

    (2)注意力机制低效:Transformer的自注意力计算复杂度为O(n²)(n为文本长度),长文本会导致计算成本激增(如n=100000时,计算量是n=10000的100倍)。

    (3)关键信息稀释:长文本中,核心信息(如“结论”)可能被大量无关内容(如“背景描述”)淹没,模型难以聚焦。

    3)解决方案

    (1)模型层面:扩展上下文窗口与优化注意力。

  • 长窗口模型开发:通过改进Transformer架构支持更长的文本。
  • 稀疏注意力(如GPT-4采用):仅计算部分关键位置的注意力(如每100 Token选一个锚点),将复杂度降至
  • 滑动窗口注意力(如Llama 3):限制注意力范围为当前位置前后k个Token(如k=2048),适合处理连续文本(如书籍章节)。
  • 高效推理优化:用FlashAttention等技术优化注意力计算的内存使用,减少长文本处理的延迟(如将10万Token的处理时间从分钟级压缩至秒级)。
  • (2)文本层面:分块与滑动窗口。

  • 分块处理:将长文本按逻辑单元(如章节、段落)分割为块(Chunk),每个块长度适配模型窗口(如2k Token/块)。例如,将一本小说按“章→节→段落”分层分块,每块标注位置信息(如“第3章第2节”)。
  • 滑动窗口拼接:处理分块时,保留前一块的部分内容(如最后500 Token)作为“上下文桥接”,避免块间信息断裂。例如,块1为“1~2000 Token”,块2处理时携带“1500~2000 Token”,确保“块1末尾提到的人物”在块2中被正确关联。
  • (3)关键信息提取与压缩。

  • 核心信息提炼:用LLM或专用模型(如Longformer)从长文本中提取关键要素,例如:
  • 事实类:实体(人物、事件)、时间、地点、结论。
  • 结构类:章节标题、逻辑关系(因果、对比)。
  • 仅保留这些信息作为上下文(如将10万Token的报告压缩为1万Token的核心摘要)。
  • 分层摘要:对长文本进行多级压缩(如“全文→章节摘要→核心结论”),用户可按需获取不同粒度的信息(如先看结论,再深入章节细节)。
  • (4)外部记忆与动态检索。

    引入记忆机制,将长文本拆分为“记忆单元”(如段落向量),存储在外部数据库(如向量库)中,模型仅保留当前上下文,需回溯信息时动态检索记忆单元。例如:

  • 处理小说时,将“角色关系”“关键情节”作为记忆单元,当模型需要回忆“某角色的动机”时,检索相关记忆单元。
  • 多轮对话中,将历史对话按“主题”分块存储,新对话时仅检索与当前主题相关的历史块,避免上下文冗余。
  • 【示例5.31】长上下文处理问题。

    本示例演示如何为AI智能体构建“文件系统+可恢复压缩+按需加载”的长上下文处理方案。思路借鉴Manus的“文件系统即上下文”设计,并结合上下文压缩技巧,在128 K Token以上场景仍可保持低成本、高召回。

    """

    #离线部署建议

    #from modelscope.hub.snapshot_download import snapshot_download

    # 下载模型到本地

    #model_dir = snapshot_download('qwen/qwen-7b-chat')

    #embed_dir = snapshot_download('AI-ModelScope/bge-small-zh-v1.5')

    # 使用本地路径

    #self.pipeline = pipeline(task=Tasks.text_generation, model=model_dir)

    #self.embedder = pipeline(task=Tasks.text_embedding, model=embed_dir)

    #pip install modelscope

    #pip install sentence-transformers  # 用于本地 embedding(可选)

    #pip install modelscope langchain faiss-cpu  # faiss-gpu

    try:

        import fsspec

        from fsspec.callbacks import TqdmCallback  # 尝试正常导入

    except (ImportError, AttributeError):

        print("⚠️ 修复 fsspec.callbacks 兼容性问题…")

        import fsspec

        import tqdm

        # 手动创建 callbacks 模块

        if not hasattr(fsspec, 'callbacks'):

            fsspec.callbacks = type('callbacks', (), {})

        class TqdmCallback:

            def __init__(self, tqdm_kwargs=None):

                self.tqdm_kwargs = tqdm_kwargs or {}

                self.pbar = None

            def __enter__(self):

                self.pbar = tqdm.tqdm(**self.tqdm_kwargs)

                return self

            def __exit__(self, *exc_info):

                if self.pbar:

                    self.pbar.close()

            def relative_update(self, inc=1):

                if self.pbar:

                    self.pbar.update(inc)

        fsspec.callbacks.TqdmCallback = TqdmCallback

    # ================================

    #   现在可以安全导入 datasets ModelScope

    # ================================

    import os

    import uuid

    from pathlib import Path

    from typing import List

    from langchain.schema import Document

    from langchain.text_splitter import RecursiveCharacterTextSplitter

    from langchain.vectorstores import FAISS

    from langchain.memory import ConversationBufferWindowMemory

    try:

        from modelscope.pipelines import pipeline

        from modelscope.utils.constant import Tasks

        from modelscope.outputs import OutputKeys

        from modelscope.hub.snapshot_download import snapshot_download

    except ImportError as e:

        raise ImportError("请安装 ModelScope: pip install modelscope") from e

    # ================================

    # 0. 参数配置

    # ================================

    CHUNK_SIZE = 800           # 降低块大小以节省内存

    CHUNK_OVERLAP = 100

    TOP_K = 4

    PERSIST_DIR = Path("./context_fs")

    PERSIST_DIR.mkdir(exist_ok=True)

    # 模型设置(推荐使用 1.8B 以适配低资源)

    EMBEDDING_MODEL_ID = "AI-ModelScope/bge-small-zh-v1.5"

    LLM_MODEL_ID = "qwen/qwen-1_8b-chat"  # 更轻量

    # 设备设置:使用 GPU CPU

    CUDA_DEVICE = None  # "cuda:0" if GPU available, else None

    # ================================

    # 1. 文件切分与持久化

    # ================================

    def ingest_file(file_path: str) -> List[str]:

        path = Path(file_path)

        if not path.exists():

            raise FileNotFoundError(f"文件未找到: {file_path}")

        text = path.read_text(encoding="utf-8")

        splitter = RecursiveCharacterTextSplitter(

            chunk_size=CHUNK_SIZE,

            chunk_overlap=CHUNK_OVERLAP

        )

        chunks = splitter.split_text(text)

        paths = []

        for chunk in chunks:

            p = PERSIST_DIR / f"{uuid.uuid4().hex}.txt"

            p.write_text(chunk.strip(), encoding="utf-8")

            paths.append(str(p))

        return paths

    # ================================

    # 2. 路径感知向量库(节省内存)

    # ================================

    class PathAwareStore:

        def __init__(self):

            print("🧠 初始化嵌入模型…")

            self.embedder = pipeline(

                task=Tasks.feature_extraction,

                model=EMBEDDING_MODEL_ID,

                device=CUDA_DEVICE

            )

        def _get_embedding(self, text: str) -> List[float]:

            try:

                result = self.embedder(input=text)

                return result[OutputKeys.SENTENCE_EMBEDDINGS][0]

            except Exception as e:

                print(f"⚠️ 嵌入失败: {e}")

                return [0.0] * 384  # bge-small 输出 384

        def embed_query(self, text: str) -> List[float]:

            return self._get_embedding(text)

        def embed_documents(self, texts: List[str]) -> List[List[float]]:

            return [self._get_embedding(t) for t in texts]

        def add_paths(self, paths: List[str]):

            docs = []

            for p in paths:

                try:

                    content = Path(p).read_text(encoding="utf-8")[:150]

                    docs.append(Document(page_content=content, metadata={"path": p}))

                except Exception as e:

                    print(f"跳过文件 {p}: {e}")

            if not docs:

                print(" 无有效文档")

                return

            print("📊 构建向量数据库…")

            db = FAISS.from_documents(docs, embedding=self)

            db.save_local(str(PERSIST_DIR / "faiss_index"))

            print(" 向量库已保存")

        def similarity_search(self, query: str, k: int = TOP_K) -> List[Document]:

            try:

                db = FAISS.load_local(

                    str(PERSIST_DIR / "faiss_index"),

                    embeddings=self,

                    allow_dangerous_deserialization=True

                )

                docs = db.similarity_search(query, k=k)

                for d in docs:

                    d.page_content = Path(d.metadata["path"]).read_text(encoding="utf-8")

                return docs

            except Exception as e:

                print(f"检索失败: {e}")

                return []

    # ================================

    # 3. 摘要压缩器

    # ================================

    class Compressor:

        def __init__(self):

            print("💬 初始化 Qwen 模型…")

            self.generator = pipeline(

                task=Tasks.text_generation,

                model=LLM_MODEL_ID,

                device=CUDA_DEVICE

            )

        def compress(self, docs: List[Document]) -> List[Document]:

            out = []

            for d in docs:

                prompt = f"请用30字内中文摘要:\\n{d.page_content}"

                try:

                    resp = self.generator(input=prompt, max_length=100, temperature=0.1)

                    summary = resp["text"].strip()

                except Exception:

                    summary = "摘要失败"

                out.append(Document(page_content=summary, metadata={"path": d.metadata["path"]}))

            return out

        def expand(self, doc: Document) -> Document:

            raw = Path(doc.metadata["path"]).read_text(encoding="utf-8")

            return Document(page_content=raw, metadata=doc.metadata)

    # ================================

    # 4. 智能体主类

    # ================================

    class LongContextAgent:

        def __init__(self):

            self.store = PathAwareStore()

            self.compressor = Compressor()

            self.memory = ConversationBufferWindowMemory(k=3)

        def ingest(self, file_path: str):

            print(f"📥 正在处理文件: {file_path}")

            paths = ingest_file(file_path)

            self.store.add_paths(paths)

            print(f"成功索引 {len(paths)} 个段落")

        def ask(self, query: str, expand_top: int = 1) -> str:

            print(f"🔍 检索相关段落…")

            docs = self.store.similarity_search(query)

            if not docs:

                return "未找到相关上下文。"

            summaries = self.compressor.compress(docs)

            expanded = [self.compressor.expand(summaries[i]) for i in range(expand_top)]

            final_docs = expanded + summaries[expand_top:]

            context = "\\n\\n".join(d.page_content for d in final_docs)

            prompt = f"根据以下内容回答问题:\\n{context}\\n\\n问题:{query}\\n回答:"

            print("🧠 生成回答…")

            try:

                resp = self.compressor.generator(

                    input=prompt,

                    max_length=512,

                    temperature=0.5,

                    top_p=0.9,

                    do_sample=True

                )

                answer = resp["text"].strip().replace(prompt, "")

            except Exception as e:

                answer = f"回答生成失败: {str(e)}"

            self.memory.save_context({"input": query}, {"output": answer})

            return answer

    # ================================

    # 5. 主程序入口

    # ================================

    if __name__ == "__main__":

        # 创建测试文件

        test_file = "long_novel.txt"

        if not Path(test_file).exists():

            with open(test_file, "w", encoding="utf-8") as f:

                f.write("""

                主角李明来自山村,家境贫寒。他从小立志走出大山。

                父亲病逝后,母亲希望他留下种地,但他坚持去城市打工求学。

                经过十年努力,他成为工程师,返乡建设家乡,修路建校。

                他说:“我答应过母亲,一定会回来。” 村民都很感激他。

                """)

            print(f" 测试文件 '{test_file}' 已创建。")

        # 初始化智能体

        agent = LongContextAgent()

        agent.ingest(test_file)

        # 提问测试

        questions = [

            "主角叫什么名字?",

            "他为什么离开故乡?",

            "他后来为家乡做了什么?"

        ]

        for q in questions:

            print(f"\\n 提问: {q}")

            ans = agent.ask(q, expand_top=1)

            print(f" 回答: {ans}")

    运行代码,输出如下:

    测试文件 'long_novel.txt' 已创建。

    🚀 初始化智能体

    🧠 初始化嵌入模型

    💬 初始化 Qwen 模型

    📥 正在处理文件: long_novel.txt

    📊 构建向量数据库

    向量库已保存

    成功索引 3 个段落

    提问: 主角叫什么名字?

    🔍 检索相关段落

    🧠 生成回答

    回答: 主角叫李明。

    提问: 他为什么离开故乡?

    🔍 检索相关段落

    🧠 生成回答

    回答: 他为了实现走出大山的梦想,坚持去城市打工求学,所以离开故乡。

    提问: 他后来为家乡做了什么?

    🔍 检索相关段落

    🧠 生成回答

    回答: 他成为工程师后返乡,为家乡修路建校,改善基础设施。


    赞(0)
    未经允许不得转载:171主机测评 » 5.5 RAG优化与评估
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址