docs/design/rt-optimization/rt-optimization-design.md
Qwen Code 的 Agent Loop 为严格串行模型:
User Prompt → [LLM 决策] → Tool Execution → [LLM 决策] → Tool Execution → ... → [LLM 回复] → Idle
~3-4s ~Xms-Ns ~3-4s ~Xms-Ns ~3-4s
每一轮 LLM 调用(含网络 RTT + 模型推理)约 3-4s,是端到端 RT 的主要成本。
测试场景:"我有哪些工作空间"(3 轮 agent loop,2 次工具调用,单次采样)
| 阶段 | 耗时 | 占比 |
|---|---|---|
| LLM Round 1(决策调 skill) | 3.8s | 28% |
| Skill 执行 | 1ms | <1% |
| LLM Round 2(决策调 shell) | 3.0s | 22% |
| Shell 执行 | 2.5s | 19% |
| LLM Round 3(文字总结) | 3.8s | 28% |
| 框架开销(状态同步、渲染) | 0.3s | 3% |
| 总计 | 13.4s | 100% |
结论:LLM 调用占 78%,工具执行 19%,框架 3%。优化的核心是减少 LLM 调用次数和降低单次 LLM 调用延迟。
注:单次采样、单一场景。19% 工具执行是 shell 慢调用支配,read-heavy 场景下工具执行可降至 <5%。方案落地前需补 ≥3 类场景(写操作、跨工具推理、错误恢复)的基线。
| 约束 | 代码位置 | 说明 |
|---|---|---|
| 工具结果无后置控制 | tools.ts ToolResult 接口 (L422) | 仅有 llmContent/returnDisplay/error,无法表达"跳过 LLM" |
| 结果无条件回传 LLM | useGeminiStream.ts handleCompletedTools (L2038) → submitQuery(ToolResult, …) (L2355) | 所有 gemini-initiated 工具结果都回传 |
| Stream 完毕后才调度 | useGeminiStream.ts processGeminiStreamEvents (L1365) | stream 循环结束后才 scheduleToolCalls,无增量调度 |
| 模型层选择无策略层 | client.ts modelOverride ?? getModel() (L1305, L1598) | 基础设施已贯通至 turn.run(model, …) (L1707),但调用方仅在 skill 显式指定时使用 |
| 能力 | 位置 | 现状 |
|---|---|---|
fastModel 配置 + /model --fast <id> | config.ts:684, 1987, 2021 | 已就绪 |
SendMessageOptions.modelOverride | client.ts:142 → 1598 → turn.run | 端到端贯通至 geminiChat.sendMessageStream(model, …) |
钩子层 modelOverrideRef(承载 skill 选模型) | useGeminiStream.ts:376, 2225, 1841 | 已贯通 |
| fast-model 非流式 side query 先例 | services/toolUseSummary.ts:108(via runSideQuery) | 已上线,证明 fast 模型配置健全;但非流式路径 |
| fast-model 流式 先例 | followup/speculation.ts:224 | 已上线,但用的是 forked chat(createForkedChat),与主 chat 隔离 |
关键空白:没有任何生产代码在主 chat 上以 fast model 跑 streaming。本方案 D2 是首个 case,需先做验证实验(详见 §3.2 前置条件)。
当前 ToolResult 不包含任何关于"接下来该怎么做"的信息。无论工具结果是否自解释,都无条件触发一轮 LLM。
扩展 ToolResult 接口(packages/core/src/tools/tools.ts L422):
export interface ToolResult {
llmContent: PartListUnion;
returnDisplay: ToolResultDisplay;
error?: { message: string; type?: ToolErrorType };
// 新增:后置执行指令
postExecution?: {
/**
* 工具结果不回传 LLM,直接作为最终回复展示给用户。
* 适用于结果完全自包含、不需要模型再解读的场景。
* 是 ToolResult 局部属性。
*/
skipLlmRound?: boolean;
/**
* 工具结果"自包含、可直接展示给用户"——即 `returnDisplay` 已经是
* 用户期望看到的最终形态,不需要模型加工。
* 是 ToolResult 局部属性,**不**预测"下一轮是否 summary"。
* 与方向三(展示解耦)联动:true → 进入 Summarizing 状态允许用户输入。
*/
resultIsTerminal?: boolean;
};
}
设计修正:早期版本曾把单一
selfExplanatory字段同时承担"工具产物属性"和"对话流预测信号"两份职责,但二者并不重合(例:用户 prompt 是"读 X 然后修 Y",read_file 输出自包含,但下一轮显然不是 summary)。预测信号属于对话流全局属性,不应通过工具字段表达——D2 改为完全用对话流启发式(见 §3.2)。
handleCompletedTools 中新增判断:
工具批次完成
→ 检查 batch 中所有工具的 postExecution.skipLlmRound
→ 全部为 true?
→ YES: markToolsAsSubmitted, 不调 submitQuery, 直接 idle
→ NO: 保持现有行为 (submitQuery)
重要约束:skipLlmRound 仅在当前 batch 的所有工具都声明 skip 时才生效。混合 batch 仍然回传。
跳过 LLM 后历史形如:user → function_call → function_response → <无 assistant>。
repairOrphanedToolUseTurnsInHistory(session-load 时调用)是否容忍此形态统一修复点:此处和 §3.3(D3 中途打断 Summarizing)破坏的是同一个历史不变量。修复方案二选一(注入空 assistant / 接受 Qwen 容忍),两个方向必须使用相同选择。
| 工具 | skipLlmRound | resultIsTerminal | 备注 |
|---|---|---|---|
read_file | 配合 query-only 场景 | true | 文件内容即答案 |
cat(via shell) | 视场景 | true | 同 read_file |
grep / glob / ls | false | false(默认) | 结果常需模型挑选/排序/总结;skill 层在已知"纯查询"场景显式 true |
git status / git log(via shell) | false | true | 输出已格式化 |
| Skill 工具 | 各 skill 自决 | 各 skill 自决 | 查询类 skill 倾向 true |
| MCP 工具 | 默认 false | 默认 false | 通过 allowlist 显式 opt-in |
第三方/MCP 工具不可信任,默认不打标;通过 config.toolPostExecAllowlist 显式启用。
grep/glob/ls默认 false 是从严选择:避免 D2/D3 在需要模型总结排序的场景误判。
| 风险 | 严重度 | 缓解 |
|---|---|---|
| 工具错误设置 skipLlmRound 导致多步任务中断 | 中 | batch 级语义 + llmContent 仍在历史中可恢复 |
| 第三方工具滥用 | 中 | MCP 默认禁用,allowlist 显式开启 |
| 历史不变量破坏 | 中 | 落地前补单测;session-load 重放覆盖 |
| 用户预期不一致(期望总结但没有) | 低 | setting alwaysSummarize: true 可覆盖 |
终态查询场景节省 3-4s(跳过最后一轮 LLM)。
本方向不引入新管道,但需要扩展 GeminiChat 接口以支持运行时模型切换。
§1.4 的基础设施提供了 fast 模型配置和 modelOverride 端到端贯通,但主 chat 上跑 fastModel + streaming 没有先例,需要:
config.getFastModel() 作为 override 传下去GeminiChat.retryStreamWithModel 新接口(处理 chat 内部状态)D2 仅作用于:
sendMessageStream 调用点 L1841acp-integration/session/Session.ts:1182,Phase 3 同步改造D2 不作用于以下路径,避免在非交互或独立上下文里引入额外失败模式:
agents/runtime/agent-core.ts:614):子 agent 已带独立模型配置SendMessageType.Cron, client.ts:127):非交互,无 RT 紧迫性SendMessageType.Notification, client.ts:129):同上submitQuery 调用时我们并不知道模型看完结果后是发起新工具还是直接出文字。如果用 fast model 调而模型实际还要调工具——后果是静默的:fast 可能调错工具或参数错,错误不会有明显信号。
任何工具级别的字段都无法可靠预测"下一轮是否 summary",因为它取决于对话流(user prompt + 累计上下文),不是工具产物的局部属性。例:
用户:"读 utils.ts 然后把里面的 console.log 都改成 logger.info"
→ Tool 1: read_file → 结果自包含
→ 但下一轮显然不是 summary
因此 D2 完全用对话流启发式预测,不依赖工具字段。
import { Kind, MUTATOR_KINDS } from '../tools/tools.js';
function selectContinuationTier(
turn: Turn,
userPrompt: string,
batch: ToolCall[],
): 'fast' | 'primary' {
// ===== 用户级别强制开关(最高优先级) =====
const userPref = config.getSummaryTierStrategy();
if (userPref === 'always_primary') return 'primary';
if (userPref === 'always_fast') return 'fast'; // 仍受运行时保险约束
// ===== 用户意图否决 =====
// 1. user prompt 含动作动词 → 下一轮大概率还要调工具
if (requestImpliesFurtherAction(userPrompt)) return 'primary';
// 2. 本轮已有 mutator 工具 → 大概率有验证/读后续
if (batch.some((c) => MUTATOR_KINDS.includes(c.tool.kind))) return 'primary';
// 3. 本轮或历史有未解决 error → 模型需要 primary 诊断
if (hasUnresolvedError(turn.toolResults, batch)) return 'primary';
// ===== 输出复杂度否决 =====
// 4. user prompt 要求深度分析(解释/对比/为什么类)
if (needsDeepReasoning(userPrompt)) return 'primary';
// 5. 工具调用 ≥3 个不同工具 → 跨结果叙述靠 primary
if (needsCrossResultReasoning(turn)) return 'primary';
// 6. 工具输出过长 → 长内容总结靠 primary
if (estimateTotalToolOutputTokens(turn) > 4000) return 'primary';
// ===== 模型可行性否决 =====
// 7. fast 模型 context window 不够 → 切到 fast 会触发 compression
// (compression 自身要 LLM 调用,反而拖慢且增加成本)
if (wouldTriggerCompression(turn.history, config.getFastModel()))
return 'primary';
// ===== 多语言兜底 =====
if (!isPromptLanguageSupported(userPrompt)) return 'primary';
// ===== Session 状态兜底 =====
if (turn.justCompacted || turn.justCleared) return 'primary';
return 'fast';
}
八个否决项含义:
requestImpliesFurtherAction:动作动词(改|删|加|替换|修复|实现|新建|create|fix|change|add|remove|implement|write|update)→ 多步任务MUTATOR_KINDS 命中:本轮已经写过 → 大概率紧跟一次读/校验。复用 tools.ts:806 已有的 MUTATOR_KINDS = [Edit, Delete, Move, Execute](每个 Tool 实例的 kind: Kind 属性是权威分类,不要重新发明 isWriteTool)hasUnresolvedError(turnResults, currentBatch):判定二段——
(toolName, args fingerprint) 去重,最后一次仍 error 视为未解决(仅按 toolName 在同名不同参数下会判错)ToolResult.error(前置数据质量依赖)needsDeepReasoning:含"分析/解释/为什么/对比/诊断"类关键词needsCrossResultReasoning:distinct 工具调用 ≥3(同工具同参数视为同一次)wouldTriggerCompression:fast 模型 context window 通常小于 primary,相同 history 在 fast 上会更早触发 tryCompress(geminiChat.ts:1418)—— compression 自身需要一次 LLM 调用,可能反向恶化 RT 和成本。预算估算:estimateHistoryTokens(history) > fastModelContextWindow × COMPACTION_THRESHOLD 即视为会触发/compact 或 /clear 后第一次 continuation → primary 重建 mental model否决方向偏向 primary(宁可多 2s 不要降质)。
GeminiChat.retryStreamWithModel问题:直接 abort + 调 client.sendMessageStream 会破坏 chat 状态:
geminiChat.ts:1428 在 stream 启动时就 push userContent 到 history;重起会再 push 一次导致 history 出现重复 function_responsesendPromise 锁(geminiChat.ts:1392, 1398)—— abort 后需要确保 streamDoneResolver 被调用pendingPartialState 等 PR #4176 引入的不变量 marker 需要正确清理新增接口(packages/core/src/core/geminiChat.ts):
/**
* Retry an in-flight or just-aborted streaming send with a different model.
* Does NOT re-push userContent (kept from original send).
* Resets pendingPartialState; releases stale sendPromise; re-opens span.
*/
async retryStreamWithModel(
model: string,
signal: AbortSignal,
): Promise<AsyncGenerator<StreamEvent>>;
调用契约:
实现工作量约 1.5d 加单测。
selectContinuationTier 返回 'fast' 但 stream 中出现 ServerGeminiEventType.ToolCallRequest 事件 → 立即 abort 当前流,调 retryStreamWithModel(primaryModel)。
这覆盖"预测为 summary 实际仍需工具"的唯一静默放错场景。代价:一次 fast 调用浪费的 tokens(成本归因见 §5.3)。
modelOverride 解耦useGeminiStream.modelOverrideRef(L376, L2225)当前承载 skill 显式选择的模型,属"业务语义"。本方向的 fast 路由属"优化语义",两者必须分离:
// 新增独立 ref
const summaryTierRef = useRef<'fast' | 'primary' | undefined>(undefined);
// 调用点合并(不复用 modelOverrideRef)
const stream = geminiClient.sendMessageStream(
finalQueryToSend,
abortSignal,
prompt_id!,
{
type: submitType,
notificationDisplayText: metadata?.notificationDisplayText,
modelOverride:
modelOverrideRef.current ?? // skill 显式选择优先
(summaryTierRef.current === 'fast' ? config.getFastModel() : undefined),
},
);
生命周期:
| 时机 | modelOverrideRef(skill) | summaryTierRef(fast 路由) |
|---|---|---|
新 user turn (!Retry && !ToolResult) | 清空 | 清空 |
skill 工具返回 modelOverride 字段 | 写入 | 不变 |
tool batch 完成 → selectContinuationTier | 不变 | 写入 |
| Runtime fallback(看到 ToolCallRequest) | 不变 | 升级为 'primary' |
| Retry(用户手动 Ctrl+Y) | 保留 | 升级为 'primary'(fast 失败不再 fast) |
skill 显式选择永远赢——用户的显式意图优先于优化策略。
client.ts:1303 的 interaction span 在 turn 启动时记录 model 属性。fallback 触发时 model 实际变了,span 数据失真。需要:
// fallback 触发时
span.setAttribute('llm.model.requested', fastModel);
span.setAttribute('llm.model.actual', primaryModel);
span.setAttribute('llm.fallback.reason', 'tool_call_seen');
并在 addUserPromptAttributes 中区分 requested / actual 模型,避免计费/审计混淆。
新增 setting(packages/cli/src/config/settingsSchema.ts):
summaryTierStrategy: 'auto' | 'always_primary' | 'always_fast';
// default: 'auto'
'auto':使用 selectContinuationTier(推荐)'always_primary':完全禁用 D2 优化(生产敏感场景)'always_fast':跳过 vetoes,仍受运行时保险约束(高级用户)理由:D2 是质量换速度,部分用户/场景需要明确退出权。
config.getFastModel() 已配置resultIsTerminal=true 工具,在主 chat 反复触发 summary 轮tryCompress 是否被错误触发(fast 模型 context window 小可能提前触发)function_response),测 P50/P95 端到端延迟与 time-to-first-tokentryCompress 触发率 P_compact,验证净 RT 收益 = (1 - P_compact) × ΔRT − P_compact × compression_RT > 0getFastModel() 层校验拒绝thinkingConfig 兼容性:
thinkingConfig.includeThoughts 支持上一致;或includeThoughts: false(与 sideQuery.ts:118-122 对齐)| 风险 | 严重度 | 缓解 |
|---|---|---|
| Fast 模型 tool-calling 静默放错 | 高 | 对话流启发式 + 运行时 ToolCallRequest abort 保险 |
| Fast 在含 error 的输入上幻觉成"对用户可见的错误回答" | 高 | hasUnresolvedError 否决;监控用户追问率(注:emitToolUseSummaries 的同类风险只影响 60 token 标签,本风险影响最终回答,量级更高) |
Fast 路径触发 tryCompress → 多一次 LLM 调用,反向恶化 RT 和成本 | 高 | wouldTriggerCompression 预判 gate(见决策函数 #7);前置基线测量 P_compact 阈值 |
| Compression 自身用谁的模型 | 中 | 触发 compression 即放弃 fast 路由(gate #7 兜底);避免回答出问题 |
| 主 chat 切模型让 chat 内部状态/recording 异常 | 中 | 前置验证实验覆盖;session resume 重放测试 |
D2 与 emitToolUseSummaries 同时触发 concurrent fast 调用,超 rate-limit | 中 | 二选一:D2 启用时禁用 emitToolUseSummaries(标题不影响功能),或共享 rate-limit token bucket |
thinkingConfig 在 fast / primary 间不一致导致 history 解析异常 | 中 | 同家族 + fast 路径强制 includeThoughts: false(见前置条件) |
| Fallback 路径反而更贵(fast tokens 浪费 + primary 全程) | 中 | fast_tokens_consumed 决策日志监控;fallback 率 >20% 自动关 flag |
| Telemetry span model 失真 | 中 | requested / actual 拆分(见 Telemetry 修正) |
| 上下文格式不兼容(跨家族) | 中 | getFastModel() 拒绝跨家族选择 |
| 与 skill modelOverride 语义冲突 | 中 | 独立 ref + skill 优先 |
/model 运行时切换主模型后 summaryTierRef 决策失效 | 低 | /model 命令处理时同步清空 summaryTierRef |
| fast tokens/s 反而更慢 | 低 | 实测时同时测 TTFT,不只总 RT |
fast_tokens_consumed 实测确认净收益用户从工具完成到可以再次输入,必须等 LLM 总结轮完成:
工具完成 → [渲染结果] → [submitQuery] → [等 LLM 流式回复 3-4s] → Idle → 可输入
~~~~~~~~~~~~~~~~~~~~~~~~
用户已看到结果但无法操作
新增 StreamingState.Summarizing 状态:
export enum StreamingState {
Idle = 'idle',
Responding = 'responding',
WaitingForConfirmation = 'waiting_for_confirmation',
Summarizing = 'summarizing', // 新增
}
工具完成且结果已展示
→ 若 batch 全员 postExecution.resultIsTerminal === true:
→ 进入 Summarizing(用户可输入)
→ submitQuery 异步执行
→ LLM 总结追加到 history(或被用户新消息取消)
→ 否则:
→ 保持 Responding(用户不可输入)
Summarizing 状态下用户提交新消息 → abort 当前总结 → 处理新消息function_response 仍保留在 history(模型知道工具执行了)partial text 分布在多处,需同时清理,缺一会导致状态不一致:
| 位置 | 清理动作 |
|---|---|
pendingHistoryItemRef.current(useGeminiStream React state) | 置 null,不调 addItem |
GeminiChat.history 内部累积 | abort 前若已 push 部分 assistant content,需通过新的 discardPendingAssistant() 接口回滚 |
ChatRecordingService buffered turn | 标记为 cancelled,不写入 JSONL |
dualOutput.emitText(如启用) | 发送 abort sentinel,sidecar 自行丢弃 |
loopDetectorRef 累积 token | 重置当前 turn 计数 |
执行顺序:abort signal 触发 → 收齐上述五处清理 → 才允许新 user message 进入 submitQuery。竞态测试覆盖:abort 触发瞬间正好收到最后一个 chunk。
batch 全员 postExecution.resultIsTerminal === true。
中途打断 Summarizing 会产生:
[user_1, function_call, function_response, user_2]
↑ 无 assistant turn
这与 §3.1 跳过 LLM 轮破坏的是同一个不变量,必须使用与 D1 相同的修复策略(注入空 assistant / 接受 Qwen 容忍)。
repairOrphanedToolUseTurnsInHistory)必须覆盖此形态| 风险 | 严重度 | 缓解 |
|---|---|---|
| Abort 时半句 assistant 进 history | 中 | 显式丢弃 partial text;仅保留 function_response;单测覆盖 race |
| 历史不变量破坏(无 assistant 接续) | 中 | 与 D1 同源问题,统一修复(见 §3.1 历史不变量) |
| UI 状态复杂度增加 | 中 | Summarizing = Idle + 背景任务;输入路径复用 Idle |
| 用户感知收益依赖行为模式 | 低 | 用户若 3s 内不输入,summary 已完成 → 无感知收益;但不退化 |
processGeminiStreamEvents 在 stream 完全结束后才批量调度工具。ToolCallRequest 事件可能在 stream 中期就已 yield。
在 stream 事件处理中对 ToolCallRequest 立即开始前置验证(不执行):
case ServerGeminiEventType.ToolCallRequest:
toolCallRequests.push(event.value);
scheduler.prevalidate(event.value, signal); // 新增
break;
CoreToolScheduler.prevalidate(request):
shouldConfirmExecute(缓存结果)schedule() 时直接使用缓存结果prevalidate 要求 shouldConfirmExecute 是 side-effect-free 且结果在 prevalidate→schedule 间隙不会被外部修改使之失效。
直接复用 tools.ts:818 的 CONCURRENCY_SAFE_KINDS:
export const CONCURRENCY_SAFE_KINDS: ReadonlySet<Kind> = new Set([
Kind.Read,
Kind.Search,
Kind.Fetch,
]);
这是项目已有的"无副作用 + 可并发"分类,正好匹配 prevalidate 需求。
| 工具 Kind | 是否在 allowlist | 理由 |
|---|---|---|
Read(read_file 等) | ✅ | 纯读 |
Search(grep / glob) | ✅ | 纯读 |
Fetch(web_fetch 等) | ✅ | 远程读,无写副作用 |
Edit | ❌(见下文 TOCTOU) | shouldConfirmExecute 纯只读,但 diff 在调度间隙可能失效 |
Delete / Move / Execute | ❌ | MUTATOR_KINDS |
Think | ❌ | 含 save_memory / todo_write 等隐式写 |
| MCP 工具 | ❌ | 不可信 |
TOCTOU:为什么 Edit 不进 allowlist
理论上 Edit 的 shouldConfirmExecute 是纯只读(读文件、算 diff)。但 prevalidate 与 schedule 之间存在时间窗:
T=0 stream 收到 Edit(file=a.ts, ...) → prevalidate
T=10ms shouldConfirmExecute 读 a.ts,缓存 diff_v0
T=300ms stream 结束,scheduler.schedule()
T=305ms 期间其他工具/IDE/外部进程修改 a.ts
T=310ms scheduler 用 diff_v0 展示给用户
T=320ms 用户基于 v0 确认
T=330ms Edit 应用旧 params 到 v1 文件 → 内容损坏 / merge 失败
这是 TOCTOU。修复方向:
CONCURRENCY_SAFE_KINDS 三类。代价:收益从"50-200ms(Edit 主导)"降到"50-100ms(仅读类)"(mtime, size, content_hash);schedule() 时校验未变才用缓存,否则重算文档暂选 A。
coreToolScheduler.attemptExecutionOfScheduledCalls(L2436+)使用 partitionToolCalls 把工具分成"并发安全 batch"和"串行 batch",并发 batch 通过 runConcurrently(L2473)执行。
prevalidate 必须与这个分批模型对齐:
callId 索引(不是 (toolName, args),避免并发同名调用冲突)shouldConfirmExecute 路径signal 级联 abort 所有 in-flight prevalidate| 风险 | 严重度 | 缓解 |
|---|---|---|
| 缓存 diff 与确认时实际文件不一致(TOCTOU) | 高 | 方案 A:Edit 不进 allowlist;方案 B:缓存附 (mtime, size, hash) 校验 |
| prevalidate 失败影响调度 | 低 | 失败/超时退回原 shouldConfirmExecute 路径,缓存缺失 ≡ 未启用 |
| 并发 prevalidate 共享 fd / 资源争抢 | 低 | QWEN_CODE_MAX_TOOL_CONCURRENCY 已限并发上限(默认 10) |
50-100ms/轮(仅 CONCURRENCY_SAFE_KINDS 范围)。若选方案 B 含 Edit,理论收益 100-200ms。
| 方向 | RT 收益 | 实施复杂度 | 质量风险 | 依赖 | 优先级 |
|---|---|---|---|---|---|
| D1 工具后置指令 | 3-4s/终态轮 | 低(2-3d) | 低 | 无 | P0 |
| D2 summary fast 路由 | 2-3s/summary 轮(待实测) | 中-高(9d) | 中-高 | D2 自带启发式 + 主 chat 验证实验 + ACP 同步 | P1 |
| D3 展示解耦 | 3-4s 感知改善(依赖用户行为) | 中(3-5d,含不变量修复) | 中 | D1 历史不变量修复 | P1 |
| D4 流式提前调度 | 50-200ms/轮 | 高(5-7d) | 极低 | 无 | P2 |
| 子任务 | 估时 |
|---|---|
| 主 chat fastModel-streaming 验证实验(含 P_compact 测量) | 1d |
Fast 候选模型基线测量(含 TTFT、P95、thinkingConfig 兼容性) | 1d |
selectContinuationTier + summaryTierRef 接入(useGeminiStream) | 0.5d |
启发式实现(含 MUTATOR_KINDS 复用 / wouldTriggerCompression 估算 / 多语言 / 状态突变) | 1d |
GeminiChat.retryStreamWithModel + discardPendingAssistant 接口实现 | 1.5d |
| ACP Session 同步改造(acp-integration/session/Session.ts) | 1d |
Telemetry span 修正(requested / actual 拆分) | 0.5d |
User-level setting summaryTierStrategy + JSON schema + /config 集成 | 0.5d |
| 单测(race、abort 时机、history 不变量、fallback 路径、ACP 路径) | 2d |
| 合计 | 9d |
注:早期估时 6.5d 未含 ACP 路径、
wouldTriggerCompressiongate、清理清单、settings schema 工程化等成本。
ToolResult.postExecution(tools.ts L422):skipLlmRound + resultIsTerminalhandleCompletedTools 实现 skipLlmRound 短路(useGeminiStream.ts L2038)resultIsTerminal(留给 Phase 3)skipLlmRound / resultIsTerminal(见 §3.1 表)修正:早期路线图估 1 周,未含 fastModel-streaming 验证实验、
retryStreamWithModel实现、不变量统一修复、ACP 路径同步。
P_compact 与 thinkingConfig 兼容性)summaryTierRef + selectContinuationTier(含 wouldTriggerCompression gate)GeminiChat.retryStreamWithModel + discardPendingAssistantStreamingState.Summarizing + 输入路径复用 + abort 清理清单experimental.summaryRoundFastModel: false,Release N 默认关summaryTierStrategyCoreToolScheduler.prevalidate + allowlistprocessGeminiStreamEvents 增量调度| 指标 | 基线 | Phase 1 | Phase 3 |
|---|---|---|---|
| 端到端 RT P50(3 轮 loop) | 13.4s | <10s | <8s(待实测) |
| 端到端 RT P95 | - | <13s | <12s(fallback 路径上限) |
| 用户感知首结果时间 P50 | 13.4s | <10s | <5s(D3 启用) |
| 用户感知首结果时间 P95 | - | <13s | <8s |
| LLM 调用次数(可跳过场景) | 3 | 2 | 2(更快) |
注:基线为单次采样,落地前需补 ≥3 类场景。
| 指标 | 基线 | 允许退化 |
|---|---|---|
| Tool-calling 准确率(fast model summary 轮) | 100% | ≥98% |
| skipLlmRound 误用率(用户追问"再详细些") | - | <1% |
| Fast model fallback_triggered 率 | - | <10%(>20% 自动关 flag) |
| Summarizing 状态下半句 assistant 入 history | 0 | 0(硬性) |
| 指标 | 基线 | Phase 3 目标 |
|---|---|---|
| 每千会话 token 成本(summary 轮) | 100% | <70% |
| Fallback 路径浪费 tokens 占比 | 0 | <15%(fallback 率 × 单次 fast tokens / 单次 primary tokens) |
每次 selectContinuationTier 与 handleCompletedTools 的关键判定写一条结构化日志:
{
turn_id, prompt_id,
decision: 'skip' | 'fast' | 'primary',
tier_requested: 'fast' | 'primary', // 决策(fallback 前)
tier_actual: 'fast' | 'primary', // 实际跑(fallback 后)
signal_skipLlmRound: bool,
signal_resultIsTerminal: bool,
user_strategy: 'auto' | 'always_primary' | 'always_fast',
veto_reason: 'further_action' | 'write_tool' | 'unresolved_error' |
'deep_reasoning' | 'cross_result' | 'output_tokens' |
'lang_unsupported' | 'compact_or_clear' | null,
tool_count, distinct_tool_count,
has_write_tool: bool,
has_error: bool, has_cancel: bool,
output_tokens_est: int,
user_prompt_classification: 'query' | 'action' | 'analysis',
fast_ttft_ms, primary_ttft_ms, // fallback 时双份
fast_tokens_consumed: int, // fallback 浪费的 tokens(成本归因)
total_rt_ms,
fallback_triggered: bool,
fallback_reason: 'tool_call_seen' | 'timeout' | 'error' | null,
}
观察指标:
fast_tokens_consumed 测量说明:
abort 中断的 stream 大概率收不到 finishReason / usageMetadata——后者只在 stream 完整结束时填充。实现需估算:
stream.return() 让生成器走 finally 路径,可能拿到 partial usagetokens_source: 'usage' | 'estimated',事后分析需区分/tmp/tool-timing.log 计时框架T_userIdle(用户可再次输入时刻)T_firstToken(流式首 token 时刻)Qwen Code 是本地 CLI,没有运行时下发能力——传统"5% / 25% / 100% 灰度"不适用。采用阶段性 release 推进:
| 阶段 | Release 节点 | feature flag 默认值 | 触发条件 |
|---|---|---|---|
| Phase 3a:dogfood | Release N | false | 内部用户用 summaryTierStrategy=always_fast 自启用 |
| Phase 3b:opt-in 默认 | Release N+1(≥2 周后) | false(不变) | dogfood 阶段决策日志达标:fallback <10%、净 RT/cost 收益 >0 |
| Phase 3c:默认开启 | Release N+2(≥4 周后) | true | Phase 3b 用户层面无质量回归报告 |
| 回滚 | Release N+3(如需) | true → false | 大规模 fallback >20% 或质量指标退化 |
回滚机制:
summaryTierStrategy=always_primary 始终提供"我要立刻退出"通道,不依赖新 releasefallback_rate / cost_regression 在每个 Release 周期评估,决定下一步skipLlmRound 是质量换速度:跳过 LLM = 放弃模型理解和纠错,仅适用确定性高场景tryCompress 触发可能反向恶化:fast 模型 context 小,compression 自身耗 LLM 调用——wouldTriggerCompression gate 是必备防御| 文件 | 关键符号 | 位置 |
|---|---|---|
packages/core/src/tools/tools.ts | ToolResult interface | L422 |
packages/core/src/tools/tools.ts | Kind enum + MUTATOR_KINDS + CONCURRENCY_SAFE_KINDS | L793, L806, L818 |
packages/core/src/tools/tools.ts | DeclarativeTool.kind: Kind(每个 Tool 实例都带) | L165 |
packages/core/src/core/client.ts | SendMessageOptions.modelOverride | L142 |
packages/core/src/core/client.ts | sendMessageStream | L1216 |
packages/core/src/core/client.ts | modelOverride ?? getModel() | L1305, L1598 |
packages/core/src/core/client.ts | turn.run(model, …) | L1707 |
packages/core/src/core/geminiChat.ts | sendMessageStream(model, …) | L1387 |
packages/core/src/core/geminiChat.ts | history.push(userContent) | L1428 |
packages/core/src/core/geminiChat.ts | sendPromise 锁 | L1392 |
packages/cli/src/ui/hooks/useGeminiStream.ts | modelOverrideRef(skill 选模型) | L376, L2225 |
packages/cli/src/ui/hooks/useGeminiStream.ts | processGeminiStreamEvents | L1365 |
packages/cli/src/ui/hooks/useGeminiStream.ts | sendMessageStream 调用点 | L1841 |
packages/cli/src/ui/hooks/useGeminiStream.ts | handleCompletedTools | L2038 |
packages/cli/src/ui/hooks/useGeminiStream.ts | submitQuery(ToolResult, …) | L2355 |
packages/core/src/services/toolUseSummary.ts | fast-model side query(非流式先例) | L108 |
packages/core/src/followup/speculation.ts | fast-model streaming(forked chat 先例) | L224 |
packages/core/src/config/config.ts | fastModel + getFastModel + setFastModel | L684, L1987, L2021 |
packages/core/src/core/coreToolScheduler.ts | attemptExecutionOfScheduledCalls | L2436 |
packages/core/src/core/coreToolScheduler.ts | runConcurrently + partitionToolCalls | L2473 |
packages/cli/src/acp-integration/session/Session.ts | sendMessageStream 调用点(ACP / IDE 路径) | L705, L965, L1182, L1423 |
packages/core/src/agents/runtime/agent-core.ts | Subagent sendMessageStream(不受 D2 影响) | L614 |
针对设计文档中只声明、未量化的几条前置数据质量假设与收益估算,启动 4 个并行 Explore subagent 做只读代码调研。每个 subagent 只回答一个事实问题,不做判断,不给优化建议。调研基于当前 main 分支(HEAD: 026f2f768)。
| 验证问题 | 关联章节 |
|---|---|
Q3 当前所有工具的 ToolResult.error 字段填充率 | §3.2 hasUnresolvedError 前置依赖 |
Q4 stream abort 后 usageMetadata 实际可得性 | §5.4 fast_tokens_consumed 测量 |
| Q5 "用户追问 / clarification" 埋点存在性 | §5.2 fast 质量回归监控信号 |
Q6 CONCURRENCY_SAFE_KINDS 工具 shouldConfirmExecute 实际 IO 工作量 | §3.4 D4 收益估算 |
hasUnresolvedError 启发式存在 32% 工具盲区(影响 D2)事实:在 22 个有错误路径的工具中,15 个(68%)规范填 ToolResult.error 字段(shell、read-file、write-file、edit、grep、glob、ls、web-fetch、mcp-tool、cron-* 等核心 I/O 工具齐备),7 个(32%)仅把错误塞进 llmContent 字符串:askUserQuestion、monitor、skill、lsp、exitPlanMode、todoWrite 等。
不存在统一的 createErrorResult helper,每个工具独立实现错误构造。
对设计的影响:
hasUnresolvedError 否决项若仅检查 ToolResult.error 字段,这 7 个工具的失败永远不会触发"切回 primary"——下一轮仍会被路由到 fast modelskill 工具的失败被 fast model 错误总结是高优风险场景(本仓库大量 skill 驱动的工作流会被影响)建议修正:把 "将 7 个仅靠 llmContent 传错的工具改造为规范填 error 字段" 列为 D2 的硬前置依赖(§3.2 前置条件),估时 ~2d;不接受 "用 llmContent.match(/^Error:/i) 兜底" 的脏路径(误判风险高)。
fast_tokens_consumed 指标实现成本被低估(影响 D2 / §5.3)事实:
turn.ts 的 abort 路径(L289-291)直接 return,没有 finally 块,也没有 stream.return() 调用——文档 §5.4 暗示的 "abort 前 stream.return() 让生成器走 finally" 在当前代码中不存在该入口geminiChat.ts:processStreamResponse 的 for await 循环只在完整遍历时记录 turn(L1286),abort 中断意味着最后的 usage-only chunk(通常携带完整 metadata)被直接丢弃agent.ts:731-744)有累计,无法复用usageMetadata 零获取,只能靠 chars/4 估算(±20% 误差)对设计的影响:
sendMessageStream 生成器结构加 finally,工作量约 1d,设计文档没体现这笔成本建议修正:
fallback_triggered 率 + fast_tokens_consumed 同向趋势" 双指标联合判断fast_tokens_consumed 实现需先改造 turn.ts abort 路径加 finally + stream.return(),作为 §3.2 工作量补充(+1d)user_prompt_classification 与"用户追问"埋点需新建(影响 D2 / §5.2)事实:
packages/core/src/followup/ 已存在 speculation.ts / suggestionGenerator.ts / followupState.ts,但其 telemetry(PromptSuggestionEvent)记录的是 "系统建议被采纳/忽略",不是"用户主动追问"ChatRecordingService 存储用户消息但不打分类标签user_prompt_classification、无中英文追问模式匹配、无 clarif* / intentDetect 类机制对设计的影响:
user_prompt_classification: 'query' | 'action' | 'analysis' 字段没有数据源——既不能从现有 PromptSuggestionEvent 推导,也不能从 ChatRecord 读出followupState.onOutcome 不可复用建议修正:
user_prompt_classification 与 requestImpliesFurtherAction 都缺数据fallback_triggered 率监控质量回归——成本低但风险高事实:
Kind.Read(read_file)、Kind.Search(glob / grep)、Kind.Fetch(web_fetch)三类工具的 shouldConfirmExecute / getConfirmationDetails,绝大多数继承 BaseToolInvocation 默认实现,做零 IO(read_file / glob / grep 完全没 override,web_fetch 只做 5-10 行字符串解析 URL hostname)Edit / WriteFile(calculateEdit + readTextFile + Diff.createPatch,典型 ~20ms),但 §3.4 方案 A 把它们排除出 allowlist 以规避 TOCTOU对设计的影响:
CONCURRENCY_SAFE_KINDS建议修正:§3.4 重写收益归因——
| 章节 | 原估时 | 验证后估时 | 增量来源 |
|---|---|---|---|
| D2 §3.2 工作量(§4.1 细分表) | 9d | 14-16d | +2d(发现 1 前置工具改造)+1d(发现 2 turn.ts finally 改造)+3d(发现 3 输入分类器,如取硬路径) |
| D4 §3.4 综合评估 | 5-7d | 5-7d(不变) | 工作量不变,但 RT 收益归因从"工具端 IO"改为"调度模型",投入 ROI 下调 |
| Phase 3 总时长(§4.2) | ~3 周 | ~4-5 周 | D2 工作量上调 + 前置工具改造 PR 单独走 review 周期 |
对原路线图的修正建议:
以下追问点属于主观判断或作者意图问题,本次验证未通过 subagent 处理,留作后续 design review 讨论:
needsCrossResultReasoning 阈值 ≥3 是否反向拟合 §1.2 基线场景(作者意图)§6 验证之后,又发现两个改变 ROI 判断的事实:
DashScope cache_control 已实装(packages/core/src/core/openaiContentGenerator/provider/dashscope.ts:172-181)
system + 最后一条 message + 最后一个 tool definitioncached_tokens 已采集到 usageMetadata.cachedContentTokenCount(converter.ts:1124-1149)system prompt 已经稳态(prompts.ts 审计结果)
process.cwd() 仅用作 isGitRepository() 开关,不写入 prompt 内容save_memory 工具触发 / /model 切换 / MCP 动态加载(均事件性,低频)§3.2 文档假设 "fast model 比 primary 快 ~2s",对照基线是 primary uncached vs fast uncached。
但现实运行中 primary 是 cached(summary 轮恰好命中最强),所以正确对照是:
primary cached vs fast uncached
| 路由 | 估算延迟 | 备注 |
|---|---|---|
| primary 命中 80% 前缀 cache | ~1.8-2.2s | summary 轮的当前实际表现 |
| fast 无 cache(跨模型不共享) | ~1.5-2s | D2 切换后的实际表现 |
净差距:几百毫秒,甚至可能 fast 反而慢。叠加 14-16d 工程成本 + 质量风险 + fallback 浪费,D2 净收益接近 0 或负。
§3.2 前置条件必须新增:基线测量必须对比 primary cached vs fast uncached,且 T_primary_cached < T_fast_uncached × 1.5 时 D2 不应启用。
真·浮油(立刻动手,< 1d 投入,极低风险,确定收益):
| 项 | 投入 | 收益 | 操作位置 |
|---|---|---|---|
| 简洁回复指令 | 30min | ~2s/summary 轮(输出 token 减半) | prompts.ts Final Reminder 段加一句 |
| 暴露 cache hit rate telemetry | 0.5d | 0s 直接,是后续决策 enabler | cachedContentTokenCount 已采集,缺暴露;并应识别 save_memory 后单独打标 |
近浮油(等数据决定,0.5-1d 投入):
| 项 | 投入 | 收益 | 决策前置 |
|---|---|---|---|
summary 轮 tool_choice='none' | 0.5-1d | 0.3-1s(sampling 跳过 tool_call token) | 需"是 summary 轮"判定逻辑,错判风险低 |
| summary 轮关 thinking | 1d | 0.5-2s | 仅对启用 thinking 的模型有意义(qwen3.5-plus、glm-4.7、kimi-k2.5 等) |
| UI 渲染层 chunk batching | 0.5d 调研 + 0.5d 实施 | 待验证 | 假设:长 summary 的 useGeminiStream token 渲染累计开销不小 |
待调研(可能是大鱼):
| 项 | 调研投入 | 潜在收益 | 关键未知 |
|---|---|---|---|
scope: 'global' 支持 | 已调研,结论 (c) 不可行(见 §7.4 发现 B 调研结果)。此行保留作为决策记录,不要重启调研 |
中等改造(不算浮油,单独评估):
| 项 | 投入 | 风险 | 收益 |
|---|---|---|---|
D1 skipLlmRound(终态查询场景) | 2-3d | 中 | 3-4s/终态轮 |
| summary 轮工具结果裁剪(D5 子集) | 2d | 中 | 1-2s |
D3 Summarizing 状态 | 3-5d | 中 | 感知改善 3s |
| system prompt 减肥 | 2-3d 含 A/B 测试 | 中 | 0.5-1s |
已废弃方向(不要再做):
| 项 | 废弃原因 |
|---|---|
| D2 fast model 路由 | 被 DashScope cache 抵消,净收益接近 0 或负 |
| D4 prevalidate | 收益归因错(真实仅 ~50ms 来自调度模型),5-7d 投入不值 |
| system prompt 稳定化 | 已稳态,无事可做 |
| 流式提前 terminal(提前 abort 收尾客套话) | 高误判风险,用户感知答案被切断 |
tool_choice='none' 的真实机制OpenAI / DashScope API 里 tool_choice='none' 不仅是"禁止调工具"——模型 sampling 阶段会完全跳过 <tool_call> 特殊 token 的概率分配,decoder 直接走自然语言生成路径。收益不在"省一两次 retry",而在 sampling 本身更快。
scope: 'global' 在仓库已有 Anthropic 先例packages/core/src/core/anthropicContentGenerator/converter.test.ts:85, 1543 已有 cache_control: { type: 'ephemeral', scope: 'global' } 用法。但 provider/dashscope.ts:288 标 cache_control 时没传 scope:
cache_control: { type: 'ephemeral' }, // 没有 scope
若 DashScope 服务端识别 scope: 'global':
通过查阿里云百炼官方文档 help.aliyun.com/zh/model-studio/context-cache 得到的事实清单:
| 问题 | 结论 | 证据 |
|---|---|---|
scope 字段支持 | 不支持。仅识别 type: 'ephemeral',任何 scope/persistent/global 会被 silently dropped | 官方文档原文:"仅支持将 type 设置为 ephemeral" |
| ephemeral 实际 TTL | 5 分钟滑动窗口(命中后重置) | 百炼文档明确说明 |
| 长 TTL / 全局机制 | 无任何公有云 API 端机制。无 persistent type 值、无独立预上传 API、无 prompt_cache_key;唯一"全局持久"产品是 PAI 全局上下文缓存(自部署 + vLLM + 灵骏 + 共享 Redis),与 DashScope API 无关 | PAI 文档 |
| 跨 session 共享 | 同账号 + 同模型 + 内容匹配 → 已经命中(这就是 ephemeral 已经在做的);不同账号绝对不共享 | 百炼文档 |
| 定价 | cache write 125%、显式 cache read 10%、隐式 cache read 20%(无 cache_control 标记也能拿到隐式 20% 折扣) | 百炼定价文档 |
| 最小可缓存 prompt | 1024 tokens | 百炼文档 |
| 模型支持(显式 cache) | qwen3.7-max / qwen3.6-plus / qwen3.5-plus / qwen3-coder-plus / qwen3-vl-plus / deepseek-v3.2 / kimi-k2.5 / glm-5.1 均显式列出。qwen3.6-plus 与 qwen3.7-max 同样享受 90% 显式 cache 折扣 | 百炼模型列表(2026-05-26 重核) |
几条副发现的连带意义:
cache_control 也能拿;但精细控制需要显式qwen3.6-plus 未在显式列表dashscope.ts:288 当前做法已经是 DashScope 公有云 API 的能力上限——没有继续榨的空间对 §7.2 D2 判断的连带加强:
TTL 滑动窗口意味着 agent loop 内 summary 轮几乎 100% 命中 primary 的 cache(前几轮刚刚命中过、5min 内)。D2 切 fast model 不仅会打碎累计的 cache 写入链,还会让 summary 轮从"近 100% 命中"退化为"完全 miss"——净收益判断比 §7.2 原假设更明确为负。
§1.2 基线把"框架开销"标为 0.3s(3%),但这是粗估。Ink 7 + React 19.2 在每个 chunk 触发 setState → re-render,长 summary 累计可能 200-500ms。需要查 useGeminiStream 怎么处理 token 流,有没有 requestAnimationFrame / useDeferredValue 合并 chunk。
本节是这份文档的活动入口:后续有任何度量数据,对照下表决定该回看哪个决策。
触发条件:浮油"暴露 cache hit rate telemetry"上线 ≥3 天,决策日志含 cached_tokens / prompt_tokens 分布。
该看的数据:
save_memory 触发后下一轮命中率(应该接近 0)/model 切换后下一轮命中率(应该接近 0)决策路径:
| 整体命中率 | 含义 | 行动 |
|---|---|---|
| > 70% | 现状已经接近理论上限 | 只做 #1 简洁指令 + 发现 B 调研;其余浮油按需 |
| 40-70% | 还有空间但来源不明 | 分析按轮次命中率,找出哪一段在 miss |
| < 40% | 有动态点在打 cache | 重新审计 system prompt / userMemory 触发频率;可能 save_memory 比预期频繁 |
scope: 'global' 文档调研结果 ✅ 已完成(2026-05-26)结果:完全不识别。详见 §7.4 发现 B 的"调研结果"段。
已执行行动:接受现状,跳过此项。dashscope.ts:288 维持现有 ephemeral 标记,无需改造。
后续不要重新启动此调研——除非 DashScope 官方公告新增持久化机制。
触发条件:发现 C 调研完成(看 useGeminiStream token 流处理 + Ink/React DevTools 实测)。
决策路径:
| 结果 | 行动 |
|---|---|
| 长 summary stream 渲染累计 > 200ms | 改用 batching(useDeferredValue 或自定义节流) |
| 渲染开销 < 100ms | 关闭此线索 |
触发条件:#1 简洁指令 + Checkpoint 1/2/3 决策完成 ≥1 周。
该看的数据:
决策路径:
| 累计节省 | 行动 |
|---|---|
| > 4s(达到 9.6s 端到端 P50) | 评估 D1 skipLlmRound(再省 3-4s/终态轮) |
| 2-4s | 接受现状,评估 D3 感知改善是否值得做 |
| < 2s | 重新审视:是否浮油本身被高估,还是有未识别的瓶颈(网络 RTT、provider 端延迟) |
基于 §6 验证 + 本节 ROI 重排:
| 方向 | §3 原优先级 | 本节判定 | 理由 |
|---|---|---|---|
| D1 工具后置指令 | P0 | P0 保留,但等浮油完成后再评估 | ROI 仍然好,但不再"立刻就做"——先把更便宜的浮油拿掉 |
| D2 summary fast 路由 | P1 | Defer / Won't Fix | 被 DashScope cache 抵消,14-16d 投入换接近 0 收益 |
| D3 展示解耦 | P1 | 保留为可选,看 Checkpoint 4 数据 | 感知改善确定,但绝对 RT 不变,依赖用户行为 |
| D4 流式提前调度 | P2 | Defer | 收益归因错,真实 ~50ms 不值 5-7d |
Day 1(可单人单日完成):
prompts.ts 加简洁回复指令(30min)cachedContentTokenCount 暴露到 telemetry + save_memory / /model 切换打标(0.5d)scope: 'global' 文档查询 + 现有 Anthropic 用法对照(0.5d)Day 2-3:
useGeminiStream 的 React 渲染路径scope: 'global' 改造Week 1 末:
tool_choice='none' / 关 thinking(根据 hit rate 数据)Week 2-3:
始终不做:D2 / D4 / system prompt 稳定化。
prompts.ts 动态内容审计(2026-05-27)§7.1 给出 "system prompt 已稳态" 的结论时只做了粗略 grep。本节是对 packages/core/src/core/prompts.ts(1169 行)的系统性审计,列清单作为后续 cache 命中率分析与浮油决策的依据。
审计方法:枚举所有 ${...} 插值表达式、IIFE、process.* / new Date / Date.now / Math.random / fs.* 调用,对每一处判断"在同一 session 内是否会变化"。
| 候选 | 代码事实 |
|---|---|
Date.now() / new Date() | 全文 零次出现(rg 全无匹配) |
Math.random() | 零次出现 |
process.cwd() 值写入 prompt | 仅 L366 if (isGitRepository(process.cwd())) { ... },值不写入字符串,只作开关 |
| git status / git branch 子进程调用 | 零次,git 段是静态指导文本 |
| 当前文件列表 / 项目结构注入 | 零次 |
| LSP 状态 / 错误数 | 零次 |
| 用户输入历史 | 零次(history 走 messages,不在 system) |
| 位置 | 内容 | 何时可能变 |
|---|---|---|
| L190 | process.env['QWEN_SYSTEM_MD'] 决定 basePrompt 来源(默认 vs 用户 system.md) | 进程内不变 |
| L342-343 | process.env['SANDBOX'] 决定 sandbox 段选哪一版(Seatbelt / Sandbox / Outside) | 进程内不变 |
| L366 | isGitRepository(process.cwd()) 决定 git 段是否插入 | cwd 同 session 内通常不变 |
| L871 | process.env['QWEN_CODE_TOOL_CALL_STYLE'] 决定 tool call 风格(qwen-coder / qwen-vl / general) | 进程内不变 |
| 参数 | 触发条件 | 频率估计 |
|---|---|---|
userMemory(getCoreSystemPrompt 第 1 参) | save_memory 工具 / /memory refresh / 扩展加载 | 0-3 次/session |
model 名(影响 getToolCallExamples 选哪一支) | /model 切换 | 罕见 |
appendInstruction | 配置项,session 内基本不变 | 几乎从不 |
deferredTools(buildDeferredToolsSection) | MCP 工具动态加载 | session 启动期居多 |
L207-209:若设置了 QWEN_SYSTEM_MD env,每次 getCoreSystemPrompt 都会 fs.readFileSync(systemMdPath):
const basePrompt = systemMdEnabled
? fs.readFileSync(systemMdPath, 'utf8')
: `...`;
.qwen/system.md,网络挂载文件会更慢)save_memory——核心功能,不能为 cache 让路