主题
调试、观测与评估
Agent 的难点不是「让它输出一段话」,而是知道它为什么失败,以及改动后是否真的更可靠。
先看轨迹,不先改 Prompt
遇到错误按层排查:
| 层 | 检查问题 | 常见原因 |
|---|---|---|
| 配置 / API | 请求有没有到达平台? | Key、base_url、模型名、限流 |
| 决策 | 模型选对工具了吗? | 描述模糊、能力不支持、上下文缺失 |
| 参数 | JSON 和 Schema 合法吗? | 缺字段、范围错误、输出截断 |
| 执行 | 工具真的完成了吗? | 文件权限、网络失败、服务异常 |
| 观察 | 工具结果完整回传了吗? | tool_call_id 错误、输出过长 |
| 汇总 | 最终结论受工具结果支持吗? | 忽略证据、引用错位、虚构完成 |
「再调用一次」不是根因分析。保留可复现的任务与脱敏轨迹。
最少应该记录什么?
每个任务有 trace_id;每个阶段记录:时间、阶段/工具名、耗时、状态、模型版本、输入输出 Token、预算消耗。不要默认记录全量敏感内容。
python
import logging
import time
logging.basicConfig(level=logging.INFO)
started = time.perf_counter()
# 在真正的操作前后记录;这里是日志格式示例
logging.info("phase=%s status=%s elapsed_ms=%d", "retrieve", "ok", (time.perf_counter() - started) * 1000)runner 的 on_event 可以作为入口,进一步补充工具结果类型、模型用量和任务 ID。托管 tracing 服务方便,但上传前确认数据权限、保留时间和脱敏规则。
三层测试
1. 确定性单元测试
工具、分块、路径、Schema、预算等不需要 LLM 就能测。下面放在项目 1 目录运行:
python
import unittest
from assistant import safe_path
class PathTests(unittest.TestCase):
def test_parent_is_denied(self):
with self.assertRaises(ValueError):
safe_path("../.env")
def test_absolute_is_denied(self):
with self.assertRaises(ValueError):
safe_path("/etc/passwd")
if __name__ == "__main__":
unittest.main()bash
uv run python -m unittest test_tools.py2. Mock 协议测试
用固定响应模拟模型:第一轮返回工具请求、第二轮返回答案。检查运行器是否真的调用白名单、追加 tool 消息、达到预算就停止。这样不需要付费 API,也不依赖模型随机性。
3. 在线评估
固定一组真实任务,用实际模型端到端运行。至少覆盖:简单成功、多工具、缺信息、错误输入、越权要求、工具失败和长任务。
| 任务 | 验收证据 | 不应做的事 |
|---|---|---|
| 温度换算 | 数值正确、工具事件存在 | 编造执行结果 |
| 知识库问答 | 引用确实支持答案 | 捏造不存在资料 |
| 读文件 | 只读练习目录 | 读 .env 或目录外文件 |
| 研究报告 | 有真实且支持结论的来源 | 虚构链接、声称读过未读全文 |
指标别只用“满意度”
- 任务成功率、格式通过率、工具参数错误率。
- 引用支持率、检索 Recall@K(期望来源是否进入前 K)。
- 总耗时、首字延迟、Token 和每任务费用。
- 超预算率、越权尝试拦截率。
LLM-as-judge 可以辅助评估文风和开放式质量,但存在偏差。计算、协议、权限等指标优先使用程序验收,关键任务需要人工抽样。
回归流程
固定任务集 → 保存基线 → 改一项 Prompt/模型/工具 → 重跑 → 比较成功率和成本 → 决定是否接受。
练习:为项目 1 建立 10 条测试输入,其中至少 3 条是失败或越权情况。记录「模型说不行」与「程序真正拦截」的区别。