feat: Agent 监控与编排、仪表盘/配置页及文档更新
- agent_runtime: orchestrator、core/memory/schemas 调整 - agent_monitoring API、service、agent_llm_log 模型与 database 注册 - 前端 AgentDashboard、AgentConfig、Agents/MainLayout/路由与 AgentChat - 文档:(红头)项目核心文档汇总、自主AI Agent改造完成情况、AI agent改造计划 Made-with: Cursor
This commit is contained in:
@@ -5,6 +5,7 @@ Agent Runtime — 自主 AI Agent 核心运行时。
|
||||
- 工具调用(复用已有 ToolRegistry)
|
||||
- 分层记忆(工作记忆 + 长期记忆)
|
||||
- 多模型(OpenAI / DeepSeek)
|
||||
- 多 Agent 编排(路由/顺序/辩论)
|
||||
- 可嵌入工作流节点或独立运行
|
||||
"""
|
||||
from app.agent_runtime.core import AgentRuntime
|
||||
@@ -14,10 +15,17 @@ from app.agent_runtime.schemas import (
|
||||
AgentLLMConfig,
|
||||
AgentToolConfig,
|
||||
AgentMemoryConfig,
|
||||
AgentStep,
|
||||
)
|
||||
from app.agent_runtime.context import AgentContext
|
||||
from app.agent_runtime.memory import AgentMemory
|
||||
from app.agent_runtime.tool_manager import AgentToolManager
|
||||
from app.agent_runtime.orchestrator import (
|
||||
AgentOrchestrator,
|
||||
OrchestratorAgentConfig,
|
||||
OrchestratorResult,
|
||||
OrchestratorStep,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AgentRuntime",
|
||||
@@ -29,4 +37,9 @@ __all__ = [
|
||||
"AgentContext",
|
||||
"AgentMemory",
|
||||
"AgentToolManager",
|
||||
"AgentStep",
|
||||
"AgentOrchestrator",
|
||||
"OrchestratorAgentConfig",
|
||||
"OrchestratorResult",
|
||||
"OrchestratorStep",
|
||||
]
|
||||
|
||||
@@ -12,11 +12,13 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
import time
|
||||
from typing import Any, Callable, Dict, List, Optional, Protocol, TypedDict
|
||||
|
||||
from app.agent_runtime.schemas import (
|
||||
AgentConfig,
|
||||
AgentResult,
|
||||
AgentStep,
|
||||
)
|
||||
from app.agent_runtime.context import AgentContext
|
||||
from app.agent_runtime.memory import AgentMemory
|
||||
@@ -24,6 +26,24 @@ from app.agent_runtime.tool_manager import AgentToolManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LLMCallMetrics(TypedDict, total=False):
|
||||
"""一次 LLM 调用的度量数据"""
|
||||
agent_id: Optional[str]
|
||||
session_id: str
|
||||
user_id: Optional[str]
|
||||
model: str
|
||||
provider: Optional[str]
|
||||
prompt_tokens: int
|
||||
completion_tokens: int
|
||||
total_tokens: int
|
||||
latency_ms: int
|
||||
iteration_number: int
|
||||
step_type: str # think / final
|
||||
tool_name: Optional[str]
|
||||
status: str # success / error
|
||||
error_message: Optional[str]
|
||||
|
||||
# 可重试的 API 异常
|
||||
_RETRYABLE_ERRORS = (
|
||||
"timed out",
|
||||
@@ -55,6 +75,7 @@ class AgentRuntime:
|
||||
tool_manager: Optional[AgentToolManager] = None,
|
||||
execution_logger: Optional[Any] = None,
|
||||
on_tool_executed: Optional[Callable[[str], Any]] = None,
|
||||
on_llm_call: Optional[Callable[[Dict[str, Any]], Any]] = None,
|
||||
):
|
||||
self.config = config or AgentConfig()
|
||||
self.context = context or AgentContext(
|
||||
@@ -72,6 +93,7 @@ class AgentRuntime:
|
||||
)
|
||||
self.execution_logger = execution_logger
|
||||
self.on_tool_executed = on_tool_executed
|
||||
self.on_llm_call = on_llm_call
|
||||
self._memory_context_loaded = False
|
||||
|
||||
async def run(self, user_input: str) -> AgentResult:
|
||||
@@ -96,6 +118,20 @@ class AgentRuntime:
|
||||
llm = _LLMClient(self.config.llm)
|
||||
tool_schemas = self.tool_manager.get_tool_schemas()
|
||||
has_tools = self.tool_manager.has_tools()
|
||||
steps: List[AgentStep] = []
|
||||
|
||||
# 构建 LLM 调用回调(包装 on_llm_call,补充上下文)
|
||||
llm_callback_ctx = {"step_type": "think", "tool_name": None}
|
||||
|
||||
def _llm_callback(metrics: Dict[str, Any]):
|
||||
if self.on_llm_call:
|
||||
metrics.update({
|
||||
"session_id": self.context.session_id,
|
||||
"user_id": self.config.user_id,
|
||||
"step_type": llm_callback_ctx["step_type"],
|
||||
"tool_name": llm_callback_ctx["tool_name"],
|
||||
})
|
||||
self.on_llm_call(metrics)
|
||||
|
||||
while self.context.iteration < max_iter:
|
||||
self.context.iteration += 1
|
||||
@@ -110,11 +146,17 @@ class AgentRuntime:
|
||||
tools=tool_schemas if has_tools and self.context.iteration == 1 else
|
||||
(tool_schemas if has_tools else None),
|
||||
iteration=self.context.iteration,
|
||||
on_completion=_llm_callback,
|
||||
)
|
||||
except Exception as e:
|
||||
err_str = str(e)
|
||||
logger.error("LLM 调用失败 (iteration=%s): %s", self.context.iteration, err_str)
|
||||
if self.context.iteration < max_iter and self._is_retryable(err_str):
|
||||
steps.append(AgentStep(
|
||||
iteration=self.context.iteration,
|
||||
type="tool_result",
|
||||
content=f"LLM 调用失败(可重试): {err_str}",
|
||||
))
|
||||
continue
|
||||
return AgentResult(
|
||||
success=False,
|
||||
@@ -127,29 +169,55 @@ class AgentRuntime:
|
||||
# 解析工具调用
|
||||
tool_calls = self._extract_tool_calls(response)
|
||||
content = self._extract_content(response)
|
||||
reasoning = getattr(response, "reasoning_content", None) or (
|
||||
response.get("reasoning_content") if isinstance(response, dict) else None
|
||||
)
|
||||
|
||||
if not tool_calls:
|
||||
# LLM 直接返回文本 → 结束
|
||||
self.context.add_assistant_message(content)
|
||||
final_text = content or "(模型未返回有效内容)"
|
||||
steps.append(AgentStep(
|
||||
iteration=self.context.iteration,
|
||||
type="final",
|
||||
content=final_text,
|
||||
reasoning=reasoning,
|
||||
))
|
||||
# 保存记忆
|
||||
await self.memory.save_context(user_input, final_text)
|
||||
await self.memory.save_context(user_input, final_text, self.context.messages)
|
||||
return AgentResult(
|
||||
success=True,
|
||||
content=final_text,
|
||||
iterations_used=self.context.iteration,
|
||||
tool_calls_made=self.context.tool_calls_made,
|
||||
steps=steps,
|
||||
)
|
||||
|
||||
# 有工具调用 → 先记录 assistant 消息(含 tool_calls + reasoning_content)
|
||||
reasoning = getattr(response, "reasoning_content", None) or (
|
||||
response.get("reasoning_content") if isinstance(response, dict) else None
|
||||
)
|
||||
# 有工具调用 → 先记录 assistant 消息(含 tool_calls)
|
||||
self.context.add_assistant_message(content or "", tool_calls, reasoning)
|
||||
|
||||
# 记录思考步骤(含工具调用意图)
|
||||
tc_names = [tc["function"]["name"] for tc in tool_calls]
|
||||
tc_args_list = []
|
||||
for tc in tool_calls:
|
||||
try:
|
||||
tc_args_list.append(json.loads(tc["function"].get("arguments", "{}")))
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
tc_args_list.append({})
|
||||
|
||||
steps.append(AgentStep(
|
||||
iteration=self.context.iteration,
|
||||
type="think",
|
||||
content=content or f"调用工具: {', '.join(tc_names)}",
|
||||
reasoning=reasoning,
|
||||
tool_name=tc_names[0] if len(tc_names) == 1 else None,
|
||||
tool_input=tc_args_list[0] if len(tc_args_list) == 1 else None,
|
||||
))
|
||||
|
||||
if self.execution_logger:
|
||||
self.execution_logger.info(
|
||||
f"Agent 调用 {len(tool_calls)} 个工具",
|
||||
data={"tool_calls": [tc["function"]["name"] for tc in tool_calls],
|
||||
data={"tool_calls": tc_names,
|
||||
"iteration": self.context.iteration},
|
||||
)
|
||||
|
||||
@@ -167,6 +235,15 @@ class AgentRuntime:
|
||||
logger.info("Agent 执行工具 [%s]: %s", tname, targs)
|
||||
result = await self.tool_manager.execute(tname, targs)
|
||||
|
||||
steps.append(AgentStep(
|
||||
iteration=self.context.iteration,
|
||||
type="tool_result",
|
||||
content=f"工具 {tname} 返回结果",
|
||||
tool_name=tname,
|
||||
tool_input=targs,
|
||||
tool_result=result[:500] + "..." if len(result) > 500 else result,
|
||||
))
|
||||
|
||||
self.context.add_tool_result(tcid, tname, result)
|
||||
self.context.tool_calls_made += 1
|
||||
|
||||
@@ -191,13 +268,20 @@ class AgentRuntime:
|
||||
break
|
||||
|
||||
logger.warning("Agent 达到最大迭代次数 (%s)", max_iter)
|
||||
await self.memory.save_context(user_input, last_content or "(已达最大迭代次数)")
|
||||
await self.memory.save_context(user_input, last_content or "(已达最大迭代次数)", self.context.messages)
|
||||
if last_content:
|
||||
steps.append(AgentStep(
|
||||
iteration=self.context.iteration,
|
||||
type="final",
|
||||
content=last_content,
|
||||
))
|
||||
return AgentResult(
|
||||
success=True,
|
||||
content=last_content or "已达最大迭代次数,但模型未返回最终回答。",
|
||||
truncated=True,
|
||||
iterations_used=self.context.iteration,
|
||||
tool_calls_made=self.context.tool_calls_made,
|
||||
steps=steps,
|
||||
)
|
||||
|
||||
async def _inject_memory_context(self) -> None:
|
||||
@@ -285,6 +369,7 @@ class _LLMClient:
|
||||
messages: List[Dict[str, Any]],
|
||||
tools: Optional[List[Dict[str, Any]]] = None,
|
||||
iteration: int = 1,
|
||||
on_completion: Optional[Callable[[Dict[str, Any]], Any]] = None,
|
||||
) -> Any:
|
||||
"""
|
||||
调用 LLM。
|
||||
@@ -326,5 +411,44 @@ class _LLMClient:
|
||||
kwargs["tools"] = tools
|
||||
kwargs["tool_choice"] = "auto"
|
||||
|
||||
response = await client.chat.completions.create(**kwargs)
|
||||
return response.choices[0].message
|
||||
start_time = time.perf_counter()
|
||||
try:
|
||||
response = await client.chat.completions.create(**kwargs)
|
||||
latency_ms = int((time.perf_counter() - start_time) * 1000)
|
||||
message = response.choices[0].message
|
||||
|
||||
# 提取 token 用量
|
||||
usage = getattr(response, "usage", None)
|
||||
prompt_tokens = usage.prompt_tokens if usage else 0
|
||||
completion_tokens = usage.completion_tokens if usage else 0
|
||||
total_tokens = usage.total_tokens if usage else 0
|
||||
|
||||
# 调用完成回调
|
||||
if on_completion:
|
||||
on_completion({
|
||||
"model": self._config.model,
|
||||
"provider": self._config.provider,
|
||||
"prompt_tokens": prompt_tokens or 0,
|
||||
"completion_tokens": completion_tokens or 0,
|
||||
"total_tokens": total_tokens or 0,
|
||||
"latency_ms": latency_ms,
|
||||
"iteration_number": iteration,
|
||||
"status": "success",
|
||||
})
|
||||
|
||||
return message
|
||||
except Exception as e:
|
||||
latency_ms = int((time.perf_counter() - start_time) * 1000)
|
||||
if on_completion:
|
||||
on_completion({
|
||||
"model": self._config.model,
|
||||
"provider": self._config.provider,
|
||||
"prompt_tokens": 0,
|
||||
"completion_tokens": 0,
|
||||
"total_tokens": 0,
|
||||
"latency_ms": latency_ms,
|
||||
"iteration_number": iteration,
|
||||
"status": "error",
|
||||
"error_message": str(e),
|
||||
})
|
||||
raise
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""
|
||||
Agent 记忆管理:包装已有 persistent_memory_service,提供会话级和长期记忆。
|
||||
支持 LLM 自动压缩总结对话历史。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -14,7 +15,6 @@ from app.services.persistent_memory_service import (
|
||||
save_persistent_memory,
|
||||
persist_enabled,
|
||||
)
|
||||
from app.core.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,7 +25,7 @@ class AgentMemory:
|
||||
|
||||
- 工作记忆:当前会话消息列表(由 AgentRuntime 直接管理)
|
||||
- 长期记忆:从 MySQL 加载/保存的用户画像和关键事实
|
||||
- 上下文压缩:对话过长时自动裁剪或总结
|
||||
- 记忆压缩:LLM 自动总结对话历史,提取关键信息存入长期记忆
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
@@ -43,6 +43,8 @@ class AgentMemory:
|
||||
self.max_history = max_history
|
||||
# 从长期记忆加载的上下文(启动时加载)
|
||||
self._long_term_context: Dict[str, Any] = {}
|
||||
# 记录已压缩的消息数,避免重复压缩
|
||||
self._last_compressed_msg_count = 0
|
||||
|
||||
async def initialize(self) -> str:
|
||||
"""
|
||||
@@ -87,9 +89,10 @@ class AgentMemory:
|
||||
return ""
|
||||
|
||||
async def save_context(
|
||||
self, user_message: str, assistant_reply: str
|
||||
self, user_message: str, assistant_reply: str,
|
||||
messages: Optional[List[Dict[str, Any]]] = None,
|
||||
) -> None:
|
||||
"""将单轮对话保存到长期记忆。"""
|
||||
"""将单轮对话保存到长期记忆。如有消息列表,LLM 自动压缩总结。"""
|
||||
if not self.persist or not self.scope_id:
|
||||
return
|
||||
|
||||
@@ -99,6 +102,11 @@ class AgentMemory:
|
||||
ctx["last_assistant_reply"] = assistant_reply[:500]
|
||||
self._long_term_context["context"] = ctx
|
||||
|
||||
# 如果有完整消息列表且新增了足够多的消息,运行 LLM 压缩总结
|
||||
if messages and len(messages) > self._last_compressed_msg_count + 2:
|
||||
await self._compress_and_summarize(messages)
|
||||
self._last_compressed_msg_count = len(messages)
|
||||
|
||||
db: Optional[Session] = None
|
||||
try:
|
||||
db = SessionLocal()
|
||||
@@ -112,6 +120,111 @@ class AgentMemory:
|
||||
if db:
|
||||
db.close()
|
||||
|
||||
async def _compress_and_summarize(
|
||||
self, messages: List[Dict[str, Any]]
|
||||
) -> None:
|
||||
"""
|
||||
使用 LLM 压缩总结对话历史,提取用户画像和关键事实。
|
||||
只处理非 system 消息。
|
||||
"""
|
||||
from openai import AsyncOpenAI
|
||||
from app.core.config import settings
|
||||
|
||||
# 提取对话消息(去掉 system 和 tool 消息)
|
||||
conversation = []
|
||||
for m in messages:
|
||||
role = m.get("role", "")
|
||||
if role == "system":
|
||||
continue
|
||||
if role == "tool":
|
||||
# 工具结果精简后加入
|
||||
content = m.get("content", "")
|
||||
name = m.get("name", "tool")
|
||||
conversation.append({"role": "user" if role == "tool" else role, "content": f"[工具 {name} 执行结果]\n{content[:200]}"})
|
||||
else:
|
||||
conversation.append({"role": role, "content": m.get("content", "")[:500]})
|
||||
|
||||
if len(conversation) < 2:
|
||||
return
|
||||
|
||||
# 构建总结 prompt
|
||||
summary_prompt = (
|
||||
"你是一个记忆管理助手。请分析以下对话历史,提取关于用户的关键信息。\n\n"
|
||||
"请返回 JSON 格式(不要 markdown 包裹),包含以下字段:\n"
|
||||
"1. user_profile: 用户画像对象,包含用户的偏好、角色、关键需求等\n"
|
||||
"2. key_facts: 从对话中提取的关键事实列表(字符串数组)\n"
|
||||
"3. summary: 对话的简要总结(100字以内)\n"
|
||||
"4. topics: 讨论过的话题列表(字符串数组)\n\n"
|
||||
"如果没有足够信息,相应字段设为空对象或空数组。"
|
||||
)
|
||||
|
||||
summary_messages = [
|
||||
{"role": "system", "content": summary_prompt},
|
||||
*conversation[-10:], # 只取最近 10 条消息
|
||||
]
|
||||
|
||||
try:
|
||||
api_key = settings.DEEPSEEK_API_KEY or settings.OPENAI_API_KEY or ""
|
||||
base_url = settings.DEEPSEEK_BASE_URL or settings.OPENAI_BASE_URL or "https://api.deepseek.com"
|
||||
if api_key == "your-openai-api-key":
|
||||
api_key = settings.DEEPSEEK_API_KEY or ""
|
||||
base_url = settings.DEEPSEEK_BASE_URL or "https://api.deepseek.com"
|
||||
|
||||
if not api_key:
|
||||
logger.warning("记忆压缩:未配置 API Key,跳过")
|
||||
return
|
||||
|
||||
client = AsyncOpenAI(api_key=api_key, base_url=base_url)
|
||||
resp = await client.chat.completions.create(
|
||||
model="deepseek-v4-flash",
|
||||
messages=summary_messages,
|
||||
temperature=0.3,
|
||||
max_tokens=1024,
|
||||
timeout=30,
|
||||
)
|
||||
raw = resp.choices[0].message.content or ""
|
||||
|
||||
# 解析 JSON
|
||||
result = json.loads(raw.strip().removeprefix("```json").removesuffix("```").strip())
|
||||
|
||||
# 合并到长期记忆
|
||||
existing_profile = self._long_term_context.get("user_profile", {})
|
||||
new_profile = result.get("user_profile", {})
|
||||
if isinstance(new_profile, dict) and new_profile:
|
||||
# 合并画像(新信息覆盖旧信息)
|
||||
existing_profile.update(new_profile)
|
||||
self._long_term_context["user_profile"] = existing_profile
|
||||
|
||||
# 合并关键事实
|
||||
existing_facts = self._long_term_context.get("key_facts", [])
|
||||
new_facts = result.get("key_facts", [])
|
||||
if isinstance(new_facts, list):
|
||||
all_facts = list(dict.fromkeys(existing_facts + new_facts)) # 去重
|
||||
self._long_term_context["key_facts"] = all_facts[-20:] # 最多保留 20 条
|
||||
|
||||
# 更新摘要
|
||||
summary = result.get("summary", "")
|
||||
if summary:
|
||||
ctx = self._long_term_context.get("context", {})
|
||||
ctx["compressed_summary"] = summary
|
||||
self._long_term_context["context"] = ctx
|
||||
|
||||
# 记录话题
|
||||
topics = result.get("topics", [])
|
||||
if isinstance(topics, list) and topics:
|
||||
existing_topics = self._long_term_context.get("topics", [])
|
||||
all_topics = list(dict.fromkeys(existing_topics + topics))
|
||||
self._long_term_context["topics"] = all_topics[-20:]
|
||||
|
||||
logger.info("记忆压缩总结完成: profile=%s facts=%d topics=%d",
|
||||
"updated" if new_profile else "unchanged",
|
||||
len(new_facts), len(topics))
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.warning("记忆压缩:LLM 返回非 JSON 格式,跳过")
|
||||
except Exception as e:
|
||||
logger.warning("记忆压缩失败: %s", e)
|
||||
|
||||
def trim_messages(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
裁剪消息列表:保留最近的 N 条,但始终保留第一条 system 消息。
|
||||
@@ -127,7 +240,7 @@ class AgentMemory:
|
||||
|
||||
@staticmethod
|
||||
def _summarize_history(history: List[Dict[str, Any]]) -> str:
|
||||
"""简单汇总历史对话(不做 LLM 压缩,仅计数)。"""
|
||||
"""汇总历史对话。"""
|
||||
turns = 0
|
||||
for m in history:
|
||||
if m.get("role") == "user":
|
||||
|
||||
377
backend/app/agent_runtime/orchestrator.py
Normal file
377
backend/app/agent_runtime/orchestrator.py
Normal file
@@ -0,0 +1,377 @@
|
||||
"""
|
||||
Agent Orchestrator — 多 Agent 编排引擎。
|
||||
|
||||
支持三种协作模式:
|
||||
- route: Router Agent 分析问题 → 分发到最合适的 Specialist Agent
|
||||
- sequential: Agent 流水线执行,前者输出作为后者输入
|
||||
- debate: 多个 Agent 独立回答 → Aggregator 汇总为最终答案
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
import uuid
|
||||
from typing import Any, Callable, Dict, List, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.agent_runtime import (
|
||||
AgentRuntime,
|
||||
AgentConfig,
|
||||
AgentLLMConfig,
|
||||
AgentToolConfig,
|
||||
AgentResult,
|
||||
)
|
||||
from app.agent_runtime.core import _LLMClient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OrchestratorAgentConfig(BaseModel):
|
||||
"""编排中单个 Agent 的配置"""
|
||||
id: str = Field(..., description="Agent 标识")
|
||||
name: str = Field(default="Agent", description="显示名称")
|
||||
system_prompt: str = Field(default="你是一个有用的AI助手。")
|
||||
model: str = Field(default="deepseek-v4-flash")
|
||||
provider: str = Field(default="deepseek")
|
||||
temperature: float = 0.7
|
||||
max_iterations: int = 10
|
||||
tools: List[str] = Field(default_factory=list, description="工具白名单,空=全部")
|
||||
description: str = Field(default="", description="Agent 专长描述(路由模式用)")
|
||||
|
||||
|
||||
class OrchestratorStep(BaseModel):
|
||||
"""编排中的单步执行记录"""
|
||||
agent_id: str
|
||||
agent_name: str
|
||||
input: str = ""
|
||||
output: str = ""
|
||||
iterations_used: int = 0
|
||||
tool_calls_made: int = 0
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class OrchestratorResult(BaseModel):
|
||||
"""编排执行结果"""
|
||||
mode: str
|
||||
final_answer: str
|
||||
steps: List[OrchestratorStep] = Field(default_factory=list)
|
||||
agent_results: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
_ROUTER_SYSTEM_PROMPT = """你是一个路由调度员。你的任务是从以下 Specialist Agent 中选择一个最适合处理用户问题的 Agent。
|
||||
|
||||
可用的 Specialist Agent:
|
||||
{agent_list}
|
||||
|
||||
请返回 JSON 格式(不要 markdown 包裹),包含:
|
||||
1. "selected_agent": 选中的 Agent ID
|
||||
2. "reason": 选择理由(一句话)
|
||||
|
||||
规则:
|
||||
- 选择与问题最匹配的 Agent
|
||||
- 如果问题涉及多个领域,选择最相关的那个
|
||||
- 必须从上述列表中选择,不能编造 Agent ID"""
|
||||
|
||||
_AGGREGATOR_SYSTEM_PROMPT = """你是一个回答汇总员。多个 AI Agent 对同一个问题给出了不同的回答。
|
||||
|
||||
请分析所有回答,输出一份综合的最终答案。
|
||||
- 如果各 Agent 回答一致,合并要点
|
||||
- 如果有分歧,指出不同观点并给出你的判断
|
||||
- 以专业、清晰的格式输出最终答案"""
|
||||
|
||||
|
||||
class AgentOrchestrator:
|
||||
"""
|
||||
多 Agent 编排器。
|
||||
|
||||
用法:
|
||||
orch = AgentOrchestrator()
|
||||
result = await orch.run("route", question, [agent1, agent2, agent3])
|
||||
"""
|
||||
|
||||
def __init__(self, default_llm_config: Optional[AgentLLMConfig] = None):
|
||||
self._default_llm = default_llm_config or AgentLLMConfig(
|
||||
model="deepseek-v4-flash",
|
||||
temperature=0.3,
|
||||
)
|
||||
|
||||
async def run(
|
||||
self,
|
||||
mode: str,
|
||||
question: str,
|
||||
agents: List[OrchestratorAgentConfig],
|
||||
on_llm_call: Optional[Callable[[Dict[str, Any]], Any]] = None,
|
||||
) -> OrchestratorResult:
|
||||
"""执行多 Agent 编排。"""
|
||||
mode = mode.lower()
|
||||
if mode == "route":
|
||||
return await self._route(question, agents, on_llm_call)
|
||||
elif mode == "sequential":
|
||||
return await self._sequential(question, agents, on_llm_call)
|
||||
elif mode == "debate":
|
||||
return await self._debate(question, agents, on_llm_call)
|
||||
else:
|
||||
raise ValueError(f"不支持的编排模式: {mode},可选: route, sequential, debate")
|
||||
|
||||
async def _route(
|
||||
self, question: str, agents: List[OrchestratorAgentConfig],
|
||||
on_llm_call: Optional[Callable] = None,
|
||||
) -> OrchestratorResult:
|
||||
"""路由模式:Router → Specialist。"""
|
||||
# 构建 Agent 列表描述
|
||||
agent_lines = []
|
||||
for a in agents:
|
||||
desc = a.description or a.name
|
||||
agent_lines.append(f"- id: {a.id}, name: {a.name}, description: {desc}")
|
||||
agent_list_str = "\n".join(agent_lines)
|
||||
|
||||
router_prompt = _ROUTER_SYSTEM_PROMPT.format(agent_list=agent_list_str)
|
||||
|
||||
# 创建 Router Agent
|
||||
router_runtime = AgentRuntime(
|
||||
AgentConfig(
|
||||
name="router",
|
||||
system_prompt=router_prompt,
|
||||
llm=AgentLLMConfig(
|
||||
model=self._default_llm.model,
|
||||
temperature=0.1, # 低温度确保确定性
|
||||
),
|
||||
tools=AgentToolConfig(
|
||||
include_tools=[], # Router 不需要工具
|
||||
),
|
||||
),
|
||||
on_llm_call=on_llm_call,
|
||||
)
|
||||
router_result = await router_runtime.run(question)
|
||||
|
||||
if not router_result.success:
|
||||
return OrchestratorResult(
|
||||
mode="route",
|
||||
final_answer=f"路由决策失败: {router_result.content}",
|
||||
steps=[],
|
||||
)
|
||||
|
||||
# 解析 Router 的输出
|
||||
selected_agent_id = None
|
||||
try:
|
||||
parsed = json.loads(router_result.content.strip().removeprefix("```json").removesuffix("```").strip())
|
||||
selected_agent_id = parsed.get("selected_agent", "")
|
||||
except (json.JSONDecodeError, AttributeError):
|
||||
# 尝试从文本中提取
|
||||
for a in agents:
|
||||
if a.id in router_result.content:
|
||||
selected_agent_id = a.id
|
||||
break
|
||||
if not selected_agent_id:
|
||||
# 取第一个
|
||||
selected_agent_id = agents[0].id if agents else ""
|
||||
|
||||
# 找到对应的 Specialist Agent
|
||||
specialist = next((a for a in agents if a.id == selected_agent_id), agents[0] if agents else None)
|
||||
if not specialist:
|
||||
return OrchestratorResult(
|
||||
mode="route",
|
||||
final_answer="没有可用的 Specialist Agent",
|
||||
steps=[],
|
||||
)
|
||||
|
||||
# 运行 Specialist Agent
|
||||
specialist_runtime = AgentRuntime(
|
||||
AgentConfig(
|
||||
name=specialist.name,
|
||||
system_prompt=specialist.system_prompt,
|
||||
llm=AgentLLMConfig(
|
||||
model=specialist.model,
|
||||
provider=specialist.provider,
|
||||
temperature=specialist.temperature,
|
||||
max_iterations=specialist.max_iterations,
|
||||
),
|
||||
tools=AgentToolConfig(
|
||||
include_tools=specialist.tools,
|
||||
),
|
||||
),
|
||||
on_llm_call=on_llm_call,
|
||||
)
|
||||
specialist_result = await specialist_runtime.run(question)
|
||||
|
||||
return OrchestratorResult(
|
||||
mode="route",
|
||||
final_answer=specialist_result.content,
|
||||
steps=[
|
||||
OrchestratorStep(
|
||||
agent_id="router",
|
||||
agent_name="Router",
|
||||
input=question,
|
||||
output=f"选择: {specialist.name} ({specialist.id})",
|
||||
),
|
||||
OrchestratorStep(
|
||||
agent_id=specialist.id,
|
||||
agent_name=specialist.name,
|
||||
input=question,
|
||||
output=specialist_result.content[:300],
|
||||
iterations_used=specialist_result.iterations_used,
|
||||
tool_calls_made=specialist_result.tool_calls_made,
|
||||
),
|
||||
],
|
||||
agent_results=[
|
||||
{"agent_id": specialist.id, "agent_name": specialist.name, "output": specialist_result.content},
|
||||
],
|
||||
)
|
||||
|
||||
async def _sequential(
|
||||
self, question: str, agents: List[OrchestratorAgentConfig],
|
||||
on_llm_call: Optional[Callable] = None,
|
||||
) -> OrchestratorResult:
|
||||
"""顺序模式:Agent A 输出 → Agent B 输入。"""
|
||||
if not agents:
|
||||
return OrchestratorResult(mode="sequential", final_answer="无 Agent 可执行")
|
||||
|
||||
steps: List[OrchestratorStep] = []
|
||||
current_input = question
|
||||
|
||||
for i, agent_cfg in enumerate(agents):
|
||||
runtime = AgentRuntime(
|
||||
AgentConfig(
|
||||
name=agent_cfg.name,
|
||||
system_prompt=agent_cfg.system_prompt,
|
||||
llm=AgentLLMConfig(
|
||||
model=agent_cfg.model,
|
||||
provider=agent_cfg.provider,
|
||||
temperature=agent_cfg.temperature,
|
||||
max_iterations=agent_cfg.max_iterations,
|
||||
),
|
||||
tools=AgentToolConfig(
|
||||
include_tools=agent_cfg.tools,
|
||||
),
|
||||
),
|
||||
on_llm_call=on_llm_call,
|
||||
)
|
||||
|
||||
# 第一个 Agent 接收原始问题,后续 Agent 接收前一个的输出
|
||||
agent_input = current_input
|
||||
if i > 0:
|
||||
agent_input = (
|
||||
f"这是前一个 Agent 的处理结果,请在此基础上继续处理。\n\n"
|
||||
f"原始问题: {question}\n\n"
|
||||
f"前序输出:\n{current_input}"
|
||||
)
|
||||
|
||||
result = await runtime.run(agent_input)
|
||||
|
||||
step = OrchestratorStep(
|
||||
agent_id=agent_cfg.id,
|
||||
agent_name=agent_cfg.name,
|
||||
input=agent_input[:200],
|
||||
output=result.content[:500],
|
||||
iterations_used=result.iterations_used,
|
||||
tool_calls_made=result.tool_calls_made,
|
||||
error=None if result.success else result.error,
|
||||
)
|
||||
steps.append(step)
|
||||
|
||||
if not result.success:
|
||||
break
|
||||
|
||||
current_input = result.content
|
||||
|
||||
final_answer = steps[-1].output if steps else "无输出"
|
||||
return OrchestratorResult(
|
||||
mode="sequential",
|
||||
final_answer=final_answer,
|
||||
steps=steps,
|
||||
agent_results=[
|
||||
{"agent_id": s.agent_id, "agent_name": s.agent_name, "output": s.output}
|
||||
for s in steps
|
||||
],
|
||||
)
|
||||
|
||||
async def _debate(
|
||||
self, question: str, agents: List[OrchestratorAgentConfig],
|
||||
on_llm_call: Optional[Callable] = None,
|
||||
) -> OrchestratorResult:
|
||||
"""辩论模式:多 Agent 独立回答 → Aggregator 汇总。"""
|
||||
if not agents:
|
||||
return OrchestratorResult(mode="debate", final_answer="无 Agent 可执行")
|
||||
|
||||
steps: List[OrchestratorStep] = []
|
||||
agent_outputs: List[Dict[str, Any]] = []
|
||||
|
||||
# 第一阶段:所有 Agent 独立回答
|
||||
for agent_cfg in agents:
|
||||
runtime = AgentRuntime(
|
||||
AgentConfig(
|
||||
name=agent_cfg.name,
|
||||
system_prompt=agent_cfg.system_prompt,
|
||||
llm=AgentLLMConfig(
|
||||
model=agent_cfg.model,
|
||||
provider=agent_cfg.provider,
|
||||
temperature=agent_cfg.temperature,
|
||||
max_iterations=agent_cfg.max_iterations,
|
||||
),
|
||||
tools=AgentToolConfig(
|
||||
include_tools=agent_cfg.tools,
|
||||
),
|
||||
),
|
||||
on_llm_call=on_llm_call,
|
||||
)
|
||||
result = await runtime.run(question)
|
||||
|
||||
step = OrchestratorStep(
|
||||
agent_id=agent_cfg.id,
|
||||
agent_name=agent_cfg.name,
|
||||
input=question,
|
||||
output=result.content[:500],
|
||||
iterations_used=result.iterations_used,
|
||||
tool_calls_made=result.tool_calls_made,
|
||||
error=None if result.success else result.error,
|
||||
)
|
||||
steps.append(step)
|
||||
agent_outputs.append({
|
||||
"agent_id": agent_cfg.id,
|
||||
"agent_name": agent_cfg.name,
|
||||
"output": result.content,
|
||||
})
|
||||
|
||||
# 第二阶段:Aggregator 汇总所有回答
|
||||
if len(agent_outputs) >= 2:
|
||||
outputs_text = "\n\n---\n\n".join(
|
||||
f"## {ao['agent_name']} 的回答\n{ao['output']}" for ao in agent_outputs
|
||||
)
|
||||
|
||||
aggregator_prompt = (
|
||||
f"用户问题: {question}\n\n"
|
||||
f"以下是多个 AI Agent 对该问题的回答:\n\n{outputs_text}\n\n"
|
||||
"请综合所有回答,输出一份完整、准确的最终答案。"
|
||||
)
|
||||
|
||||
aggregator_runtime = AgentRuntime(
|
||||
AgentConfig(
|
||||
name="aggregator",
|
||||
system_prompt=_AGGREGATOR_SYSTEM_PROMPT,
|
||||
llm=AgentLLMConfig(
|
||||
model=self._default_llm.model,
|
||||
temperature=0.3,
|
||||
),
|
||||
tools=AgentToolConfig(include_tools=[]),
|
||||
),
|
||||
on_llm_call=on_llm_call,
|
||||
)
|
||||
final_result = await aggregator_runtime.run(aggregator_prompt)
|
||||
|
||||
final_answer = final_result.content
|
||||
steps.append(OrchestratorStep(
|
||||
agent_id="aggregator",
|
||||
agent_name="Aggregator",
|
||||
input="汇总各 Agent 回答",
|
||||
output=final_answer[:500],
|
||||
))
|
||||
else:
|
||||
final_answer = agent_outputs[0]["output"] if agent_outputs else "无回答"
|
||||
|
||||
return OrchestratorResult(
|
||||
mode="debate",
|
||||
final_answer=final_answer,
|
||||
steps=steps,
|
||||
agent_results=agent_outputs,
|
||||
)
|
||||
@@ -54,6 +54,17 @@ class AgentMessage(BaseModel):
|
||||
name: Optional[str] = None
|
||||
|
||||
|
||||
class AgentStep(BaseModel):
|
||||
"""Agent 单步执行记录(用于执行追踪)"""
|
||||
iteration: int = Field(..., description="第几步")
|
||||
type: str = Field(..., description="步骤类型: think / tool_call / tool_result / final")
|
||||
content: str = Field(default="", description="步骤内容")
|
||||
tool_name: Optional[str] = Field(default=None, description="工具名称(tool_call/tool_result 类型时)")
|
||||
tool_input: Optional[Dict[str, Any]] = Field(default=None, description="工具输入参数")
|
||||
tool_result: Optional[str] = Field(default=None, description="工具执行结果")
|
||||
reasoning: Optional[str] = Field(default=None, description="思考过程")
|
||||
|
||||
|
||||
class AgentResult(BaseModel):
|
||||
"""Agent 执行结果"""
|
||||
success: bool = True
|
||||
@@ -62,3 +73,4 @@ class AgentResult(BaseModel):
|
||||
iterations_used: int = 0
|
||||
tool_calls_made: int = 0
|
||||
error: Optional[str] = None
|
||||
steps: List[AgentStep] = Field(default_factory=list, description="执行追踪步骤详情")
|
||||
|
||||
@@ -8,20 +8,24 @@ POST /api/v1/agent-chat/bare
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, Optional
|
||||
from typing import Any, Dict, List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from app.core.database import get_db
|
||||
from sqlalchemy.orm import Session
|
||||
from app.api.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.models.agent import Agent
|
||||
from app.models.agent_llm_log import AgentLLMLog
|
||||
from app.agent_runtime import (
|
||||
AgentRuntime,
|
||||
AgentConfig,
|
||||
AgentLLMConfig,
|
||||
AgentToolConfig,
|
||||
AgentStep,
|
||||
AgentOrchestrator,
|
||||
OrchestratorAgentConfig,
|
||||
)
|
||||
from app.core.config import settings
|
||||
|
||||
@@ -29,6 +33,37 @@ logger = logging.getLogger(__name__)
|
||||
router = APIRouter(prefix="/api/v1/agent-chat", tags=["agent-chat"])
|
||||
|
||||
|
||||
def _make_llm_logger(
|
||||
db: Session,
|
||||
agent_id: Optional[str] = None,
|
||||
user_id: Optional[str] = None,
|
||||
):
|
||||
"""创建 LLM 调用日志回调,写入 AgentLLMLog 表。"""
|
||||
def _log(metrics: dict):
|
||||
try:
|
||||
log = AgentLLMLog(
|
||||
agent_id=agent_id,
|
||||
session_id=metrics.get("session_id"),
|
||||
user_id=user_id,
|
||||
model=metrics.get("model", ""),
|
||||
provider=metrics.get("provider"),
|
||||
prompt_tokens=metrics.get("prompt_tokens", 0),
|
||||
completion_tokens=metrics.get("completion_tokens", 0),
|
||||
total_tokens=metrics.get("total_tokens", 0),
|
||||
latency_ms=metrics.get("latency_ms", 0),
|
||||
iteration_number=metrics.get("iteration_number", 0),
|
||||
step_type=metrics.get("step_type"),
|
||||
tool_name=metrics.get("tool_name"),
|
||||
status=metrics.get("status", "success"),
|
||||
error_message=metrics.get("error_message"),
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
except Exception as e:
|
||||
logger.warning("写入 AgentLLMLog 失败: %s", e)
|
||||
return _log
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
message: str
|
||||
session_id: Optional[str] = None
|
||||
@@ -44,12 +79,103 @@ class ChatResponse(BaseModel):
|
||||
truncated: bool
|
||||
session_id: str
|
||||
agent_id: Optional[str] = None
|
||||
steps: List[AgentStep] = Field(default_factory=list, description="执行追踪步骤")
|
||||
|
||||
|
||||
class OrchestrateAgentItem(BaseModel):
|
||||
"""编排中单个 Agent 的定义"""
|
||||
id: str
|
||||
name: str = "Agent"
|
||||
system_prompt: str = "你是一个有用的AI助手。"
|
||||
model: str = "deepseek-v4-flash"
|
||||
provider: str = "deepseek"
|
||||
temperature: float = 0.7
|
||||
max_iterations: int = 10
|
||||
tools: List[str] = Field(default_factory=list)
|
||||
description: str = ""
|
||||
|
||||
|
||||
class OrchestrateRequest(BaseModel):
|
||||
"""多 Agent 编排请求"""
|
||||
message: str
|
||||
mode: str = "debate"
|
||||
agents: List[OrchestrateAgentItem] = Field(..., min_length=1)
|
||||
model: Optional[str] = None
|
||||
|
||||
|
||||
class OrchestrateStepItem(BaseModel):
|
||||
"""编排步骤"""
|
||||
agent_id: str
|
||||
agent_name: str
|
||||
input: str = ""
|
||||
output: str = ""
|
||||
iterations_used: int = 0
|
||||
tool_calls_made: int = 0
|
||||
error: Optional[str] = None
|
||||
|
||||
|
||||
class OrchestrateResponse(BaseModel):
|
||||
"""多 Agent 编排响应"""
|
||||
mode: str
|
||||
final_answer: str
|
||||
steps: List[OrchestrateStepItem] = Field(default_factory=list)
|
||||
agent_results: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
|
||||
|
||||
@router.post("/orchestrate", response_model=OrchestrateResponse)
|
||||
async def orchestrate_agents(
|
||||
req: OrchestrateRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""多 Agent 编排:支持 route / sequential / debate 三种模式。"""
|
||||
agents = [
|
||||
OrchestratorAgentConfig(
|
||||
id=a.id, name=a.name,
|
||||
system_prompt=a.system_prompt,
|
||||
model=req.model or a.model,
|
||||
provider=a.provider,
|
||||
temperature=a.temperature,
|
||||
max_iterations=a.max_iterations,
|
||||
tools=a.tools,
|
||||
description=a.description,
|
||||
)
|
||||
for a in req.agents
|
||||
]
|
||||
|
||||
on_llm_call = _make_llm_logger(db, agent_id=None, user_id=current_user.id)
|
||||
orchestrator = AgentOrchestrator(
|
||||
default_llm_config=AgentLLMConfig(
|
||||
model=req.model or "deepseek-v4-flash",
|
||||
temperature=0.3,
|
||||
),
|
||||
)
|
||||
result = await orchestrator.run(req.mode, req.message, agents, on_llm_call=on_llm_call)
|
||||
|
||||
return OrchestrateResponse(
|
||||
mode=result.mode,
|
||||
final_answer=result.final_answer,
|
||||
steps=[
|
||||
OrchestrateStepItem(
|
||||
agent_id=s.agent_id,
|
||||
agent_name=s.agent_name,
|
||||
input=s.input,
|
||||
output=s.output,
|
||||
iterations_used=s.iterations_used,
|
||||
tool_calls_made=s.tool_calls_made,
|
||||
error=s.error,
|
||||
)
|
||||
for s in result.steps
|
||||
],
|
||||
agent_results=result.agent_results,
|
||||
)
|
||||
|
||||
|
||||
@router.post("/bare", response_model=ChatResponse)
|
||||
async def chat_bare(
|
||||
req: ChatRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""无需 Agent 配置,使用默认设置直接对话。"""
|
||||
config = AgentConfig(
|
||||
@@ -65,7 +191,8 @@ async def chat_bare(
|
||||
),
|
||||
user_id=current_user.id,
|
||||
)
|
||||
runtime = AgentRuntime(config=config)
|
||||
on_llm_call = _make_llm_logger(db, agent_id=None, user_id=current_user.id)
|
||||
runtime = AgentRuntime(config=config, on_llm_call=on_llm_call)
|
||||
result = await runtime.run(req.message)
|
||||
|
||||
return ChatResponse(
|
||||
@@ -74,6 +201,7 @@ async def chat_bare(
|
||||
tool_calls_made=result.tool_calls_made,
|
||||
truncated=result.truncated,
|
||||
session_id=runtime.context.session_id,
|
||||
steps=result.steps,
|
||||
)
|
||||
|
||||
|
||||
@@ -113,7 +241,8 @@ async def chat_with_agent(
|
||||
user_id=current_user.id,
|
||||
)
|
||||
|
||||
runtime = AgentRuntime(config=config)
|
||||
on_llm_call = _make_llm_logger(db, agent_id=agent_id, user_id=current_user.id)
|
||||
runtime = AgentRuntime(config=config, on_llm_call=on_llm_call)
|
||||
result = await runtime.run(req.message)
|
||||
|
||||
return ChatResponse(
|
||||
@@ -123,6 +252,7 @@ async def chat_with_agent(
|
||||
truncated=result.truncated,
|
||||
session_id=runtime.context.session_id,
|
||||
agent_id=agent_id,
|
||||
steps=result.steps,
|
||||
)
|
||||
|
||||
|
||||
|
||||
74
backend/app/api/agent_monitoring.py
Normal file
74
backend/app/api/agent_monitoring.py
Normal file
@@ -0,0 +1,74 @@
|
||||
"""
|
||||
Agent 监控 API — 提供 Agent 专属统计数据
|
||||
"""
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import Optional
|
||||
from app.core.database import get_db
|
||||
from app.api.auth import get_current_user
|
||||
from app.models.user import User
|
||||
from app.services.agent_monitoring_service import AgentMonitoringService
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/v1/agent-monitoring",
|
||||
tags=["agent-monitoring"],
|
||||
responses={
|
||||
401: {"description": "未授权"},
|
||||
403: {"description": "无权访问"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@router.get("/overview")
|
||||
async def get_agent_overview(
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Agent 概览统计:Agent 数、对话次数、LLM 调用次数、Token 用量、工具调用次数。"""
|
||||
user_id = None if current_user.role == "admin" else current_user.id
|
||||
return AgentMonitoringService.get_overview(db, user_id)
|
||||
|
||||
|
||||
@router.get("/llm-calls")
|
||||
async def get_llm_calls(
|
||||
days: int = Query(7, ge=1, le=30, description="统计天数"),
|
||||
limit: int = Query(50, ge=1, le=200, description="返回条数"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""最近 LLM 调用记录列表。"""
|
||||
user_id = None if current_user.role == "admin" else current_user.id
|
||||
return AgentMonitoringService.get_llm_calls(db, user_id, days, limit)
|
||||
|
||||
|
||||
@router.get("/agents-stats")
|
||||
async def get_agent_stats(
|
||||
days: int = Query(7, ge=1, le=30, description="统计天数"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""各 Agent 用量统计(按 Agent 分组)。"""
|
||||
user_id = None if current_user.role == "admin" else current_user.id
|
||||
return AgentMonitoringService.get_agent_stats(db, user_id, days)
|
||||
|
||||
|
||||
@router.get("/tool-usage")
|
||||
async def get_tool_usage(
|
||||
days: int = Query(7, ge=1, le=30, description="统计天数"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""工具调用频次统计。"""
|
||||
user_id = None if current_user.role == "admin" else current_user.id
|
||||
return AgentMonitoringService.get_tool_usage(db, user_id, days)
|
||||
|
||||
|
||||
@router.get("/daily-trend")
|
||||
async def get_daily_trend(
|
||||
days: int = Query(7, ge=1, le=30, description="统计天数"),
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
"""每日 LLM 调用趋势。"""
|
||||
user_id = None if current_user.role == "admin" else current_user.id
|
||||
return AgentMonitoringService.get_daily_trend(db, user_id, days)
|
||||
@@ -46,4 +46,5 @@ def init_db():
|
||||
import app.models.workflow_template
|
||||
import app.models.permission
|
||||
import app.models.alert_rule
|
||||
import app.models.agent_llm_log
|
||||
Base.metadata.create_all(bind=engine)
|
||||
|
||||
@@ -201,7 +201,7 @@ async def startup_event():
|
||||
# 不抛出异常,允许应用继续启动
|
||||
|
||||
# 注册路由
|
||||
from app.api import auth, uploads, workflows, executions, websocket, execution_logs, data_sources, agents, platform_templates, model_configs, webhooks, template_market, batch_operations, collaboration, permissions, monitoring, alert_rules, node_test, node_templates, tools, agent_chat
|
||||
from app.api import auth, uploads, workflows, executions, websocket, execution_logs, data_sources, agents, platform_templates, model_configs, webhooks, template_market, batch_operations, collaboration, permissions, monitoring, alert_rules, node_test, node_templates, tools, agent_chat, agent_monitoring
|
||||
|
||||
app.include_router(auth.router)
|
||||
app.include_router(uploads.router)
|
||||
@@ -224,6 +224,7 @@ app.include_router(node_test.router)
|
||||
app.include_router(node_templates.router)
|
||||
app.include_router(tools.router)
|
||||
app.include_router(agent_chat.router)
|
||||
app.include_router(agent_monitoring.router)
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
@@ -12,5 +12,6 @@ from app.models.node_template import NodeTemplate
|
||||
from app.models.permission import Role, Permission, WorkflowPermission, AgentPermission
|
||||
from app.models.alert_rule import AlertRule, AlertLog
|
||||
from app.models.persistent_user_memory import PersistentUserMemory
|
||||
from app.models.agent_llm_log import AgentLLMLog
|
||||
|
||||
__all__ = ["User", "Workflow", "WorkflowVersion", "Agent", "Execution", "ExecutionLog", "ModelConfig", "DataSource", "WorkflowTemplate", "TemplateRating", "TemplateFavorite", "NodeTemplate", "Role", "Permission", "WorkflowPermission", "AgentPermission", "AlertRule", "AlertLog", "PersistentUserMemory"]
|
||||
__all__ = ["User", "Workflow", "WorkflowVersion", "Agent", "Execution", "ExecutionLog", "ModelConfig", "DataSource", "WorkflowTemplate", "TemplateRating", "TemplateFavorite", "NodeTemplate", "Role", "Permission", "WorkflowPermission", "AgentPermission", "AlertRule", "AlertLog", "PersistentUserMemory", "AgentLLMLog"]
|
||||
29
backend/app/models/agent_llm_log.py
Normal file
29
backend/app/models/agent_llm_log.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Agent LLM 调用日志模型 — 记录每次 Agent Runtime 发起的 LLM 调用
|
||||
"""
|
||||
from sqlalchemy import Column, String, Text, Integer, DateTime, ForeignKey, func
|
||||
from sqlalchemy.dialects.mysql import CHAR
|
||||
from app.core.database import Base
|
||||
import uuid
|
||||
|
||||
|
||||
class AgentLLMLog(Base):
|
||||
"""Agent LLM 调用日志表"""
|
||||
__tablename__ = "agent_llm_logs"
|
||||
|
||||
id = Column(CHAR(36), primary_key=True, default=lambda: str(uuid.uuid4()), comment="日志ID")
|
||||
agent_id = Column(CHAR(36), ForeignKey("agents.id"), nullable=True, comment="Agent ID")
|
||||
session_id = Column(String(100), nullable=True, comment="会话ID")
|
||||
user_id = Column(CHAR(36), ForeignKey("users.id"), nullable=True, comment="用户ID")
|
||||
model = Column(String(100), nullable=False, comment="模型名称")
|
||||
provider = Column(String(50), nullable=True, comment="提供商")
|
||||
prompt_tokens = Column(Integer, default=0, comment="提示 tokens")
|
||||
completion_tokens = Column(Integer, default=0, comment="生成 tokens")
|
||||
total_tokens = Column(Integer, default=0, comment="总 tokens")
|
||||
latency_ms = Column(Integer, default=0, comment="调用耗时(ms)")
|
||||
iteration_number = Column(Integer, default=0, comment="ReAct 迭代轮次")
|
||||
step_type = Column(String(20), nullable=True, comment="步骤类型: think/final")
|
||||
tool_name = Column(String(100), nullable=True, comment="工具名称(如是工具调用)")
|
||||
status = Column(String(20), default="success", comment="状态: success/error")
|
||||
error_message = Column(Text, nullable=True, comment="错误信息")
|
||||
created_at = Column(DateTime, default=func.now(), comment="创建时间")
|
||||
234
backend/app/services/agent_monitoring_service.py
Normal file
234
backend/app/services/agent_monitoring_service.py
Normal file
@@ -0,0 +1,234 @@
|
||||
"""
|
||||
Agent 监控服务 — 提供 Agent 专属统计数据
|
||||
"""
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy import func, and_
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional
|
||||
from app.models.agent_llm_log import AgentLLMLog
|
||||
from app.models.agent import Agent
|
||||
from app.models.execution import Execution
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AgentMonitoringService:
|
||||
"""Agent 监控服务"""
|
||||
|
||||
@staticmethod
|
||||
def get_overview(db: Session, user_id: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""
|
||||
获取 Agent 概览统计。
|
||||
- 总对话次数(Execution 中 agent_id 不为空的记录数)
|
||||
- 总 LLM 调用次数
|
||||
- 总 tokens 数(近似)
|
||||
- 总工具调用次数
|
||||
- 活跃 Agent 数
|
||||
"""
|
||||
user_filter = Agent.user_id == user_id if user_id else True
|
||||
agent_ids_query = db.query(Agent.id).filter(user_filter)
|
||||
agent_ids = {row[0] for row in agent_ids_query.all()}
|
||||
|
||||
# Agent 数量
|
||||
agent_count = len(agent_ids)
|
||||
|
||||
# 对话次数(Execution 表中 agent 执行的记录)
|
||||
exec_filter = Execution.agent_id.in_(agent_ids) if agent_ids else False
|
||||
chat_count = 0
|
||||
if agent_ids:
|
||||
chat_count = db.query(func.count(Execution.id)).filter(exec_filter).scalar() or 0
|
||||
|
||||
# LLM 调用统计
|
||||
llm_filter = AgentLLMLog.agent_id.in_(agent_ids) if agent_ids else False
|
||||
llm_count = 0
|
||||
total_prompt = 0
|
||||
total_completion = 0
|
||||
if agent_ids:
|
||||
stats = db.query(
|
||||
func.count(AgentLLMLog.id),
|
||||
func.coalesce(func.sum(AgentLLMLog.prompt_tokens), 0),
|
||||
func.coalesce(func.sum(AgentLLMLog.completion_tokens), 0),
|
||||
).filter(llm_filter).first()
|
||||
llm_count = stats[0] or 0
|
||||
total_prompt = stats[1] or 0
|
||||
total_completion = stats[2] or 0
|
||||
|
||||
# 工具调用次数(从 ExecutionLog 统计 agent 相关)
|
||||
tool_call_count = 0
|
||||
if agent_ids:
|
||||
tool_call_count = db.query(func.count(AgentLLMLog.id)).filter(
|
||||
and_(
|
||||
AgentLLMLog.agent_id.in_(agent_ids),
|
||||
AgentLLMLog.tool_name.isnot(None),
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
return {
|
||||
"agent_count": agent_count,
|
||||
"chat_count": chat_count,
|
||||
"llm_call_count": llm_count,
|
||||
"total_prompt_tokens": total_prompt,
|
||||
"total_completion_tokens": total_completion,
|
||||
"total_tokens": total_prompt + total_completion,
|
||||
"tool_call_count": tool_call_count,
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_llm_calls(
|
||||
db: Session,
|
||||
user_id: Optional[str] = None,
|
||||
days: int = 7,
|
||||
limit: int = 50,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取最近 LLM 调用记录。"""
|
||||
end_time = datetime.utcnow()
|
||||
start_time = end_time - timedelta(days=days)
|
||||
|
||||
filters = [AgentLLMLog.created_at >= start_time]
|
||||
if user_id:
|
||||
filters.append(AgentLLMLog.user_id == user_id)
|
||||
|
||||
records = db.query(AgentLLMLog).filter(
|
||||
and_(*filters)
|
||||
).order_by(AgentLLMLog.created_at.desc()).limit(limit).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"id": r.id,
|
||||
"agent_id": r.agent_id,
|
||||
"session_id": r.session_id,
|
||||
"model": r.model,
|
||||
"provider": r.provider,
|
||||
"prompt_tokens": r.prompt_tokens,
|
||||
"completion_tokens": r.completion_tokens,
|
||||
"total_tokens": r.total_tokens,
|
||||
"latency_ms": r.latency_ms,
|
||||
"iteration_number": r.iteration_number,
|
||||
"step_type": r.step_type,
|
||||
"tool_name": r.tool_name,
|
||||
"status": r.status,
|
||||
"error_message": r.error_message,
|
||||
"created_at": r.created_at.isoformat() if r.created_at else None,
|
||||
}
|
||||
for r in records
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_agent_stats(
|
||||
db: Session,
|
||||
user_id: Optional[str] = None,
|
||||
days: int = 7,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取各 Agent 的用量统计(按 Agent 分组)。"""
|
||||
end_time = datetime.utcnow()
|
||||
start_time = end_time - timedelta(days=days)
|
||||
|
||||
filters = [AgentLLMLog.created_at >= start_time]
|
||||
if user_id:
|
||||
filters.append(AgentLLMLog.user_id == user_id)
|
||||
|
||||
rows = db.query(
|
||||
AgentLLMLog.agent_id,
|
||||
Agent.name.label("agent_name"),
|
||||
func.count(AgentLLMLog.id).label("call_count"),
|
||||
func.coalesce(func.sum(AgentLLMLog.prompt_tokens), 0).label("total_prompt"),
|
||||
func.coalesce(func.sum(AgentLLMLog.completion_tokens), 0).label("total_completion"),
|
||||
func.coalesce(func.sum(AgentLLMLog.total_tokens), 0).label("total_tokens"),
|
||||
func.coalesce(func.avg(AgentLLMLog.latency_ms), 0).label("avg_latency"),
|
||||
func.count(AgentLLMLog.tool_name).label("tool_calls"),
|
||||
).outerjoin(
|
||||
Agent, AgentLLMLog.agent_id == Agent.id
|
||||
).filter(
|
||||
and_(*filters)
|
||||
).group_by(AgentLLMLog.agent_id).order_by(
|
||||
func.count(AgentLLMLog.id).desc()
|
||||
).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"agent_id": r.agent_id or "未知",
|
||||
"agent_name": r.agent_name or "未知 Agent",
|
||||
"call_count": r.call_count,
|
||||
"total_prompt_tokens": r.total_prompt,
|
||||
"total_completion_tokens": r.total_completion,
|
||||
"total_tokens": r.total_tokens,
|
||||
"avg_latency_ms": round(r.avg_latency, 2) if r.avg_latency else 0,
|
||||
"tool_call_count": r.tool_calls or 0,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_tool_usage(
|
||||
db: Session,
|
||||
user_id: Optional[str] = None,
|
||||
days: int = 7,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取工具调用频次统计。"""
|
||||
end_time = datetime.utcnow()
|
||||
start_time = end_time - timedelta(days=days)
|
||||
|
||||
filters = [
|
||||
AgentLLMLog.created_at >= start_time,
|
||||
AgentLLMLog.tool_name.isnot(None),
|
||||
]
|
||||
if user_id:
|
||||
filters.append(AgentLLMLog.user_id == user_id)
|
||||
|
||||
rows = db.query(
|
||||
AgentLLMLog.tool_name,
|
||||
func.count(AgentLLMLog.id).label("call_count"),
|
||||
func.coalesce(func.sum(AgentLLMLog.total_tokens), 0).label("total_tokens"),
|
||||
func.coalesce(func.avg(AgentLLMLog.latency_ms), 0).label("avg_latency"),
|
||||
).filter(
|
||||
and_(*filters)
|
||||
).group_by(AgentLLMLog.tool_name).order_by(
|
||||
func.count(AgentLLMLog.id).desc()
|
||||
).all()
|
||||
|
||||
return [
|
||||
{
|
||||
"tool_name": r.tool_name or "未知",
|
||||
"call_count": r.call_count,
|
||||
"total_tokens": r.total_tokens,
|
||||
"avg_latency_ms": round(r.avg_latency, 2) if r.avg_latency else 0,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def get_daily_trend(
|
||||
db: Session,
|
||||
user_id: Optional[str] = None,
|
||||
days: int = 7,
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取每日 LLM 调用趋势。"""
|
||||
end_time = datetime.utcnow()
|
||||
start_time = end_time - timedelta(days=days)
|
||||
|
||||
filters = [AgentLLMLog.created_at >= start_time]
|
||||
if user_id:
|
||||
filters.append(AgentLLMLog.user_id == user_id)
|
||||
|
||||
results = []
|
||||
for i in range(days):
|
||||
day_start = end_time - timedelta(days=days - i)
|
||||
day_end = day_start + timedelta(days=1)
|
||||
day_filter = and_(
|
||||
*filters,
|
||||
AgentLLMLog.created_at >= day_start,
|
||||
AgentLLMLog.created_at < day_end,
|
||||
)
|
||||
day_count = db.query(func.count(AgentLLMLog.id)).filter(day_filter).scalar() or 0
|
||||
day_tokens = db.query(
|
||||
func.coalesce(func.sum(AgentLLMLog.total_tokens), 0)
|
||||
).filter(day_filter).scalar() or 0
|
||||
|
||||
results.append({
|
||||
"date": day_start.strftime("%m-%d"),
|
||||
"call_count": day_count,
|
||||
"total_tokens": day_tokens,
|
||||
})
|
||||
|
||||
return results
|
||||
Reference in New Issue
Block a user