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:
renjianbo
2026-05-01 19:32:59 +08:00
parent 09467568ec
commit 036f533881
21 changed files with 2601 additions and 411 deletions

View File

@@ -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