第 3 章 · 01 SPEC §3.1 Provider interface 与 Facto...


文档摘要

第 3 章 · 01 SPEC §3.1 Provider interface 与 Factory 注册表 本节摘要:本节精读 SPEC §3.1 的 Provider 抽象。Provider 是一个极简 interface——只有 与 两个方法; 是 类型别名; 把 Factory 注入进程全局注册表, 按 kind 实例化。整套抽象用几十行 Go 把"核心只懂接口、具体按名解析"变成可运行代码。本节还讲 Request/Chunk 数据类型、 的 OpenAI 兼容实现如何在 注册 kind、 与 两个兄弟实现,以及"子包 import 父包自注册、父包永不 import 子包"在 Provider 上的落地。

第 3 章 · 01 SPEC §3.1 Provider interface 与 Factory 注册表

本节摘要:本节精读 SPEC §3.1 的 Provider 抽象。Provider 是一个极简 interface——只有 Name() stringStream(ctx context.Context, req Request) (<-chan Chunk, error) 两个方法;Factoryfunc(Config) (Provider, error) 类型别名;Register(kind, Factory) 把 Factory 注入进程全局注册表,New(kind, cfg) 按 kind 实例化。整套抽象用几十行 Go 把"核心只懂接口、具体按名解析"变成可运行代码。本节还讲 Request/Chunk 数据类型、internal/provider/openai/ 的 OpenAI 兼容实现如何在 init() 注册 "openai" kind、anthropicresponses 两个兄弟实现,以及"子包 import 父包自注册、父包永不 import 子包"在 Provider 上的落地。

内容来源:原项目源码 internal/provider/provider.go(Provider/Factory/Register/New/Request/Chunk 定义)、internal/provider/openai/openai.go(init() 注册)、docs/SPEC.md §3.1 与 §4。

⚠️ 注意:Provider 是"vendor endpoint"——一个 base_url + api_key_env 暴露一个或多个 model。不要把 Provider 等同于"模型";一个 provider 可挂多个 model(通过 models = [...] 列表)。

学习目标

阅读完本节,你应当能够:

  1. 默写 Provider interface 的两个方法签名。
  2. 解释 Factory 为何是类型别名而非 interface。
  3. 说清 RegisterNew 的配合,以及 Register 为何在 init() 调用。
  4. 读懂 Request/Chunk 的字段,区分 ChunkType 的九种取值。
  5. 描述 internal/provider/openai/ 如何在 init() 注册 "openai"
  6. 区分 openaianthropicresponses 三种 kind 的用途。

一、Provider interface:两个方法

SPEC §3.1 给出 Provider interface(docs/SPEC.md:67-70,对应 internal/provider/provider.go:970-977):

type Provider interface { Name() string Stream(ctx context.Context, req Request) (<-chan Chunk, error) }

只有两个方法:

  • Name() string:返回 provider 实例名(如 "deepseek""mimo"),用于日志、default_model 解析、错误归因。
  • Stream(ctx, req) (<-chan Chunk, error):发起一次流式补全,把增量推到 channel;取消 ctx 必须中断底层请求,channel 关闭标志着补全结束。

为什么 interface 这么薄?因为 SPEC §1.1"核心只懂接口"——核心不需要知道 provider 怎么实现 HTTP、怎么处理鉴权、怎么解析 SSE。核心只要"给我一个名字、给我一个流"。所有具体细节都封装在实现里。

internal/provider/provider.go:971-976 的注释还强调:

// Stream starts a streaming completion, pushing increments on the channel. // Cancelling ctx must abort the underlying request; a closed channel marks // the end of the completion.

ctx 取消必须中断底层请求(否则 Ctrl-C 杀不掉在飞的 HTTP),channel 关闭标志着补全结束(无论是正常完成、出错、还是被取消)——这是 Provider 与 Agent 之间的硬契约。

二、Factory:类型别名

紧接 interface,SPEC 定义 Factory(docs/SPEC.md:72-73,对应 internal/provider/provider.go:1113):

// Factory builds a Provider from a resolved config instance. type Factory func(cfg Config) (Provider, error)

Factory 是一个类型别名——func(Config) (Provider, error),不是 interface。原因:

  • Factory 不需要多态,它就是个函数。
  • 用类型别名,任何签名匹配的函数都自动满足,无需 implements 声明。
  • Register 接受 Factory 类型,实现侧只需写一个普通函数。

Config 是已解析的 provider 实例配置(internal/provider/provider.go:1073-1079):

type Config struct { Name string // instance name, e.g. "deepseek" BaseURL string // OpenAI-compatible endpoint Model string // model id APIKey string // resolved from api_key_env Extra map[string]any // kind-specific options }

注意 APIKey 是"已解析"的——配置文件里写的是 api_key_env = "DEEPSEEK_API_KEY"(环境变量名),internal/config 在加载时从 <Reasonix home>/.env 读出真实值,填入 Config.APIKey。Provider 实现拿到的就是明文密钥,无需自己查环境。Extra 留给 kind 特定选项(如 reasoning_protocolauth_header 等)。

三、Register 与 New:注册表配合

SPEC §3.1 定义注册表 API(docs/SPEC.md:75-79,对应 internal/provider/provider.go:1117-1140):

// Register adds a factory under a kind (e.g. "openai"). Called from init(). func Register(kind string, f Factory) // New instantiates the provider of the given kind. func New(kind string, cfg Config) (Provider, error)

注册表本体是一行(internal/provider/provider.go:1115):

var registry = map[string]Factory{}

Register 的实现(internal/provider/provider.go:1119-1124):

func Register(kind string, f Factory) { if _, dup := registry[kind]; dup { panic("provider: duplicate kind " + kind) } registry[kind] = f }

关键点:

  • 重复 kind 直接 panic——这是编译期装配错误,不是运行时错误。init() 里注册两次同名 kind,程序起不来。这强迫开发者立刻解决冲突。
  • 没有 mutex——因为 Register 只在 init() 调用,Go 保证 init() 顺序确定且单线程;运行时只读 registry,无需加锁。

New 的实现(internal/provider/provider.go:1127-1140):

func New(kind string, cfg Config) (Provider, error) { f, ok := registry[kind] if !ok { return nil, fmt.Errorf("provider: unknown kind %q (registered: %v)", kind, Kinds()) } p, err := f(cfg) if err != nil { return nil, err } if nilutil.IsNil(p) { return nil, fmt.Errorf("provider: factory %q returned nil provider", kind) } return p, nil }
  • 找不到 kind 时,错误信息列出所有已注册 kind(Kinds() 返回排序后的 key 列表),帮用户快速定位拼写错误。
  • Factory 返回 nil 也报错——防止装配错误静默通过。

💡 契约要点:这套 Register/New 是 Go"注册表模式"的标准实现,要点有三:① 全局 map;② Registerinit() 调用,无并发;③ 重复注册 panic、未知 kind 报错列出候选。第 4 章的 Tool 注册表是同构实现。

四、Request 与 Chunk:数据类型

SPEC §4(docs/SPEC.md:628-656)定义了 Provider 用到的数据类型。Request 在源码里(internal/provider/provider.go:214-223):

type Request struct { Messages []Message Tools []ToolSchema Temperature *float64 // nil = omit; non-nil = send the value, including 0 MaxTokens int ResponseFormat *ResponseFormat `json:"ResponseFormat,omitempty"` }

注意 Temperature*float64(指针)——nil 表示"不发送该字段"(让 provider 用默认值),非 nil(包括 0)表示"显式发送"。这区分了"未配置"与"显式 0",对 prefix cache 友好(字节稳定)。

Chunk 是流式增量(internal/provider/provider.go:868-884):

type Chunk struct { Type ChunkType Text string // ChunkText, ChunkReasoning Signature string // ChunkReasoning: 推理签名(Anthropic thinking signature) ToolCall *ToolCall // ChunkToolCall(完整)/Start/ArgsDelta Usage *Usage // ChunkUsage Err error // ChunkError // ... 还有 ReasoningID/ReasoningStatus/ArgChars/ResponsesItem 等 }

ChunkType 是九种(internal/provider/provider.go:691-701):

ChunkType 含义
ChunkText 文本增量(可见答案)
ChunkReasoning 推理增量(thinking 模式的思维链)
ChunkToolCallStart 一个 tool call 开始(ID+Name,参数还在流)
ChunkToolCallArgsDelta tool call 参数流式增量
ChunkToolCall 一个完整的 tool call
ChunkUsage 本次补全的 token 计费
ChunkDone 补全正常结束
ChunkError 出错
ChunkResponsesItem Responses API 的完整输出项(用于无状态重放)

SPEC §3.1 还有一条重要约束(docs/SPEC.md:105-106):

Streaming tool-call deltas are accumulated by index inside the provider; only complete ToolCalls are emitted.

流式 tool call 增量在 provider 内部按 index 累积,只发射完整的 ToolCall。即 Agent 看到的永远是完整工具调用,不会拿到半个参数 JSON——这把流式拼接的复杂度封装在 provider 内。

五、openai 兼容实现:init() 自注册

internal/provider/openai/ 是默认 provider,实现 OpenAI /chat/completions 兼容协议。它在 init() 注册自己(internal/provider/openai/openai.go:57-58):

func init() { provider.Register("openai", New) }

这里的 New 是 openai 包内的构造函数(不是父包的 provider.New),签名匹配 Factory:func(cfg provider.Config) (provider.Provider, error)

SPEC §3.1 明确(docs/SPEC.md:90-93):

The openai kind is an OpenAI-compatible /chat/completions implementation. OpenAI-compatible vendors are config instances of kind = "openai", differing only in base_url / model / api_key_env. Adding another OpenAI-compatible model is a config edit, not a code change.

这是"薄 harness"的核心兑现:DeepSeek、Kimi、GLM、MiniMax、Qwen、Ollama Cloud 等都只是 kind = "openai" 的不同配置实例。openai 实现负责把 Request 序列化成 OpenAI 格式 JSON、POST 到 base_url + "/chat/completions"、解析 SSE 流、累积 tool call、规范化 usage(把 DeepSeek 的 prompt_cache_hit_tokens 与 OpenAI 的 prompt_tokens_details.cached_tokens 统一成 Usage.CacheHitTokens)。

依赖方向上,internal/provider/openai import 父包 internal/provider(为了拿 interface、Config、Register、types),但父包永不 import internal/provider/openai。入口 cmd/reasonix/main.go:11 用 blank-import 触发它的 init():

_ "reasonix/internal/provider/openai"

六、anthropic 与 responses 兄弟实现

除了 openai,还有两个兄弟实现:

  • internal/provider/anthropic:Anthropic 原生 Messages API(不经 OpenAI shim)。init() 注册 "anthropic" kind(internal/provider/anthropic/anthropic.go:61):provider.Register("anthropic", New)。它有自己的特性:支持扩展思考(thinking = "adaptive" 跨工具调用回放签名 reasoning block)、当前 Claude 模型不发 temperature、某些网关用 Bearer auth 而非 x-api-key(auth_header = true)。
  • internal/provider/responses:OpenAI Responses API 实现,注册两个 kind(internal/provider/responses/responses.go:32-33):provider.Register("responses", newFromConfig)provider.Register("dashscope-responses", newFromConfig)。后者专门对接 DashScope(阿里灵积)的 Responses 端点。

三者都通过同一个 Provider interface 暴露给 Agent,Agent 不知道也不关心具体是哪种。SPEC §9 Roadmap 提到未来会加"Anthropic 原生 provider kind(原生 prompt-cache 控制),证明注册表能泛化到不止一种 wire format"——这是注册表模式的扩展性证明。

⚠️ 注意:加新的 provider kind(不是 OpenAI 兼容配置实例,而是新的 wire 协议)需要:① 在 internal/provider/<name>/ 写实现;② 实现 init() 调用 provider.Register("<kind>", New);③ 在 cmd/reasonix/main.go 加 blank-import。三步缺一不可。

本节要点回顾

  1. Provider interface:Name() string + Stream(ctx, Request) (<-chan Chunk, error),极薄;ctx 取消必须中断底层请求,channel 关闭标志补全结束。
  2. Factory:func(Config) (Provider, error) 类型别名,不是 interface;Config.APIKey 是已解析的明文密钥。
  3. Register/New:全局 map[string]Factory,Registerinit() 调用无并发,重复 kind panic、未知 kind 报错列出候选。
  4. Request/Chunk:Temperature *float64 区分"未配置"与"显式 0";ChunkType 九种;tool call 增量在 provider 内累积,只发完整 ToolCall。
  5. openai 兼容实现:init() 注册 "openai" kind;DeepSeek/Kimi/GLM 等都是 kind = "openai" 的配置实例,加新 OpenAI 兼容模型是 config 编辑不是代码改动。
  6. anthropic/responses:两个兄弟实现,anthropic 走 Messages API(支持 thinking 签名回放),responses 走 Responses API(注册 responses 与 dashscope-responses 两 kind)。

下一节,我们看 DeepSeek 预设的具体配置,以及 Reasonix 最独特的能力之一——双模型 executor+planner 协作,如何在不破坏 prefix cache 的前提下让两个模型分工。


作者与出处
原作者: 灏天文库
整理: 灏天文库整理
本站整理收录,版权归原作者/开源协议所有;欢迎通过原文链接访问源仓库。
发布者: 作者: 灏天文库 转发
评论区 (0)
U