fix(aibox): Batch N 波次4+5 LLM 健壮性与权限隔离,Batch N 收束
波次 4 LLM 健壮性(T10/T19/T20/T21/T23 + T14 复现证伪): - T10 调度器 await 改 wait_for 硬超时兜底(task_timeout+30s 宽限), 软取消够不到的卡死路径不再永久停摆调度循环 - T19 FailoverClient 异常收窄为瞬时白名单(Connection/Timeout/ ChunkedEncoding),MissingSchema 等永久配置错误不再污染冷却表 - T20 流式未收到 [DONE] 抛 TransientLLMError,回答无声截断可感知 - T21 env 覆盖黑名单 filter_env_overrides(LD_*/DYLD_*/PATH 等), executors 两处 + process_manager 接入 - T23 压缩后 tool 配对双向修复(失配 tool_calls 剥离防 API 400) - T14 FTS5 复现证伪:JOIN 别名场景表名 MATCH 正常,无需修复 波次 5 权限隔离(T22/T-L17/T-L18;T1/T2 产品决策:仅告警/维持现状): - T22a TOOL_PROFILE_TASK 定时任务精简工具面(无 update_identity/ update_soul/set_proxy) - T22b ProcessManager owner 隔离:bg_write/bg_kill 属主校验, 观察面共享;DeepSeekChat 新增 owner_id 传导 - T1 绑定非回环地址时启动显著告警 - T-L17 model_config env 名按域名推荐(防跨厂商凭据外发) - T-L18 list_profiles 过滤控制字符(防 ANSI 注入) 测试:test_n_fixes.py 增至 62 项;全量 598 passed / 1 failed (Windows 向 ping 用例,环境项),零回归。审计报告补记 §24.7~24.11, README 同步 owner 隔离语义。八轮审计 T 系列 28 项全部处置完毕。
This commit is contained in:
1 parent
7e44025671
commit
67acae708c
10 files changed
+824
-107
No files matched your search
@@ -223,49 +223,6 @@ class ConversationCompressor:
|
||||
|
||||
return conversation_turns
|
||||
|
||||
def _is_range_safe(self, messages: List[Dict], start: int, end: int, protected: set) -> bool:
|
||||
"""检查消息范围是否安全 - 简化版,不检查tool完整性"""
|
||||
return True # 不保护tool消息,允许压缩
|
||||
|
||||
def _get_protected_indices(self, messages: List[Dict]) -> set:
|
||||
"""
|
||||
找出所有受保护的消息索引 - 简化版,只保护最近的对话轮次
|
||||
"""
|
||||
protected_indices = set()
|
||||
|
||||
# ========== 1. 保护最近的消息(由配置决定) ==========
|
||||
keep_recent = constants.COMPRESS_KEEP_RECENT
|
||||
if keep_recent > 0:
|
||||
recent_start = max(0, len(messages) - keep_recent)
|
||||
for i in range(recent_start, len(messages)):
|
||||
protected_indices.add(i)
|
||||
|
||||
# ========== 2. 保护前2个对话轮次 ==========
|
||||
conversation_turns = self._build_conversation_turns(messages)
|
||||
|
||||
if len(conversation_turns) >= 2:
|
||||
# 保护第1轮
|
||||
first_turn = conversation_turns[0]
|
||||
protected_indices.add(first_turn["user_index"])
|
||||
if first_turn["assistant_index"] is not None:
|
||||
protected_indices.add(first_turn["assistant_index"])
|
||||
for tool_idx in first_turn["tool_indices"]:
|
||||
protected_indices.add(tool_idx)
|
||||
|
||||
# 保护第2轮
|
||||
second_turn = conversation_turns[1]
|
||||
protected_indices.add(second_turn["user_index"])
|
||||
if second_turn["assistant_index"] is not None:
|
||||
protected_indices.add(second_turn["assistant_index"])
|
||||
for tool_idx in second_turn["tool_indices"]:
|
||||
protected_indices.add(tool_idx)
|
||||
|
||||
# 调试日志
|
||||
if self.logger.level >= 3:
|
||||
self.logger.debug(f"受保护的消息索引: {sorted(protected_indices)}")
|
||||
|
||||
return protected_indices
|
||||
|
||||
def _build_compression_prompt(self, messages: List[Dict]) -> str:
|
||||
"""构建压缩提示词 - 包含工具调用信息"""
|
||||
conversation_lines = []
|
||||
@@ -368,25 +325,57 @@ class ConversationCompressor:
|
||||
return None
|
||||
|
||||
def _validate_message_structure(self, messages: List[Dict]) -> List[Dict]:
|
||||
"""验证并修复消息结构,移除孤立的 tool 消息"""
|
||||
"""验证并修复消息结构(tool 配对双向完整性)。
|
||||
|
||||
OpenAI 协议要求 assistant.tool_calls 与对应的 tool 结果消息成对出现,
|
||||
缺任一侧后续 API 调用直接 400:
|
||||
1. 孤立 tool 消息(tool_call_id 无对应 assistant)→ 移除(既有行为)
|
||||
2. T23(审计 2026-08-21):压缩块边界按条数切,可能落在带 tool_calls
|
||||
的 assistant 消息与其 tool 结果之间——保留侧 assistant 的
|
||||
tool_calls 失去配对 → 剥离失配的 tool_calls 条目;全部失配时删除
|
||||
字段(content 为空则置空串,防产生非法空消息)
|
||||
"""
|
||||
if not messages:
|
||||
return messages
|
||||
|
||||
# 找出所有 assistant 消息的 tool_call_ids
|
||||
assistant_tool_ids = set()
|
||||
# tool 侧实际存在的 id
|
||||
present_tool_ids = {
|
||||
m.get("tool_call_id")
|
||||
for m in messages
|
||||
if m.get("role") == "tool" and m.get("tool_call_id")
|
||||
}
|
||||
|
||||
# 第一遍:assistant 剥离失去配对的 tool_calls
|
||||
first_pass = []
|
||||
kept_assistant_ids = set()
|
||||
for msg in messages:
|
||||
if msg.get("role") == "assistant" and msg.get("tool_calls"):
|
||||
for tc in msg.get("tool_calls", []):
|
||||
tc_id = tc.get("id")
|
||||
if tc_id:
|
||||
assistant_tool_ids.add(tc_id)
|
||||
calls = msg.get("tool_calls", [])
|
||||
kept_calls = [tc for tc in calls
|
||||
if tc.get("id") and tc.get("id") in present_tool_ids]
|
||||
kept_assistant_ids.update(tc.get("id") for tc in kept_calls)
|
||||
if len(kept_calls) == len(calls):
|
||||
first_pass.append(msg) # 全部有配对,原样保留
|
||||
continue
|
||||
new_msg = dict(msg)
|
||||
if kept_calls:
|
||||
new_msg["tool_calls"] = kept_calls
|
||||
print(f" ⚠️ 剥离部分失去配对 tool 结果的 tool_calls(T23)")
|
||||
else:
|
||||
new_msg.pop("tool_calls", None)
|
||||
if not new_msg.get("content"):
|
||||
new_msg["content"] = ""
|
||||
print(f" ⚠️ 剥离失去配对 tool 结果的 assistant.tool_calls(T23)")
|
||||
first_pass.append(new_msg)
|
||||
else:
|
||||
first_pass.append(msg)
|
||||
|
||||
# 过滤掉没有对应 assistant 的 tool 消息
|
||||
# 第二遍:移除孤立 tool 消息(其 id 不在任何保留的 assistant tool_calls 中)
|
||||
filtered = []
|
||||
for msg in messages:
|
||||
for msg in first_pass:
|
||||
if msg.get("role") == "tool":
|
||||
tool_call_id = msg.get("tool_call_id")
|
||||
if tool_call_id and tool_call_id not in assistant_tool_ids:
|
||||
if tool_call_id and tool_call_id not in kept_assistant_ids:
|
||||
print(f" ⚠️ 移除孤立的 tool 消息: {tool_call_id}")
|
||||
continue
|
||||
filtered.append(msg)
|
||||
|
||||
Reference in new issue
Block a user