1. 为什么 opencode 自动循环需要一个独立审计代理
如果你用 opencode 跑过多轮自动任务,大概率遇到过这种情况:脚本每隔几秒发一句「继续」,模型回一句「已完成」,循环退出,你打开代码一看——测试没跑、边界条件没处理、甚至语法都没过。问题不在于模型能力不够,而在于执行者自审天然不可靠。同一个会话里,模型既当运动员又当裁判,它倾向于把「我觉得差不多了」当成「已经完成」。
goal.ps1这个 PowerShell 脚本要解决的就是这件事:它驱动 opencode 会话持续执行任务,但把「完成确认权」从主代理手里拿走,交给一个通过 Task 工具启动的独立子代理。主代理只能输出<VERIFY>表示「我请求审计」,真正能终止循环的<DONE>必须来自子代理,并且要被原样粘贴进<AUDIT_REPORT>标签里。脚本解析这段报告,只有看到独立成行的<DONE>才退出。
这套机制适合谁?适合用 opencode 做批量修复 lint、跑回归测试、代码迁移、持续重构的开发者,尤其是希望无人值守跑长任务、又不想被虚假完成信号坑的人。它不依赖任何特殊网络环境,纯本地 PowerShell + opencode CLI 即可运行。下面我会把可复制的配置片段、审计代理的调用写法、以及一次「审计失败自动重跑」的完整验证动作拆开讲清楚。
核心检索词先明确:goal.ps1 是 opencode 的自动循环脚本,通过独立审计代理做交叉校验,用 PowerShell 实现任务完成确认。理解这一点,后面的配置和排障才有落脚点。
2. TaoToken 前置:给 opencode 配好可用的模型入口
opencode 本身是 CLI 工具,它需要一个能调用的模型服务。我这边习惯用 TaoToken 作为统一入口,原因是它的 Base URL 和 Key 管理比较清晰,opencode、Cline、Codex 这类工具都能共用一套配置。官网在 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 地址是 https://taotoken.net/api (这个不加 UTM)。
先说清楚三件套,任何接入都绕不开:Base URL、API Key、Model ID。opencode 读取配置的方式是项目根目录或用户目录下的opencode.json,也可以用环境变量。我推荐用配置文件,方便版本管理。
一个最小可用的opencode.json长这样:
{ "$schema": "https://opencode.ai/config.json", "provider": { "taotoken": { "npm": "@ai-sdk/openai-compatible", "name": "TaoToken", "options": { "baseURL": "https://taotoken.net/api", "apiKey": "{env:TAOTOKEN_API_KEY}" }, "models": { "claude-sonnet-4-5": { "name": "Claude Sonnet 4.5" }, "gpt-5": { "name": "GPT-5" } } } }, "model": "taotoken/claude-sonnet-4-5" }注意apiKey我写的是{env:TAOTOKEN_API_KEY},这是 opencode 的环境变量插值语法,避免把 Key 硬编码进仓库。你在 PowerShell 里这样设置:
$env:TAOTOKEN_API_KEY = "sk-你的key"Key 从哪来?登录 TaoToken 控制台,在 API Keys 页面创建。地址是 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。创建后复制一次,之后不再显示,建议直接写进系统环境变量而不是临时会话:
[Environment]::SetEnvironmentVariable("TAOTOKEN_API_KEY", "sk-你的key", "User")配好之后先验证模型能不能通。opencode 有个交互式对话入口,可以直接测:
opencode run --model taotoken/claude-sonnet-4-5 "回复 OK 两个字母"如果返回里出现OK,说明 Base URL、Key、Model ID 三件套都对。这一步很关键,因为 goal.ps1 的审计代理也要走同一个模型入口,模型不通的话循环第一轮就会卡死。想先在网页端确认模型可用性,可以用模型对话页 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite 发一条消息试试。
如果你打算长期跑编码 Agent 任务,Coding Plan 会比按量更划算,入口在 https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。不过 goal.ps1 本身不关心你用什么计费方式,只要模型能调通就行。
这里有个容易踩的坑:opencode 的 provider 名和 model 名是拼在一起的,taotoken/claude-sonnet-4-5里斜杠前是 provider key,斜杠后是 models 里定义的 key。写错了会报 model not found,而不是 401,别被误导。
3. 可复制配置:goal.ps1 的参数与审计提示注入
goal.ps1 的参数解析是手写的 while 循环,不走 PowerShell 的param()块。原因很实际:param()会按声明顺序把位置字符串往第一个非 switch 参数上套,ses_xxx这种字符串会被尝试转成[int]$Interval然后炸掉。手写解析还用了-ceq(大小写敏感比较),保证-m(model)和-M(max-iterations)不会互相误匹配。
先看一次典型调用:
.\goal.ps1 --new "为 auth 模块补充单元测试" ` --model taotoken/claude-sonnet-4-5 ` --interval 5 ` --max-iterations 10 ` --audit strict ` --debug参数含义对照如下:
| 参数 | 短写 | 作用 | 默认值 |
|---|---|---|---|
--new | -n | 新建会话,必须带 prompt | 无 |
--model | -m | 指定 provider/model | 配置文件里的默认 |
--interval | -i | 每轮轮询间隔秒数 | 3 |
--timeout | -t | 单次 opencode 调用超时秒数 | 1800 |
--max-iterations | -M | 最大循环轮数 | 25 |
--audit | -a | 审计严格度 auto/strict | auto |
--session | -s | 指定会话 ID,须以 ses_ 开头 | 无 |
--debug | -d | 输出内部 [log] 诊断 | 关 |
--once | -1 | 单次执行,不循环不审计 | 关 |
审计提示的注入逻辑在Invoke-SelfAudit函数里。当主代理输出<VERIFY>,脚本不再发「继续」,而是发一段强制审计提示。strict 模式下的提示核心是这几条规则:
>> AUDIT REQUIRED << You have NO authority to claim completion. YOU MUST launch a SEPARATE sub-agent via the Task tool. Sub-agent instructions: Task: <原始任务描述> 1. AUDIT the codebase. Find ALL remaining issues. 2. FIX every issue yourself. 3. When 100% complete, output EXACTLY <DONE> on its own line. 4. If ANY issue remains, describe it. Do NOT output <DONE>. After the sub-agent responds: 1. Copy the sub-agent's ENTIRE response verbatim below. 2. Output it between AUDIT_REPORT tags. Format: <AUDIT_REPORT> [paste sub-agent full response here] </AUDIT_REPORT>脚本解析返回文本时用的是单行模式正则:
$reportMatch = [regex]::Match( $auditText, '<AUDIT_REPORT>\s*(.*?)\s*</AUDIT_REPORT>', [System.Text.RegularExpressions.RegexOptions]::Singleline ) if ($reportMatch.Success) { $reportContent = $reportMatch.Groups[1].Value if ($reportContent -match '(?m)^\s*<DONE>\s*$') { return $true } }(?m)是多行模式,^和$匹配行首行尾,这样只有独立成行的<DONE>才算数,正文里提到「不要输出 」这种句子不会被误判。这是防伪造的关键细节。
还有一个工程细节值得说:Windows 命令行长度上限约 2047 字节,审计提示加上会话参数很容易超。脚本用 UTF-8 字节数判断,超过 2000 就把提示文本从 argv 里摘出来,改走 stdin:
$useStdin = ([System.Text.Encoding]::UTF8.GetByteCount($argsStr) -gt 2000) if ($useStdin) { $stdinMessage = $argv[-1] $argv.RemoveAt($argv.Count - 1) }进程启动后通过RedirectStandardInput写入再关闭。这个处理不做的话,长审计提示会被截断,子代理收到的指令不完整,审计质量直接崩。
4. 验证请求:一次审计失败后自动重跑与日志留痕
配置讲完,来跑一次真实场景。我准备了一个故意留 bug 的小项目:一个calc.py,里面有个除零没处理。任务描述是「修复 calc.py 的所有边界问题并补充测试」。
启动命令:
.\goal.ps1 --new "修复 calc.py 的所有边界问题并补充测试" ` --model taotoken/claude-sonnet-4-5 ` --interval 5 ` --max-iterations 8 ` --audit strict ` --debug第一轮,主代理改完代码,输出<VERIFY>。脚本检测到后注入审计提示。子代理审查后发现测试没覆盖除零分支,返回问题列表,没有<DONE>。主代理把这段原样贴进<AUDIT_REPORT>。脚本解析:
[goal] Sub-agent audit report does NOT contain <DONE> - task not complete. [goal] Self-audit did not pass - continuing loop ...循环继续。第二轮,主代理根据审计报告补了测试,再次<VERIFY>。子代理这次确认无遗留问题,输出<DONE>。脚本解析到独立行的<DONE>,break退出循环,调用Exit-Success打印统计:
======================================== [goal] Start : 2025-01-15 10:22:31 [goal] End : 2025-01-15 10:24:07 [goal] Elapsed: 00:01:36 [goal] Rounds : 2日志留痕方面,--debug打开后每个关键节点都有[log]输出,包括参数构造、退出码、stdin 路由决策、审计提示长度:
[log] runArgs count=3 [log] [0] run [log] [1] --session [log] [2] ses_abc123... [log] auditPrompt len=1842 first40=>> AUDIT REQUIRED << [log] END: exit=0 elapsed=12.4s这些日志默认走Write-Log,$Debug为 false 时是空操作,不影响正常输出。想持久化就重定向:
.\goal.ps1 --new "任务" --debug *> goal-run.log验证「审计失败自动重跑」这个动作,最直接的办法是故意让子代理找不到<DONE>。你可以临时把审计提示里的<DONE>条件改严,或者在一个永远修不完的任务上跑,观察Rounds是否递增、每轮是否都重新注入审计提示。实测下来,只要<AUDIT_REPORT>里没有独立行的<DONE>,循环就不会退出,这正是我们要的行为。
会话 ID 的捕获也值得验证。--new模式下脚本先取会话列表快照beforeIds,发送初始 prompt 后再取afterIds,差集得到新会话 ID:
$newIds = @() foreach ($id in $afterIds) { if ($id -notin $beforeIds) { $newIds += $id } } $sessionId = $newIds[0] $loopSessionArgs = @('--session', $sessionId)这样即使并发跑多个 goal.ps1 实例,也不会串会话。你可以开两个终端同时跑,看日志里的Captured session是否不同。
5. 本篇常见错排查:401、local proxy failed 与审计不触发
跑 goal.ps1 时最常见的几类报错,我按出现频率排一下。
401 Unauthorized。这几乎都是 Key 没生效。先确认环境变量在当前会话可见:
echo $env:TAOTOKEN_API_KEY如果为空,说明你设的是 User 级变量但当前终端是设置之前打开的,重开终端或手动$env:TAOTOKEN_API_KEY = "..."。如果 Key 有值还报 401,检查opencode.json里baseURL是不是写成了https://taotoken.net/api/(末尾多斜杠有时会导致路径拼接异常),以及 provider 名和 model 名是否匹配。三件套里任何一件错位都会以 401 或 model not found 的形式暴露。
local proxy failed / connection refused。这类报错通常不是 goal.ps1 的问题,而是 opencode 调用模型服务时网络层没通。先单独跑一次opencode run --model taotoken/claude-sonnet-4-5 "test",如果这条也失败,问题在模型接入层,跟循环脚本无关。确认 Base URL 是https://taotoken.net/api,不要带多余路径。
reading choices 相关报错。这通常出现在模型返回体格式不符合预期时,比如 provider 配成了不兼容的 npm 包。opencode.json里npm字段要用@ai-sdk/openai-compatible,这是 OpenAI 兼容协议的标准适配器。用错适配器会导致响应解析失败,报错里常带choices字样。
审计不触发,循环直接退出。检查主代理输出里<VERIFY>是否独立成行。脚本用的是(?m)^\s*<VERIFY>\s*$,如果模型把<VERIFY>写在句子中间,匹配不到。解决办法是在 continue prompt 里强调「output '' on its own line」,脚本自带的 continuePrompt 已经这么写了,但模型偶尔不遵守,可以在任务描述里再强调一次。
OAuth 相关报错。opencode 某些版本会尝试 OAuth 登录流程,如果你用的是 API Key 模式,确保没有残留的 OAuth 凭据干扰。检查用户目录下的 opencode 配置,清掉旧的 auth 缓存再试。
审计报告解析失败。日志里出现AI did not produce <AUDIT_REPORT> tags,说明主代理没按格式粘贴子代理响应。这属于模型不遵守协议,strict 模式比 auto 模式约束更强,建议长任务一律用--audit strict。如果反复出现,把--max-iterations调大给它更多纠正机会,或者换一个指令遵循更好的模型。
排障时如果怀疑是模型接入问题,可以去接入文档 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite 对照配置项,或者直接在模型对话页发一条消息确认服务可用。Key 管理在 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。
6. 把审计关接进你的 opencode 工作流
goal.ps1 的价值不在于脚本本身多复杂,而在于它把「完成确认」这件事从执行者手里剥离出来,变成一个可自动化的交叉校验动作。你不需要改 opencode 的源码,也不需要额外的服务,一个 PowerShell 脚本加一段审计提示就能跑起来。
实际用的时候,我建议先把--max-iterations设小一点(比如 5),--interval设 5 到 10 秒,观察几轮日志确认审计链路通了,再放开跑长任务。--audit strict适合交付质量要求高的场景,auto适合探索性任务。--debug在调试阶段打开,稳定后关掉减少噪音。
如果你要把它接进 CI 或者定时任务,注意 opencode 需要在 PATH 里,且TAOTOKEN_API_KEY要在运行环境可见。脚本对 opencode 是.ps1包装器的情况做了兼容,会尝试直接调用node_modules\opencode-ai\bin\opencode.exe,避免pwsh -File二次解析引号的问题。
最后留一个实用技巧:审计提示里的任务描述$TaskDescription用的是你启动时传的原始 prompt。如果你希望审计聚焦在某个具体文件或模块,把任务描述写具体,比如「修复 calc.py 的除零和空输入边界,补充 pytest 用例」,子代理的审查范围会更准,误报和漏报都会少。任务描述越模糊,审计质量越依赖模型自由发挥,这是这套机制里最需要你手动调优的地方。