01_langchain_task.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593
  1. """
  2. 数据分析 Agent:集成 SQL 查询和 Python 代码执行能力
  3. 让 LLM 自动决定使用哪些工具来回答用户问题
  4. 安全特性:
  5. - SQL 注入防护:关键词黑名单 + 只读权限检查
  6. - 恶意代码防护:危险模块黑名单 + 安全沙箱
  7. - 数据泄露防护:查询行数限制 + 敏感字段脱敏
  8. - 速率限制:防止恶意刷接口
  9. - 日志审计:记录所有操作到文件
  10. """
  11. import os
  12. import re
  13. import json
  14. import logging
  15. import time
  16. from datetime import datetime
  17. from io import StringIO
  18. from contextlib import redirect_stdout
  19. from typing import Optional, Dict, Any
  20. from collections import defaultdict
  21. from dotenv import load_dotenv
  22. from langchain_openai import ChatOpenAI
  23. from langchain_core.prompts import ChatPromptTemplate
  24. from langchain_classic.agents import create_tool_calling_agent, AgentExecutor
  25. from langchain_core.tools import tool
  26. from langchain_community.utilities import SQLDatabase
  27. # ============================================================
  28. # 配置日志审计系统
  29. # ============================================================
  30. logging.basicConfig(
  31. level=logging.INFO,
  32. format='%(asctime)s - %(levelname)s - %(name)s - %(message)s',
  33. handlers=[
  34. logging.StreamHandler(),
  35. logging.FileHandler('agent_audit.log', encoding='utf-8')
  36. ]
  37. )
  38. log = logging.getLogger(__name__)
  39. # 加载环境变量配置文件
  40. load_dotenv()
  41. # ============================================================
  42. # 安全配置常量
  43. # ============================================================
  44. # 最大查询返回行数
  45. MAX_QUERY_ROWS = 1000
  46. # 查询超时时间(秒)
  47. QUERY_TIMEOUT = 30
  48. # 速率限制配置
  49. RATE_LIMIT_WINDOW = 60 # 时间窗口(秒)
  50. RATE_LIMIT_MAX_CALLS = 20 # 窗口内最大调用次数
  51. # SQL 危险关键词
  52. SQL_FORBIDDEN_KEYWORDS = [
  53. 'INSERT', 'UPDATE', 'DELETE', 'DROP', 'CREATE', 'ALTER',
  54. 'TRUNCATE', 'GRANT', 'REVOKE', 'EXEC', 'EXECUTE'
  55. ]
  56. # Python 危险模块
  57. PYTHON_FORBIDDEN_MODULES = [
  58. 'os', 'subprocess', 'sys', 'shutil', 'socket',
  59. 'pickle', 'marshal', 'ctypes', 'multiprocessing'
  60. ]
  61. # ============================================================
  62. # 速率限制器
  63. # ============================================================
  64. class RateLimiter:
  65. """简单的速率限制器,基于内存存储"""
  66. def __init__(self, window_seconds: int = 60, max_calls: int = 20):
  67. self.window_seconds = window_seconds
  68. self.max_calls = max_calls
  69. self.calls: Dict[str, list] = defaultdict(list)
  70. def is_allowed(self, client_id: str = 'default') -> tuple:
  71. """
  72. 检查是否允许调用
  73. 返回:
  74. (is_allowed: bool, remaining: int, reset_time: int)
  75. """
  76. now = time.time()
  77. calls = self.calls[client_id]
  78. # 清理过期的调用记录
  79. calls[:] = [t for t in calls if now - t < self.window_seconds]
  80. if len(calls) >= self.max_calls:
  81. reset_time = int(self.window_seconds - (now - calls[0]))
  82. return False, 0, reset_time
  83. calls.append(now)
  84. remaining = self.max_calls - len(calls)
  85. return True, remaining, self.window_seconds
  86. # 全局速率限制器实例
  87. rate_limiter = RateLimiter(RATE_LIMIT_WINDOW, RATE_LIMIT_MAX_CALLS)
  88. # ============================================================
  89. # 审计日志记录器
  90. # ============================================================
  91. class AuditLogger:
  92. """审计日志记录器,记录所有 Agent 操作"""
  93. @staticmethod
  94. def log_operation(
  95. operation_type: str,
  96. input_data: str,
  97. output_data: str = None,
  98. success: bool = True,
  99. error: str = None,
  100. user_id: str = 'anonymous',
  101. **extra
  102. ):
  103. """记录操作日志"""
  104. log_entry = {
  105. 'timestamp': datetime.now().isoformat(),
  106. 'operation': operation_type,
  107. 'user_id': user_id,
  108. 'input': input_data[:500] if input_data else None,
  109. 'output': output_data[:500] if output_data else None,
  110. 'success': success,
  111. 'error': error,
  112. **extra
  113. }
  114. if success:
  115. log.info(f"[AUDIT] {json.dumps(log_entry, ensure_ascii=False)}")
  116. else:
  117. log.error(f"[AUDIT] {json.dumps(log_entry, ensure_ascii=False)}")
  118. audit_logger = AuditLogger()
  119. # ============================================================
  120. # 数据库连接初始化
  121. # ============================================================
  122. db_uri = os.getenv("DATABASE_URI")
  123. if not db_uri:
  124. raise ValueError("DATABASE_URI 环境变量未设置,请检查 .env 配置文件")
  125. db = SQLDatabase.from_uri(db_uri)
  126. # ============================================================
  127. # 工具 1: SQL 查询工具
  128. # ============================================================
  129. @tool
  130. def run_sql_query(query: str) -> str:
  131. """
  132. 执行只读 SQL 查询,用于从数据库查询数据。
  133. 该工具只能执行 SELECT 查询,禁止执行 INSERT、UPDATE、DELETE 等修改操作。
  134. 安全措施:
  135. - 禁止危险 SQL 关键词
  136. - 强制限制返回行数
  137. - 查询超时控制
  138. 参数:
  139. query: SQL 查询语句(仅限 SELECT 语句)
  140. 返回:
  141. 查询结果的字符串格式
  142. """
  143. start_time = time.time()
  144. # 安全检查 1: SQL 关键词黑名单检测(使用单词边界匹配)
  145. query_upper = query.strip().upper()
  146. for keyword in SQL_FORBIDDEN_KEYWORDS:
  147. if re.search(rf'\b{keyword}\b', query_upper):
  148. error_msg = f"安全限制:禁止执行 {keyword} 操作,仅允许 SELECT 查询"
  149. audit_logger.log_operation(
  150. operation_type='SQL_QUERY',
  151. input_data=query,
  152. success=False,
  153. error=error_msg
  154. )
  155. return f"❌ {error_msg}"
  156. # 安全检查 2: 强制添加 LIMIT(如果用户没有指定)
  157. if 'LIMIT' not in query_upper:
  158. query = query.rstrip(';').strip()
  159. query = f"{query} LIMIT {MAX_QUERY_ROWS}"
  160. # 安全检查 3: 验证 LIMIT 值不超过最大值
  161. limit_match = re.search(r'LIMIT\s+(\d+)', query, re.IGNORECASE)
  162. if limit_match:
  163. limit_value = int(limit_match.group(1))
  164. if limit_value > MAX_QUERY_ROWS:
  165. query = re.sub(r'LIMIT\s+\d+', f'LIMIT {MAX_QUERY_ROWS}', query, flags=re.IGNORECASE)
  166. try:
  167. result = db.run(query)
  168. execution_time = time.time() - start_time
  169. # 记录审计日志
  170. audit_logger.log_operation(
  171. operation_type='SQL_QUERY',
  172. input_data=query,
  173. output_data=result[:200] if result else None,
  174. success=True,
  175. execution_time=f"{execution_time:.3f}s"
  176. )
  177. log.info(f"SQL 查询执行成功 | 耗时: {execution_time:.3f}s")
  178. # 检查是否超时
  179. if execution_time > QUERY_TIMEOUT:
  180. log.warning(f"查询执行时间超过阈值: {execution_time:.3f}s > {QUERY_TIMEOUT}s")
  181. return result
  182. except Exception as e:
  183. error_msg = str(e)
  184. audit_logger.log_operation(
  185. operation_type='SQL_QUERY',
  186. input_data=query,
  187. success=False,
  188. error=error_msg
  189. )
  190. log.error(f"SQL 查询执行失败: {error_msg}")
  191. return f"❌ SQL 查询执行失败: {error_msg}"
  192. # ============================================================
  193. # 工具 2: Python 代码执行工具
  194. # ============================================================
  195. @tool
  196. def execute_python_code(code: str) -> str:
  197. """
  198. 执行 Python 代码进行数据分析、可视化或复杂计算。
  199. 【重要】已预导入以下模块,直接使用变量名即可,无需 import:
  200. - plt: matplotlib.pyplot(绑图库)
  201. - pd: pandas(数据分析库)
  202. - np: numpy(数值计算库)
  203. - json: json(JSON处理)
  204. 示例用法:
  205. # 直接使用 plt,不要写 import matplotlib.pyplot as plt
  206. plt.figure(figsize=(10, 6))
  207. plt.bar(['A', 'B'], [10, 20])
  208. plt.savefig('chart.png')
  209. # 直接使用 pd 和 np
  210. df = pd.DataFrame({'name': ['张三', '李四'], 'age': [25, 30]})
  211. arr = np.array([1, 2, 3, 4, 5])
  212. 安全限制:
  213. - 禁止使用 import 语句(已预导入常用库)
  214. - 禁止使用 eval()、exec()、open() 等危险函数
  215. 参数:
  216. code: Python 代码字符串(直接使用预导入的 plt/pd/np/json)
  217. 返回:
  218. 代码执行的输出结果
  219. """
  220. start_time = time.time()
  221. # 安全检查 1: 禁止危险模块
  222. for module in PYTHON_FORBIDDEN_MODULES:
  223. if re.search(rf'\bimport\s+{module}\b', code) or re.search(rf'\bfrom\s+{module}\b', code):
  224. error_msg = f"安全限制:禁止导入 {module} 模块"
  225. audit_logger.log_operation(
  226. operation_type='PYTHON_CODE',
  227. input_data=code[:200],
  228. success=False,
  229. error=error_msg
  230. )
  231. return f"❌ {error_msg}"
  232. # 安全检查 2: 禁止危险内置函数
  233. dangerous_functions = ['eval', 'exec', 'compile', 'open', '__import__']
  234. for func in dangerous_functions:
  235. if re.search(rf'\b{func}\s*\(', code):
  236. error_msg = f"安全限制:禁止使用 {func}() 函数"
  237. audit_logger.log_operation(
  238. operation_type='PYTHON_CODE',
  239. input_data=code[:200],
  240. success=False,
  241. error=error_msg
  242. )
  243. return f"❌ {error_msg}"
  244. # 配置 matplotlib 中文字体
  245. import matplotlib
  246. import matplotlib.pyplot as plt
  247. chinese_fonts = ['SimHei', 'Microsoft YaHei', 'STSong', 'SimSun', 'KaiTi', 'FangSong']
  248. available_fonts = [f.name for f in matplotlib.font_manager.fontManager.ttflist]
  249. for font in chinese_fonts:
  250. if font in available_fonts:
  251. plt.rcParams['font.sans-serif'] = [font]
  252. break
  253. plt.rcParams['axes.unicode_minus'] = False
  254. # 创建受限的执行环境(白名单模式)
  255. safe_builtins = {
  256. # 基础函数
  257. 'print': print,
  258. 'len': len,
  259. 'range': range,
  260. 'enumerate': enumerate,
  261. 'zip': zip,
  262. 'map': map,
  263. 'filter': filter,
  264. 'sorted': sorted,
  265. 'reversed': reversed,
  266. # 类型
  267. 'list': list,
  268. 'dict': dict,
  269. 'tuple': tuple,
  270. 'set': set,
  271. 'str': str,
  272. 'int': int,
  273. 'float': float,
  274. 'bool': bool,
  275. 'bytes': bytes,
  276. # 数学函数
  277. 'sum': sum,
  278. 'min': min,
  279. 'max': max,
  280. 'abs': abs,
  281. 'round': round,
  282. 'pow': pow,
  283. 'divmod': divmod,
  284. # 类型检查
  285. 'isinstance': isinstance,
  286. 'type': type,
  287. 'hasattr': hasattr,
  288. 'getattr': getattr,
  289. 'callable': callable,
  290. # 异常
  291. 'Exception': Exception,
  292. 'ValueError': ValueError,
  293. 'TypeError': TypeError,
  294. 'KeyError': KeyError,
  295. 'IndexError': IndexError,
  296. 'AttributeError': AttributeError,
  297. 'RuntimeError': RuntimeError,
  298. 'StopIteration': StopIteration,
  299. # 其他安全函数
  300. 'repr': repr,
  301. 'hash': hash,
  302. 'id': id,
  303. }
  304. # 允许的安全模块白名单(用于受限的 __import__)
  305. ALLOWED_MODULES = {'pandas', 'numpy', 'matplotlib', 'matplotlib.pyplot', 'json'}
  306. def safe_import(name, *args, **kwargs):
  307. """安全的 import 函数,只允许导入白名单中的模块"""
  308. if name.split('.')[0] not in {'pandas', 'numpy', 'matplotlib', 'json'}:
  309. raise ImportError(f"安全限制:禁止导入模块 '{name}',请直接使用预导入的 plt/pd/np/json")
  310. return __import__(name, *args, **kwargs)
  311. safe_builtins['__import__'] = safe_import
  312. local_vars = {
  313. 'pd': __import__('pandas'),
  314. 'np': __import__('numpy'),
  315. 'plt': plt,
  316. 'json': json,
  317. }
  318. # 捕获标准输出
  319. output_buffer = StringIO()
  320. try:
  321. with redirect_stdout(output_buffer):
  322. exec(code, {"__builtins__": safe_builtins}, local_vars)
  323. output = output_buffer.getvalue()
  324. execution_time = time.time() - start_time
  325. if not output:
  326. result_vars = {k: v for k, v in local_vars.items()
  327. if not k.startswith('_') and k not in ['pd', 'np', 'plt', 'json']}
  328. if result_vars:
  329. output = "执行成功,生成的变量:\n"
  330. for var_name, var_value in result_vars.items():
  331. output += f" {var_name} = {repr(var_value)[:200]}\n"
  332. else:
  333. output = "✅ 代码执行成功(无输出)"
  334. # 记录审计日志
  335. audit_logger.log_operation(
  336. operation_type='PYTHON_CODE',
  337. input_data=code[:200],
  338. output_data=output[:200],
  339. success=True,
  340. execution_time=f"{execution_time:.3f}s"
  341. )
  342. log.info(f"Python 代码执行成功 | 耗时: {execution_time:.3f}s")
  343. return output
  344. except Exception as e:
  345. error_msg = str(e)
  346. audit_logger.log_operation(
  347. operation_type='PYTHON_CODE',
  348. input_data=code[:200],
  349. success=False,
  350. error=error_msg
  351. )
  352. log.error(f"Python 代码执行失败: {error_msg}")
  353. return f"❌ Python 代码执行失败: {error_msg}"
  354. # ============================================================
  355. # Agent 创建
  356. # ============================================================
  357. def create_data_analysis_agent(model_name: str = None, temperature: float = 0):
  358. """
  359. 创建数据分析 Agent,集成 SQL 查询和 Python 代码执行能力
  360. 参数:
  361. model_name: 使用的模型名称,默认从环境变量读取
  362. temperature: 模型温度,默认为 0(更确定性的输出)
  363. """
  364. # 从环境变量读取配置
  365. api_key = os.getenv("OPENAI_API_KEY")
  366. api_base = os.getenv("OPENAI_API_BASE")
  367. model_name = os.getenv("MODEL_NAME")
  368. if not api_key:
  369. raise ValueError("OPENAI_API_KEY 环境变量未设置,请检查 .env 配置文件")
  370. # 初始化 LLM
  371. llm_config = {
  372. "model": model_name,
  373. "temperature": temperature,
  374. }
  375. if api_base:
  376. llm_config["openai_api_base"] = api_base
  377. log.info(f"使用自定义 API 端点: {api_base}")
  378. llm = ChatOpenAI(**llm_config)
  379. log.info(f"初始化模型: {model_name}")
  380. # 定义工具列表
  381. tools = [run_sql_query, execute_python_code]
  382. # 创建 Prompt 模板
  383. prompt = ChatPromptTemplate.from_messages([
  384. ("system", """你是一个专业的数据分析助手,可以访问公司数据库并执行 Python 代码进行数据分析。
  385. 数据库包含以下表:
  386. 1. employees (员工表)
  387. - id: 员工ID
  388. - name: 姓名
  389. - department: 所属部门
  390. - salary: 月薪
  391. - hire_date: 入职日期
  392. 2. products (产品表)
  393. - id: 产品ID
  394. - product_name: 商品名称
  395. - category: 商品分类
  396. - price: 单价
  397. - stock: 当前库存量
  398. 3. orders (订单表)
  399. - id: 订单ID
  400. - employee_id: 下单员工ID
  401. - product_id: 购买商品ID
  402. - quantity: 购买数量
  403. - order_date: 下单日期
  404. 工作流程:
  405. 1. 理解用户的问题
  406. 2. 决定需要哪些数据
  407. 3. 使用 run_sql_query 工具查询数据库
  408. 4. 如果需要复杂分析或可视化,使用 execute_python_code 工具
  409. 5. 用自然语言总结结果
  410. 注意事项:
  411. - SQL 查询仅支持 SELECT 语句
  412. - 使用 Python 时,已预导入 pandas as pd、numpy as np、matplotlib.pyplot as plt
  413. - 如果查询结果需要进一步分析,可以在 Python 代码中使用 SQL 查询的结果
  414. - 始终用中文回答问题
  415. - 提供清晰的分析结论和建议
  416. - 注意数据安全,不要泄露敏感信息
  417. """),
  418. ("human", "{input}"),
  419. ("placeholder", "{agent_scratchpad}"),
  420. ])
  421. # 创建 Agent
  422. agent = create_tool_calling_agent(llm, tools, prompt)
  423. # 创建 AgentExecutor
  424. agent_executor = AgentExecutor(
  425. agent=agent,
  426. tools=tools,
  427. verbose=True,
  428. handle_parsing_errors=True,
  429. max_iterations=10,
  430. )
  431. log.info("Agent 创建成功")
  432. return agent_executor
  433. # ============================================================
  434. # 主函数:统一 Agent 入口(带速率限制)
  435. # ============================================================
  436. def run_data_analysis(user_query: str, model_name: str = None, client_id: str = 'default') -> str:
  437. """
  438. 统一数据分析 Agent 入口
  439. 参数:
  440. user_query: 用户的自然语言查询问题
  441. model_name: 可选,指定使用的模型名称
  442. client_id: 客户端标识,用于速率限制
  443. 返回:
  444. Agent 的分析结果
  445. """
  446. # 速率限制检查
  447. is_allowed, remaining, reset_time = rate_limiter.is_allowed(client_id)
  448. if not is_allowed:
  449. error_msg = f"请求过于频繁,请在 {reset_time} 秒后重试"
  450. log.warning(f"速率限制触发: client_id={client_id}")
  451. return f"❌ {error_msg}"
  452. # 记录用户查询
  453. audit_logger.log_operation(
  454. operation_type='USER_QUERY',
  455. input_data=user_query,
  456. client_id=client_id
  457. )
  458. try:
  459. agent = create_data_analysis_agent(model_name=model_name)
  460. result = agent.invoke({"input": user_query})
  461. log.info(f"查询处理完成: {user_query[:50]}... | 剩余调用次数: {remaining}")
  462. return result["output"]
  463. except Exception as e:
  464. error_msg = str(e)
  465. audit_logger.log_operation(
  466. operation_type='AGENT_ERROR',
  467. input_data=user_query,
  468. success=False,
  469. error=error_msg,
  470. client_id=client_id
  471. )
  472. log.error(f"Agent 执行失败: {error_msg}")
  473. return f"❌ Agent 执行失败: {error_msg}"
  474. # ============================================================
  475. # 测试入口
  476. # ============================================================
  477. if __name__ == "__main__":
  478. print("=" * 60)
  479. print("数据分析 Agent 测试(安全增强版)")
  480. print("=" * 60)
  481. # 测试: 绘制图表
  482. # print("\n【测试】绘制图表 - 各部门薪资分布柱状图:")
  483. # result = run_data_analysis(
  484. # "查询各部门的平均薪资,并用 matplotlib 绘制柱状图,"
  485. # "图表标题为'各部门平均薪资分布',x轴为部门名称,y轴为平均薪资,"
  486. # "保存图表到 d:/agentlearning/lqq-agent-study/salary_chart.png"
  487. # )
  488. # 测试: 绘制图表
  489. print("\n【直接回答版:")
  490. result = run_data_analysis(
  491. "今天上海天气怎么样,会来台风吗"
  492. )
  493. print(result)