修复: 子代理报告双缺陷——熔断补收尾轮 + 报告新近度豁免

① 熔断无收尾轮: max_rounds 打满(达 effective_max 上界且未收敛)时, 旧逻辑直接 teardown 截断, 父任务/消费方拿到的是末轮思考独白而非任务报告。现补一次无工具收尾轮: system 指令「输出最终任务总结报告」+ tools=None(stream_round 空工具表), 成功则 push 报告文本+落库再走原收尾出口; 失败(全候选耗尽/MidStream/Stale)不重试不停靠, 静默落回原截断路径——报告是增益非闸门, 预算仅 1 轮 max_retries=0。

② 报告被视图压缩吞掉: summarize_tool_results 加新近度豁免(RECENCY_EXEMPT_TOOL_RESULTS=2), 末尾 2 条 tool_result 恒不压缩——豁免判定在冻结决策之前且不进决策表, 无前缀缓存漂移; 委托完成报告/末轮工具结果全文保留给 LLM 下一轮消费, 根治「>2KB envelope 下一轮即被 JSON 分支折叠 512 字符, 父 LLM 从未见过报告全文」。

测试: 新增 summarize_recency_exempt_keeps_last_two_tool_results 锁定豁免行为; cargo test -p devflow --lib 712 passed 0 failed; todo.md 两项销账。
This commit is contained in:
lxy
2026-10-06 16:47:07 +08:00
parent 438d47bc9f
commit 9ff26f0c8f
7 changed files with 3338 additions and 198 deletions
+62 -1
View File
@@ -1374,8 +1374,69 @@ pub(crate) async fn run_agentic_loop(
tracing::warn!(
conv_id = %conv_id,
max_iter = max_iterations,
"[ai] agentic 循环达最大轮次仍未收敛,自动完成(incomplete=true)",
"[ai] agentic 循环达最大轮次仍未收敛,补一次无工具收尾轮再自动完成(incomplete=true)",
);
// ── 熔断收尾轮(2026-10-06):max_rounds 打满是常态,旧逻辑直接截断时父任务/
// 消费方拿到的是「末轮思考独白甚至半截工具调用」,而非任务报告。修法:补一次
// 无工具(tools=None)收尾请求——LLM 只能输出文本,即最终总结报告。失败(全候选
// 耗尽/网络断)不重试不停靠,静默落回原截断收尾(报告是增益非闸门)。
{
let (wrapping, est, req) = match prepare_round_inputs(
&session_arc, &db, &app_handle, &conv_id, effective_max.saturating_sub(1),
&stop_flag, &loop_epoch_arc, my_epoch,
&mut guard, &mut tokens, &mut saved_token_snapshot,
last_round_estimated, &provider_config, &llm_concurrency,
&pinned_goals_snapshot, sys_tokens, &system_prompt, &volatile_tail, &intent,
).await {
PreparedRound::Ready { messages, estimated_prompt, agentic_req } => {
(messages, estimated_prompt, agentic_req)
}
// 收尾轮入口闸门不过(stop/stale):guard/收尾已由 prepare 内部处理,直接退
PreparedRound::Exit => return,
};
// 注入收尾指令:明确告知「这是最后一轮,输出任务总结报告」(无工具可调)
let mut wrap_msgs = wrapping;
wrap_msgs.push(ChatMessage::system(
"【任务收尾】本轮次预算已用完,这是最后一轮。请基于以上全部工作输出最终任务总结报告:已完成事项/关键结论/遗留问题与建议。不要再尝试调用任何工具。",
));
match stream_round(
&session_arc, &db, &conv_id,
&wrap_msgs, &[], &app_handle,
&stop_flag, &notify,
0, &model_override, &req, est,
&mut last_round_estimated, &mut last_reasoning_content,
&mut resolved_model, &mut provider_config,
&mut provider_saturation,
&mut tokens, &mut saved_token_snapshot, &mut guard,
&candidates, &llm_concurrency,
&pinned_goals_snapshot,
&loop_epoch_arc, my_epoch,
).await {
// 收尾轮成功:push 报告文本 + 落库(报告进会话,消费方可见)。无工具调用,
// finalize 走收敛分支不会再起轮;此处手动 push(不经 finalize,其会 emit 终态)。
RoundOutcome::Continue { full_text, .. } => {
let text = full_text.trim();
if !text.is_empty() {
let mut session = session_arc.lock().await;
if session.per_conv.contains_key(&conv_id) {
let conv = session.conv(&conv_id);
let mut msg = ChatMessage::assistant(text);
msg.model = Some(resolved_model.clone());
conv.messages.push(msg);
}
drop(session);
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
save_conversation(&session_arc, &db, &conv_id, Some(&save_usage), Some(&resolved_model), false).await;
}
}
// 收尾轮任何失败(MidStream/Fatal/Stale):不重试不停靠,
// 落回下方原截断收尾路径(guard 可能已被 stream 内部处理,teardown 幂等)。
_ => {
tracing::warn!(conv_id = %conv_id, "[ai] 熔断收尾轮失败,落回截断收尾");
}
}
}
// save_usage 增量;emit_usage 累计快照(前端展示本 loop 总量)
let save_usage = usage_delta_since(&tokens, &mut saved_token_snapshot);
let emit_usage = tokens_snapshot(&tokens);