文本分类
使用 Claude 进行文本和数据分类的技术
🌐 查看英文原文 | 源码 Notebook
使用 Claude 进行分类:保险支持工单分类器
在本指南中,你将构建一个高准确率的分类系统,将保险支持工单分类为 10 个类别。你将学习如何通过结合 prompt 工程、检索增强生成(RAG)和思维链推理,逐步将分类准确率从 70% 提升到 95% 以上。
在本指南结束时,你将理解如何设计能够处理复杂业务规则、在有限训练数据下工作、并提供可解释结果的分类系统。
前置条件
- Python 3.11+ 基础知识
- Anthropic API key(在此获取)
- VoyageAI API key(可选 - embedding 已预计算)
- 对分类问题的基本理解
环境配置
首先,我们将安装所需的包:
- anthropic
- voyageai
- pandas
- matplotlib
- sklearn
- numpy
我们还将从环境变量中加载 API 密钥并设置模型名称。
%%capture
!pip install -U anthropic voyageai pandas numpy matplotlib scikit-learn# Setup our environment
import os
import anthropic
MODEL = "claude-haiku-4-5"
client = anthropic.Anthropic(
# This is the default and can be omitted
api_key=os.getenv("ANTHROPIC_API_KEY"),
)使用 Claude 进行分类
大型语言模型(LLM)彻底改变了分类领域,特别是在传统机器学习系统面临挑战的领域。LLM 在处理具有复杂业务规则和低质量或有限训练数据的分类问题上表现出了显著的成功。此外,LLM 还能够为其行为提供自然语言的解释和理由,从而增强分类过程的可解释性和透明度。通过利用 LLM 的能力,我们可以构建超越传统机器学习方法的分类系统,并在数据稀缺或业务需求复杂的场景中表现出色。
在本指南中,我们将探索如何利用 LLM 来处理高级分类任务。我们将涵盖以下关键组件和步骤:
数据准备:我们将首先准备训练和测试数据。训练数据将用于构建分类模型,而测试数据将用于评估其性能。正确的数据准备对于确保分类系统的有效性至关重要。
Prompt 工程:Prompt 工程在发挥 LLM 分类能力方面起着至关重要的作用。我们将设计一个 prompt 模板来定义用于分类的 prompt 的结构和格式。该 prompt 模板将包含用户查询、类别定义以及来自向量数据库的相关示例。通过精心设计 prompt,我们可以引导 LLM 生成准确且与上下文相关的分类。
实现检索增强生成(RAG):为了增强分类过程,我们将使用向量数据库来存储和高效检索训练数据的 embedding。向量数据库支持相似性搜索,使我们能够为给定查询找到最相关的示例。通过用检索到的示例来增强 LLM,我们可以提供额外的上下文并提高生成分类的准确性。
测试和评估:构建分类系统后,我们将使用转换后的测试数据严格测试其性能。我们将遍历测试查询,使用分类函数对每个查询进行分类,并将预测类别与预期类别进行比较。通过分析分类结果,我们可以评估系统的有效性并找出需要改进的领域。
问题定义:保险支持工单分类器
注意:本示例中使用的问题定义、数据和标签是由 Claude 3 Opus 合成生成的
在保险行业,客户支持在确保客户满意度和留存率方面发挥着至关重要的作用。保险公司每天都会收到大量涵盖各种主题的支持工单,如账单、保单管理、理赔协助等。手动分类这些工单可能既耗时又低效,导致响应时间延长并可能影响客户体验。
类别定义
- 账单咨询(Billing Inquiries)
- 关于发票、费用、收费和保费的询问
- 要求澄清账单报表的请求
- 关于付款方式和截止日期的询问
- 保单管理(Policy Administration)
- 要求更改、更新或取消保单的请求
- 关于保单续保和恢复的询问
- 关于添加或删除保险选项的询问
- 理赔协助(Claims Assistance)
- 关于理赔流程和申请程序的询问
- 要求协助提交理赔文件的请求
- 关于理赔状态和赔付时间线的询问
- 保险范围说明(Coverage Explanations)
- 关于特定保单类型下承保范围的询问
- 要求澄清保险限额和免赔额的请求
- 关于免赔额和自付费用的询问
- 报价和方案(Quotes and Proposals)
- 要求新保单报价和价格比较的请求
- 关于可用折扣和捆绑选项的询问
- 关于从其他保险公司转保的询问
- 账户管理(Account Management)
- 要求登录凭证或重置密码的请求
- 关于在线账户功能和操作的询问
- 关于更新联系方式或个人信息的询问
- 账单争议(Billing Disputes)
- 关于意外或错误收费的投诉
- 要求退款或保费调整的请求
- 关于滞纳金或催收通知的询问
- 理赔争议(Claims Disputes)
- 关于被拒赔或赔付不足的投诉
- 要求重新审议理赔决定的请求
- 关于申诉理赔结果的询问
- 保单比较(Policy Comparisons)
- 关于不同保单选项之间差异的询问
- 要求帮助在不同保险级别之间做决定的请求
- 关于保单与竞争对手产品比较的询问
- 一般咨询(General Inquiries)
- 关于公司联系信息或营业时间的询问
- 要求获取产品或服务一般信息的请求
- 不适合归入其他类别的询问
加载和准备数据
现在我们已经定义了问题和类别,让我们加载带标签的训练和测试数据。我们将读取 TSV 文件,将其转换为结构化格式,并为评估准备测试集。
训练数据包含 68 个带标签的示例,稍后我们将用于检索增强生成(RAG)。测试集也包含 68 个示例,用于评估我们的分类方法。
我们将使用以下数据集: - ./data/test.tsv - ./data/train.tsv
import pandas as pd
data = {
"train": [],
"test": [],
}
# Helper function to convert a DataFrame to a list of dictionaries
def dataframe_to_dict_list(df):
return df.apply(lambda x: {"text": x["text"], "label": x["label"]}, axis=1).tolist()
# Read the TSV file into a DataFrame
test_df = pd.read_csv("./data/test.tsv", sep="\t")
data["test"] = dataframe_to_dict_list(test_df)
train_df = pd.read_csv("./data/train.tsv", sep="\t")
data["train"] = dataframe_to_dict_list(train_df)
# Understand the labels in the dataset
labels = list(set(train_df["label"].unique()))
# Print the first training example and the number of training examples
print(data["train"][0], len(data["train"]))
# Create the test set
X_test = [example["text"] for example in data["test"]]
y_test = [example["label"] for example in data["test"]]
# Print the length of the test set
print(len(X_test), len(y_test)){'text': 'I just got my auto policy renewal bill and the cost seems to be more than what I usually pay. Could you explain the reason for the increase?', 'label': 'Billing Inquiries'} 68
68 68
```## 评估框架
在我们构建分类器之前,让我们设置一个评估框架来衡量它们的性能。我们将创建两个关键函数:
1. **`evaluate(X, y, classifier, batch_size)`**:使用并发执行来加速对所有测试示例运行你的分类器,然后计算准确度指标并生成混淆矩阵。`batch_size`参数控制要发起多少个并发API请求。
2. **`plot_confusion_matrix(cm, labels)`**:将混淆矩阵可视化,以显示哪些类别被相互混淆,有助于识别分类器在哪些方面表现不佳。
这个框架使我们能够经验性地比较不同的分类方法,并不仅了解整体准确度,还能了解哪些具体类别具有挑战性。
**速率限制**:默认情况下,`MAXIMUM_CONCURRENT_REQUESTS`设置为1。如果你有更高的使用层,你可以增加这个值来加速评估。请参阅[速率限制文档](https://docs.claude.com/en/api/rate-limits)以获取你使用层的限制。
```python
def evaluate(X, y, classifier, batch_size):
# 代码实现
pass
def plot_confusion_matrix(cm, labels):
# 代码实现
pass
``````python
import concurrent.futures
import matplotlib.pyplot as plt
import numpy as np
from sklearn.metrics import classification_report, confusion_matrix
MAXIMUM_CONCURRENT_REQUESTS = 5
def plot_confusion_matrix(cm, labels):
# Visualize the confusion matrix
fig, ax = plt.subplots(figsize=(8, 8))
im = ax.imshow(cm, cmap="Blues")
# Add colorbar
ax.figure.colorbar(im, ax=ax)
# Set tick labels and positions
ax.set_xticks(np.arange(len(labels)))
ax.set_yticks(np.arange(len(labels)))
ax.set_xticklabels(labels, rotation=45, ha="right")
ax.set_yticklabels(labels)
# Add labels to each cell
thresh = cm.max() / 2.0
for i in range(len(labels)):
for j in range(len(labels)):
ax.text(
j,
i,
cm[i, j],
ha="center",
va="center",
color="white" if cm[i, j] > thresh else "black",
)
# Set labels and title
plt.xlabel("Predicted Labels")
plt.ylabel("True Labels")
plt.title("Confusion Matrix")
plt.tight_layout()
plt.show()
def evaluate(X, y, classifier, batch_size=MAXIMUM_CONCURRENT_REQUESTS):
# Initialize lists to store the predicted and true labels
y_true = []
y_pred = []
# Store results with their original index to maintain order
results = [None] * len(X)
# Create a ThreadPoolExecutor with limited workers
with concurrent.futures.ThreadPoolExecutor(max_workers=batch_size) as executor:
# Submit tasks one at a time, but executor will only run batch_size concurrently
future_to_index = {executor.submit(classifier, x): i for i, x in enumerate(X)}
# Process results as they complete
for future in concurrent.futures.as_completed(future_to_index):
index = future_to_index[future]
predicted_label = future.result()
results[index] = predicted_label
# Extract predictions and true labels in order
y_pred = results
y_true = y
# Normalize y_true and y_pred
y_true = [label.strip() for label in y_true]
y_pred = [label.strip() for label in y_pred]
# Calculate the classification metrics
report = classification_report(y_true, y_pred, labels=labels, zero_division=1)
cm = confusion_matrix(y_true, y_pred, labels=labels)
print(report)
plot_confusion_matrix(cm, labels)
```## 基线:随机分类器
为了建立一个性能基线和验证我们的评估框架是否正确工作,让我们从一个随机分类器开始,该分类器为每张票据随机选择一个类别。
这为我们提供了一个性能的下限:任何真正的分类方法都应该显著优于随机猜测(在10个类别中,随机猜测的准确率应该大约为10%)。这个基线帮助我们量化我们的上下文工程实际上增加了多少价值。
```python
# 以下为示例代码
def random_classifier(ticket):
# ...随机选择类别逻辑...
return category
随机分类器仅用于建立基线,实际应用中应使用更复杂的分类方法。 :::```python import random
def random_classifier(text): return random.choice(labels)
```python
print("Evaluating the random classification method on the test set...")
evaluate(X_test, y_test, random_classifier)
Evaluating the random classification method on the test set...
precision recall f1-score support
Claims Assistance 0.14 0.17 0.15 6
Quotes and Proposals 0.40 0.40 0.40 5
Claims Disputes 0.38 0.33 0.35 9
Coverage Explanations 0.29 0.22 0.25 9
Billing Disputes 0.20 0.11 0.14 9
Policy Administration 0.25 0.33 0.29 6
Billing Inquiries 0.00 0.00 0.00 6
General Inquiries 0.00 0.00 0.00 7
Policy Comparisons 0.00 0.00 0.00 5
Account Management 0.00 0.00 0.00 6
accuracy 0.16 68
macro avg 0.17 0.16 0.16 68
weighted avg 0.18 0.16 0.17 68
<Figure size 800x800 with 2 Axes>
```如预期的那样,从随机猜测来看,混淆矩阵显示预测结果散布在所有类别中,没有明显的模式。对角线(正确预测)仅显示每个类别1-4个正确的分类,而错误分布是随机的。
这证实了我们的基线大约10%的准确率——纯粹是10个类别下的随机表现。任何结构化的分类方法都应该显示出更强烈的对角线模式,表明模型实际上是在学习类别区分而不是猜测。
### 简单分类测试
现在让我们使用Claude构建一个简单的分类器。
首先,我们将类别编码为XML格式。这将使Claude更容易解释信息。在XML中编码信息是一种通用的提示策略,更多信息请参阅[这里](https://docs.claude.com/en/docs/build-with-claude/prompt-engineering/use-xml-tags)。
```python
# 以下为示例代码
def classify(data):
# 代码逻辑
pass
``````python
import textwrap
categories = textwrap.dedent("""<category>
<label>Billing Inquiries</label>
<content> Questions about invoices, charges, fees, and premiums Requests for clarification on billing statements Inquiries about payment methods and due dates
</content>
</category>
<category>
<label>Policy Administration</label>
<content> Requests for policy changes, updates, or cancellations Questions about policy renewals and reinstatements Inquiries about adding or removing coverage options
</content>
</category>
<category>
<label>Claims Assistance</label>
<content> Questions about the claims process and filing procedures Requests for help with submitting claim documentation Inquiries about claim status and payout timelines
</content>
</category>
<category>
<label>Coverage Explanations</label>
<content> Questions about what is covered under specific policy types Requests for clarification on coverage limits and exclusions Inquiries about deductibles and out-of-pocket expenses
</content>
</category>
<category>
<label>Quotes and Proposals</label>
<content> Requests for new policy quotes and price comparisons Questions about available discounts and bundling options Inquiries about switching from another insurer
</content>
</category>
<category>
<label>Account Management</label>
<content> Requests for login credentials or password resets Questions about online account features and functionality Inquiries about updating contact or personal information
</content>
</category>
<category>
<label>Billing Disputes</label>
<content> Complaints about unexpected or incorrect charges Requests for refunds or premium adjustments Inquiries about late fees or collection notices
</content>
</category>
<category>
<label>Claims Disputes</label>
<content> Complaints about denied or underpaid claims Requests for reconsideration of claim decisions Inquiries about appealing a claim outcome
</content>
</category>
<category>
<label>Policy Comparisons</label>
<content> Questions about the differences between policy options Requests for help deciding between coverage levels Inquiries about how policies compare to competitors' offerings
</content>
</category>
<category>
<label>General Inquiries</label>
<content> Questions about company contact information or hours of operation Requests for general information about products or services Inquiries that don't fit neatly into other categories
</content>
</category>""")
```## 构建简单分类器
现在,让我们使用Claude构建我们的第一个真正的分类器。`simple_classify`函数展示了三种关键的提示工程技巧:
1. **结构化提示模板**:我们以清晰的XML格式提供类别定义和支持工单,使Claude能够轻松解析信息。
2. **控制输出并预填充**:通过以`<category>`开始助手的响应并设置`stop_sequences=["</category>"]`,我们强制Claude只输出类别标签——没有解释或额外文本。这使得响应解析可靠且确定。
3. **确定性的分类**:设置`temperature=0.0`确保对于相同的输入有一致的预测,这对于分类任务至关重要。```python
def simple_classify(X):
prompt = (
textwrap.dedent("""
You will classify a customer support ticket into one of the following categories:
<categories>
{{categories}}
</categories>
Here is the customer support ticket:
<ticket>
{{ticket}}
</ticket>
Respond with just the label of the category between category tags.
""")
.replace("{{categories}}", categories)
.replace("{{ticket}}", X)
)
response = client.messages.create(
messages=[
{"role": "user", "content": prompt},
{"role": "assistant", "content": "<category>"},
],
stop_sequences=["</category>"],
max_tokens=4096,
temperature=0.0,
model=MODEL,
)
# Extract the result from the response
result = response.content[0].text # pyright: ignore[reportAttributeAccessIssue]
return result.strip()
print("Evaluating the simple classification method on the test set...")
evaluate(X_test, y_test, simple_classify)Evaluating the simple classification method on the test set...
precision recall f1-score support
Claims Assistance 1.00 0.67 0.80 6
Quotes and Proposals 1.00 0.60 0.75 5
Claims Disputes 1.00 1.00 1.00 9
Coverage Explanations 0.53 1.00 0.69 9
Billing Disputes 0.67 0.67 0.67 9
Policy Administration 0.71 0.83 0.77 6
Billing Inquiries 0.50 0.50 0.50 6
General Inquiries 1.00 0.00 0.00 7
Policy Comparisons 0.71 1.00 0.83 5
Account Management 1.00 1.00 1.00 6
accuracy 0.74 68
macro avg 0.81 0.73 0.70 68
weighted avg 0.80 0.74 0.70 68
<Figure size 800x800 with 2 Axes>
```非常好!混淆矩阵显示出强烈的对角线模式,大多数类别都实现了高准确率。
总体准确率达到了 ~70%,这显著优于随机猜测,但相似类别之间的混淆表明Claude需要更多的上下文来做出更精细的区分。
为了解决这些混淆模式,我们将通过提供从我们的训练数据中提取的相关示例来为Claude添加**检索增强生成(RAG)**。以下是其工作原理:
1. **嵌入训练示例**:使用VoyageAI的嵌入模型将所有68个训练票据转换为向量嵌入
2. **语义搜索**:对于每个新的票据,根据余弦相似度找到5个最相似的训练示例
3. **增强提示**:将这些相似示例包含在分类提示中,以引导Claude
这种方法在分类中特别有效,因为:
- 相似的过去示例帮助Claude区分语义上接近的类别
- 少样本学习提高了准确率,无需微调
- 检索是动态的——每个查询都得到最相关的示例
我们将构建一个简单的`VectorDB`类来处理嵌入存储和相似度搜索,使用[VoyageAI的嵌入模型](https://docs.claude.com/en/docs/embeddings)。
::: callout-attention
请注意,以下代码块中的变量名和函数名将保持不变:
```python
def classify_ticket(ticket):
# ...
import json import os import pickle
import numpy as np import voyageai
class VectorDB: def init(self, api_key=None): if api_key is None: api_key = os.getenv(“VOYAGE_API_KEY”) self.client = voyageai.Client(api_key=api_key) self.embeddings = [] self.metadata = [] self.query_cache = {} self.db_path = “./data/vector_db.pkl”
def load_data(self, data):
# Check if the vector database is already loaded
if self.embeddings and self.metadata:
print("Vector database is already loaded. Skipping data loading.")
return
# Check if vector_db.pkl exists
if os.path.exists(self.db_path):
print("Loading vector database from disk.")
self.load_db()
return
texts = [item["text"] for item in data]
# Embed more than 128 documents with a for loop
batch_size = 128
result = [
self.client.embed(texts[i : i + batch_size], model="voyage-2").embeddings
for i in range(0, len(texts), batch_size)
]
# Flatten the embeddings
self.embeddings = [embedding for batch in result for embedding in batch]
self.metadata = [item for item in data]
self.save_db()
# Save the vector database to disk
print("Vector database loaded and saved.")
def search(self, query, k=5, similarity_threshold=0.75):
query_embedding = None
if query in self.query_cache:
query_embedding = self.query_cache[query]
else:
query_embedding = self.client.embed([query], model="voyage-2").embeddings[0]
self.query_cache[query] = query_embedding
if not self.embeddings:
raise ValueError("No data loaded in the vector database.")
similarities = np.dot(self.embeddings, query_embedding)
top_indices = np.argsort(similarities)[::-1]
top_examples = []
for idx in top_indices:
if similarities[idx] >= similarity_threshold:
example = {
"metadata": self.metadata[idx],
"similarity": similarities[idx],
}
top_examples.append(example)
if len(top_examples) >= k:
break
self.save_db()
return top_examples
def save_db(self):
data = {
"embeddings": self.embeddings,
"metadata": self.metadata,
"query_cache": json.dumps(self.query_cache),
}
with open(self.db_path, "wb") as file:
pickle.dump(data, file)
def load_db(self):
if not os.path.exists(self.db_path):
raise ValueError(
"Vector database file not found. Use load_data to create a new database."
)
with open(self.db_path, "rb") as file:
data = pickle.load(file)
self.embeddings = data["embeddings"]
self.metadata = data["metadata"]
self.query_cache = json.loads(data["query_cache"])
VoyageAI为没有关联信用卡的账户设置了每分钟3次请求的限制。为了便于演示,我们将利用缓存。
::: callout-notice
请注意,以下代码块中的变量名和函数名将保持英文。
:::
```python
# 加载数据
def load_data():
# 代码实现
pass
# 训练模型
def train_model():
# 代码实现
pass
# 保存模型
def save_model():
# 代码实现
pass
{
"embedding": "嵌入",
"token": "标记",
"prompt": "提示",
"Agent": "代理",
"API": "API",
"Python": "Python",
"JSON": "JSON",
"SQL": "SQL"
}
``````python
vectordb = VectorDB()
vectordb.load_data(data["train"])Loading vector database from disk.
```## RAG 增强分类器
现在让我们使用检索增强生成(RAG)来重建我们的分类器。`rag_classify` 函数通过以下方式增强了简单的分类器:
1. **检索相似示例**:对于每张工单,我们在向量数据库中搜索与5个语义最相似的训练示例
2. **格式化为少样本示例**:我们使用 `<query>` 和 `<label>` 标签将这些示例结构化为XML格式,向Claude展示相似的工单是如何被分类的
3. **增强提示**:在请求Claude对新的工单进行分类之前,我们将这些示例注入到提示中
这种少样本学习方法通过展示如何对具有相似语言的工单进行分类的具体示例,帮助Claude更好地区分相似类别。例如,如果测试工单提到“意外费用”,检索过去“账单争议”与“账单查询”的示例有助于Claude理解细微的差别。
让我们看看这能多大程度上提高我们在之前运行中混淆类别的准确性。
```python
def rag_classify(ticket):
# 代码实现
pass
""")
prompt = (
textwrap.dedent("""
You will classify a customer support ticket into one of the following categories:
<categories>
{{categories}}
</categories>
Here is the customer support ticket:
<ticket>
{{ticket}}
</ticket>
Use the following examples to help you classify the query:
<examples>
{{examples}}
</examples>
Respond with just the label of the category between category tags.
""")
.replace("{{categories}}", categories)
.replace("{{ticket}}", X)
.replace("{{examples}}", rag_string)
)
response = client.messages.create(
messages=[
{"role": "user", "content": prompt},
{"role": "assistant", "content": "<category>"},
],
stop_sequences=["</category>"],
max_tokens=4096,
temperature=0.0,
model="claude-haiku-4-5",
)
# Extract the result from the response
result = response.content[0].text.strip()
return result
```python
print("Evaluating the RAG method on the test set...")
evaluate(X_test, y_test, rag_classify)
Evaluating the RAG method on the test set...
precision recall f1-score support
Claims Assistance 1.00 1.00 1.00 6
Quotes and Proposals 1.00 1.00 1.00 5
Claims Disputes 1.00 1.00 1.00 9
Coverage Explanations 0.82 1.00 0.90 9
Billing Disputes 0.82 1.00 0.90 9
Policy Administration 1.00 1.00 1.00 6
Billing Inquiries 1.00 0.67 0.80 6
General Inquiries 1.00 0.71 0.83 7
Policy Comparisons 1.00 1.00 1.00 5
Account Management 1.00 1.00 1.00 6
accuracy 0.94 68
macro avg 0.96 0.94 0.94 68
weighted avg 0.95 0.94 0.94 68
<Figure size 800x800 with 2 Axes>
```## RAG 结果分析
RAG 将我们的准确率从 ~70% 提升至 **94%**。混淆矩阵显示对角线变得更加强烈,大多数类别现在实现了完美或接近完美的准确率。
剩余的少数错误表明,即使有相关示例,一些边缘情况仍然模糊不清。这正是思维链推理可以帮助 Claude 仔细思考微妙区别的地方。
## 结合思维链推理的 RAG
思维链(CoT)提示要求 Claude 在做出分类决策之前“大声思考”。
这个显式的推理过程有助于 Claude 捕捉到直接分类可能错过的微妙区别。例如,区分“我对我的账单有疑问”(账单查询)和“这个费用看起来不对”(账单争议)需要理解意图和语气——这从逐步分析中受益。
`rag_chain_of_thought_classify` 函数添加了一个 `<scratchpad>` 部分,其中 Claude 在输出最终 `<category>` 之前通过这个推理过程进行工作。```python
def rag_chain_of_thought_classify(X):
rag = vectordb.search(X, 5)
rag_string = ""
for example in rag:
rag_string += textwrap.dedent(f"""
<example>
<query>
"{example["metadata"]["text"]}"
</query>
<label>
{example["metadata"]["label"]}
</label>
</example>
""")
prompt = (
textwrap.dedent("""
You will classify a customer support ticket into one of the following categories:
<categories>
{{categories}}
</categories>
Here is the customer support ticket:
<ticket>
{{ticket}}
</ticket>
Use the following examples to help you classify the query:
<examples>
{{examples}}
</examples>
First you will think step-by-step about the problem in scratchpad tags.
You should consider all the information provided and create a concrete argument for your classification.
Respond using this format:
<response>
<scratchpad>Your thoughts and analysis go here</scratchpad>
<category>The category label you chose goes here</category>
</response>
""")
.replace("{{categories}}", categories)
.replace("{{ticket}}", X)
.replace("{{examples}}", rag_string)
)
response = client.messages.create(
messages=[
{"role": "user", "content": prompt},
{"role": "assistant", "content": "<response><scratchpad>"},
],
stop_sequences=["</category>"],
max_tokens=4096,
temperature=0.0,
model=MODEL,
)
# Extract the result from the response
result = response.content[0].text.split("<category>")[1].strip()
return result
print("Evaluating the RAG method with Chain of Thought on the test set...")
evaluate(X_test, y_test, rag_chain_of_thought_classify)Evaluating the RAG method with Chain of Thought on the test set...
precision recall f1-score support
Claims Assistance 1.00 1.00 1.00 6
Quotes and Proposals 1.00 1.00 1.00 5
Claims Disputes 1.00 1.00 1.00 9
Coverage Explanations 1.00 1.00 1.00 9
Billing Disputes 0.82 1.00 0.90 9
Policy Administration 1.00 1.00 1.00 6
Billing Inquiries 1.00 0.67 0.80 6
General Inquiries 1.00 1.00 1.00 7
Policy Comparisons 1.00 1.00 1.00 5
Account Management 1.00 1.00 1.00 6
accuracy 0.97 68
macro avg 0.98 0.97 0.97 68
weighted avg 0.98 0.97 0.97 68
<Figure size 800x800 with 2 Axes>
```## 思维链结果分析
思维链推理将我们的准确率提升至 **97**%,解决了大多数挑战先前方法的边缘情况。
**渐进式改进总结:**
- 随机基线:约 **10**% 准确率
- 简单分类器:约 **70**% 准确率
- RAG 分类器:94% 准确率
- **RAG + 思维链:97% 准确率**
通过明确地对每个分类决策进行推理,Claude更好地区分了模糊案例。
# 使用 Promptfoo 扩展评估
在整个指南中,我们展示了在构建提示时进行 **经验评估** 的重要性。而不是依赖直觉,我们在每个步骤都测量了性能:
- 随机基线:约 **10**% → 简单:约 **70**% → RAG:94% → RAG + CoT:97%
这种数据驱动的方法揭示了哪些技术(RAG、思维链)增加了价值以及增加了多少。有关此方法的更多信息,请参阅我们的 [提示工程指南](https://docs.claude.com/en/docs/prompt-engineering)。
## 超越 Notebook
虽然 Jupyter 笔记本非常适合快速迭代和探索,但生产分类系统需要更健壮的评估基础设施:
- **更大的测试集**:您将希望评估数百或数千个示例,而不仅仅是 68 个
- **多个提示变体**:对不同措辞、结构和方法的 A/B 测试
- **模型比较**:在不同 Claude 模型(Haiku 与 Sonnet)或温度设置中进行测试
- **回归检测**:确保提示更改不会意外地损害准确率
- **版本控制**:随着系统的发展,跟踪提示性能随时间的变化
这就是像 [Promptfoo](https://www.promptfoo.dev/) 这样的专用评估工具变得至关重要的地方。
## 运行 Promptfoo 评估
Promptfoo 是一个开源的 LLM 评估工具包,它自动化了跨多个配置的提示测试。对于本指南,我们已经设置了一个 Promptfoo 评估,测试了所有三种方法(简单、RAG、RAG w/ CoT)在不同温度设置下的表现。
**运行评估:**
1. 导航到评估目录:`cd ./evaluation`
2. 按照目录下 `./evaluation/README.md` 中的设置说明进行操作
3. 运行评估,然后返回此处分析以下结果
结果将展示每个提示变体在整个测试集中的表现,揭示出仅从笔记本测试中可能不明显出现的模式。
The results will show how each prompt variant performs across the full test set, revealing patterns that might not be obvious from notebook testing alone.
::: callout-notice
请注意,以下代码块中的 `RAG` 和 `embedding` 是技术术语,应保持英文。
:::
```python
# 以下代码用于评估
def evaluate(prompt, RAG, embedding):
# ...
pass
``````python
import json
import pandas as pd
promptfoo_results = pd.read_csv("./data/results.csv")
examples_columns = promptfoo_results.columns[2:]
number_of_providers = 5
number_of_prompts = 3
prompts = ["Simple", "RAG", "RAG w/ CoT"]
columns = ["label", "text"] + [
json.loads(examples_columns[prompt * number_of_providers + provider])["provider"]
+ " Prompt: "
+ str(prompts[prompt])
for prompt in range(number_of_prompts)
for provider in range(number_of_providers)
]
promptfoo_results.columns = columns
result = (
promptfoo_results.iloc[:, 2:].astype(str).apply(lambda x: x.str.count("PASS")).sum()
/ len(promptfoo_results)
* 100
).sort_values(ascending=False)
print(result)
Haiku: T-0.0 Prompt: RAG w/ CoT 95.588235 Haiku: T-0.2 Prompt: RAG w/ CoT 95.588235 Haiku: T-0.8 Prompt: RAG w/ CoT 95.588235 Haiku: T-0.0 Prompt: RAG 94.117647 Haiku: T-0.4 Prompt: RAG 94.117647 Haiku: T-0.6 Prompt: RAG 94.117647 Haiku: T-0.4 Prompt: RAG w/ CoT 94.117647 Haiku: T-0.6 Prompt: RAG w/ CoT 94.117647 Haiku: T-0.2 Prompt: RAG 92.647059 Haiku: T-0.8 Prompt: RAG 89.705882 Haiku: T-0.6 Prompt: Simple 72.058824 Haiku: T-0.0 Prompt: Simple 70.588235 Haiku: T-0.2 Prompt: Simple 70.588235 Haiku: T-0.8 Prompt: Simple 70.588235 Haiku: T-0.4 Prompt: Simple 69.117647 dtype: float64## 使用 Promptfoo 进行系统评估
The results above demonstrate the power of systematic prompt evaluation at scale. While our notebook provided quick iteration during development, Promptfoo enabled us to rigorously test our prompts across multiple dimensions:
我们测试了什么
Using Promptfoo, we evaluated all three prompt approaches (Simple, RAG, RAG w/ CoT) across: - Multiple temperature settings (0.0, 0.2, 0.4, 0.6, 0.8): Testing whether deterministic classification (T=0.0) outperforms slightly randomized outputs - The same test set (68 examples): Ensuring fair comparison across all configurations - Automated pass/fail checking: Each prediction is automatically compared against ground truth labels
关键发现
The Promptfoo results confirm our notebook findings while revealing additional insights:
Temperature has minimal impact on CoT: RAG w/ CoT achieves 95.59% accuracy consistently across T=0.0, 0.2, and 0.8, suggesting the chain-of-thought reasoning stabilizes outputs regardless of sampling randomness
RAG is robust to temperature variation: RAG performance stays strong (89-94%) across most temperature settings, though T=0.0 performs best (94.12%)
Simple prompts are temperature-agnostic: The simple classifier hovers around 70% accuracy regardless of temperature, confirming that without RAG or CoT, the model relies primarily on category definitions
Production recommendation: Use
temperature=0.0with RAG w/ CoT for maximum consistency and accuracy (95.59%)
To run these evaluations yourself, see ./evaluation/README.md for setup instructions.
中文读者提示:本章节的代码和输出为英文原文。如需理解具体实现细节,可参考上方中文导读和代码注释。如有疑问,欢迎在 GitHub Issues 讨论。