6.3. 高效调试 Skills 需要可视化与可解释性工具
你需要什么
环境与工具
- Python 3.10+ 环境
- OpenAI Agents SDK(
pip install openai-agents) - 一个已经可以运行的 Skill 脚本(或本章提供的示例 Skill)
- OpenAI API 账号(用于开启 Tracing 可视化)
- VSCode / PyCharm(支持断点与远程调试)
- [可选] Langfuse 账号(
langfusePython 包)或 Gradio/Streamlit 环境 - [可选] 一个简单的 Streamlit 安装(
pip install streamlit)
预计时间
约 2 小时
最终成果
读完本章并完成实操后,你将获得三项硬核调试能力:
- 解释性输出 —— 让 Skill 在执行过程中暴露自己的推理链,一眼看出模型“到底想了什么”
- 断点交互调试 —— 用 IDE 直接挂到运行中的 Skill 进程,逐行检查工具函数内部状态
- 内省看板 —— 构建一个实时监控请求流、工具调用时序和错误分布的简易面板(并理解专业 Tracing 工具的使用)
为什么做这个?
上一章我们用集成测试和回归测试建好了安全网,但测试全绿只说明“功能看似正确”。线上真出问题时——比如客服订单查询陷入死循环——你需要迅速从“知道出事了”进化到“知道哪里出事了,以及为什么”。这就需要一套能透视 Agent 大脑的调试器。
场景还原
周二下午 2:10,Slack 消息图标连闪三次。
用户投诉:“订单查询机器人疯了,它一直在说‘正在查询物流信息’,已经重复 20 遍了!”
你切到仪表盘,回归套件全绿,上一个 PR 明明只重构了 Skill 的内部逻辑。
日志里所有工具调用返回码都是 200,但模型偏偏选择了无限循环。
你不能直接问模型“你当时在想什么”,但可以通过可解释性工具和可视化面板还原它的决策轨迹。
下面我们分三步解决这个问题。
步骤 1:解释性输出——让模型暴露思考链
很多 Skill 出问题后,我们本能地只检查工具函数的输入输出是否正确,却忽略了一个关键点:模型在看到工具返回结果后,是怎么“理解”并决定下一步的?
解释性输出的核心思路,就是要求 Skill 在调用工具之前或之后,以自然语言输出它的推理过程,从而暴露潜在的误解。
1.1 修改 Skill 指令
假设你的订单查询 Skill 定义如下(OpenAI Agents SDK 风格):
# skill_order_query.py
import asyncio
from agents import Agent, Runner, function_tool
@function_tool
def query_order(order_id: str) -> dict:
# 模拟物流查询,这里返回了一个“成功但物流单号为空的”结果
return {"status": "success", "tracking_number": "", "message": "物流查询完成"}
order_agent = Agent(
name="OrderAssistant",
instructions="你是一个订单查询助手。根据用户提供的 order_id 查询订单,并告知用户结果。",
tools=[query_order],
)
async def main():
result = await Runner.run(order_agent, "帮我查一下 ORDER-12345 的物流")
print(result.final_output)
asyncio.run(main())
上述 Skill 运行时,query_order 返回了 tracking_number 为空字符串,但状态为 success。实际场景中,模型可能会把这个结果误解为“查询还没完成”,从而选择重复调用 —— 因为指令里没有说明空字符串也代表“无物流信息,无需重试”。
为了看清这一点,我们为 Agent 增加一条解释性指令:
order_agent = Agent(
name="OrderAssistant",
instructions=(
"你是一个订单查询助手。"
"在每次调用工具之前,先输出一句推理:当前你为什么要调用这个工具,期望得到什么信息。"
"在收到工具返回后,再输出一句推理:你从返回结果中推断出了什么,下一步应该怎么做。"
"如果工具返回的 tracking_number 是空字符串,说明没有物流单号,不需要重试查询。"
),
tools=[query_order],
)
1.2 开启调试日志
解释性输出需要被打印出来,我们不想让它混杂在生产日志中,因此可以加一个环境变量开关:
import os, logging
DEBUG_MODE = os.getenv("DEBUG_SKILL", "false").lower() == "true"
if DEBUG_MODE:
logging.basicConfig(level=logging.INFO)
# 通过 Runner 的回调或重写 run 方法打印推理信息
# 实际 SDK 中我们可以用 Runner.run_streamed 并监听事件
(OpenAI Agents SDK 提供了 model_settings 以及 trace_include_sensitive_data 等选项,但纯控制台推理输出最简单的方式是让模型自己以文本形式“说出来”,我们通过 runner.run 得到的完整响应中,已包含了模型在调用工具时的 thinking 片段——前提是使用了支持思维链的模型(如 o1-mini、gpt-4o 等)。这里为演示,我们强制要求它在输出中包含推理。)
运行 Skill 后,控制台日志可能显示:
[推理] 用户要求查 ORDER-12345 的物流,我还不确定订单状态,需要先调用 query_order 获取 tracking_number。
[工具调用] query_order(ORDER-12345) → {"status": "success", "tracking_number": "", "message": "物流查询完成"}
[推理] 返回的 tracking_number 是空字符串,根据指令这表示没有物流单号。我不需要再次查询,可以直接告知用户暂无物流信息。
预期结果:你立刻就能看到模型在第二次调用时,已经理解到不需要重试。而在之前没有解释性输出时,模型可能会把空字符串当作“异常”,进入重试循环。
踩坑经验
- 解释性输出会显著增加 token 消耗,绝不可在生产环境默认启用。最佳实践是封装为一个开关变量(如
DEBUG_MODE),仅在问题复现时打开。 - 部分模型在包含工具定义时,推理输出可能与实际工具调用顺序不完全一致,这属于正常现象,重点在于理解它的“打算”。
- 如果你的 Skill 已使用 OpenAI Tracing,推理片段会自动出现在 Dashboard 的每个 step 中,无需额外输出。
步骤 2:使用 Debuggers 与断点交互
解释性输出帮你定位了模型决策层面的问题。但有时问题出在工具函数本身 —— 例如执行了一个隐蔽的 bug,却返回了一个表面正常的 dict。这时我们需要进入函数内部,用 IDE 断点逐行调试。
2.1 在工具函数中植入断点
假设我们发现物流查询函数实际上从缓存中获取数据,但偶尔字段名会拼写错(比如写成 traking_number)。在 Python 中,我们可以直接在函数内插入 breakpoint():
@function_tool
def query_order(order_id: str) -> dict:
# 手工插入断点
breakpoint()
# 模拟一个拼写错误的字段
raw_data = fetch_from_backend(order_id) # 假设返回 {"traking_number": "123456", "status": "shipped"}
tracking = raw_data.get("tracking_number", "") # 正确字段拼写
return {"status": "success", "tracking_number": tracking, "message": "物流查询完成"}
启动 Skill 时,用 Python 调试模式运行:
python -m pdb skill_order_query.py
# 或者使用 VSCode 的 “Python: Current File” 调试配置,并在代码中按 F9 设置断点
当 Agent 实际调用 query_order 时,程序会在 breakpoint() 处暂停。此时你可以:
- 查看
raw_data的真实内容 - 检查
tracking是否为空 - 手动修改变量值以验证后续流程
2.2 异步流程的远程调试
Skill 通常运行在异步事件循环中,直接 breakpoint() 可能会打断整个 loop。更稳健的做法是使用 debugpy 进行远程附加:
# 在你的 Skill 入口文件中
import debugpy
debugpy.listen(("0.0.0.0", 5678))
print("等待调试器附加...")
debugpy.wait_for_client()
然后在 VSCode 的 launch.json 中配置一个 "Remote Attach" 配置指向 localhost:5678,即可像调试普通脚本一样单步执行、查看变量、修改值。
预期结果:你会在断点处发现 raw_data 的键名确是 traking_number,导致 tracking 取到了空字符串,整个后续逻辑因此错乱。你当场修复拼写并重启 Skill,问题解决。
踩坑经验
- 断点仅在本地开发环境使用。生产容器上不能让
debugpy开着监听端口。 - 对于异步工具函数,断点会导致该工具挂起,后续依赖它的异步任务可能超时。可以在调试时单独加大 Runner 的
max_turns或关闭超时限制。 - 如果工具函数内部有大量 I/O,建议将断点设在关键返回点,而不是每行都设,否则调试体验会很差。
步骤 3:构建简易的 Skill 内省看板
解释性输出和断点给我们提供了单次执行的显微镜,但当系统上线、每天处理上万次请求时,我们需要一个宏观视角:哪些请求慢、哪些步骤频繁出错、工具调用顺序是否有异常循环。这就是内省看板的用武之地。
3.1 开启 OpenAI Tracing(零代码可视化)
OpenAI Agents SDK 内置了 Tracing 功能,只需设置环境变量即可启动:
export OPENAI_TRACING_ENABLED=true
# 如果你使用 Langfuse,也只需一行:
# export LANGFUSE_SECRET_KEY=sk-... LANGFUSE_PUBLIC_KEY=pk-... LANGFUSE_BASEURL=https://cloud.langfuse.com
然后正常运行你的 Skill。之后打开 OpenAI Platform Tracing 页面,你会看到类似这样的时间线视图:
| 步骤编号 | 类型 | 输入 | 输出 | 耗时(ms) | 状态 |
|---|---|---|---|---|---|
| 1 | LLM call | “查询 ORDER-12345 物流” | tool_calls: query_order | 320 | ✓ |
| 2 | Tool call | order_id: ORDER-12345 | {“status”: “success”, …} | 45 | ✓ |
| 3 | LLM call | 工具返回 + 推理 | tool_calls: query_order (again) | 280 | ✗ |
立刻就能发现步骤 3 重复调用了同一个函数,点击详情还能看到模型此时的具体推理过程,配合前面的解释性输出,问题无所遁形。
3.2 自制轻量级请求时序面板
如果因安全或网络策略无法使用外部服务,你也可以在本地启动一个简易面板,通过结构化日志实时展示请求流。步骤如下:
A. 配置 JSON 格式的结构化日志
import logging, json, time, uuid
class SkillTracingHandler(logging.Handler):
def emit(self, record):
log_entry = {
"trace_id": getattr(record, "trace_id", ""),
"event": record.msg,
"timestamp": record.created,
"extra": getattr(record, "extra_data", {})
}
with open("skill_trace.log", "a") as f:
f.write(json.dumps(log_entry) + "\n")
在 Skill 关键节点(工具调用前后、LLM 调用前后)打点,并赋予同一个 trace_id。
B. 创建实时展示面板(Streamlit 示例)
# dashboard.py
import streamlit as st
import json, time
from collections import defaultdict
st.set_page_config(page_title="Skill 内省看板", layout="wide")
st.title("Skill 请求时序与异常监控")
lines = []
try:
with open("skill_trace.log", "r") as f:
lines = f.readlines()
except:
pass
entries = [json.loads(line) for line in lines[-200:]] # 只看最近200条
traces = defaultdict(list)
for e in entries:
traces[e["trace_id"]].append(e)
for tid, events in traces.items():
with st.expander(f"Trace {tid[:8]}... — {len(events)} 步"):
for i, ev in enumerate(events):
st.write(f"`{ev['event']}` — {ev['extra'].get('duration_ms','?')} ms")
if "error" in [e["event"] for e in events]:
st.error("此 Trace 出现错误")
启动后,浏览器访问 localhost:8501,你会得到一个可折叠的请求流卡片,每个 Trace 内的事件按顺序排列,错误标记为红色。
预期结果:通过看板,你一眼就能看到订单查询 Trace 中 query_order 调用了 20 次,曲线图呈锯齿状,错误分布集中在“重试”标签上。结合解释性输出,直接定位到模型对空字符串的误解。
踩坑经验
- 日志采样:高流量下不要全量保存 JSON 日志,可以只记录错误 Trace 或者使用百分之一采样,否则磁盘会被迅速写满。
- 自制看板仅适用于小规模调试与演示,大规模生产强烈建议直接接入 Langfuse 或 OpenAI Tracing,它们提供了更完善的查询、过滤、警报功能。
- 如果你使用了多个 Agent 或子 Skill 接力,一定要在日志中携带统一的
trace_id并在多个服务间传递,否则看板上的步骤会断链。
回顾
在这一章中,我们完成了一次从“用户投诉死循环”到“精准定位并修复”的完整调试演练:
- 通过解释性输出,我们看到模型在每次工具调用前后如何推理,发现了它对空字符串的误解。
- 使用IDE断点交互,深入工具函数内部,发现一个字段拼写错误,是导致空字符串的根因。
- 搭建内省看板(从 OpenAI Tracing 到自制 Streamlit 面板),为后续监控建立了全局视图。
预计耗时:2 小时(编写解释性指令 15min,断点调试 30min,配置 Tracing + 自定义面板 45min,回顾与修正 30min)
行动清单
- [ ] 在你的 Skill 指令中添加调试开关,允许输出推理链
- [ ] 在本地开发时,为关键工具函数插入
breakpoint()或设置 VSCode 断点 - [ ] 开启 OpenAI Tracing 环境变量,或接入 Langfuse(一行代码即可)
- [ ] 设计一套结构化日志方案(至少包含
trace_id、事件名、耗时) - [ ] 与团队约定:每当线上出现不可解释的行为,先启用解释性输出复现,再通过 Tracing 看板确认模式,最后才动用断点深入内部
现在,你已经能从“黑盒”中透视 Agent 的思维运动。不过,修复了一个 bug 之后,自然要问:这次修改是让 Skill 变得更好了还是引入了新的退化?这需要科学的评估与对比实验。下一章《A/B 测试与评估指标是持续迭代 Skills 的罗盘》将带你设计量化评估框架,用数据代替直觉驱动优化决策,让你的每一次改动都心中有数。
agent skills 入门到精通
关于 LearnKu