feat: 添加记忆维护的错误处理逻辑,优化传输错误的上下文信息
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
891830779f
commit
04fc2c0710
@ -238,8 +238,18 @@ impl MemoryMaintenanceService {
|
||||
let mut results = Vec::new();
|
||||
|
||||
for scope_key in scope_keys {
|
||||
let Some(output) = self.run_for_scope(&scope_key).await? else {
|
||||
let output = match self.run_for_scope(&scope_key).await {
|
||||
Ok(Some(output)) => output,
|
||||
Ok(None) => continue,
|
||||
Err(error) if is_recoverable_maintenance_scope_error(&error) => {
|
||||
tracing::warn!(
|
||||
scope_key = %scope_key,
|
||||
error = %error,
|
||||
"Memory maintenance skipped scope after recoverable model failure"
|
||||
);
|
||||
continue;
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
|
||||
results.push(MemoryMaintenanceScopeResult { scope_key, output });
|
||||
@ -319,6 +329,10 @@ pub(crate) fn is_recoverable_maintenance_llm_error(error: &str) -> bool {
|
||||
|| normalized.contains("timeout")
|
||||
}
|
||||
|
||||
fn is_recoverable_maintenance_scope_error(error: &AgentError) -> bool {
|
||||
is_recoverable_maintenance_llm_error(&error.to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn strip_json_code_fence(content: &str) -> &str {
|
||||
let trimmed = content.trim();
|
||||
if let Some(rest) = trimmed.strip_prefix("```json") {
|
||||
|
||||
@ -1059,6 +1059,67 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_summarize_memory_maintenance_transport_error_includes_provider_context() {
|
||||
let provider_config = LLMProviderConfig {
|
||||
provider_type: "openai".to_string(),
|
||||
name: "maintenance-provider".to_string(),
|
||||
base_url: "https://example.invalid/v1".to_string(),
|
||||
api_key: "test-key".to_string(),
|
||||
extra_headers: HashMap::new(),
|
||||
model_id: "maintenance-model".to_string(),
|
||||
temperature: Some(0.0),
|
||||
max_tokens: Some(256),
|
||||
context_window_tokens: None,
|
||||
model_extra: HashMap::new(),
|
||||
max_tool_iterations: 1,
|
||||
llm_timeout_secs: 1,
|
||||
tool_result_max_chars: 20_000,
|
||||
context_tool_result_trim_chars: 20_000,
|
||||
};
|
||||
|
||||
let session_manager = SessionManager::new(
|
||||
4,
|
||||
100,
|
||||
false,
|
||||
"Asia/Shanghai".to_string(),
|
||||
provider_config.clone(),
|
||||
HashMap::from([("default".to_string(), provider_config)]),
|
||||
Arc::new(SkillRuntime::default()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
session_manager
|
||||
.store()
|
||||
.put_memory(&crate::storage::MemoryUpsert {
|
||||
scope_kind: "user".to_string(),
|
||||
scope_key: "feishu:user-1".to_string(),
|
||||
namespace: "profile".to_string(),
|
||||
memory_key: "work".to_string(),
|
||||
content: "用户在做AI产品".to_string(),
|
||||
source_type: "message".to_string(),
|
||||
source_session_id: None,
|
||||
source_message_id: None,
|
||||
source_message_seq: None,
|
||||
source_channel_name: None,
|
||||
source_chat_id: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let error = session_manager
|
||||
.summarize_memory_maintenance_for_scope("feishu:user-1")
|
||||
.await
|
||||
.unwrap_err()
|
||||
.to_string();
|
||||
|
||||
assert!(error.contains("memory maintenance model error: transport error:"));
|
||||
assert!(error.contains("provider=maintenance-provider"));
|
||||
assert!(error.contains("model=maintenance-model"));
|
||||
assert!(error.contains("url=https://example.invalid/v1/chat/completions"));
|
||||
assert!(error.contains("timeout_secs=1"));
|
||||
assert!(error.contains("error sending request for url"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_summarize_memory_maintenance_retries_recoverable_provider_errors() {
|
||||
let mock_response_content = serde_json::to_string(&json!({
|
||||
@ -1249,6 +1310,63 @@ mod tests {
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_run_memory_maintenance_for_all_scopes_skips_recoverable_transport_failures() {
|
||||
let provider_config = LLMProviderConfig {
|
||||
provider_type: "openai".to_string(),
|
||||
name: "maintenance-provider".to_string(),
|
||||
base_url: "https://example.invalid/v1".to_string(),
|
||||
api_key: "test-key".to_string(),
|
||||
extra_headers: HashMap::new(),
|
||||
model_id: "maintenance-model".to_string(),
|
||||
temperature: Some(0.0),
|
||||
max_tokens: Some(256),
|
||||
context_window_tokens: None,
|
||||
model_extra: HashMap::new(),
|
||||
max_tool_iterations: 1,
|
||||
llm_timeout_secs: 1,
|
||||
tool_result_max_chars: 20_000,
|
||||
context_tool_result_trim_chars: 20_000,
|
||||
};
|
||||
|
||||
let session_manager = SessionManager::new(
|
||||
4,
|
||||
100,
|
||||
false,
|
||||
"Asia/Shanghai".to_string(),
|
||||
provider_config.clone(),
|
||||
HashMap::from([("default".to_string(), provider_config)]),
|
||||
Arc::new(SkillRuntime::default()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for scope_key in ["feishu:user-1", "feishu:user-2"] {
|
||||
session_manager
|
||||
.store()
|
||||
.put_memory(&crate::storage::MemoryUpsert {
|
||||
scope_kind: "user".to_string(),
|
||||
scope_key: scope_key.to_string(),
|
||||
namespace: "profile".to_string(),
|
||||
memory_key: "work".to_string(),
|
||||
content: format!("{} 在做AI产品", scope_key),
|
||||
source_type: "message".to_string(),
|
||||
source_session_id: None,
|
||||
source_message_id: None,
|
||||
source_message_seq: None,
|
||||
source_channel_name: None,
|
||||
source_chat_id: None,
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let results = session_manager
|
||||
.run_memory_maintenance_for_all_scopes(None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_apply_memory_maintenance_output_merges_and_deletes_low_value_records() {
|
||||
let store = SessionStore::in_memory().unwrap();
|
||||
|
||||
@ -23,6 +23,23 @@ fn format_error_chain(error: &(dyn std::error::Error + 'static)) -> String {
|
||||
details.join("\ncaused by: ")
|
||||
}
|
||||
|
||||
fn format_transport_error_context(
|
||||
provider_name: &str,
|
||||
model_id: &str,
|
||||
url: &str,
|
||||
timeout_secs: u64,
|
||||
error: &(dyn std::error::Error + 'static),
|
||||
) -> String {
|
||||
format!(
|
||||
"transport error: provider={} model={} url={} timeout_secs={} details={}",
|
||||
provider_name,
|
||||
model_id,
|
||||
url,
|
||||
timeout_secs,
|
||||
format_error_chain(error)
|
||||
)
|
||||
}
|
||||
|
||||
fn convert_content_blocks(blocks: &[ContentBlock]) -> Value {
|
||||
if blocks.len() == 1 {
|
||||
if let ContentBlock::Text { text } = &blocks[0] {
|
||||
@ -294,7 +311,25 @@ impl LLMProvider for OpenAIProvider {
|
||||
req_builder = req_builder.header(key.as_str(), value.as_str());
|
||||
}
|
||||
|
||||
let resp = req_builder.json(&body).send().await?;
|
||||
let resp = req_builder.json(&body).send().await.map_err(|err| {
|
||||
let error_context = format_transport_error_context(
|
||||
&self.name,
|
||||
&self.model_id,
|
||||
&url,
|
||||
self.llm_timeout_secs,
|
||||
&err,
|
||||
);
|
||||
tracing::error!(
|
||||
provider = %self.name,
|
||||
model = %self.model_id,
|
||||
url = %url,
|
||||
base_url = %self.base_url,
|
||||
timeout_secs = self.llm_timeout_secs,
|
||||
error = %error_context,
|
||||
"OpenAI-compatible API transport request failed"
|
||||
);
|
||||
error_context
|
||||
})?;
|
||||
|
||||
let status = resp.status();
|
||||
let text = resp.text().await?;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user