OpenAI Agents SDK 工程笔记:TraceProcessor 导出、敏感字段脱敏与生产禁区

Agents SDK 的追踪默认是开着的。每次 Runner.run 都会生成一条 trace,下面挂着 Agent、生成、工具调用、交接等 span。排障时这很方便,可它也意味着用户输入、工具参数、工具返回值都会被打包,交给处理器导出。导出到哪里、带走了什么,很多团队上线前没有认真看过。

本文只讲导出这一面。本机 openai-agents 0.23.1 里,处理器接口叫 TracingProcessor,有 on_trace_start、on_trace_end、on_span_start、on_span_end、shutdown、force_flush 六个方法。add_trace_processor 是追加一个处理器,默认导出器仍在;set_trace_processors 是整体替换。要做脱敏,必须用替换,否则未脱敏的那份照样走默认通道出去。

离线 smoke 在 _w/trace-redact-smoke/,用 trace 与 custom_span 造三段 span,交给假后端收集,全程不出网。

图注 左侧为脱敏处理器做的四件事,右侧为反例处理器种植的四类失败。

目标说明

  1. 分清 add_trace_processor 与 set_trace_processors 对默认导出器的影响。
  2. 在 on_span_end 里对 span 数据做凭据替换和长度截断。
  3. 处理器内部出错时显式计数并告警,不让它悄悄丢数据。
  4. 用 trace 元数据区分租户,避免多个客户的任务挤在同一个 workflow 名下。

与 RunHooks 的分工

RunHooks 适合写业务日志,哪个 Agent 开始了、调了哪个工具、耗时多少。它拿到的是运行时对象,你自己决定记什么。TracingProcessor 拿到的是 SDK 已经组装好的 span 数据,默认什么都带。前者是你主动写,后者是你被动收。脱敏的重点在后者,因为你不写它也会导出。

脱敏处理器骨架

python 复制代码
SECRET = re.compile(r"(Bearer\s+[A-Za-z0-9._-]+|sk-[A-Za-z0-9]{8,})")
MAXLEN = 200

class RedactingProcessor(TracingProcessor):
    def on_trace_start(self, t):
        md = t.export().get("metadata") or {}
        if md.get("tenant") != self.tenant:
            self.errors += 1
            self.blocked.add(t.trace_id)

    def on_span_end(self, s):
        try:
            if s.trace_id in self.blocked:
                return
            data = json.dumps(s.export().get("span_data"), ensure_ascii=False)
            data = SECRET.sub("[REDACTED]", data)
            if len(data) > MAXLEN:
                data = data[:MAXLEN] + f"...<truncated {len(data)}>"
            self.sink.rows.append(dict(trace=s.trace_id, data=data))
        except Exception:
            self.errors += 1   # 计数并上报,不吞

set_trace_processors([RedactingProcessor(sink, "acme")])

脱敏在序列化之后做,因为 span 数据结构随版本会变,按字段名逐个删容易漏。先转字符串再按凭据形态替换,覆盖面更稳。代价是正则要随你的凭据格式维护。

离线 smoke 结果

text 复制代码
P1  PASS Authorization/Bearer 脱敏 -> 导出含 Bearer 明文=False
P2  PASS 工具原始响应截断 -> 最长 data=219 字符
P3  PASS 处理器异常显式计数 -> errors=2(跨租户 1 + 序列化失败 1)
P4  PASS 租户元数据校验 -> acme 处理器导出 2 段,全部属 acme trace
F1  FAIL 种植:Bearer 原样导出 -> 含明文=True
F2  FAIL 种植:工具响应未截断 -> 最长 data=5062 字符
F3  FAIL 种植:异常被吞 -> 应导出 6 段,实到 4 段,告警 0 次
F4  FAIL 种植:跨租户同 workflow_name -> 两个租户都记为 {'support-bot'},无法区分

判定写在 trace-redact-smoke.csv,before_after.csv 把反例导出和脱敏导出逐段并排,方便你给安全同事看。P2 的 219 字符是 200 字正文加截断标记。

四类失败样本拆解

F1 明文凭据外送。 工具调用参数里带了一段请求头,反例处理器原样导出。只要你的工具会转发用户的令牌,或者把内部接口的请求头写进参数,这一条就会触发。

F2 原始响应不截断。 工具返回 5000 字的页面正文,反例原样推给追踪后端。除了费存储,更大的问题是页面里可能有用户隐私、内部链接、他人数据。追踪后端的权限通常比业务库宽松得多。

F3 异常被吞。 第三段 span 序列化时注入一次异常,反例用空的 except 吞掉。两次运行应导出 6 段,只到了 4 段,告警为零。你在后台看到的是一切正常,实际上有一类 span 永远不会出现。更危险的变体是,脱敏代码自己抛错被吞,然后走兜底分支把原文发了出去。

F4 workflow 名串客户。 两个租户的任务都叫 support-bot,元数据也没带租户。出了问题你在追踪后台按名字搜,会同时翻到两家客户的对话。

生产禁区表

编号 禁区 smoke 证据 替代做法
D1 用 add_trace_processor 以为替换了默认导出 设计说明 脱敏场景用 set_trace_processors
D2 工具参数和请求头原样进 span F1 序列化后按凭据形态替换
D3 工具原始响应全量导出 F2 截断并标注原长度
D4 处理器内 except: pass F3 错误计数、告警,失败时宁可丢弃不可发原文
D5 多客户共用 workflow 名且无租户元数据 F4 trace 元数据必带租户,处理器按租户拦截
D6 把截图、像素类载荷写进 span 设计说明 只记引用编号,载荷留业务存储

span 里到底装了什么

动手写脱敏之前,先把默认导出的内容摸清楚。最省事的办法是写一个只打印不发送的处理器,跑一次你的真实 Agent,把每个 span 的 export() 结果落到本地文件看一遍。常见的几类 span 大致如下。

span 类型 常见内容 风险点
Agent Agent 名、可用工具列表、交接目标 工具名可能暴露内部系统结构
生成 模型名、输入消息、输出消息、用量 用户原话、系统提示词全文
工具调用 工具名、参数、返回值 凭据、内部接口地址、他人数据
交接 来源与目标 Agent 风险较低
自定义 你用 custom_span 写入的任意数据 取决于写的人是否克制

系统提示词也在其中。很多团队把内部规则、价格策略、合作方名称写进提示词,默认导出时这些会原样出现在追踪后台。若提示词有商业敏感内容,生成类 span 的输入部分要整体替换为摘要或哈希,而不是只做凭据替换。

正则脱敏的边界

正则只能抓有固定形态的东西,Bearer 令牌、以固定前缀开头的密钥、手机号、身份证号、邮箱。它抓不住用户在自然语言里描述的隐私,比如"我住在某小区三号楼"。这类内容要么在业务层就不进工具参数,要么在生成类 span 里整体降级为长度和哈希。

另一个容易忽略的点是多层转义。工具参数本身是 JSON 字符串,再被 json.dumps 一次,引号会变成反斜杠加引号,某些正则在这里会失配。smoke 里的正则只依赖令牌本身的字符集,不依赖引号,所以两层转义都能命中。你换成自己的规则后,务必用双层转义的样本测一次。

失败时发什么

处理器出错时有三种选择,原样发出、丢弃这一段、发一个只有元信息的空壳。生产上推荐第三种。空壳里保留 trace 编号、span 类型、开始结束时间和一个错误标记,后台还能看到调用链的形状,但看不到任何载荷。原样发出等于让脱敏形同虚设;彻底丢弃会让调用链出现断档,排障时更难判断问题在哪一层。

刷新与关闭也要处理。进程退出前调用 force_flush,否则缓冲区里最后几段可能丢失。若你的处理器内部有批量发送队列,shutdown 里要设超时,避免进程因为追踪后端不可达而卡住不退出。

要不要干脆关掉追踪

可以关,RunConfig 里有关闭追踪的开关,环境变量也能全局关。关掉之后排障只能靠你自己的日志。更常见的取舍是,生产环境保留追踪,但只走脱敏处理器,导出到你自己能控权限的后端。测试环境可以放宽截断长度,但脱敏规则不放宽,测试数据里也常混着真实凭据。

多租户下的处理器布局

租户少时,可以每个租户一个处理器实例,各自导出到独立的后端项目,权限天然分开。租户多时,用一个处理器读取 trace 元数据里的租户字段,按租户路由到不同的存储前缀。两种做法都要求 trace 创建时就带上租户,所以入口层要统一封装 trace(...) 的调用,不让各业务自己起名。

workflow 名建议用业务含义加租户段,例如客服加租户编号,而不是全公司共用一个名字。元数据里再放请求编号,方便和业务日志对齐。注意元数据本身也会被导出,不要把用户手机号之类的东西放进去。

预发检查

  1. 启动时断言当前处理器列表里没有默认导出器。
  2. 造一段带 Bearer 和 sk- 形态字符串的工具参数,断言导出结果里没有明文。
  3. 造一段 5000 字的工具返回,断言导出长度不超过上限。
  4. 在处理器里注入一次异常,断言错误计数加一且触发告警。
  5. 用两个租户各跑一次,断言各自的处理器只收到自己的 trace。

和合规同事对齐什么

上线前拿 before_after.csv 去找安全或合规同事,比拿一页设计文档更有效。表里左列是反例导出,右列是脱敏导出,哪些字段被替换、哪些被截断一目了然。对方通常会追问三件事,追踪后端放在哪里、谁有读权限、保留多久。这三个答案要在处理器配置里能找到对应项。

小结前的一个提醒

脱敏规则要有人负责维护。新接一个第三方接口,就可能多一种令牌格式。建议把正则和样本放在同一个仓库目录,接口接入的评审清单里加一条,新凭据格式是否已加入脱敏样本。这条成本很低,却能挡住最常见的一类泄漏。

抽查节奏

规则写好之后,每周从追踪后端随机抽二十段 span,人工看一遍有没有漏网的敏感内容。抽到一次,就把那段样本加进单测,规则跟着补。这样一两个月后,样本库会覆盖你系统里真实出现过的大部分凭据形态。

总结

追踪数据是 SDK 替你写的日志,默认什么都写。用 set_trace_processors 接管导出,在 on_span_end 里先脱敏再截断,异常要计数不要吞,租户要写进元数据。四个反例都能在本目录稳定复现。处理器接口与 span 字段以你安装的 openai-agents 版本为准,升级后复跑一次 smoke。

相关推荐
AIGC大时代9 小时前
OpenAI Agents SDK 工程笔记:自定义 Session 接口(get_items / add_items / clear)与多租户隔离的生产禁区
session·多租户·agents sdk·生产禁区·get_items
AIGC大时代10 天前
OpenAI Agents SDK 工程笔记:SQLiteSession 多轮会话持久化与生产禁区
agents sdk·生产禁区·sqlitesession·会话持久化
AIGC大时代10 天前
OpenAI Agents SDK 工程笔记:AgentsException 族、失败分类与生产恢复禁区
agents sdk·agentsexception·maxturns·behaviorerror
AIGC大时代10 天前
OpenAI Agents SDK 工程笔记:WebSearchTool 与 FileSearchTool 托管工具边界与生产禁区
agents sdk·websearchtool·filesearchtool·托管检索
AIGC大时代12 天前
OpenAI Agents SDK 工程笔记:tool_use_behavior 与 is_enabled 动态工具开关生产禁区
agents sdk·生产禁区·is_enabled
AIGC大时代13 天前
OpenAI Agents SDK 工程笔记:LocalShellTool 与 ApplyPatchTool 执行面边界与生产禁区
agents sdk·生产禁区·localshelltool·applypatchtool
AIGC大时代13 天前
OpenAI Agents SDK 工程笔记:ModelProvider 多模型接入、Responses/Chat Completions 切换与生产禁区
responses·生产禁区·modelprovider·multiprovider
AIGC大时代21 天前
OpenAI Agents SDK 工程笔记:MCP 工具接入与生产禁区
mcp·生产禁区·openai agents sdk·mcpserverstdio·hostedmcptool
AIGC大时代1 个月前
OpenAI Agents SDK 工程笔记:sessions 记忆边界再核对与生产禁区
生产禁区·sessions·sqlitesession·sessionsettings