_AI辅助声明:本文在Claude的协助下起草。所有技术内容、设计决策、代码引用和截图均反映我在airCloset设计并运行的生产系统;正文在发布前由我本人修订。_
大家好,我是Ryan,airCloset的CTO。
在第一部分中,我介绍了四个监控轴(应用 / 基础设施 / CI / LLM)以及每个轴最终刻意形成的不同形态。这基本涵盖了可观测性栈的写入端。
但塑造写入端并非故事的终点。当生产数据流经整个栈时,你必须阻断PII(个人身份信息)可能潜入的路径——无论是否涉及AI,这一点都成立。这是一个经典的可观测性问题:如果你偷工减料,就会直接走向数据泄露事件。
从历史上看,能读取日志的人群与能读取数据库的人群高度重叠。对于拥有数据库访问权限的工程师来说,日志并不是获取个人数据的额外路径——这使得日志侧的防御措施即使加强,对大多数组织来说也无法有意义地提升整体防线。
AI打破了这一前提。通过MCP(模型上下文协议)拉取日志的非工程师人员没有数据库访问权限。日志首次成为一条让没有数据库访问权限的人也能接触到个人数据的路径。在此基础上,日志内容现在会流入AI的输入,这引入了新的暴露面:传输给模型,以及在模型输出中重新呈现。日志PII保护已从"值得做的卫生习惯"转变为"作为信任边界重新设计的必要条件"。这就是本文的出发点。
此外,如果可观测性栈不能被AI查询,那么第一部分中"AI可消费的可观测性"这一目标就完全落空了。
第二部分是关于我如何调和这两者——在保护PII的同时保持对AI的可搜索性——以及这种组合如何最终驱动从CI故障到PR提案的自愈。
可观测性栈是PII的自然路径
应用发出日志 → 日志进入Loki → AI通过MCP查询。搭建这个简单的流程,你会得到:
明文PII汇集在可观测性栈中意味着AI可以直接搜索它。这其实不是AI问题,而是可观测性问题:栈本身变成了PII的管道。同时,如果你完全清除PII,你就会失去像"我想调查客户A的支持工单"这样的查询能力,而这本是正常的支持工作流程。
cortex(内部AI平台)必须调和这两者。关键原则是:不要让"阻断PII路径"和"通过PII搜索"相互排斥。
注意:这里的"cortex"指的是airCloset内部AI平台的代号。与Snowflake Cortex、Palo Alto Networks Cortex等无关。
多层PII设计——六层
cortex的PII处理分为六层,每层有不同的角色:
| 层 | 目的 | 机制 |
|---|---|---|
| 写入端:BQ策略标签 | 列级访问控制 | pii_high / pii_medium / pii_low 三级分类。如果没有列上的细粒度读取器,SELECT会报错 Access Denied(纯CLS(列级安全)——无动态掩码) |
| 写入端:ETL DLP | 从派生表中剥离明文PII | Cloud DLP在转换过程中进行编辑(客户支持数据等)。像 [EMAIL_ADDRESS] / [PHONE_NUMBER] 这样的占位符保留结构 |
| 写入端:日志哈希 | 明文永远不会到达Loki | 应用端在日志发出前通过 hashEmail(HMAC-SHA256 → 12字符前缀;密钥存储在可观测性栈之外)进行哈希 |
| 搜索端:两侧相同函数 | 查找特定客户的日志,无需接触明文 | 查询端在发送到Loki之前运行相同的 hashEmail |
| 输出端:MCP掩码 | AI消费时进行掩码 | 列名检测掩码本地部分(例如 r***@air-closet.com),保留 @domain,以便一线分类仍能判断账户属于哪个域名 |
| 身份分离 | 内部员工电子邮件与客户PII分开处理 | 由Edge Router进行HMAC签名作为认证归属;不属于掩码管道的一部分 |
第四行——搜索端使用两侧相同的函数——是安全性与可用性权衡最为紧张的地方。
我将以电子邮件作为贯穿示例,但这六层防护的远不止电子邮件。PII涵盖姓名(包括读音)、电话号码、地址、邮政编码、出生日期、银行卡和银行详细信息、外部服务ID等等。匿名化技术因字段性质而异——相同函数哈希以保持关联性(电子邮件、电话号码),部分掩码(姓名、地址),完全编辑(卡号、令牌)——每个字段单独决定。保持不变的是结构:这六层中哪一层保护它,以及如何保护。这是设计中可复用的部分。
而且,这种匿名化并不仅限于可观测性日志(Loki)。例如,一个查询服务数据库的MCP工具会将客户姓名、地址和电话号码拉入其结果集,因此在任何内容返回给AI之前,都会运行相同的PII匿名化规则。一致的规则是"在每一条到达AI的数据路径上对PII进行匿名化",适用于所有数据源类型,而不仅仅是某一种。
写入端和搜索端都进行哈希
天真地"从日志中移除PII"会让你无法回答"让我查一下客户A的日志"。但如果你在写入时进行哈希并将该哈希存储在日志中,搜索端可以对输入运行相同的哈希函数并找到匹配的记录。明文电子邮件永远不会触及任何一端。

具体来说:
写入端:
// 应用代码
logger.info("Subscription updated", {
user: hashEmail(user.email), // → '7a3f9c2e0b1d' (HMAC-SHA256 12字符前缀)
plan: "monthly",
});
// → 只有 hashEmail 的结果最终进入Loki
搜索端(当你想拉取特定客户的日志时):
这里有个尴尬之处。"拉取客户A的日志"——天真的构建方式是将原始电子邮件交给AI,然后AI将其传递给MCP工具进行搜索。但这意味着将明文PII交给AI(模型及其背后的供应商)。无论你在Loki内部如何用哈希保护,它都会在搜索输入处泄露,早了一步。
因此,在cortex中,搜索工具接受一个非PII的ID,在MCP服务器内部将其解析为电子邮件,在那里进行哈希,并只返回哈希值。电子邮件仅存在于MCP服务器内部,永远不会到达模型:
// MCP工具 resolve_email_hash(在服务器端运行)
// 输入是一个ID(非PII)。电子邮件永远不会返回给调用者 = AI。
const email = await resolveEmailById(userId); // 从数据库解析,服务器端
const hash = hashEmail(email, secret); // 与写入端相同的函数和密钥
// → AI只得到哈希值,永远不会得到电子邮件
AI获取该 hash 并通过Grafana MCP搜索Loki,查询条件为 {service_name="subscription"} |~ "${hash}"。写入端和搜索端都使用相同的 hashEmail 和相同的密钥,因此同一客户的日志会收敛到相同的哈希值。同时:
这复用了哈希函数"相同输入 → 相同哈希"的特性,形式为"两侧相同的函数使搜索成为可能"。安全性与调试可用性的权衡得到了简洁的压缩。
当然,这仅仅是应用日志层。BQ侧由基于策略标签的列级访问控制作为其自身层进行保护(上表第1-2行)。整个体系是多层的。
使"获取ID,在内部解析并哈希"这种模式有效的原因是明文电子邮件永远不会跨越MCP服务器的信任边界。简单的实现(将原始电子邮件交给AI,让工具搜索)会在搜索输入处将明文泄露给模型,无论你在Loki内部保护得多好。你可以争辩说"供应商的条款说它不会泄露",但那是依赖条款,在审计下是脆弱的。获取ID并在内部哈希,你在结构上(而非合同上)将明文与模型隔离。当我在开头说PII保护已成为"信任边界重新设计"时,我指的就是这种设计决策。
顺便说一句:当我解决这个问题时,我向AI寻求帮助,它建议构建一个管理界面,让人工手动将电子邮件转换为哈希。这确实是一种将PII与模型隔离的方法,但它不适合自主操作——在任何调查开始之前都需要人工介入。cortex的构建目标是贯穿到"在任何人注意到之前修复"的自愈,因此引入人工的解决方案不在考虑范围内。"获取ID,在MCP服务器内部哈希"正是源于这个约束。什么算作可接受的解决方案,最终是我这边的设计判断。
集成面——同一后端上的"人类=Web,AI=MCP"
三个后端(Prometheus / BigQuery / Loki)现在承载着可观测数据,并且PII已得到处理。下一个问题是谁来查询它们,以及如何查询。常见的陷阱是分别构建"人类仪表盘聚合"和"AI数据馈送"。一旦你这样做:
cortex的选择是:共享一个可观测性后端;只有面向消费者的接口不同。

人类侧:AI运营门户
有一个内部门户(代号PI Lab),按监控目标聚合仪表盘:
以下是MCP使用仪表盘的实际样子:

在过去30天内,service-product-graph 有37,946次调用(其中7,106次错误),gws 有19,350次,db-graph 有17,297次——这只是排名靠前的。哪个MCP被使用了多少,故障出现在哪里——每天一瞥即可见。(某些服务器看似"高错误率"的部分原因是计入了类型错误——如"权限拒绝"这类预期的拒绝——因此解读时需要谨慎。)上一系列中提到的"annotation graph MCP,约50,000次调用 / 73个用户"的数据也来自这个视图。
React侧的这些页面通过内部API从BQ / Prometheus / Loki拉取数据。聚合逻辑位于API层。
AI侧:MCP
当AI代理需要相同的数据时,它们通过特定用途的MCP进行:
claude_usage.claude_usage / cortex.mcp_tool_calls 的SQL查询设计的关键点:人类仪表盘和AI MCP共享同一个后端。 没有单独的"AI聚合表"和"人类聚合表"。一次构建可观测性后端,然后在其上提供面向消费者的接口层(Web仪表盘 / MCP)。
用DDD(领域驱动设计)术语来说,MCP和Web仪表盘都只是表示层——进入同一领域(可观测性后端)的不同I/O通道。将MCP视为"特殊的东西"会导致重复实现;将其视为一种表示层形式则能保持设计简洁。
这正是"可观测性栈对AI可见"这一目标实际成立的原因。构建后端,但如果没有面向AI的表示层(= MCP),AI就无法查询它。MCP是使"将其交给AI"真正起作用的组件。
自愈的真正驱动力
使可观测性栈不仅仅是"一个供查看的屏幕"的层是自愈。我在AI Harness系列第四部分中介绍了全貌,这里不再赘述细节,但从可观测性角度来看,链条的起点和终点是清晰的:

流程:
因此,自愈的起点是可观测性栈能否以正确的形式将"出了什么问题"交给AI。如果错误未被识别 / 堆栈跟踪未被保留 / 相关代码(PR / 提交 / 图)不可达——任何一个缺失,链条就会中断。(具体的故障模式在下一节。)换句话说:
可观测性的质量是AI自主操作的上限。
这是第二部分的核心主张。将可观测性栈重新定义为"驱动AI的输入",而非"监控基础设施",你设计决策的优先级就会相应改变。
仍然存在的问题——定义"什么算作错误"以及堆栈跟踪设计
诚实地讲,这是最大的遗留问题。
你可以将可观测性栈打磨得完美无瑕,但如果什么算作错误的设计以及堆栈跟踪是否保留出了问题,那么所有努力都是白费的。我之前在AI Harness系列第二部分中,在cortex内部知识图谱的上下文中提到过这一点,它在可观测性侧也同样存在。
具体来说,以下是故障模式:
try ~ catch 吞没了错误而没有记录 → 没有任何内容到达可观测性栈console.log 的info级别 → 未被识别为错误error.message;堆栈跟踪被丢弃 → AI无法追溯到原始代码这些都是创建可观测性入口点的代码层面的问题,而非可观测性栈本身的问题。无论栈打磨得多好,如果入口点的水龙头坏了,什么都不会流出来。
目前已有的三层防护,没有一层是完备的:
no-silent-catch 规则阻止空的catch和 .catch(() => null) 风格的吞没。但只要catch内部有任何函数调用,lint就会满意——因此像"降级为 logger.info(err.message)"或"只记录 error.message 并丢弃堆栈跟踪"这样的模式会静态地溜过去serializeError(error) 将堆栈跟踪作为结构化字段存储"和"通过 logger.error(err.message) 丢弃 stack 是重大违规"这样的规则已写入内部指南。但静态检查无法强制执行这些规则;它们依赖于人工 / AI审查换句话说:"有指南,lint能捕获一些,AI审查能捕获一些,但并非万无一失" 是诚实的描述。真正的差距在于在编写新代码的时刻,没有一个工具能够主动建议/补全"这应该被视为错误,这应该保留其堆栈跟踪"。自动审查在PR时发现问题,但针对可观测性入口点设计本身的主动工具尚未构建。
"可观测性栈:已完成。可观测性目标设计:仍依赖于人类。"这就是诚实的现状。用一个工具来弥合这一差距是下一步的工作。
结语——静态版 + 动态版已就绪;合并它们是下一个系列
代码图谱系列是关于重塑静态分析图谱,使AI能够查询它——将代码的结构作为事实交给AI。这个由两部分组成的系列是关于将生产环境中正在发生的事情,同样作为事实交给AI。
| | 形态 | 交付的内容 |
|---|---|---|
| 静态版(code-graph + db-graph + annotation graph) | 3图谱并行 + SAME_ENTITY | 代码及其含义 |
| 动态版(第一部分 + 本文) | Prometheus / BQ / Loki + MCP | 生产行为及成本 |
诚实地说:这两者目前仍然并列存在,尚未合并。对于cortex宣称的"不要让AI推断——交给它事实"这一原则来说,要真正达到完成状态,下一步是将动态数据注入静态图谱并合并它们。这正是我在code-graph第二部分末尾标记为"缺少动态分析"的未解决问题:将"生产环境中这条边实际被使用的频率"放在静态图谱的节点上。那时,"作为事实交付"才能达到其最终形态。
在静态 + 动态之上叠加自愈层,你就得到了"AI自主操作",这在今天已经可行。但是将两个版本合并为一个图谱仍然在前方——那是下一个系列。
并且再次强调,可观测性目标设计(什么算作错误,堆栈跟踪是否保留)才是真正设定上限的因素。将其工具化是下一个待办事项。
感谢阅读至此。