登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  科技周边 >  人工智能

AI 网关怎样记录 token 用量而不泄露提示词

来源:17golang原创

时间:2026-11-07 23:39:35 158浏览 收藏

AI 网关既要回答“这次请求用了多少 token”,又不能把客户提示词和模型回答原样写进日志。比较稳妥的做法,是把模型调用数据和观测数据拆成两条路径:原文只在请求转发链路中短暂存在,观测链路只接收 request_id、模型、延迟、状态和 usage 等结构化字段。

记录 token 用量不等于记录提示词。网关应在模型响应边界提取 input_tokens、output_tokens、total_tokens,生成脱敏事件,再写入指标或用量账本;默认不保存 prompt、response、完整请求体和调试异常原文。

下面按一个可迁移到不同模型供应商的架构来拆解。示例使用兼容 OpenAI 风格响应的字段名,字段实际名称应以当前供应商返回值为准。

先把“能计量”和“不能落盘”分开

网关面对的不是一个简单的日志开关,而是两类不同资产:

数据用途默认处理
input_tokens、output_tokens、total_tokens成本、配额、模型对比可以进入结构化用量事件
model、provider、tenant_id、request_id路由、追查、聚合按最小必要原则保留
prompt、response、instructions业务内容不进入普通日志和 spend 账本
API key、Cookie、用户标识身份与鉴权哈希、截断或完全丢弃

这里最容易犯的错,是先把完整请求对象交给通用 logger,再尝试在日志平台里“查询时隐藏”。这已经把泄露窗口扩大到了 SDK debug 日志、异常堆栈、重试记录、缓存和第三方 callback。脱敏要发生在网关把事件交给观测后端之前。

AI 网关从请求元数据和模型 usage 生成脱敏日志与聚合账本的结构图
图1:AI 网关的 token 用量账本结构示意图;原始提示词和回答不进入普通观测存储。

在响应边界提取 usage,而不是重新猜 token 数

最可靠的计量点通常是模型响应返回 usage 的位置。网关可以读取供应商返回的输入、输出和总 token 字段,并把它们复制到内部事件;不要把提示词再送进本地 tokenizer 后当成最终账单,因为不同模型的 tokenizer、系统消息、工具调用和缓存 token 规则可能不同。

type ProviderUsage struct {
	// 这些字段来自模型供应商的响应,不保存原始文本。
	InputTokens  int64 `json:"input_tokens"`
	OutputTokens int64 `json:"output_tokens"`
	TotalTokens  int64 `json:"total_tokens"`
}

type UsageEvent struct {
	// request_id 用于关联一次逻辑请求,不能拿提示词摘要代替。
	RequestID string `json:"request_id"`
	TenantID  string `json:"tenant_id"`
	Model     string `json:"model"`
	Status    string `json:"status"`
	LatencyMS int64  `json:"latency_ms"`
	Usage     ProviderUsage `json:"usage"`
}

func buildUsageEvent(requestID, tenantID, model, status string, latencyMS int64, u ProviderUsage) UsageEvent {
	// 只组装可聚合的标量字段;prompt 和 response 不作为参数传入。
	return UsageEvent{
		RequestID: requestID,
		TenantID:  tenantID,
		Model:     model,
		Status:    status,
		LatencyMS: latencyMS,
		Usage:     u,
	}
}

如果供应商只在流结束时返回 usage,就在流关闭或收到最终事件时写入一次;不要把每个增量片段都当成一次完整调用。非流式请求也应以网关最终收到的 usage 为准,并把缺失字段标记为 unknown,而不是用一个看似精确的估算值覆盖。

脱敏事件和用量账本应该是两层

日志适合回答“哪个请求在什么时候失败”,账本适合回答“某租户在这个时间桶用了多少 token”。把二者混成一张包含请求体的明细表,会让权限和保留期变得很难管理。

// 发送到观测后端的事件只包含结构化字段,不带 messages、input 或 output。
type RedactedEvent struct {
	RequestID    string `json:"request_id"`
	TenantHash   string `json:"tenant_hash"`
	Model        string `json:"model"`
	InputTokens  int64  `json:"input_tokens"`
	OutputTokens int64  `json:"output_tokens"`
	TotalTokens  int64  `json:"total_tokens"`
	Status       string `json:"status"`
	LatencyMS    int64  `json:"latency_ms"`
	CostEstimate int64  `json:"cost_micros"`
}

func redactForLog(e RedactedEvent) map[string]any {
	// 显式列出允许外发的字段,避免未来新增字段时把原文带入日志。
	return map[string]any{
		"request_id": e.RequestID,
		"tenant_hash": e.TenantHash,
		"model": e.Model,
		"input_tokens": e.InputTokens,
		"output_tokens": e.OutputTokens,
		"total_tokens": e.TotalTokens,
		"status": e.Status,
		"latency_ms": e.LatencyMS,
		"cost_micros": e.CostEstimate,
	}
}

这里的白名单比“把字段名里含有 prompt 的键删掉”更可靠:嵌套消息、工具参数、异常上下文和自定义 metadata 都可能藏着业务文本。租户维度也不一定要原样保存,可以使用不可逆哈希或内部租户编号,并限制可查询的人员范围。

流式、重试和缓存会改变计量语义

真实网关中,token 统计最容易在边界条件下重复或缺失。建议先定义一条逻辑请求的生命周期:

  • 重试:用 request_id 表示用户发起的一次逻辑请求,用 attempt_id 表示每次供应商尝试。成本账本按实际成功或失败尝试汇总,但报表不要把一次重试误报成两次用户请求。
  • 流式响应:打开流时只记录 started,收到最终 usage 或连接失败时再写终态;连接中断不能把最后一个增量片段当作完整 output_tokens。
  • 缓存命中:命中缓存时要明确是“模型实际 token”还是“按策略估算的节省量”。两者不能共用 total_tokens 字段。
  • 错误响应:供应商拒绝、超时和网关限流都可以写 status 和 error_type,但不要把上游原始错误全文直接写入公共日志。

一个小的字段设计能避免后续争议,例如同时保留 request_count、provider_attempt_count、usage_source 和 is_cache_hit。这样财务、SRE 和产品看到的是同一套事实,但可以按不同维度聚合。

AI 网关把原始提示词与模型请求和脱敏事件分隔开的隐私边界结构图
图2:模型请求路径与观测路径的隐私边界示意图;观测后端只接收脱敏事件和聚合用量。

把权限、保留期和调试样本拆开

“日志没有 prompt”并不代表系统天然安全。仍要检查三种常被忽略的旁路:

  1. 网关进程的 debug 级别是否会打印完整请求或响应;
  2. 失败回调、重试中间件和 trace exporter 是否自动采集消息内容;
  3. 缓存键、死信队列、样本录制和人工排障工具是否复制了原始文本。

生产环境可以把数据划成三套权限:普通指标只看聚合 token;脱敏事件可由值班和成本管理员按 request_id 查询;必要的临时调试样本单独存放,设置短保留期、审批和自动删除。不要因为排障方便,就让所有开发账号都能读到第三套数据。

如果使用现成网关或观测集成,先确认它的默认日志行为、每请求覆盖开关和异常日志策略。例如 LiteLLM 的官方日志说明提供了关闭消息记录、保留 usage/cost 元数据和按请求控制脱敏的配置线索,但具体默认值和版本行为要以部署版本文档为准。

官方地址:https://docs.litellm.ai/docs/proxy/logging

上线前用一张清单判断是否真的“没泄露”

可以用下面的最小清单做设计评审:

  • 日志事件是否由允许字段白名单构造,而不是从完整请求对象复制?
  • input_tokens 和 output_tokens 是否来自供应商响应的 usage,而不是二次猜测?
  • 流式结束、重试、缓存命中和上游错误是否有独立状态与计量语义?
  • 普通日志、聚合账本、调试样本是否有不同的权限和保留期?
  • SDK debug、异常、trace、callback、缓存和队列是否都经过同一条脱敏政策?
  • 即使调用方传入“请记录原文”的 metadata,网关是否仍能强制保持默认保护?

最终的判断标准很简单:观测系统能回答“谁、何时、调用哪个模型、用了多少 token、结果怎样”,但拿不到“用户具体说了什么、模型具体回答了什么”。这就是 AI 网关记录用量与保护提示词之间比较稳定的边界。

相关问题

只保存 prompt 的哈希可以吗?可以用于去重或关联,但哈希不是脱敏万能药。短文本、低熵模板和可猜测的系统提示词仍可能被字典攻击,生产上还应限制访问和保留期。

没有 usage 字段时要不要自己计算?可以作为估算字段单独命名,例如 estimated_input_tokens,并保留估算算法与版本;不要把估算值冒充供应商最终计量。

排查单次模型质量问题怎么办?使用经过审批的短期调试样本,单独授权、加密、脱敏并自动过期;不要为了少写一套流程而打开全局 prompt logging。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>