mirror of
https://github.com/lintsinghua/DeepAudit.git
synced 2026-08-31 01:40:04 +08:00
fix: Agent审计LLM超时可配置化,解决硬编码30s超时问题
Fixes #138 问题:Agent审计时LLM超时时间硬编码为30秒,用户配置的超时值不生效 修改内容: - 前端:在"系统配置-LLM配置-高级参数"增加完整超时配置(默认展开) - 首Token超时、流式超时、工具超时、子Agent超时、总超时 - 后端:LLMConfigSchema 增加超时配置字段 - 后端:LLMService 增加 get_agent_timeout_config() 方法 - Agent:BaseAgent 从用户配置读取超时值 - Orchestrator:子Agent调度使用配置的超时值 默认值: - 首Token超时: 30s - 流式超时: 60s - 工具超时: 60s - 子Agent超时: 600s (10分钟) - Agent总超时: 1800s (30分钟) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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 "",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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 执行,定期检查取消状态"""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -44,6 +44,9 @@ const DEFAULT_MODELS: Record<string, string> = {
|
||||
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 */}
|
||||
<details className="pt-4 border-t border-border border-dashed">
|
||||
<details open className="pt-4 border-t border-border border-dashed">
|
||||
<summary className="font-bold uppercase cursor-pointer hover:text-primary text-muted-foreground text-sm">高级参数</summary>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
||||
|
||||
{/* LLM基础参数 */}
|
||||
<div className="mt-4 mb-2">
|
||||
<span className="text-xs text-muted-foreground uppercase font-semibold">LLM 基础参数</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase">超时 (毫秒)</Label>
|
||||
<Label className="text-xs text-muted-foreground uppercase">请求超时 (毫秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.llmTimeout}
|
||||
onChange={(e) => updateConfig('llmTimeout', Number(e.target.value))}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">单次LLM请求的超时时间</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase">温度 (0-2)</Label>
|
||||
@@ -602,6 +633,7 @@ export function SystemConfig() {
|
||||
onChange={(e) => updateConfig('llmTemperature', Number(e.target.value))}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">控制输出随机性,越低越确定</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase">最大 Tokens</Label>
|
||||
@@ -611,6 +643,64 @@ export function SystemConfig() {
|
||||
onChange={(e) => updateConfig('llmMaxTokens', Number(e.target.value))}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">单次请求最大输出Token数</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Agent超时配置 */}
|
||||
<div className="mt-6 mb-2">
|
||||
<span className="text-xs text-muted-foreground uppercase font-semibold">Agent 超时配置</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase">首Token超时 (秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.llmFirstTokenTimeout}
|
||||
onChange={(e) => updateConfig('llmFirstTokenTimeout', Number(e.target.value))}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">等待LLM首个Token的超时时间</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase">流式超时 (秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.llmStreamTimeout}
|
||||
onChange={(e) => updateConfig('llmStreamTimeout', Number(e.target.value))}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">流式输出中两个Token间的超时</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase">工具超时 (秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.toolTimeout}
|
||||
onChange={(e) => updateConfig('toolTimeout', Number(e.target.value))}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">单个工具执行的默认超时时间</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase">子Agent超时 (秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.subAgentTimeout}
|
||||
onChange={(e) => updateConfig('subAgentTimeout', Number(e.target.value))}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">子Agent (Recon/Analysis/Verification) 超时</p>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs text-muted-foreground uppercase">总超时 (秒)</Label>
|
||||
<Input
|
||||
type="number"
|
||||
value={config.agentTimeout}
|
||||
onChange={(e) => updateConfig('agentTimeout', Number(e.target.value))}
|
||||
className="h-10 cyber-input"
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">整个Agent审计任务的最大时间</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
Reference in New Issue
Block a user