单模型执行器 与 OpenCode v2配置迁移 笔记

引言:两个容易被低估的工程问题

在多 Agent 编排框架里,默认范式已经相当成熟:一个顶层路由按意图分配任务,子代理各司其职,模型按难度分级(便宜模型扛高频,贵模型啃硬骨头)。这套架构解决了成本和并行问题,但它默认了一件事:执行链路允许横跨多个模型。

于是有两个问题经常被低估:

  1. 当任务要求"全程只有一个模型"时怎么办? 可复现性验证、对单个模型能力的完整评估、对子代理隐式上下文的怀疑------这些场景下,跨模型链路本身就是不被接受的。这时候不能靠"尽量别委派",需要的是结构性保证。
  2. 平台大版本升级时,配置语义怎么迁移? Agent 配置框架(如 OpenCode v2)的特点是:键名写错、写旧、写错顺序,全都不报错------静默失效才是默认失败模式。迁移的难点不在改键名,而在建立一套"能发现没生效"的验证方法。

本文围绕这两个问题,拆解 solo(单模型内联执行器)的设计,以及 OpenCode v2 的配置语义与迁移要点。


一、solo:单模型内联执行器

1.1 它是什么,为什么要存在

solo 是一个 primary 模式的 Agent:分析与规划、实现与验证全部在当前会话所选的那个模型 上内联完成,不产生任何子会话、不切换任何模型。它存在的理由不是"性能更好",而是保证:整条链路只有一个模型在思考,行为可复现、上下文不跨模型、成本和延迟都收敛到一个档位。

1.2 "零委派"必须做在权限层,而不是提示词里

写一句"请不要委派给子代理"是最容易想到的做法,也是最不可靠的:它只是一个请求,模型可以判断"这次情况特殊"从而绕过。正确做法是用平台的权限系统把委派变成不可能的:

yaml 复制代码
# agent frontmatter 示意
mode: primary
permission:
  task:
    "*": "deny"        # 委派工具在权限层被彻底否决

关键区别:task: deny 之后,子代理工具对模型来说"不存在",而不是"不该用"。结构性保证永远优于行为性请求------这正是选择 solo 的意义所在("这个任务必须在单模型上完成")。

一个由此产生的细节:全局规则里通常有"多步任务先规划、禁止单干"之类的条款,这些规则在所有有委派能力的 Agent 上都是对的,唯独对 solo 是矛盾的------它的存在前提就是"委派已被结构性禁止,所以规划、实现、验证全部内联"。因此角色提示词里要把豁免写死,例如:

全局规则要求 2+ 步骤先走 planner、要求"委派而非亲力亲为"。这些规则在这里不适用:task 权限已被拒绝,委派在结构上不可能,所以规划(先写 TODO)、实现、验证都在本会话内联完成。这正是选择本 Agent 的意义。

不要指望模型在冲突的规则之间临场权衡------把豁免显式写进角色定义。

1.3 还得禁掉"内置助手"------因为模型解析链比想象中复杂

权限层只堵住了显式委派。很多框架还提供内置助手 Agent(如 build、plan),调用它们同样会产生子会话或一次性调用------而这些调用按各自的配置档位运行,通常落在另一个模型上(比如轻量 flash 档)。一旦调用,另一档模型就进入了链路,"全程单模型"不再成立。

所以在 solo 的提示词里应直接禁止调用这些助手,并说清原因:

不要调用 build/plan 等内置助手:它们运行在引擎内置的轻量模型上,会把另一档模型带进链路,破坏单模型保证。

这里有个容易写错的认知点,值得单独强调(§2.5 会展开):Agent 配置里的 model 字段只约束子会话、一次性调用和界面默认值,并不约束主会话。所以"solo 用哪个模型"其实由会话自身决定,与内置助手的档位是两套逻辑------文档和提示词里如果写混,就会出现自相矛盾的说明。

1.4 跟随会话模型:不要硬编码 model

solo 的配置里不写 model 字段,这是刻意的:执行器的前提是"用会话当前选定的模型跑完整条任务",硬编码等于偷偷换模型,前提就不成立了。

这带来两个连锁设计:

  • 思考档随模型而定。 例如双模型场景下:默认 pro(thinking 开启、reasoning effort 高);会话切到 flash 时 thinking 关闭、temperature 固定为 0(由 provider 层配置保证)。文档里必须写成条件式,写成"固定在 pro/高思考档"就是错的------这个坑在实践里非常常见,因为早期文档很容易把"默认"当成"固定"。
  • 多模态能力也随模型而定。 拒绝契约不能一概而论:如果默认模型是纯文本的,遇到图片必须拒绝并转交视觉 Agent(或提示切换到原生多模态的模型);但如果会话本身跑在多模态模型上,读图就是合法操作。拒绝条款要加限定语------"在当前会话模型为纯文本时"。

1.5 提示词的三块:铁律、拒绝契约、技能白名单

铁律(结构层的事实陈述):零委派、禁用内置助手、直接使用工具(bash/read/write/edit/grep...)动手。每一条都是对平台事实的描述,不依赖模型自觉。

拒绝契约(什么不做,比做什么更重要) :明确列出必须当场拒绝、不允许降级交付的情形:

  • 多模态输入但当前模型不支持------直接告知用户正确的路径(视觉 Agent / 切换模型),绝不猜测图片内容;
  • 任何无法诚实完成的工作------直接说明原因,绝不把降级或残缺的结果当作完成品交付。

拒绝优于降级,这是信任问题:一个"看起来很完整"的部分结果,比一次明确的拒绝昂贵得多。

技能白名单(deny 默认 + 全量放行) :技能名册(每个技能的 name + description)是每轮常驻上下文 ,所以常规做法是按职责最小化白名单。但 solo 是个例外------它要内联完成规划、实现、审查、Git、发布等所有阶段,因此放行完整本地技能链 ,再用 "*": "deny" 兜底:

yaml 复制代码
permission:
  skill:
    "*": "deny"
    "code-review": "allow"
    "git-master": "allow"
    "diagnosing-bugs": "allow"
    # ...完整工具链

白名单不只是权限------被 deny 的技能不会进入该 Agent 的技能名册,所以这份名单同时也是上下文预算表。对执行器全量放行是对"内联需要全工具链"的补偿;对其他专职 Agent(只做搜索、只做审查),则应保持最小化。

工作流纪律:多步任务先写有序 TODO(每条带可验证的完成判据)、最小改动、自验证、遵守 Git 安全规范。这部分与全局规则一致,不重复。

1.6 落地时的高频修正点

  • 思考档描述与真实行为对齐(见 §1.4);
  • 多模态拒绝条款加"依赖会话模型"限定;
  • frontmatter YAML 的值保持引号一致(bare deny/allow 在 YAML 1.1 下虽然仍解析为字符串,但混合风格在解析器严格化时就是隐患);
  • 内置助手的档位语义要按模型解析链写对,否则会推出"主会话会跟随某档"这类错误结论。

二、OpenCode v2:配置语义与迁移

2.1 双路径加载:一个键决定你走哪条路

v2 加载配置文件有两条路径:原生解码 ,或者------只要文件中出现任意一个 v1 触发键------官方的 V1→V2 迁移路径 。触发键包括 logLevel、server、command、plugin、small_model、mode、agent、provider、permission、tools、attachment、layout 等。

这决定了迁移策略是一个二选一,而不是模糊地带:

策略 写法 适用
双运行时兼容 只写 classic 键;v2 在加载时归一化上转 需要同时支持新旧两代运行时
v2 原生 写原生键,并保证不出现任何 v1 触发键 已声明只支持 v2

两条路都可行,但不能混:混写的下场是"文件里有原生拼写,加载时却走了迁移路径",行为不由你控制。如果选择 v2 原生路线,目标就是把触发键清零------它们一个都不能残留。

2.2 键名地图

迁移的核心是把一批键换成原生拼写,并同步语义:

classic 写法 v2 原生 备注
plugin plugins 字符串或 {package, options}
provider.models.*.options ...models.*.settings 请求透传(temperature / thinking)
provider.models.*.modalities ...models.*.capabilities {tools, input[], output[]};不声明图像输入,多模态调用会被拒绝
provider.models.*.cost(平铺 cache_read) cost 数组 + 嵌套 cache: {read, write}
agent / command / permission / attachment agents / commands / permissions(有序列表)/ media 权限动作名同时改名:bash→shell、task→subagent
顶层 subagent_depth experimental.subagent_depth 顶层键会被 v2 直接丢弃
small_model ---(展开为 title Agent 的模型) 建议直接显式固定 title
compaction.reserved / preserve_recent_tokens compaction.buffer / compaction.keep.tokens prune/tail_turns 是 v1 概念,v2 不再读取

一个务实的迁移细节:没必要一次迁移所有层。譬如 provider、权限、命令这些顶层键迁移并验证完毕后,Agent 文件的 frontmatter 可以暂时保留旧写法(v2 实测接受),等下一轮单独迁移、单独验证。一次变更只动一类东西,是这种"静默失效"平台上最划算的纪律。

2.3 权限模型:有序规则 + last-match-wins

v2 的权限是一张有序规则表 ,解析语义是 最后匹配者胜(last-match-wins)。这直接决定了规则的书写顺序:

jsonc 复制代码
"permissions": [
    { "action": "shell", "resource": "*",          "effect": "ask"   },  // catch-all 放最前
    { "action": "shell", "resource": "git status*", "effect": "allow" },  // 后面的规则覆盖它
    { "action": "shell", "resource": "rg *",        "effect": "allow" },
    { "action": "read",  "resource": ".env",        "effect": "deny"  }
]

两个直接推论:

  1. catch-all 必须放最前,具体规则跟在后面覆盖它。 顺序放反(catch-all 在最后),所有写在它前面的 allow 规则全部被遮蔽------配置看起来"写了 allowlist",实际全部落到 ask,且不报任何错。
  2. 在 catch-all 已是 ask 的体系里,显式罗列危险命令(如 rm -rf*、git push --force*)是死规则。 它们匹配的结果与 catch-all 相同,纯粹是维护负担。与其维护黑名单,不如让默认值兜底:默认 ask(而不是默认放行),让任何"未识别的破坏性变体"自动落到人工确认。

2.4 静默失效清单(迁移前必读)

v2 配置世界里"写了不等于生效"的模式,值得形成一张清单:

模式 行为
未知键 两种运行时都静默丢弃,不报错
单复数 只认单数形式(如 skill、attachment);复数拼写被忽略
规则顺序 last-match-wins;顺序错了会把 allowlist 变死配置
v1/v2 混写 出现一个 v1 触发键即切换到迁移路径,原生拼写行为不受控
YAML 严格性 v2 的 frontmatter 解析是严格 YAML:description 里未引号的 ": " 会导致解析失败,该 Agent 回退为"整个文件当 system prompt + 默认配置"(丢 model / steps / 权限)------在宽松解析的旧运行时上完全不可见
改名键残留 如 autoupdate→update 之类的旧名残留,不报错、不生效

验证方法(唯一可靠的做法) :权威是二进制,不是文档。用 debug config / debug agents 之类的命令把归一化后的实际解析结果 打印出来,迁移前后做字段级 A/B 对比(commands、取值、权限顺序逐项相同);再用一次真实调用对账(例如计费与离线估算吻合)。"配置文件看起来没问题"在这个平台上没有任何证据强度。

2.5 模型解析链:agent.model 到底管谁

这是最容易误解、且直接影响 solo 正确性的一块。结论:

复制代码
agent.model 只在这三处生效:
  ① 子会话(subagent,以及绑定 subagent 档的斜杠命令------命令会创建子会话,
     按该 Agent 的档位运行,当前主会话的模型不受影响)
  ② title / summary / compaction 这类一次性调用
  ③ 界面新会话的随 Agent 联动默认值

主会话自身的解析顺序:
  -m 参数  >  会话已存模型  >  全局默认模型
  (headless 运行不读取 agent.model------想降档必须显式指定)

对 solo 的含义:task: deny 是第一道闸,内置助手是第二道闸,而解析链认知是第三道闸。前两道不齐,或误以为"主会话会跟随某 Agent 档位",都会让"全程单模型"的结论失真。

2.6 压缩窗口:把隐式默认值改成显式预算

内置上下文压缩由 limit.input 这一个开关决定触发点:

复制代码
触发条件:会话 token 数 ≥ limit.input − compaction.buffer
不声明 limit.input 时:触发点回退为 context − maxOutputTokens

回退值可能非常离谱:以一个 1M 上下文、384K 输出上限的模型为例,隐式触发点是 968K tokens ------也就是说会话可以膨胀到近百万 token 才第一次压缩。而这类 API 的计费模型是每个请求重发整个上下文:携带 968K 与携带 115K,成本差接近一个数量级。所以:

  • 显式声明每个模型的 limit.input,把"工作窗口"变成预算数字(例如给高频模型设 128K 窗口、给深度模型设 160K 窗口);
  • 理解配套语义:buffer 是压缩期间的溢出余量,keep.tokens 是压缩后保留的 verbatim 尾部(其余更早内容被摘要替换);
  • v1 的 prune/tail_turns 在 v2 不生效,reserved/preserve_recent_tokens 改名后被读取------迁移时必须逐键核对。

在纯配置体系里,显式压缩窗口是最大的单点成本杠杆:前缀越小,缓存命中率越高、压缩触发越晚,收益叠加。


三、把方法论压成几条

  1. 结构性保证优于行为性请求。 权限层拒绝 > 提示词请求;"不可能发生" > "尽量别做"。
  2. 单模型保证是一个清单,不是一句话。 权限闸(no delegation)+ 助手闸(no built-in helpers)+ 解析链认知(agent.model 不约束主会话),缺一不可。
  3. 全局规则需要角色级豁免通道。 当全局规则与角色目标冲突时,把豁免写进角色定义,别让模型临场权衡。
  4. 默认值即成本。 压缩窗口的隐式触发点、权限的默认放行、技能名册的常驻开销------每一个"平台默认"都应该被审视并显式化。
  5. 静默失效平台上,验证必须对着二进制。 A/B 对比归一化输出 + 真实调用对账;文档和"看起来对"都不算证据。
  6. 迁移分阶段、分层。 先兼容后纯化(或反之),一次只迁移一类已能独立验证的东西。

项目地址:https://github.com/znlgis/my-opencode-deepseek-config

相关推荐
Blockbuater_drug21 小时前
ADMET 预测从图神经网络到 DeepSeek RAG 解释:CPU 全流程实测跑通
图神经网络·deepseek·admet 预测·rag 解释·tdc 数据集
ss2731 天前
DSH v0.2.1-alpha.1 发布:Agent 能自己造插件,Claude Code 也能兼容,插件生态要变天
deepseek·deepseekharness·dsh
fellow991 天前
Electron + pnpm 在鸿蒙应用里跑起来
electron·harmonyos·openharmony·deepseek·deepseekharness
EatFan1 天前
从 CUDA 到昇腾:一张对照表拆解 DeepSeek 最新开源组件(TileLang / DeepGEMM / DeepEP / TileKernels)
开源·cuda·deepseek·deepgemm·tilelang·华为昇腾
skywalk81631 天前
deepseek harness 官方已经更新到新版:v0.2.1-alpha.1 Pre-release请把咱们的FreeBSD版本也同步更新到新版本!
人工智能·freebsd·实践·deepseek·harness
凉冬寒1 天前
从 npm 版 dsh 迁移到 DSH 桌面版:彻底告别 dsh web
deepseek
Ai小way2 天前
【deepseek实战·28】倒计时看板:多个日子一起盯,环形进度走到零,Web Audio 替你响一声
deepseek
AC赳赳老秦2 天前
OpenClaw 与 FineBI 联动方案:公开数据自动采集与实时业务分析看板实践
大数据·开发语言·python·php·finebi·deepseek·openclaw
章鱼哥19713 天前
我给 DeepSeek 的编程智能体写了三个插件:余额胶囊、任务面板、番茄钟
ai编程·deepseek