Agent日志审计全链路记录:prompt、工具调用
当 Agent 出现异常回答或反复调用某个工具时,只看最终结果很难定位问题。
本文介绍如何通过全链路日志记录 Agent 每次请求的 prompt、工具调用、返回结果与时间戳,并提供一套可落地的 Python 装饰器方案,帮助零基础用户快速建立起可检索的审计日志,方便后续排查与监控告警。
先确定日志要覆盖哪些字段
要真正实现全链路审计,日志字段必须足够完整。
除了最基本的 prompt 和工具返回值,建议至少包含以下内容:
- 请求ID:每次用户请求生成一个唯一标识,方便串联整个对话流程。
- prompt原文:记录用户输入的完整内容,注意去掉可能违规或敏感的信息。
- 工具名称:本次调用了哪个工具,比如
search、calculator、get_weather。 - 入参快照:调用工具时传入的关键参数。
- 返回结果:工具的原始输出,建议截断超长结果,避免日志文件膨胀。
- 时间戳与耗时:记录开始时间、结束时间和总体耗时,用于性能分析。
这些字段可以放在 JSON Lines 文件中,每行一条记录,既方便阅读,也方便后续用 jq 或日志平台解析。
用装饰器统一埋点
手动在每个工具函数里写日志容易漏报,而且代码重复。
推荐用 Python 装饰器统一处理,下面是一个最小可用的实现:
import time
import logging
import json
from functools import wraps
logger = logging.getLogger("agent_audit")
logging.basicConfig(filename="agent_audit.log", level=logging.INFO)
def audit_tool_call(request_id=None):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
# 记录入参
payload = {
"request_id": request_id,
"tool": func.__name__,
"args": kwargs,
"start_time": time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(start)),
"timestamp": int(start * 1000)
}
try:
result = func(*args, **kwargs)
payload.update({
"status": "success",
"result": result,
"duration_ms": round((time.time() - start) * 1000, 2),
"end_time": time.strftime("%Y-%m-%d %H:%M:%S")
})
logger.info(json.dumps(payload, ensure_ascii=False))
return result
except Exception as e:
payload.update({
"status": "error",
"error": str(e),
"duration_ms": round((time.time() - start) * 1000, 2)
})
logger.error(json.dumps(payload, ensure_ascii=False))
raise
return wrapper
return decorator
在工具函数上加上 @audit_tool_call(request_id=current_request_id),每次调用都会自动生成一条 JSON 日志。
如果需要在 prompt 层面记录,可以在 Agent 的主流程入口单独写一行日志,把当前 prompt 也带上。
把 prompt 和工具调用链条串起来
光记录工具调用还不够,还要知道哪个 prompt 触发了后续动作。
建议在主处理函数中先写入一条“请求开始”日志,包含请求ID、prompt、开始时间,然后再记录工具调用,这样日志文件里就能看到完整链路:
def run_agent(prompt):
request_id = uuid.uuid4().hex
logger.info(json.dumps({
"event": "prompt_received",
"request_id": request_id,
"prompt": prompt,
"timestamp": int(time.time() * 1000)
}, ensure_ascii=False))
# 后续所有工具调用都传入 request_id
@audit_tool_call(request_id=request_id)
def search(query):
return "搜索结果"
result = search(prompt)
return result
这样日志里就能按 request_id 过滤出某个请求的完整时间线,包括 prompt_received、tool: search、返回结果和耗时。
常见坑位与避坑建议
- 只记录成功不记录失败:异常路径同样要写日志,否则线上出问题时日志里一片空白。
- 日志写入阻塞业务:在低峰期直接写文件问题不大,高并发建议改用异步队列或
logging.Handler异步写入。 - 忘记截断上下文:工具返回大量文本时,完整写入会拖慢 I/O。可以只保留前几十个字符,并附加
result_truncated标记。 - 敏感信息脱敏:prompt 或结果里可能包含令牌、手机号等,记录前先做脱敏处理,避免审计日志成为数据泄露点。
验证审计日志是否完整
写完代码后,建议用一条真实 Agent 请求做验证。
日志文件生成后,用下面的命令检查 JSON 是否合法:
tail -n 5 agent_audit.log | jq .
如果 jq 能正常解析,说明日志结构没问题。
再按请求ID统计某次请求的调用次数和耗时:
grep "你的请求ID" agent_audit.log | jq -r '[.tool, .duration_ms] | @tsv'
正常情况下,你能看到 prompt_received 记录,以及后续每个工具调用的名称、状态、耗时和时间戳。
通过完整记录 prompt、工具调用过程、返回结果和时间,Agent 就不再是一个黑盒。
建议先按本文方法跑通最小闭环,再根据实际场景增加筛选和告警。
遇到异常时优先查看日志里的时间戳和调用顺序,通常能快速定位问题所在。