☰
BFE AI 访问日志可观测字段升级实战:`bfe-access-pb` 协议对齐与 `ai_*` 新字段采集改造
2026/10/10 5:34:34 网站建设 项目流程
  • 后端
  • 网络/通信
  • 云原生

【免费下载链接】bfe

A modern layer 7 load balancer from baidu

项目地址:https://gitcode.com/gh_mirrors/bf/bfe
点击查看免费下载

本文以 BFE 开源仓库(Go 语言实现的七层负载均衡器)中 AI 网关能力为背景,完整解读一份关于 AI 访问日志可观测字段升级的设计变更方案。文章从协议字段重命名与新增的动机出发,逐层展开bfe-access-pb依赖升级、AiBasicInfo/AiAuthInfo结构扩展、各 AI 模块采集逻辑填充、访问日志赋值改造、测试计划、兼容性与回滚策略,并辅以仓库源码作为实现证据。读者读完可掌握在 BFE 中为 AI 请求补齐 provider、retry、cost、路由命中、cluster/key 尝试等观测字段的完整改造路径。

1. 背景:bfe-access-pb协议升级带来的配套改造需求

BFE 通过mod_access_pb3模块输出基于 protobuf 的访问日志(b2log)。随着 AI 网关业务的发展,承载访问日志协议的独立仓库bfe-access-pb完成了 AI 可观测字段的扩展与重命名(协议定义详见bfe-access-pb/docs/protobuf.md)。本次设计变更的核心目标,就是让 BFE 侧的访问日志代码与新版协议对齐,从而把 AI 请求在网关内部的关键行为完整、安全地落进日志。

1.1 字段重命名(编号不变)

旧字段名新字段名语义变化
ai_apikeyai_apikey_id不再记录原始 API Key 值,改为记录 API Key 内部标识
ai_mapped_modelai_target_model记录路由/映射后的目标模型
ai_prompt_tokensai_input_tokens记录输入(prompt)token 数

三个字段的 protobuf 编号保持不变(分别为 701、704、706),二进制层面完全兼容;变化只体现在生成代码的 Go 字段名上。

1.2 新增字段

字段含义
ai_provider上游模型提供商标识(如 openai、deepseek)
ai_retry_count模型调用层重试次数
ai_cost_value/ai_cost_currencyRMB 成本计量与币种
ai_route_rule_hits命中的 AI 路由规则列表
ai_cluster_key_names请求处理过程中尝试过的 (cluster, key) 列表
ai_auth_hit_quota_plans正常请求时命中的 Quota Plan ID 列表

1.3 新增辅助消息

  • AIRouteRuleHit:描述命中路由规则的 owner / owner_type / name;
  • ClusterKeyName:描述尝试过的 cluster 与 key 名称组合。

变更发生前,BFE 依赖的是github.com/bfenetworks/bfe-access-pb v0.1.0,访问日志代码中引用的仍是旧字段名(AiApikey、AiMappedModel、AiPromptTokens),因此需要一次系统性的配套改造。

2. 目标与变更总览

本次改造的六项目标:

  1. 将 BFE 依赖的bfe-access-pb升级到包含新字段的版本(方案建议v0.2.0或本地 replace);
  2. 修正访问日志中对重名字段的引用;
  3. 将ai_apikey_id的数据源从原始 API Key 改为 API Key 内部 ID(Token.KeyId/AiBasicInfo.ClientKeyId);
  4. 在 BFE 各 AI 模块中补充新增字段的采集逻辑;
  5. 更新单元测试bfe_modules/mod_access_pb3/request_log_test.go;
  6. 保持非 AI 请求和未升级配置场景下的向后兼容。

从仓库当前实现看,目标 3 已在token_rule_table.go中落地:ValidateUserTokenByReq在鉴权早期就把token.KeyId写入aiBasicInfo.ClientKeyId(见 token_rule_table.go),保证即使请求后续被拒绝,访问日志仍能识别 token 身份。

下表是方案给出的全量字段映射(Proto 字段 → 编号 → 旧 BFE 字段/状态 → 新 BFE 数据源 → 需修改的文件):

Proto 字段编号旧 BFE 字段/状态新 BFE 数据源需修改的文件
ai_apikey_id701AiApikey(记录ClientApiKey)AiBasicInfo.ClientKeyIdrequest_log.go
ai_apikeytags702已支持AiBasicInfo.ApikeyTags不变
ai_requested_model703已支持AiBasicInfo.ClientModel不变
ai_target_model704AiMappedModelAiBasicInfo.TargetModelrequest_log.go
ai_stream705已支持req.IsSse不变
ai_input_tokens706AiPromptTokensTokenUsage.PromptTokensrequest_log.go
ai_output_tokens707已支持TokenUsage.CompletionTokens不变
ai_total_tokens708已支持TokenUsage.UsedQuota不变
ai_ttft_us709已支持TokenTimeInfo.TTFT不变
ai_tpot_us710已支持TokenTimeInfo.TPOT不变
ai_rate_limit_hits711已支持AiRateLimitHitInfo不变
ai_auth_reject_reason712已支持AiAuthInfo.RejectReason不变
ai_auth_reject_quota_plans713已支持AiAuthInfo.RejectQuotaPlans不变
ai_provider714缺失cluster.AIConf.Providerrequest_ai_basic.go,reverseproxy.go
ai_retry_count715缺失AiBasicInfo.RetryCountrequest_ai_basic.go,reverseproxy.go
ai_cost_value761缺失TokenUsage.UsedCostrequest_log.go
ai_cost_currency762缺失cluster.AIConf.ModelTable.Currencyrequest_ai_basic.go,reverseproxy.go,request_log.go
ai_route_rule_hits801缺失AiRouteResult→AIRouteRuleHitrequest_ai_route.go,request_log.go
ai_cluster_key_names802缺失AiBasicInfo.ClusterKeyNamesrequest_ai_basic.go,reverseproxy.go,request_log.go
ai_auth_hit_quota_plans841缺失AiAuthInfo.HitQuotaPlansrequest_ai_basic.go,token_rule_table.go,request_log.go

3. 依赖升级:bfe-access-pb版本替换

涉及文件:go.mod

方案要求将bfe-access-pb从v0.1.0升级到包含新字段的版本:

require ( ... github.com/bfenetworks/bfe-access-pb v0.2.0 ... )

本地开发阶段可临时启用 replace 指向本地源码目录:

replace github.com/bfenetworks/bfe-access-pb => ../bfe-access-pb

升级后执行:

cd bfe go mod tidy go mod download

注意:bfe-access-pb的.pb.go文件需要在 Linux 环境下执行build.sh重新生成并打 tag 后,BFE 才能引用到正确版本。

从当前仓库的 go.mod 可以看到,依赖已演进到github.com/bfenetworks/bfe-access-pb v0.3.6,且go.sum随go mod tidy自动更新,说明该升级链路已在仓库中完成闭环。

4. 核心数据结构扩展:AiBasicInfo与AiAuthInfo

4.1 扩展AiBasicInfo

涉及文件:request_ai_basic.go

方案在AiBasicInfo中新增四个字段与一个辅助结构:Provider(上游 provider,如 openai / deepseek)、RetryCount(模型调用层重试次数)、CostCurrency(成本币种,如 RMB / USD)、ClusterKeyNames(尝试过的 (cluster, key) 列表),并定义ClusterKeyName描述一次尝试的 cluster 与 key 名称组合:

type AiBasicInfo struct { ClientApiKey string ClientKeyId string ClientModel string TargetModel string Provider string // 新增:上游 provider,如 openai / deepseek RetryCount uint32 // 新增:模型调用层重试次数 CostCurrency string // 新增:成本币种,如 RMB / USD tokenUsage TokenUsage ApikeyTags []ApikeyTag TokenTimeInfo TokenTimeInfo AiAuthInfo AiAuthInfo ClusterKeyNames []ClusterKeyName // 新增:尝试过的 (cluster, key) 列表 allowEstimateToken bool } // ClusterKeyName 描述一次尝试的 cluster 与 key 名称组合 type ClusterKeyName struct { ClusterName string KeyName string }

同时新增两个辅助方法:

func (aiinfo *AiBasicInfo) AppendClusterKeyName(clusterName, keyName string) { aiinfo.ClusterKeyNames = append(aiinfo.ClusterKeyNames, ClusterKeyName{ ClusterName: clusterName, KeyName: keyName, }) } func (aiinfo *AiBasicInfo) IncrementRetryCount() { aiinfo.RetryCount++ }

从当前仓库源码看,这两处扩展均已落地:request_ai_basic.go 中AiBasicInfo已包含Provider、RetryCount、CostCurrency、ClusterKeyNames字段,AppendClusterKeyName与IncrementRetryCount方法也存在于 request_ai_basic.go。

4.2 扩展AiAuthInfo

涉及文件:request_ai_basic.go

在AiAuthInfo中新增HitQuotaPlans,用于记录成功鉴权时参与余额检查并放行的 Quota Plan ID:

type AiAuthInfo struct { RejectReason string // 拒绝原因 RejectQuotaPlans []string // 拒绝时余额不足的 Quota Plan IDs HitQuotaPlans []string // 新增:成功时命中的 Quota Plan IDs }

该结构在仓库中的实现与方案完全一致(见 request_ai_basic.go)。

5. 模块数据填充:把采集逻辑写入各 AI 模块

5.1mod_ai_token_auth:记录命中配额计划

涉及文件:token_rule_table.go

在ValidateUserTokenByReq的for _, plan := range token.QuotaPlans循环中,当 Quota Plan 通过余额检查(hasBalance == true)时,将其 ID 记录到AiAuthInfo.HitQuotaPlans:

// 在 for _, plan := range token.QuotaPlans 循环内 if !hasBalance { SetAiAuthInfo(req, bfe_basic.CodeQuotaExhausted, []string{plan.Id}) ... } // 新增:记录成功命中的 quota plan aiBasicInfo := req.GetAiBasicInfo() if aiBasicInfo != nil { aiBasicInfo.AiAuthInfo.HitQuotaPlans = append(aiBasicInfo.AiAuthInfo.HitQuotaPlans, plan.Id) }

说明:Unlimited或PassNoQuota的 plan 不经过HasBalance检查,因此不会进入HitQuotaPlans。这符合语义:HitQuotaPlans仅记录实际参与余额校验并命中的计划。

仓库中的实现印证了这一点:token_rule_table.go 中先跳过plan.Unlimited || plan.PassNoQuota,再依次处理过期、余额不足(写入RejectQuotaPlans)与通过(追加到HitQuotaPlans)三种情况。

5.2bfe_server/reverseproxy.go:记录 provider、currency、retry、cluster/key

涉及文件:reverseproxy.go

A. 在doSingleAIForward中记录 provider、currency 和 cluster/key:

func (p *ReverseProxy) doSingleAIForward(..., selectedKey cluster_conf.AIKey) (...) { ... if cluster.AIConf != nil && aiMeta != nil { if cluster.AIConf.Provider != "" { aiMeta.Provider = cluster.AIConf.Provider } if cluster.AIConf.ModelTable != nil && cluster.AIConf.ModelTable.Currency != "" { aiMeta.CostCurrency = cluster.AIConf.ModelTable.Currency } aiMeta.AppendClusterKeyName(cluster.Name, selectedKey.Name) } ... }

注意:需要确认cluster对象是否有Name字段;如果没有,使用attempt.ClusterName。

仓库中的doSingleAIForward实现在 reverseproxy.go 中,Provider/CostCurrency赋值与aiMeta.AppendClusterKeyName(cluster.Name, selectedKey.Name)调用均与方案一致,且确认cluster对象具备Name字段。

B. 在aiClusterInvoke中统计重试次数:

for retry := 0; retry <= policy.MaxRetries; retry++ { if retry > 0 { if aiMeta != nil { aiMeta.IncrementRetryCount() } ... } ... }

仓库实现中,aiClusterInvoke的 key-level 重试循环(for retry := 0; retry <= policy.MaxRetries; retry++)在每次重试前调用aiMeta.IncrementRetryCount()(见 reverseproxy.go)。

说明:RetryCount只统计同一 cluster 内 key-level 的重试次数,与 HTTP 层basicReq.RetryTime解耦。fallback 到另一个 cluster 时,该计数器不累加(符合协议语义)。这与访问日志中已有的backend_retry字段(记录 HTTP 层重试,见 request_log.go)形成互补:一个是模型调用层视角,一个是 HTTP 转发层视角。

5.3 数据源的配置层面支撑:cluster.AIConf

从配置加载代码可以看到,AIConf位于 cluster_conf_load.go,包含Provider(模型价格表中的 provider 名称)与ModelTable(含Currency字段)等配置。配置测试用例覆盖了RMB与USD两种币种(见 cluster_conf_load_test.go)。这意味着ai_provider与ai_cost_currency的实际取值直接来自集群的 AI 配置项,而非运行时推导。

6. 访问日志赋值改造:reqAiInfoGen

涉及文件:request_log.go

方案要求将reqAiInfoGen函数更新为使用新字段名并填充新增字段。核心逻辑如下:

func reqAiInfoGen(reqLog *bfe_access_pb3.RequestLog, req *bfe_basic.Request, res *bfe_http.Response) { aiInfo := req.GetAiBasicInfo() if aiInfo == nil { return } // API Key ID(不再记录原始 key) if aiInfo.ClientKeyId != "" { reqLog.AiApikeyId = proto.String(aiInfo.ClientKeyId) } // API Key Tags if len(aiInfo.ApikeyTags) > 0 { for _, tag := range aiInfo.ApikeyTags { reqLog.AiApikeytags = append(reqLog.AiApikeytags, &bfe_access_pb3.ApikeyTag{ Tagname: proto.String(tag.TagName), Tagvalue: proto.String(tag.TagValue), }) } } // Model if aiInfo.ClientModel != "" { reqLog.AiRequestedModel = proto.String(aiInfo.ClientModel) } if aiInfo.TargetModel != "" { reqLog.AiTargetModel = proto.String(aiInfo.TargetModel) } // Provider if aiInfo.Provider != "" { reqLog.AiProvider = proto.String(aiInfo.Provider) } // Stream reqLog.AiStream = proto.Bool(isStreamResponse(req, res)) // Token usage usage := aiInfo.GetTokenUsage() if usage != nil { reqLog.AiInputTokens = proto.Int64(usage.PromptTokens) reqLog.AiOutputTokens = proto.Int64(usage.CompletionTokens) reqLog.AiTotalTokens = proto.Int64(usage.UsedQuota) if usage.UsedCost > 0 { reqLog.AiCostValue = proto.Int64(usage.UsedCost) } } // Cost currency if aiInfo.CostCurrency != "" { reqLog.AiCostCurrency = proto.String(aiInfo.CostCurrency) } // Retry count if aiInfo.RetryCount > 0 { reqLog.AiRetryCount = proto.Uint32(aiInfo.RetryCount) } // TTFT / TPOT ti := aiInfo.TokenTimeInfo if ti.TTFT > 0 { reqLog.AiTtftUs = proto.Int64(ti.TTFT) } if ti.TPOT > 0 { reqLog.AiTpotUs = proto.Int64(ti.TPOT) } // Auth reject info if len(aiInfo.AiAuthInfo.RejectReason) > 0 { reqLog.AiAuthRejectReason = proto.String(aiInfo.AiAuthInfo.RejectReason) } for _, item := range aiInfo.AiAuthInfo.RejectQuotaPlans { reqLog.AiAuthRejectQuotaPlans = append(reqLog.AiAuthRejectQuotaPlans, item) } for _, item := range aiInfo.AiAuthInfo.HitQuotaPlans { reqLog.AiAuthHitQuotaPlans = append(reqLog.AiAuthHitQuotaPlans, item) } // Route rule hits if routeResult := req.GetAiRouteResult(); routeResult != nil { reqLog.AiRouteRuleHits = append(reqLog.AiRouteRuleHits, &bfe_access_pb3.AIRouteRuleHit{ RuleOwner: proto.String(routeResult.Owner), RuleOwnerType: proto.String(routeResult.RouteType), RuleName: proto.String(routeResult.RuleName), }) } // Cluster / key attempts for _, ckn := range aiInfo.ClusterKeyNames { reqLog.AiClusterKeyNames = append(reqLog.AiClusterKeyNames, &bfe_access_pb3.ClusterKeyName{ ClusterName: proto.String(ckn.ClusterName), KeyName: proto.String(ckn.KeyName), }) } // Rate limit hit info(保持不变) hitInfo := req.GetAiRateLimitHitInfo() if hitInfo != nil && len(hitInfo.HitPolicyDict) > 0 { for policyId, info := range hitInfo.HitPolicyDict { ... } } }

仓库中的reqAiInfoGen实现(见 request_log.go)与方案完全对齐,并在此基础上做了进一步细化:AiApikeytags多填充了Taglevel字段,Token 部分补充了CacheReadTokens、CacheWriteTokens、AudioInputTokens等细分 token 字段,Rate Limit 部分区分了 tpm/rpm/concurrency/redis_error 四种命中类型。

6.1AiRouteResult结构

路由命中信息来自请求上下文中的AiRouteResult(定义于 request_ai_route.go),包含RouteType(apikey / entity / global)、Owner(路由表 owner)、RuleName(命中规则名),并通过SetAiRouteResult/GetAiRouteResult在请求上下文中传递。方案中"可选:为AiRouteResult增加导出方法,便于request_log.go读取",从当前实现看字段本身已可直接访问。

6.2 敏感信息防护:原始 API Key 永不落日志

值得注意的是,仓库中requestLogGen在调用reqAiInfoGen之后还有一道maskSensitiveCredentials(requestLog, req)兜底(见 request_log.go),作为"日志输出前的最后防线",确保原始 API Key 不会进入任何字段。这与本次改造把ai_apikey_id数据源改为内部key_id的语义方向一致,二者共同落实了"日志中不泄露敏感凭证"的硬性要求。

7. 涉及文件清单

文件修改内容
bfe/go.mod升级bfe-access-pb到v0.2.0(或启用 replace)
bfe/go.sum随go mod tidy自动更新
bfe/bfe_basic/request_ai_basic.goAiBasicInfo新增Provider、RetryCount、CostCurrency、ClusterKeyNames;新增ClusterKeyName结构及辅助方法;AiAuthInfo新增HitQuotaPlans
bfe/bfe_basic/request_ai_route.go可选:为AiRouteResult增加导出方法,便于request_log.go读取
bfe/bfe_modules/mod_ai_token_auth/token_rule_table.goValidateUserTokenByReq中记录成功命中的HitQuotaPlans
bfe/bfe_server/reverseproxy.godoSingleAIForward记录 provider / currency / cluster-key;aiClusterInvoke统计 retry count
bfe/bfe_modules/mod_access_pb3/request_log.goreqAiInfoGen使用新字段名并填充新增字段
bfe/bfe_modules/mod_access_pb3/request_log_test.go更新测试断言,覆盖新字段

8. 测试计划

8.1 单元测试

涉及文件:request_log_test.go

更新TestReqAiInfoGen,覆盖以下断言变更:

  1. 将ClientApiKey替换为ClientKeyId,并断言AiApikeyId;
  2. 将AiMappedModel断言改为AiTargetModel;
  3. 将AiPromptTokens断言改为AiInputTokens;
  4. 新增断言:AiProvider、AiRetryCount、AiCostValue/AiCostCurrency、AiRouteRuleHits、AiClusterKeyNames、AiAuthHitQuotaPlans。

测试示例:

aiInfo := &bfe_basic.AiBasicInfo{ ClientKeyId: "key-id-123", ClientModel: "model-a", TargetModel: "model-b", Provider: "deepseek", RetryCount: 1, CostCurrency: "RMB", ClusterKeyNames: []bfe_basic.ClusterKeyName{ {ClusterName: "cluster-a", KeyName: "key-001"}, }, ... } usage.UsedCost = 5000 // 1e-8 元

从仓库测试代码看,上述断言均已实现:TestReqAiInfoGen构造了ClientKeyId: "key-id-123"的AiBasicInfo,并分别断言AiProvider == "deepseek"、AiCostValue == 5000、AiRetryCount == 1、AiAuthHitQuotaPlans长度为 2、AiRouteRuleHits长度为 1、AiClusterKeyNames长度为 1(见 request_log_test.go);另有测试验证路由命中 owner 与 quota plan ID 的精确匹配,以及未鉴权场景下AiAuthHitQuotaPlans应被清空(见 request_log_test.go)。

8.2 编译验证

cd bfe go build ./... go test ./bfe_modules/mod_access_pb3/...

8.3 集成验证

  1. 启用 AI 网关,发起一次带 API Key 的模型请求;
  2. 收集mod_access_pb3输出的 b2log,解码RequestLog;
  3. 校验字段:
    • ai_apikey_id等于 Token 的key_id,而不是原始key;
    • ai_target_model正确反映路由/映射后的模型;
    • ai_provider、ai_retry_count、ai_cost_value、ai_cost_currency非空(RMB 配额场景);
    • ai_route_rule_hits、ai_cluster_key_names、ai_auth_hit_quota_plans与请求行为一致。

9. 兼容性说明

  1. 字段重命名:proto 字段编号不变(701、704、706),因此 protobuf 二进制层面完全兼容;变化只体现在生成代码的 Go 字段名上。
  2. 语义变化:ai_apikey_id从记录原始 API Key 改为记录内部key_id,避免在日志中泄露敏感信息。需要确认上游日志消费方不再依赖原始 key 值。
  3. 新增字段:均为optional,对未升级的旧 BFE 版本无影响。
  4. 版本依赖:升级bfe-access-pb后,旧 BFE 代码无法直接编译,因此这是一个需要同步发布的破坏性变更(仅对 BFE 代码编译层面)。

从仓库实现看,"非 AI 请求向后兼容"也已落地:reqAiInfoGen开头对aiInfo == nil直接返回(request_log.go),非 AI 请求不会进入 AI 字段填充逻辑。

10. 风险与回滚

10.1 主要风险

风险说明规避措施
编译失败新 proto 字段名与旧 BFE 代码不匹配按本方案一次性更新所有引用
日志消费方依赖旧字段名下游解析ai_apikey、ai_mapped_model、ai_prompt_tokens会失败提前通知下游,按 proto 编号而非字段名解析;或在下游做映射
ai_apikey_id为空如果Token.KeyId未配置,日志中将缺失 key 标识确保ai-gateway-api导出的 Token 配置始终包含key_id
重试计数语义不清RetryCount仅统计 key-level 重试,不统计 cluster fallback文档中明确语义;访问日志中已有backend_retry字段记录 HTTP 层重试

10.2 回滚方案

如需回滚到旧协议:

  1. 将bfe/go.mod中的bfe-access-pb版本改回v0.1.0;
  2. 回滚request_log.go到旧字段名(AiApikey、AiMappedModel、AiPromptTokens);
  3. 移除request_ai_basic.go、reverseproxy.go、token_rule_table.go中新增字段的采集逻辑;
  4. 重新编译部署。

注意:回滚后新字段(provider、retry、cost 等)将不再输出到日志。

11. 后续可选扩展

  1. ai_route_rule_hits支持多条命中记录:当前AiRouteResult只记录最终命中的规则。未来如果路由模块支持记录所有匹配规则,可将HitPolicyDict式的列表写入日志。
  2. ai_cluster_key_names区分成功与失败尝试:当前记录所有尝试;可扩展为标记最终成功的 key。
  3. ai_auth_hit_quota_plans与 RMB 扣减计划对齐:当前记录所有通过余额检查的 plan;可与实际扣减计划做交叉验证。

12. 小结

本次设计变更以bfe-access-pb协议扩展为牵引,在 BFE 侧完成了一次从"数据结构 → 采集逻辑 → 日志赋值 → 测试覆盖"的闭环改造:AiBasicInfo/AiAuthInfo承载新增观测数据,mod_ai_token_auth、bfe_server/reverseproxy.go负责在请求处理链路中填充,mod_access_pb3/request_log.go的reqAiInfoGen负责最终落盘,并以字段编号不变保证二进制兼容、以key_id替代原始 key 落实安全要求。对于需要在 BFE 中扩展 AI 可观测性的开发者,本文给出的字段映射表、代码插桩点与测试用例可作为直接参考的改造模板。

文档生成日期:2026-08-19

  • 后端
  • 网络/通信
  • 云原生

【免费下载链接】bfe

A modern layer 7 load balancer from baidu

项目地址:https://gitcode.com/gh_mirrors/bf/bfe
点击查看免费下载

相关推荐

上一篇:CRC32工具箱:一站式CRC32校验反转与计算手册
下一篇:探索OpenSim Core:生物力学模拟的终极指南

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询