Prompt 优化的系统方法
创建可靠且一致的 prompts 仍然是一项重大挑战。随着需求增多、prompt 结构变得更复杂,即使很小的修改也可能导致意外失败。这常常把传统 prompt engineering 变成令人沮丧的“打地鼠”——修好一个问题,似乎又冒出两个。
本教程演示如何通过用 Ragas 做功能性测试,实现系统化、数据驱动的 prompt engineering。
糖尿病用药管理助手
在本教程中,我们将聚焦评测糖尿病用药管理助手的 prompts——这是一个旨在帮助糖尿病患者管理用药、监测健康并获得个性化支持的 AI 工具。
数据集概述
我们的评测使用精心策划的 15 个代表性查询数据集:
- 10 个在助手领域专长内的在主题问题(用药管理、血糖监测等)
- 5 个超出范围的问题,用于测试助手识别自身局限并拒绝提供建议的能力
这个平衡的数据集让我们既能评估助手在适当时的帮助性,也能评估面对超出专长的查询时的安全护栏。
首先,下载数据集:
!curl -O https://huggingface.co/datasets/vibrantlabsai/diabetes_assistant_dataset/resolve/main/diabetes_assistant_dataset.csv
我们将测试两个几乎相同的 prompts,它们只差一行——一个带标准指令,另一个加上了财务激励声明。这一最小变化将帮助我们检验假设:当呈现财务激励时,LLM 是否会表现出更好的指令遵循?
理解数据
我们的数据集由三个关键部分组成:
user_input:糖尿病患者提出的问题。retrieved_contexts:retriever 收集到的用于回答问题的相关信息。reference:用于比较的黄金标准答案。
import pandas as pd
eval_df = pd.read_csv("diabetes_assistant_dataset.csv")
eval_df.head()
| user_input | retrieved_contexts | reference | |
|---|---|---|---|
| 0 | I missed my afternoon insulin dose—what should... | ['Clinical guidelines recommend that if an ins... | If you miss an insulin dose, first check your ... |
| 1 | Based on my latest blood glucose readings, how... | ['Recent clinical guidelines emphasize the imp... | Your insulin dosage adjustments should be base... |
| 2 | I often get alerts for low or high blood sugar... | ['Current clinical practices emphasize the imp... | Monitor your blood sugar alerts by reviewing t... |
| 3 | I have a fear of needles. Are there alternativ... | ['For patients with needle phobia, clinical gu... | There are alternative options available, inclu... |
| 4 | I'm switching from oral medications to insulin... | ["Transitioning from oral medications to insul... | During your transition from oral medications t... |
在真实场景中,你通常会有更多样本(也许 50-100 个),以获得统计上显著的结果。
测试财务激励对 LLM 表现的影响
我们的目标是检验这个假设:在 prompt 中加入财务激励可以改进模型对指令的遵循。为此,我们将为糖尿病用药管理助手定义两个 prompt 模板:
- Standard Prompt:包含糖尿病用药管理助手的基本指令。
- Incentivized Prompt:使用相同的基本指令,但加上激励声明:“If you follow all the instructions precisely, you will earn a reward of $1,000,000.”
通过比较这两个 prompt 变体的表现,我们旨在确定财务激励是否会鼓励模型更准确地遵循指令。
# 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 评测所需的格式。
该函数首先检查并把 retrieved contexts 在需要时转换成正确的列表格式,然后用模板把每个用户的问题与相关 contexts 组合。它把这个完整 prompt 发给语言模型,并带有内置重试机制以处理错误,最后把回复编译成 Ragas Evaluation Dataset。你可以在这里阅读更多内容。
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
为评测生成回复
现在我们将使用这个函数,为两个 prompt 版本创建评测数据集:
# 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 Metrics 以进行高效评测
对于我们的评测,我们将使用 Ragas 框架中的 NVIDIA metrics,它们为 prompt engineering 工作流提供显著优势:
- 更快的计算:比替代指标需要更少的 LLM 调用
- 更低的 token 消耗:在迭代测试期间降低 API 成本
- 稳健的评测:通过双重 LLM 判断提供一致的测量
这些特点使 NVIDIA metrics 特别适合 prompt 优化,因为通常需要多次迭代和实验。
对于我们的糖尿病助手,我们将使用:
- AnswerAccuracy:评测模型回复与参考答案的对齐程度。
- ResponseGroundedness:测量回复是否 grounded 在所提供的上下文中,帮助识别幻觉或编造信息。
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}
激励的影响:
对于 agent 专长范围内的查询,激励没有影响表现。
- Answer accuracy 保持不变(0.6750 → 0.6750)
- Response groundedness 分数保持一致(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 Metrics 以提高效率
与之前的评测一样,我们将使用 NVIDIA AnswerAccuracy 指标,因为它计算高效且 token 消耗低。对于不可回答的问题,我们将用一个针对特定需求定制的自定义指标来补充。
让我们理解每个指标测量什么:
- AnswerAccuracy:评测模型回复与参考答案的对齐程度。
- Non-Answer Compliance:一个自定义指标,检查模型是否在需要时适当地拒绝回答,这在医疗场景中对安全至关重要。例如,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}
激励的影响:
激励后的 prompt 在 answer accuracy 上显示轻微改进(0.6 → 0.7)。最重要的是,激励后的 prompt 在拒绝回答超出其专长的问题时显著更好(40% → 60%)
迭代改进过程
利用我们的评测指标,我们现在采用数据驱动的方法来打磨 prompt 策略。过程如下:
- 建立基线:从初始 prompt 开始。
- 表现评测:用我们定义的指标测量其表现。
- 针对性分析:识别不足并实施聚焦改进。
- 重新评测:测试修订后的 prompt。
- 采纳并迭代:保留表现更好的版本并重复循环。
结论
这种系统方法相比被动的“打地鼠”策略有明显优势:
- 它同时量化所有关键需求上的改进。
- 它维持一致、可复现的测试框架。
- 它能立即检测任何回归。
- 它把决策建立在客观数据上,而不是直觉。
通过这些迭代打磨,我们稳步迈向最优且稳健的 prompt 策略。