AI 调用日志如何兼顾排障与数据隐私
文章建议默认记录模型、提示词版本、调用主体、令牌用量和安全状态等元数据,仅在例外情况下保存正文。通过先明确排障、审计和成本问题,避免日志成为客户敏感数据的额外副本。
AI 深度解读
给 AI 功能排查问题,最顺手的办法是把 prompt、回答和用户输入全部写进日志。问题也从这里开始:客户邮件、合同、工单被复制到另一个系统,而它的访问控制可能更宽,保存时间可能更长。
这篇文章给出的方案很具体:每次调用都记录元数据,正文只在必要时短暂采样,并且先脱敏、再截断。它值得读的地方,是把排错、审计、成本统计需要什么拆开了,也提醒我们检查异常平台和链路追踪里那些容易漏掉的数据副本。
日志要回答问题,必须保存全文吗
作者建议先列问题,再设计日志字段。小团队通常需要回答四类问题:
- 排错时,知道哪个 prompt 版本、哪个模型生成了回答,以及调用有没有失败。
- 审计时,知道哪个用户或服务,在什么时间,对哪条业务记录使用了 AI。
- 核算成本时,按功能和客户汇总 token 消耗。
- 检查安全情况时,知道调用是否被拦截、过滤或标记。
这些问题大多可以通过元数据回答。比如,工单摘要突然频繁失败,先对照 prompt_version、model 和 status,就能缩小调查范围,不必一开始就检索客户原文。
默认日志应当记录调用发生了什么,正文采样则需要单独开启。
不过,知道「哪个版本生成了坏回答」,并不等于知道「为什么回答错了」。元数据能定位和统计,涉及语义、上下文遗漏的问题,仍可能需要受控查看业务记录,或用测试数据复现。
每次调用应该留下哪些字段
下面是作者的默认日志实现。它把版本、业务记录、输入指纹、token 和耗时放进同一条结构化记录,供后续检索与聚合。
import hmac
import json
import logging
import os
import time
import uuid
log = logging.getLogger("ai_usage")
FP_KEY = os.environ["AI_LOG_FP_KEY"].encode() # 随机密钥,不写进代码
def fingerprint(text: str) -> str:
"""生成带密钥的不可逆指纹,用于归并相同输入。"""
return hmac.new(FP_KEY, text.encode("utf-8"), "sha256").hexdigest()[:16]
def log_ai_call(*, feature, prompt_name, prompt_version, model, actor_id,
record_id, prompt_text, response_text, usage, status, started):
# started:调用前取得的 time.monotonic() 值
usage = usage or {} # 失败调用经常没有用量数据
call_id = str(uuid.uuid4())
log.info(json.dumps({
"event": "ai_call",
"call_id": call_id,
"feature": feature, # 例如 "ticket_summary"
"prompt": prompt_name,
"prompt_version": prompt_version,
"model": model,
"actor_id": actor_id, # 内部用户或服务 ID,不用邮箱
"record_id": record_id, # 例如工单 ID
"input_chars": len(prompt_text),
"input_fp": fingerprint(prompt_text),
"output_chars": len(response_text or ""),
"input_tokens": usage.get("input_tokens"),
"output_tokens": usage.get("output_tokens"),
"status": status, # "ok", "error", "blocked"
"latency_ms": int((time.monotonic() - started) * 1000),
}))
return call_id
要留意 usage = usage or {}:失败请求可能没有用量信息,日志函数不能因此再次报错。latency_ms 使用 time.monotonic() 计算经过时间;调用时间则需要日志系统提供时间戳。
输入指纹(fingerprint)用于判断两次调用是否带着相同输入。普通哈希面对邮箱、是非答案等可猜测内容,可以被枚举验证,因此作者用 AI_LOG_FP_KEY 配合 HMAC-SHA256,再通过 [:16] 截取指纹。没有密钥的人无法直接按同样方式验证猜测。
但这不是隐藏标识符的通行证。作者明确要求,不要拿邮箱等标识符做指纹,也不要把邮箱写进 actor_id;指纹的适用对象是较长、变化较多的输入。
这份样例还有几个需要补齐的地方:它没有独立的客户字段,按客户统计需要从业务关系映射;token 用量要结合模型价格才能换算金额;blocked 也没有细分过滤、标记的原因。原文给的是基础记录结构。
必须查看正文时,脱敏只能挡住一部分
正文日志由 AI_LOG_CONTENT 控制,只有值等于 "1" 时才开启。作者用单独的 ai_call_content 事件保存样本,通过 call_id 关联默认记录。
采样之前,先运行下面的脱敏函数,处理明显密钥、邮箱、卡号形态数字和电话号码,并默认截取前 500 个字符。
import re
PATTERNS = [
(re.compile(r"\b(?:sk|pk|api|key|token|ghp|xox[bp])[-_][A-Za-z0-9_-]{12,}\b", re.I), "[SECRET]"),
(re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+"), "[EMAIL]"),
(re.compile(r"\b(?:\d[ -]?){13,19}\b"), "[CARD]"),
(re.compile(r"(?<![\w.])\+?\d(?:[\s()-]{0,2}\d){9,14}(?![\w.])"), "[PHONE]"),
]
def redact(text: str, limit: int = 500) -> str:
for pattern, label in PATTERNS:
text = pattern.sub(label, text)
return text[:limit]
要注意 PATTERNS 的顺序:密钥先于邮箱、卡号和电话处理。否则电话规则可能先替换掉密钥中间的数字,留下其余片段。函数也是先脱敏,再执行 text[:limit] 截断。
正则的能力到这里就有边界了。姓名、地址、医疗描述,以及许多云访问密钥和 bearer token,都可能漏过。截断也只是减少保存量,开头 500 个字符照样可能包含敏感信息。
因此作者要求,正文采样默认关闭,只针对具体问题短期开启。健康、金融或法律信息则完全不记录正文,改用测试数据复现。
存储也要分开。按 event 路由到不同日志流,让正文样本采用更短的自动保留期和更窄的访问权限。原文没有指定保存几天,团队需要自行确定。
此外,还得检查四处副本:AI provider 的保留与训练设置、异常平台捕获的局部变量、追踪 span 的属性,以及聊天和工单里的粘贴内容。异常日志尤其容易漏,作者建议保存 type(exc).__name__ 和状态码,避免 str(exc) 把 provider 回显的 prompt 片段带进去。讨论问题时则分享 call_id。
我的判断
我赞同先定义日志用途。它让团队能逐项解释:为什么存这个字段,谁需要看,保存多久。对接大客户时,功能、模型、操作者、记录和 prompt 版本,也比搜索原始文本更适合做审计查询。
但文章没有给出团队实测、脱敏漏检率或排错效率的对照数据。这是一套工程经验方案,不能据此保证调试效率不受影响,也不能把它当成合规证明。
与全量正文日志相比,它减少了数据副本,却增加了复现问题的成本;与完全不记日志相比,它保留了版本追踪、失败统计和用量分析能力。取舍是否合适,取决于业务敏感程度和团队的排错方式。
还有一个落地缺口:环境变量开关本身没有限定客户、请求或自动关闭时间。若直接全局开启,采样范围可能超过预期。内部 ID 也仍然能关联业务记录,元数据日志同样需要访问控制。
能拿来做什么
- 在 Node 服务的 AI 调用封装层统一生成结构化日志,至少保存功能、prompt 版本、模型、内部操作者 ID、业务记录 ID、token、状态和耗时,让各业务入口采用同一套字段。
- 给 prompt 建立可追溯版本,并保留仓库变更历史。前端提交坏回答反馈时附上
call_id,方便定位对应调用,减少向群聊复制正文。 - 确实需要正文采样时,把启用范围、关闭时间、脱敏规则和自动过期一起配置好;健康、金融、法律内容按作者建议使用测试数据复现。
- 检查现有异常平台、链路追踪和 AI provider 设置,重点确认请求体、prompt 变量、span 属性及错误消息是否留下了正文副本。