Chapter 08

Agent 可观测性与评估

你无法改善你看不见的东西。通过 LangSmith 追踪、量化评估指标和系统化的 Prompt 优化方法,让 Agent 质量持续提升。

可观测性的重要性

Agent 系统比传统 API 复杂得多:一次任务可能经历数十次 LLM 调用、工具调用和状态转换。没有可观测性,你无法知道:Agent 在哪一步失败了?工具被错误调用了几次?哪个 Prompt 导致了幻觉?为什么同样的问题有时成功有时失败?

追踪(Tracing)
记录 Agent 执行的完整轨迹:每个节点的输入/输出、LLM 调用的 prompt 和响应、工具调用的参数和结果、每步的耗时和 Token 消耗。LangSmith、Langfuse、Phoenix 是主流的 Agent 追踪平台。追踪是可观测性的基础层,没有追踪,调试 Agent 就像盲人摸象。
评估(Evaluation)
量化 Agent 在一组测试用例上的表现。与传统软件不同,Agent 的评估需要处理"主观正确性"——答案可以是正确的但表达不同。通常结合自动评估(规则/代码)和 LLM-as-Judge(用另一个 LLM 判断质量)。评估是提升 Agent 质量的核心工具。
LLM-as-Judge(LLM 评判者)
用更强的 LLM(如 claude-opus-4-6)来评估另一个 LLM 或 Agent 的输出质量。解决了开放式任务无法用规则判断的问题。关键技巧:评判者 Prompt 要提供明确的评分标准(1-5 分的含义)、要求输出理由(减少随机性)、避免位置偏差(不固定哪个答案先出现)。
Span(跨度)
OpenTelemetry 的基本追踪单元。每个 Span 代表一个操作(如单次 LLM 调用、单次工具调用),包含开始时间、结束时间、属性(metadata)和事件。多个 Span 组成 Trace(调用链),可视化 Agent 的完整执行路径。
Agent 可观测性的三个层次: 层 1: 追踪(Tracing)— 看清每一步发生了什么 ┌──────────────────────────────────────────────────────┐ │ 运行 ID → 节点执行序列 → LLM 输入/输出 → 工具调用记录 │ │ 延迟分布 → Token 用量 → 错误堆栈 → 重试记录 │ └──────────────────────────────────────────────────────┘ 层 2: 评估(Evaluation)— 量化质量好坏 ┌──────────────────────────────────────────────────────┐ │ 任务完成率 → 答案正确率 → 工具调用精确率 │ │ 幻觉率 → 平均步骤数 → 平均成本/任务 → 延迟P99 │ └──────────────────────────────────────────────────────┘ 层 3: 优化(Optimization)— 系统性改进 ┌──────────────────────────────────────────────────────┐ │ 错误模式分析 → Prompt A/B 测试 → 数据集扩充 │ │ 模型切换实验 → 工具描述优化 → 持续回归测试 │ └──────────────────────────────────────────────────────┘

LangSmith 集成

LangSmith 是 LangChain 官方的可观测性平台,对 LangGraph Agent 有原生支持,配置极简:

import os
from langsmith import Client, traceable

# ── 环境变量配置(.env 文件)─────────────────────────────
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com"
os.environ["LANGCHAIN_API_KEY"] = "ls__xxxxxxxxxxxxxxxx"
os.environ["LANGCHAIN_PROJECT"] = "my-agent-project"

# 配置后,所有 LangChain/LangGraph 调用自动被追踪
# 无需修改任何业务代码——零侵入

# ── 手动追踪(非 LangChain 代码)───────────────────────
@traceable(
    name="research_agent_run",
    tags=["production", "v2"],
    metadata={"agent_version": "2.1.0"}
)
def run_research_agent(user_query: str, user_id: str) -> str:
    """用 @traceable 装饰器将函数自动添加到追踪树中。"""
    result = graph.invoke(
        {"messages": [HumanMessage(content=user_query)]},
        config={
            "metadata": {       # 这些元数据会出现在 LangSmith UI 中
                "user_id": user_id,
                "query_type": "research",
                "session_id": generate_session_id()
            }
        }
    )
    return result["messages"][-1].content

# ── 程序化分析追踪数据 ──────────────────────────────────
client = Client()

# 获取最近 100 次运行
runs = list(client.list_runs(
    project_name="my-agent-project",
    run_type="chain",
    limit=100
))

# 分析失败的运行
failed_runs = [r for r in runs if r.error]
print(f"失败率:{len(failed_runs)/len(runs):.1%}")

# 计算成本(按 Token 估算)
total_tokens = sum(r.total_tokens for r in runs if r.total_tokens)
avg_tokens = total_tokens / len(runs)
print(f"平均 Token/次:{avg_tokens:.0f}")
print(f"估算成本/次:${avg_tokens / 1000 * 0.003:.4f}(Sonnet 定价)")

# 找出最慢的10次运行(延迟分析)
slow_runs = sorted(
    [r for r in runs if r.end_time and r.start_time],
    key=lambda r: (r.end_time - r.start_time).total_seconds(),
    reverse=True
)[:10]
for run in slow_runs:
    duration = (run.end_time - run.start_time).total_seconds()
    print(f"  [{duration:.1f}s] {run.name}: {str(run.inputs)[:60]}")

OpenTelemetry 追踪(自托管)

对于不能将数据发送到第三方平台的合规场景,可以使用 OpenTelemetry 自托管追踪:

from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter

# 配置 OTLP 导出器(可对接 Jaeger、Tempo、Zipkin 等)
otlp_exporter = OTLPSpanExporter(
    endpoint="http://localhost:4317",  # Jaeger/Tempo 的 gRPC 端口
    insecure=True
)
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(otlp_exporter))
trace.set_tracer_provider(provider)

tracer = trace.get_tracer("agent.research", "1.0.0")

# ── 手动 Span:追踪 LangGraph 节点 ──────────────────────
def traced_agent_node(state: dict) -> dict:
    """带 OTel Span 的 LangGraph 节点。"""
    with tracer.start_as_current_span("agent_reasoning") as span:
        # 记录节点的关键属性
        span.set_attribute("messages.count", len(state["messages"]))
        span.set_attribute("iteration", state.get("iteration", 0))
        span.set_attribute("model", "claude-sonnet-4-6")

        response = llm_with_tools.invoke(state["messages"])

        # 记录 LLM 响应属性
        span.set_attribute("tool_calls.count", len(response.tool_calls))
        span.set_attribute("output_tokens",
                           response.usage_metadata.get("output_tokens", 0))

        # 如果有错误,标记 Span 为错误状态
        if not response.content:
            span.set_status(trace.status.Status(trace.status.StatusCode.ERROR))

        return {"messages": [response]}

# ── 自定义结构化日志(与 Span 关联)───────────────────
import structlog

log = structlog.get_logger()

def log_tool_call(tool_name: str, tool_input: dict, result: str):
    """记录工具调用到结构化日志(可与 Span 关联)。"""
    span = trace.get_current_span()
    trace_id = format(span.get_span_context().trace_id, "032x")

    log.info(
        "tool_call",
        tool_name=tool_name,
        input_keys=list(tool_input.keys()),
        result_length=len(result),
        trace_id=trace_id  # 关联 trace,日志与追踪可以互相跳转
    )

Agent 评估指标体系

不同于传统系统的延迟/错误率指标,Agent 评估需要业务特定的语义指标:

任务完成率(Task Completion Rate)
Agent 成功完成用户请求的比例。需要定义"完成"的标准(如:返回了答案、执行了期望的操作、没有超过最大迭代次数)。是最核心的 KPI。目标值:>90%。失败的主要原因:工具调用错误(~40%)、上下文超长(~25%)、LLM 幻觉(~20%)、工具服务不可用(~15%)。
工具调用精确率(Tool Call Precision)
Agent 调用工具时选择了正确工具的比例。= 正确工具调用次数 / 总工具调用次数。过低说明工具描述不清晰(最常见)或工具数量过多(超过 15 个工具时精确率下降明显)。目标值:>85%。提升方法:优化工具 description,添加"何时使用"和"勿用于"说明。
平均步骤数(Average Steps)
完成任务平均需要的工具调用次数。步骤数过多说明 Agent 在"绕弯路",可能需要更好的规划能力或更清晰的工具描述。理想范围:简单任务 2-4 步,复杂任务 5-10 步。若平均超过 12 步,通常说明 Agent 陷入了低效循环。
幻觉率(Hallucination Rate)
答案中包含错误或不在工具结果中的信息的比例。使用 LLM-as-Judge 来检测(问"这个答案中是否有任何信息不在给定工具结果中?")。这是 RAG Agent 和研究类 Agent 的关键指标。目标值:<5%。
成本/任务(Cost per Task)
完成一次用户请求的平均 Token 成本。= Σ(每次 LLM 调用 Token × 单价)。生产环境必须跟踪,Agent 在意外情况下成本可能激增 10x。建议设置每任务成本上限(硬限制),超过则强制终止并告知用户。

LangSmith 评估框架

LangSmith 提供了完整的评估工作流:构建数据集 → 定义评估器 → 运行评估 → 对比实验:

from langsmith import Client, evaluate
from langsmith.schemas import Run, Example
from langchain_anthropic import ChatAnthropic
from langchain_core.messages import SystemMessage, HumanMessage
from typing import Dict
import re

client = Client()

# ── Step 1: 构建评估数据集 ────────────────────────────
# 数据集是评估的"标准答案库",应覆盖典型案例+边界情况+回归用例
dataset = client.create_dataset(
    "agent_eval_v1",
    description="Agent 核心功能评估集,涵盖查询/计算/综合任务"
)

# 添加测试用例(问题 + 期望答案 + 元数据)
test_cases = [
    {"question": "Python 3.12 的主要新特性有哪些?",
     "expected": "f-string嵌套、类型参数语法、新的perf指标",
     "category": "knowledge"},
    {"question": "1024 的平方根是多少?",
     "expected": "32",
     "category": "calculation"},
    {"question": "今天的日期用代码获取,然后告诉我今天是星期几",
     "expected": "正确的星期几",
     "category": "code_execution"},
]

for case in test_cases:
    client.create_example(
        inputs={"question": case["question"]},
        outputs={"answer": case["expected"]},
        metadata={"category": case["category"]},  # 用于分组分析
        dataset_id=dataset.id
    )

# ── Step 2: 定义评估器 ────────────────────────────────
judge_llm = ChatAnthropic(model="claude-opus-4-6", temperature=0)

def correctness_evaluator(run: Run, example: Example) -> Dict:
    """LLM-as-Judge:评估答案的实质正确性。"""
    actual = run.outputs.get("answer", "")
    expected = example.outputs.get("answer", "")

    # 让更强的模型(opus)来判断较弱模型(sonnet)的答案
    judgment = judge_llm.invoke([
        SystemMessage(content="""你是公正的答案评估者。
判断实际答案是否实质上符合期望答案(不需要措辞完全一致)。
对于计算类问题:数字必须精确匹配。
对于知识类问题:关键信息必须包含。
只回复 'yes' 或 'no',不要任何解释。"""),
        HumanMessage(content=f"期望答案:{expected}\n\n实际答案:{actual}")
    ])
    is_correct = "yes" in judgment.content.lower()
    return {"key": "correctness", "score": 1.0 if is_correct else 0.0}

def tool_efficiency_evaluator(run: Run, example: Example) -> Dict:
    """评估工具调用效率:步骤数越少越好。"""
    steps = run.extra.get("tool_call_count", 0)
    # 5步以内满分,每多一步扣0.1分,最低0分
    score = max(0.0, 1.0 - max(0, steps - 5) * 0.1)
    return {"key": "tool_efficiency", "score": score}

def hallucination_evaluator(run: Run, example: Example) -> Dict:
    """检测答案中的幻觉:答案是否包含工具结果以外的信息。"""
    actual = run.outputs.get("answer", "")
    # 获取 Agent 使用的工具结果(从 run 的子 span 中提取)
    tool_results = run.extra.get("tool_results", [])
    tool_context = "\n".join(tool_results)

    if not tool_context:
        return {"key": "no_hallucination", "score": 1.0}  # 无工具调用跳过

    judgment = judge_llm.invoke([
        SystemMessage(content="""判断答案中是否包含工具结果以外的事实声明。
如果答案完全基于工具结果,回复 'grounded'。
如果答案包含工具结果中没有的事实,回复 'hallucinated'。"""),
        HumanMessage(content=f"工具结果:\n{tool_context}\n\n答案:{actual}")
    ])
    is_grounded = "grounded" in judgment.content.lower()
    return {"key": "no_hallucination", "score": 1.0 if is_grounded else 0.0}

# ── Step 3: 运行评估实验 ──────────────────────────────
def agent_runner(inputs: dict) -> dict:
    """将 Agent 包装为评估器可调用的格式。"""
    result = graph.invoke({"messages": [HumanMessage(content=inputs["question"])]})
    return {"answer": result["messages"][-1].content}

# experiment_prefix 用于区分不同版本的实验
results = evaluate(
    agent_runner,
    data="agent_eval_v1",
    evaluators=[correctness_evaluator, tool_efficiency_evaluator, hallucination_evaluator],
    experiment_prefix="v2-claude-sonnet",
    num_repetitions=3  # 每个用例重复3次,消除随机性
)

# 查看汇总结果
df = results.to_pandas()
print(df[["correctness", "tool_efficiency", "no_hallucination"]].describe())

Prompt 优化方法论

Agent 的系统 Prompt 是最强大但也最难优化的旋钮。以下是系统化的优化流程:

Prompt 优化五步法: Step 1: 建立基线 ───────────────────────────────────────────── 用最简单的 Prompt 运行完整评估 记录:正确率 / 工具精确率 / 平均步骤数 / 成本 这是所有后续改进的对比基准 Step 2: 错误模式分析 ───────────────────────────────────────────── 从 LangSmith 中筛选失败运行(score=0) 分类失败原因: 类型A:工具选择错误(工具描述问题) 类型B:参数传错(工具 schema 不清晰) 类型C:推理错误(Prompt 指令问题) 类型D:提前停止(停止条件不当) 优先修复占比最高的失败类型 Step 3: 逐变量改进 ───────────────────────────────────────────── 每次只改变一个变量: 工具 description → 工具 JSON schema → 系统 Prompt 改完后立即重新评估,对比前后指标 不允许同时改多个变量(无法归因) Step 4: A/B 测试 ───────────────────────────────────────────── 将数据集随机分两半 版本A和版本B各跑一半(或同时跑全部) 对比关键指标,检验统计显著性(n≥30) Step 5: 持续回归 ───────────────────────────────────────────── 新版本上线前必须通过历史评估集的回归测试 设置质量门控:正确率不得低于上版本2% 将评估集纳入 Git 版本控制
# ── 工具描述优化示例 ─────────────────────────────────────
# 差的工具描述(导致工具调用精确率低):
BAD_DESCRIPTION = "搜索工具"  # 太模糊,Agent 不知道何时使用

# 好的工具描述(提供明确的使用场景和限制):
GOOD_DESCRIPTION = """在互联网上搜索实时信息。

适用场景:
- 查找最新新闻、事件、产品发布
- 获取实时数据(股价、天气、体育比赛结果)
- 查找技术文档和教程

不适用:
- 数学计算(请用 calculate 工具)
- 代码执行(请用 run_python 工具)
- 基于已有信息做分析(LLM 直接推理即可)

参数:
- query: 搜索查询词,越具体越好(如 "Python 3.12 release notes" 优于 "Python")"""

# ── 自动比较两个 Prompt 版本 ─────────────────────────────
def compare_prompts(
    system_v1: str,
    system_v2: str,
    test_queries: list[str],
    judge_fn
) -> dict:
    """A/B 比较两个系统 Prompt 的效果。"""
    v1_scores, v2_scores = [], []

    for query in test_queries:
        # 两个版本各运行一次
        answer_v1 = run_agent_with_system(query, system_v1)
        answer_v2 = run_agent_with_system(query, system_v2)

        # 评估器打分(LLM-as-Judge 或规则)
        score_v1 = judge_fn(query, answer_v1)
        score_v2 = judge_fn(query, answer_v2)
        v1_scores.append(score_v1)
        v2_scores.append(score_v2)

    import numpy as np
    from scipy import stats

    # t 检验判断差异是否显著(p < 0.05 认为有效)
    t_stat, p_value = stats.ttest_rel(v1_scores, v2_scores)

    return {
        "v1_mean": np.mean(v1_scores),
        "v2_mean": np.mean(v2_scores),
        "improvement": np.mean(v2_scores) - np.mean(v1_scores),
        "p_value": p_value,
        "significant": p_value < 0.05,  # 差异是否统计显著
        "recommendation": "使用 v2" if np.mean(v2_scores) > np.mean(v1_scores) else "保持 v1"
    }
评估的常见陷阱

陷阱1:数据集太小。少于 30 个用例的评估结论不可靠——样本量不够时,随机性会掩盖真实差异。建议:至少 50 个用例,关键场景 100+。

陷阱2:评估数据集泄露。如果你用评估结果来优化 Prompt,评估集就成了"训练集",不再代表真实表现。解决方案:保留一个绝对不参与优化的"黄金测试集",只用于最终版本验证。

陷阱3:LLM-as-Judge 的位置偏差。LLM 评判者倾向于给第一个出现的答案更高分。解决方案:随机化答案顺序,或进行双向对比(A vs B 和 B vs A)。

本章小结

可观测性是生产 Agent 的必备能力,不是可选项。构建完整的评估体系需要三层:追踪(记录执行过程)、评估(量化质量)、优化(系统性改进)。核心指标:任务完成率(最重要)、工具调用精确率、幻觉率、成本/任务。Prompt 优化遵循"单变量实验"原则,每次只改一个变量并衡量效果。LangSmith 是最易用的 Agent 可观测性平台,对 LangGraph 有原生支持。下一章进入生产部署——可靠性与安全。