ACP(七)优化RAG应用提升问答准确度

前言

在前面的课程中,你已经通过自动化评测发现答疑机器人的一些问题。优化提示词并不能解决检索召回不准确引起的回答错误问题,就像你在开卷考试时拿着错误的参考书很难给出正确的答案一样。

前文回顾

在上一章节,你发现答疑机器人不能很好的回答【张伟是哪个部门的?】。你可以通过这段代码复现问题。

python 复制代码
# 导入所需的依赖包
import os
os.environ["TRANSFORMERS_VERBOSITY"] = "error"
# 本章不使用transformer库,我们可以只关注transformer库中error级别的报警信息
from config.load_key import load_key
import logging
from llama_index.core import Settings, SimpleDirectoryReader, VectorStoreIndex, PromptTemplate
from llama_index.embeddings.dashscope import DashScopeEmbedding, DashScopeTextEmbeddingModels
from llama_index.llms.openai_like import OpenAILike
from llama_index.core.node_parser import (
    SentenceSplitter,
    SemanticSplitterNodeParser,
    SentenceWindowNodeParser,
    MarkdownNodeParser,
    TokenTextSplitter
)
from llama_index.core.postprocessor import MetadataReplacementPostProcessor
from langchain_community.llms.tongyi import Tongyi
from langchain_community.embeddings import DashScopeEmbeddings
from datasets import Dataset
from ragas import evaluate
from ragas.metrics import context_recall, context_precision, answer_correctness
from chatbot import rag
from IPython.display import display
python 复制代码
# 设置日志级别
logging.basicConfig(level=logging.ERROR)
python 复制代码
# 加载API密钥
load_key()
# 生产环境中请勿将 API Key 输出到日志中,避免泄露
print(f'''你配置的 API Key 是:{os.environ["DASHSCOPE_API_KEY"][:5]+"*"*5}''')
python 复制代码
# 配置通义千问大模型和文本向量模型
Settings.llm = OpenAILike(
    model="qwen-plus",
    api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    is_chat_model=True
)
python 复制代码
# 配置文本向量模型,设置批处理大小和最大输入长度
Settings.embed_model = DashScopeEmbedding(
    model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V3,
    embed_batch_size=6,
    embed_input_length=8192
)
python 复制代码
# 定义问答函数
def ask(question, query_engine):
    # 更新提示模板
    rag.update_prompt_template(query_engine=query_engine)

    # 输出问题
    print('=' * 50)  # 使用乘法生成分割线
    print(f'🤔 问题:{question}')
    print('=' * 50 + '\n')  # 使用乘法生成分割线

    # 获取回答
    response = query_engine.query(question)

    # 输出回答
    print('🤖 回答:')
    if hasattr(response, 'print_response_stream') and callable(response.print_response_stream):
        response.print_response_stream()
    else:
        print(str(response))

    # 输出参考文档
    print('\n' + '-' * 50)  # 使用乘法生成分割线
    print('📚 参考文档:\n')
    for i, source_node in enumerate(response.source_nodes, start=1):
        print(f'文档 {i}:')
        print(source_node)
        print()

    print('-' * 50)  # 使用乘法生成分割线

    return response
python 复制代码
query_engine = rag.create_query_engine(rag.load_index())
response = ask('张伟是哪个部门的', query_engine)
python 复制代码
==================================================
🤔 问题:张伟是哪个部门的
==================================================

🤖 回答:
根据提供的参考信息,没有找到名为张伟的员工信息。如果您能提供更多详细信息,比如部门名称或其他联系方式,我可以帮助您进一步查找。如果确实存在同名员工的情况,也请告知具体的工作内容或部门,以便准确回答您的问题。
--------------------------------------------------
📚 参考文档:

文档 1:
Node ID: e4bf6cef-a1c6-4060-9934-408a6a709d45
Text: 核,提供⾏政管理与协调⽀持,优化⾏政⼯作流程。  ⾏政部 秦⻜ 蔡静 G705 034 ⾏政 ⾏政专员 13800000034
qinf@educompany.com 维护公司档案与信息系统,负责公司通知及公告的发布,
Score:  0.193


文档 2:
Node ID: 311fbc80-e445-4399-871a-f7a3cd7a2aa5
Text: ⽀持。  绩效管理部 韩杉 李⻜ I902 041 ⼈⼒资源 绩效专员 13800000041
hanshan@educompany.com 建⽴并维护员⼯绩效档案,定期组织绩效评价会议,协调各部⻔反馈,制定考核流程与标准,确保绩效
Score:  0.189


--------------------------------------------------

你会发现,产生问题的原因是检索阶段没有召回到正确的参考信息(文档切片)。如何改进这个问题呢?你可以参考几个简单的改进策略,初步优化检索效果。

初步优化检索效果

正如前言提到的,你需要让大模型拿到正确的"参考书",才能给出正确的"答案"。因此,你可以尝试增加每次拿到"参考书"的数量(增加召回的文档切片数量),或者将"参考书中的知识点"整理成结构化的表格(文档内容结构化)。你可以先从前者入手:

让大模型获取到更多参考信息

既然知识库中存在张伟的任职信息,那么你可以通过增加一次性召回的文档切片数量的方式,从而扩大检索范围,提升找到相关信息的概率。在之前的代码里,你只召回了2个文档切片,现在,你可以将召回数量增加至5个,再次观察召回效果是否得到了提升。

调整代码

你可以通过以下设置,让检索引擎召回前5个最相关的文档切片

python 复制代码
index = rag.load_index()
query_engine = index.as_query_engine(
    streaming=True,
    # 一次检索出 5 个文档切片,默认为 2
    similarity_top_k=5
)
python 复制代码
response = ask('张伟是哪个部门的', query_engine)
python 复制代码
==================================================
🤔 问题:张伟是哪个部门的
==================================================

🤖 回答:
张伟是IT部的。
--------------------------------------------------
📚 参考文档:

文档 1:
Node ID: e4bf6cef-a1c6-4060-9934-408a6a709d45
Text: 核,提供⾏政管理与协调⽀持,优化⾏政⼯作流程。  ⾏政部 秦⻜ 蔡静 G705 034 ⾏政 ⾏政专员 13800000034
qinf@educompany.com 维护公司档案与信息系统,负责公司通知及公告的发布,
Score:  0.193


文档 2:
Node ID: 311fbc80-e445-4399-871a-f7a3cd7a2aa5
Text: ⽀持。  绩效管理部 韩杉 李⻜ I902 041 ⼈⼒资源 绩效专员 13800000041
hanshan@educompany.com 建⽴并维护员⼯绩效档案,定期组织绩效评价会议,协调各部⻔反馈,制定考核流程与标准,确保绩效
Score:  0.189


文档 3:
Node ID: b9e8dd24-6fb2-4ad9-9d7c-b3ba4170487c
Text: 效管理部 南 ⻜ 0 ⼒资源 效专员 0
责制定绩效考核体系,组织绩效评估的实施与反馈,撰写评估报告,分析绩效数据以提出优化建议,提供决策
Score:  0.176


文档 4:
Node ID: 392a1724-fb54-4380-8f33-b857af24b9a1
Text: ⽀持,确保⼈⼒资源部⻔顺畅运作。  ⾏政部 黎晗 蔡静 G704 033 ⾏政 ⾏政专员 13800000033
lih@educompany.com 负责采购办公设备与耗材,登记与管理公司固定资产,协助实施绩效考
Score:  0.164


文档 5:
Node ID: 3f0c4caa-d750-4c67-9f01-9c40d439e3f1
Text: 组织公司活动的前期准备与后期评估,确保公司各项⼯作的顺利进⾏。  IT部 张伟 ⻢云 H802 036 IT⽀撑 IT专员
13800000036 zhangwei036@educompany.com 进⾏公司⽹络及硬件设备的配置
Score:  0.161


--------------------------------------------------

可以看到:在调整了召回数量后,你的答疑机器人能够回答【张伟是哪个部门?】了,这是因为召回的文档切片已经包含了张伟和他的部门信息。

不过单纯增加召回的切片数量并不是一个好方法。想想看,如果这种方法能解决问题,那么不如召回整个知识库,这样不会遗漏任何的信息...可是这不仅会超出大模型的输入长度限制,过多的无关信息还会降低大模型回答的效率和准确性。

而且,事实上你们公司可能有很多叫张伟的同事,这会导致一个问题:当用户问"张伟是哪个部门的"时候,系统无法确定用户想问的是哪一个张伟。如果只是简单地增加召回数量,可能会召回多个张伟的信息,但系统仍然无法准确判断应该返回哪个张伟 的信息。因此,我们还需要用其他方法来进一步改进RAG效果。

评估改进效果

为了在接下来的改进中,能够量化改效果,你可以继续使用上一个章节中的Ragas来进行评测。假设你们公司有三名叫张伟的同事,他们分别在教研部、课程开发部、IT部。

python 复制代码
# 定义评估函数
def evaluate_result(question, response, ground_truth):
    # 获取回答内容
    if hasattr(response, 'response_txt'):
        answer = response.response_txt
    else:
        answer = str(response)
    # 获取检索到的上下文
    context = [source_node.get_content() for source_node in response.source_nodes]

    # 构造评估数据集
    data_samples = {
        'question': [question],
        'answer': [answer],
        'ground_truth':[ground_truth],
        'contexts' : [context],
    }
    dataset = Dataset.from_dict(data_samples)

    # 使用Ragas进行评估
    score = evaluate(
        dataset = dataset,
        metrics=[answer_correctness, context_recall, context_precision],
        llm=Tongyi(model_name="qwen-plus"),
        embeddings=DashScopeEmbeddings(model="text-embedding-v3")
    )
    return score.to_pandas()
python 复制代码
# 定义评估函数
def evaluate_result(question, response, ground_truth):
    # 获取回答内容
    if hasattr(response, 'response_txt'):
        answer = response.response_txt
    else:
        answer = str(response)
    # 获取检索到的上下文
    context = [source_node.get_content() for source_node in response.source_nodes]

    # 构造评估数据集
    data_samples = {
        'question': [question],
        'answer': [answer],
        'ground_truth':[ground_truth],
        'contexts' : [context],
    }
    dataset = Dataset.from_dict(data_samples)

    # 使用Ragas进行评估
    score = evaluate(
        dataset = dataset,
        metrics=[answer_correctness, context_recall, context_precision],
        llm=Tongyi(model_name="qwen-plus"),
        embeddings=DashScopeEmbeddings(model="text-embedding-v3")
    )
    return score.to_pandas()
python 复制代码
evaluate_result(question=question, response=response, ground_truth=ground_truth)

可以看到,当前的RAG系统还无法高效地运行,召回的文档 切片在存在了无关信息,且有效信息也没有被完全召回,最终形成的答案并不正确。你需要考虑其他改进策略。

给大模型结构更清晰的参考信息

在实际应用中,文档 的组织结构对检索效果有着重要影响。想象一下,同样的信息,放在一个结构清晰的表格中和散落在一段普通文字里,哪个更容易查找和理解?显然是前者。

大语言模型也是如此。当把原本在表格中的信息转换成普通文本时,虽然信息本身没有丢失,但结构性却降低了。这就像是把一个整齐的抽屉变成了一堆散落的物品,虽然东西都在,但查找起来就没那么方便了。

重建索引

Markdown格式是一个很好的选择,因为它:

  • 结构清晰,层次分明
  • 语法简单,易于阅读和维护
  • 特别适合RAG(检索增强生成)场景下的文档组织

为了验证结构化文档的效果,课程准备了一份经过优化的Markdown格式文件。接下来,你将:

  1. 把这份Markdown文件添加到docs目录
  2. 重新建立索引
  3. 测试检索效果的提升
python 复制代码
# 将 markdown 格式的员工信息文档复制到 ./docs 目录下
! mkdir -p ./docs/2_5
! cp ./resources/2_4/内容公司各部门职责与关键角色联系信息汇总.md ./docs/2_5
python 复制代码
print('=' * 50)
print('📂 正在加载文档...')
print('=' * 50 + '\n')

# 加载文档
documents = SimpleDirectoryReader('./docs/2_5').load_data()
print(f'✅ 文档加载完成。\n')

print('=' * 50)
print('🛠️ 正在重建索引...')
print('=' * 50 + '\n')

# 重建索引
index = VectorStoreIndex.from_documents(
    documents
)
print('✅ 索引重建完成!')

print('=' * 50)
python 复制代码
query_engine = index.as_query_engine(
    streaming=True,
    similarity_top_k=5
)
python 复制代码
response = ask('张伟是哪个部门的', query_engine)
python 复制代码
==================================================
🤔 问题:张伟是哪个部门的
==================================================

🤖 回答:
张伟是IT部的。
--------------------------------------------------
📚 参考文档:

文档 1:
Node ID: e4bf6cef-a1c6-4060-9934-408a6a709d45
Text: 核,提供⾏政管理与协调⽀持,优化⾏政⼯作流程。  ⾏政部 秦⻜ 蔡静 G705 034 ⾏政 ⾏政专员 13800000034
qinf@educompany.com 维护公司档案与信息系统,负责公司通知及公告的发布,
Score:  0.193


文档 2:
Node ID: 311fbc80-e445-4399-871a-f7a3cd7a2aa5
Text: ⽀持。  绩效管理部 韩杉 李⻜ I902 041 ⼈⼒资源 绩效专员 13800000041
hanshan@educompany.com 建⽴并维护员⼯绩效档案,定期组织绩效评价会议,协调各部⻔反馈,制定考核流程与标准,确保绩效
Score:  0.189


文档 3:
Node ID: b9e8dd24-6fb2-4ad9-9d7c-b3ba4170487c
Text: 效管理部 南 ⻜ 0 ⼒资源 效专员 0
责制定绩效考核体系,组织绩效评估的实施与反馈,撰写评估报告,分析绩效数据以提出优化建议,提供决策
Score:  0.176


文档 4:
Node ID: 392a1724-fb54-4380-8f33-b857af24b9a1
Text: ⽀持,确保⼈⼒资源部⻔顺畅运作。  ⾏政部 黎晗 蔡静 G704 033 ⾏政 ⾏政专员 13800000033
lih@educompany.com 负责采购办公设备与耗材,登记与管理公司固定资产,协助实施绩效考
Score:  0.164


文档 5:
Node ID: 3f0c4caa-d750-4c67-9f01-9c40d439e3f1
Text: 组织公司活动的前期准备与后期评估,确保公司各项⼯作的顺利进⾏。  IT部 张伟 ⻢云 H802 036 IT⽀撑 IT专员
13800000036 zhangwei036@educompany.com 进⾏公司⽹络及硬件设备的配置
Score:  0.161


--------------------------------------------------
评估改进效果

可以看到,你的答疑机器人能够准确回答这个问题。你可以再运行一次 Ragas 评测,评测数据同样显示:回答准确度更高了

python 复制代码
evaluate_result(question=question, response=response, ground_truth=ground_truth)

熟悉RAG的工作流程

截至目前,你已经完成了一些改进,让答疑机器 人的问答准确更高了。但在实际生产环境中,你可能会遇到的问题远不止于此。之前你已经了解了一些RAG的工作流程,在这里你可以回顾一下重要的步骤,方便你发现新的改进点:

RAG(Retrieval Augmented Generation,检索增强生成)是一种结合了信息检索和生成式模型的技术,能够在生成答案时利用外部知识库的相关信息。它的工作流程可以分为几个关键步骤:解析与切片、向量存储、检索召回、生成答案等。具体的概念你可以回顾"扩展答疑机器人的知识范围"这一节。

接下来,将从 RAG 中的每一个环节入手,尝试优化 RAG 的效果。

RAG应用各个环节与改进策略

文档准备阶段

在传统的客服阶段中,客服人员会根据用户所提问题,积累知识库,并共享给其他客服人员参考。在构建RAG应用时,这一过程同样不可缺少。

  • 意图空间:我们可以把用户提问背后中的需求绘制成点,这些点组成了一个用户意图空间。
  • 知识空间:而你沉淀在知识库文档 中的知识点,则构成了组成一个知识空间。这里的知识点,可以是一个段落、或者一个章节。

当我们将意图空间和知识空间投影到一起,会发现两个空间存在交集与差异。这些区域分别对应了我们后续的三个优化策略:

  1. 重叠区域:
    • 即可以依靠知识库的内容来回答用户问题的部分,这是RAG应用效果保障的基础。
    • 对于这部分用户意图,你可以通过优化内容质量、优化工程和算法,不断地提升回答质量。
  2. 未被覆盖的意图空间
    • 因为缺乏知识库内容的支撑,大模型型容易输出"幻觉"回答,例如,公司新增了一个"数据分析部",但知识库中没有相关文档,不论如何改进工程算法,RAG应用都无法准确回答这一问题。
    • 你需要做的是主动补充缺漏的知识,不断跟进用户意图空间的变化
  3. 未被利用的知识空间:
    • 召回不相关知识点可能会干扰大模型的回答。
    • 因此,需要你召回算法避免召回无关内容 。此外,你还需要定期查验知识库,剔除无关内容

在尝试优化工程或算法之前,你应该优先构建一套可以持续收集用户意图的机制。通过系统化采集真实用户需求来完善知识库内容,并邀请对用户意图有深刻理解的领域专家参与效果评估,形成"数据采集-知识更新-专家验证"的闭环优化流程,保障 RAG 应用的效果。

当你准备好这些,就可以进一步优化 RAG 应用的各个环节了

问题分类及改进策略

在文档解析与切片阶段,你可能会遇到以下问题:

类别 细分类型 改进策略 场景化示例
文档解析 文档类型不统一,部分格式的文档不支持解析 比如前面用到的 SimpleDirectoryLoader 并不支持 Keynote 格式的文件 开发对应格式的解析器,或转换文档格式 例如,某公司使用了大量的 Keynote 文件存储员工信息,但现有的解析器不支持 Keynote 格式。可以开发 Keynote 解析器或将文件转换为支持的格式(如 PDF)。
文档解析 已支持解析的文档格式里,存在一些特殊内容 比如文档里嵌入了表格、图片、视频等 改进文档解析器 例如,某文档中包含了大量的表格和图片,现有解析器无法正确提取表格中的信息。可以改进解析器,使其能够处理表格和图片。
文档解析 ... ... ...
文档切片 文档中有很多主题接近的内容 比如工作手册文档中,需求分析、开发、发布等每个阶段都有注意事项、操作指导 扩写文档标题及子标题 「注意事项」=>「需求分析>注意事项」 建立文档元数据(打标) 例如,某文档中包含多个阶段的注意事项,用户提问"需求分析的注意事项是什么?"时,系统返回了所有阶段的注意事项。可以通过扩展标题和打标来区分不同阶段的内容。
文档切片 文档切片长度过大,引入过多干扰项 减少切片长度,或结合具体业务开发为更合适的切片策略 例如,某文档的切片长度过大,包含了多个不相关的主题,导致检索时返回了无关信息。可以减少切片长度,确保每个切片只包含一个主题。
文档切片 文档切片长度过短,有效信息被截断 扩大切片长度,或结合具体业务开发为更合适的切片策略 例如,某文档中每个切片只有一句话,导致检索时无法获取完整的上下文信息。可以增加切片长度,确保每个切片包含完整的上下文。
文档切片 ... ... ...
借助百炼解析 PDF 文件

在前面的学习过程中,为了让你更快地看到格式转换带来的效果,本课程直接提供了一份从 PDF 转换的 Markdown 格式文档。但在实际工作中,编写代码将 PDF 妥善地转为 Markdown 并非易事。

实际工作中,你也可以借助百炼提供的 DashScopeParse 来完成 PDF、Word 等格式的文件解析。DashScopeParse 背后使用了阿里云的文档智能服务,能够帮助你从 PDF、Word 等格式的文件中识别文档中的图片、提取出结构化的文本信息。

python 复制代码
from llama_index.readers.dashscope.utils import ResultType
from llama_index.readers.dashscope.base import DashScopeParse
import os
import json
import nest_asyncio
python 复制代码
nest_asyncio.apply()
# 使用环境变量
os.environ['DASHSCOPE_API_KEY'] = os.getenv('DASHSCOPE_API_KEY')
python 复制代码
# 创建一个静默的日志记录器来替换原始的 logger
silent_logger = logging.getLogger(__name__)
# 设置日志级别为 ERROR,以避免输出无关信息。如果您需要查看更详细的日志信息,请设置为 INFO
silent_logger.setLevel(logging.ERROR)

class SilentDashScopeParse(DashScopeParse):
    def __init__(self, *args, **kwargs):
        # 替换所有相关模块的 logger
        import llama_index.readers.dashscope.base as base_module
        import llama_index.readers.dashscope.domain.lease_domains as lease_domains_module
        import llama_index.readers.dashscope.utils as utils_module

        base_module.logger = silent_logger
        lease_domains_module.logger = silent_logger
        utils_module.logger = silent_logger

        # 调用父类初始化
        super().__init__(*args, **kwargs)
python 复制代码
# 文件通过DashScopeParse接口解析为程序与大模型易于处理的markdown文本
def file_to_md(file, category_id):
    parse = SilentDashScopeParse(
        result_type=ResultType.DASHSCOPE_DOCMIND,
        category_id=category_id
    )
    documents = parse.load_data(file_path=file)
    # 初始化一个空字符串来存储Markdown内容
    markdown_content = ""
    for doc in documents:
        doc_json = json.loads(json.loads(doc.text))
        for item in doc_json["layouts"]:
            if item["text"] in item["markdownContent"]:
                markdown_content += item["markdownContent"]
            else:
                # DashScopeParse处理时,会将文档图片内的文本信息也解析到初始markdown文本中(类似OCR),这对于本文示例文件中的命令行截图、文本截图是足够的,示例无需深层次解析图片。
                # 实际场景中的知识库文档,如果涉及不规则、复杂信息的图片并且需要深层次理解图片内容,您可以调用视觉模型进一步理解图片含义。
                # (DashScopeParse返回的数据结构中,针对图片数据,markdownContent字段是图片url,text字段是解析出的文本)
                # if ".jpg" in item["markdownContent"] or ".jpeg" in item["markdownContent"] or ".png" in item["markdownContent"]:
                #     image_url = re.findall(r'\!\[.*?\]\((https?://.*?)\)', item["markdownContent"])[0]
                #     print(image_url)
                #     markdown_content = markdown_content + parse_image_to_text(image_url)+"\n"
                # else:
                #     markdown_content = markdown_content + item["text"]+"\n"
                markdown_content = markdown_content + item["text"]+"\n"
    return markdown_content

### 调用示例

# 1、可选配置。
# 百炼平台上,可以对不同项目配置不同的业务空间,默认情况下是使用默认业务空间。
# 如果需要使用非默认空间,可以前往[百炼控制台-业务空间管理](https://bailian.console.aliyun.com/?admin=1#/efm/business_management),配置业务空间并获取Workspace ID。
# 完成后,取消注释并修改这段代码为实际值:
# os.environ['DASHSCOPE_WORKSPACE_ID'] = "<Your Workspace id, Default workspace is empty.>"

# 2、可选配置。
# 文件通过DashScopeParse进行解析时,需要配置上传的数据目录id。可以前往[百炼控制台-数据管理](https://bailian.console.aliyun.com/#/data-center),配置类目并获取ID
category_id="default" # 建议修改为自定义的类目ID,以便分类管理文件

md_content = file_to_md(['./docs/内容公司各部门职责与关键角色联系信息汇总.pdf'], category_id)
print("解析后的Markdown文本:")
print("-"*100)
print(md_content)

由于pdf/docx等多种文件格式来源的多样性,文件解析到markdown过程中可能存在一些格式上的小问题,比如 PDF 里的跨页表格行可能被解析为多行。

可以使用大模型对生成的markdown文本进行润色,修正目录层级、缺失信息等。

python 复制代码
from dashscope import Generation
python 复制代码
def md_polisher(data):
    messages = [
        {'role': 'user', 'content': '下面这段文本是由pdf转为markdown的,格式和内容可能存在一些问题,需要你帮我优化下:\n1、目录层级,如果目录层级顺序不对请以markdown形式补全或修改;\n2、内容错误,如果存在上下文不一致的情况,请你修改下;\n3、如果有表格,注意上下行不一致的情况;\n4、输出文本整体应该与输入没有较大差异,不要自己制造内容,我是需要对原文进行润色;\n4、输出格式要求:markdown文本,你的所有回答都应该放在一个markdown文件里面。\n特别注意:只输出转换后的 markdown 内容本身,不输出任何其他信息。\n需要处理的内容是:' + data}
    ]
    response = Generation.call(
        model="qwen-plus",
        messages=messages,
        result_format='message',
        stream=True,
        incremental_output=True
    )
    result = ""
    print("润色后的Markdown文本:")
    print("-"*100)
    for chunk in response:
        print(chunk.output.choices[0].message.content, end='')
        result += chunk.output.choices[0].message.content

    return(result)
python 复制代码
md_polisher(md_content)
使用多种文档切片方法

在文档切片的过程中,切片方式会影响检索召回的效果。让我们通过具体例子来了解不同切片方法的特点。首先创建一个通用的评测函数

python 复制代码
def evaluate_splitter(splitter, documents, question, ground_truth, splitter_name):
    """评测不同文档切片方法的效果"""
    print(f"\n{'='*50}")
    print(f"🔍 正在使用 {splitter_name} 方法进行测试...")
    print(f"{'='*50}\n")

    # 构建索引
    print("📑 正在处理文档...")
    nodes = splitter.get_nodes_from_documents(documents)
    index = VectorStoreIndex(nodes, embed_model=Settings.embed_model)

    # 创建查询引擎
    query_engine = index.as_query_engine(
        similarity_top_k=5,
        streaming=True
    )

    # 执行查询
    print(f"\n❓ 测试问题: {question}")
    print("\n🤖 模型回答:")
    response = query_engine.query(question)
    response.print_response_stream()

    # 输出参考片段
    print(f"\n📚 {splitter_name} 召回的参考片段:")
    for i, node in enumerate(response.source_nodes, 1):
        print(f"\n文档片段 {i}:")
        print("-" * 40)
        print(node)

    # 评估结果
    print(f"\n📊 {splitter_name} 评估结果:")
    print("-" * 40)
    display(evaluate_result(question, response, ground_truth))

接下来,让我们看看各种切片方法的特点和示例:

Token切片

适合对Token数量有严格要求的场景,比如使用上下文长度较小的模型时。

示例文本:"LlamaIndex是一个强大的RAG框架。它提价了多种文档处理方式。用户可以根据需要选择合适的方法。"

使用Token切片(chunk_size=10)后可能的结果:

  • 切片1:"LlamaIndex是一个强大的RAG"
  • 切片2:"框架。它提供了多种文"
  • 切片3:"档处理方式。用户可以"
python 复制代码
token_splitter = TokenTextSplitter(
    chunk_size=1024,
    chunk_overlap=20
)
evaluate_splitter(token_splitter, documents, question, ground_truth, "Token")
句子切片

这是默认的切片策略,会保持句子的完整性。

同样的文本使用兔子切片后:

  • 切片1:"LlamaIndex是一个强大的RAG框架"
  • 切片2:"它提供了多种文档处理方式"
  • 切片3:"用户可以根据需求选择合适的方法"
python 复制代码
sentence_splitter = SentenceSplitter(
    chunk_size=512,
    chunk_overlap=50
)
evaluate_splitter(sentence_splitter, documents, question, ground_truth, "Sentence")
句子窗口切片

每个切片都包含周围的句子作为上下文窗口。

示例文本使用句子窗口切片(window_size=1)后:

  • 切片1:"LlamaIndex是一个强大的RAG框架" 上下文:"它提供了多种文档处理方式"
  • 切片2:"它提供了多种文档处理方式" 上下文:"LlamaIndex是一个强大的RAG框架,用户可以根据需求选择合适的方法"
  • 切片3:"用户可以根据需求选择合适的方法" 上下文:"它提供了多种文档处理方式"
python 复制代码
sentence_window_splitter = SentenceWindowNodeParser.from_defaults(
    window_size=3,
    window_metadata_key="window",
    original_text_metadata_key="original_text"
)
# 注意:句子窗口切片需要特殊的后处理器
query_engine = index.as_query_engine(
    similarity_top_k=5,
    streaming=True,
    node_postprocessors=[MetadataReplacementPostProcessor(target_metadata_key="window")]
)
evaluate_splitter(sentence_window_splitter, documents, question, ground_truth, "Sentence Window")
语义切片

根据语义相关性自适应地选择切片点

示例文本: "LlamaIndex是一个强大的RAG框架。它提供了多种文档处理方式。用户可以根据需求选择合适的方法。此外,它还支持向量检索。这种检索方式非常高效。"

语义切片可能的结果:

  • 切片1: "LlamaIndex是一个强大的RAG框架。它提供了多种文档处理方式。用户可以根据需求选择合适的方法。"
  • 切片2: "此外,它还支持向量检索。这种检索方式非常高效。" (注意这里是按语义相关性分组的)
python 复制代码
semantic_splitter = SemanticSplitterNodeParser(
    buffer_size=1,
    breakpoint_percentile_threshold=95,
    embed_model=Settings.embed_model
)
evaluate_splitter(semantic_splitter, documents, question, ground_truth, "Semantic")
Markdown 切片 ¶

专门针对 Markdown 文档优化的切片方法。

示例 Markdown 文本:

复制代码
# RAG框架
LlamaIndex是一个强大的RAG框架。

## 特点
- 提供多种文档处理方式
- 支持向量检索
- 使用简单方便

### 详细说明
用户可以根据需求选择合适的方法。

Markdown切片会根据标题层级进行智能分割:

切片1: "# RAG框架\nLlamaIndex是一个强大的RAG框架。"

切片2: "## 特点\n- 提供多种文档处理方式\n- 支持向量检索\n- 使用简单方便"

切片3: "### 详细说明\n用户可以根据需求选择合适的方法。"

python 复制代码
markdown_splitter = MarkdownNodeParser()
evaluate_splitter(markdown_splitter, documents, question, ground_truth, "Markdown")

在实际应用中,选择切片方法时不必过于纠结,你可以这样思考:

  • 如果你刚开始接触RAG,建议先使用默认的句子切片方法,它在大多数场景下都能提价不错效果
  • 当你发现检索结果不够理想时,可以尝试:
    • 处理长文档且需要保持上下文?试试句子窗口切片
    • 文档逻辑性强、内容专业?语义切片可能会有帮助
    • 模型总是报Token超限?Token切片可以帮你精确控制
    • 处理Markdown文档?别忘了有专门的Markdow切片

没有最好的切片方法,只有最适合你场景的方法。你可以尝试不同的切片方法,观察Ragas评估结果,找到最适合你的需求的方案。学习的过程就是不断尝试和调整的过程!

切片向量化与存储阶段

文档切片后,你还需要对其建立索引,以便后续检索。一个常见的方案使用嵌入(Embedding)模型将切片向量化,并存储到到向量数据库中。

在这一阶段,你需要选择合适的Embedding模型以及向量数据库,这对于提升检索效果至关重要

了解Embedding与向量化

Embedding模型可以将文本转换为高维向量,用于表示文本语义,相似的文本会映射到相近的向量上,检索时可以根据问题的向量找到相似度高的文档切片。

平面坐标系中的有向线段是2维向量。例如,从原点(0,0)到A(xa,ya)的有向线段可以称为向量A。向量A与向量B之前的夹角越小,也就意味着其相似度越高。

python 复制代码
import numpy as np

def cosine_similarity(a, b):
    """余弦相似度"""
    return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))

# 示例向量
a = np.array([0.2, 0.8])
b = np.array([0.3, 0.7])
c = np.array([0.8, 0.2])

print(f"A 与 B 的余弦相似度: {cosine_similarity(a, b)}")
print(f"B 与 C 的余弦相似度: {cosine_similarity(b, c)}")
python 复制代码
A 与 B 的余弦相似度: 0.987241120712647
B 与 C 的余弦相似度: 0.6050832675335579
选择合适的Embedding模型

不同的Embedding模型对相同的几段文字进行计算时,得到的向量可能会完全不同。通常越新的Embedding模型,其表现越好。例如前文中使用的是阿里云百炼上提供的text-embedding-v2。如果换成更新的版本text-embedding-v3你会发现即使不去做前面的优化,检索效果也会有一定的提升。

比如运行下面的代码可以看到,不同版本的Embedding模型,对于【张伟是哪个部门的】这个问题和不同文档切片的相似度也是不同的。

python 复制代码
def compare_embeddings(query, chunks, embedding_models):
    """比较不同嵌入模型的文本相似度

    Args:
        query: 查询文本
        chunks: 待比较的文本片段列表
        embedding_models: 嵌入模型字典,格式为 {模型名称: 模型实例}
    """
    # 打印输入文本
    print(f"查询: {query}")
    for i, chunk in enumerate(chunks, 1):
        print(f"文本 {i}: {chunk}")

    # 计算并显示每个模型的相似度结果
    for model_name, model in embedding_models.items():
        print(f"\n{'='*20} {model_name} {'='*20}")
        query_embedding = (model.get_query_embedding(query) if hasattr(model, 'get_query_embedding')
                         else model.get_text_embedding(query))

        for i, chunk in enumerate(chunks, 1):
            chunk_embedding = model.get_text_embedding(chunk)
            similarity = cosine_similarity(query_embedding, chunk_embedding)
            print(f"查询与文本 {i} 的相似度: {similarity:.4f}")

# 准备测试数据
query = "张伟是哪个部门的"
chunks = [
    "核,提供⾏政管理与协调⽀持,优化⾏政⼯作流程。 ⾏政部 秦⻜ 蔡静 G705 034 ⾏政 ⾏政专员 13800000034 qinf@educompany.com 维护公司档案与信息系统,负责公司通知及公告的发布",
    "组织公司活动的前期准备与后期评估,确保公司各项⼯作的顺利进⾏。 IT部 张伟 ⻢云 H802 036 IT⽀撑 IT专员 13800000036 zhangwei036@educompany.com 进⾏公司⽹络及硬件设备的配置"
]

# 定义要测试的嵌入模型
embedding_models = {
    "text-embedding-v2": DashScopeEmbedding(model_name="text-embedding-v2"),
    "text-embedding-v3": DashScopeEmbedding(model_name="text-embedding-v3")
}

# 执行比较
compare_embeddings(query, chunks, embedding_models)
python 复制代码
查询: 张伟是哪个部门的
文本 1: 核,提供⾏政管理与协调⽀持,优化⾏政⼯作流程。 ⾏政部 秦⻜ 蔡静 G705 034 ⾏政 ⾏政专员 13800000034 qinf@educompany.com 维护公司档案与信息系统,负责公司通知及公告的发布
文本 2: 组织公司活动的前期准备与后期评估,确保公司各项⼯作的顺利进⾏。 IT部 张伟 ⻢云 H802 036 IT⽀撑 IT专员 13800000036 zhangwei036@educompany.com 进⾏公司⽹络及硬件设备的配置

==================== text-embedding-v2 ====================
查询与文本 1 的相似度: 0.3692
查询与文本 2 的相似度: 0.2580

==================== text-embedding-v3 ====================
查询与文本 1 的相似度: 0.4946
查询与文本 2 的相似度: 0.5603

除了通过相似度对比来评估不同Embedding模型的效果,你可以从实际应用的角度来评测。下面你将使用使用Ragas评测工具来对比text-embedding-v2和text-embedding-v3两个模型在RAG系统中的实际表现。

通过运行以下代码,你可以清晰地看到在相同的RAG策略下,text-embedding-v3模型的整体效果要优化text-embedding-v2。

python 复制代码
def compare_embedding_models(documents, question, ground_truth, sentence_splitter):
    """比较不同嵌入模型在RAG中的表现

    Args:
        documents: 文档列表
        question: 查询问题
        ground_truth: 标准答案
        sentence_splitter: 文本分割器
    """
    # 文档分割
    print("📑 正在处理文档...")
    nodes = sentence_splitter.get_nodes_from_documents(documents)

    # 定义要测试的嵌入模型配置
    embedding_models = {
        "text-embedding-v2": DashScopeEmbedding(
            model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V2
        ),
        "text-embedding-v3": DashScopeEmbedding(
            model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V3,
            embed_batch_size=6,
            embed_input_length=8192
        )
    }

    # 测试每个模型
    for model_name, embed_model in embedding_models.items():
        print(f"\n{'='*50}")
        print(f"🔍 正在测试 {model_name}...")
        print(f"{'='*50}")

        # 构建索引和查询引擎
        index = VectorStoreIndex(nodes, embed_model=embed_model)
        query_engine = index.as_query_engine(streaming=True, similarity_top_k=5)

        # 执行查询
        print(f"\n❓ 测试问题: {question}")
        print("\n🤖 模型回答:")
        response = query_engine.query(question)
        response.print_response_stream()

        # 显示召回的文档片段
        print(f"\n📚 召回的参考片段:")
        for i, node in enumerate(response.source_nodes, 1):
            print(f"\n文档片段 {i}:")
            print("-" * 40)
            print(node)

        # 评估结果
        print(f"\n📊 {model_name} 评估结果:")
        print("-" * 40)
        evaluation_score = evaluate_result(question, response, ground_truth)
        display(evaluation_score)

# 准备测试数据
documents = SimpleDirectoryReader('./docs/2_5').load_data()
sentence_splitter = SentenceSplitter(
    chunk_size=1000,
    chunk_overlap=200,
)

# 执行比较
compare_embedding_models(
    documents=documents,
    question=question,
    ground_truth=ground_truth,
    sentence_splitter=sentence_splitter
)


你可以看到:

  • 新版本的Embedding模型通常能带来更好的效果(如text-embedding-v3比v2表现更好)
  • 在实践中,单纯升级Embedding模型就可能显著提升检索质量
  • 建议你首先尝试最新的text-embedding-v3模型,它在大多数任务上都能取得不错的效果。同时可以持续关注DashscopeEmbedding新模型的进展,根据实际需求选择升级到性能更好的版本。
选择合适的向量数据库

在构建RAG应用时,你有多种向量存储方案可以选择,从简单到复杂依次是:

内存向量存储

最简单的方式是使用LlamaIndex内置的内存向量存储。只需安装llama-index包无需额外配置,就能快速开发和测试RAG应用。

python 复制代码
在这里插入代码片

优点是快速上手,适合开发测试;缺点是数据无法持久化,且受限于内存大小。

本地向量数据库

当数据量增大时,可以使用开源的向量数据库,如Milvus、Qdrant等。这些数据库提供了数据持久化和高效检索能力。

优点是功能完整、可控性强;缺点是需要自行部署维护。

云服务向量存储

对于生产环境,推荐使用云服务提供的向量存储能力。阿里云提供了多种选择:

  • 向量检索服务(DashVector):按量付费、自动扩容,适合快速启动项目。
  • 向量检索服务Milvus版:兼容开源Milvus,便于迁移已有应用
  • 已有数据库的向量能力:如果已使用阿里云数据库(RDS、PolarDB等)

云服务的优势在于:

  • 无需关注运维,自动扩容
  • 提供完善的监控和管理工具
  • 按量付费,成本可控
  • 支持向量+标量的混合检索,提升检索准确性

选择建议:

  1. 开发测试时使用内存向量存储
  2. 小规模应用可以使用本地向量数据库
  3. 生产环境推荐使用云服务,可根据具体需求选择合适的服务类型

DashVector 中支持 age、name 等标签过滤 + 向量相似度检索

检索召回阶段

检索阶段会遇到的主要问题就是,很难从众多文档切片中,找出和用户问题最相关、且包含正确答案信息的片段。

从切入时机来看,可以将解法分为两磊类:

  1. 在执行检索前,很多用户问题描述是不完整、甚至有歧义的,你需要想办法还原用户真实意图、以便提升检索效果。
  2. 在执行检索后,你可能会发现存在一些无关的信息,需要想办法减少无关信息,避免干扰下一步的答案生成。
时机 改进策略 示例
检索前 问题改写 「附近有好吃的餐厅吗?」=> 「请推荐我附近的几家评价较高的餐厅」
检索前 问题扩写 通过增加更多信息,让检索结果更全面 「张伟是哪个部门的?」=> 「张伟是哪个部门的?他的联系方式、职责范围、工作目标是什么?」
检索前 基于用户画像扩展上下文 结合用户信息、行为等数据扩写问题 内容工程师提问「工作注意事项」=> 「内容工程师有哪些工作注意事项」 项目经理提问「工作注意事项」=> 「项目经理有哪些工作注意事项」
检索前 提取标签 提取标签,用于后续标签过滤+向量相似度检索 「内容工程师有哪些工作注意事项」=> * 标签过滤:{"岗位": "内容工程师"} * 向量检索:「内容工程师有哪些工作注意事项」
检索前 反问用户 「工作职责是什么」=> 大模型反问:「请问你想了解哪个岗位的工作职责」 实现反问的提示词可以参考: 10分钟构建能主动提问的智能导购
检索前 思考并规划多次检索 「张伟不在,可以找谁」 => 大模型思考规划: => task_1:张伟的职责是什么, task_2:${task_1_result}职责的人有谁 => 按顺序执行多次检索
检索前 ... ...
检索后 重排序 ReRank + 过滤 多数向量数据库会考虑效率,牺牲一定精确度,召回的切片中可能有一些实际相关性不够高 chunk1、chunk2...、chunk10 => chunk 2、chunk4、chunk5
检索后 滑动窗口检索 在检索到一个切片后,补充前后相邻的若干个切片。这样做的原因是:相邻切片之间往往存在语义联系,仅看单个切片可能会丢失重要信息。 滑动窗口检索确保了不会因为过度切分而丢失文本间的语义连接。 常见的实现是句子滑动窗口,你可以用下方的简化形式来理解: 假设原始文本为:ABCDEFG(每个字母代表一个句子) 当检索到切片:D 补充相邻切片后:BCDEF(前后各取2个切片) 这里的BC和EF是D的上下文。比如: * BC可能包含解释D的背景信息 * EF可能包含D的后续发展或结果 * 这些上下文信息能帮助你更准确地理解D的完整含义 通过召回这些相关的上下文切片,你可以提高检索结果的准确性和完整性。
检索后 ... ...
问题改写

🤔为什么需要问题改写?

想象一下你在搜索"找张伟"或者"张伟 部门"这样的关键词。看似简单,但对于RAG系统来说,这样零散的搜索词可能不太好回答。因为在真实场景中,可能存在多个叫张伟的同事,而且用户输入的关键词往往过于简单,缺少必要的上下文信息。

✨问题改写能带来什么?

问题改写就像是帮助系统更好地理解用户意思。比如当你问"找张伟"时,系统可以把问题改写为更完整的形式,比如"请告诉我公司中所有叫张伟的员工及其所在部门"。这样的改写不仅能提高检索的准确性,还能让回答更加全面。

接下来,你可以通过实际案例来体验不同的问题改写策略。在这个案例中,你将使用以下配置:

  • 文档:Markdown格式
  • 切片:默认句子切片策略
  • 模型:text-embedding-v3
  • 存储:默认向量存储
python 复制代码
# 配置嵌入模型
Settings.embed_model = DashScopeEmbedding(
    model_name=DashScopeTextEmbeddingModels.TEXT_EMBEDDING_V3,
    embed_batch_size=6,
    embed_input_length=8192
)

# 加载文档
documents = SimpleDirectoryReader('./docs/2_5').load_data()

# 配置文档分割器
sentence_splitter = SentenceSplitter(
    chunk_size=1000,
    chunk_overlap=200,
)

# 文档分割
sentence_nodes = sentence_splitter.get_nodes_from_documents(documents)

# 构建索引
sentence_index = VectorStoreIndex(sentence_nodes, embed_model=Settings.embed_model)

【常规方法:不改写问题,直接检索】

在你尝试问题改写之前,先看看直接使用原始问题进行检索的效果。这样的对比能让你更直观地感受问题改写带来的提升:

python 复制代码
# 创建查询引擎
query_engine = sentence_index.as_query_engine(
    streaming=True,
    similarity_top_k=5
)

# 执行查询
print(f"❓ 用户问题: {question}\n")
streaming_response = query_engine.query(question)

print("\n💭 AI回答:")
print("-" * 40)
streaming_response.print_response_stream()
print("\n")

# 显示参考文档
print("\n📚 参考依据:")
print("-" * 40)
for i, node in enumerate(streaming_response.source_nodes, 1):
    print(f"\n文档片段 {i}:")
    print(f"相关度得分: {node.score:.4f}")
    print("-" * 30)
    print(node.text)

# 评估结果
print("\n📊 回答质量评估:")
print("-" * 40)
evaluation_score = evaluate_result(question, streaming_response, ground_truth)
display(evaluation_score)

运行完这段代码,你可能会发现结果不太理想。虽然系统召回了5个相关片段,但并没有找到所有"张伟"的信息。这是为什么呢?

问题就出在提问方式上。当用户问"张伟是哪个部门的"时,这个问题对人来说很好理解,但对大模型来说却缺少了重要的上下文 ------ 公司里不止一个张伟!这就好比你去一个有多个王老师的学校问"王老师在哪个办公室",别人一定会反问你"你说的是哪个王老师呀?"

那么,如果让问题表达得更完整一些会怎样?比如明确说明你想知道"公司里所有叫张伟的同事的部门信息"。接下来,你可以试试用大模型来改写问题,看看效果会不会更好。

【方法一:使用大模型扩充用户问题】

你可以让大模型充当一个问题改写助手。它会帮你简单的问题改写得更加完整和清晰。比如,它不仅会考虑到可能存在个张伟的情况,还会把相关的上下文信息都补充进去。看看具体怎么做:

python 复制代码
query_gen_str = """\
系统角色设定:
你是一个专业的问题改写助手。你的任务是将用户的原始问题扩充为一个更完整、更全面的问题。

规则:
1. 将可能的歧义、相关概念和上下文信息整合到一个完整的问题中
2. 使用括号对歧义概念进行补充说明
3. 添加关键的限定词和修饰语
4. 确保改写后的问题清晰且语义完整
5. 对于模糊概念,在括号中列举主要可能性

原始问题:
{query}

请生成一个综合的改写问题,确保:
- 包含原始问题的核心意图
- 涵盖可能的歧义解释
- 使用清晰的逻辑关系词连接不同方面
- 必要时使用括号补充说明

输出格式:
[综合改写] - 改写后的问题
"""
query_gen_prompt = PromptTemplate(query_gen_str)
python 复制代码
def generate_queries(query: str):
    response = Settings.llm.predict(
        query_gen_prompt, query=query
    )
    return response
python 复制代码
# 生成扩展查询
print("\n🔍 原始问题:")
print(f"   {question}")
query = generate_queries(question)
print("\n📝 扩展查询:")
print(f"   {query}\n")

# 创建查询引擎
query_engine = sentence_index.as_query_engine(
    streaming=True,
    similarity_top_k=5
)
# 执行查询
response = query_engine.query(query)

print("💭 AI回答:")
print("-" * 40)
response.print_response_stream()
print("\n")

# 显示参考文档
print("\n📚 参考依据:")
print("-" * 40)
for i, node in enumerate(response.source_nodes, 1):
    print(f"\n文档片段 {i}:")
    print(f"相关度得分: {node.score:.4f}")
    print("-" * 30)
    print(node.text)

# 评估结果
print("\n📊 回答质量评估:")
print("-" * 40)
evaluation_score = evaluate_result(query, response, ground_truth)
display(evaluation_score)

【方法二:将单一查询改写为多步步骤查询】

除了改写问题,你还可以尝试另一种思路:把复杂的问题拆解成简单的步骤。LlamaIndex提供了两个强大的工具来实现这个功能:

  • StepDecomposeQuerytransform:这个工具可以帮你把一个复杂问题分解成多个子问题。比如对于"张伟是哪个部门的?",它会先分解为:
    1. "公司里有几个叫张伟的员工?"
    2. "这些张伟分别在哪些部门?"
      这样可以更全面地获取所有张伟的信息。
  • MultiStepQueryEngine:这个查询引擎会按顺序处理这些子问题。它会先获取公司所有张伟的信息,然后再查询每个张伟的部门信息,最终将答案整合成一个完整的回应,告诉用户"公司有三名张伟,分别在教研部、课程开发部和IT部"。

这种方法就像解决数学题一样------把大问题分解成小问题往往更容易得到准确的答案。不过要注意,这种方法会多次调用大模型,所以会消耗更多的token。

python 复制代码
from llama_index.core.indices.query.query_transform.base import (
    StepDecomposeQueryTransform,
)
step_decompose_transform = StepDecomposeQueryTransform(verbose=True)
# set Logging to DEBUG for more detailed outputs
from llama_index.core.query_engine import MultiStepQueryEngine
query_engine = sentence_index.as_query_engine(streaming=True,similarity_top_k=5)
query_engine = MultiStepQueryEngine(
    query_engine=query_engine,
    query_transform=step_decompose_transform,
    index_summary="公司人员信息"
)
print(f"❓ 用户问题: {question}\n")
print("🤖 AI正在进行多步查询...")
response = query_engine.query(question)
print("\n📚 参考依据:")
print("-" * 40)
for i, node in enumerate(response.source_nodes, 1):
    print(f"\n文档片段 {i}:")
    print("-" * 30)
    print(node.text)

# 评估结果
print("\n📊 多步查询评估结果:")
print("-" * 40)
evaluation_score = evaluate_result(question, response, ground_truth)
display(evaluation_score)

【方法三:用假设文档来增强检索(HyDE)】

前面的方法都是在调整问题本身,现在让我们换个思路:如果我们先假设一个可能的答案来怎样?这就是HyDE(Hypothetical Document Embeddings)方法的独特之处。

它的工作方式很有趣:

  1. 先让大模型基于问题编一个"假想的答案文档"
  2. 用这个假想文档来检索真实文档
  3. 最后用检索到的真实文档来生成实际答案

这就像你在找一本书时,心里已经有了一个大致的内容轮廓,然后用这个轮廓去图书馆匹配相似的书籍。让我们看看具体怎么实现:

从评测结果可以看到,这种方法确实带来了一些改善。你可能会好奇:系统是如何生成这个"假想文档"的呢?一起来看看这个过程中,AI实际生成了什么内容:

python 复制代码
query_bundle = hyde(question)
hyde_doc = query_bundle.embedding_strs[0]
print(f"🤖 AI生成的假想文档:\n{hyde_doc}\n")
python 复制代码
🤖 AI生成的假想文档:
张伟是市场部的经理。他自2018年加入公司以来,一直负责市场推广、品牌建设和客户关系管理等工作。在他的带领下,市场部成功策划并执行了多个大型营销活动,显著提升了公司的市场知名度和产品销量。张伟拥有丰富的市场营销经验,曾在多家知名企业任职,具备敏锐的市场洞察力和出色的团队领导能力。他目前直接向公司副总裁汇报工作,并与其他部门密切协作,推动公司整体战略目标的实现。

有趣的是,虽然这个"假想文档"完全是AI编造的,但它的结构和风格与真实的公司员工信息非常相似。LlamaIndex提供了灵活的控制机制来优化这个过程:

HyDEQuerytransform类允许我们通过以下方式精确控制假想文档的生成:

  • 自定义LLM:通过llm参数传入不同的大模型配置,可以选择更适合的语言模型来生成假想文档
  • 提示词模板:通过hyde_prompt参数自定义提示词模板,精确控制输出的格式和内容
  • 查询策略:使用include_original参数决定是否将原始查询与假想文档结合使用

TransformQueryEngine则作为查询引擎的包装器,它会:

  1. 先调用HyDEQueryTransform生成假想文档
  2. 使用假想文档进行向量检索
  3. 最后返回查询结果

这种架构让我们能在不修改底层查询引擎的情况下,通过调整HyDEQueryTransform的参数来优化检索效果。即使假想文档的具体内容可能不够准确,但通过精心设计的配置,它可以非常帮助系统更准确地检索相关信息。

提取标签增强检索

在向量检索的基础上,我们还可以添加标签过滤来提升检索精度。这种方式类似于图书馆既有书名检索,又有分类编号系统,能让检索更精准。

标签提取有两个关键场景:

  1. 建立索引时,从文档切片中提取结构化标签
  2. 检索时,从用户问题中提取对应的标签进行过滤

让我们看两个例子来理解如何从不同类型的广西中提取标签:

python 复制代码
import os
from openai import OpenAI
client = OpenAI(api_key=os.getenv("DASHSCOPE_API_KEY"), base_url="https://dashscope.aliyuncs.com/compatible-mode/v1")
system_message = """你是一个标签提取专家。请从文本中提取结构化信息,并按要求输出标签。
---
【支持的标签类型】
- 人名
- 部门名称
- 职位名称
- 技术领域
- 产品名称
---
【输出要求】
1. 请用 JSON 格式输出,如:[{"key": "部门名称", "value": "教研部"}]
2. 如果某类标签未识别到,则不输出该类
---
待分析文本如下:
"""
def extract_tags(text):
    completion = client.chat.completions.create(
        model="qwen-turbo",
        messages=[
            {'role': 'system', 'content': system_message},
            {'role': 'user', 'content': text}
        ],
        response_format={"type": "json_object"}
    )
    return completion.choices[0].message.content
python 复制代码
# 示例1:人事文档
hr_text = """张明是我们AI研发部的技术主管,他带领团队开发了新一代智能对话平台 ChatMax,在自然语言处理领域有着丰富经验。如果您需要了解项目细节,可以直接联系他。"""
print("人事文档标签提取结果:")
print(extract_tags(hr_text))

# 示例2:技术文档
tech_text = """本论文提出了一种基于深度学习的图像识别算法,在医疗影像分析中取得了突破性进展。该算法已在北京协和医院的CT诊断系统中得到应用。"""
print("\n技术文档标签提取结果:")
print(extract_tags(tech_text))
python 复制代码
人事文档标签提取结果:
[{"key": "人名", "value": "张明"}, {"key": "部门名称", "value": "AI研发部"}, {"key": "职位名称", "value": "技术主管"}, {"key": "产品名称", "value": "ChatMax"}, {"key": "技术领域", "value": "自然语言处理"}]

技术文档标签提取结果:
[{"key": "技术领域", "value": "深度学习"}, {"key": "部门名称", "value": "北京协和医院"}, {"key": "产品名称", "value": "CT诊断系统"}]

当我们建立索引时,可以将这些标签与文档切片一起存储。这样在检索时,比如用户问"张伟是哪个部门的",我们可以:

  1. 从问题中提取人名标签{"key":"人名",value:"张伟"}
  2. 先用标签过滤出所有包含"张伟"的文档切片
  3. 再用向量相似度检索找出最相关的内容

这种"标签过滤+向量检索"的相似方式,能大幅提升检索的准确性。特别是在处理结构化程度较高的企业文档时,这个方法效果更好。

重排序
python 复制代码
![ -f ./docs/内容公司各部门职责与关键角色联系信息汇总.md ] && rm ./docs/内容公司各部门职责与关键角色联系信息汇总.md && echo "文件已删除。" || echo "文件不存在,无需删除。"
  • Markdown

Markdown

删除文件后,可以执行下面的代码。可以看到,你设置了从向量数据库中检索召回 3 条相关的文档切片。

从结果上来看,这 3 条切片,事实上不够相关,问答机器人无法正确回答「张伟是哪个部门的」。

python 复制代码
from llama_index.llms.dashscope import DashScope
from chatbot import rag
python 复制代码
index = rag.create_index('./docs')
query_engine = index.as_query_engine(
    similarity_top_k=3,
    streaming=True,
)
python 复制代码
response = ask("张伟是哪个部门的", query_engine=query_engine)
python 复制代码
display(evaluate_result(question, response, ground_truth))

你可以调整代码,先从向量数据库中检索召回 20 条文档切片,再借助阿里云百炼提供的文本排序模型进行重排序,并且筛选出其中最相关的 3 条参考信息。

运行代码后你可以看到,同样是 3 条参考信息,这次大模型能够准确回答问题了。

python 复制代码
from llama_index.postprocessor.dashscope_rerank import DashScopeRerank
from llama_index.core.postprocessor import SimilarityPostprocessor
python 复制代码
query_engine = index.as_query_engine(
    # 先设置一个较大的召回切片数量
    similarity_top_k=20,
    streaming=True,
    node_postprocessors=[
        # 在rerank 模型中选择你最终想召回的切片个数,重排模型选择通义实验室的gte-rerank模型
        DashScopeRerank(top_n=3, model="gte-rerank"),
        # 设置一个相似度阈值,低于该阈值的切片会被过滤掉
        SimilarityPostprocessor(similarity_cutoff=0.2)
    ]
)
python 复制代码
response = ask("张伟是哪个部门的", query_engine=query_engine)

生成答案阶段

现在,大模型会根据你的问题和检索召回的内容,生成最终的答案。然而,这个答案可以还是不及你的预期。你可能会遇到的问题有:

  1. 没有检索到相关信息,大模型捏造答案。
  2. 检索到了相关信息,但是大模型没有按照要求生成答案
  3. 检索到了相关信息,大模型给出了答案,但是你希望AI给出更全面的答案。

为了解决这些问题,你可以从以下角度着手分析与解决:

  1. 选择合适的大模型
    • A.如果只是简单的信息查询总结,小参数量的模型足以满足需求,比如plus-turbo.
    • B.如果你希望答疑机器人能完成较为复杂的逻辑推理,建议选择参数量更大、推理能力更强的大模型,比如
    • C. 如果你的问题需要查阅大量的文档片段,建议选择上下文长度更大的模型
    • D.如果你构建的RAG应用面向一些非通用领域,如法律领域,建议面向特定领域训练的模型
  2. 充分优化提示词模板,比如:
    • A.明确要求不编造答案:大模型可能会产生一些不真实的内容,通常称为幻觉。你可以通过提示词要求大模型:【如果所提供的信息不足以回答问题,请明确告知:"根据现有信息,我无法回答这个问题"切勿编造答案。】,来减少大模型产生幻觉的几率。
    • B.添加内容分隔标记:检索召回的文档切片如果随意混杂在提示词里,人也很难看清整个提示词的结构,大模型也会受到干扰。建议将提示词和检索切片明确地分开,以便大模型能够正确地理解你的意图。
    • C.根据问题类型调整模板:不同问题的回答范式可能是不同的,你可以借助大模型识别问题类型,然后映射使用不同的提示词模板。比如有些问题,你希望大模型先输出整体框架,然后再输出细节;有些问题你可能希望大模型言简意赅的给出结论。
  3. 调整大模型的参数,比如:
    • A.如果你希望大模型输出在相同的问题下,输出的内容尽可能相同,你可以在每次模型调用时传入相同的seed值。
    • B.如果你希望大模型在回答用户问题是不要总是用重复的句子,你可以适当调高presence_penalty值。
    • C.如果你希望查询事实性的内容,可以适当降低temperature或top_p值;反之,查询创造性的内容时,可以适当增加它们的值。
    • D.如果你需要限制字数(如生成摘要、关键词)、控制成本或减少响应时间的场景,可以适当降低max_tokens的值,但是若max_tokens过小,可能会导致输出截断,反之,需要生成大段文本时,可以提高它的值。
python 复制代码
from llama_index.llms.openai_like import OpenAILike
from llama_index.core import Settings
import os
python 复制代码
# 事实查询场景 - 低温度、高确定性
factual_llm = OpenAILike(
    model="qwen-plus",  # 使用通义千问-Plus模型
    api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    is_chat_model=True,
    temperature=0.1,      # 降低temperature使输出更确定性
    max_tokens=512,       # 控制输出长度,但是若max_tokens过小,可能会导致输出截断
    presence_penalty=0.0, # 默认presence_penalty
    seed=42              # 固定seed使输出可重现
)
python 复制代码
# 创造性场景 - 高温度、更多样化
creative_llm = OpenAILike(
    model="qwen-plus",
    api_base="https://dashscope.aliyuncs.com/compatible-mode/v1",
    api_key=os.getenv("DASHSCOPE_API_KEY"),
    is_chat_model=True,
    temperature=0.7,      # 提高temperature使输出更有创造性
    max_tokens=1024,      # 允许更长的输出
    presence_penalty=0.6  # 提高presence_penalty减少重复
)
相关推荐
m0_603888711 小时前
FineInstructions Scaling Synthetic Instructions to Pre-Training Scale
人工智能·深度学习·机器学习·ai·论文速览
爬台阶的蚂蚁1 小时前
RAG概念和使用
ai·rag
undsky_2 小时前
【RuoYi-SpringBoot3-Pro】:将 AI 编程融入传统 java 开发
java·人工智能·spring boot·ai·ai编程
AI应用开发实战派2 小时前
AI人工智能中Bard的智能电子商务优化
人工智能·ai·bard
AI原生应用开发2 小时前
AIGC领域Bard在通信领域的内容创作
ai·aigc·bard
唐诺2 小时前
深入了解AI
人工智能·ai
ZEGO即构开发者3 小时前
如何用一句话让AI集成 ZEGO 产品
ai·实时互动·实时音视频·rtc
阿杰学AI3 小时前
AI核心知识76——大语言模型之RAG 2.0(简洁且通俗易懂版)
人工智能·ai·语言模型·自然语言处理·rag·检索增强生成·rag2.0
GuoDongOrange3 小时前
智能体来了从 0 到 1:工作流在智能体系统中的真实作用
ai·智能体·从0到1·智能体来了·智能体来了从0到1
爱吃涮肉4 小时前
# 第二章:ClaudeCode核心功能(详细版)
ai