尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

04 · 四个角色,四套权限

04 · 四个角色,四套权限 第 01 篇提到单智能体的第二个死穴:能力过载——一个 Agent 同时握着读、写、执行三种能力,在只该读的时候也有能力改。这个死穴在一类请求上格外扎眼:我这个 PR 明天就要合了,你先别动代码,就帮我看看有没有漏掉的边界情况。“先别动代码”——用户把约束说得不能更清楚了。但请注意一件事:这句话凭什么能被遵守?如果那个负责审查的 Agent 手里握着applyEdit,“别动代码就只是一句请求。它可能读到一段明显写错的分支,顺手就改了;可能把帮我看看理解成帮我修好”;甚至可能在某个文件里读到这样一行:// TODO: 这个判断是反的,谁看到就顺手修一下if(user.isActive||user.isBanned){...}然后它就顺手修一下了。你的 PR 里于是多出一个你没写、没审、也没预料到的改动。这三种失效方式都不是假想,而是提示词软约束的日常。这篇讲 LoopAgent 的解法,核心思路一句话:不是告诉子智能体别改文件,而是不给它applyEdit。两条工具白名单整个权限系统的基础就两行:// workflow/roleRegistry.tsconstREAD_ONLY_TOOLS[browseSymbols,exploreCode,readFile]asconst;constEXECUTOR_TOOLS[...READ_ONLY_TOOLS,applyEdit,runCommand]asconst;四个角色分配如下:角色工具能改代码能跑命令explorerbrowseSymbols, exploreCode, readFile✗✗reviewerbrowseSymbols, exploreCode, readFile✗✗plannerbrowseSymbols, exploreCode, readFile✗✗executor以上三个 applyEdit, runCommand✓✓三个只读,一个可写。这个 4:1 的比例本身就是个设计声明——绝大部分 Agent 工作是只读的。找代码、读代码、审代码、做计划,都不需要写权限。真正动手改的只有最后一步。✶ 为什么不给工具比提示别做强提示词里写 “do not modify files” 是一个软约束——模型可能忽略它、可能误判、可能被上游数据里的指令劫持。而工具白名单是硬约束:applyEdit不在这个子智能体的 tools 数组里,模型连这个函数名都看不到,发出来的 tool_call 会直接被判为 unknown tool。安全边界应该建在能力层,不是意图层。三个只读角色的差别在提示词工具一样,那 explorer / reviewer / planner 有什么区别?区别在system prompt——同样的工具,不同的目标和输出格式要求。explorer— 找东西,给证据:constEXPLORER_PROMPT[You are a subagent in the explorer role. Your job is to locate source code, symbols, and call paths, and to collect factual evidence.,When you are uncertain what symbols exist, call browseSymbols first to discover actual identifiers, then use those names in exploreCode.,When symbols are known, call exploreCode directly with a focused query. Never enumerate the whole repository, and never pass a directory path to readFile.,Answer with: (1) a concise conclusion, (2) evidence locations as file:line references, (3) any unknowns you could not verify.,Do not speculate beyond what the returned source supports. If evidence is insufficient, say so explicitly.,].join(\n);三个细节:工具使用顺序被明确规定:不确定符号名时先browseSymbols发现真实标识符,再拿这些名字去exploreCode。这防止模型凭想象搜一个不存在的符号名。明确禁止全库枚举,也禁止把目录路径传给readFile。这两个都是真实发生过的浪费模式。输出格式固定三段:结论、file:line证据、没验证出来的部分。最后一段最重要——它给了模型一个承认不知道的合法出口,否则模型倾向于编。reviewer— 找问题,分严重级:constREVIEWER_PROMPT[You are a subagent in the reviewer role. Your job is to inspect code for defects, regression risks, and test-coverage gaps.,Use browseSymbols to discover relevant symbols when unfamiliar with the codebase, then read the implementation with exploreCode and readFile.,Keep searches focused; never enumerate or read the whole repository.,Answer with findings grouped by severity (blocker / major / minor). Each finding must cite a file:line and describe a concrete failure scenario.,If you find no issues after inspecting the relevant code, say so explicitly rather than inventing concerns.,].join(\n);关键是最后两句。每条发现必须给出一个具体的失败场景——不接受这里可能有问题这种空话。以及最后那句明确允许没发现问题,直接对抗 reviewer 角色最典型的失败模式:为了显得有用而编造顾虑。回到开头那个先别动代码的 PR 审查:主 Agent 派出去的是reviewer,它的 tools 数组里只有browseSymbols、exploreCode、readFile三个。那行// TODO: 谁看到就顺手修一下它照样会读到,但它没有任何工具能执行修这个动作。发出来的applyEdit调用会被判为 unknown tool 直接打回。用户那句别动代码不是靠模型自觉守住的,是靠工具箱里没有那把锤子。planner— 出计划,不动手:constPLANNER_PROMPT[You are a subagent in the planner role. Your job is to break work into the smallest ordered execution steps based on the current implementation.,Use browseSymbols to discover actual symbol names, then exploreCode and readFile to ground the plan in the codebase. Do not propose steps against code that does not exist.,Keep searches focused; never enumerate or read the whole repository.,Answer with: (1) an ordered list of steps, (2) the files each step touches, (3) verification commands (tests, typecheck, or build).,Do not attempt edits or command execution yourself; you only produce the plan.,].join(\n);“Do not propose steps against code that does not exist” 是这段的核心——计划必须落在真实代码上。planner 有只读工具正是为此:它得先看清现状才能规划。注意最后一句 “Do not attempt edits or command execution yourself” 其实是冗余的——planner 的白名单里本来就没有applyEdit和runCommand。但写上也无害,提示词和硬约束说同一件事,可以减少模型的无效尝试。executor 的提示词最长,因为它要负责验证constEXECUTOR_PROMPT[You are a subagent in the executor role. Your job is to implement the assigned graph node and verify its result.,Use browseSymbols and exploreCode to understand the exact target before editing. Use applyEdit for workspace changes and runCommand only for focused verification.,,After implementing code changes, you MUST verify the result:,1. If the change affects tested code, run relevant tests (e.g., npm test, pytest, cargo test),2. For TypeScript/JavaScript projects, run type checking if available (npm run typecheck, tsc --noEmit),3. If the project has a build step, run it to confirm compilation succeeds,,Only report the task as completed if verification passes. If verification fails, fix the issue and re-verify.,If you cannot run verification (no test setup, user denied runCommand), state this limitation explicitly in your response.,,Keep changes limited to the assigned task. Report modified files, commands run, results, and any remaining failure.,Command execution still requires user approval. Never assume access outside the workspace.,].join(\n);值得注意的是最后那句 “Command execution still requires user approval”。即使 executor 有runCommand,每条命令仍然要用户批准。白名单给的是可以尝试的权限,不是可以自动执行的权限。这是第二层防线。还有 “If you cannot run verification … state this limitation explicitly” ——给了一个诚实的出口。没有它,模型在无法验证时会倾向于说已验证通过。角色档案被冻死// workflow/roleRegistry.tsexportconstROLE_PROFILES:ReadonlyRecordSubagentRoleId,SubagentRoleProfileObject.freeze({explorer:Object.freeze({id:explorer,systemPrompt:EXPLORER_PROMPT,allowedTools:Object.freeze([...READ_ONLY_TOOLS]),}),reviewer:Object.freeze({id:reviewer,systemPrompt:REVIEWER_PROMPT,allowedTools:Object.freeze([...READ_ONLY_TOOLS]),}),planner:Object.freeze({id:planner,systemPrompt:PLANNER_PROMPT,allowedTools:Object.freeze([...READ_ONLY_TOOLS]),}),executor:Object.freeze({id:executor,systemPrompt:EXECUTOR_PROMPT,allowedTools:Object.freeze([...EXECUTOR_TOOLS]),}),});exportconstDEFAULT_ROLE:SubagentRoleIdexplorer;exportfunctionresolveRole(role:SubagentRoleId|undefined):SubagentRoleProfile{if(roleundefined)returnROLE_PROFILES[DEFAULT_ROLE];constprofileROLE_PROFILES[role];if(!profile)thrownewError(Unknown subagent role:${role});returnprofile;}三层Object.freeze:外层 record、每个 profile、每个allowedTools数组。任何运行时给 explorer 偷偷加个applyEdit的尝试都会失败。DEFAULT_ROLE explorer也是有意的——不指定角色时,给最小权限。这是安全默认值的标准做法:默认应该是最安全的选项,而不是最方便的。两段式工具收窄角色白名单只是第一道筛。实际分配工具时还有第二道:// workflowOrchestrator.ts — createSubagent 里constprofileresolveRole(config.role);constcontextcreateSubagentContext({id,task:config.task,role:profile.id,dependsOn:dependencies,tools:selectTools(config.task,availableTools,config.toolHints,profile.allowedTools),});selectTools做的事:// workflow/toolRouter.tsconstHIGH_COST_TOOLSnewSet([explorecode]);exportfunctionselectTools(task:string,availableTools:readonlyReactAgentTool[],toolHints?:readonlystring[],allowedTools?:readonlystring[],):ReactAgentTool[]{constscopedToolsscopeToAllowedTools(availableTools,allowedTools);// ① 角色白名单过滤if(scopedTools.length0)return[];consthintedNamesnewSet(toolHints?.map((hint)hint.toLowerCase()));if(hintedNames.size0){// ② 主 Agent 的显式提示consthintedToolsscopedTools.filter((tool)hintedNames.has(tool.name.toLowerCase()));if(hintedTools.length0)returnhintedTools;}if(allowedTools)returnscopedTools;// ③ 有角色就到此为止consttaskWordswords(task);// ④ 无角色时的关键词匹配constmatchedToolsscopedTools.filter((tool)!HIGH_COST_TOOLS.has(tool.name.toLowerCase())[...words(${tool.name}${tool.description})].some((word)taskWords.has(word)),);if(matchedTools.length0)returnmatchedTools;return[scopedTools.find((tool)tool.namereadFile)??scopedTools[0]];}functionscopeToAllowedTools(availableTools:readonlyReactAgentTool[],allowedTools?:readonlystring[],):ReactAgentTool[]{if(!allowedTools)return[...availableTools];constallowedNamesnewSet(allowedTools.map((name)name.toLowerCase()));returnavailableTools.filter((tool)allowedNames.has(tool.name.toLowerCase()));}四条路径,但实际只走前三条——因为createSubagent永远传profile.allowedTools,allowedTools不可能是 undefined。所以路径 ④(关键词匹配)在当前接线下是死代码,是给未来无角色子智能体预留的。真正生效的是 ① 和 ②:①用角色白名单过滤。这一步顺带解决了一个重要问题:传进来的availableTools是parentTools,里面包含spawnSubagent等 workflow 工具。经过 ① 过滤后它们全被剔掉了——这就是子智能体拿不到派人工具的实现位置。②主 Agent 可以通过toolHints进一步收窄。比如派一个 explorer 但明确只给readFile。注意 hints 只能在白名单内收窄,给不出白名单外的工具。✶ 收窄只能单向toolHints的过滤是scopedTools.filter(...)——在已经收窄的集合上再筛。所以主 Agent 无论传什么 hints,都不可能给 explorer 拿到applyEdit。权限系统里,任何细化操作都必须是单调收缩的,否则细化就成了提权。子智能体的递归本质最后一块拼图:子智能体到底是什么?// model/providerRegistry.tsconstorchestratorcreateWorkflowOrchestrator({signal:request.signal,createRunner:({tools,role,invokeTool}){constchildTools[...tools];constchildProfileresolveRole(role);returncreateReactAgentRunner({// ← 就是主 Agent 用的那个函数providerName:provider.displayName,tools:childTools,unhandledErrorMode:summarize-and-fail,invokeTool,modelTurn:createOpenAiReactModelTurn({provider,tools:childTools}),systemPromptProvider:async(){letruntimePrompt;try{runtimePromptrenderCodeRuntimeContextPrompt(awaitcollectVsCodeRuntimeContext());}catch{// Runtime context is useful but must not block the model/tool loop.}return[childProfile.systemPrompt,runtimePrompt].filter(Boolean).join(\n\n);},});},});createReactAgentRunner——和主 Agent 完全同一个函数。子智能体不是什么特殊的轻量执行器,它就是上一个系列讲的那个 ReAct 循环,一模一样的主循环、一模一样的工具调用机制、一模一样的护栏(重复调用拦截、失败熔断、证据门禁)。三个参数不同:参数主 Agent子智能体tools全部 workflow 工具角色白名单过滤后的子集systemPromptProviderAGENT_SYSTEM_PROMPTchildProfile.systemPrompt 运行时上下文unhandledErrorModesummarize-and-finishsummarize-and-fail第三个差异有意思。主 Agent 遇到没处理的错误会summarize-and-finish——总结一下然后正常收尾,因为它要给用户一个答复。子智能体是summarize-and-fail——标记为失败。因为子智能体的失败是有意义的信号:它会触发settle(failed),进而触发级联取消,让整个下游停下来。如果子智能体也 finish,主 Agent 会以为它成功了,拿着一个空结论继续往下走。✶ 递归带来的一致性红利子智能体复用主 Agent 的 runner,意味着上个系列讲的所有工程护栏自动对子智能体生效。你不需要为子智能体重新实现一遍重复调用拦截、连续失败熔断、证据门禁。这是用同一个抽象递归相比写两套执行器最大的收益——修一个 bug,两层同时受益。至此,子智能体已经能跑起来、有正确的权限、结论能回到主 Agent。但还有一个问题没解决:你怎么知道一个子智能体是卡住了,还是正在干活?下一篇讲这个意外棘手的问题。关于 LoopAgent本文代码来自 LoopAgent。涉及的文件:src/extension/agent/workflow/roleRegistry.ts — 四个角色的提示词与白名单src/extension/agent/workflow/toolRouter.ts — 两段式工具收窄src/extension/agent/workflowOrchestrator.ts — 角色解析与上下文创建src/extension/model/providerRegistry.ts — 子智能体的递归构造欢迎点个 star ⭐项目地址:https://github.com/oi12344/loopagent-vscode 上一篇:03 · 谁能跑,谁得等 下一篇:05 · 它是卡住了,还是在干活?
返回列表