- 后端
- API网关
- 模型推理服务
- AI Agent
【免费下载链接】semantic-router
An open, programmable decision layer for models and compute.
Semantic Router(仓库根目录 README.md)是一个开放、可编程的"模型与计算决策层"。其数据面(Router data plane)面临一个核心问题:客户端可能以 OpenAI Chat Completions、OpenAI Responses、Anthropic Messages 等多种线协议(wire format)发来请求,而最终选中的后端模型可能只原生支持其中一种协议,响应又必须回到客户端原始协议。本文围绕仓库中已落地(status: Implemented)的提案 website/docs/proposals/multi-protocol-adaptor.md 展开,系统讲解"协议中立编解码矩阵"的设计:一次解码进入中立语义、一次编码离开中立语义、codec 与路由策略严格分层,以及支撑它的能力声明、翻译规则、流式引擎、usage 结算与 18 格 E2E 验证矩阵。读完本文,你将掌握该数据面如何在不增加路由策略协议分支的前提下,为任意客户端格式与任意后端协议提供全配对翻译。
一、总体设计:一次解码、一次编码、职责三分
提案给出的数据流如下(mermaid 图来自 multi-protocol-adaptor.md):
其核心结论(Outcome)可以概括为三点:
- 一次解码、一次编码:无论客户端或后端采用何种线格式,Router 只对一份"协议中立(protocol-neutral)语义请求"做路由决策。Wire JSON 在入口(ingress)被解码一次,在 provider 边界被编码一次;响应体与流式事件沿反向路径解码/编码后返回客户端。原始 wire 对象从不进入语义路由,也就不会带着未经验证的字段穿透策略层。
- 职责三分:传输(transport)拥有监听器、连接、重试与转发;路由核心(routing core)拥有语义模型选择与请求/响应策略;codec只负责把 wire 契约映射到中立类型,不拥有任何一方的生命周期。Standalone 是默认传输;
--gateway extproc则把同一套路由核心挂接到 Envoy。 - 注册表不可变:codec 注册表(registry)在构造后不可变;新增一种线格式只需新增一对 buffered/streaming codec、声明能力并补齐矩阵测试,路由策略不会因为新增 codec 而长出协议分支。
仓库实现中,这一分工落在两个 Go 包上:pkg/llmprotocol(中立语义契约)与pkg/protocolcodec(线协议编解码引擎),详见 src/semantic-router/pkg/protocolcodec/engine.go 与 src/semantic-router/pkg/protocolcodec/registry.go。
二、中立契约(Neutral contract):pkg/llmprotocol 是什么
pkg/llmprotocol是 codec 与 Router 策略共享的唯一语义契约(src/semantic-router/pkg/llmprotocol/types.go 包注释明确写道:"Wire JSON belongs to codecs; it must not leak into this package.")。它表示:
- 有序的 instructions、messages 与多模态内容块(
Content按Kind闭合,区分text、image、audio、video、file、tool_call、tool_result、reasoning,以及独立的generated_image); - 工具定义、工具调用、工具结果、托管式图像生成控制(hosted image-generation)与 tool choice(
auto/none/required/named/image_generation); - 采样与结构化输出约束(
Sampling中的 temperature、top_p、top_k、min_p、repetition_penalty、max_output_tokens、seed、penalties、stop 序列;OutputFormat的text/json_object/json_schema); - reasoning 控制与 reasoning 内容(模式
enabled/disabled/adaptive、effort、budget、display,以及区分完整文本与摘要的ReasoningScope); - 响应备选(
Alternatives)与停止原因(StopReason,共 9 种枚举); - provider 请求身份与类型化传输错误(
TransportError与ProtocolError); - 带 provenance 的 token usage——标准输入、缓存读、缓存写、reasoning 输出、其他输出与合计;
- 受信任的传输元数据(
TrustedMetadata:namespace/actor/subject/session/agent/task/turn/correlation 标识),codec 不能从客户端输入填充这些字段。
值得强调的边界:原始 wire 对象不进入语义路由。Envelope虽然可以保留有界的同格式表示细节(src/semantic-router/pkg/llmprotocol/policy.go 定义其为 bounded、ephemeral 的 wire fidelity 状态,且注释明确禁止它被序列化进日志、快照、YAML 或 usage 记录),但它不是路由状态,不能用于绕过校验。
从实现看,中立契约对"缺省值显式化"也很讲究:applyRequestSemanticDefaults会在解码后把"有工具就默认tool_choice=auto"这一 provider 文档化缺省显式写入中立请求(engine.go),从而避免路由插件因客户端是否显式写出缺省而行为不同。
三、Codec 契约与不可变注册表
3.1 每个 codec 要做什么
每个注册的 codec 都是无状态、可并发安全的,声明自己的线格式与能力,并实现四个缓冲操作(文档列出的四项):
- 把请求解码为中立请求(
DecodeRequest); - 把中立请求编码给后端(
EncodeRequest); - 把响应解码为中立响应(
DecodeResponse); - 把中立响应编码给客户端(
EncodeResponse)。
在 registry.go 中,这四个操作对应MessageCodec接口,此外还有独立的TransportErrorCodec(DecodeTransportError/EncodeTransportError)。传输错误使用独立的类型化契约,因为 HTTP 错误不是失败的模型响应资源——这一点在 pkg/llmprotocol/errors.go 中有明确注释:TransportError代表非 2xx API 响应,而Response.Error是模型生成结果(例如 Responses 资源 status 为failed)。
流式 codec 创建请求作用域(request-scoped)的 decoder/encoder,二者交换中立事件(stream.go 的StreamDecoder.Push/Finalize与StreamEncoder.Push/Finalize);注册表中不存任何可变流状态。
3.2 注册表构建:不可变、强制无状态
Registry构造后不可变,Resolve从不暴露底层 map(registry.go)。NewRegistry会强制校验:拒绝 nil codec;拒绝Stateless() == false的 codec(因为一个注册 codec 可能被并发调用);格式标识必须匹配^[a-z][a-z0-9._-]{0,127}$;每个格式必须实现流式;拒绝重复注册;拒绝空注册表。内建注册表由四种 codec 组成:
NewRegistry(OpenAIChatCodec{}, OpenAIResponsesCodec{}, AnthropicMessagesCodec{}, ImagesCodec{})(见 NewBuiltinRegistry。)其中openai.images.v1是 sink 方言:Responses 的托管图像生成请求会被重编码到/v1/images/generations,生成的图像再解码回中立GeneratedImage输出项(types.go)。
3.3 能力声明:编码前显式检查
能力(capability)以不可变位集(bitset)形式声明(src/semantic-router/pkg/llmprotocol/capabilities.go),覆盖文本、图像/音频/视频/文件输入输出、工具与并行工具、reasoning(含 adaptive/signature/display/budget)、结构化输出(JSON object / strict schema / strict tool schema)、流式、缓存记账、权威 usage、多候选、采样控制(top_k/min_p/seed/penalties/stop)、缓存指令与隔离、对话状态、自定义工具、matched stop sequence、图像生成、文本 verbosity 等 40+ 项。RequiredCapabilities(request)会从请求中推导所需能力集(例如请求含 strict tool schema、自定义工具、reasoning 或图像生成时分别置位),而能力检查发生在编码之前(RequireCapabilities在EncodeRequest/EncodeResponse/TranslateRequest中均在 encode 前调用,见 engine.go),使不支持的 tools、媒体、多候选、严格 schema、reasoning 或流式行为变得显式、可测试。
四、支持矩阵与翻译规则
4.1 支持矩阵
| Wire format | Buffered request | Buffered response | Streaming | Tools | Images | Structured output | Usage |
|---|---|---|---|---|---|---|---|
| OpenAI Chat Completions | Decode and encode | Decode and encode | SSE decode and encode | Yes | Input | JSON object and schema | Authoritative when present |
| OpenAI Responses | Decode and encode | Decode and encode | Event decode and encode | Yes | Input plus hosted image-generation lifecycle | JSON schema | Authoritative when present |
| Anthropic Messages | Decode and encode | Decode and encode | Event decode and encode | Yes | Input | Supported schema subset | Authoritative when present |
完整的两两配对矩阵(pairwise matrix)——请求、响应、传输错误、流式——都被测试覆盖(matrix_test.go 对三种内建格式两两执行TranslateRequest后再用目标格式解码回验)。请求可以以任意受支持的客户端格式进入,以所选 provider 模型声明的格式离开;响应则回到客户端原始格式。
4.2 翻译是语义翻译,不是字段复制
翻译规则(Translation rules)是文档中的关键约定:
- 具有等效中立含义的字段被保留;
- 目标不支持的字段以类型化
unsupported_feature错误失败; - 未知字段只允许在未修改的同格式(same-format)缓冲往返中存活;
- 跨格式翻译与语义变更(semantic mutation)会拒绝未知字段,而不是静默丢弃;
- 请求与响应的
Generation在语义变更后递增; - 诊断是有界的,通过既有 Router 可观测性契约返回;
- codec 从不抓取 URL、解析文件、认证调用者或调用 provider。
引擎层通过translationDecodePolicy落实这一规则(engine.go):只有当UnknownFields == UnknownPreserveSameFormat且源格式等于目标格式且未被变更时,才允许保留未知字段;一旦跨格式或存在 mutation,立即切换到UnknownReject+SourceDisabled。DecodeRequestForMutation则是 Router 入口使用的严格契约——模型选择与策略插件必然会在解码后修改请求,因此入口绝不允许接受会被静默擦除的不透明字段。TranslateTransportError特意不走DecodeResponse:失败的模型 Response 资源与 API 传输错误是两种 wire 对象(engine.go)。
类型化错误类别在 pkg/llmprotocol/errors.go 中枚举:invalid_request、authentication、permission、not_found、conflict、unsupported_feature、rate_limited、upstream_unavailable、upstream_timeout、internal。ProtocolError支持Unwrap以透传Cause。嵌套字段的拒绝行为由测试固化,例如 official_schema_contract_test.go:Chat message name、prompt cache breakpoint、Responses defer_loading/reasoning mode、Anthropic eager tool 等嵌套字段均须返回ErrorUnsupportedFeature。
五、流式引擎:事件先解码、终结唯一、usage 结算
5.1 中立事件与请求作用域
每个 provider 事件在进入策略或客户端编码前,先被解码为中立事件(stream.go 的EventType:response.started、output.item.started、output.text.delta、output.reasoning.delta、tool.call.delta、image_generation.progress、output.item.completed、usage.updated、response.completed、response.failed、provider.opaque)。事件带序号(Sequence)、内容索引(ContentIndex,独立于 ItemIndex)与失败作用域(FailureTransport/FailureResponse)。分片网络帧由 wire decoder 缓冲——Router 策略从不解析不完整的 JSON 或 SSE 记录。
StreamEngine(engine.go)强制:
- 终结状态唯一:
response.completed或response.failed出现后流即 terminal,再次 Push 返回stream_terminal冲突错误; - 成功终结延迟发布(deferSuccessfulTerminal):provider 的语义成功事件被暂存,直到 HTTP body 干净地到达流尾——否则"终结帧 + 后续分片中的畸形残片"可能在尾部字节被校验前就发布成功(engine.go);
- 有界诊断:诊断数量被
Limits.Diagnostics截断,超限时写入diagnostic_truncated; - 最终 usage 结算:流终止时,最终 provider usage 替换中间估计值。
流式场景没有同格式字节级重放路径,因此strictStreamPolicy会拒绝未知字段——接受未知字段就意味着静默丢弃 provider 语义(engine.go)。
5.2 图像生成的生命周期状态机
托管式图像生成复用同一流引擎。输出项从in_progress开始,进度可经generating与有序部分图像推进,最终恰好一次以completed或failed结束。以下情况会在客户端成功终结发布前失败:向后转换(backward transitions)、稀疏的部分图像索引(sparse partial indexes)、冲突的终结状态、以及进度事件上携带结果数据(result data on a progress event)。图像生成测试与 fixture 覆盖了每个已发布选项、null结果与空 payload 的区分、有序进度、连续的部分图像索引、终结成功/失败、畸形 base64 与目标能力拒绝(见 src/semantic-router/pkg/protocolcodec/image_generation_test.go)。
5.3 Router 自产响应不走 provider 形状
Router 自产响应(如拒绝、诊断、合成回复)通过NewEventStreamEncoder直接用中立事件编码到公开线格式(engine.go),不构造中间 provider 形状的流。取消与背压(backpressure)跟随所选传输的请求生命周期。编码器同样受能力检查约束(RequiredEventCapabilities),如 target 无法表达 matched stop sequence 或 custom tool,事件会以类型化错误失败。
六、Usage 与成本:provenance 优先,pricing 是部署元数据
6.1 Token 计数保留 provenance
Token 计数是带指针的(Value *int64),使"缺省"与"权威零值"可区分;Provenance区分authoritative/derived/estimated/unknown(types.go)。这样记账(accounting)可以区分权威 provider usage 与派生、估算或不可用的值。Usage结构体覆盖标准输入、缓存读、缓存写、reasoning 输出、其他输出、输入合计、输出合计与总计。
6.2 定价留在 provider model 上
定价是部署元数据,挂在每个 provider model 上,而routing.modelCards只描述语义能力,不拥有连接或定价数据(src/semantic-router/pkg/catalog/types.go 的Pricing结构与此对应):
providers: models: - name: local/fast pricing: currency: USD prompt_per_1m: 0.20 cached_input_per_1m: 0.02 cache_write_per_1m: 0.25 completion_per_1m: 0.80规则:货币可选,省略时按 USD 记账;配置的费率必须有限且非负;显式 0 表示免费费率。语义能力(modelCards)与连接/定价数据的分层,使得能力声明可复用于路由决策,而定价只服务于成本结算。
七、安全边界:可信元数据只能由传输填充
- 客户端可控的 header 与 body 元数据一律不可信。只有经过认证的传输边界才能填充 trusted identity / session / task / correlation 字段(
TrustedMetadata),且必须在传输层确立这些字段之后; - codec 不能把 wire 元数据提升为 trusted 元数据——中立请求的
Trusted字段(types.go)注释明确:"populated by the Router after transport authentication. Codecs never populate trusted fields from client headers"; - 公共推理监听器与管理监听器保持分离;codec 层本身不认证调用者、不代理请求、不管理模型 worker——这些分别是前端与运行时的职责。
八、验证契约:schema 钉扎 + fixture 语料 + 18 格 E2E
8.1 Schema 契约钉扎到上游修订版
模式契约钉扎到已发布的上游修订版:OpenAI OpenAPI690521b1753dce0c6d6b275f583d22537679cff9、Anthropic SDKd19dea9ed85bbb5fdb2d6f20fb6f903920ed23fa。测试将每个顶层字段、嵌套对象字段与已发布的 union 判别符闭合到显式的语义(semantic)、仅传输(transport-only)、扩展(extension)或不支持(unsupported)处置类别。顶层请求/响应清单对已发布的 OpenAI OpenAPI schema 与生成的 Anthropic Messages 类型封闭——新发布的字段不能穿过未类型化的 JSON 桶:它必须映射到中立契约、作为显式不支持特性失败、或被记录为有意从客户端表示中省略的有界 provider 元数据(相关测试见 official_schema_contract_test.go 与 official_schema_inventory_test.go)。
8.2 E2E provider 模拟器与 Go 一致性测试
E2E provider 模拟器属于同一契约:其原生 Chat、Responses、Messages 边界使用修订钉扎的闭合清单,对未知顶层字段以 provider 原生错误拒绝,并在无未类型化规范化步骤的情况下保留嵌套 wire 对象。Go 一致性测试 provider_simulator_contract_test.go 把模拟器清单(schema_contract.json,含schema_revision、官方/扩展请求响应字段、usage 字段与 required 字段)与 codec 的请求/响应/usage wire 清单直接比对。模拟器测试重放每个已发布顶层请求字段,codec goldens 闭合嵌套字段与 union 判别符——这防止"宽松的 mock 隐藏 ExtProc 丢弃或发明的字段"。
8.3 人类可读 fixture 约定
fixture 采用稳定的输入/输出命名约定:
NNN-{client-protocol}-{case}-in.json NNN-{client-protocol}-{case}-{backend-protocol}-out.json每个请求、响应、传输错误、流、能力边界与类型化拒绝输入,对每种内建目标协议恰好有一个期望输出。流 fixture 保留精确的 SSE transcript,并逐字节重放一次,以证明传输 chunk 边界不改变语义(golden 测试见 golden_matrix_test.go)。语料覆盖:畸形与截断 JSON、重复字段、非法 union 与枚举、有序多模态内容、工具与工具结果、结构化输出、reasoning、usage、取消、超时、不完整流、流中途失败、身份变更、序列违规、托管图像生成与资源限制。
8.4 部署级 18 格矩阵
部署级覆盖是必需的 18 格矩阵:
3 client protocols x 3 native backend protocols x 2 modes = 18 E2E cells每个格穿越 Envoy 与 ExtProc:验证客户端原生缓冲信封或 SSE 生命周期,要求翻译后的输出带确定性后端标记,并拒绝泄漏的后端 wire 形状。另有附加 E2E 契约覆盖每个后端协议的结构化输出、缓冲 provider 错误、工具调用延续、不完整流与部分输出后的错误。--gateway extproc与 standalone 两种模式的差异与选用见 website/docs/installation/k8s/gateways.md(Helm chart 中gateway.mode: extproc时 Router 在 50051 端口服务 ext_proc gRPC)。
九、扩展清单:新增一种线格式要做什么
一个新 codec 只有在提供以下全部内容时才算完成:
- 稳定的 wire-format 标识符(匹配
^[a-z][a-z0-9._-]{0,127}$); - 显式的能力声明(capability declaration);
- 严格的 buffered 请求、响应与传输错误 codec;
- 请求作用域的流解码与编码(request-scoped stream decoding and encoding);
- 畸形输入与不支持特性的测试;
- 对每种内建格式的两两矩阵覆盖;
- 权威 usage 与终结事件测试;
- ExtProc 回归覆盖,证明路由行为不变。
由于注册表不可变、能力检查前置、未知字段策略严格,扩展者不需要触碰任何路由策略代码——这正是该设计"策略无协议分支"的工程回报。
十、关键文件导航
- 提案原文:website/docs/proposals/multi-protocol-adaptor.md
- 中立语义契约:src/semantic-router/pkg/llmprotocol/types.go、src/semantic-router/pkg/llmprotocol/errors.go、src/semantic-router/pkg/llmprotocol/capabilities.go、src/semantic-router/pkg/llmprotocol/policy.go、src/semantic-router/pkg/llmprotocol/stream.go
- 编解码引擎与注册表:src/semantic-router/pkg/protocolcodec/engine.go、src/semantic-router/pkg/protocolcodec/registry.go
- 各线格式 codec:
codec_openai_chat.go、codec_openai_responses.go、codec_anthropic_messages.go、images_codec.go(均位于 src/semantic-router/pkg/protocolcodec) - 矩阵与契约测试:matrix_test.go、official_schema_contract_test.go、provider_simulator_contract_test.go、golden_matrix_test.go
- 定价元数据:src/semantic-router/pkg/catalog/types.go
- 相关文档:Router API、Semantic Router 系统总览、Kubernetes 网关部署选项
- 后端
- API网关
- 模型推理服务
- AI Agent
【免费下载链接】semantic-router
An open, programmable decision layer for models and compute.
相关推荐
Operit OpenCode Provider 路由隔离:多协议请求边界与专用适配实现解析
Operit OpenCode Provider 路由隔离:多协议请求边界与专用适配实现解析 导读 本文基于 Operit 仓库的 docs/TODO/open
AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆GUI 自动化Pixie 的 PostgreSQL Wire Protocol 解析器:从 v3 协议消息到 pgsql_events 观测数据
Pixie 的 PostgreSQL Wire Protocol 解析器:从 v3 协议消息到 pgsql_events 观测数据 本文深入剖析开源可观测性平台
可观测性云原生btcd wire 包深度解析:用 Go 实现比特币点对点线上协议(Wire Protocol)
btcd wire 包深度解析:用 Go 实现比特币点对点线上协议(Wire Protocol) btcd 的 wire 子包(仓库路径 wire/ https
区块链
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考