重构: F-09 batch5 llm_concurrency会话级改造(决策c)
- state.rs: per_conv HashMap<conv_id,Semaphore>(每对话permits=2) + per_conv_permits热改 + acquire_per_conv(conv_id) + release_conv - agentic.rs: loop入口acquire_global+acquire_per_conv(conv_id)整loop持有(会话级并发上限3) + 删每轮acquire/drop - compress/title/knowledge_inject: acquire_per_conv(conv_id)+签名conv_id - project.rs: 扫描合成key + ai_conversation_delete release_conv global: LLM调用并发→并发会话数上限;F-260616-12 retry permit自洽 主代兜底: cargo check --workspace 0 + test 96 + grep印证
This commit is contained in:
@@ -509,6 +509,17 @@ pub(crate) async fn run_agentic_loop(
|
||||
// 其 token 估算在 loop 外算一次缓存复用,避免每轮/每次重试重复 estimate_text(低收益优化,行为不变)。
|
||||
let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
|
||||
|
||||
// F-260616-09 B 批5 / 决策 c-1:global 改「并发会话数上限」,每 loop 入口拿 1 permit 持整个 loop
|
||||
// 生命周期(含工具执行/审批等待/重试)。同一时刻最多 N 个对话并发跑 loop(N=global permits,默认 3),
|
||||
// 第 N+1 个对话的 acquire_global await 阻塞排队。permit 绑 guard(函数返回)Drop 自动释放——
|
||||
// 各 return 点(save_conversation/stop/conv 删除/收敛/达 MAX)退出即释放槽位。
|
||||
// per_conv 同样整 loop 持有:同一对话内主循环 stream_llm + 后台标题/压缩/提炼共享该 conv 的 permits=2,
|
||||
// 防单对话内并发 LLM 调用失控。**F-260616-12 核验**:retry 同 loop 内,持 per_conv 合理;
|
||||
// global 是会话级,retry 不再阻塞他对话(原每轮 acquire/drop 语义下 global 短暂释放,
|
||||
// 现整 loop 持有更贴合"会话级并发上限"语义)。
|
||||
let _conv_global_permit = llm_concurrency.acquire_global().await;
|
||||
let _conv_per_conv_permit = llm_concurrency.acquire_per_conv(&conv_id).await;
|
||||
|
||||
for iteration in start_iteration..max_iterations {
|
||||
// 用户请求停止 → 收尾退出(已生成文本已在上一轮入库)
|
||||
if stop_flag.load(Ordering::SeqCst) {
|
||||
@@ -640,6 +651,7 @@ pub(crate) async fn run_agentic_loop(
|
||||
&provider_config,
|
||||
active_msgs,
|
||||
&lang,
|
||||
&conv_id,
|
||||
&llm_concurrency,
|
||||
).await.map(Some)
|
||||
};
|
||||
@@ -720,20 +732,19 @@ pub(crate) async fn run_agentic_loop(
|
||||
messages.iter().map(|m| est.estimate_message(m)).sum()
|
||||
};
|
||||
|
||||
// LLM 并发限流(全局 + 单对话双层),仅覆盖 stream_llm 调用本身;
|
||||
// 工具执行(process_tool_calls)是本地操作无 RPM 成本,permit 在 stream 后立即释放避免占槽
|
||||
// LLM 并发限流(global + per_conv 双层)— F-260616-09 B 批5 已上移到 loop 入口
|
||||
// (L520-521 _conv_global_permit/_conv_per_conv_permit),整 loop 持有(含工具执行/审批等待/重试)。
|
||||
// 原每轮 acquire + stream 后 drop(L747-748 acquire / L914-915 drop)已移除——会话级并发语义下,
|
||||
// permit 应绑 loop 生命周期而非单次 stream。工具执行期间占槽是决策 c 的有意行为
|
||||
// (会话级并发上限含全部 LLM 相关工作,非仅 stream 调用)。
|
||||
//
|
||||
// CR-30-1 / F-260616-07 / 决策 a1: 流前失败(Init Err)重试,流中途失败(MidStream
|
||||
// Partial)不重试保文。重试退避复用 retry::backoff_delay(1s→2s→4s±20% jitter) +
|
||||
// retry::is_status_retryable Fatal 分类(stream_recv classify_status_or_class 镜像,
|
||||
// 4xx 非429 立即放弃) + 30s 总挂钟预算。重试期间持有 permit 不释放(防新请求挤占)。
|
||||
//
|
||||
// F-260614-04 / F-260614-04b: global/per_conv permit 整轮迭代共享(不随 candidate 切换
|
||||
// 释放——并发语义是「全局/单对话」级,与具体 provider 无关);per-provider permit 改为
|
||||
// 在 candidate 循环内取(切换 candidate 时释放旧取新,避免占用未用 provider 的槽)。
|
||||
// 两 permit 均 Drop 释放(L803-804 显式 drop _global/_per_conv)。
|
||||
let _global_permit = llm_concurrency.acquire_global().await;
|
||||
let _per_conv_permit = llm_concurrency.acquire_per_conv().await;
|
||||
// F-260614-04 / F-260614-04b: per-provider permit 仍在 candidate 循环内取
|
||||
// (切换 candidate 时释放旧取新,避免占用未用 provider 的槽);global/per_conv 由 loop 入口持有。
|
||||
|
||||
// 重试总预算(挂钟,含 sleep + 各次请求耗时),对齐 retry::MAX_TOTAL_BUDGET 30s。
|
||||
// F-260614-04b:本轮各 candidate 共享一个 30s 预算(切换 provider 不重置预算,
|
||||
@@ -896,11 +907,9 @@ pub(crate) async fn run_agentic_loop(
|
||||
});
|
||||
return;
|
||||
}
|
||||
// stream 结束立即释放 permit,后续工具执行不受限流(本地操作无 RPM 成本)
|
||||
// per-provider permit(_provider_permit)在 candidate 循环内随作用域 Drop 自动释放,
|
||||
// 此处仅显式释放 global/per_conv(F-04b:per-provider 改为循环内取)。
|
||||
drop(_global_permit);
|
||||
drop(_per_conv_permit);
|
||||
// F-260616-09 B 批5: global/per_conv permit 已上移 loop 入口(L520-521)整 loop 持有,
|
||||
// stream 后不再立即释放(会话级并发语义:工具执行期间也占槽)。per-provider permit
|
||||
// (_provider_permit)仍在 candidate 循环内随作用域 Drop 自动释放(F-04b 语义不变)。
|
||||
|
||||
// 累加本轮 token:provider 流式 usage 的 prompt_tokens 为 0 时(GLM 等),用预估输入兜底
|
||||
let round_prompt = if round_usage.prompt_tokens == 0 { estimated_prompt } else { round_usage.prompt_tokens };
|
||||
|
||||
@@ -692,6 +692,7 @@ pub async fn ai_chat_compress_context(
|
||||
&provider_config,
|
||||
active_msgs,
|
||||
&lang,
|
||||
&conv_id,
|
||||
&llm_concurrency,
|
||||
).await {
|
||||
Ok(s) => s,
|
||||
@@ -1727,6 +1728,13 @@ pub async fn ai_conversation_delete(
|
||||
session.active_conversation_id = None;
|
||||
session.messages.clear();
|
||||
}
|
||||
// F-260616-09 B 批5:同时清理 LlmConcurrency 的 per_conv Semaphore 条目(防 HashMap 无限增长)。
|
||||
// conv 已删=LlmConcurrency 该条目不再被 acquire(无 conv 则无 loop/标题/提炼/压缩针对它)。
|
||||
// 已持 permit 不受影响(permit 绑旧 Arc,随 Drop 释放),仅阻止新条目累积。
|
||||
// 时机:conv 删除即清理(比"loop 结束 + 无 pending"更确定——conv 删了必无 pending,
|
||||
// 上述 retain 已清)。loop 正常收敛/达 MAX/stop 但 conv 未删时不清理(下次发消息复用,限流计数连续)。
|
||||
drop(session); // 释放 AiSession 锁再取 LlmConcurrency 锁(避免潜在锁序问题)
|
||||
state.llm_concurrency.release_conv(&conversation_id).await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ pub(crate) async fn compress_via_llm(
|
||||
provider_config: &AiProviderRecord,
|
||||
active_msgs: Vec<ChatMessage>,
|
||||
lang: &str,
|
||||
conv_id: &str,
|
||||
llm_concurrency: &LlmConcurrency,
|
||||
) -> Result<String, String> {
|
||||
if active_msgs.is_empty() {
|
||||
@@ -79,8 +80,9 @@ pub(crate) async fn compress_via_llm(
|
||||
};
|
||||
|
||||
// LLM 并发限流(压缩属独立调用,纳入双层 Semaphore)
|
||||
// F-09 B 批5: per_conv 改 HashMap<conv_id>,压缩在 loop 内针对本对话,用 conv_id 共享限流槽。
|
||||
let _global_permit = llm_concurrency.acquire_global().await;
|
||||
let _per_conv_permit = llm_concurrency.acquire_per_conv().await;
|
||||
let _per_conv_permit = llm_concurrency.acquire_per_conv(conv_id).await;
|
||||
|
||||
let resp = provider
|
||||
.complete(request)
|
||||
|
||||
@@ -388,8 +388,9 @@ async fn extract_knowledge_from_conversation(
|
||||
Err(e) => return Err(anyhow::anyhow!("provider 密钥不可用: {}", e)),
|
||||
};
|
||||
// LLM 并发限流(知识提炼属独立调用,纳入双层 Semaphore)
|
||||
// F-09 B 批5: per_conv 改 HashMap<conv_id>,知识提炼针对本对话,用 conv_id 共享该 conv 限流槽。
|
||||
let _global_permit = llm_concurrency.acquire_global().await;
|
||||
let _per_conv_permit = llm_concurrency.acquire_per_conv().await;
|
||||
let _per_conv_permit = llm_concurrency.acquire_per_conv(conv_id).await;
|
||||
let resp = provider.complete(request).await?;
|
||||
let raw = resp.text.trim();
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ pub(crate) async fn ensure_conversation_title(
|
||||
// 超时或返回 None 均保留上方已落的 extract 兜底,不覆盖。
|
||||
let title = match tokio::time::timeout(
|
||||
std::time::Duration::from_secs(20),
|
||||
generate_title_via_llm(&*provider, &title_model, summary_msgs, &llm_concurrency),
|
||||
generate_title_via_llm(&*provider, &title_model, summary_msgs, conv_id, &llm_concurrency),
|
||||
).await {
|
||||
Ok(Some(t)) => t,
|
||||
Ok(None) => {
|
||||
@@ -167,6 +167,7 @@ async fn generate_title_via_llm(
|
||||
provider: &dyn LlmProvider,
|
||||
model: &str,
|
||||
msgs: Vec<ChatMessage>,
|
||||
conv_id: &str,
|
||||
llm_concurrency: &LlmConcurrency,
|
||||
) -> Option<String> {
|
||||
let mut prompt = vec![ChatMessage::system(
|
||||
@@ -184,8 +185,9 @@ async fn generate_title_via_llm(
|
||||
reasoning_content: None,
|
||||
};
|
||||
// LLM 并发限流(标题生成属独立调用,纳入双层 Semaphore)
|
||||
// F-09 B 批5: per_conv 改 HashMap<conv_id>,标题针对本对话,用 conv_id 共享限流槽。
|
||||
let _global_permit = llm_concurrency.acquire_global().await;
|
||||
let _per_conv_permit = llm_concurrency.acquire_per_conv().await;
|
||||
let _per_conv_permit = llm_concurrency.acquire_per_conv(conv_id).await;
|
||||
let resp = provider.complete(request).await.ok()?;
|
||||
Some(clean_title(&resp.text))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user