멀티턴 평가
멀티턴 평가 (Multi-Turn Evals)
대화를 시뮬레이션하고 멀티턴 유스케이스에 대한 종단 간 테스트를 실행해볼게요. 멀티턴 평가에는 대화형 골든스로 이뤄진 멀티턴 데이터셋, 챗봇을 감싸 대화 턴을 생성하는 콜백 함수, 그리고 사용할 멀티턴 메트릭 목록이 필요해요.
출처: 문서
본문
개요
멀티턴 평가에는 다음이 필요해요:
- 대화형 골든스(conversational goldens)로 이뤄진 멀티턴 데이터셋
- 챗봇을 감싸 대화 턴을 생성하는 콜백 함수
- 평가에 사용할 멀티턴 메트릭 목록
유저 턴을 시뮬레이션하려면 각 대화형 골든스에
scenario가 있어야 해요. 더 높은 품질의 시뮬레이션을 위해 user description을 제공하는 것도 강력히 권장돼요.
동작 방식
- Confident AI에서 멀티턴 데이터셋 가져오기
- 챗봇을 호출해 대화 턴을 생성하는 콜백 정의
- 데이터셋의 각 골든스에 대해 대화 시뮬레이션
- 생성된 테스트 케이스에 평가 실행
콜백 정의
챗봇을 감싸 다음 대화 턴을 생성하는 콜백을 정의해요:
from deepeval.test_case import Turn
from typing import List
def chatbot_callback(input: str, turns: List[Turn], thread_id: str) -> Turn:
messages = [{"role": turn.role, "content": turn.content} for turn in turns]
messages.append({"role": "user", "content": input})
response = your_chatbot(messages) # Replace with your chatbot
return Turn(role="assistant", content=response)
콜백은 인풋과, 선택적으로
Turn목록과 스레드 id를 받아야 해요. 그리고 대화의 다음Turn을 반환해야 해요.
로컬에서 평가 실행
로컬 평가는 Python deepeval 라이브러리로만 가능해요. TypeScript나 다른 언어라면 원격 평가로 건너뛰세요.
데이터셋 가져오기
멀티턴 데이터셋을 가져와요(아직 없다면 만들기):
from deepeval.dataset import EvaluationDataset
dataset = EvaluationDataset()
dataset.pull(alias="YOUR-DATASET-ALIAS")
대화 시뮬레이션
콜백으로 시뮬레이터를 만들고 골든스에서 테스트 케이스를 생성해요:
from deepeval.simulator import ConversationSimulator
simulator = ConversationSimulator(model_callback=chatbot_callback)
for golden in dataset.goldens:
test_case = simulator.simulator(golden)
dataset.add_test_case(test_case)
ConversationalTestCase에서turns를 생성하거나 골든스 속성을 수동으로 매핑하는 다른 방법을 써도 돼요.
평가 실행
evaluate() 함수는 테스트 스위트를 실행하고 결과를 Confident AI에 업로드해요:
from deepeval.metrics import TurnRelevancyMetric
from deepeval import evaluate
# Replace with your metrics
evaluate(test_cases=dataset.test_cases, metrics=[TurnRelevancyMetric()])
완료! 새로 만들어진 공유 가능한 테스팅 리포트 링크가 보일 거예요.
- 각 메트릭은 모든 테스트 케이스에 적용돼요(예: 테스트 케이스 10개 × 메트릭 2개 = 평가 20개)
- 모든 메트릭이 통과해야 테스트 케이스가 통과해요
- 테스트 런의 통과율은 통과한 테스트 케이스의 비율이에요
deepeval은 기본적으로 브라우저를 자동으로 열어요. 이 동작을 끄려면CONFIDENT_BROWSER_OPEN=NO를 설정하세요.
멀티턴 테스팅 리포트
원격에서 평가 실행 (Run Evals Remotely)
메트릭 컬렉션 만들기
Project > Metric > Collections로 이동해요:
원격 평가용 메트릭 컬렉션
멀티턴 컬렉션을 만드는 것을 잊지 마세요.
데이터셋 가져오고 대화 시뮬레이션
Python
시뮬레이션을 원격으로 실행하려면 run_remote를 true로 설정해요:
from deepeval.simulator import ConversationSimulator
from deepeval.dataset import EvaluationDataset
dataset = EvaluationDataset()
dataset.pull(alias="YOUR-DATASET-ALIAS")
simulator = ConversationSimulator(model_callback=chatbot_callback, run_remote=True)
for golden in dataset.goldens:
test_case = simulator.simulator(golden)
dataset.add_test_case(test_case)
TypeScript
import {
ConversationalGolden,
ConversationSimulator,
EvaluationDataset,
} from "deepeval";
const dataset = new EvaluationDataset();
await dataset.pull({ alias: "YOUR-DATASET-ALIAS" });
const simulator = new ConversationSimulator({ modelCallback: chatbotCallback });
const testCases = await simulator.simulate({
conversationalGoldens: dataset.goldens as ConversationalGolden[],
});
for (const testCase of testCases) {
dataset.addTestCase(testCase);
}
TypeScript 예시 콜백 보려면 클릭
const chatbotCallback = async (args: {
input: string;
turns: Turn[];
threadId: string;
}): Promise<Turn> => {
return new Turn({
role: "assistant",
content: your_chatbot(args.input),
});
};
curl
/v1/simulate를 사용해 각 골든스의 첫 번째 유저 턴을 생성해요:
curl -X POST https://api.confident-ai.com/v1/simulate \
-H "CONFIDENT_API_KEY: <PROJECT-API-KEY>" \
-H "Content-Type: application/json" \
-d '{
"golden": [{
"scenario": "A frustrated user asking for a refund.",
"userDescription": "A white male who is a customer for over 2 years."
}]
}'
원하는 턴 수만큼 시뮬레이션할 때까지 /v1/simulate를 계속 호출해요.
/v1/simulate엔드포인트는 다음 유저 턴을 반환해요. 이 턴을 골든스에 추가하고 챗봇의 응답을 덧붙인 뒤 엔드포인트를 다시 호출해야 해요.
turns가 제공됐을 때 마지막 턴의 역할이assistant가 아니면/v1/simulate는 실패해요.
평가 실행
Python
from deepeval import evaluate
evaluate(test_case=dataset.test_cases, metric_collection="YOUR-COLLECTION-NAME")
TypeScript
import {
ConversationalTestCase,
evaluate,
EvaluationDataset,
} from "deepeval";
const dataset = new EvaluationDataset();
dataset.pull({ alias: "YOUR-DATASET-ALIAS" });
evaluate({
conversationalTestCases: dataset.testCases as ConversationalTestCase[],
metricCollection: "YOUR-COLLECTION-NAME",
});
curl
Request (POST /v1/evaluate) — API reference
curl -X POST "https://api.confident-ai.com/v1/evaluate" \
-H "CONFIDENT_API_KEY: <PROJECT-API-KEY>" \
-H "Content-Type: application/json" \
-d '{
"metricCollection": "Multi-Turn Collection Name",
"conversationalTestCases": [
{
"turns": [
{
"role": "user",
"content": "How tall is Mount Everest?"
},
{
"role": "assistant",
"content": "Mount Everest is approximately 8,848 meters tall."
},
{
"role": "user",
"content": "Wow, that is really high! Has that changed recently?"
},
{
"role": "assistant",
"content": "Yes, a 2020 survey by China and Nepal revised the height to 8,848.86 meters."
}
]
}
]
}'
import requests
response = requests.post(
"https://api.confident-ai.com/v1/evaluate",
headers={
"CONFIDENT_API_KEY": "<PROJECT-API-KEY>",
},
json={
"metricCollection": "Multi-Turn Collection Name",
"conversationalTestCases": [
{
"turns": [
{
"role": "user",
"content": "How tall is Mount Everest?"
},
{
"role": "assistant",
"content": "Mount Everest is approximately 8,848 meters tall."
},
{
"role": "user",
"content": "Wow, that is really high! Has that changed recently?"
},
{
"role": "assistant",
"content": "Yes, a 2020 survey by China and Nepal revised the height to 8,848.86 meters."
}
]
}
]
},
)
print(response.json())
const response = await fetch("https://api.confident-ai.com/v1/evaluate", {
method: "POST",
headers: {
"CONFIDENT_API_KEY": "<PROJECT-API-KEY>",
"Content-Type": "application/json",
},
body: JSON.stringify({
"metricCollection": "Multi-Turn Collection Name",
"conversationalTestCases": [
{
"turns": [
{
"role": "user",
"content": "How tall is Mount Everest?"
},
{
"role": "assistant",
"content": "Mount Everest is approximately 8,848 meters tall."
},
{
"role": "user",
"content": "Wow, that is really high! Has that changed recently?"
},
{
"role": "assistant",
"content": "Yes, a 2020 survey by China and Nepal revised the height to 8,848.86 meters."
}
]
}
]
}),
});
const data = await response.json();
console.log(data);
package main
import (
"fmt"
"io"
"net/http"
"strings"
)
func main() {
body := `{
"metricCollection": "Multi-Turn Collection Name",
"conversationalTestCases": [
{
"turns": [
{
"role": "user",
"content": "How tall is Mount Everest?"
},
{
"role": "assistant",
"content": "Mount Everest is approximately 8,848 meters tall."
},
{
"role": "user",
"content": "Wow, that is really high! Has that changed recently?"
},
{
"role": "assistant",
"content": "Yes, a 2020 survey by China and Nepal revised the height to 8,848.86 meters."
}
]
}
]
}`
req, err := http.NewRequest("POST", "https://api.confident-ai.com/v1/evaluate", strings.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("CONFIDENT_API_KEY", "<PROJECT-API-KEY>")
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer res.Body.Close()
out, err := io.ReadAll(res.Body)
if err != nil {
panic(err)
}
fmt.Println(string(out))
}
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
public class Example {
public static void main(String[] args) throws Exception {
String body = """
{
"metricCollection": "Multi-Turn Collection Name",
"conversationalTestCases": [
{
"turns": [
{
"role": "user",
"content": "How tall is Mount Everest?"
},
{
"role": "assistant",
"content": "Mount Everest is approximately 8,848 meters tall."
},
{
"role": "user",
"content": "Wow, that is really high! Has that changed recently?"
},
{
"role": "assistant",
"content": "Yes, a 2020 survey by China and Nepal revised the height to 8,848.86 meters."
}
]
}
]
}""";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://api.confident-ai.com/v1/evaluate"))
.header("CONFIDENT_API_KEY", "<PROJECT-API-KEY>")
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response = HttpClient.newHttpClient()
.send(request, HttpResponse.BodyHandlers.ofString());
System.out.println(response.body());
}
}
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let response = reqwest::Client::new()
.post("https://api.confident-ai.com/v1/evaluate")
.header("CONFIDENT_API_KEY", "<PROJECT-API-KEY>")
.json(&json!({
"metricCollection": "Multi-Turn Collection Name",
"conversationalTestCases": [
{
"turns": [
{
"role": "user",
"content": "How tall is Mount Everest?"
},
{
"role": "assistant",
"content": "Mount Everest is approximately 8,848 meters tall."
},
{
"role": "user",
"content": "Wow, that is really high! Has that changed recently?"
},
{
"role": "assistant",
"content": "Yes, a 2020 survey by China and Nepal revised the height to 8,848.86 meters."
}
]
}
]
}))
.send()
.await?;
println!("{}", response.text().await?);
Ok(())
}
고급 사용법 (Advanced Usage)
조기 종료 (Early Stopping)
최대 턴 수에 도달하기 전에 시뮬레이션을 자연스럽게 멈추려면 각 골든스에 expected_outcome을 제공해요. 예상 결과에 도달하면 대화가 자동으로 끝나요.
Python
from deepeval.dataset import ConversationalGolden
conversation_golden = ConversationalGolden(
scenario="Andy Byron wants to purchase a VIP ticket to a cold play concert.",
expected_outcome="Successful purchase of a ticket.",
user_description="Andy Byron is the CEO of Astronomer.",
)
expected_outcome을 사용할 수 없으면simulate메서드의max_user_simulations파라미터가 일정 수의 유저 턴 후에 시뮬레이션을 멈춰요. 기본값은 10이에요.
TypeScript
import { ConversationalGolden } from "deepeval";
const conversationGolden = new ConversationalGolden({
scenario: "Andy Byron wants to purchase a VIP ticket to a cold play concert.",
expectedOutcome: "Successful purchase of a ticket.",
userDescription: "Andy Byron is the CEO of Astronomer.",
});
curl
예상 결과가 제공되고 시뮬레이션 중에 도달하면 /v1/simulate 응답에서 대화가 완료된 것으로 표시돼요.
curl -X POST https://api.confident-ai.com/v1/simulate \
-H "CONFIDENT_API_KEY: <PROJECT-API-KEY>" \
-H "Content-Type: application/json" \
-d '{
"golden": [{
"scenario": "Andy Byron wants to purchase a VIP ticket to a cold play concert.",
"userDescription": "Andy Byron is the CEO of Astronomer.",
"expectedOutcome": "Successful purchase of a ticket."
}],
"callback": "https://your-callback-endpoint.com/simulate"
}'
기존 턴 확장
각 골든스에 기존 Turn들을 제공해 기존 대화를 확장할 수 있어요. 시뮬레이터가 기존 턴을 자동으로 감지하고 그 턴부터 계속 시뮬레이션해요.
Python
from deepeval.dataset import ConversationalGolden
from deepeval.test_case import Turn
conversation_golden = ConversationalGolden(
scenario="Andy Byron wants to purchase a VIP ticket to a cold play concert.",
user_description="Andy Byron is the CEO of Astronomer.",
turns=[
Turn(role="user", content="Hi"),
Turn(role="assistant", content="Hello! How can I help you today?"),
Turn(role="user", content="I want to purchase a VIP ticket to a cold play concert."),
]
)
TypeScript
import { ConversationalGolden, Turn } from "deepeval";
const firstTurn = new Turn({
role: "user",
content: "Hi",
});
const secondTurn = new Turn({
role: "assistant",
content: "Hello! How can I help you today?",
});
const thirdTurn = new Turn({
role: "user",
content: "I want to purchase a VIP ticket to a cold play concert.",
});
const conversationGolden = new ConversationalGolden({
scenario: "Andy Byron wants to purchase a VIP ticket to a cold play concert.",
userDescription: "Andy Byron is the CEO of Astronomer.",
turns: [firstTurn, secondTurn, thirdTurn],
});
curl
curl -X POST https://api.confident-ai.com/v1/simulate \
-H "CONFIDENT_API_KEY: <PROJECT-API-KEY>" \
-H "Content-Type: application/json" \
-d '{
"golden": [{
"scenario": "Andy Byron wants to purchase a VIP ticket to a cold play concert.",
"userDescription": "Andy Byron is the CEO of Astronomer.",
"turns": [
{"role": "user", "content": "Hi"},
{"role": "assistant", "content": "Hello! How can I help you today?"}
]
}]
}'