fix(multi-agent): 修复压缩404、prompt冻结串扰、子智能体状态串扰及/multiagent/new跳转问题

This commit is contained in:
JOJO 2026-07-14 23:22:06 +08:00
parent ce92697181
commit 351cd311c2
10 changed files with 95 additions and 60 deletions

View File

@ -201,6 +201,25 @@
1. **meta 部分只放参数**`tool-result-meta` 仅用于展示工具调用的参数、配置、状态标识等元信息。例如状态、ID、路径、任务描述、超时时间等。禁止在 meta 区域放置执行结果、统计摘要、输出内容等。
2. **content 部分只放结果**`tool-result-content` 仅用于展示工具执行后的结果、输出、统计、总结等。例如:文件内容、命令输出、搜索命中、执行统计(工作时间 / 调用次数 / 工具次数)、最终回复摘要等。禁止在 content 区域重复展示工具参数。
## 5.7) 调试输出规范(强制)
> 约束级别:需要临时加调试日志排查问题时**必须遵守**。
### 前端调试日志
- **统一筛选词**:所有临时 `console.log` 必须使用**同一个**筛选前缀(例如 `[route-debug]`),方便用户在浏览器控制台用该前缀一次性筛选。
- **控制数量与时机**
- 禁止一次性输出上百上千条日志刷屏。
- 只在关键路径(入口、分支判断、状态变化、导航动作)打印,避免在循环、高频事件、每帧渲染中输出。
- 需要大量结构化数据时,优先用 `console.group` / `console.table` 或对象快照,而不是逐条打印。
- **用完即清**:问题定位后应及时删除或注释掉临时调试日志,不要长期留在代码里。
### 后端调试日志
- **必须写入文件**:后端调试信息禁止直接 `print` 到终端刷屏,必须写入日志文件。
- **复用现有 logger**:优先复用项目已有 logger`utils/logger.py`、`modules/multi_agent/debug_logger.py` 等),按模块落到 `~/.astrion/astrion/<mode>/logs/` 下。
- **关键状态转换必打**:多智能体、子智能体、任务轮询等复杂链路应在关键状态转换点写结构化日志,便于复现问题后按时间线追溯。
## 6) Git 工作流(开发 + Review
### 6.1 核心原则

View File

@ -117,15 +117,29 @@ class MessagesMixin:
model_key = getattr(self, "model_key", None)
prompt_replacements = get_model_prompt_replacements(model_key)
is_multi_agent_mode = bool(getattr(self, "multi_agent_mode", False))
def _build_main_system_prompt() -> str:
if is_multi_agent_mode:
# 多智能体模式下主 prompt 使用 Team Leader 专属模板
try:
from modules.multi_agent.prompts import _load_template
return _load_template("master")
except Exception as exc:
logger.warning(f"[messages] 加载多智能体主 prompt 失败: {exc}")
system_prompt_template = self.load_prompt(prompt_name)
# main_system.txt / main_system_vl.txt 仅使用 {model_description}
return system_prompt_template.format(
model_description=prompt_replacements.get("model_description", "")
)
main_system_frozen_key = (
"frozen_main_system_prompt_multi_agent"
if is_multi_agent_mode
else "frozen_main_system_prompt"
)
system_prompt = self._get_or_init_frozen_prompt(
"frozen_main_system_prompt",
main_system_frozen_key,
_build_main_system_prompt,
)
@ -225,6 +239,13 @@ class MessagesMixin:
return agents_md_template.replace("{{AGENTS_MD_CONTENT}}", agents_md_content)
return f"【AGENTS.md 项目规范】\n\n{agents_md_content}\n\n---\n请注意:以上规范来自工作区根目录的 AGENTS.md 文件,若有冲突请以 AGENTS.md 为准。"
# AGENTS.md 注入开关可能变化,只在开启时生成并缓存,避免关闭时把空字符串冻住
agents_md_inject_enabled = (
bool(personalization_config.get("agents_md_auto_inject", False))
if isinstance(personalization_config, dict)
else False
)
if agents_md_inject_enabled:
agents_md_text = self._get_or_init_frozen_prompt(
"frozen_agents_md_prompt",
_build_agents_md_prompt,
@ -280,17 +301,8 @@ class MessagesMixin:
lambda: self._format_disabled_tool_notice() or "",
)
# 多智能体模式提示词:当会话 metadata.multi_agent_mode 为真时追加
# 多智能体模式额外提示词:可用的子智能体角色(动态 prompt第一个用户消息后冻结
if getattr(self, "multi_agent_mode", False):
try:
from modules.multi_agent.prompts import build_multi_agent_master_prompt
multi_agent_prompt = build_multi_agent_master_prompt(self.project_path, base="")
if multi_agent_prompt:
messages.append({"role": "system", "content": multi_agent_prompt})
except Exception as exc:
logger.warning(f"[messages] 注入多智能体 prompt 失败: {exc}")
# 可用的子智能体角色(动态 prompt第一个用户消息后冻结
try:
available_agents_prompt = self._get_or_init_frozen_prompt(
"frozen_available_agents_prompt",

View File

@ -221,7 +221,12 @@ class ModeMixin:
conv_id = getattr(cm, "current_conversation_id", None) if cm else None
if cm and conv_id:
try:
cm.conversation_manager.update_conversation_metadata(conv_id, {key: built})
target_manager = (
cm._get_conversation_manager_for_id(conv_id)
if hasattr(cm, "_get_conversation_manager_for_id")
else cm.conversation_manager
)
target_manager.update_conversation_metadata(conv_id, {key: built})
if isinstance(cm.conversation_metadata, dict):
cm.conversation_metadata[key] = built
except Exception:

View File

@ -1675,13 +1675,26 @@ def list_sub_agents(terminal: WebTerminal, workspace: UserWorkspace, username: s
pass
conversation_id = terminal.context_manager.current_conversation_id
data = manager.get_overview(conversation_id=conversation_id)
# 传统模式子智能体列表必须排除多智能体任务,避免 /new 等无当前对话场景
# 把最近一次多智能体模式的 running/idle 子智能体串过来。
def _is_multi_agent_task(item: dict) -> bool:
task_id = item.get("task_id")
raw_task = manager.tasks.get(task_id) if task_id else None
return bool(raw_task.get("multi_agent_mode")) if isinstance(raw_task, dict) else False
data = [item for item in data if not _is_multi_agent_task(item)]
# 仅在未绑定具体对话时(如全新会话尚未创建 conversation_id才回退为全局运行态
# 否则只显示当前对话关联的子智能体,避免新对话看到其他对话的任务。
if not data and not conversation_id:
all_overview = manager.get_overview(conversation_id=None)
if all_overview:
terminal_statuses = TERMINAL_STATUSES.union({"terminated"})
running_only = [item for item in all_overview if item.get("status") not in terminal_statuses]
running_only = [
item for item in all_overview
if item.get("status") not in terminal_statuses and not _is_multi_agent_task(item)
]
if running_only:
data = running_only
debug_log(

View File

@ -312,7 +312,12 @@ async def run_deep_compression(
sender=None,
) -> Dict[str, Any]:
cm = web_terminal.context_manager
conv_data = cm.conversation_manager.load_conversation(conversation_id)
target_manager = (
cm._get_conversation_manager_for_id(conversation_id)
if hasattr(cm, "_get_conversation_manager_for_id")
else cm.conversation_manager
)
conv_data = target_manager.load_conversation(conversation_id)
if not conv_data:
return {"success": False, "error": f"对话不存在: {conversation_id}"}
@ -419,7 +424,7 @@ async def run_deep_compression(
# 关键:重置 current_context_tokens避免自动压缩续接后阈值判断仍读到压缩前的大值而陷入死循环。
# 真实上下文长度会在下一次 API 响应后被重新写入。
try:
cm.conversation_manager.update_token_statistics(
target_manager.update_token_statistics(
conversation_id,
input_tokens=0,
output_tokens=0,
@ -484,7 +489,7 @@ async def run_deep_compression(
}
for frozen_key in REBUILD_FROZEN_KEYS:
meta_updates[frozen_key] = None
cm.conversation_manager.update_conversation_metadata(conversation_id, meta_updates)
target_manager.update_conversation_metadata(conversation_id, meta_updates)
# 同步内存中的 metadata清除 frozen 缓存
try:
if getattr(cm, "current_conversation_id", None) == conversation_id and isinstance(cm.conversation_metadata, dict):

View File

@ -134,10 +134,12 @@ export const syncMethods = {
const pathFragment = this.stripConversationPrefix(data.conversation_id);
const currentPath = window.location.pathname.replace(/^\/+/, '');
const urlPrefix = this.multiAgentMode ? '/multiagent/' : '/';
const targetPath = `${urlPrefix}${pathFragment}`;
if (data.created) {
history.pushState({ conversationId: data.conversation_id }, '', `/${pathFragment}`);
history.pushState({ conversationId: data.conversation_id }, '', targetPath);
} else if (currentPath !== pathFragment) {
history.replaceState({ conversationId: data.conversation_id }, '', `/${pathFragment}`);
history.replaceState({ conversationId: data.conversation_id }, '', targetPath);
}
}
}

View File

@ -36,6 +36,8 @@ export const routeMethods = {
this.multiAgentMode = true;
this.currentConversationId = null;
this.currentConversationTitle = '多智能体模式';
this.logMessageState('bootstrapRoute:clear-messages-for-multiagent-new');
this.messages = [];
this.titleReady = true;
this.suppressTitleTyping = false;
this.startTitleTyping('多智能体模式', { animate: false });
@ -47,38 +49,6 @@ export const routeMethods = {
} catch (_e) {
// 重建失败不阻断主流程
}
// 多智能体模式下自动创建一个带 metadata.multi_agent_mode=true 的新对话
try {
const resp = await fetch('/api/multiagent/conversations', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({})
});
const result = await resp.json();
if (result && result.success && result.conversation_id) {
this.currentConversationId = result.conversation_id;
// 拉取完整会话信息
try {
const loadResp = await fetch(`/api/conversations/${result.conversation_id}/load`, { method: 'PUT' });
const loadResult = await loadResp.json();
if (loadResult.success) {
if (typeof loadResult.run_mode === 'string') {
this.runMode = loadResult.run_mode;
this.thinkingMode = typeof loadResult.thinking_mode === 'boolean' ? loadResult.thinking_mode : loadResult.run_mode !== 'fast';
}
if (typeof loadResult.model_key === 'string' && loadResult.model_key) this.modelSet(loadResult.model_key);
this.currentConversationTitle = loadResult.title || '多智能体模式';
this.startTitleTyping(this.currentConversationTitle, { animate: false });
history.replaceState({ conversationId: result.conversation_id }, '', `/multiagent/${result.conversation_id.replace(/^conv_/, '')}`);
this.logMessageState('bootstrapRoute:multi-agent-loaded');
}
} catch (_e) {
// 加载失败也继续,主对话 已创建
}
}
} catch (error) {
console.warn('[multiagent] 初始化多智能体对话失败:', error);
}
await this.restoreComposerDraftState('bootstrap-route:multiagent');
return;
}
@ -214,7 +184,7 @@ export const routeMethods = {
},
isExplicitNewConversationRoute() {
const normalizedPath = window.location.pathname.replace(/^\/+|\/+$/g, '');
return normalizedPath === 'new';
return normalizedPath === 'new' || normalizedPath === 'multiagent/new' || normalizedPath === 'multiagent';
},
stripConversationPrefix(conversationId) {
if (!conversationId) return '';

View File

@ -321,10 +321,13 @@ export const socketMethods = {
const statusConversationId = statusData.conversation && statusData.conversation.current_id;
const resumeConversationId = statusConversationId || runningConversationId;
const shouldResumeOnNewRoute = isExplicitNewRoute && !!runningConversationId;
const currentPath = window.location.pathname.replace(/^\/+/, '');
const isMultiAgentNewRoute = currentPath === 'multiagent/new' || currentPath === 'multiagent';
if (
resumeConversationId &&
!this.currentConversationId &&
!isMultiAgentNewRoute &&
(!isExplicitNewRoute || shouldResumeOnNewRoute)
) {
this.skipConversationHistoryReload = true;

View File

@ -804,7 +804,6 @@ export async function initializeLegacySocket(ctx: any) {
return;
}
const convId = data.conversation_id;
console.log('[conv-trace] socket:conversation_resolved', data);
// 初始化期间不修改 currentConversationId避免与 bootstrapRoute 冲突
if (ctx.initialRouteResolved) {
@ -815,10 +814,12 @@ export async function initializeLegacySocket(ctx: any) {
ctx.promoteConversationToTop(convId);
const pathFragment = ctx.stripConversationPrefix(convId);
const currentPath = window.location.pathname.replace(/^\/+/, '');
const urlPrefix = ctx.multiAgentMode ? '/multiagent/' : '/';
const targetPath = `${urlPrefix}${pathFragment}`;
if (data.created) {
history.pushState({ conversationId: convId }, '', `/${pathFragment}`);
history.pushState({ conversationId: convId }, '', targetPath);
} else if (currentPath !== pathFragment) {
history.replaceState({ conversationId: convId }, '', `/${pathFragment}`);
history.replaceState({ conversationId: convId }, '', targetPath);
}
}
});

View File

@ -102,7 +102,12 @@ class CompressionMixin:
self.conversation_metadata[k] = v
if self.current_conversation_id:
try:
self.conversation_manager.update_conversation_metadata(self.current_conversation_id, updates)
target_manager = (
self._get_conversation_manager_for_id(self.current_conversation_id)
if hasattr(self, "_get_conversation_manager_for_id")
else self.conversation_manager
)
target_manager.update_conversation_metadata(self.current_conversation_id, updates)
except Exception as exc:
print(f"[ContextCompression] 更新压缩状态失败: {exc}")