开源终端 Agent 项目 opencode 的多 agent 怎么配
本文基于 opencode 仓库 commit 7fe9938(2026-08-04)梳理,该项目仍在高频迭代,具体行为以仓库 https://github.com/anomalyco/opencode 最新代码与文档为准。
opencode 里没有一套单独的”多 agent 编排语言”,它的多 agent 全部落在一张权限规则表上。 主 agent 和子 agent 的差别只是一个 mode 字段,加上各自那份 ruleset;谁能用哪个工具、谁能派谁干活、派出去的子会话继承什么,全是规则匹配的结果。理解了这张表怎么匹配,配置就不用靠猜了。
这篇只谈配置机制与判断标准。想看 Claude Code 那套 subagent 的写法,去读 Claude Code 自定义 Subagent 怎么写;想看脱离具体工具的角色划分方法论,去读 Agent 角色分工 和 ECC 的 agent 分工模型。本篇的每个名字都能在 opencode 仓库里当场搜到。
一、它把一个 agent 定义成了什么
先看数据结构。packages/opencode/src/agent/agent.ts 顶上导出了一个 Info 的 Schema,字段是这些:name、description、mode、native、hidden、topP、temperature、color、permission、model、variant、prompt、options、steps。其中 mode 是三选一的字面量:subagent、primary、all;model 是一个可选的 { modelID, providerID } 结构;permission 是必填的 ruleset。
也就是说,一个 agent 在 opencode 眼里就是「一段系统提示 + 一份权限规则 + 可选的模型与采样参数」。没有工作流图,没有状态机,没有消息总线。
同一个文件里硬编码了内置 agent 表:build、plan 是 mode: primary,general、explore 是 mode: subagent,另外 compaction、title、summary 三个是 mode: primary 且 hidden: true 的系统 agent,分别用 ./prompt/compaction.txt、./prompt/title.txt、./prompt/summary.txt 作提示词,不出现在选择列表里。它们全部带 native: true。
这里有个值得留意的细节:文档 packages/web/src/content/docs/agents.mdx 里写了三个内置子 agent(general、explore、scout),但这个 commit 的 agent.ts 内置表里只有 general 和 explore 两条。文档和代码存在落差,是这类高频迭代项目的常态。判断某个 agent 在你机器上到底存不存在,用 opencode agent list 看实际结果,别照着文档的段落数。
配置来源有两种。一是 opencode.json 里的 agent 字段;二是 markdown 文件,packages/opencode/src/config/agent.ts 的 load 函数用的 glob 是 {agent,agents}/**/*.md——单数目录和复数目录都认,而且支持嵌套子目录,文件名就是 agent 名。同一个文件里还有个 loadMode,扫的是 {mode,modes}/*.md,解析成功后会被强制打上 mode: "primary"。
二、权限表就是工具白名单
agent.ts 里给所有 agent 铺了一份 defaults,展开来是:"*": "allow"(默认全开)、doom_loop: "ask"、question: "deny"、plan_enter: "deny"、plan_exit: "deny",加上 external_directory 的 "*": "ask" 配一批白名单目录,以及 read 的细则——"*": "allow",但 "*.env" 和 "*.env.*" 是 ask,"*.env.example" 放行。这条注释直接写明是照着 gitignore 的 Node 模板抄的 .env 模式。
默认全开这件事你得先记住:不写任何配置,agent 就是满权限的,只有读 .env 会拦一下问你。
内置 agent 就在这份 defaults 上叠加。plan 叠的是 edit: { "*": "deny", ... },只给计划文件目录开了口子,同时把 task: { general: "deny" } 关掉;explore 叠的是先 "*": "deny" 再逐个放行 grep、glob、list、bash、webfetch、websearch、read,外加一份只读的 external_directory;general 只叠了一条 todowrite: "deny"。三个隐藏 agent 全是 "*": "deny"。
关键在合并和匹配这两步。packages/opencode/src/permission/index.ts 里,merge 的实现就是 rulesets.flat()——把几份规则首尾接起来,不去重不排序。evaluate 则是 findLast:从拼好的数组里找最后一条 permission 和 pattern 都匹配上的规则;一条都没匹配上,兜底返回 action: "ask"。
最后匹配的规则赢。这一条决定了你写配置的顺序:宽的通配符放前面,具体的放后面。文档里那个 bash 例子就是这么排的,"*": "ask" 在前,"git status *": "allow" 在后。反过来写就是静默失效,不会报错。
还有一个容易误判的点。同一个文件里的 disabled 函数决定哪些工具会被从模型看到的工具表里摘掉,它的条件是:最后匹配的那条规则 pattern === "*" 且 action === "deny"。换句话说,只有整条 * 级别的 deny 才会让工具彻底消失;你写的细粒度 deny(比如只禁某个 bash 命令)不会缩短工具表,工具照样出现在模型面前,只是调用时被拒。session/llm/request.ts 和 debug 命令用的都是这个函数。所以”少给工具省 token”和”防止它乱来”是两件事,得用不同写法。
三、模型:钉住还是继承
Info.model 是可选的。配置里写成 provider/model-id 这种形式,进来时由 Provider.parseModel 解析成 { providerID, modelID }。
不写会怎样?分两种情况。文档的说法是:primary agent 用全局配置的模型,subagent 用调用它的那个 primary agent 的模型。代码这边更具体——packages/opencode/src/tool/task.ts 里那行是 const model = next.model ?? { modelID: msg.info.modelID, providerID: msg.info.providerID },msg 是发起这次 task 调用的那条 assistant 消息。紧接着还有一行 variant: next.model ? undefined : variant:子 agent 自己钉了模型,就不再沿用父消息的 variant。
采样参数同理。temperature 不写就走模型自己的默认(文档说多数模型是 0,Qwen 系是 0.55),内置的 title agent 自己写死了 0.5。除此之外,配置里没被识别的字段会原样透传给模型服务商当参数用——文档拿 OpenAI 推理模型的 reasoningEffort、textVerbosity 举了例子。这意味着写错一个键名不会报错,只会悄悄多发一个没人认识的参数过去。
给不同 agent 配不同模型,等于把你的代码分发给不同的服务商。各家的数据使用与保留规则不同且会调整,以官方最新说明为准;至少在你把某个 agent 指向一个新 provider 之前,值得先确认这件事。
四、主 agent 派活时到底发生了什么
派活走的是 task 工具,代码在 packages/opencode/src/tool/task.ts,参数是 description、prompt、subagent_type、task_id、command,实验开关下多一个 background。整个流程按顺序是这样:
先数深度。它顺着 parentID 一路往上爬到根会话,如果 depth >= (cfg.subagent_depth ?? 1) 就直接失败,错误信息里明说让你调大 subagent_depth。默认值是 1,也就是默认不允许套娃。
再问权限。除非上下文里带了 bypassAgentCheck,否则会走一次 ctx.ask,permission 是 task,patterns 就是目标子 agent 的名字。所以 permission.task 里那些 glob(文档里的 orchestrator-* 之类)匹配的是 agent 名。
顺带一提,模型能看到哪些子 agent,也是这条权限决定的。packages/opencode/src/tool/registry.ts 里的 describeTask 先取出所有 mode !== "primary" 的 agent,再用 Permission.evaluate("task", item.name, agent.permission) 过滤掉 deny 的,剩下的按名字排序拼成一段「Available agent types and the tools they have access to」写进 task 工具描述。被 deny 的 agent 压根不会出现在描述里。没写 description 的会显示成「This subagent should only be called manually by the user.」——所以 description 不是装饰,它是模型判断该不该派活的唯一依据。
然后建子会话,权限由 packages/opencode/src/agent/subagent-permissions.ts 里的 deriveSubagentSessionPermission 推导。这个函数只有二十来行,逻辑是:从父会话的 ruleset 里只挑出 external_directory 规则和所有 action === "deny" 的规则带下去;然后,如果子 agent 自己的 ruleset 里没有 todowrite 相关规则就补一条 deny,没有 task 相关规则也补一条 deny。文件顶上的注释把意图写得很清楚:父 agent 的限制只约束它自己,子 agent 的能力由它自己的权限决定。
翻译成人话:父的禁止会往下传,父的放行不会。 你在主 agent 上开的口子,子 agent 一律享受不到,得在它自己的定义里写全。
最后跑。子会话默认是全新上下文,除非你传 task_id 续上之前那个子会话。跑完只把最后一段文本取出来回传(result.parts.findLast((item) => item.type === "text")?.text)。task 工具的描述文件 task.txt 里写得直白:agent 只会返回一条消息,这条消息用户看不见,你得自己转述;而且每次调用都要给出高度详细的任务描述,并明确说清要它返回什么。
五、这套东西由哪几块拼起来
| 组成部分 | 它负责什么 | 仓库位置 | 你什么时候会碰到它 |
|---|---|---|---|
| agent 定义与内置表 | 定义 Info 结构,硬编码 build/plan/general/explore 和三个隐藏 agent,合并用户配置 | packages/opencode/src/agent/agent.ts | 想知道某个内置 agent 到底默认给了什么权限 |
| 权限求值 | fromConfig 展开配置成规则数组,merge 拼接,evaluate 用 findLast 判定,disabled 决定工具是否可见 | packages/opencode/src/permission/index.ts | 规则写了没生效、或想让工具彻底消失 |
| 子会话权限推导 | 决定子 agent 从父会话继承什么、默认禁什么 | packages/opencode/src/agent/subagent-permissions.ts | 子 agent 报权限不足,或想搞清嵌套边界 |
| task 工具 | 深度检查、权限询问、建子会话、跑任务、回传文本 | packages/opencode/src/tool/task.ts 与同目录 task.txt | 调 subagent 报错、想续跑同一个子会话 |
| 工具注册与 task 描述 | 组装工具集,把可派的子 agent 列表拼进 task 工具描述 | packages/opencode/src/tool/registry.ts | 模型死活不肯派活给你新写的 agent |
| markdown agent 加载 | 扫 {agent,agents}/**/*.md 和 {mode,modes}/*.md,frontmatter 当配置、正文当提示词 | packages/opencode/src/config/agent.ts | 文件放了没被识别 |
| 调试子命令 | 打印 agent 完整配置外加一张工具 true/false 表,也能单独执行某个工具 | packages/opencode/src/cli/cmd/debug/agent.ts 与 agent.handler.ts | 上线一个新 agent 前核实它的实际权限 |
| 文档 | agents.mdx 讲配置项与示例,是最快的上手入口 | packages/web/src/content/docs/agents.mdx | 忘了某个键叫什么 |
顺带给个规模感:这个仓库 packages/ 下有 32 个包,英文文档 36 份 mdx,packages/opencode/src/tool/ 下 25 个 .ts 配 15 个 .txt(工具描述单独放文本文件),packages/opencode/src/session/prompt/ 里 14 份提示词,全仓 6358 个受版本控制文件。项目采用 MIT 许可证,LICENSE 文件署的是 Copyright 2025 opencode。
六、什么活值得单开一个 agent
判断标准就三条,任意一条成立就值得开:
一、产出是结论而不是过程。 翻代码找实现、确认某个约定在项目里怎么用——中间要读十几个文件,最后你只要一句话。这类活扔给一个只读 agent,脏上下文留在子会话里。内置 explore 就是这个定位,它的 description 甚至要求调用方指定 quick / medium / very thorough 的彻底程度。
二、权限需要收窄。 代码评审、安全审计这类活,本来就不该有写文件的能力。开一个 edit: deny 的 agent,比每次自己提醒模型”别改代码”可靠得多。文档里给的两个示例 agent(文档写作、安全审计)都是这个路子。
三、能并行。 task.txt 明确建议在一条消息里发多个 tool call 来并发跑多个 agent,并且交代完就别自己重复干那份活。几块互不相干的工作同时推进,是子 agent 最实在的收益。
反过来,不值得开的活 task.txt 也列了:读一个已知路径的文件、搜一个具体的类定义、在两三个文件里找东西——直接用 read / glob / grep 更快。派一次 task 的固定开销(新会话、重新铺上下文、结果转述)不是零。
七、边界与代价
这套设计放弃了不少东西,得认。
没有跨 agent 的共享状态。 子会话默认全新上下文,回来只有一段文本。你想让两个子 agent 接力,只能靠 task_id 续同一个会话,或者自己把上一段结论写进下一次的 prompt。别指望它们之间有共享记忆。
它不是沙箱。 权限系统解决的是”要不要问你”,不是”能不能突破”。一旦某条规则求值成 allow,bash 就是真的在你机器上执行,edit 就是真的改你的文件。默认 ruleset 是 "*": "allow",也就是不配置等于全开——除了读 .env 会问一下。误删误改、把密钥连同代码一起发出去,这些风险在放开权限的那一刻就是实打实的。
规则的正确性靠你自己保证。 merge 就是数组拼接,不做冲突检测;写反顺序、拼错权限键名,都不会报错,只会安静地不生效。这也是为什么值得在改完配置后用调试子命令实际核一遍。
嵌套深度默认只有一层。 想做多层编排要显式调大 subagent_depth,而层数一多,上下文丢失和成本失控都会跟着放大。
它明确不管的事:不管多个子 agent 产出的合并与冲突,不管失败重试策略,不管跨会话的长期记忆,也不替你判断某个模型适不适合某类活。这些都在这套机制之外。
还有一条前面提过但值得重复:文档和代码会漂移。以仓库代码和你本机 opencode agent list 的实际输出为准。
八、上手与避坑清单
1. 通配符顺序写反。 为什么会踩:直觉上”具体规则应该优先”,但 evaluate 用的是 findLast,后面的赢。怎么避:宽的写前面、窄的写后面,"*": "ask" 永远放第一行。
2. 以为 deny 就能少发工具、省 token。 为什么会踩:disabled 只摘掉”最后匹配规则是 * 且 deny”的工具,细粒度 deny 的工具仍在工具表里。怎么避:真想让模型看不见某个工具,就整条 "*": "deny" 地关;只是想拦某几个操作,那就接受工具表不会变短。
3. 子 agent 报权限不足,回头去改主 agent 的配置。 为什么会踩:deriveSubagentSessionPermission 只把父会话的 deny 规则和 external_directory 规则带下去,allow 一律不继承。怎么避:把子 agent 需要的权限写在它自己的定义里。
4. 子 agent 没钉模型,却指望它便宜。 为什么会踩:不写 model 时它跟着发起调用的那条消息的模型走,主 agent 用什么它就用什么。怎么避:想省钱就显式钉住;同时留意钉了 model 之后 variant 不再沿用父消息。
5. markdown 文件放了不生效。 为什么会踩:目录名和层级记混。怎么避:agent 的 glob 是 {agent,agents}/**/*.md,单复数都行、可嵌套;但 {mode,modes}/*.md 只扫一层,且解析出来会被强制成 mode: "primary"——放错目录,你写的 mode: subagent 会被覆盖掉。
6. description 随手写一句,然后抱怨模型不派活。 为什么会踩:describeTask 把 description 原样拼进 task 工具描述,那是模型选 agent 的唯一线索;缺了会显示成”只能由用户手动调用”。怎么避:description 里写清”什么时候用它”,而不只是”它是什么”。
7. 做编排 agent 时忘了收口。 为什么会踩:默认 "*": "allow" 意味着它能派任何子 agent。怎么避:用 permission.task 先 "*": "deny" 再按前缀放行;注意文档也说了,用户通过 @ 手动调用不受这条限制。
8. 改完配置直接上生产任务。 为什么会踩:规则错了不报错。怎么避:先用仓库里那条 debug 用的 agent <name> 子命令(packages/opencode/src/cli/cmd/debug/agent.ts,describe 是 show agent configuration details)打印实际配置和工具开关表,确认再用;它还支持 --tool 和 --params 单独跑一个工具验证。
收束
opencode 的多 agent 配置,本质上是”给每个角色写一份规则表,再让 findLast 去裁决”。上手时按这个顺序自检就够了:mode 对不对,permission 的通配符顺序对不对,model 钉没钉,description 能不能让模型认出该派给它,深度会不会撞上 subagent_depth。
接下来想再深一层,按这个顺序读:packages/opencode/src/agent/agent.ts 看内置默认值,packages/opencode/src/permission/index.ts 看求值规则,packages/opencode/src/agent/subagent-permissions.ts 看继承边界,packages/opencode/src/tool/task.ts 看派活全流程。四个文件加起来一千行出头,读完你对这套机制的判断会比任何二手文章都准。
想横向对比不同工具在权限模型上的取向差异,可以接着看 Agent 最小权限设计。
本篇属于一个把开源AI 编程 Agent 项目 opencode逐层拆开讲的系列,整体地图见 opencode 是什么:终端里的开源 AI 编程 Agent 全景地图;沿着这条线往下,还可以看 开源终端 Agent opencode 界面用熟:键位、主题与区块含义 和 终端 AI 编程 Agent opencode 的权限闸门怎么设。