privacy-client-side-log-hygiene · ZH · 2026-11-03

你的日志比聚合站漏得更多:在一把 key 上把提示词与响应从客户端追踪里擦掉

提示词与响应经常在你自己的日志、调试器和 CI 日志里先泄漏,而不是聚合站。本文从开发者侧出发,介绍在客户端把请求与响应内容从日志中擦除的实操方法,包括日志分级、结构化脱敏、错误负载处理、CI 日志抑制,以及失败时的降级观察策略。

泄漏往往先发生在你这一侧

讨论多模型聚合站时,很多人担心的是服务端会不会留存提示词。但更常见、更隐蔽的泄漏点,其实在开发者自己的机器和流水线里:

  • 本地调试时 print 或 console.log 直接打印整个请求体。
  • HTTP 调试代理或抓包工具把带 Authorization 和 messages 的完整请求落盘。
  • 错误处理里把上游返回的原始响应体拼进异常消息,再被上报到日志平台。
  • CI 里跑测试或 eval 时,stdout 被采集进构建日志,而日志默认保留很久。
  • 容器日志、Kubernetes events、serverless 平台日志自动捕获 stdout。

这些位置大多不由聚合站控制。要先把这一侧的出口堵住,再谈服务端留存。

先做日志分级:默认不打印正文

最有效的做法是改变默认行为,而不是依赖开发者自觉。

  • 把日志内容分成几档:元信息(时间、模型、status、耗时、request_id)、结构信息(消息条数、token 估算、工具调用名)、正文(messages 内容、choices 内容)。
  • 默认只记录前两档。正文打印需要显式开关,例如环境变量 LLMLOGPAYLOAD=1,且只在交互式终端内生效。
  • 生产环境强制关闭正文打印;开发环境也让它只在该次调试会话内有效。

这样做的好处是:即使有人随手写了 print(response),日志框架因为默认脱敏而不会把正文带出去。

结构化脱敏:在序列化前动手

在日志框架里做字符串正则匹配容易漏,更稳的方式是在对象被序列化之前替换字段。

  • 写一个 redact(obj) 函数,递归遍历请求和响应对象。
  • 对 messages[].content、choices[].message.content、prompt、input 等字段,替换成固定占位符,例如 <redacted:{len}>,保留长度信息便于排查截断问题。
  • 对 Authorization、api-key、x-api-key 等头直接置空。
  • 对工具调用的 arguments 与 function 结果同样处理,这部分常被忽略。
  • 对 base64 或长字符串先截断再打印,避免大字段污染日志。

把 redact 挂在日志记录的入口,而不是挂在每个调用点,这样新增代码路径也不会绕过。

错误负载:最容易漏的出口

异常路径是脱敏的重灾区,因为开发者往往想保留“上游原样返回”以便排查。

  • 不要直接把上游响应体塞进异常消息。单独存一个错误对象,只带 status、request_id、错误码和简短的 message。
  • 如果必须保留原始响应用于工单,写入一个独立的、访问受限的调试目录,并在写入前做同样的脱敏。
  • 日志平台的上报 SDK 常会序列化异常对象的所有属性;确认你的 redact 也覆盖异常携带的 request/response 挂载字段。

一个可行的观察策略是:只记录“发生了什么错误”(如 401、429、超时、内容过滤)和请求的元信息,不记录触发了错误的正文。排查多数问题并不需要正文。

CI 与自动化流水线

测试和评测脚本经常把真实提示词发出去,然后把响应打印到构建日志。

  • 在 CI 中把 LLMLOGPAYLOAD 一类开关强制置为关闭,即使仓库里的配置文件误开也不生效。
  • 评测脚本输出只写指标与判定结果,不写原始输入输出。需要复现时,用指标 + 种子 + 脱敏片段。
  • 确认 CI 平台是否把 stdout 转发到第三方日志服务,以及保留期。保留期越长,越要默认关闭正文。
  • 不要把含真实提示词的 fixture 提交进仓库;用合成数据或占位符。

调试代理与抓包工具

本地 HTTP 调试器很方便,但会把完整请求和响应写到磁盘或界面历史里。

  • 为 LLM 请求设置单独的代理配置,只在你明确需要时开启捕获。
  • 关闭“保存响应体”选项,或者把捕获文件放到临时目录,会话结束后清理。
  • 注意工具是否会把历史同步到云端或分享链接;分享出去的历史同样属于泄漏。
  • 如果必须保存一次完整会话用于复现,用脱敏后的副本,而不是原始抓包文件。

一把 key 下的实践约定

聚合站用同一把 key 调多个模型,请求结构大致一致,这反而让客户端脱敏更容易统一:

  • 在封装层统一处理请求构造与日志,所有模型走同一个 redact 出口。
  • 把元信息日志(模型名、request_id、耗时、状态码)和正文日志彻底分离,前者可以长期保留,后者默认丢弃。
  • 给团队一个明确的规则:要看正文,就在本地交互式会话里看,看完即丢;不要把它带进共享日志系统。

检查清单

  • 默认日志级别不包含任何 messages 或 choices 的 content。
  • 异常对象在进入日志框架前已经过 redact。
  • CI 中正文打印开关被强制关闭,评测输出只含指标。
  • 抓包工具不落盘响应体,历史不自动同步到云端。
  • 仓库里没有含真实提示词的 fixture。
  • 需要复现时,用长度、哈希前缀或合成样本代替原文。

把这几件事做好,提示词与响应在到达聚合站之前就已经在你的客户端被擦掉了。剩下的服务端留存问题,才是聚合站该回答的部分。