AI Engineering Academy · 课时

LLM 应用为何难以调试

了解为什么传统日志记录不足以应对 LLM 应用,诊断 RAG 和智能体流程中的故障需要哪些信息,以及追踪数据模型是什么。

第 1 / 4 课13 个步骤

LLM 应用为何难以调试 是 CoddyKit 上的免费 AI Engineering Academy 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 AI Engineering Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 AI Engineering Academy 课程共包含 4 节课。

LLM 调试面临的独特挑战

传统软件以确定性方式失败:给定相同的输入,它始终产生相同的输出,而堆栈跟踪会直接指向发生失败的那一行。LLM 应用打破了这些假设。相同的提示在不同调用中可能产生不同的输出,失败通常是静默的(返回错误答案而不是抛出异常),其原因还可能隐藏在调用链前面 5 个步骤所使用的提示中。标准的日志记录和调试工具根本不是为这种情况设计的。

非确定性使复现变得困难

LLM 的输出默认具有非确定性。即使 temperature=0,相同的提示也可能因批处理和数值精度的差异而产生略有不同的输出。这意味着错误具有间歇性:如果某个提示有 20% 的概率失败,而您只运行一次测试套件,它仍可能顺利通过。要复现某次特定的失败,必须在失败发生时记录确切的输入、模型参数和输出,而不仅仅是输入。

import json
import time

def logged_llm_call(client, messages, model, temperature, **kwargs):
    request_id = f'{int(time.time() * 1000)}-{id(messages)}'
    
    response = client.chat.completions.create(
        model=model,
        messages=messages,
        temperature=temperature,
        **kwargs
    )
    
    # Log EVERYTHING needed to reproduce this exact call
    log_entry = {
        'request_id': request_id,
        'model': model,
        'temperature': temperature,
        'messages': messages,
        'response': response.choices[0].message.content,
        'finish_reason': response.choices[0].finish_reason,
        'usage': response.usage.model_dump(),
        'timestamp': time.time()
    }
    write_to_trace_store(log_entry)
    return response

静默失败:答案错误而非程序崩溃

LLM 最隐蔽的失败是静默失败:API 调用成功(HTTP 200,没有异常),但答案可能是错误的、虚构的、不完整的,或偏离主题。您的应用会毫无察觉地处理错误答案,并将其返回给用户,不会显示任何失败迹象。只监控异常和错误代码的传统监控永远无法捕获这类问题——您需要对输出质量进行语义监控。

# This succeeds with HTTP 200 but returns wrong information
response = client.chat.completions.create(
    model='gpt-4o',
    messages=[{'role': 'user', 'content': 'What is the boiling point of water at sea level?'}]
)

output = response.choices[0].message.content
# response.status_code: None (not relevant - always 200 if we got here)
# No exception thrown
# But if output is '90 degrees Celsius', it is WRONG and your app will serve bad data

# You need semantic validation:
def validate_boiling_point_answer(text: str) -> bool:
    return '100' in text  # Rough check - real validation is more sophisticated

多步骤链:问题出在哪里?

在 RAG 流水线或代理链中,最终响应的失败可能追溯到某个返回了无关片段的检索步骤;而这个检索问题又可能源于分块策略将一个关键句子拆到了两个片段中;再往前追溯,则可能是嵌入模型对技术术语处理不佳。没有逐步追踪时,您只能看到错误的最终答案,无法确定究竟是哪一步引入了错误。

# Without tracing: you see only the final wrong answer
def rag_pipeline_naive(query):
    chunks = retrieve(query)         # step 1 - might return bad chunks
    context = format_context(chunks) # step 2 - might truncate key info
    answer = generate(query, context) # step 3 - LLM gets bad context
    return answer  # WRONG - but why?

# With tracing: you can see each step's input and output
def rag_pipeline_traced(query):
    with trace_span('retrieve') as span:
        chunks = retrieve(query)
        span.set_attribute('num_chunks', len(chunks))
        span.set_attribute('top_chunk_score', chunks[0]['score'] if chunks else 0)
    
    with trace_span('format_context') as span:
        context = format_context(chunks)
        span.set_attribute('context_length', len(context))
    
    with trace_span('generate') as span:
        answer = generate(query, context)
        span.set_attribute('answer_length', len(answer))
    
    return answer  # Now you can diagnose: was retrieve the problem?

令牌数量和成本带来的意外

如果没有插桩,令牌数量和成本会一直不可见,直到月底账单到来。系统提示可能因疏忽从 500 个令牌增长到 5000 个令牌,检索函数可能返回 20 个片段而不是 5 个,循环也可能调用 LLM 100 次而不是 10 次——所有这些都会在不知不觉中成倍增加成本。请为每次 LLM 调用添加插桩,记录提示令牌数、完成令牌数和预计成本,以便实时发现异常。

COST_PER_1K = {'gpt-4o': {'input': 0.005, 'output': 0.015},
               'gpt-4o-mini': {'input': 0.000150, 'output': 0.000600}}

def compute_cost(model: str, usage) -> float:
    pricing = COST_PER_1K.get(model, {'input': 0.005, 'output': 0.015})
    input_cost = (usage.prompt_tokens / 1000) * pricing['input']
    output_cost = (usage.completion_tokens / 1000) * pricing['output']
    return input_cost + output_cost

def instrumented_call(client, model, messages):
    response = client.chat.completions.create(model=model, messages=messages)
    cost = compute_cost(model, response.usage)
    
    # Alert if single call is unexpectedly expensive
    if cost > 0.10:  # more than 10 cents for one call
        print(f'WARNING: Expensive LLM call: ${cost:.4f} ({response.usage.prompt_tokens} prompt tokens)')
    
    metrics.record('llm_cost_usd', cost, tags={'model': model})
    metrics.record('llm_prompt_tokens', response.usage.prompt_tokens)
    return response

延迟:哪一步最慢?

用户感受到的 LLM 延迟像是一段完整的等待时间,但实际上它是许多独立步骤的总和:向量数据库查询、文档检索、提示组装、API 网络调用、令牌生成以及响应解析。如果没有逐步计时,您无法判断响应缓慢是因为检索器慢,还是因为 LLM 调用慢。请为每一步添加延迟测量,以找出真正的瓶颈。

import time
from contextlib import contextmanager

@contextmanager
def timed(name: str, metrics_client):
    start = time.monotonic()
    try:
        yield
    finally:
        elapsed_ms = (time.monotonic() - start) * 1000
        metrics_client.histogram(f'step_latency_ms', elapsed_ms, tags={'step': name})
        if elapsed_ms > 2000:  # flag steps taking more than 2 seconds
            print(f'SLOW STEP [{name}]: {elapsed_ms:.0f}ms')

# Usage
def rag_with_timing(query, metrics):
    with timed('embed_query', metrics):
        query_embedding = embed(query)
    
    with timed('vector_search', metrics):
        chunks = vector_db.search(query_embedding, top_k=5)
    
    with timed('llm_generate', metrics):
        answer = generate(query, chunks)
    
    return answer

您实际需要哪些信息

要诊断任何 LLM 应用的失败,您需要捕获并存储:完整的输入提示(系统消息加上所有消息)、所使用的模型和参数(temperature、max_tokens)、完整输出、令牌数量和预计成本、每一步的延迟、所有工具调用及其结果,以及用于关联同一用户请求所有步骤的会话 ID 或请求 ID。这就是最低可行的追踪数据集。

from dataclasses import dataclass, field
from typing import Optional
import time

@dataclass
class LLMTrace:
    request_id: str
    session_id: str
    step_name: str
    model: str
    temperature: float
    system_prompt: str
    user_messages: list[dict]
    response: str
    finish_reason: str
    prompt_tokens: int
    completion_tokens: int
    cost_usd: float
    latency_ms: float
    tool_calls: list[dict] = field(default_factory=list)
    error: Optional[str] = None
    timestamp: float = field(default_factory=time.time)

    def is_anomalous(self) -> bool:
        return (
            self.cost_usd > 0.10 or
            self.latency_ms > 10000 or
            self.finish_reason == 'length' or  # was cut off
            self.error is not None
        )

使用请求 ID 关联追踪记录

单个用户请求可能会在不同服务中触发 10 次 LLM 调用。如果没有贯穿所有调用的关联 ID,您就无法将这些调用归入同一条追踪记录。在每个用户请求的入口处注入唯一的请求 ID,并在每次下游 LLM 调用、数据库查询和日志消息中传递该 ID。这样,您就可以重建任何特定用户请求的完整执行路径。

import uuid
from contextvars import ContextVar

# Thread-safe request ID propagation using context variables
request_id_var: ContextVar[str] = ContextVar('request_id', default='unknown')

def handle_user_request(query: str):
    # Set request ID at the entry point
    req_id = str(uuid.uuid4())[:8]
    request_id_var.set(req_id)
    return rag_pipeline(query)

def get_current_request_id() -> str:
    return request_id_var.get()

# Every LLM call logs with the same request_id
def log_llm_call(model, prompt, response):
    logger.info('LLM call', extra={
        'request_id': get_current_request_id(),  # automatically correlates all calls
        'model': model,
        'prompt_length': len(prompt),
        'response_length': len(response)
    })

LLM 可观测性技术栈

LLM 可观测性技术栈分为三层:日志记录捕获每次 LLM 调用的结构化记录(LangSmith、Langfuse、自定义日志);指标跟踪一段时间内的汇总数据:请求数量、平均延迟、错误率和每日成本;追踪记录单个请求内部各步骤之间的因果链。这三大支柱结合起来,可以帮助您诊断失败、发现性能退化并优化性能。

针对质量下降设置提醒

传统软件中的错误通常是二元的(正常/故障),而 LLM 的质量会逐渐下降。提示变更可能会使答案质量从 85% 降至 70%,却不会抛出任何异常。请每天对一部分生产环境响应运行自动化评估(由 LLM 作为评判者进行评分)来监控质量。当滚动平均质量分数低于阈值时发出提醒,在用户开始抱怨之前发现问题。

开始实施可观测性

不要等到生产环境发生事故后才添加可观测性。请从三个最基本的步骤开始:(1)将每次 LLM 调用的完整输入、输出和令牌数量记录到数据库或文件中;(2)为每次用户交互分配请求 ID,并将其包含在所有日志条目中;(3)为流水线添加逐步计时。仅凭这三点,就能让 80% 的调试任务在几分钟而不是几小时内完成。

快速检查

测试您对本课中 LLM 应用为何难以调试的理解。

课程回顾

本课您学习了:非确定性使 LLM 错误具有间歇性;如果不捕获完整的请求上下文,就很难复现这些错误;静默失败(成功的 API 调用返回错误答案)会绕过传统错误监控,因此需要进行语义质量检查;在多步骤 RAG 和代理流水线中,通过请求 ID 关联的逐步追踪是诊断失败所需的最低条件。接下来,我们将使用 LangSmith 实现追踪。

免费开始

用 AI 导师学习 Python — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
30
课程
120

常见问题解答

「LLM 应用为何难以调试」课时是免费的吗?

是的 — 「LLM 应用为何难以调试」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 AI Engineering Academy 课程的其余内容,请升级到 CoddyKit PRO。 AI Engineering Academy 课程共包含 4 节课。

「LLM 应用为何难以调试」这节课中我会学到什么?

了解为什么传统日志记录不足以应对 LLM 应用,诊断 RAG 和智能体流程中的故障需要哪些信息,以及追踪数据模型是什么。 你通过在浏览器中直接运行的动手代码来练习 AI Engineering Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 AI Engineering Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 AI Engineering Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「LLM 应用为何难以调试」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 AI Engineering Academy 课中编写并运行代码吗?

能。每节 AI Engineering Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. LLM 应用为何难以调试
  2. 使用 LangSmith 进行追踪
  3. 使用 Langfuse 实现与模型无关的可观测性
  4. 针对延迟、成本和质量下降设置告警
← 返回 AI Engineering Academy