audit_middleware.py 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. # -*- coding: utf-8 -*-
  2. """
  3. 日志审计中间件
  4. 功能:
  5. - 监听模型调用,在控制台打印审计日志
  6. - 支持通过环境变量开关控制
  7. - 打印内容:已消耗 token 数、本次调用传入的上下文
  8. 使用方法:
  9. 1. 在 .env 中设置 AUDIT_LOG_ENABLED=true 启用
  10. 2. 在 create_deep_agent 的 middleware 参数中传入
  11. """
  12. from typing import Any, Dict, Optional
  13. from langgraph.utils.runnable import RunnableCallable
  14. def create_audit_middleware() -> RunnableCallable:
  15. """
  16. 创建审计中间件(符合 DeepAgents 协议)
  17. Returns:
  18. RunnableCallable: 包装后的可调用对象
  19. """
  20. # 从环境变量读取配置
  21. from dotenv import dotenv_values
  22. from pathlib import Path
  23. _env_file = Path(__file__).parent.parent / ".env"
  24. _cfg = dotenv_values(str(_env_file))
  25. enabled = _cfg.get("AUDIT_LOG_ENABLED", "false").lower() == "true"
  26. total_tokens = [0] # 使用列表以在闭包中修改
  27. async def audit_wrapper(input: Any, config: Optional[Dict] = None, **kwargs: Any) -> Any:
  28. """审计包装器"""
  29. if not enabled:
  30. # 如果未启用,直接调用下一个步骤
  31. from langgraph.utils.runnable import Runnable
  32. next_step = kwargs.get("_next") or kwargs.get("next_step")
  33. if next_step:
  34. return await next_step.ainvoke(input, config=config, **kwargs)
  35. return input
  36. # 提取输入消息
  37. messages = _extract_messages(input)
  38. # 打印上下文信息
  39. _log_context(messages)
  40. # 获取下一个步骤
  41. next_step = kwargs.pop("_next", None)
  42. if next_step is None:
  43. from langgraph.utils.runnable import Runnable
  44. for key in kwargs:
  45. if isinstance(kwargs[key], Runnable):
  46. next_step = kwargs.pop(key)
  47. break
  48. if next_step is None:
  49. return input
  50. # 调用下一个步骤
  51. result = await next_step.ainvoke(input, config=config, **kwargs)
  52. # 尝试提取并打印 token 使用情况
  53. _log_tokens(result, total_tokens)
  54. return result
  55. return RunnableCallable(ainvoke=audit_wrapper)
  56. def _extract_messages(input: Any) -> list:
  57. """从输入中提取消息列表"""
  58. if isinstance(input, dict):
  59. messages = input.get("messages", [])
  60. if isinstance(messages, list):
  61. return messages
  62. elif isinstance(input, list):
  63. return input
  64. return []
  65. def _log_context(messages: list):
  66. """打印上下文信息"""
  67. print("\n" + "=" * 60)
  68. print("【审计日志】模型调用上下文")
  69. print("=" * 60)
  70. for i, msg in enumerate(messages):
  71. role = getattr(msg, "role", "unknown")
  72. content = getattr(msg, "content", str(msg))
  73. # 限制内容长度
  74. if len(str(content)) > 500:
  75. content = str(content)[:500] + "...(截断)"
  76. print(f"\n[消息 {i+1}] 角色: {role}")
  77. print(f"内容: {content}")
  78. print("\n" + "-" * 60)
  79. def _log_tokens(result: Any, total_tokens_ref: list):
  80. """打印 token 使用情况"""
  81. # 尝试从结果中提取 token 信息
  82. token_info = {}
  83. total_tokens = total_tokens_ref[0]
  84. # 尝试从 AIMessage 响应中提取
  85. if hasattr(result, "response_metadata"):
  86. metadata = result.response_metadata
  87. if hasattr(metadata, "get"):
  88. usage = metadata.get("usage", {})
  89. if isinstance(usage, dict):
  90. token_info["prompt_tokens"] = usage.get("prompt_tokens", 0)
  91. token_info["completion_tokens"] = usage.get("completion_tokens", 0)
  92. token_info["total_tokens"] = usage.get("total_tokens", 0)
  93. # 更新累计消耗
  94. if token_info.get("total_tokens"):
  95. total_tokens_ref[0] = total_tokens + token_info["total_tokens"]
  96. print(f"【审计日志】Token 使用情况:")
  97. print(f" - 本次 Prompt Tokens: {token_info.get('prompt_tokens', 'N/A')}")
  98. print(f" - 本次 Completion Tokens: {token_info.get('completion_tokens', 'N/A')}")
  99. print(f" - 本次 Total Tokens: {token_info.get('total_tokens', 'N/A')}")
  100. print(f" - 累计消耗 Tokens: {total_tokens_ref[0]}")
  101. print("=" * 60 + "\n")