返回博客

API 设计哲学:63 个 proto + 59 个 REST 端点的统一治理

在任何分布式平台中,API 不仅仅是"接口"——它是系统的契约层、协作协议和演进边界。一个设计良好的 API 体系可以让团队独立开发、独立部署、独立演进;一个混乱的 API 体系则会让每次变更都变成一场噩梦。

Coomia发布于 2025年7月1日22 分钟阅读
分享本文Twitter / X

API 设计哲学:63 个 proto + 59 个 REST 端点的统一治理

系列:S2 架构全景 · 第 8 篇 | 难度:中级 | 阅读时间:18 分钟

#TL;DR

  • 智策平台采用 Contract-First 开发模式,所有内部通信通过 63 个 Protobuf 文件定义契约,外部客户端通过 59 个 REST 端点接入,两层之间由 API Gateway 自动转换。
  • 63 个 proto 文件按 8 个 Layer 分包组织,每个 Layer 独立维护自己的 .proto 文件,通过统一的错误码体系和版本策略实现跨 Layer 一致性。
  • Python SDK 从 proto 文件自动生成 38 个 gRPC 客户端,实现了 "改一次 proto,SDK/Server/文档三处同步" 的开发体验。

#引言:API 是分布式系统的神经系统

在任何分布式平台中,API 不仅仅是"接口"——它是系统的契约层协作协议演进边界。一个设计良好的 API 体系可以让团队独立开发、独立部署、独立演进;一个混乱的 API 体系则会让每次变更都变成一场噩梦。

智策平台面临的 API 治理挑战尤其严峻:

Code
8 个 Layer × 多种技术栈 × 3 种语言(Java/Python/TypeScript)
= 大量跨边界调用需要精确定义

我们的选择是:Protobuf First, REST Second。所有核心逻辑通过 gRPC 通信,REST 只是外部客户端的"翻译层"。本文将详细讲解这个 API 体系是如何设计、组织和治理的。

#1. Contract-First 开发:proto 文件是唯一真相

#1.1 什么是 Contract-First

传统开发模式是 Code-First:先写代码,再从代码生成 API 文档(如 Swagger)。这种方式看似高效,但有一个致命问题——接口定义散落在代码中,没有单一真相源

Contract-First 反过来:先写接口定义(proto 文件),再从定义生成代码

Code
Contract-First 流程:

  ┌─────────────┐
  │  .proto 文件  │  ← 唯一真相源(Single Source of Truth)
  └──────┬──────┘
         │
    ┌────┴────┬──────────┬──────────┐
    ▼         ▼          ▼          ▼
  Java      Python    TypeScript   文档
  Server    Client    Client      (自动)
  Stub      Stub      Stub

#1.2 为什么选择 Protobuf 而不是 OpenAPI

维度Protobuf/gRPCOpenAPI/REST
序列化性能二进制,比 JSON 快 5-10xJSON 文本
类型安全强类型,编译时检查运行时验证
流式传输原生支持双向流SSE/WebSocket 需额外处理
代码生成一等公民,官方支持工具链碎片化
向后兼容字段编号机制天然支持需手动管理
浏览器友好不友好,需要 proxy原生支持

结论很清楚:内部用 gRPC(性能+类型安全),外部用 REST(兼容性+浏览器友好)

#1.3 proto 文件的开发流程

Code
1. 设计阶段:
   ├── 在设计文档中定义接口语义
   ├── 编写 .proto 文件
   └── Team Review(检查向后兼容性)

2. 代码生成阶段:
   ├── protoc 生成 Java/Python stubs
   ├── grpc-gateway 生成 REST 反向代理
   └── 自动更新 SDK 客户端

3. 实现阶段:
   ├── Server 端实现 Service 接口
   ├── 编写单元测试
   └── 集成测试验证跨 Layer 调用

4. 发布阶段:
   ├── 版本号递增
   ├── 更新 CHANGELOG
   └── SDK 发布新版本

#2. 63 个 proto 文件的组织结构

#2.1 按 Layer 分包

我们的 proto 文件按照 8 个 Layer 的边界进行组织,每个 Layer 维护自己的 proto 包:

Code
protos/
├── plane_b/                          # Control Layer (Control Layer)
│   ├── ontology/
│   │   ├── object_type.proto         # ObjectType CRUD
│   │   ├── link_type.proto           # LinkType CRUD
│   │   ├── property_type.proto       # PropertyType 管理
│   │   ├── action_type.proto         # ActionType 定义
│   │   └── ontology_version.proto    # Ontology 版本管理
│   ├── world/
│   │   ├── world.proto               # World 生命周期
│   │   ├── world_branch.proto        # World 分支管理
│   │   └── world_merge.proto         # World 合并操作
│   ├── auth/
│   │   ├── authentication.proto      # 认证服务
│   │   ├── authorization.proto       # 授权检查
│   │   └── rbac.proto                # 角色权限管理
│   ├── governance/
│   │   ├── audit.proto               # 审计日志
│   │   ├── data_lineage.proto        # 数据血缘
│   │   └── compliance.proto          # 合规检查
│   └── registry/
│       ├── service_registry.proto    # 服务注册
│       └── config_registry.proto     # 配置注册
│
├── plane_c/                          # Data Layer (Data Layer)
│   ├── storage/
│   │   ├── object_storage.proto      # 对象实例存储
│   │   ├── link_storage.proto        # 关系存储
│   │   ├── property_storage.proto    # 属性存储
│   │   └── time_series.proto         # 时序数据
│   ├── query/
│   │   ├── object_query.proto        # 对象查询
│   │   ├── aggregation.proto         # 聚合计算
│   │   └── search.proto              # 全文搜索
│   ├── pipeline/
│   │   ├── sync_task.proto           # 数据同步任务
│   │   ├── transform.proto           # 数据转换
│   │   └── schedule.proto            # 调度管理
│   └── materialization/
│       ├── view.proto                # 物化视图
│       └── subscription.proto        # 变更订阅
│
├── plane_d/                          # Reasoning & Decision (Reasoning & Decision Layer)
│   ├── reasoning/
│   │   ├── rule.proto                # 规则定义
│   │   ├── rule_evaluation.proto     # 规则评估
│   │   └── constraint.proto          # 约束条件
│   ├── decision/
│   │   ├── decision.proto            # 决策定义
│   │   ├── decision_tree.proto       # 决策树
│   │   └── scoring.proto             # 评分模型
│   ├── derived/
│   │   ├── derived_property.proto    # 派生属性
│   │   └── dependency_dag.proto      # 依赖 DAG
│   └── simulation/
│       ├── scenario.proto            # 场景模拟
│       └── what_if.proto             # What-If 分析
│
├── plane_e/                          # Agent Runtime (Agent Runtime Layer)
│   ├── action/
│   │   ├── action.proto              # Action 执行
│   │   ├── action_template.proto     # Action 模板
│   │   └── approval.proto            # 审批流程
│   ├── workflow/
│   │   ├── workflow.proto            # 工作流定义
│   │   └── workflow_execution.proto  # 工作流执行
│   └── agent/
│       ├── agent.proto               # Agent 定义
│       └── agent_execution.proto     # Agent 执行
│
├── plane_h/                          # SDK & Developer Experience (SDK & Developer Experience Layer)
│   ├── sdk/
│   │   ├── client_config.proto       # SDK 配置
│   │   └── batch_operation.proto     # 批量操作
│   └── developer/
│       ├── function.proto            # 自定义函数
│       └── webhook.proto             # Webhook 管理
│
└── common/                           # 跨 Layer 共享类型
    ├── types.proto                   # 基础类型
    ├── pagination.proto              # 分页
    ├── error.proto                   # 错误码
    ├── metadata.proto                # 元数据
    └── health.proto                  # 健康检查

#2.2 proto 文件的命名与编号规范

每个 proto 文件遵循严格的规范:

PROTOBUF
// file: protos/plane_b/ontology/object_type.proto
syntax = "proto3";

package onto.plane_b.ontology;

option java_package = "com.onto.control.ontology.grpc";
option java_outer_classname = "ObjectTypeProto";
option java_multiple_files = true;

import "common/types.proto";
import "common/pagination.proto";
import "common/error.proto";

// ObjectType 定义了本体模型中的对象类型
service ObjectTypeService {
  // 创建对象类型
  rpc CreateObjectType(CreateObjectTypeRequest)
      returns (CreateObjectTypeResponse);

  // 获取对象类型
  rpc GetObjectType(GetObjectTypeRequest)
      returns (GetObjectTypeResponse);

  // 列出对象类型
  rpc ListObjectTypes(ListObjectTypesRequest)
      returns (ListObjectTypesResponse);

  // 更新对象类型
  rpc UpdateObjectType(UpdateObjectTypeRequest)
      returns (UpdateObjectTypeResponse);

  // 删除对象类型
  rpc DeleteObjectType(DeleteObjectTypeRequest)
      returns (DeleteObjectTypeResponse);
}

message CreateObjectTypeRequest {
  string world_id = 1;           // 所属 World
  string api_name = 2;           // API 名称(驼峰)
  string display_name = 3;       // 显示名称
  string description = 4;        // 描述
  string icon = 5;               // 图标标识
  repeated PropertyDefinition properties = 6;  // 属性列表
  string primary_key_property = 7;  // 主键属性名
}

message CreateObjectTypeResponse {
  onto.common.ResponseMetadata metadata = 1;
  ObjectType object_type = 2;
}

message ObjectType {
  string id = 1;
  string api_name = 2;
  string display_name = 3;
  string description = 4;
  string icon = 5;
  repeated PropertyDefinition properties = 6;
  string primary_key_property = 7;
  int64 created_at = 8;
  int64 updated_at = 9;
  string created_by = 10;
  ObjectTypeStatus status = 11;
}

enum ObjectTypeStatus {
  OBJECT_TYPE_STATUS_UNSPECIFIED = 0;
  OBJECT_TYPE_STATUS_ACTIVE = 1;
  OBJECT_TYPE_STATUS_DEPRECATED = 2;
  OBJECT_TYPE_STATUS_ARCHIVED = 3;
}

#2.3 字段编号策略

我们采用分段编号策略来预留扩展空间:

Code
字段编号分配策略:
1-15    : 高频字段(使用 1 字节 varint,最优性能)
16-99   : 常规字段
100-199 : 扩展字段(预留给未来版本)
200-299 : 内部字段(不对外暴露)
900-999 : 调试/诊断字段

#3. 59 个 REST 端点与 Gateway 模式

#3.1 REST 端点分布

虽然内部通信全部使用 gRPC,但外部客户端(浏览器、第三方系统、移动端)需要 REST API。我们通过 API Gateway 实现 REST 到 gRPC 的自动转换:

Code
59 个 REST 端点按 Layer 分布:

Control Layer (Control):
  /api/v1/ontology/object-types     (CRUD = 5)
  /api/v1/ontology/link-types       (CRUD = 5)
  /api/v1/ontology/action-types     (CRUD = 5)
  /api/v1/worlds                    (CRUD + branch/merge = 8)
  /api/v1/auth/*                    (login/logout/token = 4)
  小计: 27 端点

Data Layer (Data):
  /api/v1/objects                   (CRUD + search = 6)
  /api/v1/links                     (CRUD = 4)
  /api/v1/queries                   (execute/save/list = 3)
  /api/v1/pipelines                 (CRUD + run = 5)
  小计: 18 端点

Reasoning & Decision Layer (Intelligence):
  /api/v1/rules                     (CRUD + evaluate = 5)
  /api/v1/decisions                 (CRUD + execute = 3)
  小计: 8 端点

Agent Runtime Layer (Agent):
  /api/v1/actions                   (execute/list/status = 3)
  /api/v1/workflows                 (CRUD = 3)
  小计: 6 端点

合计: 59 端点

#3.2 Gateway 转换架构

Code
                    ┌──────────────────────────────────┐
                    │          API Gateway              │
                    │   (Spring Cloud Gateway / Envoy)  │
                    │                                   │
  REST Client ─────►│  1. 认证/鉴权 (JWT 验证)          │
  (Browser,         │  2. 限流 (Token Bucket)           │
   Mobile,          │  3. REST→gRPC 转换               │
   3rd Party)       │  4. 响应 gRPC→JSON 转换           │
                    │  5. 错误码映射                     │
                    └────────┬─────────┬────────┬──────┘
                             │         │        │
                    ┌────────▼──┐ ┌────▼────┐ ┌─▼──────────┐
                    │onto-control│ │onto-data│ │onto-intelli│
                    │  (gRPC)   │ │ (gRPC)  │ │  (gRPC)    │
                    │ Control Layer   │ │ Data Layer │ │ Reasoning & Decision Layer + Agent Runtime Layer  │
                    └───────────┘ └─────────┘ └────────────┘

#3.3 REST→gRPC 映射规则

转换遵循一套确定性的映射规则:

Code
REST 方法 → gRPC 方法映射:

POST   /api/v1/object-types          → ObjectTypeService.CreateObjectType
GET    /api/v1/object-types/{id}     → ObjectTypeService.GetObjectType
GET    /api/v1/object-types          → ObjectTypeService.ListObjectTypes
PUT    /api/v1/object-types/{id}     → ObjectTypeService.UpdateObjectType
DELETE /api/v1/object-types/{id}     → ObjectTypeService.DeleteObjectType

URL 路径参数 → Protobuf 字段映射:
  {id} → request.id
  ?page_size=20 → request.pagination.page_size
  ?page_token=xxx → request.pagination.page_token

HTTP Header → gRPC Metadata 映射:
  Authorization: Bearer <token> → metadata["authorization"]
  X-World-Id: <world_id>       → metadata["x-world-id"]
  X-Request-Id: <uuid>         → metadata["x-request-id"]

#3.4 JSON 与 Protobuf 字段名转换

Code
Protobuf (snake_case)  →  JSON (camelCase)

object_type_id         →  objectTypeId
display_name           →  displayName
created_at             →  createdAt
page_token             →  pageToken

这个转换由 protobuf-java-utilJsonFormat 自动完成,无需手动映射。

#4. API 版本策略

#4.1 版本号设计

Code
版本策略:

URI 版本: /api/v1/...  /api/v2/...
Proto 包版本: onto.plane_b.ontology.v1  → onto.plane_b.ontology.v2

当前版本: v1(所有 59 个端点)
规划版本: v2(当 v1 有不兼容变更时启用)

版本生命周期:
  v1 发布 → v2 发布 → v1 标记废弃 → 6 个月过渡期 → v1 下线

#4.2 向后兼容规则

我们定义了明确的"兼容性契约":

Code
✅ 向后兼容的变更(不需要升级版本):
  - 添加新的 RPC 方法
  - 添加新的 message 字段(使用新编号)
  - 添加新的 enum 值
  - 添加新的 REST 端点
  - 放宽验证规则(如从 required 改为 optional)

❌ 不兼容的变更(必须升级版本):
  - 删除或重命名 RPC 方法
  - 删除或重命名 message 字段
  - 修改字段类型或编号
  - 修改 RPC 方法的语义
  - 收紧验证规则

#4.3 Proto 兼容性检查

我们在 CI/CD 中使用 buf 工具进行自动兼容性检查:

YAML
# .gitlab-ci.yml 中的 proto 检查阶段
proto-lint:
  stage: validate
  script:
    - buf lint protos/
    - buf breaking protos/ --against .git#branch=main
  rules:
    - changes:
        - protos/**/*.proto
YAML
# buf.yaml 配置
version: v1
breaking:
  use:
    - WIRE_JSON      # 检查 wire format 和 JSON 兼容性
    - PACKAGE        # 检查包级别变更
lint:
  use:
    - DEFAULT
    - COMMENTS       # 强制要求注释
  except:
    - PACKAGE_VERSION_SUFFIX

#5. 认证与限流

#5.1 认证流程

Code
认证架构:

  Client                Gateway              Auth Service (Control Layer)
    │                      │                        │
    │  1. Login            │                        │
    │  POST /auth/login    │                        │
    │  {user, password}    │                        │
    │─────────────────────►│                        │
    │                      │   2. gRPC Authenticate │
    │                      │───────────────────────►│
    │                      │                        │ 3. 验证凭证
    │                      │   4. JWT + Refresh     │    查询RBAC
    │                      │◄───────────────────────│
    │  5. {access_token,   │                        │
    │      refresh_token}  │                        │
    │◄─────────────────────│                        │
    │                      │                        │
    │  6. API 请求         │                        │
    │  Authorization:      │                        │
    │  Bearer <jwt>        │                        │
    │─────────────────────►│                        │
    │                      │  7. JWT 本地验证        │
    │                      │  (公钥缓存,不调用      │
    │                      │   Auth Service)         │
    │                      │                        │
    │                      │  8. 转发 gRPC 请求      │
    │                      │  (metadata 注入         │
    │                      │   user_id, roles)       │
    │                      │───────────────────────►│

#5.2 JWT Token 结构

JSON
{
  "sub": "user-001",
  "iss": "onto-platform",
  "iat": 1711234567,
  "exp": 1711238167,
  "roles": ["admin", "data-engineer"],
  "worlds": ["world-prod", "world-staging"],
  "permissions": [
    "ontology:read",
    "ontology:write",
    "objects:read",
    "objects:write",
    "actions:execute"
  ]
}

#5.3 限流设计

Code
限流策略(三层防护):

第一层:全局限流
  ├── 所有客户端共享:10,000 req/s
  └── 超限返回 HTTP 429

第二层:租户限流
  ├── 每个租户:1,000 req/s
  ├── 关键 API(写操作):100 req/s
  └── 超限返回 HTTP 429 + Retry-After header

第三层:用户限流
  ├── 每个用户:100 req/s
  ├── 特殊 API(导出/批量):10 req/s
  └── 超限返回 HTTP 429

算法:Token Bucket(令牌桶)
实现:Gateway 内存 + Redis 共享计数
Java
// Gateway 限流配置示例
@Configuration
public class RateLimitConfig {

    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest()
                .getHeaders()
                .getFirst("X-User-Id")
        );
    }

    @Bean
    public RateLimiter rateLimiter() {
        return new RedisRateLimiter(100, 200); // 100 req/s, burst 200
    }
}

#6. Python SDK 自动生成 38 个 gRPC 客户端

#6.1 生成流程

这是我们开发效率的核心机制之一:proto 文件一旦变更,Python SDK 的 gRPC 客户端代码自动重新生成。

Code
Proto 文件变更触发的自动化链:

protos/*.proto
    │
    ▼
protoc + grpc_python_plugin
    │
    ├── *_pb2.py          (消息类)
    ├── *_pb2_grpc.py     (Stub 类)
    └── *_pb2.pyi         (类型提示)
    │
    ▼
SDK 封装层(手写)
    │
    ├── grpc_object_type_client.py    (ObjectType 客户端)
    ├── grpc_world_client.py          (World 客户端)
    ├── grpc_action_client.py         (Action 客户端)
    └── ...共 38 个客户端

#6.2 SDK 客户端的封装模式

每个 gRPC 客户端遵循统一的封装模式:

Python
"""ObjectType gRPC 客户端封装"""
from typing import Optional, List
import grpc
from ontology_sdk.grpc_client.base_client import BaseGrpcClient
from ontology_sdk.models.object_type import (
    ObjectTypeModel,
    CreateObjectTypeRequest,
    ObjectTypeListResponse,
)
from plane_b.ontology import object_type_pb2, object_type_pb2_grpc


class GrpcObjectTypeClient(BaseGrpcClient):
    """ObjectType gRPC 客户端

    提供对 ObjectType 的 CRUD 操作,自动处理:
    - gRPC 连接管理(连接池 + 重连)
    - 认证 token 注入
    - 错误码转换(gRPC Status → SDK Exception)
    - Protobuf ↔ Pydantic 模型转换
    """

    def __init__(self, channel: grpc.Channel, metadata_provider):
        super().__init__(channel, metadata_provider)
        self._stub = object_type_pb2_grpc.ObjectTypeServiceStub(channel)

    def create(
        self,
        world_id: str,
        api_name: str,
        display_name: str,
        description: str = "",
        properties: Optional[List[dict]] = None,
    ) -> ObjectTypeModel:
        """创建新的 ObjectType

        Args:
            world_id: 所属 World ID
            api_name: API 名称(驼峰命名)
            display_name: 显示名称
            description: 描述
            properties: 属性定义列表

        Returns:
            ObjectTypeModel: 创建的 ObjectType

        Raises:
            AlreadyExistsError: api_name 已存在
            InvalidArgumentError: 参数校验失败
        """
        request = object_type_pb2.CreateObjectTypeRequest(
            world_id=world_id,
            api_name=api_name,
            display_name=display_name,
            description=description,
        )

        if properties:
            for prop in properties:
                request.properties.append(
                    self._to_property_definition(prop)
                )

        response = self._call_with_retry(
            self._stub.CreateObjectType,
            request,
        )

        return ObjectTypeModel.from_proto(response.object_type)

    def get(self, object_type_id: str) -> ObjectTypeModel:
        """获取 ObjectType 详情"""
        request = object_type_pb2.GetObjectTypeRequest(
            id=object_type_id
        )
        response = self._call_with_retry(
            self._stub.GetObjectType, request
        )
        return ObjectTypeModel.from_proto(response.object_type)

    def list(
        self,
        world_id: str,
        page_size: int = 20,
        page_token: str = "",
    ) -> ObjectTypeListResponse:
        """列出 World 下的所有 ObjectType"""
        request = object_type_pb2.ListObjectTypesRequest(
            world_id=world_id,
            pagination=common_pb2.PaginationRequest(
                page_size=page_size,
                page_token=page_token,
            ),
        )
        response = self._call_with_retry(
            self._stub.ListObjectTypes, request
        )
        return ObjectTypeListResponse.from_proto(response)

#6.3 BaseGrpcClient 的通用能力

Python
class BaseGrpcClient:
    """所有 gRPC 客户端的基类

    提供通用的:
    - 重试机制(指数退避)
    - 错误码转换
    - 元数据注入
    - 日志记录
    """

    RETRYABLE_CODES = {
        grpc.StatusCode.UNAVAILABLE,
        grpc.StatusCode.DEADLINE_EXCEEDED,
        grpc.StatusCode.RESOURCE_EXHAUSTED,
    }

    def _call_with_retry(self, method, request, max_retries=3):
        """带重试的 gRPC 调用"""
        last_error = None

        for attempt in range(max_retries + 1):
            try:
                metadata = self._metadata_provider.get_metadata()
                return method(request, metadata=metadata)

            except grpc.RpcError as e:
                last_error = e
                if e.code() not in self.RETRYABLE_CODES:
                    raise self._convert_error(e)

                if attempt < max_retries:
                    delay = (2 ** attempt) * 0.1  # 0.1s, 0.2s, 0.4s
                    time.sleep(delay)

        raise self._convert_error(last_error)

    def _convert_error(self, rpc_error: grpc.RpcError):
        """gRPC 错误码 → SDK 异常"""
        code = rpc_error.code()
        detail = rpc_error.details()

        mapping = {
            grpc.StatusCode.NOT_FOUND: NotFoundError,
            grpc.StatusCode.ALREADY_EXISTS: AlreadyExistsError,
            grpc.StatusCode.INVALID_ARGUMENT: InvalidArgumentError,
            grpc.StatusCode.PERMISSION_DENIED: PermissionDeniedError,
            grpc.StatusCode.UNAUTHENTICATED: UnauthenticatedError,
            grpc.StatusCode.RESOURCE_EXHAUSTED: RateLimitError,
            grpc.StatusCode.INTERNAL: InternalError,
        }

        exc_class = mapping.get(code, PlatformError)
        return exc_class(detail, grpc_code=code)

#6.4 38 个客户端清单

Code
38 个 gRPC 客户端按 Layer 分组:

Control Layer (Control) — 16 个客户端:
  grpc_object_type_client.py
  grpc_link_type_client.py
  grpc_property_type_client.py
  grpc_action_type_client.py
  grpc_ontology_version_client.py
  grpc_world_client.py
  grpc_world_branch_client.py
  grpc_world_merge_client.py
  grpc_auth_client.py
  grpc_rbac_client.py
  grpc_audit_client.py
  grpc_lineage_client.py
  grpc_compliance_client.py
  grpc_service_registry_client.py
  grpc_config_registry_client.py
  grpc_metric_client.py

Data Layer (Data) — 10 个客户端:
  grpc_object_storage_client.py
  grpc_link_storage_client.py
  grpc_property_storage_client.py
  grpc_time_series_client.py
  grpc_query_client.py
  grpc_aggregation_client.py
  grpc_search_client.py
  grpc_sync_task_client.py
  grpc_view_client.py
  grpc_subscription_client.py

Reasoning & Decision Layer (Intelligence) — 7 个客户端:
  grpc_rule_client.py
  grpc_rule_evaluation_client.py
  grpc_decision_client.py
  grpc_derived_property_client.py
  grpc_dependency_dag_client.py
  grpc_scenario_client.py
  grpc_reasoning_client.py

Agent Runtime Layer (Agent) — 5 个客户端:
  grpc_action_client.py
  grpc_action_template_client.py
  grpc_approval_client.py
  grpc_workflow_client.py
  grpc_agent_client.py

#7. 统一错误码体系

#7.1 错误码结构

跨所有 Layer 的统一错误码格式:

Code
错误码格式:ONTO-{Layer}-{CATEGORY}-{NUMBER}

Layer:
  B = Control Data Layer = Data Reasoning & Decision Layer = Intelligence Agent Runtime Layer = Agent Runtime
  H = SDK

CATEGORY:
  VAL = 验证错误
  AUTH = 认证/授权
  RES = 资源错误
  SYS = 系统错误
  BIZ = 业务错误

示例:
  ONTO-B-VAL-001  = Control Layer 参数验证失败
  ONTO-C-RES-003  = Data Layer 对象不存在
  ONTO-D-BIZ-007  = Intelligence Layer 规则冲突
  ONTO-E-SYS-002  = Agent Runtime 工作流超时

#7.2 gRPC Status 与 HTTP Status 映射

Code
gRPC → HTTP 状态码映射:

gRPC Code               HTTP Status    含义
─────────────────────────────────────────────────
OK                       200           成功
INVALID_ARGUMENT         400           参数错误
UNAUTHENTICATED          401           未认证
PERMISSION_DENIED        403           无权限
NOT_FOUND                404           资源不存在
ALREADY_EXISTS           409           资源已存在
FAILED_PRECONDITION      412           前置条件失败
RESOURCE_EXHAUSTED       429           限流
CANCELLED                499           客户端取消
INTERNAL                 500           内部错误
UNAVAILABLE              503           服务不可用
DEADLINE_EXCEEDED        504           超时

#7.3 错误响应格式

JSON
{
  "error": {
    "code": "ONTO-B-RES-003",
    "message": "ObjectType 'CustomerOrder' not found in World 'world-prod'",
    "grpc_code": "NOT_FOUND",
    "http_status": 404,
    "details": {
      "resource_type": "ObjectType",
      "resource_id": "CustomerOrder",
      "world_id": "world-prod"
    },
    "request_id": "req-abc-123",
    "timestamp": "2026-03-24T10:30:00Z"
  }
}

#7.4 Proto 定义的错误详情

PROTOBUF
// common/error.proto
syntax = "proto3";
package onto.common;

message ErrorDetail {
  string code = 1;            // ONTO-X-XXX-NNN
  string message = 2;         // 人类可读描述
  string request_id = 3;      // 请求追踪 ID
  int64 timestamp = 4;        // 错误发生时间
  map<string, string> context = 5;  // 上下文信息

  // 可选:重试建议
  RetryInfo retry_info = 6;
}

message RetryInfo {
  bool retryable = 1;
  int32 retry_after_seconds = 2;
  int32 max_retries = 3;
}

#8. 跨 Layer 调用链路追踪

#8.1 请求追踪设计

Code
一次 API 请求的完整调用链:

Client → Gateway → onto-control → onto-data → onto-intelligence
                                                       │
                                                       ▼
                                                  Kafka Event
                                                       │
                                                       ▼
                                              onto-data (Consumer)

每一跳都携带:
  X-Request-Id:   全局唯一请求 ID
  X-Trace-Id:     分布式追踪 ID(OpenTelemetry)
  X-Span-Id:      当前 Span ID
  X-Parent-Span:  父 Span ID
  X-World-Id:     当前 World 上下文

#8.2 gRPC Interceptor 实现

Python
class TracingInterceptor(grpc.UnaryUnaryClientInterceptor):
    """客户端追踪拦截器"""

    def intercept_unary_unary(self, continuation,
                               client_call_details, request):
        # 注入追踪上下文到 gRPC metadata
        metadata = list(client_call_details.metadata or [])

        trace_context = get_current_trace_context()
        metadata.extend([
            ("x-request-id", trace_context.request_id),
            ("x-trace-id", trace_context.trace_id),
            ("x-span-id", generate_span_id()),
            ("x-parent-span", trace_context.span_id),
        ])

        new_details = client_call_details._replace(
            metadata=metadata
        )

        return continuation(new_details, request)
Java
// Java 端的服务端拦截器
@Component
public class TracingServerInterceptor
    implements ServerInterceptor {

    @Override
    public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
        ServerCall<ReqT, RespT> call,
        Metadata headers,
        ServerCallHandler<ReqT, RespT> next) {

        String requestId = headers.get(
            Metadata.Key.of("x-request-id",
                Metadata.ASCII_STRING_MARSHALLER)
        );
        String traceId = headers.get(
            Metadata.Key.of("x-trace-id",
                Metadata.ASCII_STRING_MARSHALLER)
        );

        // 设置 MDC 上下文用于日志关联
        MDC.put("requestId", requestId);
        MDC.put("traceId", traceId);

        try {
            return next.startCall(call, headers);
        } finally {
            MDC.clear();
        }
    }
}

#9. 对比分析:Palantir vs 智策平台的 API 设计

维度Palantir Foundry智策平台
外部 APIREST (OSDK)REST (59 端点)
内部通信未公开(推测 gRPC/自研RPC)gRPC (63 proto)
API 定义部分公开 SwaggerProtobuf Contract-First
SDK 生成OSDK CLIprotoc + 手动封装
认证方式OAuth 2.0JWT + OAuth 2.0
API 版本URI 版本 (/v1, /v2)URI + Proto 包版本
限流有(未公开细节)Token Bucket 三层限流
错误码HTTP Status + JSON统一编码 ONTO-X-XXX-NNN
API 网关私有Spring Cloud Gateway

#10. 实践经验与避坑指南

#10.1 proto 设计的 5 个教训

Code
教训 1:字段命名要从 Day 1 就严格统一
  错误:有的叫 object_type_id,有的叫 objectTypeId,有的叫 type_id
  修正:全部使用 snake_case,严格遵循 {entity}_{field} 命名

教训 2:不要把 domain model 直接暴露为 proto message
  错误:proto message 和数据库表结构一一对应
  修正:proto 是 API 契约,可以与内部模型不同

教训 3:enum 的第一个值必须是 UNSPECIFIED
  错误:enum Status { ACTIVE = 0; }
  修正:enum Status { STATUS_UNSPECIFIED = 0; ACTIVE = 1; }

教训 4:分页从 Day 1 就必须是标准组件
  错误:每个 List 方法自己定义 offset/limit
  修正:统一 PaginationRequest/PaginationResponse

教训 5:预留字段编号空间
  错误:字段编号连续分配 1,2,3,4,5...
  修正:按类别分段 1-15(高频), 16-99(常规), 100+(扩展)

#10.2 Gateway 层的 3 个陷阱

Code
陷阱 1:流式 API 的 REST 映射
  问题:gRPC 的 ServerStream 无法直接映射为 REST
  方案:使用 SSE (Server-Sent Events) 或分页轮询

陷阱 2:大文件上传
  问题:gRPC 默认消息大小 4MB
  方案:大文件走独立的 REST 端点,使用分块上传

陷阱 3:WebSocket 需求
  问题:实时推送场景 REST 不够用
  方案:Gateway 提供独立的 WebSocket 端点,
        内部转换为 gRPC 双向流

#Key Takeaways

  1. Contract-First 是大型分布式平台的必选项:63 个 proto 文件是系统的唯一真相源,从 proto 生成代码而不是反过来,确保了跨语言、跨团队的接口一致性。

  2. REST 和 gRPC 不是二选一:内部 gRPC 保证性能和类型安全,外部 REST 保证兼容性和易用性,Gateway 层负责转换——两全其美。

  3. 统一的错误码和追踪体系是可观测性的基础:ONTO-X-XXX-NNN 编码让任何错误都能快速定位到具体 Layer 和类别,配合 X-Request-Id 实现端到端追踪。

#下一篇预告

S2-09 数据流全景:一条数据从采集到决策的完整旅程 —— 我们将追踪一条数据变更如何穿越整个系统:从外部数据库变更 → Flink CDC 捕获 → Kafka 传输 → 存储层写入 → Ontology Runtime 触发订阅 → 派生属性重算 → 规则评估 → 决策执行 → 审计记录,完整展示智策平台的数据流动全景。

tags: API-Design, Protobuf, gRPC, REST, Gateway, Contract-First, SDK, Error-Handling, Rate-Limiting, coomia-dip