修复: DeepSeek reasoning_content 全链路透传(跨6 crate补字段+映射+agentic loop累积回传)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,6 +77,7 @@ pub(crate) async fn compress_via_llm(
|
||||
stream: false,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
reasoning_content: None,
|
||||
};
|
||||
|
||||
// LLM 并发限流(压缩属独立调用,纳入双层 Semaphore)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user