LLM应用调试指南-从print到系统化排查
LLM 应用的调试和传统代码调试有根本性的区别:传统代码的 bug 是确定性的,同样的输入必然产生同样的错误;LLM 应用的问题是概率性的,同样的 Prompt 可能 80% 的时候正常、20% 的时候出错,而且出错的方式还不一样。
LLM 应用调试指南:从 print 到系统化排查
LLM 应用的调试和传统代码调试有根本性的区别:传统代码的 bug 是确定性的,同样的输入必然产生同样的错误;LLM 应用的问题是概率性的,同样的 Prompt 可能 80% 的时候正常、20% 的时候出错,而且出错的方式还不一样。
这种不确定性让"随便跑一遍看看"变得不可靠,需要系统化的调试方法。
1.1 LLM 应用的四类问题
LLM 应用调试四层演进——print 调试 → 结构化日志 → 链路追踪 → 评估体系
在动手调试之前,先识别问题属于哪一类。
类型一:Prompt 问题
模型输出格式不对、回答跑题、指令没有被遵循。这类问题最常见,也最容易被误认为是代码 bug。
特征:换一个 Prompt 或者换一个模型,问题消失或者变化。
类型二:工具调用问题
Agent 选错了工具、工具参数传错了、工具执行结果没有被正确处理。
特征:LLM 的 Thought 看起来合理,但 Action 或 Action Input 有问题。
类型三:上下文管理问题
对话历史太长被截断、关键信息没有被包含进上下文、上下文顺序混乱。
特征:单轮对话正常,多轮对话后出现问题;短文档正常,长文档出问题。
类型四:工程问题
API 超时、速率限制、并发竞争、状态不一致。
特征:错误有明确的异常信息,与模型输出内容无关。
1.2 调试工具链
1.2.1 最快的起点:打印完整的消息历史
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 循环。
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 能帮你看到什么:
调用链路示例(在 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 费用
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。
# 调试时临时设置 temperature=0
response = client.chat.completions.create(
model="gpt-4o",
messages=messages,
temperature=0, # 确定性输出,方便复现
)
第二步:隔离问题层
用户报告的问题
↓
是 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 调试,但生产环境需要更系统的方案。
结构化日志
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% 的请求,以及所有出错的请求。
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 应用的可观测性》:系统化的可观测性方案