一种系统性的提示词优化方法
创建可靠且一致的提示词仍然是一个重大挑战。随着需求的增多和提示词结构的日益复杂,即使是微小的修改也可能导致意外的失败。这常常使传统的提示词工程变成一场令人沮丧的“打地鼠”游戏——解决一个问题,似乎又会出现两个新问题。
本教程演示了如何通过使用 Ragas 进行功能测试,实现一种系统性的、数据驱动的提示词工程方法。
糖尿病药物管理助手
在本教程中,我们将专注于评估一个糖尿病药物管理助手的提示词——这是一款旨在帮助糖尿病患者管理药物、监测健康状况并获得个性化支持的人工智能工具。
数据集概述
我们的评估使用了一个精心策划的数据集,包含15个有代表性的查询
- 10 个在助手专业领域内的主题相关问题(药物管理、血糖监测等)
- 5 个范围外的问题,旨在测试助手识别其局限性并拒绝提供建议的能力
这个平衡的数据集使我们能够评估助手在适当时提供帮助的能力,以及在面对超出其专业知识范围的查询时的安全防护能力。
首先,下载数据集
!curl -O https://hugging-face.cn/datasets/vibrantlabsai/diabetes_assistant_dataset/resolve/main/diabetes_assistant_dataset.csv
理解数据
我们的数据集包含三个关键部分: - user_input:这些是糖尿病患者提出的问题。 - retrieved_contexts:这是检索器为回答问题而收集的相关信息。 - reference:这些是用于比较的黄金标准答案。
| 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%)
迭代改进过程
利用我们的评估指标,我们现在采用一种数据驱动的方法来优化我们的提示词策略。该过程展开如下
- 建立基线:从一个初始提示词开始。
- 性能评估:使用我们定义的指标衡量其性能。
- 针对性分析:识别不足之处并实施有针对性的改进。
- 重新评估:测试修订后的提示词。
- 采纳并迭代:保留性能更好的版本,并重复此循环。
结论
这种系统性方法相比于被动的“打地鼠”策略具有明显的优势: - 它同时量化了所有关键需求的改进。 - 它维持了一个一致、可复现的测试框架。 - 它能够立即检测到任何性能回归。 - 它基于客观数据而非直觉做出决策。
通过这些迭代优化,我们稳步迈向一个最优且稳健的提示词策略。