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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user