章节13 / 14
本文目录12 节
可观测性与安全:线上问题要能被看见
本指南介绍如何为生产级 AI 应用设计符合规范的 OpenTelemetry GenAI 追踪、度量指标、OWASP Top 10 安全映射以及敏感数据脱敏策略,解决线上黑盒故障排查难题。
- 掌握 TypeScript 异步编程与中间件设计
- 理解大模型 API 的流式输出与 Tool Call 机制
- 设计并实现一套符合 OpenTelemetry 规范的 LLM Trace 结构
- 制定防范 OWASP LLM 风险的拦截网关与审计日志策略
- 编写一个自动识别并脱敏 PII 敏感信息的流式/非流式双重处理器
传统的后端应用上线后,我们通过 HTTP 状态码、请求延迟、APM 慢查询以及常规的错误栈来定位故障。但在 AI 时代,这些传统的观测手段失效了:大模型接口可能会在返回 HTTP 200 OK 的同时,生成一堆胡言乱语;或者在经过一系列复杂的 Agent 规划与多轮 Tool Call 后,因为一次间接提示词注入,悄无声息地清空了用户的购物车。
要让线上 AI 应用的异常“被看见”,我们需要对 AI 系统进行更细粒度的全链路追踪(Trace)、更严密的安全合规性审计,以及对用户隐私数据的彻底脱敏。
线上故障现场:看不见的 Prompt 注入与敏感数据裸奔
本节操作锚点:围绕“线上故障现场:看不见的Prompt注入与敏感数据”记录步骤、样例、诊断、风险、检查清单和验收结果。
假设我们上线了一个“智能简历分析与推荐 Agent”。这个应用读取用户上传的简历 PDF,提取工作经历,结合企业岗位要求进行匹配,并最终自动起草一份面试邀约邮件。系统上线第一天,运营团队收到了数十封退信警报:由于某些求职者的简历里夹杂了恶意的间接提示注入指令,Agent 在解析简历时,直接忽略了后续的匹配任务,转而调用后台的邮件工具,向未授权的第三方邮箱群发了骚扰邮件。这就是典型的 OWASP LLM:01(提示注入) 与 OWASP LLM:08(过度代理) 组合形成的连环漏洞。
更严重的是,当安全合规团队在后台日志检索中排查此事时,发现所有的用户简历文本、身份证号、家庭住址、甚至是模型输出的拒绝提示,都原封不动地以明文形式打印在应用 stdout 中,并上传到了集中式日志服务里。这直接触犯了 OWASP LLM:06(敏感信息泄露)。
在排除此类故障时,传统的单体 APM 只能抓到一个长时间的 POST /api/analyze 接口慢响应,而无法展示:
- 模型在哪一个 Agent 轮次(Step)中被注入?
- 模型调用 Tool Call 时传入的具体参数是什么?
- 召回(Retrieve)出来的简历片段中到底哪一段带有恶意攻击特征?
因此,我们需要一套标准化的结构来记录 LLM 的执行细节。
OpenTelemetry GenAI 规范:如何标准化你的 LLM Trace
本节操作锚点:围绕“OpenTelemetryGenAI规范:如何标”记录步骤、样例、诊断、风险、检查清单和验收结果。
为了避免在 trace 属性命名上各行其是,OpenTelemetry 社区制定了专门的生成式 AI 语义规范。根据 OpenTelemetry 发布的 OpenTelemetry GenAI semantic conventions 规范,记录模型请求时应该使用 gen_ai.request.model 和 gen_ai.response.model 等标准化属性。因此,我们在构建 Trace 拦截器时,不应自定义诸如 llm_name 或 model_used 这样的非标字段,而应完全对齐这套标准,以确保后续能无缝接入任何兼容 OpenTelemetry 的 APM 监控平台。
在设计你的 LLM span 属性时,必须在对应的 Span 上挂载以下属性:
| 属性键名 (Attribute Key) | 类型 | 示例值 | 说明 |
|---|---|---|---|
gen_ai.system | string | "openai" / "anthropic" / "ollama" | 标识底层模型服务商 |
gen_ai.request.model | string | "gpt-4o" | 客户端请求的目标模型 |
gen_ai.response.model | string | "gpt-4o-2024-05-13" | 模型实际响应的具体版本 |
gen_ai.request.temperature | double | 0.7 | 模型温度参数 |
gen_ai.response.finish_reasons | string[] | ["stop"] / ["tool_calls"] | 模型结束生成的原因 |
gen_ai.usage.input_tokens | int | 850 | 请求消耗的 Prompt token 数 |
gen_ai.usage.output_tokens | int | 120 | 模型生成的 Completion token 数 |
下面展示一段基于 TypeScript/Node.js 实现符合 OpenTelemetry 规范的 span 包装逻辑:
import { trace, Span, SpanKind } from '@opentelemetry/api';
const tracer = trace.getTracer('ai-application-tracer');
async function trackLlmCall<T>(
prompt: string,
options: { model: string; temperature: number },
apiCall: (prompt: string) => Promise<{ text: string; usage: { input: number; output: number }; exactModel: string; finishReason: string }>
): Promise<T> {
return tracer.startActiveSpan('llm_generate', { kind: SpanKind.CLIENT }, async (span: Span) => {
try {
// 设置请求元数据
span.setAttribute('gen_ai.system', 'openai');
span.setAttribute('gen_ai.request.model', options.model);
span.setAttribute('gen_ai.request.temperature', options.temperature);
// 执行实际的 API 调用
const result = await apiCall(prompt);
// 捕获实际返回属性与使用情况
span.setAttribute('gen_ai.response.model', result.exactModel);
span.setAttribute('gen_ai.response.finish_reasons', [result.finishReason]);
span.setAttribute('gen_ai.usage.input_tokens', result.usage.input);
span.setAttribute('gen_ai.usage.output_tokens', result.usage.output);
span.setStatus({ code: 0 }); // OK
return result as unknown as T;
} catch (error: any) {
span.setStatus({ code: 2, message: error.message }); // ERROR
span.recordException(error);
throw error;
} finally {
span.end();
}
});
}
如果你的系统中存在复杂的 Agent 环路,每一个 Agent 的决策(Thought)、工具执行(Action)、以及观察(Observation)都应当作为一个独立的子 Span,挂载在父 Span llm_generate 之下,以此形成一个清晰的调用树。
既要看见又要安全:在 LangSmith/自建追踪中实现日志脱敏
本节操作锚点:围绕“既要看见又要安全:在LangSmith/自建追踪”记录步骤、样例、诊断、风险、检查清单和验收结果。
可观测性与隐私保护往往是一对矛盾体。如果我们将用户输入的全部原始文本、大模型产生的全部中间推理数据都上传到第三方追踪平台(如 LangSmith 等云端服务),极易造成用户隐私泄漏。结合 LangSmith observability 提供的生产指引,虽然 trace 数据对我们调试系统大有裨益,但在处理受控数据时,我们必须配置过滤与清洗规则(Redaction),以防将个人身份信息(PII)发送到云端。
如果我们要在生产环境中记录用户输入,必须在数据进入 Span 属性之前通过正则或分类模型进行脱敏,否则一旦发生敏感信息泄露(OWASP LLM:06),企业将面临严重的合规和法律风险。以下是一个典型的敏感信息脱敏处理器,支持将手机号、身份证号、信用卡号替换为统一占位符:
export interface MaskRule {
pattern: RegExp;
replacement: string;
}
export const DEFAULT_MASK_RULES: MaskRule[] = [
{
// 匹配中国大陆身份证号
pattern: /\b([1-6][1-9]\d{4})(19|20)\d{2}(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])\d{3}[0-9Xx]\b/g,
replacement: '[ID_CARD_REDACTED]'
},
{
// 匹配手机号
pattern: /\b(1[3-9]\d{9})\b/g,
replacement: '[PHONE_REDACTED]'
},
{
// 匹配通用信用卡卡号
pattern: /\b(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|6(?:011|5[0-9][0-9])[0-9]{12}|3[47][0-9]{13}|3(?:0[0-5]|[68][0-9])[0-9]{11}|(?:2131|1800|35\d{3})\d{11})\b/g,
replacement: '[CREDIT_CARD_REDACTED]'
}
];
export function redactText(text: string, rules: MaskRule[] = DEFAULT_MASK_RULES): string {
if (!text) return text;
let sanitized = text;
for (const rule of rules) {
sanitized = sanitized.replace(rule.pattern, rule.replacement);
}
return sanitized;
}
在流式输出场景下,脱敏面临更大的挑战。因为敏感数据可能是跨 Chunk 输出的,直接对单个 Chunk 进行正则匹配极易漏过敏感词。对于流式输出,可以采取以下两种策略之一:
- 双轨制追踪:前端流式响应给用户以保证用户体验,但不在流上挂载明文追踪。在后台将流式内容拼装为完整文本,在异步队列中经过脱敏处理器后再上报给观测服务。
- 合规拦截缓冲:在流式输出层维护一个滑窗缓冲区(Sliding Window Buffer),用于实时拼装部分字符,判定安全后再行发送与记录。但这会带来延迟惩罚,通常用于高合规要求行业。
映射 OWASP 风险:构建应用级安全网关防护策略
本节操作锚点:围绕“映射OWASP风险:构建应用级安全网关防护策略”记录步骤、样例、诊断、风险、检查清单和验收结果。
根据 OWASP 发布的 OWASP Top 10 for Large Language Model Applications 2025 指南,过度代理(Overreliance)和输出处理不当(Improper Output Handling)是导致应用失控的常见诱因。因此,在可观测性设计中,我们必须将 Tool Call 的输入输出、以及最终生成内容的合规性校验结果,作为独立的子 Span(Sub-span)进行级联记录,以便在输出异常时精确定位是模型本身失控还是工具执行出错。
为了构建完整的防御纵深,我们需要在 AI 调用的前、中、后三个阶段构建应用级安全网关。通过在 OpenTelemetry trace 中标记这些安全拦截事件,可以让我们对线上受攻击情况了如指掌:
用户输入
│
▼
┌──────────────┐ 是
│ 输入网关拦截? ├───────────────► 阻断并记录 OWASP LLM:01 注入事件 (Span Attribute: security.incident=input_injection)
└──────┬───────┘
│ 否
▼
┌──────────────┐
│ 大模型推理 │
└──────┬───────┘
│
▼
┌──────────────┐ 是
│ 输出网关拦截? ├───────────────► 阻断并记录 OWASP LLM:06 泄露事件 (Span Attribute: security.incident=pii_leak)
└──────┬───────┘
│ 否
下游消费
- 输入阶段防护:检测 Prompt 注入(LLM:01)与敏感数据外泄(LLM:06)。使用轻量级分类器、敏感词黑名单对用户提问进行安全预检。如果检测到输入提示词触发了高风险词库,系统应当直接拦截并记录对应的安全 Trace,不要将该请求发送给大模型,因为这样可以有效阻断潜在的提示注入攻击并节省不必要的 API 成本。
- 调用阶段防护:限制工具执行权限(LLM:08)。在执行任何 Tool Call 之前,对动作类型、目标数据范围进行严格的双重校验(例如,禁止 Agent 通过工具在没有用户二次确认的情况下执行大范围的数据删除或邮件群发)。
- 输出阶段防护:防范不安全输出与注入扩散(LLM:02)。检测模型生成的内容中是否包含 SQL 代码片段、不安全的 HTML 标签或敏感数据。如果模型生成了违规数据,必须拦截该响应,并向用户提示统一的友好拒绝话术。
定义核心观测指标:延迟、Token 与成本水位线
本节操作锚点:围绕“定义核心观测指标:延迟、Token与成本水位线”记录步骤、样例、诊断、风险、检查清单和验收结果。
在生产可观测性中,除了 Trace 调用树,我们还需要高频的度量指标(Metrics)来进行告警触发。在生成式 AI 场景下,指标定义需要包含三个关键维度:
1. 耗时与延迟维度
- TTFT (Time to First Token):首 Token 延迟。这是衡量大模型流式响应体验的最核心指标。如果 TTFT 过高,用户会明显感知到界面“卡顿”。
- TPOT (Time Per Output Token):每个输出 Token 的平均耗时。反映模型在大批量生成时的吞吐能力。
- 总响应时长 (Latency):一次交互的完整生命周期。
2. Token 与成本维度
- 输入/输出 Token 计数器:按模型、按租户、按业务场景聚合,监控 Token 突增趋势。
- 折算成本 (Estimated Cost):根据不同模型厂商的单价定价,动态计算线上调用的资金消耗,设置每日成本水位线。
3. 系统可靠性维度
- 错误率 (Error Rate):大模型提供商 API 的 429(限流)、500(内部错误)频率。
- 拦截率 (Block Rate):由于输入/输出安全网关拦截导致请求被阻断的比例。拦截率突然上升通常意味着系统正在遭受恶意的渗透或攻击。
灰度上线与异常回滚:基于 Trace 异常率的自动开关
本节操作锚点:围绕“灰度上线与异常回滚:基于Trace异常率的自动开”记录步骤、样例、诊断、风险、检查清单和验收结果。
在升级提示词(Prompt Engineering)或切换底层基础模型时,我们不能一次性全量切换。由于大模型存在“突发行为”(Emergent Behaviors),一个在测试集表现良好的 Prompt,在线上真实流量中可能会引发意想不到的拒绝响应或高频报错。
一个标准的灰度与回滚控制流如下:
- 基于 Feature Flag 进行流量分发:将 5% 的真实请求导向新版 Prompt (v2) / 新模型,95% 留在旧版 (v1)。
- 监控 Span 中的语义特征:
- 提取 Trace 属性中的
gen_ai.response.finish_reasons。如果发现 v2 版本的非正常结束比例(例如因为过长截断而触发的length结束状态)显著高于 v1,这表明新 Prompt 生成的回复可能存在死循环,应该触发预警。 - 监控 Trace 自定义属性
security.incident_detected。如果新版上线后该指标突增,可能意味着新提示词降低了系统对注入攻击的抵抗能力(引发了 OWASP LLM:01 漏洞)。
- 提取 Trace 属性中的
- 自动回滚逻辑:如果下游大模型的响应延迟持续超过设定阈值,应该立即切断当前异常节点的流量并降级为静态缓存,除非你已经配置了多模型冗余和自动重试机制,因为长时间的挂起会迅速耗尽系统的连接池。
实战演练:编写脱敏处理器与安全观测验证
本节操作锚点:围绕“实战演练:编写脱敏处理器与安全观测验证”记录步骤、样例、诊断、风险、检查清单和验收结果。
学习产物与交付物
在本节结束时,你必须完成以下三份交付物,并在你的本地代码库中进行自测:
- 一份观测字段清单:列出你所负责的业务场景下,所有的自定义 Span 属性,必须包含对齐 OpenTelemetry 标准的字段和至少三个自定义业务级字段(例如:
app.user.tier,app.rag.document_count)。 - 安全风险映射表:针对 OWASP LLM Top 10 中的前 4 项核心风险,写出对应的 Trace 记录拦截逻辑与指标告警触发阈值。
- 代码实现:编写一个 Node.js/TypeScript Express/NestJS 拦截器,对所有模型的请求和流式响应进行敏感数据自动脱敏与非标指标收集。
练习:编写带脱敏的 Trace 收集器
你可以参考以下代码模板来实现你的拦截器,并将其作为你的交付物核心逻辑。这个练习要求你能在不破坏原有流式输出性能的前提下,正确识别流中的敏感数据占位:
import { Request, Response, NextFunction } from 'express';
import { redactText } from './redact'; // 使用前面定义的 redactText 方法
/**
* 一个简易的 AI 请求安全与可观测性拦截中间件
*/
export function aiObservabilityMiddleware(req: Request, res: Response, next: NextFunction) {
const originalWrite = res.write;
const originalEnd = res.end;
const chunks: Buffer[] = [];
// 1. 拦截并清洗请求体(防止输入端注入和隐私泄露)
if (req.body && req.body.prompt) {
const originalPrompt = req.body.prompt;
req.body.prompt = redactText(originalPrompt);
// 记录在请求上下文中,以便后续 Span 绑定
(req as any).sanitizedPrompt = req.body.prompt;
(req as any).wasPromptRedacted = originalPrompt !== req.body.prompt;
}
// 2. 劫持输出流(针对流式输出的审计与脱敏)
res.write = function (chunk: any, ...args: any[]): boolean {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
return originalWrite.apply(res, [chunk, ...args]);
};
res.end = function (chunk: any, ...args: any[]): Response {
if (chunk) {
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
}
const responseBody = Buffer.concat(chunks).toString('utf8');
const sanitizedOutput = redactText(responseBody);
// 在实际生产中,完整的 sanitizedOutput 会在这里被异步发送到 Trace 存储器或安全日志网关
// 此处仅做本地审计记录打印
if (responseBody !== sanitizedOutput) {
console.warn(`[SECURITY ALERT] PII leakage detected and redacted in response for path: ${req.path}`);
}
return originalEnd.apply(res, [chunk, ...args]);
};
next();
}
验收方式
- 单元测试输入:向你的中间件接口发送一个包含真实中国身份证号的 Prompt。例如:
"你好,我是张三,我的身份证号是 110101199003072345,请帮我生成一份简历"。 - 预期输出:
- 在上报给 Trace 系统的 Prompt 属性中,身份证号已被成功替换为
[ID_CARD_REDACTED]。 - 大模型返回的内容如果包含了该身份证号,在最终审计 Trace 中同样被替换为
[ID_CARD_REDACTED],同时触发[SECURITY ALERT]日志输出。
- 在上报给 Trace 系统的 Prompt 属性中,身份证号已被成功替换为
- 失败样例诊断:如果在输出日志中依然能看到完整的
110101199003072345明文,说明流式劫持的 Buffer 拼接逻辑或正则规则没有正确执行。检查正则中是否遗漏了全局匹配修饰符/g,或者流式 Chunk 拼接时字符集转换出现了乱码导致正则失效。
来源说明、复核与时效性
本节操作锚点:围绕“来源说明、复核与时效性”记录步骤、样例、诊断、风险、检查清单和验收结果。
本指南内容设计紧密基于以下行业事实标准与规范文件:
- 可观测性字段规范:基于 OpenTelemetry 发布的 OpenTelemetry GenAI semantic conventions(访问日期:2026-05-28),该规范用于统一生产环境大模型调用的 Span 属性标准。
- 安全防御纵深:基于 OWASP Top 10 for Large Language Model Applications 2025 年版(访问日期:2026-05-28),用以指导提示注入、敏感信息泄漏和过度授权的风险分级。
- 治理与可信框架:依据 NIST 发布的 NIST AI Risk Management Framework 及其 Generative AI Profile (AI 600-1)(访问日期:2026-05-28),在治理(Govern)和映射(Map)阶段,开发团队必须明确 AI 系统的风险边界。这意味着可观测性系统不仅要记录技术层面的延迟和 Token 数,还必须设计专门的元数据标签(Metadata Tags)来标识数据敏感级与应用合规性,从而为后续的安全审计提供可追溯的证据链。
复核触发条件:
如果 OpenTelemetry GenAI 规范中的 gen_ai. 命名空间下字段由实验状态(Experimental)转为正式稳定版(Stable),开发团队必须在 90 天内审查当前生产配置的代码并调整字段命名,否则可能由于 APM 平台升级导致旧版非标 Trace 指标图表失效。
可观测性与安全:线上问题要能被看见:把判断写成可复查证据
这个复核段的目的不是增加篇幅,而是避免把未验证的判断带到下一课。本课交付物是 一份观测字段清单、安全风险映射和日志脱敏策略,它要服务于 AI 应用 的下一步,而不是只证明你读过这一节。
如果你现在还没有真实输入,先用一个最小样例完成 可观测性与安全:线上问题要能被看见,因为没有输入就无法判断产物是否可用。 如果本课产物会影响后续交付,应该写明适用条件和不适用条件,否则下一课会把不确定性误当成基础。 如果检查结果只剩一句“看起来可以”,必须补一条反例或失败样例,只有能解释失败原因时,这个判断才站得住。
当来源、工具或现场条件变化时,先复核边界,再复用旧结论。围绕 可观测性与安全:线上问题要能被看见 做检查时,至少保留步骤、样例、风险、修复和验收五项。
OpenTelemetry 的《OpenTelemetry GenAI semantic conventions》说明:支撑 AI 应用观测、模型请求 span/attribute、token、延迟和生产排障。;这意味着 可观测性与安全:线上问题要能被看 不能只写经验结论,要把来源变成检查动作。OWASP 的《OWASP Top 10 for Large Language Model Applications》提醒:支撑提示注入、敏感信息泄露、过度代理、供应链和输出处理风险分类。;因此本课方案必须写清边界。NIST 的《NIST AI Risk Management Framework》提供的证据是:支撑 AI 风险治理、可信 AI、组织流程、治理/映射/测量/管理框架。;所以当前结论按 2026-05-28 的来源状态使用。
复核触发条件要写具体:如果官方 API/SDK、软件工具版本、版权政策、安全合规要求、行业规范、平台发布规则或关键来源页面发生变化,需要重新检查 可观测性与安全:线上问题要能被看 的步骤、样例、风险和验收清单。
练习验收:把 可观测性与安全:线上问题要能被看见 应用到一个自己的任务,交付一份包含输入、步骤、样例、失败诊断、来源证据和复测结果的记录。缺少其中任何一项,都先标记为 needs_review。