优化: aichat效率批(save无变化捷径/断路器尾窗/system缓存指纹/会话列表刷新收敛) + 新增 LLM prompt caching(cache_control开关默认关/system稳定段易变段分离治缓存命中率) + 销账
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
//! 系统提示词构建 + 活跃提供商获取
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::sync::{Arc, Mutex, OnceLock};
|
||||
|
||||
use df_storage::db::Database;
|
||||
use df_storage::models::AiProviderRecord;
|
||||
|
||||
use crate::commands::err_str;
|
||||
@@ -43,7 +47,24 @@ pub(crate) async fn get_active_provider(state: &AppState) -> Result<AiProviderRe
|
||||
/// 跨设备:cfg! 分支,平台各自正确姿势。补充探测见 detect_environment 工具(失败自愈)。
|
||||
fn env_profile_line() -> String {
|
||||
let today = chrono::Local::now().format("%Y-%m-%d");
|
||||
let (os, shell, interp) = if cfg!(target_os = "windows") {
|
||||
format!("当前日期: {today} | {}", env_profile_stable_body())
|
||||
}
|
||||
|
||||
/// env_profile_line 的静态稳定段(OS + shell + 执行姿势,不含日期)。
|
||||
///
|
||||
/// Anthropic prompt caching 模式把日期挪到易变段(env_profile_date_volatile,拼首条 user 消息),
|
||||
/// 本稳定段留在 system 供缓存命中——日期每日变,留 system 会每日失效整段缓存。
|
||||
fn env_profile_stable_body() -> String {
|
||||
let (os, shell, interp) = env_os_shell_interp();
|
||||
format!(
|
||||
"运行环境: {os} | shell: {shell}\n\
|
||||
执行姿势: 复杂脚本(多行/含引号/$变量)写成 .ps1 或 .py 文件,用 `powershell -File x.ps1` 或 `python x.py` 执行,避免命令行 -c 内联(引号转义易出错)。可用解释器: {interp}。\n\n"
|
||||
)
|
||||
}
|
||||
|
||||
/// 平台对应 (os, shell, 可用解释器) 三元组(cfg! 分支,见 env_profile_line 文档)。
|
||||
fn env_os_shell_interp() -> (&'static str, &'static str, &'static str) {
|
||||
if cfg!(target_os = "windows") {
|
||||
("Windows", "PowerShell", "python(无 python3)、node")
|
||||
} else if cfg!(target_os = "macos") {
|
||||
("macOS", "bash/zsh", "python3、node")
|
||||
@@ -51,11 +72,13 @@ fn env_profile_line() -> String {
|
||||
("Linux", "bash", "python3、node")
|
||||
} else {
|
||||
("Unknown", "sh", "python3、node")
|
||||
};
|
||||
format!(
|
||||
"当前日期: {today} | 运行环境: {os} | shell: {shell}\n\
|
||||
执行姿势: 复杂脚本(多行/含引号/$变量)写成 .ps1 或 .py 文件,用 `powershell -File x.ps1` 或 `python x.py` 执行,避免命令行 -c 内联(引号转义易出错)。可用解释器: {interp}。\n\n"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Anthropic prompt caching 模式:日期易变段(拼到首条 user 消息,不进 system)。
|
||||
fn env_profile_date_volatile() -> String {
|
||||
let today = chrono::Local::now().format("%Y-%m-%d");
|
||||
format!("今天是 {today}")
|
||||
}
|
||||
|
||||
/// 按语言返回系统提示词的 (固定前缀, 项目上下文标题, 任务上下文标题)
|
||||
@@ -247,6 +270,98 @@ fn app_config_query_guidance_section(lang: &str) -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
// ── BE3(AC-EFF-L0-2): system prompt 会话内缓存 ──
|
||||
//
|
||||
// 背景:同一会话连续轮次(每次 send)都全量重建 system prompt——查活跃 project/task 清单 +
|
||||
// 逐条 format 拼多 KB 字符串,对高频轮次是纯重复成本。
|
||||
//
|
||||
// 方案:按「数据指纹」缓存已拼好的 system 串(指纹 = 日期 + lang + custom_prompt + 活跃
|
||||
// project/task 的 id+updated_at + 去重排除集)。任何依赖数据变更 → 指纹变 → 重建;未变直接
|
||||
// 复用缓存串(省 2 次全行物化查询 + 字符串拼接)。
|
||||
//
|
||||
// 指纹键而非 conv_id:system prompt 内容本就与 conv_id 无关,同数据跨会话共享同一缓存串,
|
||||
// 免 per-conv 生命周期清理(换会话/删会话零维护)。模块级静态(OnceLock+Mutex),不新增
|
||||
// AppState 字段,进程内单例,懒初始化。
|
||||
//
|
||||
// 指纹查询只取轻量列(id+updated_at 两列,省 list_active 的全行物化);查询失败回退「强制重建」,
|
||||
// 正确性不受损(只是多一次重建)。缓存按指纹封顶,超上限清空防长跑进程内存膨胀。
|
||||
static SYSTEM_PROMPT_CACHE: OnceLock<Mutex<HashMap<String, String>>> = OnceLock::new();
|
||||
|
||||
/// 缓存上限(条目)。system prompt 内容按指纹去重后通常仅 1~2 条;上限防御性封顶。
|
||||
const SYSTEM_PROMPT_CACHE_MAX: usize = 32;
|
||||
|
||||
fn system_prompt_cache() -> &'static Mutex<HashMap<String, String>> {
|
||||
SYSTEM_PROMPT_CACHE.get_or_init(|| Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
/// 活跃实体(project/task)数据指纹:轻量查 id+updated_at 两列,拼成指纹串。
|
||||
/// 返回 None = 查询失败(调用方按「已变更」处理强制重建,正确性不受损)。
|
||||
async fn entity_fingerprint(db: &Arc<Database>, sql: &'static str) -> Option<String> {
|
||||
let conn = db.conn().clone();
|
||||
let result = tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut stmt = guard.prepare(sql).ok()?;
|
||||
let rows = stmt
|
||||
.query_map([], |row| {
|
||||
let id: String = row.get(0)?;
|
||||
let updated: String = row.get(1)?;
|
||||
Ok(format!("{}@{};", id, updated))
|
||||
})
|
||||
.ok()?;
|
||||
let mut out = String::new();
|
||||
for r in rows {
|
||||
out.push_str(&r.ok()?);
|
||||
}
|
||||
Some(out)
|
||||
})
|
||||
.await;
|
||||
result.ok().flatten()
|
||||
}
|
||||
|
||||
/// 计算 system prompt 缓存指纹(返回 None 表示指纹依赖查询失败,调用方按 miss 处理强制重建)。
|
||||
///
|
||||
/// 指纹覆盖全部依赖输入:
|
||||
/// - 日期:env_profile_line 注入当日日期,跨天必须失效(否则缓存串日期过期)。
|
||||
/// - lang:system_prompt_parts / 各策略段 / 注明语随 lang 分支。
|
||||
/// - custom_prompt:与 build 内读的同一设置,变更即失效。
|
||||
/// - 活跃 project/task 的 id+updated_at:新增/软删/改 updated_at 均反映到指纹(比 MAX 更稳)。
|
||||
/// - 去重排除集:被@实体走 augmentation 精准投影,排除集变化会改变清单内容,须纳入指纹。
|
||||
async fn system_prompt_fingerprint(
|
||||
state: &AppState,
|
||||
lang: &str,
|
||||
excl_proj: &[String],
|
||||
excl_task: &[String],
|
||||
) -> Option<String> {
|
||||
let date = chrono::Local::now().format("%Y-%m-%d").to_string();
|
||||
let custom = state
|
||||
.settings
|
||||
.get("custom_prompt")
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
let proj_fp = entity_fingerprint(
|
||||
&state.db,
|
||||
"SELECT id, updated_at FROM projects WHERE deleted_at IS NULL",
|
||||
)
|
||||
.await?;
|
||||
let task_fp = entity_fingerprint(
|
||||
&state.db,
|
||||
"SELECT id, updated_at FROM tasks WHERE deleted_at IS NULL",
|
||||
)
|
||||
.await?;
|
||||
let mut key = format!("{date}|{lang}|{custom}|{proj_fp}|{task_fp}");
|
||||
for id in excl_proj {
|
||||
key.push_str("|p:");
|
||||
key.push_str(id);
|
||||
}
|
||||
for id in excl_task {
|
||||
key.push_str("|t:");
|
||||
key.push_str(id);
|
||||
}
|
||||
Some(key)
|
||||
}
|
||||
|
||||
/// 构建系统提示词(环境信息 + 固定前缀 + 当前项目/任务**全局清单**)
|
||||
///
|
||||
/// 本函数注入"全貌"清单:最近 20 项目 + 20 任务的 id/name/status/description(无 path),
|
||||
@@ -260,6 +375,11 @@ fn app_config_query_guidance_section(lang: &str) -> &'static str {
|
||||
///
|
||||
/// 保留旧签名供 agentic/mod.rs(无 mention 场景)等零改动调用,内部委托给
|
||||
/// [`build_system_prompt_with_excluded`] 传两个空排除列表(无去重)。
|
||||
///
|
||||
/// 注:caching 拆分后主链(send/regenerate/edit/force_send/续跑)已改走
|
||||
/// [`build_system_prompt_parts`](开关关逐字等价),本 wrapper 暂零调用方——保留作预留 API
|
||||
/// (文档/文档注释多处引用,后续非缓存场景调用点回迁即用),标 allow 不盲删。
|
||||
#[allow(dead_code)]
|
||||
pub(crate) async fn build_system_prompt(state: &AppState, lang: &str) -> String {
|
||||
build_system_prompt_with_excluded(state, lang, &[], &[]).await
|
||||
}
|
||||
@@ -283,7 +403,15 @@ pub(crate) async fn build_system_prompt_with_excluded(
|
||||
excluded_project_ids: &[String],
|
||||
excluded_task_ids: &[String],
|
||||
) -> String {
|
||||
let (prefix, proj_label, task_label) = system_prompt_parts(lang);
|
||||
// BE3:先算指纹查缓存,命中直接复用已拼好的 system 串(省查库拼清单)。
|
||||
let fp_key = system_prompt_fingerprint(state, lang, excluded_project_ids, excluded_task_ids).await;
|
||||
if let Some(key) = &fp_key {
|
||||
let cache = system_prompt_cache();
|
||||
if let Some(cached) = cache.lock().unwrap_or_else(|p| p.into_inner()).get(key) {
|
||||
return cached.clone();
|
||||
}
|
||||
}
|
||||
let (prefix, _, _) = system_prompt_parts(lang);
|
||||
let mut prompt = env_profile_line();
|
||||
prompt.push_str(prefix);
|
||||
// 文档探索策略段(URL→fetch_url 嗅探→识别→提取→验证→应用,治盲目 http_request 瞎试)
|
||||
@@ -291,55 +419,142 @@ pub(crate) async fn build_system_prompt_with_excluded(
|
||||
// 查 DevFlow 自身配置引导(治 AI 查配置绕 run_command PowerShell 内联脚本引号嵌套失败)
|
||||
prompt.push_str(app_config_query_guidance_section(lang));
|
||||
|
||||
// 当前数据上下文(项目/任务全局清单)+ 自定义提示词 —— 拆出共用 helper,行为逐字等价。
|
||||
prompt.push_str(&build_context_lists(state, excluded_project_ids, excluded_task_ids, lang).await);
|
||||
prompt.push_str(&build_custom_section(state).await);
|
||||
|
||||
// BE3:未命中 → 存入缓存供下轮复用(按指纹封顶,超上限清空防长跑进程内存膨胀)。
|
||||
if let Some(key) = fp_key {
|
||||
let mut cache = system_prompt_cache().lock().unwrap_or_else(|p| p.into_inner());
|
||||
if cache.len() >= SYSTEM_PROMPT_CACHE_MAX {
|
||||
cache.clear();
|
||||
}
|
||||
cache.insert(key, prompt.clone());
|
||||
}
|
||||
prompt
|
||||
}
|
||||
|
||||
/// 项目/任务全局清单段(DRY:主构建 [`build_system_prompt_with_excluded`] 与 caching 易变段
|
||||
/// [`build_system_prompt_parts`] 共用)。
|
||||
///
|
||||
/// 拼接项目段 + 任务段(含机制层注明语);无项目/任务返回空串。
|
||||
/// 语义与旧 build_system_prompt_with_excluded 内联块逐字一致(纯提取,零行为变更)。
|
||||
async fn build_context_lists(
|
||||
state: &AppState,
|
||||
excluded_project_ids: &[String],
|
||||
excluded_task_ids: &[String],
|
||||
lang: &str,
|
||||
) -> String {
|
||||
let (_, proj_label, task_label) = system_prompt_parts(lang);
|
||||
let mut out = String::new();
|
||||
// 附加当前数据上下文
|
||||
if let Ok(projects) = state.projects.list_active().await {
|
||||
if !projects.is_empty() {
|
||||
prompt.push_str(proj_label);
|
||||
out.push_str(proj_label);
|
||||
// system prompt 前缀克制:仅最近 20 个项目,防 context 膨胀
|
||||
// 去重:被 @ 的项目跳过(已有 augmentation 精准投影,清单再现致同一实体两次入 prompt)
|
||||
for p in projects.iter().take(20) {
|
||||
if excluded_project_ids.iter().any(|id| id == &p.id) {
|
||||
continue;
|
||||
}
|
||||
prompt.push_str(&format!("- {} (id: {}) ({}): {}\n", p.name, p.id, p.status.as_str(), p.description));
|
||||
out.push_str(&format!("- {} (id: {}) ({}): {}\n", p.name, p.id, p.status.as_str(), p.description));
|
||||
if let Some(ref dir) = p.path {
|
||||
prompt.push_str(&format!(" 目录: {}\n", dir));
|
||||
out.push_str(&format!(" 目录: {}\n", dir));
|
||||
}
|
||||
}
|
||||
// 机制层注明语(中/英):项目已全部列出,降 list_projects 重复调用
|
||||
prompt.push_str(&projects_listed_note(lang, 20));
|
||||
out.push_str(&projects_listed_note(lang, 20));
|
||||
}
|
||||
}
|
||||
// 任务全貌清单(供 LLM 知道存在哪些任务;被@任务的精准投影走 augmentation 层)
|
||||
// 仅最近 20 条未删除任务,按 created_at DESC(同 list_active 顺序)。
|
||||
if let Ok(tasks) = state.tasks.list_active().await {
|
||||
if !tasks.is_empty() {
|
||||
prompt.push_str(task_label);
|
||||
out.push_str(task_label);
|
||||
// 去重:被 @ 的任务跳过(同项目去重机理)
|
||||
for tk in tasks.iter().take(20) {
|
||||
if excluded_task_ids.iter().any(|id| id == &tk.id) {
|
||||
continue;
|
||||
}
|
||||
prompt.push_str(&format!("- {} (id: {}) ({}): {}\n", tk.title, tk.id, tk.status.as_str(), tk.description));
|
||||
out.push_str(&format!("- {} (id: {}) ({}): {}\n", tk.title, tk.id, tk.status.as_str(), tk.description));
|
||||
}
|
||||
// 机制层注明语(中/英):仅最近 20 条,全量/按项目查询走 list_tasks
|
||||
prompt.push_str(&tasks_listed_note(lang));
|
||||
out.push_str(&tasks_listed_note(lang));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
// 自定义提示词(设置中配置,追加到系统 prompt 末尾)
|
||||
/// 自定义提示词段(设置 custom_prompt,追加到 system prompt 末尾)。无/空返回空串。
|
||||
async fn build_custom_section(state: &AppState) -> String {
|
||||
if let Ok(Some(custom)) = state.settings.get("custom_prompt").await {
|
||||
if !custom.is_empty() {
|
||||
let clean = custom.trim().trim_matches('"');
|
||||
if !clean.is_empty() {
|
||||
prompt.push_str("\n## 自定义指令\n");
|
||||
prompt.push_str(clean);
|
||||
prompt.push('\n');
|
||||
return format!("\n## 自定义指令\n{}\n", clean);
|
||||
}
|
||||
}
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
prompt
|
||||
/// 读取 Anthropic prompt caching 开关(AppState settings KV,默认关)并同步 df-ai 进程级开关。
|
||||
///
|
||||
/// 值语义:非 "true"/"1"(含缺省/空)一律视为关——默认关、用户对支持的 anthropic 端点显式开启。
|
||||
/// 同步:anthropic_compat::convert_request 读取同一进程级开关决定 system 形态
|
||||
/// (数组+cache_control / 纯字符串),保证开关关时非官方网关零风险。
|
||||
pub(crate) async fn cache_enabled(state: &AppState) -> bool {
|
||||
let v = state
|
||||
.settings
|
||||
.get("df-ai-anthropic-cache")
|
||||
.await
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or_default();
|
||||
let enabled = v == "true" || v == "1";
|
||||
df_ai::anthropic_compat::set_anthropic_cache_enabled(enabled);
|
||||
enabled
|
||||
}
|
||||
|
||||
/// 构建 system prompt(Anthropic prompt caching 模式)。
|
||||
///
|
||||
/// 返回 `(system 稳定段, 易变尾段)`:
|
||||
/// - 开关关(`cache_enabled=false`)→ `(完整 system, 空串)`:与 [`build_system_prompt_with_excluded`]
|
||||
/// 逐字等价(零行为变更,兼容非官方网关)。
|
||||
/// - 开关开 → `(静态稳定段, 易变段)`:日期/项目/任务清单从 system 挪出,由上层把易变段
|
||||
/// **追加到消息流末尾(user 消息,不进 system → 不失效 system 缓存)**。放消息流末尾而非
|
||||
/// 首条 user:任何 system 之后的消息级缓存断点都在易变段之前,每日/每请求变化的易变内容
|
||||
/// 不会失效已缓存前缀(跨天缓存仍命中)。
|
||||
///
|
||||
/// 注意:调用方须先按「开关开 AND 当前 provider 为 anthropic 类型」gate 再传 `cache_enabled`,
|
||||
/// 保证 OpenAI 路径零影响(开关开也不改动 openai 请求结构)。
|
||||
pub(crate) async fn build_system_prompt_parts(
|
||||
state: &AppState,
|
||||
lang: &str,
|
||||
excluded_project_ids: &[String],
|
||||
excluded_task_ids: &[String],
|
||||
cache_enabled: bool,
|
||||
) -> (String, String) {
|
||||
if !cache_enabled {
|
||||
let full = build_system_prompt_with_excluded(state, lang, excluded_project_ids, excluded_task_ids).await;
|
||||
return (full, String::new());
|
||||
}
|
||||
// 开关开:稳定段留 system(OS/shell 执行姿势 + 语言前缀 + 文档探索 + 查配置引导 + 自定义),
|
||||
// 易变段(日期 + 项目/任务清单)挪到消息流末尾,不失效 system 缓存。
|
||||
let (prefix, _, _) = system_prompt_parts(lang);
|
||||
let mut stable = env_profile_stable_body();
|
||||
stable.push_str(prefix);
|
||||
stable.push_str(doc_exploration_strategy_section(lang));
|
||||
stable.push_str(app_config_query_guidance_section(lang));
|
||||
stable.push_str(&build_custom_section(state).await);
|
||||
|
||||
let lists = build_context_lists(state, excluded_project_ids, excluded_task_ids, lang).await;
|
||||
let volatile = if lists.is_empty() {
|
||||
env_profile_date_volatile()
|
||||
} else {
|
||||
format!("{}\n\n{}", env_profile_date_volatile(), lists)
|
||||
};
|
||||
(stable, volatile)
|
||||
}
|
||||
|
||||
/// 项目清单尾部注明语(中/英)。
|
||||
|
||||
Reference in New Issue
Block a user