优化: aichat效率批(save无变化捷径/断路器尾窗/system缓存指纹/会话列表刷新收敛) + 新增 LLM prompt caching(cache_control开关默认关/system稳定段易变段分离治缓存命中率) + 销账

This commit is contained in:
lxy
2026-08-09 21:35:13 +08:00
parent 4df91ed155
commit 7eb2e25ed5
13 changed files with 698 additions and 180 deletions
+86 -4
View File
@@ -29,10 +29,38 @@ use crate::retry::{
// 协议数据结构 + SSE 纯解析(apply_anthropic_event / AnthropicRequest 等)抽至 anthropic_helpers
// 此处 use 引入以保持本模块内引用路径不变(零行为变更搬迁)。
use crate::anthropic_helpers::{
apply_anthropic_event, AnthropicRequest, AnthropicResponse, AnthropicToolDef,
apply_anthropic_event, AnthropicRequest, AnthropicResponse, AnthropicToolDef, SystemBlock,
ANTHROPIC_VERSION, DEFAULT_MAX_TOKENS,
};
// ============================================================
// Anthropic prompt caching 开关(进程级全局,受 AppState settings 同步)
// ============================================================
/// Anthropic prompt caching 总开关(默认关)。
///
/// 该分支同时服务 Claude 官方 / GLM 订阅端点 (open.bigmodel.cn/api/anthropic) / 任意 Messages
/// API 网关——**cache_control 字段可能被非官方网关拒收**,故必须默认关、用户对支持的端点显式开启。
///
/// 开关开:`convert_request` 把 system 包成 blocks 数组并附加 `cache_control:{type:ephemeral}`
/// (配合上层把易变段挪出 system,让 system 稳定段真正命中缓存)。
/// 开关关:system 保持纯字符串(现状行为,零风险兼容网关)。
///
/// 进程级全局而非 per-provider:上层(AppState settings)按「用户对当前启用的 anthropic 端点
/// 是否支持缓存」一次性开启,所有 anthropic 请求一致生效。默认关。
static ANTHROPIC_CACHE_ENABLED: std::sync::atomic::AtomicBool =
std::sync::atomic::AtomicBool::new(false);
/// 设置 anthropic prompt caching 开关(上层读取 AppState settings 后同步)。
pub fn set_anthropic_cache_enabled(enabled: bool) {
ANTHROPIC_CACHE_ENABLED.store(enabled, std::sync::atomic::Ordering::SeqCst);
}
/// 读取 anthropic prompt caching 开关(`convert_request` 组装 system 形态用)。
pub fn anthropic_cache_enabled() -> bool {
ANTHROPIC_CACHE_ENABLED.load(std::sync::atomic::Ordering::SeqCst)
}
// ============================================================
// Provider 实现
// ============================================================
@@ -92,8 +120,10 @@ impl AnthropicCompatProvider {
req.model
};
// 抽离 system 消息
let system: Option<String> = {
// 抽离 system 消息
// 开关开 → blocks 数组 + cache_control(Anthropic prompt caching,让 system 稳定段在
// 支持缓存的端点上命中);开关关 → 纯字符串(兼容非官方网关,零风险)。
let system: Option<SystemBlock> = {
let sys: Vec<String> = req
.messages
.iter()
@@ -102,8 +132,14 @@ impl AnthropicCompatProvider {
.collect();
if sys.is_empty() {
None
} else if anthropic_cache_enabled() {
Some(SystemBlock::Cached(vec![serde_json::json!({
"type": "text",
"text": sys.join("\n\n"),
"cache_control": { "type": "ephemeral" },
})]))
} else {
Some(sys.join("\n\n"))
Some(SystemBlock::Plain(sys.join("\n\n")))
}
};
@@ -1126,4 +1162,50 @@ mod tests {
"补占位后 precheck 应通过"
);
}
// ---------- prompt caching:开关驱动 system 形态 ----------
/// 开关控制 system 形态:开 → blocks 数组含 cache_control:ephemeral;关 → 纯字符串。
/// (两态共用进程级全局开关,拆两个用例并行跑会互相污染——单测内顺序切换断言,末尾复位。)
#[test]
fn anthropic_cache_switch_controls_system_shape() {
set_anthropic_cache_enabled(false); // 兜底复位,防污染其他并行用例
let provider = AnthropicCompatProvider::new("https://api.anthropic.com", "k", "claude-3-5-sonnet");
let mk = || CompletionRequest {
model: "claude-3-5-sonnet".into(),
messages: vec![
ChatMessage::system("a"),
ChatMessage::system("b"),
ChatMessage::user("hi"),
],
temperature: None,
max_tokens: None,
stream: false,
tools: None,
tool_choice: None,
reasoning_content: None,
};
// 开:数组形态 + cache_control(Anthropic prompt caching)
set_anthropic_cache_enabled(true);
let body = provider.convert_request(mk());
match body.system.expect("开关开时应有 system") {
SystemBlock::Cached(blocks) => {
assert_eq!(blocks.len(), 1, "开关开应包成单 blocks 数组");
assert_eq!(blocks[0]["type"], "text");
assert_eq!(blocks[0]["text"], "a\n\nb", "多条 system 应以 \\n\\n join 进单个 text 块");
assert_eq!(blocks[0]["cache_control"]["type"], "ephemeral");
}
SystemBlock::Plain(s) => panic!("开关开应为数组形态,实际 Plain: {}", s),
}
// 关:纯字符串(兼容非官方网关,零行为变更)
set_anthropic_cache_enabled(false);
let body = provider.convert_request(mk());
match body.system {
Some(SystemBlock::Plain(s)) => assert_eq!(s, "a\n\nb", "开关关多条 system 应以 \\n\\n join 纯字符串"),
other => panic!("开关关应为纯字符串,实际: {:?}", other),
}
}
}
+15 -1
View File
@@ -18,6 +18,20 @@ use crate::provider::{tool_call_id_or_fallback, StreamChunk, TokenUsage, ToolCal
// Anthropic API 请求/响应结构体
// ============================================================
/// Anthropic `system` 字段形态(受 prompt caching 开关 `ANTHROPIC_CACHE_ENABLED` 控制):
/// - `Plain`:纯字符串。开关关时保持原形态,兼容非官方网关(GLM 订阅端点 / 任意 Messages API
/// 代理——部分网关会拒收 cache_control 字段)。
/// - `Cached`:blocks 数组 `[{type:text, text, cache_control:{type:ephemeral}}]`。开关开时启用
/// Anthropic prompt caching,让 system 稳定段在支持缓存的端点上命中。
#[derive(Debug, Clone, Serialize)]
#[serde(untagged)]
pub(crate) enum SystemBlock {
/// 纯字符串形态(兼容网关,开关关)
Plain(String),
/// blocks 数组形态(含 cache_control,开关开)
Cached(Vec<serde_json::Value>),
}
/// Anthropic 请求体
#[derive(Debug, Clone, Serialize)]
pub(crate) struct AnthropicRequest {
@@ -25,7 +39,7 @@ pub(crate) struct AnthropicRequest {
pub messages: Vec<serde_json::Value>,
pub max_tokens: u32,
#[serde(skip_serializing_if = "Option::is_none")]
pub system: Option<String>,
pub system: Option<SystemBlock>,
#[serde(skip_serializing_if = "Option::is_none")]
pub temperature: Option<f32>,
pub stream: bool,
+11
View File
@@ -275,6 +275,17 @@ impl ContextManager {
self.messages.iter().map(|t| t.message.clone()).collect()
}
/// 只读最近 N 条消息(尾部切片 clone)。BE2(AC-EFF-R2-1):断路器/探索熔断检查等只关心
/// 最近消息的读路径,用本方法避免 all_messages_clone 每轮全量 clone(长对话每轮 O(n) 深克隆浪费)。
/// 尾部顺序保持时间正序(与 all_messages_clone 一致,调用方从尾部反向扫即可)。
pub fn recent_messages(&self, n: usize) -> Vec<ChatMessage> {
let skip = self.messages.len().saturating_sub(n);
self.messages[skip..]
.iter()
.map(|t| t.message.clone())
.collect()
}
/// 从 Vec 恢复(兼容从 DB 加载)
pub fn restore_from_messages(&mut self, messages: Vec<ChatMessage>) {
// clear() 会置 needs_full_rewrite=true + persisted_msg_count=0;但本入口是"DB 刚加载",