第 3 章 · 01 SPEC §3.1 Provider interface 与 Factory 注册表 本节摘要:本节精读 SPEC §3.1 的 Provider 抽象。Provider 是一个极简 interface——只有 与 两个方法; 是 类型别名; 把 Factory 注入进程全局注册表, 按 kind 实例化。整套抽象用几十行 Go 把"核心只懂接口、具体按名解析"变成可运行代码。本节还讲 Request/Chunk 数据类型、 的 OpenAI 兼容实现如何在 注册 kind、 与 两个兄弟实现,以及"子包 import 父包自注册、父包永不 import 子包"在 Provider 上的落地。
本节摘要:本节精读 SPEC §3.1 的 Provider 抽象。Provider 是一个极简 interface——只有
Name() string与Stream(ctx context.Context, req Request) (<-chan Chunk, error)两个方法;Factory是func(Config) (Provider, error)类型别名;Register(kind, Factory)把 Factory 注入进程全局注册表,New(kind, cfg)按 kind 实例化。整套抽象用几十行 Go 把"核心只懂接口、具体按名解析"变成可运行代码。本节还讲 Request/Chunk 数据类型、internal/provider/openai/的 OpenAI 兼容实现如何在init()注册"openai"kind、anthropic与responses两个兄弟实现,以及"子包 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 = [...]列表)。
阅读完本节,你应当能够:
Register 与 New 的配合,以及 Register 为何在 init() 调用。internal/provider/openai/ 如何在 init() 注册 "openai"。openai、anthropic、responses 三种 kind 的用途。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 之间的硬契约。
紧接 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。原因:
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_protocol、auth_header 等)。
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 }
关键点:
init() 里注册两次同名 kind,程序起不来。这强迫开发者立刻解决冲突。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 }
Kinds() 返回排序后的 key 列表),帮用户快速定位拼写错误。💡 契约要点:这套 Register/New 是 Go"注册表模式"的标准实现,要点有三:① 全局 map;②
Register在init()调用,无并发;③ 重复注册 panic、未知 kind 报错列出候选。第 4 章的 Tool 注册表是同构实现。
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 内。
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
openaikind is an OpenAI-compatible/chat/completionsimplementation. OpenAI-compatible vendors are config instances ofkind = "openai", differing only inbase_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"
除了 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。三步缺一不可。
Name() string + Stream(ctx, Request) (<-chan Chunk, error),极薄;ctx 取消必须中断底层请求,channel 关闭标志补全结束。func(Config) (Provider, error) 类型别名,不是 interface;Config.APIKey 是已解析的明文密钥。map[string]Factory,Register 在 init() 调用无并发,重复 kind panic、未知 kind 报错列出候选。Temperature *float64 区分"未配置"与"显式 0";ChunkType 九种;tool call 增量在 provider 内累积,只发完整 ToolCall。init() 注册 "openai" kind;DeepSeek/Kimi/GLM 等都是 kind = "openai" 的配置实例,加新 OpenAI 兼容模型是 config 编辑不是代码改动。下一节,我们看 DeepSeek 预设的具体配置,以及 Reasonix 最独特的能力之一——双模型 executor+planner 协作,如何在不破坏 prefix cache 的前提下让两个模型分工。