diff --git a/backend/app/api/v1/endpoints/config.py b/backend/app/api/v1/endpoints/config.py index 0423f51..38e1b43 100644 --- a/backend/app/api/v1/endpoints/config.py +++ b/backend/app/api/v1/endpoints/config.py @@ -55,7 +55,14 @@ class LLMConfigSchema(BaseModel): llmTemperature: Optional[float] = None llmMaxTokens: Optional[int] = None llmCustomHeaders: Optional[str] = None - + + # Agent超时配置 + llmFirstTokenTimeout: Optional[int] = None # 首Token超时(秒) + llmStreamTimeout: Optional[int] = None # 流式超时(秒) + agentTimeout: Optional[int] = None # Agent总超时(秒) + subAgentTimeout: Optional[int] = None # 子Agent超时(秒) + toolTimeout: Optional[int] = None # 工具执行超时(秒) + # 平台专用配置 geminiApiKey: Optional[str] = None openaiApiKey: Optional[str] = None @@ -111,6 +118,13 @@ def get_default_config() -> dict: "llmTemperature": settings.LLM_TEMPERATURE, "llmMaxTokens": settings.LLM_MAX_TOKENS, "llmCustomHeaders": "", + # Agent超时配置(秒) + "llmFirstTokenTimeout": getattr(settings, 'LLM_FIRST_TOKEN_TIMEOUT', 30), + "llmStreamTimeout": getattr(settings, 'LLM_STREAM_TIMEOUT', 60), + "agentTimeout": settings.AGENT_TIMEOUT_SECONDS, + "subAgentTimeout": getattr(settings, 'SUB_AGENT_TIMEOUT_SECONDS', 600), + "toolTimeout": getattr(settings, 'TOOL_TIMEOUT_SECONDS', 60), + # 平台专用配置 "geminiApiKey": settings.GEMINI_API_KEY or "", "openaiApiKey": settings.OPENAI_API_KEY or "", "claudeApiKey": settings.CLAUDE_API_KEY or "", diff --git a/backend/app/core/config.py b/backend/app/core/config.py index 2db4ae8..e9784a7 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -44,6 +44,12 @@ class Settings(BaseSettings): LLM_TIMEOUT: int = 150 # 超时时间(秒) LLM_TEMPERATURE: float = 0.1 LLM_MAX_TOKENS: int = 4096 + + # Agent 流式超时配置(秒) + LLM_FIRST_TOKEN_TIMEOUT: int = 30 # 等待首个Token的超时时间 + LLM_STREAM_TIMEOUT: int = 60 # 流式输出中两个Token之间的超时时间 + SUB_AGENT_TIMEOUT_SECONDS: int = 600 # 子Agent超时时间(10分钟) + TOOL_TIMEOUT_SECONDS: int = 60 # 工具执行默认超时时间 # 各LLM提供商的API Key配置(兼容单独配置) OPENAI_API_KEY: Optional[str] = None diff --git a/backend/app/services/agent/agents/base.py b/backend/app/services/agent/agents/base.py index bdc5188..f41e2e1 100644 --- a/backend/app/services/agent/agents/base.py +++ b/backend/app/services/agent/agents/base.py @@ -297,6 +297,9 @@ class BaseAgent(ABC): self._total_tokens = 0 self._tool_calls = 0 self._cancelled = False + + # 获取超时配置 + self._timeout_config = self._get_timeout_config() # 🔥 协作状态 self._incoming_handoff: Optional[TaskHandoff] = None @@ -347,19 +350,43 @@ class BaseAgent(ABC): """加载知识模块到系统提示词""" if not self.knowledge_modules: return - + try: from ..knowledge import knowledge_loader - + enhanced_prompt = knowledge_loader.build_system_prompt_with_modules( self.config.system_prompt or "", self.knowledge_modules, ) self.config.system_prompt = enhanced_prompt - + logger.info(f"[{self.name}] Loaded knowledge modules: {self.knowledge_modules}") except Exception as e: logger.warning(f"Failed to load knowledge modules: {e}") + + def _get_timeout_config(self) -> Dict[str, int]: + """ + 获取超时配置(秒) + + 优先级:用户配置 > 环境变量默认值 + + Returns: + 包含各种超时配置的字典 + """ + from app.core.config import settings + + # 尝试从 llm_service 获取用户配置的超时值 + if hasattr(self.llm_service, 'get_agent_timeout_config'): + return self.llm_service.get_agent_timeout_config() + + # 回退到环境变量默认值 + return { + 'llm_first_token_timeout': getattr(settings, 'LLM_FIRST_TOKEN_TIMEOUT', 30), + 'llm_stream_timeout': getattr(settings, 'LLM_STREAM_TIMEOUT', 60), + 'agent_timeout': getattr(settings, 'AGENT_TIMEOUT_SECONDS', 1800), + 'sub_agent_timeout': getattr(settings, 'SUB_AGENT_TIMEOUT_SECONDS', 600), + 'tool_timeout': getattr(settings, 'TOOL_TIMEOUT_SECONDS', 60), + } @property def name(self) -> str: @@ -983,10 +1010,12 @@ class BaseAgent(ABC): break try: - # 🔥 第一個 token 30秒超时,后续 token 60秒超时 - # 这是一个应用层的安全网,防止底层 LLM 客户端挂死 - timeout = 30.0 if not first_token_received else 60.0 - + # 🔥 使用用户配置的超时时间 + # 第一个 token 使用首Token超时,后续 token 使用流式超时 + first_token_timeout = float(self._timeout_config.get('llm_first_token_timeout', 30)) + stream_timeout = float(self._timeout_config.get('llm_stream_timeout', 60)) + timeout = first_token_timeout if not first_token_received else stream_timeout + chunk = await asyncio.wait_for(iterator.__anext__(), timeout=timeout) last_activity = time.time() @@ -1110,7 +1139,9 @@ class BaseAgent(ABC): "sql_injection_test": 30, "xss_test": 30, } - timeout = tool_timeouts.get(tool_name, 30) # 默认30秒 + # 🔥 使用用户配置的默认工具超时时间 + default_tool_timeout = self._timeout_config.get('tool_timeout', 60) + timeout = tool_timeouts.get(tool_name, default_tool_timeout) # 🔥 使用 asyncio.wait_for 添加超时控制,同时支持取消 async def execute_with_cancel_check(): diff --git a/backend/app/services/agent/agents/orchestrator.py b/backend/app/services/agent/agents/orchestrator.py index 1e3c8c1..48e8d4f 100644 --- a/backend/app/services/agent/agents/orchestrator.py +++ b/backend/app/services/agent/agents/orchestrator.py @@ -709,13 +709,15 @@ Action Input: {{"参数": "值"}} return f"## {agent_name} Agent 执行取消\n\n任务已被用户取消" # 🔥 执行子 Agent - 支持取消和超时 - # 设置子 Agent 超时(根据 Agent 类型) + # 使用用户配置的子Agent超时时间 + default_sub_agent_timeout = self._timeout_config.get('sub_agent_timeout', 600) + # 设置子 Agent 超时(根据 Agent 类型,recon稍短) agent_timeouts = { - "recon": 300, # 5 分钟 - "analysis": 600, # 10 分钟 - "verification": 600, # 10 分钟 + "recon": min(300, default_sub_agent_timeout), # recon 通常较快 + "analysis": default_sub_agent_timeout, + "verification": default_sub_agent_timeout, } - timeout = agent_timeouts.get(agent_name, 300) + timeout = agent_timeouts.get(agent_name, default_sub_agent_timeout) async def run_with_cancel_check(): """包装子 Agent 执行,定期检查取消状态""" diff --git a/backend/app/services/llm/service.py b/backend/app/services/llm/service.py index 088413e..5a32234 100644 --- a/backend/app/services/llm/service.py +++ b/backend/app/services/llm/service.py @@ -23,16 +23,33 @@ logger = logging.getLogger(__name__) class LLMService: """LLM服务类""" - + def __init__(self, user_config: Optional[Dict[str, Any]] = None): """ 初始化LLM服务 - + Args: user_config: 用户配置字典,包含llmConfig字段 """ self._config: Optional[LLMConfig] = None self._user_config = user_config or {} + + def get_agent_timeout_config(self) -> Dict[str, int]: + """ + 获取Agent超时配置(秒) + + Returns: + 包含各种超时配置的字典 + """ + user_llm_config = self._user_config.get('llmConfig', {}) + + return { + 'llm_first_token_timeout': user_llm_config.get('llmFirstTokenTimeout') or getattr(settings, 'LLM_FIRST_TOKEN_TIMEOUT', 30), + 'llm_stream_timeout': user_llm_config.get('llmStreamTimeout') or getattr(settings, 'LLM_STREAM_TIMEOUT', 60), + 'agent_timeout': user_llm_config.get('agentTimeout') or getattr(settings, 'AGENT_TIMEOUT_SECONDS', 1800), + 'sub_agent_timeout': user_llm_config.get('subAgentTimeout') or getattr(settings, 'SUB_AGENT_TIMEOUT_SECONDS', 600), + 'tool_timeout': user_llm_config.get('toolTimeout') or getattr(settings, 'TOOL_TIMEOUT_SECONDS', 60), + } @property def config(self) -> LLMConfig: diff --git a/frontend/src/components/system/SystemConfig.tsx b/frontend/src/components/system/SystemConfig.tsx index 405ad5c..02cff4f 100644 --- a/frontend/src/components/system/SystemConfig.tsx +++ b/frontend/src/components/system/SystemConfig.tsx @@ -44,6 +44,9 @@ const DEFAULT_MODELS: Record = { interface SystemConfigData { llmProvider: string; llmApiKey: string; llmModel: string; llmBaseUrl: string; llmTimeout: number; llmTemperature: number; llmMaxTokens: number; + // Agent超时配置 + llmFirstTokenTimeout: number; llmStreamTimeout: number; + agentTimeout: number; subAgentTimeout: number; toolTimeout: number; githubToken: string; gitlabToken: string; giteaToken: string; maxAnalyzeFiles: number; llmConcurrency: number; llmGapMs: number; outputLanguage: string; } @@ -89,6 +92,12 @@ export function SystemConfig() { llmTimeout: llmConfig.llmTimeout || 150000, llmTemperature: llmConfig.llmTemperature ?? 0.1, llmMaxTokens: llmConfig.llmMaxTokens || 4096, + // Agent超时配置 + llmFirstTokenTimeout: llmConfig.llmFirstTokenTimeout || 30, + llmStreamTimeout: llmConfig.llmStreamTimeout || 60, + agentTimeout: llmConfig.agentTimeout || 1800, + subAgentTimeout: llmConfig.subAgentTimeout || 600, + toolTimeout: llmConfig.toolTimeout || 60, githubToken: otherConfig.githubToken || '', gitlabToken: otherConfig.gitlabToken || '', giteaToken: otherConfig.giteaToken || '', @@ -111,6 +120,8 @@ export function SystemConfig() { setConfig({ llmProvider: 'openai', llmApiKey: '', llmModel: '', llmBaseUrl: '', llmTimeout: 150000, llmTemperature: 0.1, llmMaxTokens: 4096, + llmFirstTokenTimeout: 30, llmStreamTimeout: 60, + agentTimeout: 1800, subAgentTimeout: 600, toolTimeout: 60, githubToken: '', gitlabToken: '', giteaToken: '', maxAnalyzeFiles: 0, llmConcurrency: 3, llmGapMs: 2000, outputLanguage: 'zh-CN', }); @@ -120,6 +131,8 @@ export function SystemConfig() { setConfig({ llmProvider: 'openai', llmApiKey: '', llmModel: '', llmBaseUrl: '', llmTimeout: 150000, llmTemperature: 0.1, llmMaxTokens: 4096, + llmFirstTokenTimeout: 30, llmStreamTimeout: 60, + agentTimeout: 1800, subAgentTimeout: 600, toolTimeout: 60, githubToken: '', gitlabToken: '', giteaToken: '', maxAnalyzeFiles: 0, llmConcurrency: 3, llmGapMs: 2000, outputLanguage: 'zh-CN', }); @@ -230,6 +243,12 @@ export function SystemConfig() { llmModel: config.llmModel, llmBaseUrl: config.llmBaseUrl, llmTimeout: config.llmTimeout, llmTemperature: config.llmTemperature, llmMaxTokens: config.llmMaxTokens, + // Agent超时配置 + llmFirstTokenTimeout: config.llmFirstTokenTimeout, + llmStreamTimeout: config.llmStreamTimeout, + agentTimeout: config.agentTimeout, + subAgentTimeout: config.subAgentTimeout, + toolTimeout: config.toolTimeout, }, otherConfig: { githubToken: config.githubToken, gitlabToken: config.gitlabToken, giteaToken: config.giteaToken, @@ -249,6 +268,12 @@ export function SystemConfig() { llmTimeout: llmConfig.llmTimeout || 150000, llmTemperature: llmConfig.llmTemperature ?? 0.1, llmMaxTokens: llmConfig.llmMaxTokens || 4096, + // Agent超时配置 + llmFirstTokenTimeout: llmConfig.llmFirstTokenTimeout || 30, + llmStreamTimeout: llmConfig.llmStreamTimeout || 60, + agentTimeout: llmConfig.agentTimeout || 1800, + subAgentTimeout: llmConfig.subAgentTimeout || 600, + toolTimeout: llmConfig.toolTimeout || 60, githubToken: otherConfig.githubToken || '', gitlabToken: otherConfig.gitlabToken || '', giteaToken: otherConfig.giteaToken || '', @@ -579,17 +604,23 @@ export function SystemConfig() { )} {/* Advanced Parameters */} -
+
高级参数 -
+ + {/* LLM基础参数 */} +
+ LLM 基础参数 +
+
- + updateConfig('llmTimeout', Number(e.target.value))} className="h-10 cyber-input" /> +

单次LLM请求的超时时间

@@ -602,6 +633,7 @@ export function SystemConfig() { onChange={(e) => updateConfig('llmTemperature', Number(e.target.value))} className="h-10 cyber-input" /> +

控制输出随机性,越低越确定

@@ -611,6 +643,64 @@ export function SystemConfig() { onChange={(e) => updateConfig('llmMaxTokens', Number(e.target.value))} className="h-10 cyber-input" /> +

单次请求最大输出Token数

+
+
+ + {/* Agent超时配置 */} +
+ Agent 超时配置 +
+
+
+ + updateConfig('llmFirstTokenTimeout', Number(e.target.value))} + className="h-10 cyber-input" + /> +

等待LLM首个Token的超时时间

+
+
+ + updateConfig('llmStreamTimeout', Number(e.target.value))} + className="h-10 cyber-input" + /> +

流式输出中两个Token间的超时

+
+
+ + updateConfig('toolTimeout', Number(e.target.value))} + className="h-10 cyber-input" + /> +

单个工具执行的默认超时时间

+
+
+ + updateConfig('subAgentTimeout', Number(e.target.value))} + className="h-10 cyber-input" + /> +

子Agent (Recon/Analysis/Verification) 超时

+
+
+ + updateConfig('agentTimeout', Number(e.target.value))} + className="h-10 cyber-input" + /> +

整个Agent审计任务的最大时间