修复: namespace 占位符发 LLM 前未展开 + key 含斜杠丢原文
This commit is contained in:
@@ -157,11 +157,18 @@ impl NamespaceStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 简单的基于长度的哈希(唯一性由 content 长度 + 前 16 字符保证)
|
/// 内容 → 引用 key 段(确定性,不含 `/` 等会破坏 `parse_namespace_key` 的字符)。
|
||||||
/// 用于构建引用路径中的 key 段,不需要密码学强度
|
///
|
||||||
|
/// 历史 bug:旧实现用 `"{len}_{前16字符}"`,前 16 字符原样进 key;内容前缀含 `/`(目录列表、
|
||||||
|
/// 带路径的 grep 结果、文件头是路径)时,`namespace://tool/{key}` 被 `parse_namespace_key`
|
||||||
|
/// 的 `split('/').nth(1)` 从首个 `/` 截断 → read/read_only 拿回 None → 原文彻底丢失。
|
||||||
|
/// 现改用 SipHash u64 → 16 位 hex:确定性 + 极低碰撞 + 不含特殊字符。
|
||||||
fn simple_hash(content: &str) -> String {
|
fn simple_hash(content: &str) -> String {
|
||||||
let prefix: String = content.chars().take(16).collect();
|
use std::collections::hash_map::DefaultHasher;
|
||||||
format!("{}_{}", content.len(), prefix)
|
use std::hash::{Hash, Hasher};
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
content.hash(&mut hasher);
|
||||||
|
format!("{:016x}", hasher.finish())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -226,6 +233,18 @@ mod tests {
|
|||||||
assert_eq!(r1, r2);
|
assert_eq!(r1, r2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn store_content_with_slash_in_prefix_reads_back() {
|
||||||
|
// 回归:旧 simple_hash 把内容前 16 字符原样进 key,前缀含 '/' 时(目录列表/带路径 grep
|
||||||
|
// 结果/文件头是路径)namespace://tool/{key} 被 parse_namespace_key 的 split('/') 截断
|
||||||
|
// → read None,原文丢失。hex hash 修复后 store→read 正确取回。
|
||||||
|
let mut ns = NamespaceStore::new(100_000);
|
||||||
|
let content = "foo/bar/baz/qux\nline2\nline3";
|
||||||
|
let path = ns.store("list_directory", content);
|
||||||
|
assert!(is_namespace_ref(&path));
|
||||||
|
assert_eq!(ns.read(&path).map(str::to_owned), Some(content.to_string()));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn eviction_oldest_removed_when_over_limit() {
|
fn eviction_oldest_removed_when_over_limit() {
|
||||||
let mut ns = NamespaceStore::new(100); // 极小容量
|
let mut ns = NamespaceStore::new(100); // 极小容量
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ pub(super) async fn maybe_auto_compress(
|
|||||||
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
let _ = app_handle.emit("ai-chat-event", ev.clone());
|
||||||
// L3 emit 双写:tunnel subscriber透传 miniapp
|
// L3 emit 双写:tunnel subscriber透传 miniapp
|
||||||
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(ev);
|
let _ = app_handle.state::<AppState>().ai_event_bus.publish_event(ev);
|
||||||
let (active_msgs, lang) = {
|
let (mut active_msgs, lang) = {
|
||||||
let mut session = session_arc.lock().await;
|
let mut session = session_arc.lock().await;
|
||||||
let conv = session.conv(&conv_id);
|
let conv = session.conv(&conv_id);
|
||||||
conv.messages.set_compressing(true);
|
conv.messages.set_compressing(true);
|
||||||
@@ -118,6 +118,22 @@ pub(super) async fn maybe_auto_compress(
|
|||||||
(active_msgs, lang)
|
(active_msgs, lang)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// T2-修复(方案A):展开 namespace 引用为真实内容,再喂压缩 LLM/关键词摘要。
|
||||||
|
// 同 agentic/mod.rs 的 build_for_request 出口展开。未展开则压缩 LLM 只看到
|
||||||
|
// "namespace://..." 占位符,摘要基于占位符丢主题。read_only None(重启后 namespace
|
||||||
|
// 清空/残留引用)→ 保留占位符降级,非崩溃。
|
||||||
|
{
|
||||||
|
let session = session_arc.lock().await;
|
||||||
|
for m in &mut active_msgs {
|
||||||
|
if df_ai::namespace_store::is_namespace_ref(&m.content) {
|
||||||
|
let path = m.content.clone();
|
||||||
|
if let Some(original) = session.namespace_store.read_only(&path) {
|
||||||
|
m.content = original.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 改进3 B:在 active_msgs move 进 compress_via_llm 前,先算关键词摘要兜底文本。
|
// 改进3 B:在 active_msgs move 进 compress_via_llm 前,先算关键词摘要兜底文本。
|
||||||
// LLM 压缩失败时仍想保留用户反复提及的主题词(续接锚点),避免裸裁剪丢主题。
|
// LLM 压缩失败时仍想保留用户反复提及的主题词(续接锚点),避免裸裁剪丢主题。
|
||||||
// KEYWORD_FALLBACK_ENABLED=false → 跳过(回退原裸裁剪行为,排障/对比用)。
|
// KEYWORD_FALLBACK_ENABLED=false → 跳过(回退原裸裁剪行为,排障/对比用)。
|
||||||
|
|||||||
@@ -1209,7 +1209,21 @@ pub(crate) async fn run_agentic_loop(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let (history_msgs, _trimmed) = conv.messages.build_for_request(sys_tokens);
|
let (mut history_msgs, _trimmed) = conv.messages.build_for_request(sys_tokens);
|
||||||
|
// T2-修复(方案A):发 LLM 前展开 namespace 引用为真实内容(view-only,仅本轮请求视图)。
|
||||||
|
// push 时(audit/mod.rs)read_file/list_directory/grep/diff_files 等大结果被
|
||||||
|
// namespace_store 引用化进 messages;持久化(conversation.rs)已展开写 DB,但发 LLM 的
|
||||||
|
// 实时请求此前未展开 → LLM 每轮只看到 "namespace://..." 占位符,AI 实际拿不到工具结果。
|
||||||
|
// 此处展开,namespace 退化为内存/持久化层优化,LLM 永远看真实内容。
|
||||||
|
// read_only None(重启后 namespace 清空 / 跨会话残留引用)→ 保留占位符降级,非崩溃。
|
||||||
|
for m in &mut history_msgs {
|
||||||
|
if df_ai::namespace_store::is_namespace_ref(&m.content) {
|
||||||
|
let path = m.content.clone();
|
||||||
|
if let Some(original) = session.namespace_store.read_only(&path) {
|
||||||
|
m.content = original.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
let mut msgs = vec![ChatMessage::system(&system_prompt)];
|
let mut msgs = vec![ChatMessage::system(&system_prompt)];
|
||||||
msgs.extend(history_msgs);
|
msgs.extend(history_msgs);
|
||||||
msgs
|
msgs
|
||||||
|
|||||||
Reference in New Issue
Block a user