From 5eec77ae10b77d9808b940eb89b250e17f148b2e Mon Sep 17 00:00:00 2001 From: oudecheng <13802883547@139.com> Date: Wed, 5 Aug 2026 08:10:36 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=87=8F=E5=B0=91=E7=83=AD=E8=B7=AF?= =?UTF-8?q?=E5=BE=84=E6=97=A0=E8=B0=93=E6=8B=B7=E8=B4=9D=E4=B8=8E=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E6=AD=A3=E5=88=99=E7=BC=96=E8=AF=91=EF=BC=8C=E5=B9=B6?= =?UTF-8?q?=E8=B7=B3=E8=BF=87=E5=B7=B2=E5=AE=8C=E6=88=90=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - agent_loop::filter_images_by_age_and_count 改返回 Cow<[ChatMessage]>, 无图片或无需过滤时零拷贝返回原切片引用,避免每次 LLM 调用都完整克隆消息列表 - anthropic::convert_image_url_to_anthropic 用 OnceLock 缓存 data URL 正则, 避免每次图片转换都重新编译正则 - memory_scope_key 迁移用 PRAGMA user_version 记录已迁移版本, 避免每次启动都执行全表 DELETE + UPDATE --- src/agent/agent_loop.rs | 21 ++++++++++++--------- src/providers/anthropic.rs | 10 ++++++---- src/storage/migrations.rs | 20 ++++++++++++++++++++ 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/src/agent/agent_loop.rs b/src/agent/agent_loop.rs index e137614..1dfffeb 100644 --- a/src/agent/agent_loop.rs +++ b/src/agent/agent_loop.rs @@ -13,6 +13,7 @@ use crate::storage::ConversationRepository; use crate::text::{char_count, take_prefix_chars, take_suffix_chars}; use crate::tools::{ToolContext, ToolRegistry}; use async_trait::async_trait; +use std::borrow::Cow; use std::collections::VecDeque; use std::hash::{Hash, Hasher}; use std::io::Read; @@ -226,19 +227,20 @@ fn count_supported_image_media_refs(messages: &[ChatMessage]) -> usize { /// - `max_images`: 最多发送多少张图片(优先保留最近的图片) /// /// # 返回 -/// 过滤后的消息列表,图片被转换为文本提示 "[图片已过期]" +/// 过滤后的消息列表,图片被转换为文本提示 "[图片已过期]"。 +/// 无图片或无需过滤时返回 `Cow::Borrowed`(零拷贝),有图片需过滤时返回 `Cow::Owned`。 fn filter_images_by_age_and_count( messages: &[ChatMessage], max_age_rounds: usize, max_images: usize, -) -> Vec { +) -> Cow<'_, [ChatMessage]> { if messages.is_empty() || (max_age_rounds == 0 && max_images == 0) { - return messages.to_vec(); + return Cow::Borrowed(messages); } let total_images = count_supported_image_media_refs(messages); if total_images == 0 { - return messages.to_vec(); + return Cow::Borrowed(messages); } // 从最新消息向前遍历,优先保留最新的图片 @@ -354,7 +356,7 @@ fn filter_images_by_age_and_count( }); } - filtered + Cow::Owned(filtered) } fn target_image_bytes_for_tokens(target_tokens: usize) -> usize { @@ -1332,7 +1334,8 @@ impl AgentLoop { self.runtime_config.max_image_age_rounds, self.runtime_config.max_images_in_context, ); - let image_count = count_supported_image_media_refs(&filtered_messages); + let filtered_messages_ref: &[ChatMessage] = &filtered_messages; + let image_count = count_supported_image_media_refs(filtered_messages_ref); let system_prompt = system_prompt_context.and_then(|ctx| { self.system_prompt_provider @@ -1342,19 +1345,19 @@ impl AgentLoop { // 轻量 token 估算:直接从 ChatMessage 累加,无需构造中间 Vec let text_tokens = estimate_text_tokens_from_chat_messages( - &filtered_messages, + filtered_messages_ref, system_prompt.as_ref().map(|p| p.content.as_str()), ); let image_tokens = image_token_budget_for_request(&self.runtime_config, text_tokens, tools_tokens); let mut image_budget = ImageInlineBudget::new(image_tokens, image_count); - let mut messages_for_llm: Vec = Vec::with_capacity(filtered_messages.len() + 2); + let mut messages_for_llm: Vec = Vec::with_capacity(filtered_messages_ref.len() + 2); if let Some(ref prompt) = system_prompt { messages_for_llm.push(Message::system(prompt.content.clone())); } messages_for_llm.extend( - filtered_messages + filtered_messages_ref .iter() .map(|message| chat_message_to_llm_message(message, &mut image_budget)), ); diff --git a/src/providers/anthropic.rs b/src/providers/anthropic.rs index cf3778f..57b096b 100644 --- a/src/providers/anthropic.rs +++ b/src/providers/anthropic.rs @@ -2,6 +2,7 @@ use async_trait::async_trait; use reqwest::Client; use serde::{Deserialize, Serialize}; use std::collections::HashMap; +use std::sync::OnceLock; use std::time::Duration; use super::traits::Usage; @@ -104,10 +105,11 @@ fn convert_content_blocks( fn convert_image_url_to_anthropic(url: &str) -> serde_json::Value { // data:image/png;base64,... -> Anthropic image block - if let Some(caps) = regex::Regex::new(r"data:(image/\w+);base64,(.+)") - .ok() - .and_then(|re| re.captures(url)) - { + static RE: OnceLock = OnceLock::new(); + let re = RE.get_or_init(|| { + regex::Regex::new(r"data:(image/\w+);base64,(.+)").expect("valid regex") + }); + if let Some(caps) = re.captures(url) { let media_type = caps.get(1).map(|m| m.as_str()).unwrap_or("image/png"); let data = caps.get(2).map(|d| d.as_str()).unwrap_or(""); return serde_json::json!({ diff --git a/src/storage/migrations.rs b/src/storage/migrations.rs index 8d5f2f6..3776351 100644 --- a/src/storage/migrations.rs +++ b/src/storage/migrations.rs @@ -132,6 +132,19 @@ pub(super) fn ensure_scheduler_schema(conn: &Connection) -> Result<(), StorageEr } pub(super) fn ensure_memory_scope_key_migration(conn: &Connection) -> Result<(), StorageError> { + // 用 PRAGMA user_version 追踪迁移是否已完成,避免每次启动都执行全表 DELETE + UPDATE。 + // user_version 是 SQLite 内置的 32 位整数,持久化在数据库文件头中。 + // 版本 0:未迁移;版本 1:memory_scope_key 迁移已完成。 + const MEMORY_SCOPE_KEY_MIGRATION_VERSION: i64 = 1; + + let current_version: i64 = + conn.query_row("PRAGMA user_version", [], |row| row.get(0))?; + + if current_version >= MEMORY_SCOPE_KEY_MIGRATION_VERSION { + // 已迁移过,跳过 + return Ok(()); + } + // 步骤1:去重。多条记录 scope_key 不同,改为 "default" 后会违反唯一约束。 // 对每个 (scope_kind, namespace, memory_key) 组合保留 updated_at 最新的一条。 conn.execute( @@ -156,6 +169,13 @@ pub(super) fn ensure_memory_scope_key_migration(conn: &Connection) -> Result<(), "UPDATE memories SET scope_key = 'default' WHERE scope_key != 'default'", [], )?; + + // 步骤3:记录迁移版本,后续启动直接跳过 + conn.execute( + &format!("PRAGMA user_version = {}", MEMORY_SCOPE_KEY_MIGRATION_VERSION), + [], + )?; + Ok(()) }