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:
lintsinghua
2026-01-24 14:09:36 +08:00
parent c725be7f26
commit 551780810c
6 changed files with 179 additions and 19 deletions
+15 -1
View File
@@ -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 "",
+6
View File
@@ -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
+39 -8
View File
@@ -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 执行,定期检查取消状态"""
+19 -2
View File
@@ -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>