fix(audit): 第五轮审计修复 — 反思LLM分析、语义路由、并行执行、错误中文化

- P2: 反思引擎接入 LLM 深度行为分析 (analyze_patterns_with_llm)
- P3-M6: 语义路由 RuntimeLlmIntentDriver 真实 LLM 匹配
- P3-L1: V2 Pipeline execute_parallel 改用 buffer_unordered 真正并行
- P3-S10: Rust 用户可见错误提示统一中文化

累计修复 27 项,完成度 ~72% → ~78%
This commit is contained in:
iven
2026-03-27 12:10:48 +08:00
parent 30b2515f07
commit 256dba49db
10 changed files with 393 additions and 84 deletions

View File

@@ -19,6 +19,10 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
// Re-export from zclaw-runtime for LLM integration
use zclaw_runtime::driver::{CompletionRequest, ContentBlock, LlmDriver};
// === Types ===
@@ -187,9 +191,33 @@ impl ReflectionEngine {
}
/// Execute reflection cycle
pub fn reflect(&mut self, agent_id: &str, memories: &[MemoryEntryForAnalysis]) -> ReflectionResult {
// 1. Analyze memory patterns
let patterns = self.analyze_patterns(memories);
pub async fn reflect(
&mut self,
agent_id: &str,
memories: &[MemoryEntryForAnalysis],
driver: Option<Arc<dyn LlmDriver>>,
) -> ReflectionResult {
// 1. Analyze memory patterns (LLM if configured, rules fallback)
let patterns = if self.config.use_llm {
if let Some(ref llm) = driver {
match self.analyze_patterns_with_llm(memories, llm).await {
Ok(p) => p,
Err(e) => {
tracing::warn!("[reflection] LLM analysis failed, falling back to rules: {}", e);
if self.config.llm_fallback_to_rules {
self.analyze_patterns(memories)
} else {
Vec::new()
}
}
}
} else {
tracing::debug!("[reflection] use_llm=true but no driver available, using rules");
self.analyze_patterns(memories)
}
} else {
self.analyze_patterns(memories)
};
// 2. Generate improvement suggestions
let improvements = self.generate_improvements(&patterns, memories);
@@ -282,7 +310,65 @@ impl ReflectionEngine {
result
}
/// Analyze patterns in memories
/// Analyze patterns using LLM for deeper behavioral insights
async fn analyze_patterns_with_llm(
&self,
memories: &[MemoryEntryForAnalysis],
driver: &Arc<dyn LlmDriver>,
) -> Result<Vec<PatternObservation>, String> {
if memories.is_empty() {
return Ok(Vec::new());
}
// Build memory summary for the prompt
let memory_summary: String = memories.iter().enumerate().map(|(i, m)| {
format!("{}. [{}] (重要性:{}, 访问:{}) {}",
i + 1, m.memory_type, m.importance, m.access_count, m.content)
}).collect::<Vec<_>>().join("\n");
let system_prompt = r#"你是行为分析专家。分析以下 Agent 记忆条目,识别行为模式和趋势。
请返回 JSON 数组,每个元素包含:
- "observation": string — 模式描述(中文)
- "frequency": number — 该模式出现的频率估计1-10
- "sentiment": "positive" | "negative" | "neutral" — 情感倾向
- "evidence": string[] — 支持该观察的证据记忆内容摘要最多3条
只返回 JSON 数组,不要其他内容。如果没有明显模式,返回空数组。"#
.to_string();
let request = CompletionRequest {
model: driver.provider().to_string(),
system: Some(system_prompt),
messages: vec![zclaw_types::Message::assistant(
format!("分析以下记忆条目:\n\n{}", memory_summary)
)],
max_tokens: Some(2048),
temperature: Some(0.3),
stream: false,
..Default::default()
};
let response = driver.complete(request).await
.map_err(|e| format!("LLM 调用失败: {}", e))?;
// Extract text from response
let text = response.content.iter()
.filter_map(|block| match block {
ContentBlock::Text { text } => Some(text.as_str()),
_ => None,
})
.collect::<Vec<_>>()
.join("");
// Parse JSON response (handle markdown code blocks)
let json_str = extract_json_from_llm_response(&text);
serde_json::from_str::<Vec<PatternObservation>>(&json_str)
.map_err(|e| format!("解析 LLM 响应失败: {} — 原始响应: {}", e, &text[..text.len().min(200)]))
}
/// Analyze patterns in memories (rule-based fallback)
fn analyze_patterns(&self, memories: &[MemoryEntryForAnalysis]) -> Vec<PatternObservation> {
let mut patterns = Vec::new();
@@ -633,7 +719,6 @@ pub fn pop_restored_result(agent_id: &str) -> Option<ReflectionResult> {
// === Tauri Commands ===
use std::sync::Arc;
use tokio::sync::Mutex;
pub type ReflectionEngineState = Arc<Mutex<ReflectionEngine>>;
@@ -679,7 +764,7 @@ pub async fn reflection_reflect(
state: tauri::State<'_, ReflectionEngineState>,
) -> Result<ReflectionResult, String> {
let mut engine = state.lock().await;
Ok(engine.reflect(&agent_id, &memories))
Ok(engine.reflect(&agent_id, &memories, None).await)
}
/// Get reflection history
@@ -785,3 +870,28 @@ mod tests {
assert!(!patterns.iter().any(|p| p.observation.contains("待办任务")));
}
}
// === Helpers ===
/// Extract JSON from LLM response, handling markdown code blocks and extra text
fn extract_json_from_llm_response(text: &str) -> String {
let trimmed = text.trim();
// Try to find JSON array in markdown code block
if let Some(start) = trimmed.find("```json") {
if let Some(content_start) = trimmed[start..].find('\n') {
if let Some(end) = trimmed[content_start..].find("```") {
return trimmed[content_start + 1..content_start + end].trim().to_string();
}
}
}
// Try to find bare JSON array
if let Some(start) = trimmed.find('[') {
if let Some(end) = trimmed.rfind(']') {
return trimmed[start..end + 1].to_string();
}
}
trimmed.to_string()
}

View File

@@ -7,9 +7,12 @@
use tracing::debug;
use std::sync::Arc;
use crate::intelligence::identity::IdentityManagerState;
use crate::intelligence::heartbeat::HeartbeatEngineState;
use crate::intelligence::reflection::{MemoryEntryForAnalysis, ReflectionEngineState};
use zclaw_runtime::driver::LlmDriver;
/// Run pre-conversation intelligence hooks
///
@@ -43,6 +46,7 @@ pub async fn post_conversation_hook(
_user_message: &str,
_heartbeat_state: &HeartbeatEngineState,
reflection_state: &ReflectionEngineState,
llm_driver: Option<Arc<dyn LlmDriver>>,
) {
// Step 1: Record interaction for heartbeat
crate::intelligence::heartbeat::record_interaction(agent_id);
@@ -80,7 +84,7 @@ pub async fn post_conversation_hook(
memories.len()
);
let reflection_result = engine.reflect(agent_id, &memories);
let reflection_result = engine.reflect(agent_id, &memories, llm_driver.clone()).await;
debug!(
"[intelligence_hooks] Reflection completed: {} patterns, {} suggestions",
reflection_result.patterns.len(),

View File

@@ -442,17 +442,21 @@ pub async fn agent_chat_stream(
).await.unwrap_or_default();
// Get the streaming receiver while holding the lock, then release it
let mut rx = {
let (mut rx, llm_driver) = {
let kernel_lock = state.lock().await;
let kernel = kernel_lock.as_ref()
.ok_or_else(|| "Kernel not initialized. Call kernel_init first.".to_string())?;
// Clone LLM driver for reflection engine (Arc clone is cheap)
let driver = Some(kernel.driver());
// Start the stream - this spawns a background task
// Use intelligence-enhanced system prompt if available
let prompt_arg = if enhanced_prompt.is_empty() { None } else { Some(enhanced_prompt) };
kernel.send_message_stream_with_prompt(&id, message.clone(), prompt_arg)
let rx = kernel.send_message_stream_with_prompt(&id, message.clone(), prompt_arg)
.await
.map_err(|e| format!("Failed to start streaming: {}", e))?
.map_err(|e| format!("Failed to start streaming: {}", e))?;
(rx, driver)
};
// Lock is released here
@@ -492,7 +496,7 @@ pub async fn agent_chat_stream(
// POST-CONVERSATION: record interaction + trigger reflection
crate::intelligence_hooks::post_conversation_hook(
&agent_id_str, &message, &hb_state, &rf_state,
&agent_id_str, &message, &hb_state, &rf_state, llm_driver.clone(),
).await;
StreamChatEvent::Complete {

View File

@@ -763,6 +763,7 @@ pub struct PipelineCandidateInfo {
#[tauri::command]
pub async fn route_intent(
state: State<'_, Arc<PipelineState>>,
kernel_state: State<'_, KernelState>,
user_input: String,
) -> Result<RouteResultResponse, String> {
use zclaw_pipeline::{TriggerParser, Trigger, TriggerParam, compile_trigger};
@@ -859,6 +860,54 @@ pub async fn route_intent(
});
}
// Semantic match via LLM (if kernel is initialized)
let triggers = parser.triggers();
if !triggers.is_empty() {
let llm_driver = {
let kernel_lock = kernel_state.lock().await;
kernel_lock.as_ref().map(|k| k.driver())
};
if let Some(driver) = llm_driver {
use zclaw_pipeline::{RuntimeLlmIntentDriver, LlmIntentDriver};
let intent_driver = RuntimeLlmIntentDriver::new(driver);
if let Some(result) = intent_driver.semantic_match(&user_input, &triggers).await {
tracing::debug!(
"[route_intent] Semantic match: pipeline={}, confidence={}",
result.pipeline_id, result.confidence
);
let trigger = parser.get_trigger(&result.pipeline_id);
let mode = "auto".to_string();
let missing_params: Vec<MissingParamInfo> = trigger
.map(|t| {
t.param_defs.iter()
.filter(|p| p.required && !result.params.contains_key(&p.name) && p.default.is_none())
.map(|p| MissingParamInfo {
name: p.name.clone(),
label: p.label.clone(),
param_type: p.param_type.clone(),
required: p.required,
default: p.default.clone(),
})
.collect()
})
.unwrap_or_default();
return Ok(RouteResultResponse::Matched {
pipeline_id: result.pipeline_id,
display_name: trigger.and_then(|t| t.display_name.clone()),
mode,
params: result.params,
confidence: result.confidence,
missing_params,
});
}
}
}
// No match - return suggestions
let suggestions: Vec<PipelineCandidateInfo> = parser.triggers()
.iter()