第 2 章 · 02 config 四级覆盖与 reasonix.example.toml 精读 本节摘要:本节深潜 Reasonix 的配置体系。先讲 config 四级覆盖(flag 命令行 > project > user > built-in defaults),理解"哪一级生效、为什么 user/global 字段不被 project 覆盖"。然后逐段精读 260 行——DeepSeek 预设( 的 endpoint 复用)、双模型 分离缓存会话、 定价、 工具启用、 MCP 服务器、 权限策略等核心配置块。最后回到 SPEC §1.
本节摘要:本节深潜 Reasonix 的配置体系。先讲 config 四级覆盖(flag 命令行 > project
./reasonix.toml> user~/.reasonix/config.toml> built-in defaults),理解"哪一级生效、为什么 user/global 字段不被 project 覆盖"。然后逐段精读reasonix.example.toml260 行——DeepSeek 预设([[providers]] kind = "openai"的 endpoint 复用)、双模型[agent] executor+planner分离缓存会话、[[provider.pricing]]定价、[tools] enabled工具启用、[[plugins]]MCP 服务器、[permission]权限策略等核心配置块。最后回到 SPEC §1.3——为什么 TOML 是唯一被接受的第三方依赖(BurntSushi/toml),以及"加新模型是 config 编辑不是代码改动"如何成为"薄 harness"的最佳注脚。
内容来源:原项目源码
reasonix.example.toml(260 行)、docs/SPEC.md§5 Configuration、internal/config/(约 31.8K 行)。
⚠️ 注意:
reasonix.example.toml只是"示例文件",真正生效的是./reasonix.toml(项目级)与~/.reasonix/config.toml(用户级)。example 文件用于reasonix setup写出默认配置与文档展示,不会被自动加载。
阅读完本节,你应当能够:
reasonix.toml 覆盖。reasonix.example.toml 中 DeepSeek 预设的每个字段。executor + planner 如何"各自独立缓存稳定会话"。[[plugins]](MCP stdio/http/sse)与 [[extension.sidecars]](Extension Protocol)。SPEC §5 开篇钉死了解析顺序(docs/SPEC.md:660-662):
Resolution order: flag > project
./reasonix.toml> the user config file > built-in defaults.
从高到低四级:
| 级别 | 来源 | 作用域 | 示例 |
|---|---|---|---|
| 1 最高 | flag 命令行 | 单次运行 | reasonix --model deepseek-pro |
| 2 | project ./reasonix.toml |
当前项目 | 项目根的 reasonix.toml |
| 3 | user 用户级 | 当前用户所有项目 | macOS/Linux ~/.reasonix/config.toml,Windows %AppData%\reasonix\config.toml |
| 4 最低 | built-in defaults | 全局兜底 | 代码内置默认值 |
注意自 v1.8.1 起,用户级配置文件位置从 ~/.reasonix.toml 改为 ~/.reasonix/config.toml(Windows 为 %AppData%\reasonix\config.toml),reasonix setup 会写出默认配置到这个路径。
reasonix.example.toml 顶部也标注了这条顺序(reasonix.example.toml:1-3):
# Reasonix configuration. # Resolution order: flag > ./reasonix.toml > <Reasonix home>/config.toml > built-in defaults.
例外:user/global only 字段。SPEC §5 说明(docs/SPEC.md:665-666):
Fields marked user/global only are not overridden by project
reasonix.toml.
也就是说,有些字段(如桌面偏好、全局通知开关)标记为"仅用户级",项目 reasonix.toml 写了也不生效——这是为了防止恶意项目通过 reasonix.toml 篡改用户的全局偏好。Provider 的密钥通过 api_key_env 命名环境变量、实际值存在 <Reasonix home>/.env,绝不在配置文件里写明文密钥(reasonix.example.toml:4-6):
# Provider entries name secrets via api_key_env; saved key values live in # Reasonix's global <Reasonix home>/.env. Never put API key values here.
example.toml 第 7-15 行:
default_model = "deepseek" # a provider name (→ its default model) or "provider/model" # language = "zh" # ui language; empty = auto-detect from $LANG / $REASONIX_LANG [ui] theme = "auto" # auto|dark|light; controls CLI colors only; REASONIX_THEME can override per run # theme_style = "graphite" # graphite|aurora|slate|carbon|nocturne|amber and legacy aliases show_turn_usage = true # CLI/TUI: show per-request token and cost receipts in transcript scrollback
default_model 是最关键的全局字段——它可以是 provider 名(如 "deepseek",自动取该 provider 的 default model),也可以是裸模型名,或显式 "provider/model"。Config.ResolveModel 负责解析这三种形态。language 空 = 自动从 $LANG / $REASONIX_LANG 探测,显式设 "zh" 强制中文。[ui] 控制终端 UI 外观(主题/样式/是否显示每轮 token 与成本回执),不影响模型行为。这是 example.toml 最核心的一段(reasonix.example.toml:59-74):
[[providers]] name = "deepseek" kind = "openai" base_url = "https://api.deepseek.com" models = ["deepseek-v4-flash", "deepseek-v4-pro"] default = "deepseek-v4-flash" # optional; defaults to the first of `models` api_key_env = "DEEPSEEK_API_KEY" context_window = 1000000 effort = "high"
逐字段解读:
name = "deepseek":provider 实例名,default_model 与 --model 用它引用。kind = "openai":provider 的"种类"——注册表里的 key。DeepSeek 走的是 openai kind,因为 DeepSeek API 是 OpenAI /chat/completions 兼容的。base_url:端点 URL,OpenAI 兼容实现会 POST 到 base_url + "/chat/completions"。models:一个 vendor endpoint 可暴露多个模型,列表形式免去重复声明 base_url/api_key。default:列表形式时的默认模型(缺省取 models[0])。api_key_env:密钥的环境变量名,实际密钥从 <Reasonix home>/.env 读。context_window:provider 级上下文窗口(token),接近这个值时触发压缩(第 6 章);0 禁用压缩。effort:DeepSeek thinking 模式的推理强度(high | max),DeepSeek thinking 始终开启。💡 契约要点:这就是"薄 harness"的最佳注脚。SPEC §3.1 原文(
docs/SPEC.md:91-93):
Adding another OpenAI-compatible model is a config edit, not a code change.
加一个新的 OpenAI 兼容模型,是改 config,不是改代码。DeepSeek、Kimi、GLM、MiniMax、Qwen……它们在 Reasonix 里都是 kind = "openai" 的配置实例,只是 base_url/models/api_key_env 不同。
example.toml 也展示了 Anthropic 原生 provider(reasonix.example.toml:104-114):
[[providers]] name = "claude" kind = "anthropic" model = "claude-opus-4-8" api_key_env = "ANTHROPIC_API_KEY" context_window = 1000000 price = { cache_hit = 0.5, input = 5, output = 25, currency = "$" } thinking = "adaptive" effort = "high"
kind = "anthropic" 走的是 internal/provider/anthropic(原生 Messages API,不经 OpenAI shim)。它有自己的特性:thinking = "adaptive" 开启扩展思考并跨工具调用回放签名 reasoning block;Anthropic 当前模型不接受 temperature(代码里 internal/provider/anthropic 不发采样参数)。
注意 kind = "responses" 也存在(走 internal/provider/responses,注册了 "responses" 与 "dashscope-responses" 两个 kind),用于 OpenAI Responses API 与 DashScope。三种 kind 覆盖了主流端点。
[agent] 段(reasonix.example.toml:31-48):
[agent] temperature = 0.0 soft_compact_ratio = 0.5 # notice only; keeps the cache-first prefix intact tool_result_snip_ratio = 0.6 # snip stale tool results before summary compaction compact_ratio = 0.8 # try compacting when prompt reaches this fraction compact_force_ratio = 0.9 # force compacting at this high-water mark # planner_model = "deepseek-pro" # optional: enable two-model collaboration
注释掉的 planner_model 是双模型协作的开关。SPEC §3.5 讲清了为什么双模型必须分离会话(docs/SPEC.md:201-204):
When
agent.planner_modelnames a provider different from the executor, aCoordinatorruns two models in separate sessions to keep each one's prompt prefix cache-stable.
SPEC §3.5 最后一句点睛(docs/SPEC.md:237-240):
The sessions never mix, so neither model's prefix is disturbed by the other's turns; both grow prepend-only and stay cache-friendly. This reconciles "cache-first" with "two-model collaboration": switching models inside one shared conversation would break the prefix and tank cache hits, so we don't.
"在同一个共享会话里切换模型"会破坏 prefix、击穿缓存命中,所以绝不那么做——而是开两个会话。这是 Reasonix 把"缓存优先"与"双模型协作"调和出来的精巧设计,第 3 章第 2 节会展开。
compact_ratio 等四个比例是上下文维护的阈值(第 6 章详讲):soft_compact_ratio 只是提醒,tool_result_snip_ratio 裁剪旧工具输出,compact_ratio 触发摘要压缩,compact_force_ratio 强制压缩。
[tools] 工具启用(reasonix.example.toml:121-125):
[tools] enabled = [] # empty = all built-in tools bash_timeout_seconds = 120 mcp_startup_timeout_seconds = 30 mcp_call_timeout_seconds = 300
enabled = [] 表示启用全部内置工具;列出名字则只启用子集。三个超时分管 bash、MCP 启动、MCP 调用。
[[plugins]] MCP 服务器(reasonix.example.toml:170-181):
# [[plugins]] # name = "example" # command = "reasonix-plugin-example" # # startup_timeout_seconds = 60 # # call_timeout_seconds = 600
每个 [[plugins]] 是一个 MCP 兼容的外部可执行文件,type 默认 stdio(本地子进程),也支持 http(Streamable HTTP)与 sse(legacy)。${VAR} 在 command/args/env/url/headers 里展开,密钥来自环境而非配置文件。第 7 章详讲。
[permissions] 权限策略(SPEC §5 例子,docs/SPEC.md:733-736):
[permissions] mode = "ask" deny = ["Bash(rm -rf*)", "Bash(git push*)"] allow = ["Bash(go test:*)", "Bash(git status:*)"] ask = []
mode 是 writer 工具的兜底姿态(ask|allow|deny);deny/allow/ask 是规则列表,语法是 Claude Code 风格的 Tool(specifier)。优先级 deny > ask > allow > fallback。第 5 章详讲。
SPEC §1.3 原文(docs/SPEC.md:14-16):
A third-party dependency must be pure-Go, lightweight, and must not compromise the single-binary / cross-platform / distribution story. TOML parsing is the one accepted dependency.
为什么是 TOML 而不是 YAML/JSON?
BurntSushi/toml),正好满足 SPEC §1.3 三条要求(纯 Go、轻量、不破坏单二进制)。internal/config 包约 31.8K 行,负责四级覆盖合并、api_key_env 解析、模型引用解析(Config.ResolveModel)、.mcp.json 合并等。它是 Reasonix 把"配置驱动一切"落到实处的核心,第 2 章只讲用法,内部实现细节会在用到时穿插。
💡 契约要点:配置驱动不是"用 TOML 写配置"那么简单,而是把模型、工具、插件、权限、沙箱、压缩阈值全部声明式化。Reasonix 的几乎所有运行时行为都能通过 config 调整,这是"薄 harness"的工程兑现。
./reasonix.toml > ~/.reasonix/config.toml > built-in defaults;user/global only 字段不被 project 覆盖;密钥走 api_key_env + .env,绝不在配置写明文。[[providers]] kind = "openai" 复用 OpenAI 兼容实现,base_url/models/api_key_env 不同即新模型。planner_model 开启,两会话分离、各自 prefix cache 稳定;在共享会话切模型会击穿缓存,所以绝不那么做。[tools] 启用与超时、[[plugins]] MCP 服务器(stdio/http/sse)、[permissions] 权限(deny>ask>allow>fallback)。internal/config 约 31.8K 行落地"配置驱动一切"。下一章,我们正式进入 Provider——SPEC §3.1 的 Provider interface 与 Factory 注册表,看
internal/provider如何用几十行 Go 把"核心只懂接口"变成可运行的代码。