优化: df-ai/df-storage/df-nodes 底层加固: 上下文/模型/节点/仓储
df-ai: - context/mod.rs + manager_tests.rs: 上下文管理调整 + 测试同步(48 行) - openai_compat.rs: OpenAI 兼容层增强(+51) - presets/models.json: 模型预设 +18 df-storage: - crud/conversation_repo.rs: 会话仓储(loop_state 相关) - crud/task_repo.rs: 任务仓储 - crud/mod.rs: 导出调整 df-nodes: - task_advance_node.rs: 任务推进节点 验收: cargo check -p devflow EXIT 0。
This commit is contained in:
@@ -1045,9 +1045,12 @@ fn phase1_append_after_dirty_stays_dirty_until_save() {
|
||||
// ============================================================
|
||||
|
||||
#[test]
|
||||
fn eviction_inserts_summary_message_at_view_head() {
|
||||
// 触发淘汰 → 视图头部应有一条 "[早期对话概要]" user 摘要(补偿无声丢弃的早期主题),
|
||||
fn eviction_inserts_summary_message_at_view_tail() {
|
||||
// 触发淘汰 → 视图尾部应有一条 "[早期对话概要]" user 摘要(补偿无声丢弃的早期主题),
|
||||
// 概要 ≤ EVICTION_SUMMARY_MAX_CHARS,保护消息完好,且纯视图(不污染内存全量/不落库)。
|
||||
// 缓存友好(2026-10):摘要改尾部追加(push)而非头部插入(insert(0))——头部插入位移
|
||||
// 整条前缀字节,使 DeepSeek 等自动前缀缓存 provider 全部缓存失效(命中率掉到个位数)。
|
||||
// 尾部追加让稳定前缀(system + 早期 history)逐轮不变,命中率最大化。
|
||||
let mut mgr = ContextManager::new(cfg(200));
|
||||
for i in 0..20 {
|
||||
if i % 2 == 0 {
|
||||
@@ -1063,46 +1066,47 @@ fn eviction_inserts_summary_message_at_view_head() {
|
||||
let (msgs, trimmed) = mgr.build_for_request(0);
|
||||
assert!(trimmed, "超预算应触发裁剪");
|
||||
|
||||
// 视图头部 = 补偿摘要(role=user,"[早期对话概要]" 前缀 + 高频主题)
|
||||
let head = msgs.first().expect("视图不应为空");
|
||||
// 视图尾部 = 补偿摘要(role=user,"[早期对话概要]" 前缀 + 高频主题)
|
||||
let tail = msgs.last().expect("视图不应为空");
|
||||
assert!(
|
||||
matches!(head.role, MessageRole::User),
|
||||
"视图头部应为 user 摘要, 实际 {:?}",
|
||||
head.role
|
||||
matches!(tail.role, MessageRole::User),
|
||||
"视图尾部应为 user 摘要, 实际 {:?}",
|
||||
tail.role
|
||||
);
|
||||
assert!(
|
||||
head.content.starts_with("[早期对话概要] 高频主题: "),
|
||||
tail.content.starts_with("[早期对话概要] 高频主题: "),
|
||||
"摘要格式不符, 实际 {}",
|
||||
head.content
|
||||
tail.content
|
||||
);
|
||||
assert!(
|
||||
head.content.contains("审批"),
|
||||
tail.content.contains("审批"),
|
||||
"摘要应含被淘汰区高频主题词'审批', 实际 {}",
|
||||
head.content
|
||||
tail.content
|
||||
);
|
||||
assert!(
|
||||
head.content.chars().count() <= EVICTION_SUMMARY_MAX_CHARS,
|
||||
tail.content.chars().count() <= EVICTION_SUMMARY_MAX_CHARS,
|
||||
"摘要应 ≤ {} 字符, 实际 {}",
|
||||
EVICTION_SUMMARY_MAX_CHARS,
|
||||
head.content.chars().count()
|
||||
tail.content.chars().count()
|
||||
);
|
||||
|
||||
// 保护消息完好:末条(i=19 assistant)保留;摘要 user 与首轮真实 user 相邻,由
|
||||
// sanitize 的 ensure_sequence_legal 合并(摘要在前,换行接原文),故 msgs[1] 为 i=1 的 assistant
|
||||
// 保护消息完好:末条(i=19 assistant)保留;摘要 user 追加在尾部,与末尾 assistant
|
||||
// 不同 role 不合并,故 msgs[倒数第2] 仍为 i=19 的 assistant
|
||||
assert_eq!(
|
||||
msgs.last().unwrap().content,
|
||||
msgs[msgs.len() - 2].content,
|
||||
"第19条回复内容同样较长用于撑爆上下文预算触发视图裁剪逻辑",
|
||||
"保护区最新消息被误裁"
|
||||
);
|
||||
assert!(
|
||||
matches!(msgs[1].role, MessageRole::Assistant),
|
||||
"摘要与首轮 user 合并后,第二条应为 assistant, 实际 {:?}",
|
||||
msgs[1].role
|
||||
matches!(msgs[msgs.len() - 2].role, MessageRole::Assistant),
|
||||
"摘要前一条应为 assistant(i=19), 实际 {:?}",
|
||||
msgs[msgs.len() - 2].role
|
||||
);
|
||||
// 首条仍是保留区真实首条 user(摘要不再抢占头部,不位移前缀)
|
||||
assert!(
|
||||
msgs[0].content.contains("审批流程与审批门禁需要适配"),
|
||||
"合并不应丢失被保留的首轮真实 user 文本, 实际 {}",
|
||||
msgs[0].content
|
||||
matches!(msgs[0].role, MessageRole::User),
|
||||
"视图头部应为真实首条 user(摘要已移尾部), 实际 {:?}",
|
||||
msgs[0].role
|
||||
);
|
||||
|
||||
// 纯视图:摘要不进内存全量,被淘汰原文存储态不变
|
||||
|
||||
@@ -246,16 +246,18 @@ impl ContextManager {
|
||||
.collect();
|
||||
|
||||
// 淘汰补偿摘要:视图裁剪无声丢弃被淘汰内容,LLM 从此看不到早期主题,与压缩
|
||||
// 路径(有摘要补偿)行为不一致。此处把被淘汰文本提炼成一短段摘要插回发送
|
||||
// 视图最前部(淘汰区头部、保护消息之前)。角色必须 user:role=system 会被
|
||||
// anthropic_compat 折叠进带 cache_control 的顶层 system 段,而本摘要纯视图
|
||||
// 每轮即时再生成(淘汰区单调增长致内容逐轮漂移),落 system 即逐轮改写缓存
|
||||
// 指纹使全部前缀缓存失效;连续 role 由既有 sanitize/协议层自愈兜底。摘要只进
|
||||
// messages 易变区,严禁拼进 system_prompt/任何稳定段;纯视图语义:不落库、
|
||||
// 不改 conv.messages 存储、不标记 compressed,被淘汰原文存储态不变。
|
||||
// 极短/空淘汰文本提炼为空 → 跳过(不值得插)。
|
||||
// 路径(有摘要补偿)行为不一致。此处把被淘汰文本提炼成一短段摘要插回发送视图。
|
||||
// **缓存友好修正(2026-10)**:原 insert(0) 头部插入会位移整条前缀字节,使
|
||||
// DeepSeek 等自动前缀缓存 provider 的稳定前缀(顶层 system + 早期 history)
|
||||
// 全部缓存失效——命中率掉到个位数。改尾部追加(push):摘要本身每轮即时再生成
|
||||
// (淘汰区单调增长致内容逐轮漂移)本就无法命中缓存,但放尾部不再拖累稳定前缀,
|
||||
// 命中率最大化。角色必须 user:role=system 会被 anthropic_compat 折叠进带
|
||||
// cache_control 的顶层 system 段,落 system 即逐轮改写缓存指纹;user 放尾部
|
||||
// 由 sanitize/协议层自愈兜底。摘要只进 messages 易变区,严禁拼进 system_prompt/
|
||||
// 任何稳定段;纯视图语义:不落库、不改 conv.messages 存储、不标记 compressed,
|
||||
// 被淘汰原文存储态不变。极短/空淘汰文本提炼为空 → 跳过(不值得插)。
|
||||
if let Some(text) = build_eviction_summary_text(&evicted) {
|
||||
msgs.insert(0, ChatMessage::user(text));
|
||||
msgs.push(ChatMessage::user(text));
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
|
||||
@@ -211,6 +211,10 @@ impl OpenAICompatProvider {
|
||||
|
||||
// 模型不支持 temperature(如商汤 sensenova 仅接受 1.0)→ 省略该字段,避免上游 HTTP 400。
|
||||
// 判定走 model_probe(命中 presets 表;未命中默认支持,保持原样透传零回归)。
|
||||
// 商汤网关(token.sensenova.cn)下温度支持**按模型各异**:sensenova-6.x 与
|
||||
// sn2-kimi 实测仅接受 1(传其它值 400),部分新模型支持常规温度。
|
||||
// 故不做 base_url 一刀切,改由 presets 逐模型精确维护 temperature_supported:
|
||||
// 受限模型(含实测 sn2-kimi)标 false 省略;支持温度变化的模型标 true 正常透传。
|
||||
let temperature = if req.temperature.is_some()
|
||||
&& !crate::model_probe::probe(&model).temperature_supported
|
||||
{
|
||||
@@ -1018,6 +1022,53 @@ mod tests {
|
||||
assert_eq!(msg.content, serde_json::Value::String("hello".into()));
|
||||
}
|
||||
|
||||
/// 温度支持按模型精确判定(presets 表,probe 命中),不按 base_url 一刀切——
|
||||
/// 商汤网关下温度支持各异:sn2-kimi 实测仅接受 temperature=1(传 0.7 → 上游 400),
|
||||
/// presets 已标 temperature_supported=false,任何 provider 下都省略。
|
||||
/// 回归 B5 挂机 sn2-kimi 传 0.7 → 400。省略交上游默认,宁默认不 400。
|
||||
#[test]
|
||||
fn openai_temperature_omitted_for_sn2_kimi_via_presets() {
|
||||
let provider =
|
||||
OpenAICompatProvider::new("https://token.sensenova.cn", "k", "sensenova-6.8-flash-lite");
|
||||
let req = CompletionRequest {
|
||||
model: "sn2-kimi".into(), // presets 精确标记受限,probe 命中 false
|
||||
messages: vec![ChatMessage::user("hi")],
|
||||
temperature: Some(0.7),
|
||||
max_tokens: None,
|
||||
stream: true,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
reasoning_content: None,
|
||||
};
|
||||
let out = provider.convert_request(req);
|
||||
assert_eq!(out.temperature, None, "sn2-kimi presets 标 false,应省略 temperature");
|
||||
}
|
||||
|
||||
/// 反向保证:支持常规温度的模型(presets 未标 false,如 deepseek-v4-flash)
|
||||
/// 即使挂在商汤 provider 池也按模型判定透传,不因 base_url 误杀——商汤新模型
|
||||
/// 支持温度变化,一刀切会错误降级。
|
||||
#[test]
|
||||
fn openai_temperature_passed_for_supported_model_on_sensenova_base() {
|
||||
let provider =
|
||||
OpenAICompatProvider::new("https://token.sensenova.cn", "k", "sensenova-6.8-flash-lite");
|
||||
let req = CompletionRequest {
|
||||
model: "deepseek-v4-flash".into(), // presets 未标受限 → 默认支持温度
|
||||
messages: vec![ChatMessage::user("hi")],
|
||||
temperature: Some(0.7),
|
||||
max_tokens: None,
|
||||
stream: true,
|
||||
tools: None,
|
||||
tool_choice: None,
|
||||
reasoning_content: None,
|
||||
};
|
||||
let out = provider.convert_request(req);
|
||||
assert_eq!(
|
||||
out.temperature,
|
||||
Some(0.7),
|
||||
"支持温度的模型应透传,不因 base_url 是商汤而省略"
|
||||
);
|
||||
}
|
||||
|
||||
// ---------- ensure_leading_user(首条非 user/system → 补 user 占位,OpenAI 对称 Anthropic)----------
|
||||
|
||||
/// 首条 assistant → 补 user 占位(对齐 Anthropic)。上游绕过 sanitize 的
|
||||
|
||||
Reference in New Issue
Block a user