Qwen Code ACP 重复工具调用失败保护(Repeated Tool-Call Protection)设计与实现指南
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
导读
当 AI 编码 Agent 在 ACP(Agent Client Protocol)交互式前台提示循环中反复调用同一个工具、并因同一结构化原因持续执行失败时,模型会浪费大量轮次、工具延迟与 Token,最终才被总调用上限兜底拦住。Qwen Code 为此实现了ACP Repeated Tool-Call Protection:一个保守的、每提示(per-prompt)粒度的内存态守卫,通过"双轴执行结果契约 + 阈值状态机 + 分阶段灰度"的方式,在工具执行边界上识别真正的重复失败,并在给出一次纠正性提醒后仍重复失败时主动停止自动循环。本文基于设计文档 acp-repeated-tool-call-protection.md,结合 repeated-tool-failure-guard.ts、Session.ts 等仓库源码,完整讲解其设计动机、结果契约、状态机、四种运行模式、遥测隐私边界与分阶段灰度流程。
背景:三个已有"断路器"为何不够
在引入本守卫之前,ACP 路径上已有三个防护,但都不完整:
- 重复的 provider call ID 去重——只能拦截传输层/服务商层面的重放;
- 重复非法工具参数——仅对
executionStatus = not_started的 schema 预执行失败,三次后停止; - 每轮提示的总工具调用上限——是绝对的兜底,但触发时已经消耗了大量模型轮次、工具延迟与 Token。
它们共同抓不住最常见的语义循环:模型持续为同一个"确实被进入执行"的工具签发全新 call ID,且每次都因同一结构化原因失败。总调用上限最终会拦住失控,但在此之前成本已经产生。
为什么不能只看终态error
设计文档明确警告:终态error单独不足以作为信号。它当前既包含从未执行的调用(校验失败、权限拒绝),也包含执行成功之后发生的失败(后处理失败)。把所有这些都当作"重复工具失败"会产生大量误报,尤其会把用户取消变成产品稳定性问题——文档提到历史上曾有 468 条权限取消被错误记录为 error 的实例(见 acp-repeated-tool-call-protection.md)。
因此,本守卫的判定必须建立在两个独立轴之上:终态status与独立的执行状态executionStatus。
前置契约:PR #8176 与 PR #8180 的双轴结果契约
本守卫依赖两个前置遥测/契约变更的整合结果,不得从旧的success字段、错误字符串、UI 帧或 span 中重建执行状态:
- PR #8176:让终态
status成为权威信号,并规范化取消(cancelled)与错误(error)字段; - PR #8180:新增独立的
executionStatus轴,并修正 ACP 权限取消的分类问题。
整合后的判定表是守卫一切决策的根基:
终态status | executionStatus | 对本守卫的含义 |
|---|---|---|
success | success | 重置同一已解析工具的候选 |
success | not_started | 重置同一工具候选;协议级合成结果 |
error | not_started | 重置;校验、权限拒绝、hook 拦截或查找失败 |
error | error | 仅当带有可信的冻结executionErrorType时才可计入 |
error | success | 重置;执行成功但后续处理失败 |
cancelled | 任意 | 重置 |
| 任意 | cancelled | 重置 |
| 任意 | 缺失或unknown | 重置,并将该提示剩余部分降级为至多warn |
PR #8180 定义的两个非法组合(success/error、success/cancelled)被视为契约违反:重置守卫、发出诊断、不执行强制。
取消仲裁以status为准:如果执行错误与用户/父级取消竞态且终态为cancelled,该调用不计入失败。
文档还强调一个关键细节:终态errorType不是重复失败键。后处理 hook、图像桥接等收尾步骤可能在其后替换errorType,即使executionStatus仍是error。因此 ACP 内部批次回执(batch receipt)在执行落定时就复制冻结的executionErrorType。缺失或ToolErrorType.UNKNOWN的执行分类不具备候选资格——这一点在 repeated-tool-failure-guard.ts 中通过executionErrorType === ToolErrorType.UNKNOWN的显式排除得到印证。
候选资格与失败键
一个合格的失败(eligible failure)必须同时满足:
- 终态
status = error; executionStatus = error;- 非空的、且不等于
ToolErrorType.UNKNOWN的结构化executionErrorType; - 来自工具注册表的已解析内置(native)或 MCP 工具身份;
- 由完全落定的交互式 ACP 前台批次产生的最终结果。
失败键定义为:
(policyToolName, executionErrorType)其中policyToolName是 ACP 做权限检查时使用的已解析tool.name,不是模型提供的显示名;MCP 注册名本身已包含 server 限定的身份。executionErrorType在执行落定时冻结,独立于终态调用错误。
参数被刻意排除在键之外:这样既能捕获针对同一执行边界的参数抖动(parameter thrashing),又依靠阈值与"两个批次"要求提供误报缓冲。原始参数、输出、路径与错误文本一律不得存入守卫或上报到中心遥测。
未知工具、空身份、未分类错误、以及结果字段不完整的第三方事件,都不具备候选资格。
状态机:idle → tracking → warned → latched
每个 Session 为每个交互式前台 ACP 提示持有一个守卫实例。其状态类型定义如下(与文档一致,且已在源码 repeated-tool-failure-guard.ts 中落地为RepeatedToolFailureGuardPhase与RepeatedToolFailureGuardState):
type RepeatedToolFailureState = | { phase: 'idle' } | { phase: 'tracking'; key: FailureKey; failureCount: number; batchCount: number; } | { phase: 'warned'; key: FailureKey; failureCount: number; batchCount: number; } | { phase: 'latched'; key: FailureKey; failureCount: number; batchCount: number; };阈值是代码常量,不是用户设置:8 次合格失败,且必须横跨至少 2 个完整模型批次(源码中为REPEATED_TOOL_FAILURE_THRESHOLD = 8与REPEATED_TOOL_FAILURE_BATCH_THRESHOLD = 2,见 repeated-tool-failure-guard.ts)。
每个批次完全落定后的归约规则
- 忽略已由 call-ID 去重器处理的重复 provider 事件——它们既不推进也不重置语义状态;
- 先排干(drain)已接受的 mid-turn 输入:若观察到新的外部输入或排队中的完整提示(queued full prompt),重置为
idle并保持既有输入与 FIFO 行为权威;若排干不可靠,重置并在该提示剩余部分禁用强制; - 批次不完整、违反结果契约、或包含取消/未知/未启动/后执行失败时,重置为
idle; - 收集批次内的合格失败键:若存在多于一个唯一键,重置为
idle;同一已解析工具的成功观察只重置该工具候选,其他工具的成功既不推进也不重置;成功没有执行错误类型,因此会在剩余唯一失败键被选中前清除该工具的所有失败分类; - 若键与当前追踪键不同,从本批次开始新的一次连击(streak);否则累加本批次合格失败数并递增批次计数;只包含无关成功工具的完整批次不改变当前连击;
- 当
failureCount >= 8且batchCount >= 2时,转入warned并请求模式对应的提醒动作; - 若后续完整批次仍含同一合格键且无重置条件,先完整执行并记录整批,再转入
latched。中途来自其他工具的成功不会掩盖重复失败,但候选工具自身的成功会重置它。随后是否再发一次模型请求由配置的模式决定。
从源码 reduceRepeatedToolFailureGuard 可以看出该流程的精确实现:providerDuplicate先被过滤;terminalStatus === 'cancelled'、executionStatus缺失/unknown、not_started、post_execution_failure、契约违反分别收集到resetReasons,按INELIGIBLE_RESET_REASON_PRECEDENCE优先级(contract_violation>cancelled>unknown>not_started>post_execution_failure)取最高优先级重置;mixed(多个唯一键)直接重置;warned阶段再遇匹配批次则进入latched并产出stop或would_stop。
状态转换与控制动作分离
| 模式 | 阈值达到时 | 下一个匹配批次时 |
|---|---|---|
shadow | 记录would_warn;不注入任何内容 | 记录would_stop、锁定(latch),继续 |
warn | 注入一次提醒并记录warned | 记录would_stop、锁定,继续 |
enforce | 注入一次提醒并记录warned | 记录stopped、锁定,并在再次发送前停止 |
一旦锁定(latched),该提示内守卫不再产生任何决策——避免 shadow/warn 模式下重复提醒与遥测放大。"一次提醒"保证是按候选连击(per candidate streak)而非按顶层提示:候选被重置后追踪到不同失败键,可以产生新的提醒。
首版刻意只保留一个活跃候选而非多个独立连击的映射:同工具成功被清除后,若仍有多个失败键则重置,因为"哪个并发失败应拥有提醒或停止"存在歧义。这保持了保守性,同时仍能捕获主导的交替形态——一个工具反复失败,而 read/edit/inspection 等工具在尝试之间成功。
提醒与停止文案(固定系统上下文)
提醒(reminder):
System: the same tool execution has failed repeatedly for the same classified reason. Do not repeat the same approach. Inspect the returned result, change the approach or required preconditions, or explain the blocker.
停止消息(stop message):
System: Automatic continuation stopped because the same tool execution failure continued after a corrective reminder. New user input is required to continue.
两段文案在源码中分别定义为REPEATED_TOOL_FAILURE_REMINDER与REPEATED_TOOL_FAILURE_STOP_MESSAGE(见 repeated-tool-failure-guard.ts)。它们不含任何原始参数或原始错误文本,是固定的系统上下文,而非伪造的用户输入。
批次与并发规则:以完整批次为归约单元
归约的单元是一个已完成的模型工具批次,而不是单个流式事件——因为 Agent 调用可能并发执行,终态帧到达的顺序可能与模型 function call 的顺序不同。
runToolCalls只在所有被接纳的调用全部落定后返回一个窄回执(narrow batch receipt)。每个回执条目包含:callId、policyToolName、终态status、冻结的executionStatus、冻结的executionErrorType、以及是否为 provider 重复——不包含参数、结果或原始错误。在 Session.ts 中,回执由finalizeRunToolResult在排序后的记录上构造,其complete字段要求记录数与去重后的 function call 数一致且序号集合完整——这正对应文档"批次完整性必须可证明"的要求。
#buildNextMessageAfterToolRun先排干 mid-turn 输入,再把回执与排干得到的parts、hasQueuedPrompt、reliable状态一起交给 reducer,之后才构造下一条模型消息,从而保留既有"外部输入优先"语义。结果按模型原始调用顺序保留,尽管归约本身与顺序无关。
守卫从不:
- 在批次中途停止;
- 在某个调用达到阈值后取消其兄弟调用;
- 把被跳过的兄弟调用变成执行失败;
- 把迟到的重复终态帧当作新观察。
若 Session 无法证明批次完整,就重置且不强制。PR #8180 的冻结执行状态是必要条件,但单独不足以证明批次完成——reducer 只在落定的runToolCalls边界被调用。
与既有保护的协作次序
四层保护按从最具体到最宽泛的顺序保持:
- Provider call-ID 去重——处理传输/服务商重放;
- 既有非法参数守卫——处理重复的预执行 schema 失败(
executionStatus = not_started); - 本守卫——处理重复的、带类型的执行失败;
- 每轮总工具调用上限——仍是绝对兜底。
只有第一个停止该轮的守卫记录终态循环原因。本设计新增了独立的LoopType.REPEATED_TOOL_EXECUTION_FAILURE,使运维可以将其与非法参数、重复 ID、总上限区分开。
当本守卫停止时,ACP 必须:
- 在聊天历史中保留全部已落定的 function 响应;
- 向历史追加固定停止上下文,并通过既有可重放的 ACP agent-message 更新路径发送一次;
- 为该提示挂起 Todo Stop Guard及其他自动续跑机制;
- 保持排队中的外部输入原封不动;
- 结束当前 ACP 请求且不再开启新的模型流。
源码中stop分支的执行正符合上述要求:this.todoStopGuard.suspend()挂起自动续跑,#preserveUnsentMessageHistory保留历史,recordDaemonLoopDetected以recordToQwenLogger: false记录LoopType.REPEATED_TOOL_EXECUTION_FAILURE,并通过messageEmitter.emitAgentMessage发送停止消息后返回stoppedByRepeatedToolFailure: true(见 Session.ts)。
守卫保持锁定直到当前提示结束;之后的顶层提示(包括显式重试或继续请求)在既有 Session 生命周期下创建全新守卫。
作用域与四种运行模式
首版仅应用于被选中的 live Session 所有者处理的交互式前台 ACP 提示。两个 channel 桥接实现会显式标记自己的提示,Session 将这些被标记的提示强制为off——即使进程配置为 enforce。它不是进程全局的,工作区所有权未知时不得回退到 legacy 或 primary 运行时。标记是客户端断言的 ACP 元数据,因此另一客户端也可将自己的提示退出此保守保护;它只是路由提示,不是信任信号或授权边界。
四种模式:
off:无 reducer、无遥测;shadow:计算决策但不注入、不停止;warn:注入提醒但永不停止;enforce:按状态机注入并停止。
默认是shadow。部署控制面不得将enforce分配给未知所有权、不受信任的生产者或混合部署版本;运行时不推断这些部署属性。缺少executionStatus或出现不支持的结果组合时,重置连击并将该提示剩余部分降级为至多warn。Cron、通知与后台路由在首版保持off。
环境变量配置
模式是运维控制的部署策略,不是面向用户的设置。环境变量为QWEN_CODE_ACP_REPEATED_TOOL_FAILURE_GUARD(源码常量见 shared-env-keys.ts),取值off、shadow、warn、enforce,在 Session 启动时解析:
- 缺失或非法值解析为
shadow; - 非空非法值会记录一条运维诊断日志(见 Session.ts 中
parseRepeatedToolFailureGuardMode与默认回退逻辑,解析函数本身在 repeated-tool-failure-guard.ts)。
# 在启动 Session 前设置(示例) export QWEN_CODE_ACP_REPEATED_TOOL_FAILURE_GUARD=shadow # off | shadow | warn | enforce项目.env、项目.qwen/.env、工作区settings.env等来源不允许设置此策略;导出的进程值或用户级环境文件仍有效。部署控制面只应在指定的、版本固定的队列(version-pinned cohort)上将模式提升到shadow以上。该特性不引入第二个 rollout 或所有者分配服务。
输入边界依赖
守卫依赖 ACP host 实现craft/drainMidTurnQueue且带布尔hasQueuedPrompt。较旧或第三方 host 若拒绝、超时或返回不完整的 drain 响应,会产生一次unreliable_input重置诊断、在该提示禁用强制,并在输入边界可靠前无法积累候选。这种fail-open行为兼容性安全,但必须与受支持 host 的 shadow 基线分开统计。源码中,#buildNextMessageAfterToolRun仅在repeatedToolFailureMode !== 'off'时设置watchQueuedPrompt,且把drained.reliable直接作为 reducer 的inputReliable输入(见 Session.ts)。
遥测与隐私边界
每次 reducer 转换发出低基数计数器与一条数据最小化的结构化诊断日志:
- 部署环境与服务版本来自既有 OpenTelemetry resource,而非新守卫标签;
- route:interactive ACP foreground;
- mode;
- 转换前后的 phase;
- decision:
reset、tracked、would_warn、warned、would_stop、stopped; - 当批次只有一个合格键时,记录候选终态状态、执行状态与工具类型;冻结的执行错误类型只保留在结构化诊断日志中,不作为指标标签;
- 否则仅记录低基数重置原因,如
success、cancelled、not_started、unknown、mixed、incomplete、external_input、contract_violation(源码中这些枚举见 repeated-tool-failure-guard.ts); - 失败计数分桶:
0、1-2、3-4、5-7、8+;批次计数分桶:0、1、2、3+; - 同一原始 ACP
prompt_id(工具调用遥测已发出的)仅出现在诊断日志中,以便授权的 rollout 分析将守卫转换与落定的工具批次关联,而不引入第二套标识; - 仅诊断日志中的提示内候选序号(candidate ordinal):reducer 在同一个私有键活跃时复用该序号,键变化时分配新序号。
prompt ID 与候选序号永不作指标标签。序号无法跨提示关联工具,也不会泄露工具身份。idle到idle的观察不发出任何内容。
终态repeated_tool_execution_failure循环事件使用同一 OpenTelemetry prompt ID,并在 Session 调用点显式绕过 QwenLogger/RUM(对应源码recordToQwenLogger: false)。共享 Core logger 不根据循环类型推断目的地;其他循环类型保持既有遥测行为。
不得在守卫专用字段中发出:工具参数、结果、原始错误消息、堆栈、路径、MCP server 名、用户 ID 或私有失败键。
取消被排除在所有失败率分子之外。主执行 SLI 使用 PR #8180 的契约:
execution_status = error ──────────────────────────────────────── execution_status in {success, error}旧success字段与 PR 前部署数据不得用于验证本守卫。
灰度与 rollout:shadow → warn → 限量 enforce
Phase 0:整合结果契约
- 合并 PR #8176;
- 在其上 rebase PR #8180,同时保留 #8176 的终态
status维度与 #8180 的独立执行计数器; - 在 ACP 批次回执中保留 #8180 的内部
executionErrorType,不要事后从终态errorType推断; - 优先将 #8180 拆分为可审查的小改动:执行契约、ACP producer 修复、遥测/spans、MCP 取消/超时仲裁;
- 先部署整合后的契约,再收集新基线。
那 468 条历史权限取消记录是旧 producer bug 的证据,不是合格的守卫失败。七天基线只针对包含整合契约的部署版本重新计算,内部与公有云分别进行,不得混用新旧版本。
Phase 1:shadow(影子模式)
实现纯 reducer 并接入落定的 ACP 批次边界,以shadow模式运行至少 7 个完整日(两个环境各自)。shadow 只推进虚拟 warned 状态而不注入提醒,因此would_warn与would_stop只估算体量,无法证明模型看到提醒后的行为。
基线开始前,每个部署必须配置稳定的 OpenTelemetry Resource 属性(如deployment.environment)以区分内部与公有云。SDK 总是提供service.version,但刻意不发明部署环境。缺少该 Resource 维度,Phase 1 无法得出按环境结论,rollout 不得推进。
shadow 模式不改变模型续跑或注入消息,但会向既有 mid-turn drain 请求添加todoStopGuardWatchQueuedPrompt: true,使 reducer 能证明没有完整提示在排队。不具备该响应契约的 host 通过unreliable_input计数,并排除在受支持 host 的 shadow 结论之外。
必需不变量(必须保持零违反):
- 零条取消调用被计为合格失败;
- 零条
not_started、未知执行分类或后执行失败被计入; - 零条基于不完整批次的决策;
- 零次在不可靠输入排干后的强制;
- 遥测中零个原始参数/结果/路径/错误文本字段;
- 每个
would_stop前都有同一候选序号与提示的would_warn; - 既有总工具调用与重复 ID 保护保持不变。
人工审查隐私安全的 would-stop 会话样本(授权本地 trace 访问),分类"未修改的续跑是否取得有效进展"——这用于拒绝明显不安全的阈值,而不是批准 enforce。
Phase 2:warn(提醒模式)
对内部交互式 ACP 前台提示启用warn,保持 7 天,确认提醒注入不增加取消、重连、延迟、Token 或轮次回归。公有云保持 shadow。只有 warn 模式提示能展示"模型在真实纠正提醒后是否仍重复失败",该队列为 enforce 提供语义证据。
Phase 3:限量 enforcement
对最多 5%的稳定、版本固定的内部交互式 ACP 前台所有者启用强制。按所有者确定性分配(同一提示运行中不能切换处理方式),其余 95% 保持warn——因此处理组与对照组收到相同的纠正提醒,唯一差别是提醒后匹配批次是否停止。每波保持 7 天。
晋升条件:
- 所有正确性不变量保持零违反;
- warn 模式审查未发现匹配的提醒后失败中有明显有效进展;
- 强制的停止具有预期键、提醒、完整批次回执与保留的历史;
- 完成率不劣于对照组超过 1 个百分点;
- 十分钟内断线或重试不劣于对照组超过 0.5 个百分点;
- p95 延迟、平均 Token、平均轮次各不劣于对照组的 1.10 倍;
- 停止循环率与节省调用估算与 warn 模式观察一致。
使用所有者级分块分析与置信区间——同一所有者的调用不是独立样本。任何契约违反或取消误分类都会使环境立即回到shadow。5% 以上不得加码,除非通过投影全流量下的容量测试确认了更高分配级别下的处理饱和;若无法排除干扰,则保留永久对照组并将强制封顶在 5%。
公有云在内部闸门通过后独立重复 shadow、warn 与限量 enforce,绝不继承内部的通过。
实现形态与交付顺序
代码改动刻意保持小:
- 在 ACP Session 代码旁新增纯
repeated-tool-failure-guard.tsreducer; - Session 将落定的批次记录翻译成 reducer 的窄回执、排干外部输入并应用返回的动作;
- 通过既有低基数日志路径新增循环类型与遥测事件字段;
- 在两个桥接边界标记 channel 提示,并使 rollout 变量远离项目控制的环境来源;
- 不改变任何工具实现,也不新增执行调度器。
因为改动触及 Core 遥测类型与 ACP Session 编排,需要在仓库的 core-infrastructure gate 下由维护者负责。
建议交付顺序:
- 结果契约整合与修正后的七天基线;
- reducer、单元测试与 shadow 遥测;
- 提醒注入与 warn 模式 E2E 覆盖;
- 停止接线、生命周期重置与休眠的 enforce 模式;
- 受控 rollout——推进模式无需代码变更。
验证体系
纯 reducer 单元测试
repeated-tool-failure-guard.test.ts 覆盖:
- 完整终态/执行判定表;
- 单批次 8 次失败不触发警告;
- 跨两个批次 8 次失败触发警告;
- 下一个匹配批次落定后才停止;
- 同工具成功、取消、
not_started、unknown、后处理失败、混合失败键、不完整批次、不可靠排干、排队提示与新输入均重置; - 其他工具的成功在匹配失败批次内部与间隔成功批次中均被忽略;
- 重复 provider 事件被忽略而非计数或重置;
- 新键开启新连击;
- 警告与停止文案固定且不含工具数据;
- 不支持的结果组合永不强制。
ACP Session 与 channel 测试
- 内部回执保留冻结的执行失败类型而非之后的终态错误类型;
off保持 legacy drain 请求形态;- shadow 观察而不改变轮次;
- warn 对候选恰好注入一次提醒;
- enforce 在历史中保留已落定的提醒后结果且不开启额外模型流;
- 排队输入与取消优先于强制;
- 不支持 host 通过
unreliable_input降级; - 两个 channel 桥接路径都标记提示且 Session 强制其为 off。
遥测测试
- 低基数属性;
- 终态循环事件显式排除 QwenLogger/RUM,同时保留标准 OpenTelemetry 关联字段;守卫专用遥测排除敏感工具字段。
手工 E2E 与发布前检查
行为变更将使用.qwen/e2e-tests/下的本地 E2E 计划。准备该计划并完成手工 ACP fixture 运行仍在进行中,且是合并前的必要条件。"Ready for review"只表示维护者评审可以进行,不代表手工 ACP fixture 验证已完成。fixture 必须覆盖:一个带类型的失败工具、权限取消、提醒后成功恢复、重复失败停止、并发兄弟调用、不支持 host、channel 排除、重连与重启行为、历史重放、停止后的新提示、以及 shadow 模式不干扰。合并并部署后,分别开始内部与公有云各 7 天的 shadow 基线;它们门控后续晋升到 warn/enforce,而非门控合并。
交付前,从各自包目录运行针对性的 Core 与 CLI Vitest 文件,然后执行npm run build、npm run typecheck与npm run lint。
失败处理与边界声明
- 遥测发送失败永不改变工具或模型控制流;
- 缺失或畸形的结果数据会重置并降级强制;
- 携带提醒的模型请求若失败,该提示在后续停止决策前退出;守卫永不在未先构造并发送提醒轮次的情况下停止;
- 停止历史持久化失败时返回既有 ACP 内部错误,不假装停止已被持久记录;
- 进程重启按设计丢失语义连击:基于历史的 provider call-ID 去重仍防止已应答 provider 调用的重放,总工具调用上限约束新启动的提示。
总结
ACP Repeated Tool-Call Protection 通过"双轴结果契约 + 8 次/2 批次阈值状态机 + 每候选一次提醒 + 四模式分阶段灰度"的组合,在不依赖自由文本错误解析、不破坏既有去重/非法参数/总上限保护的前提下,精准识别工具执行边界的重复语义失败。它的保守性体现在每一层:只观察完全落定的批次、取消/未知/未启动/后执行失败一律重置、一次只追踪一个候选、默认 shadow 且公有云独立灰度。首版明确将跨重启精确一次执行、传输层重放、全局 rollout 编排排除在范围之外——除非生产证据表明有界的内存守卫不够用。
如需深入,可继续阅读:repeated-tool-failure-guard.ts、repeated-tool-failure-guard.test.ts、Session.ts、shared-env-keys.ts,以及设计文档 acp-repeated-tool-call-protection.md。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考