perf: 减少热路径无谓拷贝与重复正则编译,并跳过已完成迁移

- 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
This commit is contained in:
oudecheng 2026-08-05 08:10:36 +08:00
parent adada2652c
commit 5eec77ae10
3 changed files with 38 additions and 13 deletions

View File

@ -13,6 +13,7 @@ use crate::storage::ConversationRepository;
use crate::text::{char_count, take_prefix_chars, take_suffix_chars}; use crate::text::{char_count, take_prefix_chars, take_suffix_chars};
use crate::tools::{ToolContext, ToolRegistry}; use crate::tools::{ToolContext, ToolRegistry};
use async_trait::async_trait; use async_trait::async_trait;
use std::borrow::Cow;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
use std::io::Read; use std::io::Read;
@ -226,19 +227,20 @@ fn count_supported_image_media_refs(messages: &[ChatMessage]) -> usize {
/// - `max_images`: 最多发送多少张图片(优先保留最近的图片) /// - `max_images`: 最多发送多少张图片(优先保留最近的图片)
/// ///
/// # 返回 /// # 返回
/// 过滤后的消息列表,图片被转换为文本提示 "[图片已过期]" /// 过滤后的消息列表,图片被转换为文本提示 "[图片已过期]"。
/// 无图片或无需过滤时返回 `Cow::Borrowed`(零拷贝),有图片需过滤时返回 `Cow::Owned`。
fn filter_images_by_age_and_count( fn filter_images_by_age_and_count(
messages: &[ChatMessage], messages: &[ChatMessage],
max_age_rounds: usize, max_age_rounds: usize,
max_images: usize, max_images: usize,
) -> Vec<ChatMessage> { ) -> Cow<'_, [ChatMessage]> {
if messages.is_empty() || (max_age_rounds == 0 && max_images == 0) { 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); let total_images = count_supported_image_media_refs(messages);
if total_images == 0 { 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 { 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_image_age_rounds,
self.runtime_config.max_images_in_context, 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| { let system_prompt = system_prompt_context.and_then(|ctx| {
self.system_prompt_provider self.system_prompt_provider
@ -1342,19 +1345,19 @@ impl AgentLoop {
// 轻量 token 估算:直接从 ChatMessage 累加,无需构造中间 Vec<Message> // 轻量 token 估算:直接从 ChatMessage 累加,无需构造中间 Vec<Message>
let text_tokens = estimate_text_tokens_from_chat_messages( let text_tokens = estimate_text_tokens_from_chat_messages(
&filtered_messages, filtered_messages_ref,
system_prompt.as_ref().map(|p| p.content.as_str()), system_prompt.as_ref().map(|p| p.content.as_str()),
); );
let image_tokens = let image_tokens =
image_token_budget_for_request(&self.runtime_config, text_tokens, tools_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 image_budget = ImageInlineBudget::new(image_tokens, image_count);
let mut messages_for_llm: Vec<Message> = Vec::with_capacity(filtered_messages.len() + 2); let mut messages_for_llm: Vec<Message> = Vec::with_capacity(filtered_messages_ref.len() + 2);
if let Some(ref prompt) = system_prompt { if let Some(ref prompt) = system_prompt {
messages_for_llm.push(Message::system(prompt.content.clone())); messages_for_llm.push(Message::system(prompt.content.clone()));
} }
messages_for_llm.extend( messages_for_llm.extend(
filtered_messages filtered_messages_ref
.iter() .iter()
.map(|message| chat_message_to_llm_message(message, &mut image_budget)), .map(|message| chat_message_to_llm_message(message, &mut image_budget)),
); );

View File

@ -2,6 +2,7 @@ use async_trait::async_trait;
use reqwest::Client; use reqwest::Client;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::OnceLock;
use std::time::Duration; use std::time::Duration;
use super::traits::Usage; use super::traits::Usage;
@ -104,10 +105,11 @@ fn convert_content_blocks(
fn convert_image_url_to_anthropic(url: &str) -> serde_json::Value { fn convert_image_url_to_anthropic(url: &str) -> serde_json::Value {
// data:image/png;base64,... -> Anthropic image block // data:image/png;base64,... -> Anthropic image block
if let Some(caps) = regex::Regex::new(r"data:(image/\w+);base64,(.+)") static RE: OnceLock<regex::Regex> = OnceLock::new();
.ok() let re = RE.get_or_init(|| {
.and_then(|re| re.captures(url)) 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 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(""); let data = caps.get(2).map(|d| d.as_str()).unwrap_or("");
return serde_json::json!({ return serde_json::json!({

View File

@ -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> { pub(super) fn ensure_memory_scope_key_migration(conn: &Connection) -> Result<(), StorageError> {
// 用 PRAGMA user_version 追踪迁移是否已完成,避免每次启动都执行全表 DELETE + UPDATE。
// user_version 是 SQLite 内置的 32 位整数,持久化在数据库文件头中。
// 版本 0未迁移版本 1memory_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" 后会违反唯一约束。 // 步骤1去重。多条记录 scope_key 不同,改为 "default" 后会违反唯一约束。
// 对每个 (scope_kind, namespace, memory_key) 组合保留 updated_at 最新的一条。 // 对每个 (scope_kind, namespace, memory_key) 组合保留 updated_at 最新的一条。
conn.execute( 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'", "UPDATE memories SET scope_key = 'default' WHERE scope_key != 'default'",
[], [],
)?; )?;
// 步骤3记录迁移版本后续启动直接跳过
conn.execute(
&format!("PRAGMA user_version = {}", MEMORY_SCOPE_KEY_MIGRATION_VERSION),
[],
)?;
Ok(()) Ok(())
} }