LLM系统提示词泄露风险与防护体系构建指南 1. 项目概述这不是“泄露”而是系统提示词的意外暴露与风险显形最近在多个技术社区和开发者群组里“system_prompts_leaks”这个短语突然高频出现不是作为某个新工具的名字也不是某次黑客攻击的代号而是一类真实发生、反复复现、却长期被低估的工程现象——系统提示词system prompt在生产环境中非预期暴露。它不涉及传统意义上的数据 breach 或 credential theft但其影响范围之广、后果之隐蔽、修复之困难远超多数工程师最初的判断。我过去三年深度参与过 7 个面向终端用户的 LLM 应用交付项目其中 4 个在上线后 3–6 个月内都遭遇过不同程度的 system prompt 暴露事件轻则导致模型行为漂移、用户质疑“为什么今天回答变傻了”重则引发合规审计问询、客户合同条款触发、甚至品牌信任度断崖式下滑。所谓“leaks”本质是提示工程prompt engineering从开发侧走向生产侧时缺乏配套的边界防护、版本管控与上下文隔离机制所必然暴露的系统性短板。它不是 bug而是 design gap不是偶然而是默认配置下的大概率事件。适合阅读本文的不是只写过几行 chat-completion 调用的新手而是已经把模型接入业务流程、开始关注稳定性与可控性的中高级开发者、AI 产品经理、以及负责 AI 系统治理的技术负责人。你不需要懂大模型训练原理但必须清楚自己部署的 API 接口、前端 SDK、中间件代理层到底在什么环节、以什么形式、向谁传递了哪些原始指令。这个现象之所以在近期集中爆发并非因为攻击手段升级而是因为应用形态发生了质变越来越多产品不再满足于“调用一次 API 得到一个回答”而是构建多轮对话、状态保持、角色切换、权限分级的复杂交互链路。当 system prompt 不再是静态字符串而成为动态拼接、条件注入、用户偏好融合、甚至实时策略干预的运行时变量时它的生命周期就从 IDE 里的一个常量延伸到了 Nginx 配置、React 组件 props、FastAPI 路由参数、Redis 缓存键值、甚至浏览器 DevTools 的 network tab 里。而绝大多数团队在设计之初根本没给这段文本分配独立的“安全域”。它像一段没有门禁的走廊连接着模型大脑与外部世界任何一次调试日志打印、一次错误堆栈输出、一次前端 console.log、一次未过滤的 HTTP 响应头都可能成为它的出口。我见过最典型的案例是一家教育 SaaS 公司他们的 system prompt 里明确写着“你是一名持有国家二级心理咨询师证书的 AI 辅导员请严格遵循《未成年人保护法》第 72 条……”结果某次前端异常捕获逻辑把完整请求体含 system prompt上报到了 Sentry而 Sentry 的错误详情页对所有协作者开放——不到 48 小时竞品公司就在其官网 FAQ 中更新了一条“我们的 AI 辅导员同样具备专业资质认证且严格遵守相关法规”措辞几乎一字不差。这不是巧合是 system prompt 作为核心业务规则载体其价值早已超越技术实现成为可被直接观测、分析、模仿的竞争资产。2. 核心设计思路拆解为什么“隐藏提示词”不是加个 if-else 就能解决的问题很多人第一反应是“那我把 system prompt 放进环境变量前端绝对拿不到不就安全了”——这恰恰是踩坑的第一步。system_prompts_leaks 的根源从来不在“存储位置”而在“流转路径”与“作用域边界”的彻底缺失。我们来拆解一个典型现代 LLM 应用的请求链路用户在 React App 中点击发送按钮 → 前端构造 {messages: [...], model: gpt-4-turbo} → 发送 POST 到 /api/chat → 后端 FastAPI 服务接收 → 读取数据库获取该用户所属角色学生/教师/管理员→ 动态拼接 system prompt例如f你是一名{role}请用{language}回答禁止提及内部系统名称...→ 调用 OpenAI SDK → SDK 将完整 payload含 system prompt序列化为 JSON → 经过公司自建的 API 网关做限流、鉴权→ 最终抵达 LLM 提供商。在这个链条里system prompt 至少在 5 个不同环节存在暴露风险① 前端构造时若误将 prompt 放入 messages 数组而非单独字段会被用户直接看到② 后端日志若开启 DEBUG 级别并记录 request.body会完整落盘③ API 网关若配置了全量请求体审计日志且日志系统权限宽松运维/安全人员均可查阅④ LLM 提供商返回的 usage 字段虽不含 prompt但某些错误响应如 400 Bad Request会返回原始 payload 片段用于调试⑤ 更隐蔽的是当使用 streaming 响应时部分前端库如 react-chatbot-kit会将 initial system message 作为 conversation history 的一部分缓存若缓存未加密或被 XSS 攻击窃取prompt 即失守。因此真正的防护不是“藏起来”而是建立一套分层拦截、按需加载、最小暴露的提示词治理体系。这套体系的核心设计原则有三条缺一不可第一物理隔离原则——system prompt 的定义、管理、加载必须与业务逻辑代码分离。我坚持使用 YAML 文件而非 Python dict定义所有角色模板文件存放在独立的 /config/prompts/ 目录下该目录被 gitignore且 CI/CD 流水线在构建镜像时仅将编译后的 prompt hashSHA256注入容器环境变量而非原始文本。这样即使攻击者拿到服务器 shell也看不到明文 prompt只能看到一串哈希值。第二运行时裁剪原则——任何环节都不应传递完整 prompt。后端服务在拼接完最终 prompt 后立即执行一次“敏感词脱敏”操作遍历所有预设关键词如“internal_api_key”、“admin_only_feature”、“debug_mode_enabled”将其替换为占位符REDACTED此操作在内存中完成不修改原始配置。第三作用域绑定原则——prompt 必须与具体请求强绑定禁止全局单例。我们曾用过一个全局 PromptManager 类所有请求共享同一实例结果某次热更新配置时新旧 prompt 混合导致部分用户收到错误角色设定。现在改为每个请求生成独立的 PromptContext 对象其生命周期严格限定在本次 HTTP 请求内结束后自动销毁。这三个原则共同构成了一道纵深防御网即使某一层被突破比如日志泄露攻击者拿到的也只是脱敏后的片段且无法跨请求关联更无法反推原始模板结构。这不是过度设计而是当你的 system prompt 开始承载业务规则、合规要求、甚至法律声明时它本质上已等同于一份可执行的合同条款其严肃性必须匹配。3. 关键环节实操解析从定义、加载到注入每一步都藏着“泄漏点”真正决定 system_prompts_leaks 是否发生的不是宏观架构而是具体代码行中的微小决策。下面我以一个真实电商客服助手项目为例逐环节拆解关键操作与避坑要点。该项目要求 system prompt 根据用户等级普通/ VIP/ 黑金动态调整服务策略例如黑金用户可获得“优先人工转接通道”提示VIP 用户有“专属优惠券发放权限”普通用户则无。整个流程分为四个核心环节配置定义、运行时加载、上下文注入、错误处理。3.1 配置定义YAML 模板 Jinja2 变量注入拒绝硬编码我们放弃在 Python 代码里写SYSTEM_PROMPT You are a customer service agent...这种方式改用/config/prompts/service_agent.yaml# /config/prompts/service_agent.yaml templates: - role: standard content: | 你是一名{{ company_name }}的在线客服助手。请用{{ language }}回答保持礼貌专业。 当用户提出退款申请时请引导其提供订单号并说明处理时效为3-5个工作日。 {{ disclaimer }} - role: vip content: | 你是一名{{ company_name }}的VIP客户服务经理。请用{{ language }}回答语气亲切自信。 当用户提出退款申请时请立即确认订单状态并承诺2小时内给出处理方案。 {{ disclaimer }} 【VIP特权】您可随时要求转接至人工坐席我们将优先为您安排。 - role: black_gold content: | 你是一名{{ company_name }}黑金会员专属服务顾问。请用{{ language }}回答体现尊贵与效率。 当用户提出退款申请时请直接调取订单物流信息并同步启动加急审核流程。 {{ disclaimer }} 【黑金特权】您的退款将在24小时内到账无需等待审批。关键点在于① 使用{{ variable }}占位符而非 Python f-string确保模板本身不包含任何运行时逻辑②disclaimer是一个独立的、带版本号的法律声明片段存放在/config/prompts/disclaimers/v2024_q3.yaml通过 include 机制引入避免重复维护③ 所有模板内容使用|符号保留换行保证格式可读性。这种设计让 prompt 成为可版本控制、可 A/B 测试、可审计的独立资产。我在一次客户审计中仅凭 Git 历史就能清晰展示v1.2 版本增加了 GDPR 数据删除条款v1.3 版本移除了“永久保存聊天记录”的表述——这比解释一堆代码逻辑有力得多。3.2 运行时加载基于请求上下文的精准匹配与内存安全加载加载逻辑封装在PromptLoader类中其核心方法load_for_user(user_id: str, user_role: str) - strfrom jinja2 import Environment, FileSystemLoader import hashlib import os class PromptLoader: def __init__(self): # 指定模板目录不加载任何实际内容 self.env Environment(loaderFileSystemLoader(/config/prompts)) # 从环境变量读取密钥用于后续 HMAC 验证 self.secret_key os.getenv(PROMPT_SECRET_KEY, dev_default) def load_for_user(self, user_id: str, user_role: str) - str: # 1. 从 YAML 加载模板仅一次缓存到内存 template_data self._load_yaml_template() # 2. 根据 user_role 查找对应模板 target_template next((t for t in template_data[templates] if t[role] user_role), None) if not target_template: raise ValueError(fNo template found for role: {user_role}) # 3. 渲染 Jinja2 模板注入 runtime 变量 template self.env.from_string(target_template[content]) rendered template.render( company_name星辰电商, language中文, disclaimerself._load_disclaimer() ) # 4. 执行敏感词脱敏关键防护步骤 sanitized self._redact_sensitive_content(rendered) # 5. 计算本次渲染的唯一指纹用于审计追踪 fingerprint hashlib.sha256( f{user_id}_{user_role}_{sanitized}.encode() ).hexdigest()[:16] return sanitized, fingerprint这里的关键防护点是第 4 步_redact_sensitive_content()。它不是简单 replace而是基于正则表达式匹配预设的敏感模式def _redact_sensitive_content(self, text: str) - str: # 匹配形如 INTERNAL_API_URL: https://internal-api.company.com 的行 text re.sub(r(INTERNAL_API_URL|DB_CONNECTION_STRING|ADMIN_TOKEN):\s*https?://[^\s], r\1: REDACTED, text) # 匹配形如 DEBUG_MODE: true 的开关 text re.sub(r(DEBUG_MODE|DEV_FEATURE_FLAG):\s*(true|false), r\1: REDACTED, text) # 匹配形如 Version: 2.3.1 的版本号防止泄露内部迭代节奏 text re.sub(rVersion:\s*\d\.\d\.\d, Version: REDACTED, text) return text这个函数在每次渲染后立即执行确保无论后续哪个环节出错日志或响应中都不会出现原始敏感信息。我曾在线上环境发现一个严重问题某次部署后一位测试工程师在本地调试时将DEBUG_MODE: true写进了 YAML 模板结果所有用户收到的 prompt 都包含了这行。由于脱敏规则存在线上日志里只看到DEBUG_MODE: REDACTED但前端开发者误以为这是正常占位符未及时上报。直到一周后一位黑金用户投诉“AI 总说正在 debug 模式”我们才意识到问题。这反而证明了脱敏的价值——它把一个高危配置错误降级为一个低优先级的文案问题避免了更大范围的业务影响。3.3 上下文注入API 层的“零信任”封装与字段校验LLM API 调用不再是简单的client.chat.completions.create(...)而是经过严格封装的SafeChatServiceclass SafeChatService: def create_chat_completion(self, messages: List[Dict], user_id: str, user_role: str, model: str gpt-4-turbo) - Dict: # 1. 严格校验 messages 结构禁止 system message 出现在 messages 数组中 for msg in messages: if msg.get(role) system: raise ValueError(Direct system message injection is forbidden) # 2. 加载并脱敏 prompt prompt, fingerprint PromptLoader().load_for_user(user_id, user_role) # 3. 构造符合 OpenAI 规范的 payload payload { model: model, messages: [ {role: system, content: prompt}, # 唯一合法的 system 位置 *messages # 用户历史消息 ], temperature: 0.7, max_tokens: 1024 } # 4. 添加审计头非敏感仅用于追踪 headers { X-Prompt-Fingerprint: fingerprint, X-Request-ID: generate_request_id() } # 5. 调用底层 SDK捕获所有异常 try: response client.chat.completions.create(**payload) return self._process_response(response, fingerprint) except Exception as e: # 关键错误处理中绝不记录原始 payload logger.error(fLLM call failed for user {user_id}, fingerprint {fingerprint}: {str(e)}) raise这个封装强制执行了两条铁律①禁止前端传入 system role——所有 system 指令必须由后端生成前端只能传 user 和 assistant 消息②payload 构造与日志分离——日志中只记录 fingerprint 和错误类型绝不记录prompt或messages内容。我们在一次压力测试中发现当并发超过 2000 QPS 时OpenAI 的 429 错误响应体里会包含部分原始请求字段用于调试。由于我们从未在日志中记录这些字段即使响应体被截获攻击者也无法还原出完整的 prompt 结构。这种“零信任”设计让 system prompt 的暴露面从“整个请求体”缩小到“仅 API 调用层内部的一个内存变量”大大提升了防护基线。3.4 错误处理流式响应与异常场景下的“静默兜底”Streaming 响应是 system_prompts_leaks 的高发区。当使用streamTrue时OpenAI 返回的是一个 Server-Sent Events (SSE) 流前端需逐块解析。常见错误是前端库将初始的 system message 作为第一条消息推送给 UI导致用户在聊天窗口第一行就看到“你是一名客服助手……”。我们的解决方案是在后端做流式响应的首帧过滤。def stream_chat_completion(self, messages: List[Dict], user_id: str, user_role: str): prompt, fingerprint PromptLoader().load_for_user(user_id, user_role) # 构造 payload同上 payload {...} # 使用 requests.stream 发起底层调用 with requests.post( https://api.openai.com/v1/chat/completions, headers{Authorization: fBearer {api_key}}, jsonpayload, streamTrue ) as r: for line in r.iter_lines(): if line: # 解析 SSE 格式data: {json} if line.startswith(bdata: ): data line[6:] try: chunk json.loads(data.decode()) # 过滤掉包含 system prompt 的初始化 chunk通常为空或仅含 usage if choices in chunk and chunk[choices]: delta chunk[choices][0].get(delta, {}) if content in delta and delta[content]: # 只转发有实际内容的 chunk yield json.dumps(chunk).encode() b\n except json.JSONDecodeError: continue同时针对所有异常场景网络超时、模型不可用、token 超限我们定义了统一的 fallback promptFALLBACK_PROMPT ( 当前服务暂时繁忙请稍后再试。您的问题已记录我们的工程师正在紧急处理。 感谢您的耐心与理解。 )这个 fallback prompt 是硬编码在代码里的且长度严格控制在 50 字以内不包含任何业务规则、不引用任何内部系统名、不承诺任何 SLA。它存在的唯一目的就是在所有防护机制失效时提供一个绝对安全的“空白画布”确保用户永远看不到原始 system prompt 的任何痕迹。我在一次灰度发布中故意注入了一个会导致 500 错误的配置结果监控显示 fallback prompt 的触发率高达 92%而所有错误日志中均未出现任何 prompt 片段——这证明了兜底机制的有效性。4. 实操过程全记录一次真实的泄漏事件复盘与加固全流程去年 Q4我们为一家金融客户上线了智能投顾助手系统平稳运行 42 天后安全团队在例行日志审计中发现异常Sentry 错误日志里有 3 条记录的request.body字段包含了完整的 system prompt 片段内容为“你是一名持牌基金销售顾问请严格遵守《证券投资基金销售管理办法》第 38 条……”。这并非外部攻击而是内部开发流程的疏漏。下面是我主导的 72 小时应急响应与加固全过程所有步骤均已在生产环境验证。4.1 定位泄漏源头从日志线索逆向追踪第一步锁定日志来源。Sentry 的错误详情页显示这 3 条日志均来自同一个前端页面/advisor/chat且 User Agent 均为Chrome/120.0.0.0IP 地址集中在公司办公网段。这强烈暗示是内部调试行为。我们立即检查该页面的前端代码发现一个被注释掉的调试函数// TODO: remove before prod —— 这行注释被忽略了 function debugLogFullRequest(payload) { console.log(FULL REQUEST PAYLOAD:, payload); // ⚠️ 危险 // ... 其他逻辑 }该函数在用户点击“开始咨询”按钮时被调用而payload对象正是前端构造的、准备发送给后端的完整请求体其中system_prompt字段被错误地加入到了payload里本应由后端生成。更糟的是这个console.log在生产环境并未被移除因为团队使用了 Webpack 的DefinePlugin将process.env.NODE_ENV设为production但忘记配置console.*的 tree-shaking导致日志依然输出。而 Sentry 的beforeSend钩子恰好将console.log的输出捕获并上传——这就是泄漏的完整链条前端调试代码 → 生产环境未清理 → Sentry 全量上报 → 日志系统开放查询。4.2 紧急止血三分钟内完成的线上热修复定位后我们没有选择停服而是实施了“热修复三步法”前端紧急 patch通过 CDN 快速推送一个 JS 补丁覆盖原页面的debugLogFullRequest函数使其变为function debugLogFullRequest() {}空函数。CDN 缓存刷新时间 2 分钟所有用户在下次页面加载时即生效。后端熔断加固在 API 网关层添加一条规则对所有来自/advisor/chat的 POST 请求若request.body中包含system_prompt字段则立即返回 400 错误并记录告警。这条规则 5 分钟内上线彻底阻断任何前端误传 system prompt 的可能性。日志权限收紧联系运维将 Sentry 中request.body字段的访问权限从“所有协作者”降级为“仅安全团队后端负责人”并启用字段级脱敏对所有匹配system_prompt的 key 自动替换为REDACTED。整个过程耗时 17 分钟期间服务零中断用户无感知。这得益于我们前期建立的快速响应机制CDN 补丁模板、网关规则模板、权限变更 SOP 均已预置只需填入参数即可执行。4.3 深度加固从流程到工具的系统性改进止血只是开始真正的加固在后续 72 小时内展开流程层面修订《前端代码上线 checklist》新增强制项“检查所有console.log、debugger语句确认其已被移除或包裹在if (process.env.NODE_ENV development)中”。并引入 pre-commit hook使用eslint-plugin-no-console插件在 git commit 时自动扫描并阻止包含console.log的提交。工具层面开发了一个内部 CLI 工具prompt-scan集成到 CI/CD 流水线中。它会扫描所有前端代码仓库查找以下模式system_prompt、sys_prompt、role: system等关键词JSON.stringify( 包含messages数组的变量任何fetch/axios调用中body参数包含system字段的代码行 工具发现即 fail build并附带精确的文件路径与行号。上线首周该工具拦截了 12 处潜在风险其中 3 处是新入职工程师在 demo 代码中留下的调试痕迹。监控层面在 Prometheus 中新增指标llm_prompt_exposure_count通过日志采集器Filebeat实时统计所有日志中匹配system_prompt、role.*system等正则的行数。设置告警阈值为 0一旦触发立即通知值班工程师。同时将该指标与sentry_error_rate关联形成“异常日志 错误率”双因子告警避免误报。文化层面组织了一次全员 workshop主题为“你的 console.log 可能正在泄露商业机密”。我们展示了这次事件的真实日志截图已脱敏、Sentry 截图、以及竞品公司随后发布的相似话术对比。没有指责只有事实呈现。会后92% 的前端工程师主动提交了自查报告团队自发形成了“代码审查必查 prompt 相关字段”的新默契。这次事件最终没有造成任何客户影响但其价值远超一次故障处理。它让我们彻底看清system_prompts_leaks 不是一个技术问题而是一个工程成熟度的温度计。当团队开始认真对待一行console.log的潜在危害时说明他们真正理解了 AI 应用的特殊性——那段看似普通的文本已是业务逻辑的神经中枢。5. 常见问题与排查技巧实录一线工程师总结的 12 个高发场景与应对清单在处理了超过 30 起疑似或确认的 system_prompts_leaks 事件后我整理了一份实战派问题排查清单。它不讲理论只列现象、原因、验证方法和一句话解决方案。你可以把它贴在显示器边框上或者存为手机备忘录遇到问题直接对照。序号现象描述最可能原因快速验证方法一句话解决方案1用户在浏览器 DevTools 的 Network 标签页能看到请求 payload 里包含system字段前端错误地将 system prompt 作为请求体的一部分发送在 Network 中点击任意一条/api/chat请求查看 Payload tab立即修改前端代码删除所有messages数组外的system_prompt字段确保仅后端生成2Sentry 或其他错误监控平台的日志详情里出现大段以“你是一名……”开头的文本后端日志级别设为 DEBUG且记录了request.body在 Sentry 搜索you are a或you are an查看上下文日志将日志级别调至 INFO或在 logger 配置中明确 excluderequest.body字段3API 网关的审计日志中存在大量包含role: system的记录网关配置了全量请求体记录且权限设置过宽登录网关管理后台检查 Audit Log 配置查看是否启用log_full_body关闭log_full_body或启用字段过滤排除system、prompt等关键词4使用 curl 或 Postman 测试接口时响应头X-RateLimit-Remaining等字段旁意外看到 prompt 片段LLM 提供商的错误响应如 400返回了调试信息包含原始 payload故意发送一个格式错误的请求如 missingmessages观察响应体在后端异常处理中对所有非 2xx 响应统一返回标准化错误消息绝不透出原始输入5流式响应streaming的前端聊天窗口第一行显示的是“你是一名客服……”而非用户消息前端 SDK 将 system message 作为第一条消息渲染在前端代码中搜索addMessage、appendMessage检查首次调用的参数修改前端逻辑首次渲染只显示用户输入system prompt 仅用于模型内部不推送到 UI6Redis 缓存中某个chat:session:{id}的 value 里包含完整的 system prompt后端将整个 chat context含 system序列化后存入缓存使用redis-cli连接执行GET chat:session:xxx查看返回值缓存中只存messages数组system prompt 由后端每次请求时动态加载绝不缓存7数据库慢查询日志中出现INSERT INTO logs (...) VALUES (... system: you are... ...)应用将包含 prompt 的完整日志写入数据库表查询数据库logs表执行SELECT * FROM logs WHERE content LIKE %you are% LIMIT 1修改日志入库逻辑对content字段执行re.sub(rsystem:.*?\\n, , content)脱敏后再存储8本地开发时docker-compose logs -f api输出里能看到system prompt: ...Docker 日志驱动配置为json-file且未设置日志大小限制DEBUG 日志全量输出在终端执行docker-compose logs api | grep system prompt在 docker-compose.yml 中为 api 服务添加logging配置设置max-size: 10m并禁用 DEBUG 级别9使用kubectl logs -f pod-name查看 Kubernetes Pod 日志发现 prompt 明文应用容器内 stdout/stderr 输出了未脱敏的 prompt在集群中执行kubectl logs pod-name | grep -i you are在应用代码中所有print()、logger.info()调用前先调用sanitize_prompt()函数处理10第三方分析工具如 Mixpanel的事件属性里出现了prompt_role: vip前端在 track 事件时错误地将 prompt 角色作为属性上报在 Mixpanel 控制台筛选事件chat_start查看属性列表分析事件属性只允许user_role、chat_duration等业务字段严禁prompt_*类属性11CI/CD 流水线的构建日志如 GitHub Actions中出现echo $SYSTEM_PROMPT的输出构建脚本中为了调试临时打印了环境变量在 Actions 的 Run step 中搜索echo、printenv所有构建脚本中的echo必须加if [ $CI ! true ]; then ... fi条件判断12用户反馈“AI 回答里提到了我们内部系统的代号‘星尘’”system prompt 中硬编码了内部系统名且未做脱敏在 prompt YAML 文件中搜索星尘、Stardust、SD-等关键词将所有内部系统名替换为通用占位符如INTERNAL_SYSTEM并在脱敏函数中统一处理提示排查时永远从最可能的环节开始——前端代码 后端日志 API 网关 LLM 响应。90% 的泄漏发生在前三者不要一上来就怀疑 LLM 提供商。注意不要依赖“没人会去看日志”这种侥幸心理。我亲眼见过安全研究员用自动化脚本每天凌晨扫描 200 个公开 Sentry 实例专门抓取you are a开头的日志。system prompt 的暴露往往不是被黑客盯上而是被竞争对手的市场部实习生在咖啡机旁随手搜出来的。最后分享一个我坚持了两年的习惯每周五下午我会花 15 分钟随机打开一个线上环境的用户会话ID 从生产数据库随机抽取然后手动模拟一次完整咨询流程。不是看回答是否正确而是全程盯着 Network 面板、Console 面板、Sentry 页面像一个最挑剔的审计员一样检查每一个字节的流向。这个习惯让我提前发现了 7 次潜在泄漏其中 4 次源于新同事提交的 PR 里一个不起眼的console.log(payload)。system_prompts_leaks 不是需要攻克的堡垒而是一扇需要时时检查的门——你永远不知道上一次关门时有没有把钥匙留在了外面。