Published2026-05-19
TopicAgent · 源码
Seriesagent eval · 一
Size2485 chars · 5 code blocks · 8 images

agent eval:(一)deepeval

agent eval:(一)deepeval

特性分析

集成支持

deepeval 通过一个统一的 Tracing 核心层来兼容所有框架。无论外部框架的形式如何不同,最终都汇聚到同一套数据模型和 trace 管理器。

  • Monkey-Patch 直接替换 SDK 类方法
  • Callback Handler 实现框架原生回调接口
  • Trace Processor 实现框架的追踪处理器协议
  • OTel 桥接则通过 OpenTelemetry 标准协议转换

所有路径最终都通过 TraceManager 统一管理 span 的生命周期。

在其中不同的数据最终都会转变成span系统

  • BaseSpan 是所有 span 的基类,包含通用字段(uuid、父子关系、时间戳、状态)。
  • 四种子类分别对应 LLM 调用、Agent执行、工具调用、检索器调用,各自携带领域特定的属性。
  • Trace 是顶层容器,包含一棵 span 树。

其核心在于Observer 与 TraceManager

Observer 是一个上下文管理器,负责 span 的创建、注册、上下文传播和结束:

# 简化的 Observer 工作流程
class Observer:
      def __enter__(self):
          # 1. 确保有活跃的 trace(没有则创建)
          # 2. 根据 span_type 创建对应的 span 实例
          # 3. 将 span 注册到 trace_manager.active_spans
          # 4. 设置 current_span_context(ContextVar)
          # 5. 建立父子关系
          return self

      def __exit__(self, exc_type, exc_val, exc_tb):
          # 1. 设置 span 状态(SUCCESS / ERRORED)
          # 2. 记录 end_time
          # 3. 恢复 current_span_context 到父 span
          # 4. 如果是根 span,结束 trace
trace_manager = TraceManager()  # 全局单例

  class TraceManager:
      def configure(self, openai_client=None, anthropic_client=None, ...):
          # 配置客户端 patch、采样率、环境等

      def start_new_trace(self) -> Trace:
          # 创建新 trace,注册到 active_traces

      def end_trace(self, trace_uuid):
          # 结束 trace,根据 eval_session.mode 决定:
          # - 非评估模式:post_trace() 发送到 Confident AI
          # - 评估模式:存入 eval_session 供 evaluate() 使用

      def post_trace(self, trace):
          # 通过后台 worker 线程异步发送 trace 到 API
  # deepeval/tracing/context.py

  current_trace_context: ContextVar[Optional[Trace]] = ContextVar(...)

  current_span_context: ContextVar[Optional[BaseSpan]] = ContextVar(...)

所有集成都通过这两个 ContextVar 实现 span 的嵌套和父子关系。

异步场景下,LangChain 的 CallbackHandler 通过_ctx()上下文管理器显式恢复context,解决跨 Task 边界的 ContextVar 丢失问题。

模式一:Monkey-Patch(OpenAI / Anthropic)

用户只需将from openai import OpenAI改为 from deepeval.openai import OpenAI,导入时自动触发类级别的 monkey-patch。

每次 API 调用被包裹在@observe(type="llm") 中,自动提取 model、messages、token 用量等信息填充到 LlmSpan。

这些主要是在 deepeval/openai 和deepeval/tracing/patchers.py 。

OpenAI 的两种 patch 方式区别:

  1. 类级别 patch(from deepeval.openai import OpenAI):替换 SDK 类的方法,所有实例自动生效
  2. 实例级别 patch(trace_manager.configure(openai_client=client)):只 patch 传入的特定客户端实例

Anthropic 仅支持实例级别 patch:

  from anthropic import Anthropic

  from deepeval.tracing import trace_manager

  client = Anthropic()

  trace_manager.configure(anthropic_client=client)

Anthropic patcher 本身不创建 span,它依赖调用方已通过 @observe(type="llm") 在上下文中放置了 LlmSpan,patcher 只负责填充model、input、output、token 用量。

模式二:Callback Handler(LangChain / LangGraph / LlamaIndex / CrewAI)

框架在执行过程中主动调用回调方法,deepeval 的 CallbackHandler 在每个回调中创建/结束对应类型的 span。

通过 run_id → span_uuid的映射维护正确的父子层级关系。

  • LlamaIndex span 分类启发式:基于方法名(run → agent, call_tool → tool, retrieve → retriever)

  • LangGraph 兼容:LangGraph 构建在 LangChain 之上,使用相同的 callback 系统,on_chain_start/end 自动捕获 graph node 执行

  • CrewAI 双层 hook:

    • Monkey-patch 层(wrap_all()):替换 Crew.kickoff、Agent.execute_task 等方法,用 Observer 创建顶层 span
    • 事件监听层(CrewAIEventsListener(BaseEventListener)):监听 LLMCallStartedEvent、ToolUsageStartedEvent 等细粒度事件创建子 span

模式三:Trace Processor(OpenAI Agents)

OpenAI Agents SDK 提供了TracingProcessor接口,deepeval 实现该接口后注册为 trace processor。

SDK 在执行过程中主动推送 span的开始/结束事件,deepeval 根据 SpanData 的具体类型(AgentSpanData、GenerationSpanData、FunctionSpanData 等)分类为对应的 span 类型。

在deepeval/openai_agents/callback_handler.py中有:

class DeepEvalTracingProcessor(TracingProcessor):
      def on_span_start(self, span: "Span") -> None:
          span_type = self.get_span_kind(span.span_data)
          if span_type == "noop":
              return
          observer = Observer(span_type=span_type, func_name="NA")
          self.span_observers[span.span_id] = observer
          observer.__enter__()

      def on_span_end(self, span: "Span") -> None:
          observer = self.span_observers.pop(span.span_id, None)
          if observer:
              observer.__exit__(None, None, None)

      def get_span_kind(self, span_data) -> str:
          if isinstance(span_data, AgentSpanData): return "agent"
          if isinstance(span_data, FunctionSpanData): return "tool"
          if isinstance(span_data, GenerationSpanData): return "llm"
          if isinstance(span_data, ResponseSpanData): return "llm"
          # TaskSpanData, TurnSpanData 等 → "noop"(忽略)

模式四:OTel SpanProcessor(Pydantic AI / AWS AgentCore)

这些框架原生支持 OpenTelemetry,deepeval 注册自定义的 SpanProcessor 来拦截 OTel span。

  • SpanInterceptor 负责将框架特定的 OTel 属性翻译为confident.* 命名空间的属性。

  • ContextAwareSpanProcessor 根据是否存在 deepeval trace context 来智能路由:

    • 有 context 时走 REST 路径(通过ConfidentSpanExporter 重建为 deepeval 的 BaseSpan 树)
    • 无 context 时走 OTLP 直推

eval 指标

所有指标位于deepeval/metrics/目录下,每个指标一个子文件夹,包含三个核心文件:

  • <metric>.py — 核心实现(measure / a_measure)
  • schema.py — Pydantic 输出模型
  • template.py — LLM 提示词模板

真实性

这是 RAG 系统最核心的诉求。用户给了agent一堆参考资料,agent的回答必须忠于这些资料,不能"创造性发挥"。

它的颗粒度在sequence,所以一般都会拆开来句子,然后逐个判断。

这个范式是 deepeval 最常见的设计模式。你会在后面的偏见、毒性、指令遵循等指标里反复看到它。

主要治标有:

  • FaithfulnessMetric:每句话都能在参考资料里找到依据吗
  • HallucinationMetric:编了多少没有出处的东西
  • ContextualRecallMetric:参考资料里的关键信息有没有用

只有拆开来每一句分析,才能知道溯源回去看看哪里不对。

除了真实性本身错,还有比如说检索不对导致的真实性错了

这三个参数是互补的,巧妙之处在于 ContextualPrecision 不是简单的 "相关数/总数"。

它用的是加权精确率@k——相关文档排在越前面,得分越高。

这背后的直觉是:如果有用的段落被埋在第 10 条,LLM可能根本看不到它(受限于注意力窗口),等于白检索。