当 AI Agent 成为主流开发工具,每一次对话都携带完整的历史记录发送给 LLM 提供商。这对 DLP(数据泄露防护)系统提出了一个不容忽视的挑战:同样的内容被反复扫描,造成巨大的性能浪费。

问题:O(N²) 的扫描开销

LLM API 的对话格式要求每次请求都在 messages[] 数组中携带完整的对话历史。一个 50 轮的 Agent 对话意味着:

  • 第 1 轮:扫描 1 条消息
  • 第 2 轮:扫描 2 条消息
  • 第 3 轮:扫描 3 条消息
  • 第 50 轮:扫描 50 条消息

总扫描量为 1 + 2 + 3 + … + 50 = 1,275 次消息扫描,其中只有 50 次是真正的新内容。剩下的 1,225 次都是对历史消息的重复扫描。

这不仅浪费计算资源,更关键的是会产生重复的告警和审计记录,让安全团队淹没在噪音中。

当前方案:消息级哈希缓存

Bastion 目前采用的方案是消息级哈希缓存。核心思路很简单:对每条消息的内容计算 SHA-256 哈希,用哈希值作为缓存键,将 DLP 扫描结果缓存起来。

工作流程如下:

  1. 收到 API 请求后,解析 messages[] 数组
  2. 对每条消息计算内容哈希
  3. 命中缓存 → 直接返回缓存的扫描结果,标记为「已知发现」
  4. 未命中 → 执行实际扫描,将结果写入缓存,标记为「新发现」
  5. 只对「新发现」触发告警和审计记录

这样,一个 50 轮的对话实际只扫描了 50 条消息(每条只扫描一次),复杂度从 O(N²) 降到了 O(N)。在实际运行中,第 50 轮请求的缓存命中率高达 98%,只有最新的 1 条消息需要扫描。

方案的边界

消息级缓存解决了最突出的性能问题,但在实际使用中还存在一些边界情况。

缓存粒度问题。 当前缓存以整条消息为单位。如果 Agent 在回复中追加了少量内容(比如工具调用结果),整条消息的哈希就会改变,需要重新扫描。这在 tool use 密集的场景下会降低命中率。

跨会话隔离。 缓存是进程内的 LRU 缓存,重启后丢失。不同用户的会话共享同一个缓存实例——这在单用户网关场景下是合理的,但如果面向多租户则需要隔离。

流式响应的时机。 对于 streaming 响应,DLP 扫描发生在完整响应组装完成后。如果在流式传输过程中发现敏感内容,此时数据已经部分送达客户端。

后续的思考方向

分块哈希。 将消息内容按固定大小(如 512 字节)分块计算哈希,这样即使消息末尾追加了内容,前面的块仍然命中缓存。代价是实现复杂度增加,且需要处理跨块的模式匹配。

持久化缓存。 将缓存写入 SQLite,这样重启后不需要重新预热。对于长时间运行的 Agent 任务(可能跨越多次网关重启),这可以避免首次请求的全量扫描。

会话感知扫描。 在代理层维护会话状态,记录每个会话已扫描到的消息索引。新请求进来时,只扫描索引之后的新消息,完全跳过历史部分。这比哈希缓存更高效,但需要准确追踪会话边界。

增量 Redaction。 对于选择 redact(脱敏)模式的用户,可以在首次发现敏感内容时就将脱敏后的版本缓存起来。后续请求直接使用已脱敏的版本,避免重复的正则匹配和替换操作。

小结

Agent 模式下的 DLP 重复扫描本质上是一个「带历史的增量处理」问题。消息级哈希缓存是一个简单有效的第一步,将 O(N²) 降到了 O(N)。但随着 Agent 对话越来越长、工具调用越来越频繁,更细粒度的缓存策略和会话感知的扫描机制将成为必要的演进方向。

安全不应该成为 AI 开发体验的瓶颈。好的 DLP 系统应该像 Git 一样——你知道它在保护你,但几乎感觉不到它的存在。