新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理

后端:
- 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展
- 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通
- 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦
- AI planner/plan_hint/intent:aichat B 路线并行多轮基础
- patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环
- 压缩超时兜底(F-15 卡死根治)
- F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本
- 知识注入 DRY/skills/audit 扩展

清理:
- aichat 技术债(误报 allow/死导入/过时注释 30 项)
- URGENT.md 删除(11 项加急全解决/迁 todo)
- 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
This commit is contained in:
2026-06-21 20:51:26 +08:00
parent 330bb7f505
commit bd6a41fe6e
111 changed files with 11932 additions and 1034 deletions

View File

@@ -420,6 +420,106 @@ pub fn filter_tool_defs(
.collect()
}
// ---- 工具子集过滤 + plan_hint 编排(B 路线 Phase 1 接入主 loop) --------------
/// 在 `filter_tool_defs` 收敛的扁平子集之上,叠加 `plan_hint` 的编排关系
/// (并行组同批聚拢 / 顺序依赖源在前),供 LLM 看到一份**按编排意图排序**的工具列表。
///
/// **Phase 1 接入语义**(依据
/// `docs/02-架构设计/单对话并行多轮-设计-2026-06-20.md` §五 Phase 1 +
/// `单对话并行多轮-Phase0落地路线图-2026-06-20.md` §八 Phase 1 硬前置):
/// - **纯函数零延迟**(与 intent/plan_hint 一致,无 IO 无 LLM 调用)。
/// - **不真正多轮并行**(Phase 1 仅提示层;真正子流并行 execution 留 Phase 3)。
/// - 行为 = 先 `filter_tool_defs` 收敛 → 再按 `plan_hint` 产出排序。
///
/// **三重 fallback(与 filter_tool_defs 一致的安全语义)**:
/// 1. `plan_hint` 返空 PlanHint(无编排信号/单工具/闲聊)→ 退 `filter_tool_defs` 扁平结果。
/// 2. `plan_hint::validate` 判非法(环/空组/自环/重复边)→ 丢弃 hint 退扁平结果
/// (主 loop 不能据非法 hint 调度,否则死锁)。
/// 3. hint 引用的工具名不在 filtered 子集(registry 漂移)→ 该名跳过,剩余照常排序。
///
/// **排序规则**(确定性,便于单测固定行为):
/// - 收集 hint 所有并行组里的工具名,按「组序 → 组内顺序」展开成有序列表 `ordered`。
/// - 输出 = `ordered` 中能在 filtered 找到的(保 ordered 顺序) +
/// filtered 中未被 ordered 命中的(保 filtered 原序,放尾部兜底,防丢工具)。
/// - 顺序依赖(`sequence` 边)的「from」节点天然被并行组顺序吸收(组在前 = from 先出现),
/// Phase 1 不额外重排依赖边(真正 DAG 调度留 Phase 3 Kahn 分层)。
///
/// **关键安全**(与 filter_tool_defs 同):本函数**只改 LLM 可见 tool_defs 的顺序/可见性**,
/// 不改执行路径(audit 走 tools_arc 完整 registry)。LLM 即使幻觉被滤掉的顺序,
/// audit 仍能查到/拒绝。
///
/// `intent`:主 loop 已识别的意图;其 `as_str()` 标签内部派生传给 plan_hint。
/// `intent_label`:**冗余参数**(等价于 `intent.as_str()`),仅为调用方签名兼容保留,
/// 内部已忽略——以 `intent.as_str()` 为权威源,防调用方传错标签与实际 intent 不一致。
/// `context`:用户末条消息原文(plan_hint 关键词检测用)。
pub fn filter_tool_defs_planned(
all_defs: &[df_ai_core::types::ToolDefinition],
intent: &Intent,
intent_label: &str,
context: &str,
) -> Vec<df_ai_core::types::ToolDefinition> {
// 第一步:先得扁平收敛子集(复用 filter_tool_defs 的全部 fallback 语义)。
let filtered = filter_tool_defs(all_defs, intent);
// 第二步:产 PlanHint。PLAN_HINT_ENABLED 关时 plan_hint 内部返空 → 退扁平。
// intent_label 内部由 intent.as_str() 派生(冗余参数权威性低于 intent 本身)。
let label = intent.as_str();
let _ = intent_label; // 冗余:签名兼容保留,内部以 intent.as_str() 为准。
let hint = crate::plan_hint::plan_hint(label, context);
// 空 hint(无编排信号)→ 直接返扁平收敛结果(零行为变更 vs filter_tool_defs)。
if hint.is_empty() {
return filtered;
}
// 非法 hint(环/空组/自环/重复边)→ 丢弃,退扁平(主 loop 不能据非法 hint 调度)。
if !matches!(crate::plan_hint::validate(&hint), crate::plan_hint::HintValidity::Ok) {
return filtered;
}
// 第三步:按 hint 编排关系重排 filtered。先展开「组序 → 组内序」的有序工具名表。
let mut ordered_names: Vec<&'static str> = Vec::new();
for g in &hint.groups {
for t in &g.tools {
if !ordered_names.contains(t) {
ordered_names.push(*t);
}
}
}
// 第四步:用 HashMap 索引(filtered 名 → 其在该 Vec 中的位置)替代「ordered × filtered」
// 嵌套线性扫描。原版对每个 ordered 名做一次 O(filtered) 内层扫描,总 O(ordered × filtered);
// 索引化后 O(filtered) 建索引 + 每 ordered 名 O(1) 查(filtered 通常 5-20 个工具,
// 索引开销极小,但消除最坏 O(n²) 当 ordered/filtered 同时增长)。
use std::collections::HashMap;
// name_to_idx:工具名 → filtered 中首次出现位置(filtered 内工具名唯一,无二义)。
let mut name_to_idx: HashMap<&str, usize> = HashMap::with_capacity(filtered.len());
for (i, d) in filtered.iter().enumerate() {
// filtered 名唯一(同 domain 不会重复),with_capacity 后直接 insert 不冲突。
name_to_idx.entry(d.function.name.as_str()).or_insert(i);
}
// 按 ordered 顺序挑 filtered 里的(命中编排顺序的工具置前)。
let mut result: Vec<df_ai_core::types::ToolDefinition> = Vec::with_capacity(filtered.len());
let mut consumed: std::collections::HashSet<usize> = std::collections::HashSet::new();
for name in &ordered_names {
if let Some(&idx) = name_to_idx.get(*name) {
if consumed.insert(idx) {
result.push(filtered[idx].clone());
}
}
}
// 未被 ordered 命中的(filtered 里有但 hint 没标)→ 保 filtered 原序追加尾部兜底(防丢工具)。
for (i, d) in filtered.iter().enumerate() {
if consumed.insert(i) {
result.push(d.clone());
}
}
result
}
// ---- 单测 -------------------------------------------------------------------
#[cfg(test)]
@@ -455,7 +555,7 @@ mod tests {
}
#[test]
fn recognize_debug_zh() {
fn recognize_code_wins_over_debug_in_specific_group() {
let (i, c) = IntentRecognizer::recognize("这个 bug 怎么复现,帮我调试排查一下");
// "bug" 在 Code 组,"复现/调试/排查" 在 Debug 组 —— SPECIFIC 组内 Code 命中先于 Debug
// Code 命中(bug=1.0) 即胜出本组,不再看 Debug。断言优先级正确Code 胜出。
@@ -975,4 +1075,138 @@ mod tests {
assert_eq!(http_count, 1, "Intent {:?} subset http_request 应去重为 1", intent);
}
}
// ===== filter_tool_defs_planned(B 路线 Phase 1 接入主 loop 用) =====
//
// 单测覆盖 Phase 1 接入语义:flag 关(filter_tool_defs 旧行为)、
// plan_hint 空 hint 退扁平、非法 hint 退扁平、registry 漂移跳过、有序重排。
// PLANNING_ENABLED(planner.rs)门控主 loop 接入点;PLAN_HINT_ENABLED(plan_hint.rs)
// 门控 plan_hint 函数内部产出。本组单测直接验证 filter_tool_defs_planned
// 纯函数行为(两层 flag 均为当前默认值时 plan_hint 函数产非空 hint)。
#[test]
fn planned_empty_intent_subset_falls_back_to_full() {
// Unknown subset 空 → filter_tool_defs 回全量 → plan_hint(无编排信号或闲聊)→ 扁平全量。
// 校验 fallback 链完整:不会因 plan_hint 误丢工具。
let all = vec![tool_def("read_file"), tool_def("write_file"), tool_def("http_request")];
let out = filter_tool_defs_planned(&all, &Intent::Unknown, "unknown", "你好谢谢");
assert_eq!(out.len(), all.len(), "Unknown + 闲聊应回全量(双 fallback)");
}
#[test]
fn planned_plain_chat_yields_filter_result_unordered() {
// Chat subset 空 → filter_tool_defs 回全量;context 无编排信号 → plan_hint 空 → 退扁平。
// 关键:即使 PLANNING_ENABLED(true 时)走 planned 路径,无编排信号时输出与
// filter_tool_defs 一致(顺序 + 内容),零行为变更。
let all = vec![
tool_def("read_file"),
tool_def("write_file"),
tool_def("http_request"),
];
let planned = filter_tool_defs_planned(&all, &Intent::Chat, "chat", "你好谢谢");
let flat = filter_tool_defs(&all, &Intent::Chat);
let planned_names: Vec<&str> = planned.iter().map(|d| d.function.name.as_str()).collect();
let flat_names: Vec<&str> = flat.iter().map(|d| d.function.name.as_str()).collect();
assert_eq!(planned_names, flat_names, "无编排信号 planned 应与 filter 扁平一致");
}
#[test]
fn planned_read_then_modify_orders_read_first() {
// 读后写编排信号 → plan_hint 产 read_file→write_file 依赖边 → planned 把 read_file 置前。
// File intent subset 含 read_file/write_file/patch_file 等全 File domain。
let all = vec![
// 故意把 write_file 放前,验证 planned 把 read_file 重排到 write_file 之前
tool_def("write_file"),
tool_def("patch_file"),
tool_def("read_file"),
tool_def("list_directory"),
tool_def("search_files"),
tool_def("file_info"),
tool_def("append_file"),
tool_def("delete_file"),
tool_def("rename_file"),
tool_def("http_request"), // 不在 File domain,应被滤掉
];
let out = filter_tool_defs_planned(&all, &Intent::File, "file", "先 read 这个文件再修改它");
let names: Vec<&str> = out.iter().map(|d| d.function.name.as_str()).collect();
// http_request 应被滤掉(intent 收敛)
assert!(!names.contains(&"http_request"), "File 意图应滤掉 http_request");
// read_file 应出现在 write_file 之前(plan_hint 编排:读组在前)
let read_idx = names.iter().position(|n| *n == "read_file").expect("应含 read_file");
let write_idx = names.iter().position(|n| *n == "write_file").expect("应含 write_file");
assert!(read_idx < write_idx, "plan_hint 应把 read_file 排到 write_file 前, 实际顺序: {:?}", names);
// 不丢工具:http_request 滤掉后剩 9 个(全 File domain 9 工具)
assert_eq!(out.len(), 9, "planned 不应丢工具(除被 intent 滤掉), 实际: {:?}", names);
}
#[test]
fn planned_preserves_unordered_tail_for_hint_missing_tools() {
// plan_hint 标了 read_file/write_file,但 filtered 里还有 hint 没标的工具(list_directory 等)
// → 未被 hint 命中的按 filtered 原序追加尾部兜底(防丢工具)。
let all = vec![
tool_def("list_directory"),
tool_def("search_files"),
tool_def("write_file"),
tool_def("read_file"),
tool_def("patch_file"),
tool_def("file_info"),
tool_def("append_file"),
tool_def("delete_file"),
tool_def("rename_file"),
];
let out = filter_tool_defs_planned(&all, &Intent::File, "file", "先 read 文件再修改 write_file");
let names: Vec<&str> = out.iter().map(|d| d.function.name.as_str()).collect();
// 全部 9 个工具都应保留(hint 未标的不丢)
assert_eq!(out.len(), 9, "未命中 hint 的工具应追加尾部兜底, 不丢, 实际: {:?}", names);
// read_file 仍置前(命中 hint 编排)
let read_idx = names.iter().position(|n| *n == "read_file").unwrap();
let write_idx = names.iter().position(|n| *n == "write_file").unwrap();
assert!(read_idx < write_idx);
}
#[test]
fn planned_registry_drift_skips_missing_names() {
// registry 漂移:hint 标 read_file/write_file 但 all_defs 没这俩(改了名)。
// → ordered 命中 0 个,全部走「未被 ordered 命中」追加 = filtered 原序(退 filter_tool_defs 行为)。
let all = vec![
tool_def("list_directory"),
tool_def("search_files"),
tool_def("file_info"),
tool_def("append_file"),
tool_def("delete_file"),
tool_def("rename_file"),
];
// 这条消息会产 read→write hint,但 all_defs 里没 read_file/write_file
let out = filter_tool_defs_planned(&all, &Intent::File, "file", "先 read 再修改 write_file");
let flat = filter_tool_defs(&all, &Intent::File);
// 漂移:hint 工具名都不在 → 退扁平(filtered 原序),不 panic,不丢不增。
assert_eq!(out.len(), flat.len(), "漂移应退扁平, 实际 planned={}, flat={}", out.len(), flat.len());
let out_names: Vec<&str> = out.iter().map(|d| d.function.name.as_str()).collect();
let flat_names: Vec<&str> = flat.iter().map(|d| d.function.name.as_str()).collect();
assert_eq!(out_names, flat_names, "漂移时 planned 应与 filter 扁平一致");
}
#[test]
fn planned_multi_read_single_parallel_group_preserves_all() {
// 规则 3:读多文件(无写)→ plan_hint 产单并行组 [read_file, list_directory]。
// 全部命中 filtered → 两个工具置前(组内序),其余 filtered 工具追加尾部。
let all = vec![
tool_def("patch_file"),
tool_def("read_file"),
tool_def("write_file"),
tool_def("list_directory"),
tool_def("search_files"),
tool_def("file_info"),
tool_def("append_file"),
tool_def("delete_file"),
tool_def("rename_file"),
];
let out = filter_tool_defs_planned(&all, &Intent::File, "file", "read 文件再看 directory 目录");
let names: Vec<&str> = out.iter().map(|d| d.function.name.as_str()).collect();
assert_eq!(out.len(), 9, "planned 不应丢工具");
// read_file 和 list_directory 都在 hint 单组,应置前
let read_idx = names.iter().position(|n| *n == "read_file").unwrap();
let dir_idx = names.iter().position(|n| *n == "list_directory").unwrap();
assert!(read_idx < 4 && dir_idx < 4, "组内工具应置前, 实际 read_idx={} dir_idx={}", read_idx, dir_idx);
}
}