
深度解析team_create双参冲突死循环基于 Oh My OpenAgent 的 inline_spec 优先级修复实证【免费下载链接】oh-my-openagentOmO: Just type mass ulw keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagentteam_create是 Senpi 团队生命周期中的核心工具用于创建多智能体协作团队。本文基于仓库中.omo/evidence/20260804-team-create-recovery/目录下的完整诊断与修复记录深入剖析一个真实发生、反复出现的模型侧故障模型在调用team_create时同时传入team_name与inline_spec两个互斥参数导致工具反复返回invalid_arguments即使模型已正确理解并复述互斥规则仍然持续重试直至死循环。文章将还原故障现场、分析根因、讲解inline_spec优先级修复方案及其验证过程。故障现场还原主会话同样的错误重复七次在原始会话019fcb34-df92-78b4-af23-747951793586中模型kimi-k3-ultrafast-unlocked走openai-completions兼容通道对team_create工具连续发起七次调用调用参数形态结果team_create:23team_nameinline_specinvalid_argumentsteam_create:24仅team_name命名规格未找到team_create:25team_nameinline_specinvalid_argumentsteam_create:26team_nameinline_specinvalid_argumentsteam_create:27team_nameinline_specinvalid_argumentsteam_create:28team_nameinline_specinvalid_argumentsteam_create:29team_nameinline_specinvalid_argumentsteam_create:30team_nameinline_specinvalid_arguments更关键的是从:25到:30模型的推理文本里明确写着自己要移除team_name但最终发出的工具参数中仍然同时包含两个字段。每次失败结果的isError都为false——即模型并未把失败视为硬错误于是陷入「理解规则 → 声称修正 → 实际依旧双字段 → 再次失败」的循环。独立会话并非偶发在另一独立会话019fc0fb-d81b-7d40-9e58-c1ae3130f858同样的模型/提供商路径中双字段调用在team_create:39、:46、:47、:49再次出现。两个独立 Kimi 会话重复相同形态的调用说明这不是单次生成抖动而是模型对「两个可选兄弟字段 散文式互斥说明」这一参数模式的系统性失稳。根因分析为什么模型会反复犯错问题一XOR 规则只存在于散文和运行时校验中修复前的TeamCreateParams暴露了两个可选的兄弟字段见 lifecycle.tsteam_name指定命名团队规格项目.omo/teams或omo.jsoninline_spec内联团队规格。「二者只能选其一」的互斥XOR规则此前只存在于工具描述的散文文本与runTeamCreate的运行时校验里const hasName params.team_name ! undefined params.team_name.length 0 const hasInline params.inline_spec ! undefined if (!hasName !hasInline) { return toolErrorResult(Provide team_name or inline_spec., { kind: invalid_arguments, reason: provide team_name or inline_spec }) }问题二schema 层面的oneOf/anyOf在此路径不可靠一个直觉上的修复方向是在 JSON Schema 层面声明互斥如oneOf。但诊断记录明确指出这条路在此处走不通。Senpi 的 Moonshot 兼容性归一化见兄弟路径packages/ai/src/utils/tool-schema-compat.ts会展平根级对象联合类型且只保留所有分支的共同要求。这意味着oneOf/anyOf的互斥语义在归一化后会被稀释甚至丢失模型看到的 schema 依旧允许双字段共存。问题三把结果标记为错误治标不治本另一个候选方案是把invalid_arguments的isError置为true让模型把失败当作硬错误处理。但诊断明确指出主会话的模型已经读懂了文本错误、已经正确地复述了互斥规则却依旧输出相同的参数形态。问题不在「模型没有意识到错误」而在「模型受 schema 诱导反复产生双字段输出」。因此强化错误标记只会让模型更频繁地撞上同一堵墙。修复方案让inline_spec成为权威来源决策原则修复的核心理念是把互斥从「运行时拒绝」改为「确定性优先级」只要inline_spec存在就以它为权威来源team_name仅在未提供inline_spec时才被使用。更丰富的内联载荷可以在第一次过度指定调用时就执行而不是进入由模型驱动的重试循环。这一决策同时满足了诊断中的三个约束不新增抽象层、不改变无关错误语义、保持所有边界情况。源码实现修复后的runTeamCreatelifecycle.ts核心逻辑如下export async function runTeamCreate(service: TeamToolsService, params: TeamCreateInput): PromiseToolExecutionResultTeamCreateDetails { const hasName params.team_name ! undefined params.team_name.length 0 const hasInline params.inline_spec ! undefined if (!hasName !hasInline) { return toolErrorResult(Provide team_name or inline_spec., { kind: invalid_arguments, reason: provide team_name or inline_spec }) } let inlineSpec: unknown if (hasInline) { const coerced coerceInlineSpec(params.inline_spec) if (!coerced.ok) { return toolErrorResult(coerced.reason, { kind: invalid_arguments, reason: coerced.reason }) } inlineSpec coerced.spec } try { const result await service.createTeam( hasInline ? { inlineSpec } : { teamName: params.team_name }, ) // ... } catch (error) { if (error instanceof SenpiTeamSpecError) return toolErrorResult(error.message, { kind: spec_error, code: error.code, reason: error.message }) if (error instanceof SenpiTeamRuntimeError) return toolErrorResult(error.message, { kind: runtime_error, code: error.code, reason: error.message }) throw error } }关键变化点双字段时以inline_spec为准hasInline ? { inlineSpec } : { teamName: params.team_name }服务调用只携带内联规格invalid_arguments保留两种场景两个字段都没有、或inline_spec是非法 JSON 字符串coerceInlineSpec负责把 JSON 字符串自动解析为对象错误分类不变spec_error规格无效、runtime_error派生/边界失败的语义保持原样。参数 schema 同步更新工具描述与参数 schema 同步说明了新语义lifecycle.tsexport const TeamCreateParams Type.Object({ team_name: Type.Optional( Type.String({ description: Named team spec (project .omo/teams or omo.json) to create. Ignored when inline_spec is also provided. }), ), inline_spec: Type.Optional( Type.Union([InlineTeamSpecSchema, Type.String({ description: The same spec as a JSON string; parsed automatically. Passing the object form is preferred. })], { description: Inline team spec, e.g. { name, members: [{ name, category|subagent_type, prompt? }] }. A JSON string of the same object is also accepted and parsed automatically. Takes precedence when team_name is also provided., }), ), })inline_spec支持两种形态对象形态推荐{ name?, members: [...] }其中members既可以是数组也可以是单个成员对象自动包裹为数组JSON 字符串形态同一对象的 JSON 字符串由coerceInlineSpec自动解析解析失败返回invalid_arguments。CREATE_DESCRIPTION也明确写出「inline_spectakes precedence when both are provided」双字段时inline_spec优先。内联规格的成员定义InlineTeamSpecMemberSchemalifecycle.ts定义了内联成员的字段字段类型说明name可选字符串成员名团队内唯一按小写词干归一化kind可选枚举category/subagent_type/agentagent是subagent_type的别名缺省时按category/subagent_type字段推断category可选字符串以 category 路由该成员subagent_type可选字符串以指定 agent 定义运行该成员prompt可选字符串成员指令必须用英文书写task_summary可选字符串成员任务的一句话摘要展示在任务页脚/小组件 UI 中超长会被强制截断到 80 字符TASK_SUMMARY_MAX_LENGTH团队本身有两点约定name缺省时自动派生内联名当前会话始终是 lead不要声明 lead 成员。边界情况与回归测试行为矩阵修复后的runTeamCreate保持所有边界语义输入行为仅team_name走命名规格查找仅inline_spec走内联规格创建两者都没有返回invalid_argumentsinline_spec为非法 JSON 字符串在服务调用之前即失败返回invalid_arguments双字段同时提供inline_spec优先直接执行内联创建回归测试先红后绿修复采用 TDD 流程新增测试lifecycle-precedence.test.tspackages/senpi-task/src/tools/team/lifecycle-precedence.test.tsdescribe(team_create inline precedence, () { test(#given both team_name and inline_spec #when team_create runs #then inline_spec is authoritative, async () { // given const inlineSpec { name: inline-team, members: [] } const service createFakeTeamService({ createTeam: async () fakeCreateResult() }) // when const result await runTeamCreate(service, { team_name: stale-named-team, inline_spec: inlineSpec }) // then expect(result.details.kind).toBe(created) expect(service.calls).toEqual([{ method: createTeam, args: [{ inlineSpec }] }]) }) })RED 阶段在生产代码修改前运行该测试结果为Expected: created/Received: invalid_arguments见 red-focused-test.txt。测试精确复现了真实故障载荷——同时携带team_name: stale-named-team与完整的内联规格与恢复出的 Kimi 会话参数形态一致GREEN 阶段修复后测试通过并断言服务收到的调用只包含inlineSpec验证了「双字段时inline_spec权威」的语义而不只是「不报错」。端到端验证真实 Senpi 进程 本地模拟提供商单元级回归证明逻辑正确但还需要证明它穿越真实插件边界有效。Live QAlive-senpi-qa.md使用真实senpiCLI、重建的工作区扩展与本地 mock provider在一次性隔离环境中执行QA_HOME$(mktemp -d -t omo-team-create-qa-home.XXXXXX) HOME$QA_HOME \ TEAM_E2E_OUT_DIR$PWD/.omo/evidence/20260804-team-create-recovery/live-team-e2e-isolated-home \ SENPI_BIN$(command -v senpi) \ node packages/omo-senpi/scripts/qa/team-e2e.mjs rm -rf $QA_HOMEmock 被设计为只发一次team_create调用参数中故意同时包含team_name: stale-named-team与名为e2eteam的完整inline_spec。观察到的结果verdict.json总体判定PASSdual_field_calls: 1恰好一次过度指定调用invalid_argument_results: 0不再有任何非法参数结果唯一一次team_create结果为details.kind: created团队e2eteam创建成功并有两个运行中的成员两个成员都解析到本地omo-mock/mock-1模型全程未调用任何网络模型 API团队生命周期、邮箱注入、崩溃恢复、恰好一次投递等全部检查通过credentialIsolationClean: true、wholeDirUnchanged: true、leakedPids: 0同时隔离环境的处理也值得注意首次 live QA 尝试继承了调用方的HOME从而带入了用户真实的模型路由最终证据使用一次性 HOME重跑并通过了全部检查此前的诊断性失败被保留但未计入结果。这保证了验证过程既不泄露凭据、也不污染真实 Senpi 代理目录。验证门禁与仓库级检查verification-gates.txt 记录了完整的门禁清单全部通过门禁命令结果包级测试bun run --cwd packages/senpi-task testexit 0团队服务测试bun test ./packages/omo-senpi/src/components/task/team-service.test.ts7 pass / 0 fail / 19 个 expectQA 脚本自检node packages/omo-senpi/scripts/qa/team-e2e.mjs --self-testSELF-TEST OK仓库级测试bun run test:senpiexit 0 / 0 fail类型检查bun run typecheck:packagesexit 0构建bun run buildexit 0全部步骤完成此外lsp-diagnostics.txt记录了变更文件的共享 LSP 守护进程诊断所有改动的 TypeScript 文件零问题——修复没有引入any、抑制或非安全类型转换。变更范围与历史脉络提交范围控制修复的提交集被严格限制在Senpi 团队生命周期runTeamCreate及相关工具定义聚焦回归测试lifecycle-precedence.test.tslive QA 脚本输入重新生成的 lead 扩展。仅用于构建的 Codex/安装产物被显式排除在外符合「最小正确行为变更」的自审结论。相关历史提交诊断记录给出了三条相关历史0be02d59f389引入 Senpiteam_create的运行时 XOR 校验即本修复替换掉的那个互斥拒绝逻辑1b580615ad0e移除模型提供的 lead 会话覆盖a8654d385a7d为inline_spec增加 JSON 字符串形态支持本修复中coerceInlineSpec沿用了该能力。残留风险自审明确承认其他面向模型的工具仍然使用「仅运行时互斥参数」模式。本次修复有意不将策略泛化到team_create之外避免超出报告故障范围的过度设计见 self-review.md首次 live QA 因继承调用方 HOME 而受用户模型路由影响最终证据用一次性 HOME 重跑通过。同类实现的对照omo-opencode 侧仍保留严格互斥值得注意的是同一team_create能力在 omo-opencode 侧lifecycle-inline-spec.ts仍采用严格的恰好一个约束export const TeamCreateArgsSchema z.preprocess(omitEmptyStringArgs, z.object({ teamName: z.string().min(1).nullish(), inline_spec: z.unknown().nullish(), leadSessionId: z.string().nullish(), }).superRefine((value, ctx) { const optionCount Number(value.teamName ! null) Number(value.inline_spec ! null) if (optionCount ! 1) { ctx.addIssue({ code: custom, message: Provide exactly one of teamName or inline_spec. }) } }))其中omitEmptyStringArgs会先剔除空字符串参数避免teamName: 这类「看似存在、实则为空」的误判parseInlineTeamSpec支持对象与 JSON 字符串两种输入并通过normalizeTeamSpecInputTeamSpecSchemavalidateSpec完成归一化、解析与校验。两处实现共享同一业务目标创建多智能体团队但对「互斥失败如何兜底」选择了不同策略Senpi 路径用优先级吸收过度指定omo-opencode 路径用 schema 校验拒绝。这种差异也解释了为什么诊断中特别强调「本次修复不泛化到其他工具」——不同调用路径的 provider 兼容性归一化行为不同修复策略必须逐路径评估。经验总结schema 互斥约束在兼容性归一化后会失真当 provider 适配层会展平根级对象联合时oneOf/anyOf不能作为跨路径的可靠互斥手段——必须先验证归一化行为再决定是否依赖 schema 语义模型复述规则不等于模型遵守规则主会话中模型在推理文本里正确复述了 XOR 规则输出却依然双字段。散文约束对某些模型路径尤其 Moonshot 兼容通道的约束力不可高估把「拒绝」改成「优先级」是更稳的兜底当两个参数中一个信息量明显更丰富inline_spec携带完整团队定义时让丰富者获胜、直接执行比让模型反复纠错更能收敛回归测试要复现真实载荷RED 测试特意构造了与生产故障完全一致的「team_name 完整inline_spec」双字段载荷并断言服务实际收到的参数形状只含inlineSpec既证明不报错也证明语义正确隔离验证防串扰live QA 使用一次性 HOME、本地 mock provider、凭据隔离与目录摘要不变校验保证验证过程零副作用、可复现。【免费下载链接】oh-my-openagentOmO: Just type mass ulw keyword with your prompt. Now you are the master of graph engineering.项目地址: https://gitcode.com/gh_mirrors/oh/oh-my-openagent创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考