游乐游手机版
首页/AI热点日报/热点详情

Self-RAG下篇:构建自省式RAG应用与模型微调

类型:热点整理2026-07-17
基于Self-RAG开源微调模型,完整演示了整个自省式RAG应用的构建流程,涵盖模型测试、检索触发、多维度评分(相关度、支持度与有用性)及最优答案选择,实现了按需检索能力与自我反省能力,显著提升了回答质量。

CAMPING

一文彻底搞懂Self-RAG【下】:构建自省式的RAG应用与模型微调

上一篇文章中,我们详细拆解了自省式RAG(Self-RAG)的诞生背景与核心原理。它的整体思路非常清晰,可以归纳为以下三个要点: * 只在真正需要时才去检索外部知识,避免无谓的资源消耗。 * 借助一套评分算法,从多次LLM输出中筛选出最优答案。 * 通过微调训练,让LLM在生成内容时主动嵌入用于自我反省的专用token标记。 今天,我们将基于Self-RAG开源项目中的selfrag_llama2_7b微调模型,一步步演示如何构建一个完整的Self-RAG应用。内容将涵盖以下几个关键环节: * **模型测试** * **构建Self-RAG应用** * **应用的优化思考** * **模型的微调方法**

模型测试:selfrag_llama2_7b

我们先直接上手测试这个selfrag_llama2_7b模型,感受一下它输出的自省token究竟有何不同。这里选用`llama_cpp`作为本地推理工具(当然,你也可以选择`vLLM`)。 1. 首先安装Llama_cpp和huggingface工具:
pip install llama_cpp_python
pip install huggingface-hub
2. 下载llama_cpp推理所需的gguf版本模型:
huggingface-cli download m4r1/selfrag_llama2_7b-GGUF selfrag_llama2_7b.q4_k_m.gguf --local-dir ./model --local-dir-use-symlinks False
3. 运行如下简单的测试代码:
from llama_cpp import Llama
_MODEL_KWARGS = {"logits_all": True, "n_ctx": 2048, "n_gpu_layers":200}
_GENERATE_KWARGS = {"temperature": 0.0,"top_p": 1.0,"max_tokens": 1024,"logprobs": 1000}

#模型
llm=Llama(model_path="./model/selfrag_llama2_7b.q4_k_m.gguf",**_MODEL_KWARGS)

#格式化Prompt,注意必须按照此格式输入问题和关联知识
def format_prompt(input, paragraph=None):
    prompt = "### Instruction:n{0}nn### Response:n".format(input)
    if paragraph is not None:
       prompt += "[Retrieval]{0}".format(paragraph)
    return prompt

#测试两个问题,一个无需检索,一个需要检索知识
query_1 = "写一首歌颂母爱的小诗"
query_2 = "能否介绍下字节跳动的AI平台Coze?"
queries = [query_1, query_2]

#分别测试,并打印出结果(response); details用来查看更详细的tokens输出细节
for query in queries:
    pred = llm(format_prompt(query),_GENERATE_KWARGS)
    print("nResponse: {0}".format(pred["choices"][0]["text"]))
    print(pred["choices"][0])
来看看第一个问题的输出:`Response: Mother love, so pu re and true,A bond that's stronger than any tie.[No Retrieval]You give your all, 【此处省略】......follow its owners everywhere.[Utility:5]` 第一个问题是创作型任务,不涉及具体事实,因此无需检索额外知识。推理结果中明确带上了[No Retrieval]标记。 再看第二个问题的输出: `Response: Certainly![Retrieval]Coze is a platform.[Utility:5]` 这是一个事实性问题。模型在推理过程中发现自己需要额外知识补充,于是主动输出了[Retrieval]标记。 4. 针对第二个问题模拟带入检索知识,再次观察模型输出: 这里我们模拟一段检索出来的知识,并通过`paragraph`参数带入`prompt`。只需修改少量代码即可:
paragraph="""Coze是字节跳动的大模型应用一站式开发平台。"""
...
#注意此处把输入参数paragraph默认成上面的知识
def format_prompt(input, paragraph=paragraph):
...
此时模型对第二个问题的输出如下: `Response: [Relevant]Coze is a platform developed by ByteDance, the parent company of TikTok, for building and deploying large-scale AI models.[Fully supported][Continue to Use Evidence]It provides an all-in-one development platform that includes tools for training, testing, and deploying AI models.[Utility:5]` 可以看到,模型根据提供的知识生成了输出,并且响应中嵌入了自省token标记。例如相关性的[Relevant]、支持度的[Fully supported],以及有用性的[Utility:5]。这些标记正是后续用于评分的关键指标。

构建Self-RAG应用

接下来,我们在上面测试的微调模型基础上,构建一个简单的上层应用,来实现完整的RAG流程。 有了前面的模型基础,这个应用的实现并不复杂。其中相对复杂的部分,是如何对多个增强生成的响应结果进行评分,从而选出“最优解”。评分算法在上一篇文章中已经介绍过:我们借助LLM输出的一个特殊信息——每个位置输出所有可能token及其概率值。主要涉及三个指标: * **知识相关度**:检索到的知识与输入问题的相关性。 * **响应支持度**:检索到的知识对最终输出的支持程度。 * **答案有用性**:最终输出的答案对输入问题是否有用。 上篇文章已给出了响应支持度的评分算法,答案有用性的实现与之类似,此处不再赘述。知识相关度的计算相对简单:
#相关度计算:[Relevant]输出的概率占比
def _relevance_score(pred_log_probs: Dict[str, float]) -> float:
    rel_prob = np.exp(float(pred_log_probs["[Relevant]"]))
    irel_prob = np.exp(float(pred_log_probs["[Irrelevant]"]))
    return rel_prob / (rel_prob + irel_prob)
有了这三个评分基础算法,我们再设计一个简单的查询引擎。其大致结构如下: 【主测试程序】 主程序的逻辑非常直接。我们构造几个简单的文本作为知识库,然后创建检索器与生成器来构造查询引擎,最后测试两个输入问题:
import os
from llama_index.llms.llama_cpp import LlamaCPP
from llama_index.core import Document, VectorStoreIndex
from llama_index.core.retrievers import VectorIndexRetriever
from pathlib import Path

#导入SelfRAGQueryEngine引擎
from selfrag_queryengine import SelfRAGQueryEngine

#模型参数,注意打开logits_all参数
_MODEL_KWARGS = {"logits_all": True, "n_ctx": 2048, "n_gpu_layers": -1}
_GENERATE_KWARGS = {"temperature": 0.0,"top_p": 1.0,"max_tokens": 1000,"logprobs": 32016,}

# 下载的selfrag_llama2_7b模型的保存目录
download_dir = "../../model"

# 创建测试文档,此处直接用documents构建,方便观察
documents = [
    Document(text="Xiaomi 14 is the latest smartphone released by Xiaomi. It adopts a new design concept, the body is lighter and thinner, equipped with the latest processor, and the performance is more powerful."),
    """此处省略了更多的Document对象"""
]

# 创建retriever
index = VectorStoreIndex.from_documents(documents)
retriever = VectorIndexRetriever(index=index,similarity_top_k=5)

# 创建llm(采用llama_cpp)
model_path = Path(download_dir) / "selfrag_llama2_7b.q4_k_m.gguf"
llm = LlamaCPP(model_path=str(model_path), model_kwargs=_MODEL_KWARGS, generate_kwargs=_GENERATE_KWARGS)

# 构造查询引擎,传入llm与retriever
query_engine = SelfRAGQueryEngine(llm, retriever)

# 查询一:无需检索的创作问题
response = query_engine.query("write a poem about beautiful sunset")

# 查询二:需要检索的事实性问题
response = query_engine.query("Tell me some truth about xiaomi 14 phone, especially about its battery and camera?")
【构建查询引擎】 核心组件是`SelfRAGQueryEngine`,其核心的`query`接口实现如下:
"""此处省略初始化代码"""
def query(self, query_str: str) -> str:
            #调用模型生成
            response = self.llm.complete(_format_prompt(query_str))
            answer = response.text

            #如果模型反馈需要检索
            if "[Retrieval]" in answer:

                #检索多个相关知识
                print_text("需要检索知识,开始检索...n", color="blue")
                documents = self.retriever.retrieve(query_str)
                print_text(f"共检索到 {len(documents)} 个相关知识n", color="blue")

                #用检索到的多个知识组装多个Prompt
                paragraphs = [
                    _format_prompt(query_str, document.node.text) for document in documents
                ]

                #重新生成并评估每个结果的评分
                print_text("=====开始:重新生成并评估====n", color="blue")
                llm_response_per_paragraph,paragraphs_final_score = 
                    self._regen_then_eval(paragraphs)
                print_text("===结束:重新生成并评估====n", color="blue")

                #选择评分最高的答案
                best_paragraph_id = max(
                    paragraphs_final_score, key=paragraphs_final_score.get
                )
                answer = llm_response_per_paragraph[best_paragraph_id]
                print_text(f"已选择最佳答案: {answer}n", color="blue")

            else:
                print_text("无需检索知识,直接输出答案n",color="green")

            #输出结果,此处需要去除答案中的特殊token
            answer = _postprocess_answer(answer)
            print_text(f"最终答案: {answer}n", color="green")
            return str(answer)
【实现_regen_then_eval】 这里有一个关键方法`_regen_then_eval`,用于对多个输入的Prompt进行生成与评分,最后返回响应及其评分:
def _regen_then_eval(self, paragraphs: List[str]) ->Tuple[Dict[int,str],Dict[int,float]]:
            """
            运行评判模块,对给定的段落进行生成,并评分。
            参数:
            paragraphs (List[str]): 包含要评分的段落的列表。
            返回:
            Tuple[Dict[int,str],Dict[int,float]]: 包含生成的结果和评分。
            """
            paragraphs_final_score = {}
            llm_response_text = {}

            for p_idx, paragraph in enumerate(paragraphs):
                #循环生成多个响应
                response = self.llm.complete(paragraph)
                pred = response.raw
                llm_response_text[p_idx] = response.text

                #从raw信息中取得tokens概率相关信息
                #top_logprobs保存了每个位置上输出每个token的概率
                logprobs = pred["choices"][0]["logprobs"]
                pred_log_probs = logprobs["top_logprobs"]

                # 计算isRel分数,相关度为第一个token,直接传入0
                isRel_score = _relevance_score(pred_log_probs[0])

                # 计算isSup分数
                isSup_score = _supported_score(logprobs["tokens"], pred_log_probs)

                # 计算isUse分数
                isUse_score = _useful_score(logprobs["tokens"], pred_log_probs)

                #最终得分
                paragraphs_final_score[p_idx] = (
                    isRel_score + isSup_score + 0.5 * isUse_score
                )

                print_text(
                    f"输入: {paragraph}n响应: {llm_response_text[p_idx]}n评分: {paragraphs_final_score[p_idx]}n",
                    color="blue",
                 )
                print_text(
                    f"已完成 {p_idx + 1}/{len(paragraphs)} 段落nn", color="blue"
                )

            return llm_response_text, paragraphs_final_score
【测试结果】 这样就构建了一个简单的Self-RAG应用。直接运行,就能看到两个测试问题的处理过程与区别: **问题一:** 由于是创作型问题,LLM判断无需检索,直接输出答案。 **问题二:** 这是一个需要基于事实回答的问题,LLM认为需要检索。检索到知识后,应用通过循环重新生成并评分。比如第一个知识段落生成的评分为`1.767657...`。在所有响应都被重新评估后,最终有一个评分最高的答案“脱颖而出”。 测试结果符合对Self-RAG的预期。

应用的优化思考

Self-RAG通过在模型层面进行微调,让LLM具备了自主判断、按需检索和自我反省的能力,这极大地降低了应用层面的复杂性,同时也没有损失模型本身的性能。当然,我们完全可以将RAG其他范式的优化方法结合起来,让Self-RAG运行得更完美。 在上面的原型应用中,有一个明显的优化点:Self-RAG的多次生成是基于检索出的top-K文档逐个进行的。但实际测试中,我们发现几个问题: * 由于检索出的文档已经按语义相似度排序,很多时候生成结果的评分与文档排序一致,这会让评估机制失去意义。 * 实际应用中的知识结构往往很复杂,很多时候需要将多个知识片段一次性输入给LLM,让它有更完整的“参考上下文”。 * 当前的实现没有采用并行的生成方式。 因此,要充分释放Self-RAG的自我评估潜力,需要根据实际需求优化检索策略。比如,多次检索知识,然后针对每次检索的结果分别进行生成与评估。这样做,既能给LLM提供更丰富的上下文知识,又能让Self-RAG的自省机制在多次生成中找到质量最高的输出。实现多次检索的策略可以很灵活: * 对查询进行重写(Rewrite)后再检索,可以使用不同的重写算法。 * 采用不同的检索算法(比如关键词检索和语义检索)来获取不同的知识文档。 * 检索后使用不同的重排序(Rerank)算法对知识文档进行重排,然后再进行多次生成。 更多的优化方法,还有待在实际应用中不断发现和完善。

模型的微调

上面我们依托`selfrag_llama2_7b`这个微调模型完成了各项测试。最后,我们来简单聊聊如何训练这样一个模型。 【训练目标】 核心目标是让LLM能够生成带有自省token(如`[Irrelevant]`或`[Relevant]`)的文本。 【训练方法】 为了实现这一目标,Self-RAG需要训练两个模型:一个是评估模型,一个是生成模型。两者都需要用自省token来扩展词汇表并进行训练。整个过程分为两个阶段:
  1. 评估模型训练
评估模型的训练目的是让它能根据指令和输入,直接生成不同类型的自省token。这需要大量的输入文本及其对应的自省token标签,显然无法靠人工完成。因此,Self-RAG借助GPT-4来批量生成训练数据。此阶段的步骤如下:
  • 训练数据创建:使用 GPT-4 来帮助生成训练数据。

  • 评估模型训练:使用生成的训练数据来训练评估模型。

2. 生成模型训练

评估模型训练完成后,会借助检索与训练好的评估模型,对大量输入-输出文本对插入自省token,形成“增强后”的输出文本,作为生成模型的训练数据。注意看下图中的两个例子,右边那个例子中,评估模型发现需要检索,因此插入了`[Retrieve]`、`[Relevant]`等标记token,形成了一条新的训练数据。 训练数据准备完成后,最后再将其用于模型训练。训练的目标不仅仅是让模型能预测下一个内容token,还要能预测自省token标记。 具体的训练数据生成代码、微调脚本等,可以参考Github上的Self-RAG项目。

END

来源:https://www.53ai.com/news/qianyanjishu/1208.html

相关热点

继续查看同栏目近期热点。

延伸阅读

补充最近整理过的热点入口。