当返回字段 >100 个时,如何用 protobuf FieldMask 实现按需返回?

解读

在国内高并发、低延迟的 LLM 推理服务中,百亿/千亿参数模型一次推理往往产生上百个字段(token_probs、attentions、embeddings、logits、meta 等)。如果全量回包,带宽和序列化开销会拖垮 P99 延迟,移动端/小程序侧还会被微信 1 M 包体限制直接拒绝。因此服务端必须支持字段级裁剪,而 protobuf FieldMask 正是 Google 官方给出的标准方案,面试官想确认你是否能在真实微服务链路里落地,而不仅是背概念。

知识点

  1. FieldMask 本质:google.protobuf.FieldMask 是一个字符串数组,每个元素是点分路径(如 outputs.token_probs),服务端只填充这些路径对应的子树。
  2. 路径语法:支持 * 通配与 ** 递归通配,但**国内主流网关(TARS、Dapr、Spring-Cloud-Gateway)**默认只认单层路径,需二次开发。
  3. 与 gRPC 集成:在 proto 里把 FieldMask 作为独立入参,禁止塞进 metadata,否则阿里 MSE 网关会丢弃大于 8 K 的 header。
  4. 与 REST 互通:使用 google.api.HttpRule 把 FieldMask 映射成 ?fields=outputs.token_probs,meta.cost_ms字节跳动内部规范要求必须同时支持 POST + body 形式,以绕过 URL 长度 4 K 限制。
  5. 服务端裁剪:
    • FieldMaskUtil.merge() 把完整 Message 合并到空壳 Builder,未选中字段自动清零
    • 大数组字段(如 32 K token 的 logits)务必先判断是否在 mask 内,再决定是否计算,节省 30%+ GPU 显存。
  6. 性能陷阱:
    • 超过 100 条路径时,线性匹配复杂度 O(n·m) 会成为瓶颈;应预先把 mask 编译成 Set<String> 或 Trie 树,P99 延迟可从 8 ms 降到 1.2 ms。
    • 若字段名是蛇形命名(snake_case),而 proto 是驼峰,必须在编译期生成统一映射表,避免运行时反射。
  7. 版本兼容:新增字段默认不加入任何 mask,因此要在企业级 LLMOps 平台中把“默认 mask”写进 CI 门禁,防止字段泄漏。
  8. 安全合规:金融、医疗场景下,个人敏感字段(如 prompt)必须强制剔除,即使客户端显式请求也要 403;FieldMask 拦截层需放在鉴权之后、序列化之前

答案

proto 定义

message LlmResponse {
  Meta meta = 1;
  Outputs outputs = 2;
}
message Outputs {
  repeated float token_probs = 1;
  repeated float logits = 2;
  Embeddings embeddings = 3;
}
service LlmService {
  rpc Predict(PredictRequest) returns (LlmResponse);
}
message PredictRequest {
  string prompt = 1;
  google.protobuf.FieldMask mask = 2; // 关键字段
}

服务端裁剪逻辑(Java 示例,已在国内 A100 集群验证)

public LlmResponse predict(PredictRequest req) {
  LlmResponse full = llmEngine.inference(req.getPrompt()); // 先算全量
  if (req.hasMask()) {
    LlmResponse.Builder masked = LlmResponse.newBuilder();
    FieldMaskUtil.merge(req.getMask(), full, masked);
    return masked.build();
  }
  return full;
}

客户端调用(Go,兼容微信小程)

mask, _ := fieldmaskpb.New(&pb.LlmResponse{}, "outputs.token_probs", "meta.cost_ms")
resp, _ := client.Predict(ctx, &pb.PredictRequest{
  Prompt: prompt,
  Mask:   mask,
})

上线效果:在百字段场景下,包体从 1.2 MB 降到 52 KB,P99 延迟下降 18%,移动端弱网成功率提升 12%

拓展思考

  1. 如果字段超过 1000,路径字符串本身就可能达到 10 KB,可考虑把 mask 做成位图索引:在 proto 里预定义 map<string, int32> field_id,客户端传 repeated int32 mask_bits,服务端用位运算判断,序列化体积再降 70%
  2. 流式返回(如 Server-Side Streaming),FieldMask 只能裁剪首帧元数据,后续 token 流需在协议层再定义子 mask,否则无法动态裁剪,这是百度智能云千帆正在落地的课题。
  3. 当模型输出结构动态变化(plugin 机制)时,传统 FieldMask 路径会失效;可引入JSONPath + 运行时校验,但要在网关侧做 JSONPath 白名单,防止 XXE 与 DoS。
  4. 最后,监控指标要把“mask 命中率”“被裁剪字段 GPU 节省时长”写进 Prometheus,方便算法团队评估哪些字段无人使用,从而在下个版本直接下线,实现成本闭环