跳转到内容
Tutorial

在 AI 代理日志到达数据库之前对其进行编辑

| 7 min read

您的代理会记录每个提示和工具调用。 成绩单中遗漏的 SSN 会变成 GDPR 披露。 三行中间件在写入行之前修复了它。

Padlock representing PII redaction and data protection in AI agent logs
Photo by FLY:D on Unsplash

您的 AI 代理会记录提示。 工具调用输入。 工具结果。 最后的回应。 这曾经是调试数据的金矿。 今天,这是一项 GDPR 披露,正在等待支持票以携带 SSN 通过验证。

您无法阻止用户粘贴敏感数据。 您可以阻止原始文本到达日志存储、可观察性供应商和每周评估导出。 一个小型中间件可以一次性完成此操作。

大多数代理设置已经存在的泄漏

// before: every raw prompt, tool call, and tool result lands in the log row
logger.info({
  event: 'agent.turn',
  prompt: userInput,
  tool_calls: toolCalls,
  tool_results: toolResults,
});

// one support ticket later: "My SSN is 123-45-6789 and card 4111 1111 1111 1111"
// sits in the logs, the observability vendor, and the weekly eval export.

现在每一行都有一个卡号。 日志存储将其保留 30 天。 可观测性 SDK 将其发送给第三方。 两天后,eval 导出会拾取相同的字符串。 一张卡五份; 所有这些都超出了静态加密策略的范围。

通过一次调用即可检测 PII

波托伊 /v1/pii/detect 端点扫描文本中的电子邮件、电话号码、SSN、信用卡(经过 Luhn 验证)、IP 地址和出生日期。 它返回每个结果,其中包含起始偏移量、结束偏移量和可以放置在适当位置的屏蔽值。

要求

curl -X POST https://api.botoi.com/v1/pii/detect \\
  -H "Content-Type: application/json" \\
  -d '{"text": "Reach me at alice@example.com or 555-123-4567. Card: 4111 1111 1111 1111."}'

回复

{
  "found": true,
  "count": 3,
  "findings": [
    { "type": "email",       "value": "alice@example.com",      "start": 12, "end": 29, "masked": "al***@example.com" },
    { "type": "phone",       "value": "555-123-4567",           "start": 33, "end": 45, "masked": "***-***-4567" },
    { "type": "credit_card", "value": "4111 1111 1111 1111",    "start": 53, "end": 72, "masked": "************1111" }
  ]
}

三场比赛,三场蒙面替换,位置可以拼接干净。 无需维护正则表达式库,无需保持最新的 SSN 前缀表,无需自己编写 Luhn 通行证。

写入前进行编辑的日志中间件

正确的位置是行离开进程之前的最后一跳。 每个上游组件仍然可以看到它需要的原始文本; 持久副本已被清理。

// log-redact.ts
import type { LogRecord } from './types';

const PII_FIELDS = ['prompt', 'tool_calls', 'tool_results', 'output'] as const;

export async function redactPii(record: LogRecord): Promise<LogRecord> {
  const clone = structuredClone(record);
  for (const field of PII_FIELDS) {
    const value = clone[field];
    if (!value) continue;
    clone[field] = await scrub(JSON.stringify(value));
  }
  return clone;
}

async function scrub(text: string): Promise<string> {
  const res = await fetch('https://api.botoi.com/v1/pii/detect', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: \`Bearer \${process.env.BOTOI_API_KEY}\`,
    },
    body: JSON.stringify({ text }),
  });
  const data = await res.json();
  if (!data.found) return text;

  // Replace from the end of the string so offsets stay valid.
  const sorted = [...data.findings].sort((a, b) => b.start - a.start);
  let scrubbed = text;
  for (const f of sorted) {
    scrubbed = scrubbed.slice(0, f.start) + f.masked + scrubbed.slice(f.end);
  }
  return scrubbed;
}

该片段中的三个细节很重要。 首先,它遍历已知的 PII 密集字段,而不是整个记录; 您无需清除请求 ID。 其次,它在发送到 API 之前将每个字段序列化为单个字符串,因此一次调用即可覆盖整个工具结果。 第三,它从字符串末端拼接替换,因此偏移不会在字符串下方移动。

将其连接到记录器中

// logger.ts
import { redactPii } from './log-redact';

export async function logTurn(raw: LogRecord) {
  const safe = await redactPii(raw);
  await logStore.write(safe);
}

// anywhere in your agent loop:
await logTurn({
  event: 'agent.turn',
  prompt: userInput,
  tool_calls: toolCalls,
  tool_results: toolResults,
});

称呼 logTurn 代替您现有的 logger.info 在转弯边界处。 上游的一切都保持不变。

失败关闭,而非失败静默

检测端点通常会在 20 毫秒内做出响应。 当超时时,您仍然可以选择:记录原始行(泄漏风险)或删除敏感字段并记录标记。 对于合规性敏感的工作负载,删除是更安全的默认设置。

async function redactPiiSafe(record: LogRecord): Promise<LogRecord> {
  try {
    return await Promise.race([
      redactPii(record),
      new Promise<LogRecord>((_, reject) =>
        setTimeout(() => reject(new Error('pii-detect timeout')), 250)
      ),
    ]);
  } catch (err) {
    // Fail closed: drop the sensitive fields rather than logging them raw.
    return { ...record, prompt: '[REDACT_FAILED]', tool_calls: [], tool_results: [] };
  }
}

将超时设置为较小的值。 250 毫秒足以吸收区域减速,而不会阻塞健康的请求路径。

Python版本

# log_redact.py
import os, json, httpx

PII_FIELDS = ('prompt', 'tool_calls', 'tool_results', 'output')
API = 'https://api.botoi.com/v1/pii/detect'

async def scrub(text: str) -> str:
    async with httpx.AsyncClient(timeout=0.25) as client:
        r = await client.post(
            API,
            headers={'Authorization': f"Bearer {os.environ['BOTOI_API_KEY']}"},
            json={'text': text},
        )
    data = r.json()
    if not data.get('found'):
        return text
    out = text
    for f in sorted(data['findings'], key=lambda x: x['start'], reverse=True):
        out = out[:f['start']] + f['masked'] + out[f['end']:]
    return out

降低 scrub 进入代理框架的日志挂钩。 FastAPI 中间件、LangChain 回调和 OpenInference span 导出器都接受异步函数。

这个中间件不做什么

  • 它不会捕获看起来不像任何受支持类型的名称、地址或帐号。 这些需要一个命名实体模型和一个策略决策(掩码?丢弃?编辑整个回合?)。
  • 它不能保护您的模型供应商看到原始提示。 For that, run the same detect call on the client before you send to the model.
  • 它不会取代数据保留政策。 不管怎样,缩短日志 TTL。

这属于两个地方

保护 通话时间
模型请求之前 模型供应商、训练数据、评估泄漏 阻塞、用户可见的延迟
写入日志之前 日志存储、可观测性供应商、导出 带外,对用户不可见

首先发送日志写入中间件。 它在热路径之外运行并阻止最常见的泄漏模式。 一旦覆盖原木面,就添加预模型版本。

获取 API 密钥并开始

匿名访问每分钟为您提供 5 个请求,足以针对示例日志尝试端点。 对于生产中间件,请获取免费密钥: botoi.com/api/signup。 免费套餐涵盖每天 1,000 次擦洗呼叫,无需信用卡。

请参阅以下位置的完整端点参考: PII 检测 API 页面 或浏览 api.botoi.com/docs 对于其他 149 个端点。

FAQ

为什么 AI 代理日志比普通服务器日志泄露更多 PII?
代理记录整个提示、每个工具调用输入以及每个工具输出。 曾经存在于“不记录”标志后面的支持记录现在出现在五个地方:协调器、工具服务器、可观察性供应商、模型提供者和训练评估集。
编辑步骤应该在哪里运行?
在行发送到日志存储之前,在日志写入器边界运行它。 这样,每个上游组件(编排器、工具、可观察性 SDK)都可以看到它需要的原始文本,并且只有持久的副本会被清理。
正则表达式编辑器能捕获所有内容吗?
不会。自行编写的正则表达式会错过间距异常的信用卡号、看起来像其他 9 位数字的 SSN 以及人名。 像 /v1/pii/detect 这样的 API 在卡片上运行 Luhn、过滤 SSN 前缀并返回位置,这样您就可以只删除匹配项,而不是整行。
Botoi PII 检测 API 会增加多少延迟?
端点在边缘运行,并在 20 毫秒内返回 500 个令牌的有效负载。 您可以在日志中间件中同步调用它,而不影响用户可见的响应时间; 发送响应后发生日志记录。
在发送给模型之前我可以在客户端上进行编辑吗?
是的,这是一个很好的第二层。 在服务器中间件中进行编辑可以保护您的日志存储; 在客户端中进行编辑可以防止您的模型供应商看到原始 PII。 两者都是 GDPR 友好的设置。

开始使用 botoi 构建

150+ 个 API 端点,涵盖查询、文本处理、图片生成和开发者工具。免费套餐,无需信用卡。