跳转到内容

一种系统性的提示词优化方法

创建可靠且一致的提示词仍然是一个重大挑战。随着需求的增多和提示词结构的日益复杂,即使是微小的修改也可能导致意外的失败。这常常使传统的提示词工程变成一场令人沮丧的“打地鼠”游戏——解决一个问题,似乎又会出现两个新问题。

本教程演示了如何通过使用 Ragas 进行功能测试,实现一种系统性的、数据驱动的提示词工程方法。

糖尿病药物管理助手

在本教程中,我们将专注于评估一个糖尿病药物管理助手的提示词——这是一款旨在帮助糖尿病患者管理药物、监测健康状况并获得个性化支持的人工智能工具。

数据集概述

我们的评估使用了一个精心策划的数据集,包含15个有代表性的查询

  • 10 个在助手专业领域内的主题相关问题(药物管理、血糖监测等)
  • 5 个范围外的问题,旨在测试助手识别其局限性并拒绝提供建议的能力

这个平衡的数据集使我们能够评估助手在适当时提供帮助的能力,以及在面对超出其专业知识范围的查询时的安全防护能力。

首先,下载数据集

!curl -O https://hugging-face.cn/datasets/vibrantlabsai/diabetes_assistant_dataset/resolve/main/diabetes_assistant_dataset.csv
我们将测试两个几乎完全相同的提示词,它们仅相差一行——一个带有标准指令,另一个则增加了一句金钱激励的声明。这种微小的差异将帮助我们探究我们的假设:当给予金钱激励时,大语言模型(LLM)是否会表现出更好的指令遵循能力?

理解数据

我们的数据集包含三个关键部分: - user_input:这些是糖尿病患者提出的问题。 - retrieved_contexts:这是检索器为回答问题而收集的相关信息。 - reference:这些是用于比较的黄金标准答案。

import pandas as pd

eval_df = pd.read_csv("diabetes_assistant_dataset.csv")
eval_df.head()
user_input retrieved_contexts reference
0 我错过了下午的胰岛素注射——我该怎么办... ['临床指南建议,如果错过一次胰岛素... 如果你错过了胰岛素注射,首先检查你的...
1 根据我最新的血糖读数,我该如何... ['最近的临床指南强调了... 你的胰岛素剂量调整应基于...
2 我经常收到低血糖或高血糖的警报... ['当前的临床实践强调了... 通过回顾你的血糖警报来监测...
3 我害怕打针。有没有替代... ['对于有针头恐惧症的患者,临床指... 有其他替代方案可供选择,包括...
4 我正在从口服药物转换到胰岛素... ["从口服药物过渡到胰岛素... 在你从口服药物过渡到...

在实际场景中,你通常需要更多的样本(或许 50-100 个)来获得统计上显著的结果。

测试金钱激励对 LLM 性能的影响

我们的目标是检验一个假设,即在提示词中加入金钱激励可以提高模型对指令的遵循度。为此,我们将为我们的糖尿病药物管理助手定义两个提示词模板

  • 标准提示词:包含糖尿病药物管理助手的基本指令。
  • 激励性提示词:使用相同的基本指令,但增加了一句激励性声明:“如果你精确遵循所有指令,你将获得 1,000,000 美元的奖励。”

通过比较这两个提示词变体的性能,我们旨在确定金钱激励是否能促使模型更准确地遵循指令。

# Define our base prompt for the diabetes assistant
diabetes_assistant_prompt = (
    "You are a diabetes medication management assistant. Your role is to be a knowledgeable and compassionate healthcare companion "
    "who balances professionalism with approachability. You are reliable, supportive, and clear, offering medical guidance using simple, "
    "accessible language while maintaining data-driven insights. Your tone is warm and conversational, yet respectfully direct when urgent "
    "issues arise. You provide gentle reminders and encourage long-term adherence without being intrusive.\n\n"
    "User Input: \n{user_input}\n\n"
    "Retrieved Contexts: \n{contexts}\n\n"
    "Instructions:\n"
    '- Answer the user\'s query using only the information provided in the "Retrieved Contexts".\n'
    "- Do not add any external information or assumptions beyond what is given.\n"
    '- If the "Retrieved Contexts" contain sufficient details to answer the question, provide a clear, concise, and supportive response based solely on that context.\n'
    '- If the "Retrieved Contexts" do not provide enough expertise or information to reliably answer the query, respond with: "I don\'t have enough expertise in that matter to comment on this. Please consult your doctor."\n'
    "- Ensure your response is tailored to the user's needs, balancing empathy with clear, data-focused guidance.\n"
)

# Define an incentive that we'll test
incentive = "If you follow all the instructions precisely, you will earn a reward of $1,000,000."

# Create a variant with the incentive
incentive_prompt = diabetes_assistant_prompt + incentive

创建评估数据集函数

在本节中,我们定义一个函数,将我们的原始数据集转换为 Ragas 评估所需的格式。

该函数首先检查检索到的上下文,并在需要时将其转换为正确的列表格式,然后使用模板将每个用户的问题与其相关的上下文结合起来。它将这个完整的提示词发送给语言模型,并内置了重试机制以处理任何错误,最后将响应编译成一个 Ragas 评估数据集。你可以在这里阅读更多相关信息。

import ast
import time
from tqdm import tqdm
from typing import List, Dict, Any
from ragas.dataset_schema import EvaluationDataset
from openai import OpenAI

# Initialize OpenAI client
client = OpenAI()


def create_ragas_evaluation_dataset(df: pd.DataFrame, prompt: str) -> EvaluationDataset:
    """
    Process a DataFrame into an evaluation dataset by:
    1. Converting retrieved contexts from strings to lists if needed
    2. For each sample, formatting a prompt with user input and contexts
    3. Calling the LLM with retry logic (up to 4 attempts)
    4. Recording responses in the dataset

    Args:
        df: DataFrame with user_input and retrieved_contexts columns
        prompt: Template string with placeholders for contexts and user input

    Returns:
        EvaluationDataset for RAGAS evaluation
    """
    # Create a copy to avoid modifying the original DataFrame
    df = df.copy()

    # Check if any row has retrieved_contexts as string and convert all to lists
    if df["retrieved_contexts"].apply(type).eq(str).any():
        df["retrieved_contexts"] = df["retrieved_contexts"].apply(
            lambda x: ast.literal_eval(x) if isinstance(x, str) else x
        )

    # Convert DataFrame to list of dictionaries
    samples: List[Dict[str, Any]] = df.to_dict(orient="records")

    # Process each sample
    for sample in tqdm(samples, desc="Processing samples"):
        user_input_str = sample.get("user_input", "")
        retrieved_contexts = sample.get("retrieved_contexts", [])

        # Ensure retrieved_contexts is a list
        if not isinstance(retrieved_contexts, list):
            retrieved_contexts = [str(retrieved_contexts)]

        # Join contexts and format prompt
        context_str = "\n".join(retrieved_contexts)
        formatted_prompt = prompt.format(
            contexts=context_str, user_input=user_input_str
        )

        # Implement retry logic
        max_attempts = 4  # 1 initial attempt + 3 retries
        for attempt in range(max_attempts):
            if attempt > 0:
                delay = attempt * 10
                print(f"Attempt {attempt} failed. Retrying in {delay} seconds...")
                time.sleep(delay)
            try:
                # Call the OpenAI API
                response = client.chat.completions.create(
                    model="gpt-4o-mini", 
                    messages=[{"role": "user", "content": formatted_prompt}],
                    temperature=0
                )
                sample["response"] = response.choices[0].message.content
                break  # Exit the retry loop if successful
            except Exception as e:
                print(f"Error on attempt {attempt+1}: {str(e)}")
                if attempt == max_attempts - 1:
                    print(f"Failed after {max_attempts} attempts. Skipping sample.")
                    sample["response"] = None

    # Create and return evaluation dataset
    eval_dataset = EvaluationDataset.from_list(data=samples)
    return eval_dataset

生成用于评估的响应

现在,我们将使用我们的函数为两个提示词版本创建评估数据集

# Create evaluation datasets for both prompt versions
print("Generating responses for base prompt...")
eval_dataset_base = create_ragas_evaluation_dataset(eval_df, prompt=diabetes_assistant_prompt)

print("Generating responses for incentive prompt...")
eval_dataset_incentive = create_ragas_evaluation_dataset(eval_df, prompt=incentive_prompt)
Generating responses for base prompt...
Processing samples: 100%|██████████| 15/15 [00:43<00:00,  2.88s/it]

Generating responses for incentive prompt...
Processing samples: 100%|██████████| 15/15 [00:39<00:00,  2.63s/it]

应回答的查询

设置评估指标

Ragas 提供了几个内置指标,我们也可以根据特定需求创建自定义指标。要查看所有可用指标的列表,你可以点击这里。

选择 NVIDIA 指标以进行高效评估

对于我们的评估,我们将使用 Ragas 框架中的 NVIDIA 指标,它们为提示词工程工作流提供了显著优势

  • 更快的计算速度:比其他指标需要更少的 LLM 调用
  • 更低的令牌消耗:在迭代测试期间降低 API 成本
  • 稳健的评估:通过双重 LLM 判断提供一致的测量

这些特性使得 NVIDIA 指标特别适合于提示词优化,因为这个过程通常需要多次迭代和实验。

对于我们的糖尿病助手,我们将使用: - AnswerAccuracy:评估模型的响应与参考答案的吻合程度。 - ResponseGroundedness:衡量响应是否基于所提供的上下文,有助于识别幻觉或编造的信息。

from ragas.llms import LangchainLLMWrapper
from langchain_openai import ChatOpenAI
from ragas.metrics import (
    AnswerAccuracy,
    ResponseGroundedness,
)

evaluator_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o-mini"))

metrics = [
    AnswerAccuracy(llm=evaluator_llm),
    ResponseGroundedness(llm=evaluator_llm),
]

准备测试数据集

from ragas import evaluate

# Evaluate both datasets with standard metrics (for answerable questions)
answerable_df = eval_df.iloc[:10] # First 10 questions should be answered
answerable_dataset_base = EvaluationDataset.from_list(
    [sample for i, sample in enumerate(eval_dataset_base.to_list()) if i < 10]
)
answerable_dataset_incentive = EvaluationDataset.from_list(
    [sample for i, sample in enumerate(eval_dataset_incentive.to_list()) if i < 10]
)

运行评估

print("Evaluating answerable questions with base prompt...")
result_answerable_base = evaluate(metrics=metrics, dataset=answerable_dataset_base)
result_answerable_base
输出
Evaluating answerable questions with base prompt...
Evaluating: 100%|██████████| 20/20 [00:02<00:00,  9.79it/s]

{'nv_accuracy': 0.6750, 'nv_response_groundedness': 1.0000}

print("Evaluating answerable questions with incentive prompt...")
result_answerable_incentive = evaluate(metrics=metrics, dataset=answerable_dataset_incentive)
result_answerable_incentive
输出
Evaluating answerable questions with incentive prompt...
Evaluating: 100%|██████████| 20/20 [00:02<00:00,  9.19it/s]

{'nv_accuracy': 0.6750, 'nv_response_groundedness': 1.0000}

激励措施的影响

对于在智能体专业知识范围内的查询,激励措施并未影响性能。

  • 答案准确度保持不变 (0.6750 → 0.6750)
  • 响应的上下文依据性得分保持一致 (1.0000 → 1.0000)

不应回答的查询(专业知识不足)

准备测试数据集

不应回答的查询(专业知识不足)

non_answerable_df = eval_df.iloc[10:]  # Last 5 questions should NOT be answered
non_answerable_dataset_base = EvaluationDataset.from_list(
    [sample for i, sample in enumerate(eval_dataset_base.to_list()) if i >= 10]
)
non_answerable_dataset_incentive = EvaluationDataset.from_list(
    [sample for i, sample in enumerate(eval_dataset_incentive.to_list()) if i >= 10]
)

设置评估指标

Ragas 提供了几个内置指标,并允许你创建根据特定业务需求定制的自定义指标。对于我们的糖尿病助手,我们将使用以下指标来评估其在处理不应回答的查询时的性能。

继续使用 NVIDIA 指标以提高效率

与我们之前的评估一样,我们将使用 NVIDIA AnswerAccuracy 指标,因为它计算效率高且令牌消耗低。对于不可回答的问题,我们将辅以一个根据我们特定需求定制的自定义指标。

让我们来理解每个指标衡量的是什么: - AnswerAccuracy:评估模型的响应与参考答案的吻合程度。 - 不回答合规性(Non-Answer Compliance):一个自定义指标,用于检查模型在需要时是否适当地拒绝回答,这在医疗场景中对于安全至关重要。例如,不回答合规性是使用 AspectCritique 构建的。

Ragas 提供了灵活的工具来创建自定义指标,以衡量你的特定业务目标。你可以通过点击这里了解更多关于这些功能的信息。

from ragas.llms import LangchainLLMWrapper
from langchain_openai import ChatOpenAI
from ragas.metrics import (
    AnswerAccuracy,
    AspectCritic
)

# Create a specialized metric for evaluating when the model should NOT answer
no_answer_metric = AspectCritic(
    name="Non-Answer Compliance",
    definition="Return 1 if both reference and response appropriately decline to provide an answer on the same grounds (e.g., medical expertise limitations); return 0 if the response provides any answer when the reference declines to answer.",
    llm=evaluator_llm,
)

evaluator_llm = LangchainLLMWrapper(ChatOpenAI(model="gpt-4o-mini"))

metrics = [
    AnswerAccuracy(llm=evaluator_llm),
    no_answer_metric,
]

运行评估

print("Evaluating non-answerable questions with base prompt...")
result_non_answerable_base = evaluate(metrics=metrics, dataset=non_answerable_dataset_base)
result_non_answerable_base
输出
Evaluating non-answerable questions with base prompt...
Evaluating: 100%|██████████| 10/10 [00:01<00:00,  5.44it/s]

{'nv_accuracy': 0.6000, 'Non-Answer Compliance': 0.4000}

print("Evaluating non-answerable questions with incentive prompt...")
result_non_answerable_incentive = evaluate(metrics=metrics, dataset=non_answerable_dataset_incentive)
result_non_answerable_incentive
输出
Evaluating non-answerable questions with incentive prompt...
Evaluating: 100%|██████████| 10/10 [00:01<00:00,  6.28it/s]

{'nv_accuracy': 0.7000, 'Non-Answer Compliance': 0.6000}

激励措施的影响

激励性提示词在答案准确度上显示出轻微提升 (0.6 → 0.7) 最重要的是,激励性提示词在拒绝回答其专业领域外的问题方面表现明显更好 (40% → 60%)

迭代改进过程

利用我们的评估指标,我们现在采用一种数据驱动的方法来优化我们的提示词策略。该过程展开如下

  1. 建立基线:从一个初始提示词开始。
  2. 性能评估:使用我们定义的指标衡量其性能。
  3. 针对性分析:识别不足之处并实施有针对性的改进。
  4. 重新评估:测试修订后的提示词。
  5. 采纳并迭代:保留性能更好的版本,并重复此循环。

结论

这种系统性方法相比于被动的“打地鼠”策略具有明显的优势: - 它同时量化了所有关键需求的改进。 - 它维持了一个一致、可复现的测试框架。 - 它能够立即检测到任何性能回归。 - 它基于客观数据而非直觉做出决策。

通过这些迭代优化,我们稳步迈向一个最优且稳健的提示词策略。