跳转至

ADR-0002: 业务 = REST/SSE; gRPC = 观测面

  • Status: Accepted (方案 A 修正版落地)
  • Date: 2026-04-26
  • Deciders: PM (产品经理) + Flyto Agent core team
  • Related code: platform/common/internal/server/, platform/common/cmd/common/main.go, platform/common/internal/api/grpc/proto/{health,safetychain}.proto, deploy/docker-compose.yml, deploy/Caddyfile
  • Related TODO: core/TODO.md L692 (登记 + 完成同日, 2026-04-26); 衍生 tracked debt L693 / L694 / L695
  • Commit chain: 01f08e7 (C1) → 8189d05 (C2) → 694bd07 (C3) → e1db327 (C4) → c35e761 (C5) → C6 (本 ADR)
  • Cross-reference: ADR-0001 (反向思维 gate); memory feedback_architecture_principle_over_rule_of_two.md (gRPC 优 HTTP 是 SafetyChain 通路铁律)

1. 背景 / Context

platform/common/internal/server/server.go 早期写好了 1263 行业务 REST/SSE 实现 (SSE 流式 / CORS / Bearer auth / IP 限速 / 15s 心跳 / 会话 CRUD / panic recovery / request-id 中间件), 但 cmd/common/main.go 没启动它. 注释明说 "REST/SSE 仍由 internal/server 管理, 目前和本二进制正交; 未来 REST 侧需要同 一套 auth/tenant 接线时, 再并入这里." -- 即计划好了等连, 还没并入.

PM 在 L569 反事实工作流缩水后提新方向: 消费者语言无关, 引擎自身无需为特殊 语言频繁优化. 论据是: logistics platform 是 C#, 但未来可能加 Java/Python 等其他行业 platform. 如果业务通道走 gRPC, 每加一种语言就要 protoc gen + 维护 stub, 每个新行业要复制业务 RPC. 走 REST 则任何 HTTP 客户端通用.

但 9 天前 (2026-04-17) PM 拍板的 SafetyChain 方向是 "C# 走 gRPC 优于 HTTP" (memory feedback_architecture_principle_over_rule_of_two.md 记载). 这个 决定基于: SafetyChain 是 platform/common 内部观测面, 给 C# logistics 拉 verdict / breaker 状态, 是后台到后台的强类型 RPC, gRPC 适配.

两个方向看似矛盾. 本 ADR 的核心工作就是阐明: 它们不冲突, 是 bifurcation (分层) 不是 U-turn (反悔).


2. 决策 / Decision

采纳方案 A 修正版 (激活 server.go + 改造接 OIDC/tenant/safetychain wire + 不加 grpc-gateway + 立 ADR), 拒绝原方案 A 完整版 (含 grpc-gateway 给观测 RPC 加 REST 镜像).

2.1 业务通道 = REST/SSE (/api/v1/* → common:8080)

  • 引擎消费层 (engine.Run / engine.Session) 唯一业务通道走 HTTP/1.1 + SSE
  • 端点形态 (server.go:286+ 已实现): GET /api/v1/health / POST /api/v1/agent/run (SSE 流式) / POST /api/v1/sessions / GET|DELETE /api/v1/sessions/{id} / POST /api/v1/sessions/{id}/messages / POST /api/v1/sessions/{id}/permissions/{request_id}
  • 鉴权: OIDC bearer token via auth.HTTPMiddleware, 与 admin HTTP 共用同一 套 Verifier (commit 2 把 raw shared-secret BearerToken 删除, 不再两套)
  • streaming: SSE text/event-stream (text_delta / tool_use / done 等 named events), W3C 标准, 浏览器 EventSource 原生支持
  • 任何语言任何 HTTP 客户端都能消费, 不需要预先 codegen stub
  • C# logistics platform 业务调用走 HttpClient (Refit / RestSharp 强类型 绑定), 不需要写业务 gRPC stub

2.2 观测通道 = gRPC (:9090)

  • SafetyChainService (ListVerdicts / ListBreakerStates) + HealthService 仅 gRPC. 这两个是 9 天前 (2026-04-17) PM 拍板的内部强类型观测 RPC, 沿用 不变
  • 不再加任何业务 gRPC RPC. C# logistics 后续业务调用走 REST/SSE
  • 观测的 HTTP 镜像由 admin/server.go 单独实现 (/admin/safetychain/verdicts
  • /admin/safetychain/breakers), 不引 grpc-gateway 重复一份

2.3 Tool 级安全链装饰是行业 platform 责任

  • cmd/common 起 REST listener 后自动调 safetychain.Assemble 装饰 Tool. 一刀切 AlwaysApprove + DefaultExtractor 会把所有 Tool 锁同一组合 或 fan out 到不匹配 Tool 语义的 wrap.
  • common 保持纯 transport. verdictStore + breakerRegistry 接线就绪, 由行业 驱动代码 (logistics 等) 在 Tool 注册时调 safetychain.Assemble 装饰并 写数据. v0.3 阶段 verdictStore 仍然空 (C# logistics 的业务 Tool wire 还没起步).

2.4 端口拓扑

cmd/common 进程
├ :9090  gRPC          — Health + SafetyChain (compose 内部, 不映射 host)
├ :8081  admin HTTP    — /admin/health, /admin/version, /admin/tenant,
│                        /admin/safetychain/{verdicts,breakers} (opt-in)
└ :8080  business REST — /api/v1/agent/run (SSE), /api/v1/sessions/*, /api/v1/health
                         (新增, 由 --rest-addr 启用)

Caddy 边界: hub.flytoex.net/api/v1/* → common:8080 (flush_interval -1 + response_header_timeout 0); /admin/* → common:8081; 其他 → logistics:8080.


3. 评估流程 / Evaluation Process

3-agent 并行 review (memory feedback_agent_teams_review_mode.md 模式), 三方 独立产出报告后主线程 reconcile, PM 二次拍板.

3.1 调研 agent (业界证据)

调研 11 个主流 LLM Agent / SaaS 框架的 transport 选型:

框架 业务 transport streaming 双通道?
Anthropic Messages REST SSE (named events) 无 gRPC
OpenAI Chat / Assistants REST SSE 无 gRPC
AWS Bedrock REST binary application/vnd.amazon.eventstream 无 gRPC
Google Vertex AI gRPC + REST mirror gRPC streaming 同源 proto transcoding
Cohere / Mistral / Replicate / Together / Fireworks REST SSE 无 gRPC
Ollama REST NDJSON N/A 单进程
LM Studio REST (OpenAI 兼容) SSE N/A
LangServe REST (FastAPI auto) SSE / chunked HTTP 无 gRPC

业界共识: 10 / 11 公开 REST/SSE, 仅 Vertex 用 gRPC. Vertex 是 Google 内部全栈 gRPC 的延伸, 不仿照. streaming 共识: SSE 是 LLM 子领域共识 (Ollama / Bedrock 是少数派但仍是 HTTP/1.1 chunked, 不是 gRPC).

双通道 (业务 REST + 观测 gRPC) 在 LLM 圈零先例, 但与 LLM 主流 (REST 业务) 对齐. 唯一合法 prior art 是 GCP transcoding (一份 proto 同时长出 gRPC + REST), 不是 etcd/K8s 的反向 (gRPC 主, REST 副).

grpc-gateway 调研: v2.29.0 (2026-04-16) 健康活跃, 19,846 stars, etcd / Istio 生产案例. 给我们场景 (SafetyChainService 加 REST 镜像) 价值窄: admin / server.go 已实现 handleSafetychainVerdicts + handleSafetychainBreakers REST handler, 重复.

gRPC-Web 现状: Google 官方 grpc-web 仍要 Envoy. ConnectRPC 是 2024-2025 转向的现代替代 (Buf 自家替换 grpc-web 为 connect-web), 不需要 Envoy. 但 Flyto 没有浏览器前端需求, 这一项不影响本决策.

3.2 质疑 agent (6 道硬质疑, 4 个 blocker)

Q 严重程度 关键发现
Q1: SSE streaming/性能坑 严重 Caddy proxy buffering / Chrome 6-conn / 1000 单/s 带宽 / mobile NAT — Q1.1 上线 100% 踩坑须 Caddyfile flush_interval -1
Q2: auth 双份维护 致命 server.go 用 raw BearerToken ConstantTimeCompare, 不走 OIDC. 接进 cmd/common 后跑两套不兼容 auth. 必须 commit 2 单独修
Q3: 端口/mux 整合 严重 同端口分路径 vs 双端口, 没拍板必返工. 推荐双端口
Q4: grpc-gateway 价值 致命 admin/server.go 已有 /admin/safetychain/{verdicts,breakers} REST handler, 加 grpc-gateway 是把同一表面再做一次. 砍
Q5: ADR-0002 立硬规则 严重 与 9 天前 "gRPC 优 HTTP" 看似对冲. 实为分层. ADR 须显式 cross-reference
Q6: rollback / migration 严重 server.go in-memory sessions 单实例假设, k8s 多副本立刻挂. ADR 须显式标注限制

3.3 设计 agent (4 备选 + 推荐方案 A 修正版)

方案 范围 commit 数 优点 代价
A 完整版 激活 + grpc-gateway 镜像 + ADR 8-10 观测 RPC 双通道 与 Q4 冲突, admin 已有 REST handler 重复
A 修正版 激活 + 不加 gateway + ADR 6 3-agent 共识 server.go 改造工作量 5 处替换非 trivial
B 只激活不加 gateway 不立 ADR 同 A 修正版减 ADR 5 最快 失去架构原则记录, 下次再有人想加业务 RPC 又要重新讨论
C 重写 server.go 不复用 1263 行 10+ 干净起步 浪费已踩坑过的 SSE/permission 桥接/限速实现
D 全 gRPC 化业务 与 PM 方向冲突 12+ C# 强类型 stub LLM 圈零先例, sanity check 通过即砍

设计 agent 自行砍掉 grpc-gateway (与 Q4 共识), 推荐 A 修正版.

3.4 PM 拍板与 reconcile

主线程 reconcile 3 份报告, 关键结论: 1. grpc-gateway 砍 (3 方共识, admin 已有 REST handler) 2. ADR-0002 立, 但定性 bifurcation 不是 U-turn — 9 天前 SafetyChain 走 gRPC 是观测面方向, 现在仍成立; PM 新拍 "业务用 REST" 是补另一面 3. server.go 改造工作量 = 5 处替换 (auth OIDC / 删 provider 写死 / 加 tenant / 接 safetychain wire / 拆 signal), 6 commit 节奏对 4. 双端口拓扑 (业务 :8080 + admin :8081 + gRPC :9090) 5. Caddyfile flush_interval -1 必须显式 (Q1.1 缓解) 6. 4 tracked debt 登记: in-memory sessions / cross-transport request-id / SSE 带宽 / Swagger 与 L407 关联

PM 拍板 A 修正版后开干. 6 commit 节奏完整落地.


4. 后果 / Consequences

正面

  • 与 LLM 业界 10/11 主流 (REST/SSE 业务通道) 对齐, 任何 HTTP 客户端通用
  • 浏览器 EventSource 原生支持, 未来加 Web dashboard 零代理依赖
  • C# logistics 不需要为业务调用写 gRPC stub, HttpClient (Refit / RestSharp) 即用; 后续加新行业 (Java / Python) 同样不需要 codegen
  • OIDC 鉴权跨 gRPC + admin HTTP + 业务 REST 同一套 Verifier, dev 模式 Verifier=nil 跳过与 admin 一致
  • SafetyChain / Health gRPC 观测面保留, 沿 9 天前方向不破坏
  • common 保持纯 transport, Tool 级 SafetyChain 装饰由行业 platform 自决 + 自填 Validator + extractor, 避免 cmd/common 一刀切锁死单一组合

负面

  • 失去 gRPC 强类型契约, REST 协议靠 OpenAPI/Swagger 文档维护 (L407 直接 子项)
  • SSE framing 比 gRPC binary 多 5-10x 带宽 overhead (Q1.3); 1000 单/s 上量后跨 region 流量成本要量化 (L695 monitoring)
  • in-memory sessions 单实例假设, k8s 多副本要换 SessionStore (L693)
  • gRPC + REST cross-transport request-id / trace 串通要 OpenTelemetry 投入 (L694)

中性

  • ADR-0002 是 "当前阶段默认 + 决策上下文", 不是硬规则. 触发 § 6 重评估 条件可改方向

5. 替代方案保留 / Alternatives Preserved (CLAUDE.md 原则 5)

方案 A 完整版 (含 grpc-gateway)

如果未来 SafetyChain 观测 RPC 加到 5+ 个且 admin/server.go 手写镜像维护 负担显现, 可以从 A 修正版升级到 A 完整版. 升级路径:

  1. SafetyChainService.proto 加 option (google.api.http) = { ... } 注解
  2. Makefile 加 protoc-gen-grpc-gateway / protoc-gen-openapiv2 step
  3. cmd/common 加 grpc-gateway runtime.ServeMux, 路径 /api/observe/*
  4. admin/server.go 现有 REST handler 视情况 deprecate 或保留作为 fallback

预期升级成本: ~300 行 Go (含 generated code) + 1 个新依赖 (grpc-ecosystem/grpc-gateway/v2). 触发条件: 见 § 6.

方案 D 全 gRPC 化业务 + grpc-gateway 反向产 REST

与 PM 拍板的方向冲突 (REST 业务). 列出来作 sanity check, 已 PM 二次确认 不走. 触发重评估: 见 § 6.

不立 ADR (方案 B)

放弃记录架构原则的代价是: 下次有人想加业务 RPC 时又要重新讨论. 已选立 ADR.

重写 server.go (方案 C)

放弃 1263 行已踩坑过的 SSE / permission 桥接 / 限速实现, 浪费. 已 PM 拍板复用.


6. 触发重新评估的条件

未来出现以下任一情况, 应重新评估 ADR-0002 的决定:

  1. SSE 性能瓶颈实测: logistics platform 上量后, SSE 跨 region 带宽 成本超过 gRPC streaming 5x (Q1.3 数学预测), 且 L695 monitoring 数据 支持
  2. 第二个非 C# 行业 platform 出现 gRPC 强需求: 如银行业 ERP 要求"高频 RPC 必须 gRPC + mTLS 过合规审计", rule of two 满足后 重评估业务 transport
  3. 业界共识改变: Anthropic / OpenAI / Bedrock 任一在自家公开 API 引入 gRPC 业务通道并稳定运行 6 个月以上
  4. 观测 RPC 数量爆炸: SafetyChain 加到 5+ RPC 且 admin/server.go 手写镜像维护负担显现, 触发 § 5 方案 A 完整版升级
  5. grpc-gateway 生态激进变化: ConnectRPC 全面取代 grpc-gateway 后, 升级 路径要重写 (但当前 ConnectRPC 与 Flyto 无浏览器前端需求无关, 不影响)

满足任一即可重新走 3-agent review 评估升级 / 切方向.


7. 参考链接

业界 LLM API 文档

gRPC / 替代方案

项目内部参考

  • core/docs/adr/0001-reverse-thinking-gate.md — ADR 体例参考
  • ~/.claude/projects/-home-admin-ccm/memory/feedback_architecture_principle_over_rule_of_two.md — gRPC 优 HTTP 是 SafetyChain 通路铁律 (本 ADR 的 cross-reference 论据)
  • platform/common/internal/server/server.go — 业务 REST/SSE 实现 (1263 行)
  • platform/common/cmd/common/main.go — 三 listener wire (gRPC + admin HTTP
  • 业务 REST)
  • platform/common/internal/admin/server.go — admin 观测面 (含 /admin/safetychain/* REST handler, 复用论据见 § 2.2)
  • platform/common/internal/auth/middleware.go — auth.HTTPMiddleware, 跨 admin + 业务 REST 共用
  • deploy/docker-compose.yml + deploy/Caddyfile — 端口拓扑 + Caddy 路由

8. 修订记录

日期 版本 变更
2026-04-26 v1.0 初稿. 方案 A 修正版落地 6 commit, ADR 起草