DeepEval
Quick Introduction | DeepEval - The Open-Source LLM Evaluation Framework
いろいろ OpenAI API 前提
CustomModel 使える仕組みではあるが意外と Gemini でも組込みの指標すんなり動かない
Using Custom LLMs for Evaluation | DeepEval - The Open-Source LLM Evaluation Framework
OpenAI API と使える JSON Schema の差異?
VertexAI Gemini と Google Gemini の混同も見られる
langchain-google が悪いやつ、かなり gether しているので...
Thread Safety and with_structured_output Wrapping · Issue 636 · langchain-ai/langchain-google
まあよくあるように英語前提、まあ LLM が賢いので動くは動く
出力のスキーマ割と使うのに DeepEvalBaseLLM のシグネチャでは表現していない
これ別々なのなんなの
deepeval/deepeval/models/gpt_model.py at main · confident-ai/deepeval
deepeval/deepeval/models/gpt_model_schematic.py at main · confident-ai/deepeval
TestCase に input も常に必要
出力だけ評価したいとき・多段の処理でどう扱うか困る
Pytest と統合されている
出力のレポート化は Confident AI 使わせたいのだろう、OSS 版をただ使うだけはちょっといいテーブルがテキストで出るだけ
Running an Evaluation | DeepEval - The Open-Source LLM Evaluation Framework
Quick Introduction | DeepEval - The Open-Source LLM Evaluation Framework
DEEPEVAL_RESULTS_FOLDER=./data を指定すると ./data/yyyyMMdd_hhmmss に JSON が書かれている
これ可視化してあげると良いかなあ
コードは平易で良い
Gemini 等 LangChain 経由で使う
こうかねえ
code:gemini.py
from typing import Generic, TypeVar, overload
from deepeval.models.base_model import DeepEvalBaseLLM
from langchain_core.language_models import BaseChatModel
from langchain_core.output_parsers.string import StrOutputParser
T = TypeVar("T", bound=BaseModel)
class LangChainModel(DeepEvalBaseLLM, GenericT):
def __init__(self, model: BaseChatModel):
self.model = model
def load_model(self) -> BaseChatModel:
return self.model
def _chain(self, schema: typeT | None = None): # type: ignore
m = self.load_model()
if schema is None:
return m | StrOutputParser()
else:
return m.with_structured_output(schema)
@overload
def generate(self, prompt: str) -> str: ...
@overload
def generate(self, prompt: str, schema: typeT) -> T: ...
def generate(self, prompt: str, schema: typeT | None = None) -> T | str:
return self._chain(schema).invoke(prompt)
@overload
async def a_generate(self, prompt: str) -> str: ...
@overload
async def a_generate(self, prompt: str, schema: typeT) -> T: ...
async def a_generate(self, prompt: str, schema: typeT | None = None) -> T | str:
return await self._chain(schema).ainvoke(prompt)
def get_model_name(self):
return getattr(self.model, "model_name", "LangChainModel")
from langchain_google_vertexai import ChatVertexAI, HarmBlockThreshold, HarmCategory
safety_settings = {
HarmCategory.HARM_CATEGORY_UNSPECIFIED: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_HATE_SPEECH: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_HARASSMENT: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT: HarmBlockThreshold.BLOCK_NONE,
HarmCategory.HARM_CATEGORY_CIVIC_INTEGRITY: HarmBlockThreshold.BLOCK_NONE,
}
langchain_model_vertexai = ChatVertexAI(
model_name="gemini-2.0-flash-exp",
safety_settings=safety_settings,
project="pokutuna-playground",
location="us-central1",
temperature=0,
)
model = LangChainModel(model=vertexai)
カスタムメトリック
単に内容が含まれるかを LLM で判定する
code:custom_metric.py
from textwrap import dedent
from deepeval.metrics import BaseMetric
from deepeval.test_case import LLMTestCase
class ContainingOutput(BaseModel):
contains: bool = Field(description="期待する内容が含まれているなら true")
explanation: str = Field(description="結果の説明")
class MyCustomMetric(BaseMetric):
def __init__(self, threshold: float, model: DeepEvalBaseLLM, expected_content: str):
self.threshold = threshold
self.model = model
self.expected_content = expected_content
self.evaluation_model = model.get_model_name()
@classmethod
def eval_prompt(cls, output: str, content: str) -> str:
return (
dedent("""
あなたは LLM 出力評価の専門家です
あなたのタスクは LLM の出力に、期待する内容が含まれているかを 0,1 で判断することです
## LLM の出力
<LLM_OUTPUT>
{output}
</LLM_OUTPUT>
## 期待する内容
<EXPECTED>
{content}
</EXPECTED>
""")
.strip()
.format(output=output, content=content)
)
def measure(self, test_case: LLMTestCase) -> float:
try:
prompt = self.eval_prompt(
output=test_case.actual_output,
content=self.expected_content,
)
output: ContainingOutput = self.model.generate(
prompt, schema=ContainingOutput
)
# この output 生成 DeepEvalBaseLLM だから補完効かないね...
# 組込み Metric で LLM に対し型以上の仮定があるものがあるし Custom では具体的な型置いてもいいかも
self.score = 1 if output.contains else 0
self.reason = output.explanation
return self.score
except Exception as e:
self.error = str(e)
raise
async def a_measure(self, test_case, *args, **kwargs):
return self.measure(test_case)
def is_successful(self) -> bool:
if self.error is not None:
return False
return self.score >= self.threshold
def test_custom_metric():
case = LLMTestCase(
input="",
actual_output="おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。おばあさんが川で洗濯をしていると、ドロボーが現れておばあさんの洗濯物を盗み去って行きました。",
)
metric1 = MyCustomMetric(
threshold=1.0,
model=vertexai_gemini,
expected_content="おばあさんの洗濯物は盗み取られた",
)
metric2 = MyCustomMetric(
threshold=1.0,
model=vertexai_gemini,
expected_content="おばあさんは山に行かなかった",
)
metric3 = MyCustomMetric(
threshold=1.0,
model=vertexai_gemini,
expected_content="おじいさんの洗濯物も取られた", # => fail
)
assert_test(case, metric1, metric2, metric3)
出力されるデータ
code:output.json
{
"testFile": "tests/",
"testCases": [
{
"name": "test_custom_metric",
"input": "",
"actualOutput": "おじいさんは山へ芝刈りに、おばあさんは川へ洗濯に行きました。おばあさんが川で洗濯をしていると、ドロボーが現れておばあさんの洗濯物を盗み去っていきました",
"success": false,
"metricsData": [
{
"name": "Base Metric",
"threshold": 1,
"success": true,
"score": 1,
"reason": "LLMの出力には、おばあさんの洗濯物が盗まれたという記述があるので、期待する内容が含まれています。",
"strictMode": false,
"evaluationModel": "gemini-2.0-flash-exp"
},
{
"name": "Base Metric",
"threshold": 1,
"success": true,
"score": 1,
"reason": "LLMの出力におばあさんは山へ行っていないことが記述されているため",
"strictMode": false,
"evaluationModel": "gemini-2.0-flash-exp"
},
],
"runDuration": 4.306083207949996
}
],
"conversationalTestCases": [],
"metricsScores": [],
"runDuration": 0
}
追加のデータ入れたい
metricsData 作っているのはこの辺
confident-ai/deepeval@f359df1 - deepeval/evaluate.py#L78
confident-ai/deepeval@main - deepeval/metrics/indicator.py
testCase のほうのデータ
confident-ai/deepeval@main - deepeval/test_run/api.py#L20
additionalmetadata
confident-ai/deepeval@4450ac8 - deepeval/test_run/test_run.py#L261
書き出しは素朴
hack 的なことしないと追加はできなさそうかなあ
まあ ConfidentAI 使ってほしいわけだからリッチになっていくのは期待できない
上の例だと、Metric の expect_content を人間が分かる程度に出力に含めたいだけだから、name で工夫してもいいかも
code:name.py
@property
def __name__(self):
return f"Contains({self.expected_content})"
あるいは verbose_logs に JSON 埋める
こうなる
https://gyazo.com/9888f3a4b73caa800db584566905db44
https://gyazo.com/93bd32b35df09fdcc4760067ec7480a6
Metrics のパラメータは引数名と同じでないといけない
えー...別名つけれないじゃん
copied_metrics.append(metric_class(**valid_args))
confident-ai/deepeval@main - deepeval/metrics/utils.py#L50