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:
parent
adada2652c
commit
5eec77ae10
@ -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<ChatMessage> {
|
||||
) -> 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<Message>
|
||||
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<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 {
|
||||
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)),
|
||||
);
|
||||
|
||||
@ -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<regex::Regex> = 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!({
|
||||
|
||||
@ -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(())
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user