22QAAgent:专门处理简单问候与项目/上下文问答
33- 所有回复均通过 LLM 生成,不设规则快捷回复
44- 问候、闲聊、项目能力、帮助等均走 LLM
5- - Ask 模式 :带上下文的 LLM 问答,可选用通用工具(搜索、系统信息、CVE、文件分析)以更准确回答
5+ - 问答 :带上下文的 LLM 问答,可选用通用工具(搜索、系统信息、CVE、文件分析)以更准确回答
66"""
77
88import asyncio
1212from utils .logger import logger
1313
1414
15- # Ask 模式系统提示词(无工具)
16- ASK_SYSTEM_PROMPT = """你是 Hackbot 的 Ask 模式助手。你的任务是**仅根据当前对话上下文**来回答用户的问题。
15+ _AUTH_ERROR_REPLY = "当前推理后端 API Key 无效或已过期,请使用 /model 重新配置后再试。"
16+
17+
18+ def _is_auth_error (error : Exception ) -> bool :
19+ text = f"{ type (error ).__name__ } { error } " .lower ()
20+ return any (
21+ marker in text
22+ for marker in (
23+ "401" ,
24+ "unauthorized" ,
25+ "authentication" ,
26+ "invalid api key" ,
27+ "api key is invalid" ,
28+ "invalid_request_error" ,
29+ )
30+ )
31+
32+
33+ # 问答系统提示词(无工具)
34+ ASK_SYSTEM_PROMPT = """你是 Hackbot 的问答助手。你的任务是**仅根据当前对话上下文**来回答用户的问题。
1735
1836规则:
1937- 仅根据对话上下文中已有的信息来回答
2442- 如果涉及扫描结果、漏洞发现等安全数据,引用上下文中的具体内容
2543- 使用 Markdown 格式化输出以提高可读性"""
2644
27- # Ask 模式系统提示词 (带工具:用于更确切回答)
28- ASK_SYSTEM_PROMPT_WITH_TOOLS = """你是 Hackbot 的 Ask 模式助手 。你的任务是根据当前对话上下文**并结合可选工具**来准确回答用户问题。
45+ # 问答系统提示词 (带工具:用于更确切回答)
46+ ASK_SYSTEM_PROMPT_WITH_TOOLS = """你是 Hackbot 的问答助手 。你的任务是根据当前对话上下文**并结合可选工具**来准确回答用户问题。
2947
3048规则:
3149- 优先根据对话上下文中已有信息回答;若信息不足或用户问题涉及实时/外部数据,可调用工具获取后再回答
3755
3856
3957def get_ask_tools () -> List [Any ]:
40- """返回 Ask 模式可用的通用工具列表 (只读/低敏感:搜索、系统信息、CVE、文件分析)。"""
58+ """返回问答可用的通用工具列表 (只读/低敏感:搜索、系统信息、CVE、文件分析)。"""
4159 from tools .base import BaseTool
4260
4361 tools : List [BaseTool ] = []
4462 try :
4563 from tools .web_search import WebSearchTool
4664 tools .append (WebSearchTool ())
4765 except Exception as e :
48- logger .bind (agent = "qa" , event = "agent_error" , attempt = 1 ).debug (f"Ask 工具 web_search 未加载: { e } " )
66+ logger .bind (agent = "qa" , event = "agent_error" , attempt = 1 ).debug (f"问答工具 web_search 未加载: { e } " )
4967 try :
5068 from tools .defense .system_info_tool import SystemInfoTool
5169 tools .append (SystemInfoTool ())
5270 except Exception as e :
53- logger .bind (agent = "qa" , event = "agent_error" , attempt = 1 ).debug (f"Ask 工具 system_info 未加载: { e } " )
71+ logger .bind (agent = "qa" , event = "agent_error" , attempt = 1 ).debug (f"问答工具 system_info 未加载: { e } " )
5472 try :
5573 from tools .utility .cve_lookup_tool import CveLookupTool
5674 tools .append (CveLookupTool ())
5775 except Exception as e :
58- logger .bind (agent = "qa" , event = "agent_error" , attempt = 1 ).debug (f"Ask 工具 cve_lookup 未加载: { e } " )
76+ logger .bind (agent = "qa" , event = "agent_error" , attempt = 1 ).debug (f"问答工具 cve_lookup 未加载: { e } " )
5977 try :
6078 from tools .utility .file_analyze_tool import FileAnalyzeTool
6179 tools .append (FileAnalyzeTool ())
6280 except Exception as e :
63- logger .bind (agent = "qa" , event = "agent_error" , attempt = 1 ).debug (f"Ask 工具 file_analyze 未加载: { e } " )
81+ logger .bind (agent = "qa" , event = "agent_error" , attempt = 1 ).debug (f"问答工具 file_analyze 未加载: { e } " )
6482 return tools
6583
6684
@@ -69,7 +87,7 @@ class QAAgent(BaseAgent):
6987 问答 Agent:仅做简短回复,不调用工具、不生成执行计划。
7088 用于:问候、闲聊、了解项目能力、了解对话上下文等。
7189
72- Ask 模式 :带上下文的 LLM 问答;可选接入通用工具以更确切回答。
90+ 问答 :带上下文的 LLM 问答;可选接入通用工具以更确切回答。
7391 """
7492
7593 def __init__ (self , name : str = "QAAgent" ):
@@ -81,23 +99,23 @@ def __init__(self, name: str = "QAAgent"):
8199回复应简洁,不要展开长篇说明,不要调用任何工具。"""
82100 super ().__init__ (name = name , system_prompt = system_prompt )
83101 self ._llm = None # 延迟初始化
84- self ._ask_tools : Optional [List [Any ]] = None # Ask 模式通用工具 ,延迟加载
102+ self ._ask_tools : Optional [List [Any ]] = None # 问答通用工具 ,延迟加载
85103 logger .bind (agent = self .name , event = "stage_start" , attempt = 1 ).info ("初始化 QAAgent" )
86104
87105 def _ensure_llm (self ):
88- """延迟创建 LLM 实例(仅 ask 模式需要 )"""
106+ """延迟创建 LLM 实例(仅问答需要 )"""
89107 if self ._llm is None :
90108 try :
91109 from secbot_agent .core .patterns .security_react import _create_llm
92110
93111 self ._llm = _create_llm ()
94- logger .bind (agent = self .name , event = "stage_start" , attempt = 1 ).info ("QAAgent: LLM 实例已创建(用于 Ask 模式 )" )
112+ logger .bind (agent = self .name , event = "stage_start" , attempt = 1 ).info ("QAAgent: LLM 实例已创建(用于问答 )" )
95113 except Exception as e :
96114 logger .bind (agent = self .name , event = "llm_error" , attempt = 1 ).error (f"QAAgent: 创建 LLM 实例失败: { e } " )
97115 raise
98116
99117 def _get_ask_tools_langchain (self ):
100- """返回 Ask 工具经 LangChain 包装后的列表,用于 bind_tools。"""
118+ """返回问答工具经 LangChain 包装后的列表,用于 bind_tools。"""
101119 from secbot_agent .core .agents .tool_calling_agent import LangChainToolWrapper
102120
103121 if self ._ask_tools is None :
@@ -144,22 +162,22 @@ async def answer_with_context(
144162 self ,
145163 user_input : str ,
146164 conversation_history : List [dict ],
165+ context_block : str = "" ,
147166 ) -> str :
148167 """
149- Ask 模式 :带对话上下文的 LLM 问答。
168+ 问答 :带对话上下文的 LLM 问答。
150169 仅根据上下文回答问题,不执行任何动作。
151170
152171 Args:
153172 user_input: 用户当前的问题
154173 conversation_history: 对话历史,格式 [{"role": "user"|"assistant", "content": "..."}]
174+ context_block: ContextAssembler 组装的预算上下文
155175
156176 Returns:
157177 LLM 根据上下文生成的回答
158178 """
159179 from langchain_core .messages import SystemMessage , HumanMessage , AIMessage
160180
161- self ._ensure_llm ()
162-
163181 # 构建消息列表
164182 messages = [SystemMessage (content = ASK_SYSTEM_PROMPT )]
165183
@@ -176,29 +194,41 @@ async def answer_with_context(
176194 content = content [:2000 ] + "\n ... (已截断)"
177195 context_lines .append (f"[{ role_label } ]: { content } " )
178196
179- context_block = "\n \n " .join (context_lines )
197+ conversation_block = "\n \n " .join (context_lines )
180198 messages .append (
181- HumanMessage (content = f"以下是当前对话的上下文记录:\n \n { context_block } " )
199+ HumanMessage (content = f"以下是当前对话的上下文记录:\n \n { conversation_block } " )
182200 )
183201 messages .append (
184202 AIMessage (content = "好的,我已了解当前对话上下文。请问你想了解什么?" )
185203 )
186204
205+ context_block = (context_block or "" ).strip ()
206+ if context_block :
207+ if len (context_block ) > 6000 :
208+ context_block = context_block [:6000 ] + "\n ... (已截断)"
209+ messages .append (
210+ HumanMessage (content = f"以下是当前请求可用的补充上下文:\n \n { context_block } " )
211+ )
212+ messages .append (
213+ AIMessage (content = "好的,我会结合这些补充上下文回答,并避免编造不存在的信息。" )
214+ )
215+
187216 # 用户的实际问题
188217 messages .append (HumanMessage (content = user_input ))
189218
190219 try :
220+ self ._ensure_llm ()
191221 response = await asyncio .wait_for (self ._llm .ainvoke (messages ), timeout = 30.0 )
192222 if isinstance (response , str ):
193223 return response .strip ()
194224 if hasattr (response , "content" ) and response .content is not None :
195225 return str (response .content )
196226 return str (response )
197227 except asyncio .TimeoutError :
198- return "Ask 模式回答超时 ,请稍后重试。"
228+ return "问答回复超时 ,请稍后重试。"
199229 except (AttributeError , TypeError ) as e :
200230 if "model_dump" in str (e ) or "model_dump" in type (e ).__name__ :
201- logger .warning (f"QAAgent ask_with_context 解析触发 model_dump 异常,改用 HTTP 直连回退: { e } " )
231+ logger .warning (f"QAAgent answer_with_context 解析触发 model_dump 异常,改用 HTTP 直连回退: { e } " )
202232 fallback_payload = []
203233 for m in messages :
204234 role = getattr (m , "type" , None ) or "user"
@@ -209,11 +239,17 @@ async def answer_with_context(
209239 else :
210240 fallback_payload .append ({"role" : "user" , "content" : getattr (m , "content" , "" ) or "" })
211241 return await self ._answer_via_http_fallback (fallback_payload )
212- logger .error (f"QAAgent ask_with_context 错误: { e } " )
213- return f"Ask 模式回答出错: { e } "
242+ if _is_auth_error (e ):
243+ logger .warning (f"QAAgent answer_with_context 鉴权失败: { e } " )
244+ return _AUTH_ERROR_REPLY
245+ logger .error (f"QAAgent answer_with_context 错误: { e } " )
246+ return f"问答回复出错: { e } "
214247 except Exception as e :
215- logger .error (f"QAAgent ask_with_context 错误: { e } " )
216- return f"Ask 模式回答出错: { e } "
248+ if _is_auth_error (e ):
249+ logger .warning (f"QAAgent answer_with_context 鉴权失败: { e } " )
250+ return _AUTH_ERROR_REPLY
251+ logger .error (f"QAAgent answer_with_context 错误: { e } " )
252+ return f"问答回复出错: { e } "
217253
218254 async def answer_with_context_and_tools (
219255 self ,
@@ -222,21 +258,27 @@ async def answer_with_context_and_tools(
222258 max_tool_rounds : int = 5 ,
223259 ) -> str :
224260 """
225- Ask 模式 :带对话上下文,并可调用通用工具(搜索、系统信息、CVE、文件分析)以更准确回答。
261+ 问答 :带对话上下文,并可调用通用工具(搜索、系统信息、CVE、文件分析)以更准确回答。
226262 若模型不支持 bind_tools 或无可用工具,则回退到纯 answer_with_context。
227263 """
228264 from langchain_core .messages import SystemMessage , HumanMessage , AIMessage , ToolMessage
229265
230- self ._ensure_llm ()
231- langchain_tools = self ._get_ask_tools_langchain ()
266+ try :
267+ self ._ensure_llm ()
268+ langchain_tools = self ._get_ask_tools_langchain ()
269+ except Exception as e :
270+ if _is_auth_error (e ):
271+ logger .warning (f"QAAgent answer_with_context_and_tools 鉴权失败: { e } " )
272+ return _AUTH_ERROR_REPLY
273+ raise
232274 if not langchain_tools :
233- logger .info ("Ask 模式无可用工具 ,回退到纯上下文问答" )
275+ logger .info ("问答无可用工具 ,回退到纯上下文问答" )
234276 return await self .answer_with_context (user_input , conversation_history )
235277
236278 try :
237279 llm_with_tools = self ._llm .bind_tools (langchain_tools )
238280 except (NotImplementedError , AttributeError , Exception ) as e :
239- logger .info ("Ask 模式 bind_tools 不可用,回退到纯上下文问答: %s" , e )
281+ logger .info ("问答 bind_tools 不可用,回退到纯上下文问答: %s" , e )
240282 return await self .answer_with_context (user_input , conversation_history )
241283
242284 tools_dict : Dict [str , Any ] = {t .name : t for t in langchain_tools }
@@ -297,13 +339,13 @@ async def answer_with_context_and_tools(
297339 result = await tools_dict [tool_name ]._arun (** (tool_args or {}))
298340 tool_results .append (f"工具 { tool_name } 执行结果: { result } " )
299341 except Exception as e :
300- logger .warning ("Ask 工具 %s 执行失败: %s" , tool_name , e )
342+ logger .warning ("问答工具 %s 执行失败: %s" , tool_name , e )
301343 tool_results .append (f"工具 { tool_name } 执行失败: { str (e )} " )
302344 for i , res in enumerate (tool_results ):
303345 messages .append (ToolMessage (content = res , tool_call_id = tool_calls [i ].get ("id" , f"call_{ i } " )))
304346 return (content or "" ).strip () or "抱歉,已达到工具调用轮数上限,未能生成最终回复。"
305347 except asyncio .TimeoutError :
306- return "Ask 模式回答超时 ,请稍后重试。"
348+ return "问答回复超时 ,请稍后重试。"
307349 except (AttributeError , TypeError ) as e :
308350 if "model_dump" in str (e ).lower ():
309351 fallback_payload = [{"role" : "system" , "content" : ASK_SYSTEM_PROMPT_WITH_TOOLS }]
@@ -314,11 +356,17 @@ async def answer_with_context_and_tools(
314356 role = "user"
315357 fallback_payload .append ({"role" : role , "content" : getattr (m , "content" , "" ) or "" })
316358 return await self ._answer_via_http_fallback (fallback_payload )
359+ if _is_auth_error (e ):
360+ logger .warning ("QAAgent answer_with_context_and_tools 鉴权失败: %s" , e )
361+ return _AUTH_ERROR_REPLY
317362 logger .error ("QAAgent answer_with_context_and_tools 错误: %s" , e )
318- return f"Ask 模式回答出错 : { e } "
363+ return f"问答回复出错 : { e } "
319364 except Exception as e :
365+ if _is_auth_error (e ):
366+ logger .warning ("QAAgent answer_with_context_and_tools 鉴权失败: %s" , e )
367+ return _AUTH_ERROR_REPLY
320368 logger .error ("QAAgent answer_with_context_and_tools 错误: %s" , e )
321- return f"Ask 模式回答出错 : { e } "
369+ return f"问答回复出错 : { e } "
322370
323371 @staticmethod
324372 def _extract_ask_response_content (response : Any ) -> str :
@@ -348,8 +396,6 @@ async def _answer_via_llm(
348396 """通过 LLM 生成回复,不设规则快捷回复"""
349397 from langchain_core .messages import SystemMessage , HumanMessage
350398
351- self ._ensure_llm ()
352-
353399 user_content = user_input .strip ()
354400 if context and isinstance (context , list ):
355401 recent = context [- 10 :]
@@ -369,6 +415,7 @@ async def _answer_via_llm(
369415 ]
370416
371417 try :
418+ self ._ensure_llm ()
372419 response = await asyncio .wait_for (
373420 self ._llm .ainvoke (messages ), timeout = 30.0
374421 )
@@ -385,8 +432,14 @@ async def _answer_via_llm(
385432 return await self ._answer_via_http_fallback (
386433 [{"role" : "system" , "content" : self .system_prompt or "" }, {"role" : "user" , "content" : user_content }]
387434 )
435+ if _is_auth_error (e ):
436+ logger .warning (f"QAAgent answer 鉴权失败: { e } " )
437+ return _AUTH_ERROR_REPLY
388438 logger .error (f"QAAgent answer LLM 错误: { e } " )
389439 return f"回复出错: { e } "
390440 except Exception as e :
441+ if _is_auth_error (e ):
442+ logger .warning (f"QAAgent answer 鉴权失败: { e } " )
443+ return _AUTH_ERROR_REPLY
391444 logger .error (f"QAAgent answer LLM 错误: { e } " )
392445 return f"回复出错: { e } "
0 commit comments