课程0基础Agent开发课 / Agent基础 / LLM应用调试指南-从print到系统化排查
— 12 min read

LLM应用调试指南-从print到系统化排查

LLM 应用的调试和传统代码调试有根本性的区别:传统代码的 bug 是确定性的,同样的输入必然产生同样的错误;LLM 应用的问题是概率性的,同样的 Prompt 可能 80% 的时候正常、20% 的时候出错,而且出错的方式还不一样。

LLM 应用调试指南:从 print 到系统化排查

LLM 应用的调试和传统代码调试有根本性的区别:传统代码的 bug 是确定性的,同样的输入必然产生同样的错误;LLM 应用的问题是概率性的,同样的 Prompt 可能 80% 的时候正常、20% 的时候出错,而且出错的方式还不一样。

这种不确定性让"随便跑一遍看看"变得不可靠,需要系统化的调试方法。


1.1 LLM 应用的四类问题

print 调试
最快起点

结构化日志
可检索记录

链路追踪
全链路可视

评估体系
系统化度量

LLM 应用调试四层演进——print 调试 → 结构化日志 → 链路追踪 → 评估体系

在动手调试之前,先识别问题属于哪一类。

类型一:Prompt 问题

模型输出格式不对、回答跑题、指令没有被遵循。这类问题最常见,也最容易被误认为是代码 bug。

特征:换一个 Prompt 或者换一个模型,问题消失或者变化。

类型二:工具调用问题

Agent 选错了工具、工具参数传错了、工具执行结果没有被正确处理。

特征:LLM 的 Thought 看起来合理,但 Action 或 Action Input 有问题。

类型三:上下文管理问题

对话历史太长被截断、关键信息没有被包含进上下文、上下文顺序混乱。

特征:单轮对话正常,多轮对话后出现问题;短文档正常,长文档出问题。

类型四:工程问题

API 超时、速率限制、并发竞争、状态不一致。

特征:错误有明确的异常信息,与模型输出内容无关。


1.2 调试工具链

1.2.1 最快的起点:打印完整的消息历史

python
import json

def debug_messages(messages: list, label: str = "Messages") -> None:
    """打印完整的消息历史,用于调试"""
    print(f"\n{'='*50}")
    print(f"[DEBUG] {label}")
    print(f"{'='*50}")
    for i, msg in enumerate(messages):
        role = msg.get("role", "unknown")
        content = msg.get("content", "")
        # 截断过长的内容
        if isinstance(content, str) and len(content) > 500:
            content = content[:500] + f"... [截断,共{len(content)}字符]"
        print(f"\n[{i}] {role.upper()}:")
        print(content)
    print(f"\n{'='*50}\n")


# 使用示例
messages = [
    {"role": "system", "content": "你是一个助手"},
    {"role": "user", "content": "帮我写一个排序函数"},
]

debug_messages(messages, "调用前的消息历史")

# 调用 LLM
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
)

messages.append({"role": "assistant", "content": response.choices[0].message.content})
debug_messages(messages, "调用后的消息历史")

为什么打印完整消息历史比打印输出更有用:很多问题出在"发给 LLM 的内容",而不是"LLM 的回答"。打印完整历史能发现:System Prompt 是否正确、用户消息是否被正确格式化、工具调用结果是否被正确回填。


1.2.2 用 LangSmith 追踪 Agent 的每一步

对于 LangChain/LangGraph 应用,LangSmith 是最强的调试工具。它能记录每次 LLM 调用的完整输入输出、token 消耗、延迟,以及 Agent 的每一步 Thought-Action-Observation 循环。

python
import os
from langchain_openai import ChatOpenAI

# 设置 LangSmith 环境变量
os.environ["LANGCHAIN_TRACING_V2"] = "true"
os.environ["LANGCHAIN_API_KEY"] = "your-langsmith-api-key"
os.environ["LANGCHAIN_PROJECT"] = "my-agent-debug"  # 项目名,用于分组

# 之后所有 LangChain 调用都会自动被追踪
llm = ChatOpenAI(model="gpt-4o")
response = llm.invoke("你好")
# 在 LangSmith 控制台可以看到这次调用的完整记录

LangSmith 能帮你看到什么

code
调用链路示例(在 LangSmith 控制台看到的):

AgentExecutor
├── LLM Call #1
│   ├── Input: [system prompt] + [user message]
│   ├── Output: "Thought: 需要查询天气\nAction: get_weather\nAction Input: {...}"
│   └── Tokens: 245 input + 67 output = 312 total
├── Tool: get_weather
│   ├── Input: {"city": "北京"}
│   └── Output: "晴,22°C"
└── LLM Call #2
    ├── Input: [previous messages] + [tool result]
    ├── Output: "北京今天晴,22°C"
    └── Tokens: 312 input + 15 output = 327 total

这比看 print 日志清晰得多,特别是在多步骤 Agent 的调试中。


1.2.3 用 Mock 测试 LLM 输出

LLM 调用有成本,而且不稳定。调试时,用 Mock 替换真实 LLM 调用,可以:

  • 控制 LLM 的输出,测试特定的边界情况
  • 加快测试速度(不需要等待 API 响应)
  • 节省 API 费用
python
from unittest.mock import MagicMock, patch
from openai.types.chat import ChatCompletion, ChatCompletionMessage, Choice

def make_mock_response(content: str) -> ChatCompletion:
    """创建一个模拟的 OpenAI API 响应"""
    return ChatCompletion(
        id="mock-id",
        object="chat.completion",
        created=1234567890,
        model="gpt-4o",
        choices=[
            Choice(
                index=0,
                message=ChatCompletionMessage(
                    role="assistant",
                    content=content,
                ),
                finish_reason="stop",
            )
        ],
        usage=None,
    )


# 测试:当 LLM 返回格式错误的 JSON 时,应用是否能正确处理
def test_malformed_json_handling():
    """测试 LLM 返回格式错误的 JSON 时的处理"""
    with patch("openai.OpenAI") as mock_openai:
        # 模拟 LLM 返回格式错误的 JSON
        mock_client = MagicMock()
        mock_openai.return_value = mock_client
        mock_client.chat.completions.create.return_value = make_mock_response(
            '{"name": "张三", "age": 25,}'  # 尾部多了逗号,是非法 JSON
        )

        # 运行你的应用代码
        from your_app import extract_user_info
        result = extract_user_info("张三,25岁")

        # 验证应用没有崩溃,而是有合理的降级处理
        assert result is not None or result == {}  # 根据你的降级策略调整


# 测试:Agent 在工具调用失败时的行为
def test_tool_failure_handling():
    """测试工具调用失败时 Agent 的处理"""
    with patch("openai.OpenAI") as mock_openai:
        mock_client = MagicMock()
        mock_openai.return_value = mock_client

        # 第一步:LLM 决定调用工具
        # 第二步:工具调用失败
        # 第三步:LLM 处理失败情况
        mock_client.chat.completions.create.side_effect = [
            make_mock_response(
                'Thought: 需要查询天气\nAction: get_weather\nAction Input: {"city": "北京"}'
            ),
            make_mock_response(
                "Thought: 工具调用失败,我来提供一个通用回答\nFinish: 抱歉,暂时无法获取实时天气信息。"
            ),
        ]

        from your_app import weather_agent
        result = weather_agent("北京今天天气怎么样?")
        assert "抱歉" in result or "无法" in result

1.3 系统化排查流程

遇到 LLM 应用问题时,按以下顺序排查,避免在错误的层面浪费时间:

第一步:确认问题是否可复现

LLM 输出有随机性。先把 temperature 设为 0,重跑 3-5 次。如果每次结果都不同,问题可能是随机性导致的,不是 bug;如果每次都出同样的问题,才是真正需要调试的 bug。

python
# 调试时临时设置 temperature=0
response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    temperature=0,  # 确定性输出,方便复现
)

第二步:隔离问题层

code
用户报告的问题
    ↓
是 API 层面的错误(超时/限流)?
    是 → 查错误日志,处理工程问题
    否 ↓
是工具调用的问题(工具没被调用/参数错误)?
    是 → 打印完整消息历史,看 LLM 输出的 Action 部分
    否 ↓
是 Prompt 问题(输出格式/内容不对)?
    是 → 简化场景,直接在 Playground 里测试 Prompt
    否 ↓
是上下文管理问题(多轮后出错)?
    是 → 检查消息历史的长度和内容

第三步:最小化复现场景

把问题场景简化到最小:去掉所有不必要的工具、简化 Prompt、用简短的测试输入。最小化场景有两个好处:排除无关因素的干扰;方便在 OpenAI Playground 或 Claude.ai 里直接测试,不需要跑完整代码。

第四步:对比正常和异常的差异

找一个正常的输入和一个异常的输入,打印它们发给 LLM 的完整消息,对比差异。通常问题就藏在差异里。


1.4 常见问题的快速诊断

问题:Agent 在循环,不停调用同一个工具

可能原因:工具返回的结果格式 LLM 看不懂;System Prompt 没有说清楚"什么情况下停止";max_steps 没有设置。

快速验证:打印每步的 Observation,看工具返回了什么。

问题:结构化输出(JSON)解析失败

可能原因:LLM 在 JSON 里加了注释;JSON 有多余的逗号;LLM 在 JSON 前后加了多余的文字。

快速验证:打印 LLM 的原始输出,看实际返回的字符串。

问题:多轮对话后模型"忘记"了之前说的内容

可能原因:消息历史没有正确追加;上下文窗口被截断;Memory 的实现有 bug。

快速验证:打印完整的 messages 列表,确认历史记录是否完整。

问题:同样的 Prompt,有时正常有时不正常

可能原因:temperature 太高导致输出不稳定;Prompt 对某些特定输入有歧义;模型的随机性正常表现。

快速验证:把 temperature 设为 0,重复测试同样的输入,看是否稳定。


1.5 生产环境的调试策略

开发阶段可以用 print 和 LangSmith 调试,但生产环境需要更系统的方案。

结构化日志

python
import logging
import json
from datetime import datetime

logger = logging.getLogger(__name__)

def log_llm_call(messages: list, response: str, model: str, tokens: dict) -> None:
    """记录每次 LLM 调用的关键信息"""
    logger.info(json.dumps({
        "timestamp": datetime.utcnow().isoformat(),
        "model": model,
        "message_count": len(messages),
        "last_user_message": messages[-1]["content"][:200] if messages else "",
        "response_preview": response[:200],
        "tokens": tokens,
    }, ensure_ascii=False))

采样记录完整请求

生产环境不能记录所有请求(成本和存储),但可以采样记录 1-5% 的请求,以及所有出错的请求。

python
import random

def should_log_full_request() -> bool:
    """1% 的请求记录完整内容"""
    return random.random() < 0.01

if should_log_full_request() or is_error:
    log_full_request(messages, response)

1.6 工具推荐

工具 适用场景 成本
print / 结构化日志 快速排查,开发阶段 免费
LangSmith LangChain/LangGraph 应用,需要追踪调用链 免费额度
Langfuse 开源替代 LangSmith,可自部署 免费/自托管
OpenAI Playground 直接测试 Prompt,不需要写代码 按 token 计费
pytest + Mock 自动化测试,CI/CD 集成 免费

调试 LLM 应用的核心心态是:不要假设问题在哪里,先收集证据。打印完整的消息历史,用 LangSmith 追踪调用链,用 Mock 隔离变量——这些工具能把"感觉哪里不对"变成"确认是这里出了问题"。

后续阅读

  • 第 18 章《AI 应用的错误处理与重试策略》:生产环境的错误处理和容错机制
  • 第 18 章《LangSmith 与 Langfuse:AI 应用的可观测性》:系统化的可观测性方案
本页目录