修复: DeepSeek reasoning_content 全链路透传(跨6 crate补字段+映射+agentic loop累积回传)

This commit is contained in:
2026-06-17 18:08:04 +08:00
parent 1cd7652a34
commit 74003bc04f
11 changed files with 251 additions and 20 deletions

View File

@@ -131,6 +131,8 @@ enum StreamOutcome {
usage: df_ai::provider::TokenUsage,
incomplete: bool,
resolved_model: String,
/// DeepSeek thinking 模式推理内容(需回传到下一轮请求)
reasoning_content: Option<String>,
},
InitFailedExhausted,
Fatal,
@@ -164,6 +166,7 @@ async fn stream_one_provider(
retry_deadline: tokio::time::Instant,
model_override: &Option<String>,
agentic_req: &TaskRequirements,
last_reasoning_content: &Option<String>,
) -> StreamOutcome {
// resolve→ensure→build 三步(复用 secret::build_provider_for,DRY)。
// key 缺失/损坏 → Err → 归 Fatal(stream_llm Fatal 也是 key 类错,语义一致)。
@@ -209,17 +212,19 @@ async fn stream_one_provider(
stream: true,
tools: if tool_defs.is_empty() { None } else { Some(tool_defs.to_vec()) },
tool_choice: None,
reasoning_content: last_reasoning_content.clone(),
};
match stream_llm(&*provider, retry_request, app_handle, stop_flag, notify, conv_id).await {
StreamResult::Complete { text, tool_calls, usage } => {
StreamResult::Complete { text, tool_calls, usage, reasoning_content } => {
return StreamOutcome::Success {
text, tool_calls, usage,
incomplete: false,
resolved_model,
reasoning_content,
};
}
StreamResult::Partial { text, tool_calls, usage } => {
StreamResult::Partial { text, tool_calls, usage, reasoning_content } => {
// MidStream 保文不重试(决策 a1)。携带 incomplete=true 交调用方走保文路径。
tracing::warn!(
conv_id = %conv_id,
@@ -231,6 +236,7 @@ async fn stream_one_provider(
text, tool_calls, usage,
incomplete: true,
resolved_model,
reasoning_content,
};
}
StreamResult::InitFailed { retryable } => {
@@ -435,6 +441,9 @@ pub(crate) async fn run_agentic_loop(
// 区分"正常收敛退出"与"达 MAX 被截断退出"——后者末轮 tool_calls 仍非空(tool_result 不再回传 LLM),属异常
let mut converged = false;
// BUG-260617-12: DeepSeek thinking 模式推理内容跨轮透传
let mut last_reasoning_content: Option<String> = None;
// F-260616-13: system_prompt 是 run_agentic_loop 的不变参数(整个 loop 期间文本不变),
// 其 token 估算在 loop 外算一次缓存复用,避免每轮/每次重试重复 estimate_text(低收益优化,行为不变)。
let sys_tokens = TokenEstimator::default().estimate_text(&system_prompt);
@@ -656,9 +665,9 @@ pub(crate) async fn run_agentic_loop(
let mut candidate_chain: Vec<AiProviderRecord> = Vec::with_capacity(1 + candidates.len());
candidate_chain.push(provider_config.clone());
candidate_chain.extend(candidates.iter().cloned());
let (full_text, tool_calls_acc, round_usage, incomplete) = {
let (full_text, tool_calls_acc, round_usage, incomplete, round_reasoning_content) = {
// outcome 累积成功结果(Complete/Partial),InitFailed 不写入。
let mut outcome: Option<(String, std::collections::HashMap<u32, super::ToolCallDraft>, df_ai::provider::TokenUsage, bool)> = None;
let mut outcome: Option<(String, std::collections::HashMap<u32, super::ToolCallDraft>, df_ai::provider::TokenUsage, bool, Option<String>)> = None;
'candidate: for candidate in &candidate_chain {
// per-provider permit(可选):set_provider_caps 未配置时返回 None(单 provider 零变化);
@@ -678,14 +687,16 @@ pub(crate) async fn run_agentic_loop(
retry_deadline,
&model_override,
&agentic_req,
&last_reasoning_content,
).await {
StreamOutcome::Success { text, tool_calls, usage, incomplete, resolved_model: m } => {
StreamOutcome::Success { text, tool_calls, usage, incomplete, resolved_model: m, reasoning_content: rc } => {
// 成功:更新迭代级 resolved_model + provider_config(供后续 push/save/标题)。
// mut 解构(已在顶部声明 mut):本轮后续 save/push 用「实际成功所用 provider」
// 而非主 candidate。compress 已在本轮 stream 之前用过 primary,不受影响。
resolved_model = m;
provider_config = candidate.clone();
outcome = Some((text, tool_calls, usage, incomplete));
last_reasoning_content = rc;
outcome = Some((text, tool_calls, usage, incomplete, last_reasoning_content.clone()));
break 'candidate;
}
StreamOutcome::InitFailedExhausted => {
@@ -750,6 +761,8 @@ pub(crate) async fn run_agentic_loop(
if !full_text.is_empty() {
let mut msg = ChatMessage::assistant(&full_text);
msg.model = Some(resolved_model.clone());
// BUG-260617-12: MidStream 保文也回填 reasoning_content
msg.reasoning_content = round_reasoning_content.clone();
session.messages.push(msg);
// 追加系统提示消息:响应因网络中断不完整(对齐决策 a1 系统提示机制)
let mut notice = ChatMessage::system("⚠ 响应因网络中断不完整,以上为已接收的部分内容。可重新发送以获取完整回复。");
@@ -807,10 +820,13 @@ pub(crate) async fn run_agentic_loop(
.collect();
let mut msg = ChatMessage::assistant_with_tools(&full_text, ai_tool_calls);
msg.model = Some(resolved_model.clone());
// BUG-260617-12: 回填 reasoning_content 供下一轮请求透传
msg.reasoning_content = last_reasoning_content.clone();
session.messages.push(msg);
} else if !full_text.is_empty() {
let mut msg = ChatMessage::assistant(&full_text);
msg.model = Some(resolved_model.clone());
msg.reasoning_content = last_reasoning_content.clone();
session.messages.push(msg);
}
}

View File

@@ -77,6 +77,7 @@ pub(crate) async fn compress_via_llm(
stream: false,
tools: None,
tool_choice: None,
reasoning_content: None,
};
// LLM 并发限流(压缩属独立调用,纳入双层 Semaphore)

View File

@@ -357,6 +357,7 @@ async fn extract_knowledge_from_conversation(
stream: false,
tools: None,
tool_choice: None,
reasoning_content: None,
};
let provider: Box<dyn LlmProvider> = match super::secret::build_provider_for(provider_config) {

View File

@@ -101,6 +101,8 @@ pub(crate) enum StreamResult {
text: String,
tool_calls: HashMap<u32, ToolCallDraft>,
usage: df_ai::provider::TokenUsage,
/// DeepSeek thinking 模式推理内容(需回传到下一轮请求)
reasoning_content: Option<String>,
},
/// 流中途断MidStream chunk Err / idle timeout / provider stream-error / 有 partial_text
/// 但流尽未收到 finished。已 emit 过 AiTextDelta前端 currentText 已累积),
@@ -110,6 +112,8 @@ pub(crate) enum StreamResult {
text: String,
tool_calls: HashMap<u32, ToolCallDraft>,
usage: df_ai::provider::TokenUsage,
/// DeepSeek thinking 模式推理内容(部分累积)
reasoning_content: Option<String>,
},
/// Init 失败provider.stream() Err建连/鉴权/HTTP non-2xx。已 emit AiError。
/// `retryable`: 据状态码分类(retry::is_status_retryable 镜像)true=5xx/429/timeout/connect
@@ -154,6 +158,8 @@ pub(crate) async fn stream_llm(
let mut finished_received = false;
let mut stopped = false;
let mut final_usage: Option<df_ai::provider::TokenUsage> = None;
// BUG-260617-12: DeepSeek thinking 模式推理内容累积(多轮需回传)
let mut reasoning_content_acc: Option<String> = None;
// B-260615-15:heartbeat interval 提至 loop 外复用,避免每轮重建计时器
// (每轮重建会丢已积累的节拍,且 interval 首次 tick 立即返回的特性会被误用)。
@@ -208,6 +214,7 @@ pub(crate) async fn stream_llm(
text: full_text,
tool_calls: tool_calls_acc,
usage: final_usage.unwrap_or_default(),
reasoning_content: reasoning_content_acc,
};
}
Ok(None) => break, // 流正常结束
@@ -231,6 +238,10 @@ pub(crate) async fn stream_llm(
if let Some(u) = &chunk.usage {
final_usage = Some(u.clone());
}
// BUG-260617-12: DeepSeek thinking 模式推理内容累积
if let Some(ref rc) = chunk.reasoning_content {
reasoning_content_acc.get_or_insert_with(String::new).push_str(rc);
}
// provider 流式错误事件Anthropic SSE `type=="error"` 等):
// UX-2025-04 / CR-30-2 / 决策 a1: MidStream 类失败——已有文本则保文(Partial),
// 不重试。空文本无文可保,emit AiError + InitFailed(保守 retryable=true,
@@ -260,6 +271,7 @@ pub(crate) async fn stream_llm(
text: full_text,
tool_calls: tool_calls_acc,
usage: final_usage.unwrap_or_default(),
reasoning_content: reasoning_content_acc,
};
}
if chunk.finished {
@@ -301,6 +313,7 @@ pub(crate) async fn stream_llm(
text: full_text,
tool_calls: tool_calls_acc,
usage: final_usage.unwrap_or_default(),
reasoning_content: reasoning_content_acc,
};
}
}
@@ -333,6 +346,7 @@ pub(crate) async fn stream_llm(
text: full_text,
tool_calls: tool_calls_acc,
usage: final_usage.unwrap_or_default(),
reasoning_content: reasoning_content_acc,
};
}
@@ -358,6 +372,7 @@ pub(crate) async fn stream_llm(
text: full_text,
tool_calls: tool_calls_acc,
usage: final_usage.unwrap_or_default(),
reasoning_content: reasoning_content_acc,
};
}
@@ -366,6 +381,7 @@ pub(crate) async fn stream_llm(
text: full_text,
tool_calls: tool_calls_acc,
usage: final_usage.unwrap_or_default(),
reasoning_content: reasoning_content_acc,
}
}
Err(e) => {

View File

@@ -55,6 +55,7 @@ pub(crate) async fn ensure_conversation_title(
tool_calls: None,
model: None,
status: None,
reasoning_content: m.reasoning_content.clone(),
})
.collect();
(summary, session.messages.all_messages_clone())
@@ -134,6 +135,7 @@ async fn generate_title_via_llm(
stream: false,
tools: None,
tool_choice: None,
reasoning_content: None,
};
// LLM 并发限流(标题生成属独立调用,纳入双层 Semaphore
let _global_permit = llm_concurrency.acquire_global().await;