[add] multi agent handoff

This commit is contained in:
Mark
2025-12-29 12:17:32 +08:00
parent 667c3393bc
commit de714d0422
8 changed files with 3555 additions and 1401 deletions

View File

@@ -0,0 +1,375 @@
"""Agent Handoff 机制 - 实现 Agent 之间的动态切换和协作
基于 LangChain 的 handoffs 模式,支持:
1. Agent 之间的动态切换transfer
2. 工具驱动的状态转换
3. 会话上下文的保持
4. 协作历史的追踪
"""
import uuid
from typing import Dict, Any, Optional, List
from datetime import datetime
from pydantic import BaseModel, Field
from app.core.logging_config import get_business_logger
logger = get_business_logger()
class HandoffContext(BaseModel):
"""Handoff 上下文信息"""
from_agent_id: str = Field(..., description="源 Agent ID")
to_agent_id: str = Field(..., description="目标 Agent ID")
reason: str = Field(..., description="切换原因")
timestamp: datetime = Field(default_factory=datetime.now)
user_message: Optional[str] = Field(None, description="触发切换的用户消息")
context_summary: Optional[str] = Field(None, description="上下文摘要")
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
class AgentHandoffTool(BaseModel):
"""Agent Handoff 工具定义"""
name: str = Field(..., description="工具名称,如 transfer_to_math_agent")
target_agent_id: str = Field(..., description="目标 Agent ID")
target_agent_name: str = Field(..., description="目标 Agent 名称")
description: str = Field(..., description="工具描述")
trigger_keywords: List[str] = Field(default_factory=list, description="触发关键词")
def to_tool_schema(self) -> Dict[str, Any]:
"""转换为 LLM 工具 schema"""
return {
"type": "function",
"function": {
"name": self.name,
"description": self.description,
"parameters": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "切换到该 Agent 的原因"
},
"context_summary": {
"type": "string",
"description": "需要传递给目标 Agent 的上下文摘要(可选)"
}
},
"required": ["reason"]
}
}
}
class HandoffState(BaseModel):
"""Handoff 状态管理"""
conversation_id: str = Field(..., description="会话 ID")
current_agent_id: str = Field(..., description="当前活跃的 Agent ID")
handoff_history: List[HandoffContext] = Field(default_factory=list, description="切换历史")
context_data: Dict[str, Any] = Field(default_factory=dict, description="共享上下文数据")
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
def add_handoff(self, context: HandoffContext):
"""添加 handoff 记录"""
self.handoff_history.append(context)
self.current_agent_id = context.to_agent_id
self.updated_at = datetime.now()
logger.info(
"Agent handoff 记录",
extra={
"conversation_id": self.conversation_id,
"from_agent": context.from_agent_id,
"to_agent": context.to_agent_id,
"reason": context.reason
}
)
def get_recent_handoffs(self, limit: int = 5) -> List[HandoffContext]:
"""获取最近的 handoff 记录"""
return self.handoff_history[-limit:] if self.handoff_history else []
def get_handoff_count(self) -> int:
"""获取 handoff 次数"""
return len(self.handoff_history)
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
class HandoffManager:
"""Handoff 管理器 - 管理 Agent 之间的切换"""
def __init__(self):
"""初始化 Handoff 管理器"""
self._states: Dict[str, HandoffState] = {}
logger.info("Handoff 管理器初始化完成")
def create_state(
self,
conversation_id: str,
initial_agent_id: str
) -> HandoffState:
"""创建新的 handoff 状态
Args:
conversation_id: 会话 ID
initial_agent_id: 初始 Agent ID
Returns:
HandoffState
"""
state = HandoffState(
conversation_id=conversation_id,
current_agent_id=initial_agent_id
)
self._states[conversation_id] = state
logger.info(
"创建 handoff 状态",
extra={
"conversation_id": conversation_id,
"initial_agent": initial_agent_id
}
)
return state
def get_state(self, conversation_id: str) -> Optional[HandoffState]:
"""获取 handoff 状态
Args:
conversation_id: 会话 ID
Returns:
HandoffState 或 None
"""
return self._states.get(conversation_id)
def execute_handoff(
self,
conversation_id: str,
from_agent_id: str,
to_agent_id: str,
reason: str,
user_message: Optional[str] = None,
context_summary: Optional[str] = None
) -> HandoffState:
"""执行 Agent 切换
Args:
conversation_id: 会话 ID
from_agent_id: 源 Agent ID
to_agent_id: 目标 Agent ID
reason: 切换原因
user_message: 用户消息
context_summary: 上下文摘要
Returns:
更新后的 HandoffState
"""
state = self.get_state(conversation_id)
if not state:
# 如果状态不存在,创建新状态
state = self.create_state(conversation_id, from_agent_id)
# 创建 handoff 上下文
context = HandoffContext(
from_agent_id=from_agent_id,
to_agent_id=to_agent_id,
reason=reason,
user_message=user_message,
context_summary=context_summary
)
# 添加到状态
state.add_handoff(context)
logger.info(
"执行 Agent handoff",
extra={
"conversation_id": conversation_id,
"from_agent": from_agent_id,
"to_agent": to_agent_id,
"handoff_count": state.get_handoff_count()
}
)
return state
def should_handoff(
self,
conversation_id: str,
current_agent_id: str,
message: str,
available_agents: Dict[str, Any]
) -> Optional[Dict[str, Any]]:
"""判断是否需要 handoff
Args:
conversation_id: 会话 ID
current_agent_id: 当前 Agent ID
message: 用户消息
available_agents: 可用的 Agent 字典
Returns:
如果需要 handoff返回目标 Agent 信息,否则返回 None
"""
state = self.get_state(conversation_id)
# 简单的关键词匹配策略
message_lower = message.lower()
for agent_id, agent_info in available_agents.items():
if agent_id == current_agent_id:
continue
# 检查 Agent 的能力关键词
capabilities = agent_info.get("info", {}).get("capabilities", [])
role = agent_info.get("info", {}).get("role", "")
# 关键词匹配
keywords = capabilities + ([role] if role else [])
for keyword in keywords:
if keyword.lower() in message_lower:
return {
"target_agent_id": agent_id,
"target_agent_name": agent_info.get("info", {}).get("name", ""),
"reason": f"检测到关键词: {keyword}",
"confidence": 0.8
}
# 检查是否频繁切换到同一个 Agent可能需要固定使用
if state:
recent_handoffs = state.get_recent_handoffs(3)
if len(recent_handoffs) >= 2:
# 检查是否有重复的目标 Agent
target_agents = [h.to_agent_id for h in recent_handoffs]
from collections import Counter
most_common = Counter(target_agents).most_common(1)
if most_common and most_common[0][1] >= 2:
# 频繁切换到同一个 Agent建议继续使用
return None
return None
def generate_handoff_tools(
self,
current_agent_id: str,
available_agents: Dict[str, Any]
) -> List[AgentHandoffTool]:
"""为当前 Agent 生成可用的 handoff 工具
Args:
current_agent_id: 当前 Agent ID
available_agents: 可用的 Agent 字典
Returns:
AgentHandoffTool 列表
"""
tools = []
for agent_id, agent_data in available_agents.items():
if agent_id == current_agent_id:
continue
agent_info = agent_data.get("info", {})
name = agent_info.get("name", "未命名")
role = agent_info.get("role", "")
capabilities = agent_info.get("capabilities", [])
# 生成工具名称
tool_name = f"transfer_to_{agent_id.replace('-', '_')}"
# 生成工具描述
description = f"切换到 {name}"
if role:
description += f"{role}"
if capabilities:
description += f"。擅长: {', '.join(capabilities[:3])}"
description += "。当用户的问题更适合该 Agent 处理时使用此工具。"
tool = AgentHandoffTool(
name=tool_name,
target_agent_id=agent_id,
target_agent_name=name,
description=description,
trigger_keywords=capabilities + ([role] if role else [])
)
tools.append(tool)
logger.info(
"生成 handoff 工具",
extra={
"current_agent": current_agent_id,
"tool_count": len(tools)
}
)
return tools
def get_handoff_context_for_agent(
self,
conversation_id: str,
agent_id: str
) -> Optional[str]:
"""获取传递给目标 Agent 的上下文信息
Args:
conversation_id: 会话 ID
agent_id: 目标 Agent ID
Returns:
上下文字符串
"""
state = self.get_state(conversation_id)
if not state:
return None
recent_handoffs = state.get_recent_handoffs(3)
if not recent_handoffs:
return None
# 构建上下文摘要
context_parts = []
for handoff in recent_handoffs:
if handoff.to_agent_id == agent_id:
context_parts.append(
f"{handoff.from_agent_id} 切换而来,原因: {handoff.reason}"
)
if handoff.context_summary:
context_parts.append(f"上下文: {handoff.context_summary}")
if context_parts:
return "\n".join(context_parts)
return None
def clear_state(self, conversation_id: str):
"""清除会话状态
Args:
conversation_id: 会话 ID
"""
if conversation_id in self._states:
del self._states[conversation_id]
logger.info(f"清除 handoff 状态: {conversation_id}")
# 全局单例
_handoff_manager = None
def get_handoff_manager() -> HandoffManager:
"""获取全局 Handoff 管理器单例"""
global _handoff_manager
if _handoff_manager is None:
_handoff_manager = HandoffManager()
return _handoff_manager

View File

@@ -0,0 +1,686 @@
"""协作编排器 - 支持 Agent 之间的动态切换和协作
基于 LangChain handoffs 模式,实现:
1. Agent 之间的动态切换tool-based handoffs
2. 会话上下文的保持
3. 智能路由决策
4. 协作历史追踪
"""
import json
import uuid
import time
from typing import Dict, Any, Optional, List, AsyncGenerator
from sqlalchemy.orm import Session
from app.services.agent_handoff import (
get_handoff_manager,
HandoffManager,
AgentHandoffTool
)
from app.services.dynamic_handoff_tools import DynamicHandoffToolCreator
from app.core.logging_config import get_business_logger
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode
from app.core.models import RedBearLLM
from app.core.models.base import RedBearModelConfig
from app.models import ModelType
logger = get_business_logger()
class CollaborativeOrchestrator:
"""协作编排器 - 管理多 Agent 协作和切换"""
def __init__(
self,
db: Session,
config: Any,
handoff_manager: Optional[HandoffManager] = None
):
"""初始化协作编排器
Args:
db: 数据库会话
config: 多 Agent 配置
handoff_manager: Handoff 管理器(可选)
"""
self.db = db
self.config = config
self.handoff_manager = handoff_manager or get_handoff_manager()
# 解析配置
self.sub_agents = self._parse_sub_agents(config.sub_agents)
self.execution_config = config.execution_config or {}
# 协作模式
self.enable_handoffs = self.execution_config.get("enable_handoffs", True)
self.max_handoffs = self.execution_config.get("max_handoffs", 5)
logger.info(
"协作编排器初始化",
extra={
"sub_agent_count": len(self.sub_agents),
"enable_handoffs": self.enable_handoffs,
"max_handoffs": self.max_handoffs
}
)
def _parse_sub_agents(self, sub_agents_data: List[Dict]) -> Dict[str, Any]:
"""解析子 Agent 配置
Args:
sub_agents_data: 子 Agent 配置列表
Returns:
Agent ID 到配置的映射
"""
agents = {}
for agent_data in sub_agents_data:
agent_id = agent_data.get("agent_id")
if agent_id:
agents[str(agent_id)] = {
"info": agent_data,
"config": None # 稍后加载
}
return agents
async def execute_with_handoffs(
self,
message: str,
conversation_id: Optional[str] = None,
user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None,
initial_agent_id: Optional[str] = None
) -> Dict[str, Any]:
"""执行支持 handoffs 的多 Agent 协作
Args:
message: 用户消息
conversation_id: 会话 ID
user_id: 用户 ID
variables: 变量参数
initial_agent_id: 初始 Agent ID可选
Returns:
执行结果
"""
if not conversation_id:
conversation_id = str(uuid.uuid4())
# 1. 确定初始 Agent
if not initial_agent_id:
initial_agent_id = await self._select_initial_agent(message, conversation_id)
# 2. 创建或获取 handoff 状态
state = self.handoff_manager.get_state(conversation_id)
if not state:
state = self.handoff_manager.create_state(conversation_id, initial_agent_id)
current_agent_id = state.current_agent_id
handoff_count = 0
conversation_history = []
# 3. 执行循环(支持多次 handoff
while handoff_count < self.max_handoffs:
logger.info(
f"执行 Agent: {current_agent_id}",
extra={
"conversation_id": conversation_id,
"handoff_count": handoff_count,
"message_length": len(message)
}
)
# 3.1 生成当前 Agent 的 handoff 工具
handoff_tools = self.handoff_manager.generate_handoff_tools(
current_agent_id,
self.sub_agents
)
# 3.2 执行当前 Agent
result = await self._execute_agent(
agent_id=current_agent_id,
message=message,
conversation_id=conversation_id,
user_id=user_id,
variables=variables,
handoff_tools=handoff_tools,
conversation_history=conversation_history
)
# 3.3 检查是否有 handoff 请求
handoff_request = result.get("handoff_request")
if not handoff_request:
# 没有 handoff返回结果
return {
"message": result.get("message", ""),
"conversation_id": conversation_id,
"final_agent_id": current_agent_id,
"handoff_count": handoff_count,
"handoff_history": [
{
"from_agent": h.from_agent_id,
"to_agent": h.to_agent_id,
"reason": h.reason
}
for h in state.handoff_history
],
"elapsed_time": result.get("elapsed_time", 0),
"usage": result.get("usage")
}
# 3.4 执行 handoff
target_agent_id = handoff_request.get("target_agent_id")
reason = handoff_request.get("reason", "Agent 请求切换")
context_summary = handoff_request.get("context_summary")
if target_agent_id not in self.sub_agents:
logger.warning(f"目标 Agent 不存在: {target_agent_id}")
# 返回当前结果
return {
"message": result.get("message", ""),
"conversation_id": conversation_id,
"final_agent_id": current_agent_id,
"handoff_count": handoff_count,
"error": f"目标 Agent 不存在: {target_agent_id}",
"elapsed_time": result.get("elapsed_time", 0)
}
# 执行 handoff
state = self.handoff_manager.execute_handoff(
conversation_id=conversation_id,
from_agent_id=current_agent_id,
to_agent_id=target_agent_id,
reason=reason,
user_message=message,
context_summary=context_summary
)
# 更新当前 Agent
current_agent_id = target_agent_id
handoff_count += 1
# 添加到会话历史
conversation_history.append({
"agent_id": state.handoff_history[-1].from_agent_id,
"message": result.get("message", ""),
"handoff_to": target_agent_id,
"reason": reason
})
# 如果 Agent 返回了最终答案,结束循环
if result.get("is_final_answer"):
return {
"message": result.get("message", ""),
"conversation_id": conversation_id,
"final_agent_id": current_agent_id,
"handoff_count": handoff_count,
"handoff_history": [
{
"from_agent": h.from_agent_id,
"to_agent": h.to_agent_id,
"reason": h.reason
}
for h in state.handoff_history
],
"elapsed_time": result.get("elapsed_time", 0),
"usage": result.get("usage")
}
# 达到最大 handoff 次数
logger.warning(
f"达到最大 handoff 次数: {self.max_handoffs}",
extra={"conversation_id": conversation_id}
)
return {
"message": "已达到最大协作次数限制,请重新提问。",
"conversation_id": conversation_id,
"final_agent_id": current_agent_id,
"handoff_count": handoff_count,
"error": "达到最大 handoff 次数",
"elapsed_time": 0
}
async def _select_initial_agent(
self,
message: str,
conversation_id: str
) -> str:
"""选择初始 Agent
Args:
message: 用户消息
conversation_id: 会话 ID
Returns:
Agent ID
"""
# 检查是否有历史状态
state = self.handoff_manager.get_state(conversation_id)
if state:
# 继续使用当前 Agent
return state.current_agent_id
# 简单的关键词匹配
message_lower = message.lower()
for agent_id, agent_data in self.sub_agents.items():
agent_info = agent_data.get("info", {})
capabilities = agent_info.get("capabilities", [])
role = agent_info.get("role", "")
# 检查关键词
keywords = capabilities + ([role] if role else [])
for keyword in keywords:
if keyword.lower() in message_lower:
logger.info(
f"根据关键词选择初始 Agent: {agent_id}",
extra={"keyword": keyword}
)
return agent_id
# 默认使用第一个 Agent
default_agent_id = next(iter(self.sub_agents.keys()))
logger.info(f"使用默认初始 Agent: {default_agent_id}")
return default_agent_id
async def _execute_agent(
self,
agent_id: str,
message: str,
conversation_id: str,
user_id: Optional[str],
variables: Optional[Dict[str, Any]],
handoff_tools: List[AgentHandoffTool],
conversation_history: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""执行单个 Agent
Args:
agent_id: Agent ID
message: 用户消息
conversation_id: 会话 ID
user_id: 用户 ID
variables: 变量参数
handoff_tools: Handoff 工具列表
conversation_history: 会话历史
Returns:
执行结果
"""
start_time = time.time()
try:
# 获取 Agent 配置
agent_data = self.sub_agents.get(agent_id)
if not agent_data:
raise BusinessException(
f"Agent 不存在: {agent_id}",
BizCode.RESOURCE_NOT_FOUND
)
# 加载 Agent 的完整配置
agent_config = await self._load_agent_config(agent_id, agent_data)
# 构建增强的 prompt包含 handoff 上下文)
enhanced_message = self._build_enhanced_message(
message,
conversation_id,
agent_id,
conversation_history
)
# 创建动态工具创建器
tool_creator = DynamicHandoffToolCreator(agent_id, self.sub_agents)
# 获取动态创建的工具
dynamic_tools = tool_creator.get_tools_for_llm()
logger.info(
f"为 Agent {agent_id} 创建了 {len(dynamic_tools)} 个 handoff 工具",
extra={"tool_names": tool_creator.get_tool_names()}
)
# 调用 Agent 的 LLM注入动态工具
response = await self._call_agent_llm(
agent_config=agent_config,
message=enhanced_message,
tools=dynamic_tools,
conversation_history=conversation_history
)
# 构建结果
result = {
"message": response.get("content", ""),
"elapsed_time": time.time() - start_time,
"usage": response.get("usage", {"total_tokens": 0}),
"is_final_answer": True
}
# 检查是否有工具调用handoff
tool_calls = response.get("tool_calls", [])
if tool_calls:
for tool_call in tool_calls:
tool_name = tool_call.get("name")
tool_args = tool_call.get("arguments", {})
# 检查是否是 handoff 工具
if tool_name in tool_creator.get_tool_names():
# 处理 handoff
handoff_request = tool_creator.handle_tool_call(tool_name, tool_args)
if handoff_request:
result["handoff_request"] = handoff_request
result["is_final_answer"] = False
logger.info(
f"检测到 handoff 请求: {agent_id}{handoff_request['target_agent_id']}",
extra={"reason": handoff_request.get("reason")}
)
break
return result
except Exception as e:
logger.error(f"Agent 执行失败: {str(e)}", exc_info=True)
return {
"message": f"Agent 执行出错: {str(e)}",
"elapsed_time": time.time() - start_time,
"error": str(e),
"is_final_answer": True
}
async def _load_agent_config(self, agent_id: str, agent_data: Dict[str, Any]) -> Dict[str, Any]:
"""加载 Agent 的完整配置
Args:
agent_id: Agent ID
agent_data: Agent 数据
Returns:
Agent 配置
"""
from app.models import AppRelease
from app.services.model_service import ModelApiKeyService
# 从数据库加载 Agent Release
try:
agent_uuid = uuid.UUID(agent_id)
release = self.db.get(AppRelease, agent_uuid)
if not release:
raise BusinessException(
f"Agent Release 不存在: {agent_id}",
BizCode.RESOURCE_NOT_FOUND
)
# 获取配置
config_data = release.config or {}
# 获取模型配置
model_config_id = release.default_model_config_id
if not model_config_id:
raise BusinessException(
f"Agent 未配置模型: {agent_id}",
BizCode.AGENT_CONFIG_MISSING
)
# 获取 API Key
api_key_config = ModelApiKeyService.get_a_api_key(self.db, model_config_id)
if not api_key_config:
raise BusinessException(
f"Agent 模型没有可用的 API Key: {agent_id}",
BizCode.API_KEY_NOT_FOUND
)
return {
"agent_id": agent_id,
"name": release.name,
"system_prompt": config_data.get("system_prompt", ""),
"model_name": api_key_config.model_name,
"provider": api_key_config.provider,
"api_key": api_key_config.api_key,
"api_base": api_key_config.api_base,
"model_parameters": config_data.get("model_parameters", {})
}
except ValueError:
raise BusinessException(
f"无效的 Agent ID: {agent_id}",
BizCode.INVALID_PARAMETER
)
async def _call_agent_llm(
self,
agent_config: Dict[str, Any],
message: str,
tools: List[Dict[str, Any]],
conversation_history: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""调用 Agent 的 LLM
Args:
agent_config: Agent 配置
message: 消息
tools: 工具列表(包含 handoff 工具)
conversation_history: 会话历史
Returns:
LLM 响应
"""
try:
# 构建系统提示(包含工具说明)
system_prompt = self._build_system_prompt_with_tools(
agent_config.get("system_prompt", ""),
tools
)
# 构建消息列表
messages = [{"role": "system", "content": system_prompt}]
# 添加历史消息(最近 5 轮)
if conversation_history:
for item in conversation_history[-5:]:
messages.append({
"role": "assistant",
"content": f"[Agent {item['agent_id']}] {item['message']}"
})
# 添加当前消息
messages.append({"role": "user", "content": message})
# 配置 LLM
model_params = agent_config.get("model_parameters", {})
extra_params = {
"temperature": model_params.get("temperature", 0.7),
"max_tokens": model_params.get("max_tokens", 2000)
}
# 如果有工具,添加到配置中
if tools:
extra_params["tools"] = tools
extra_params["tool_choice"] = "auto"
model_config = RedBearModelConfig(
model_name=agent_config["model_name"],
provider=agent_config["provider"],
api_key=agent_config["api_key"],
base_url=agent_config.get("api_base"),
extra_params=extra_params
)
# 创建 LLM 实例
llm = RedBearLLM(model_config, type=ModelType.CHAT)
# 调用 LLM
response = await llm.ainvoke(messages)
# 解析响应
result = {
"content": "",
"tool_calls": [],
"usage": {}
}
if hasattr(response, 'content'):
result["content"] = response.content
else:
result["content"] = str(response)
# 提取工具调用
if hasattr(response, 'tool_calls') and response.tool_calls:
for tool_call in response.tool_calls:
result["tool_calls"].append({
"name": tool_call.function.name if hasattr(tool_call, 'function') else tool_call.name,
"arguments": json.loads(tool_call.function.arguments) if hasattr(tool_call, 'function') else tool_call.arguments
})
# 提取 usage
if hasattr(response, 'usage_metadata'):
result["usage"] = {
"prompt_tokens": response.usage_metadata.get("input_tokens", 0),
"completion_tokens": response.usage_metadata.get("output_tokens", 0),
"total_tokens": response.usage_metadata.get("total_tokens", 0)
}
return result
except Exception as e:
logger.error(f"LLM 调用失败: {str(e)}", exc_info=True)
raise
def _build_system_prompt_with_tools(self, base_prompt: str, tools: List[Dict[str, Any]]) -> str:
"""构建包含工具说明的系统提示
Args:
base_prompt: 基础提示词
tools: 工具列表
Returns:
增强的系统提示
"""
if not tools:
return base_prompt
tools_desc = "\n\n## 可用的协作工具\n\n"
tools_desc += "当你发现用户的问题超出你的专业领域时,可以使用以下工具切换到专业的 Agent\n\n"
for tool in tools:
func = tool.get("function", {})
tools_desc += f"- **{func.get('name')}**: {func.get('description')}\n"
tools_desc += "\n请根据用户问题的性质,判断是否需要切换到其他专业 Agent。"
tools_desc += "如果需要切换,请调用相应的工具并说明原因。"
return base_prompt + tools_desc
def _build_enhanced_message(
self,
message: str,
conversation_id: str,
agent_id: str,
conversation_history: List[Dict[str, Any]]
) -> str:
"""构建增强的消息(包含 handoff 上下文)
Args:
message: 原始消息
conversation_id: 会话 ID
agent_id: 当前 Agent ID
conversation_history: 会话历史
Returns:
增强后的消息
"""
# 获取 handoff 上下文
handoff_context = self.handoff_manager.get_handoff_context_for_agent(
conversation_id,
agent_id
)
if not handoff_context and not conversation_history:
return message
# 构建上下文前缀
context_parts = []
if handoff_context:
context_parts.append(f"[协作上下文] {handoff_context}")
if conversation_history:
context_parts.append("[之前的对话]")
for item in conversation_history[-3:]: # 只保留最近3轮
context_parts.append(
f"- Agent {item['agent_id']}: {item['message'][:100]}"
)
context_parts.append(f"\n[当前问题] {message}")
return "\n".join(context_parts)
def _build_tools_with_handoffs(
self,
handoff_tools: List[AgentHandoffTool]
) -> List[Dict[str, Any]]:
"""构建包含 handoff 工具的工具列表(已废弃,使用动态工具创建)
Args:
handoff_tools: Handoff 工具列表
Returns:
工具 schema 列表
"""
# 这个方法已被 DynamicHandoffToolCreator 替代
# 保留用于向后兼容
tools = []
for tool in handoff_tools:
tools.append(tool.to_tool_schema())
return tools
async def execute_stream_with_handoffs(
self,
message: str,
conversation_id: Optional[str] = None,
user_id: Optional[str] = None,
variables: Optional[Dict[str, Any]] = None,
initial_agent_id: Optional[str] = None
) -> AsyncGenerator[str, None]:
"""流式执行支持 handoffs 的多 Agent 协作
Args:
message: 用户消息
conversation_id: 会话 ID
user_id: 用户 ID
variables: 变量参数
initial_agent_id: 初始 Agent ID
Yields:
SSE 格式的事件流
"""
if not conversation_id:
conversation_id = str(uuid.uuid4())
# 发送开始事件
yield f"data: {json.dumps({'event': 'start', 'conversation_id': conversation_id})}\n\n"
try:
# 执行协作
result = await self.execute_with_handoffs(
message=message,
conversation_id=conversation_id,
user_id=user_id,
variables=variables,
initial_agent_id=initial_agent_id
)
# 发送结果事件
yield f"data: {json.dumps({'event': 'message', 'data': result})}\n\n"
# 发送结束事件
yield f"data: {json.dumps({'event': 'end'})}\n\n"
except Exception as e:
logger.error(f"流式执行失败: {str(e)}")
yield f"data: {json.dumps({'event': 'error', 'error': str(e)})}\n\n"

View File

@@ -0,0 +1,481 @@
"""动态 Handoff 工具创建器
展示如何在运行时动态创建 Agent 切换工具,并将其注入到 LLM 的工具列表中
"""
import json
from typing import Dict, Any, List, Optional, Callable
from pydantic import BaseModel, Field
from app.core.logging_config import get_business_logger
logger = get_business_logger()
class DynamicHandoffToolCreator:
"""动态 Handoff 工具创建器
核心功能:
1. 根据可用 Agent 动态生成工具定义
2. 将工具转换为 LLM 可理解的 schema
3. 处理工具调用并执行 handoff
"""
def __init__(self, current_agent_id: str, available_agents: Dict[str, Any]):
"""初始化工具创建器
Args:
current_agent_id: 当前 Agent ID
available_agents: 可用的 Agent 字典
"""
self.current_agent_id = current_agent_id
self.available_agents = available_agents
self.tools = []
self.tool_handlers = {}
# 动态创建工具
self._create_handoff_tools()
def _create_handoff_tools(self):
"""动态创建所有 handoff 工具"""
for agent_id, agent_data in self.available_agents.items():
if agent_id == self.current_agent_id:
continue # 不创建切换到自己的工具
# 创建工具
tool_def = self._create_single_tool(agent_id, agent_data)
self.tools.append(tool_def)
# 创建工具处理器
handler = self._create_tool_handler(agent_id, agent_data)
self.tool_handlers[tool_def["function"]["name"]] = handler
logger.info(
f"为 Agent {self.current_agent_id} 创建了 {len(self.tools)} 个 handoff 工具"
)
def _create_single_tool(self, target_agent_id: str, agent_data: Dict[str, Any]) -> Dict[str, Any]:
"""创建单个 handoff 工具定义
Args:
target_agent_id: 目标 Agent ID
agent_data: Agent 数据
Returns:
工具定义OpenAI function calling 格式)
"""
agent_info = agent_data.get("info", {})
name = agent_info.get("name", "未命名")
role = agent_info.get("role", "")
capabilities = agent_info.get("capabilities", [])
# 生成工具名称(符合函数命名规范)
tool_name = f"transfer_to_{self._sanitize_name(target_agent_id)}"
# 生成描述
description = f"切换到 {name}"
if role:
description += f"{role}"
if capabilities:
cap_str = "".join(capabilities[:3])
description += f"。擅长: {cap_str}"
description += "。当用户的问题更适合该 Agent 处理时调用此工具。"
# 构建工具定义OpenAI function calling 格式)
tool_def = {
"type": "function",
"function": {
"name": tool_name,
"description": description,
"parameters": {
"type": "object",
"properties": {
"reason": {
"type": "string",
"description": "为什么要切换到该 Agent请简要说明原因。"
},
"context_summary": {
"type": "string",
"description": "需要传递给目标 Agent 的上下文摘要(可选)。例如:之前的计算结果、用户的具体需求等。"
}
},
"required": ["reason"]
}
}
}
return tool_def
def _create_tool_handler(
self,
target_agent_id: str,
agent_data: Dict[str, Any]
) -> Callable:
"""创建工具处理器函数
Args:
target_agent_id: 目标 Agent ID
agent_data: Agent 数据
Returns:
工具处理器函数
"""
def handler(reason: str, context_summary: Optional[str] = None) -> Dict[str, Any]:
"""处理 handoff 工具调用
Args:
reason: 切换原因
context_summary: 上下文摘要
Returns:
Handoff 请求
"""
agent_info = agent_data.get("info", {})
logger.info(
f"Handoff 工具被调用: {self.current_agent_id}{target_agent_id}",
extra={
"reason": reason,
"has_context": bool(context_summary)
}
)
return {
"type": "handoff",
"target_agent_id": target_agent_id,
"target_agent_name": agent_info.get("name", ""),
"reason": reason,
"context_summary": context_summary,
"from_agent_id": self.current_agent_id
}
return handler
def _sanitize_name(self, name: str) -> str:
"""清理名称,使其符合函数命名规范
Args:
name: 原始名称
Returns:
清理后的名称
"""
# 替换特殊字符为下划线
sanitized = name.replace("-", "_").replace(" ", "_")
# 移除其他非法字符
sanitized = "".join(c for c in sanitized if c.isalnum() or c == "_")
return sanitized.lower()
def get_tools_for_llm(self) -> List[Dict[str, Any]]:
"""获取用于 LLM 的工具列表
Returns:
工具定义列表OpenAI function calling 格式)
"""
return self.tools
def handle_tool_call(self, tool_name: str, arguments: Dict[str, Any]) -> Optional[Dict[str, Any]]:
"""处理 LLM 的工具调用
Args:
tool_name: 工具名称
arguments: 工具参数
Returns:
Handoff 请求或 None
"""
handler = self.tool_handlers.get(tool_name)
if not handler:
logger.warning(f"未找到工具处理器: {tool_name}")
return None
try:
return handler(**arguments)
except Exception as e:
logger.error(f"工具调用失败: {tool_name}, 错误: {str(e)}")
return None
def get_tool_names(self) -> List[str]:
"""获取所有工具名称
Returns:
工具名称列表
"""
return [tool["function"]["name"] for tool in self.tools]
# ==================== 使用示例 ====================
def example_usage():
"""展示如何使用动态工具创建器"""
# 1. 准备可用的 Agent 信息
available_agents = {
"math-agent-uuid": {
"info": {
"name": "数学助手",
"role": "数学专家",
"capabilities": ["数学计算", "方程求解", "几何问题"]
}
},
"creative-agent-uuid": {
"info": {
"name": "创意助手",
"role": "创意专家",
"capabilities": ["写作", "诗歌", "故事创作"]
}
},
"code-agent-uuid": {
"info": {
"name": "代码助手",
"role": "编程专家",
"capabilities": ["代码编写", "调试", "代码审查"]
}
}
}
# 2. 为当前 Agent 创建工具
current_agent_id = "general-agent-uuid"
tool_creator = DynamicHandoffToolCreator(current_agent_id, available_agents)
# 3. 获取工具定义(用于 LLM
tools = tool_creator.get_tools_for_llm()
print("=" * 60)
print("动态创建的 Handoff 工具:")
print("=" * 60)
for tool in tools:
print(f"\n工具名称: {tool['function']['name']}")
print(f"描述: {tool['function']['description']}")
print(f"参数: {json.dumps(tool['function']['parameters'], indent=2, ensure_ascii=False)}")
# 4. 模拟 LLM 调用工具
print("\n" + "=" * 60)
print("模拟 LLM 工具调用:")
print("=" * 60)
# LLM 决定切换到数学 Agent
tool_call = {
"name": "transfer_to_math_agent_uuid",
"arguments": {
"reason": "用户问题涉及数学计算",
"context_summary": "用户想解方程 x^2 + 5x + 6 = 0"
}
}
print(f"\nLLM 调用: {tool_call['name']}")
print(f"参数: {json.dumps(tool_call['arguments'], indent=2, ensure_ascii=False)}")
# 5. 处理工具调用
handoff_request = tool_creator.handle_tool_call(
tool_call["name"],
tool_call["arguments"]
)
if handoff_request:
print(f"\n✓ Handoff 请求已创建:")
print(f" 类型: {handoff_request['type']}")
print(f" 从: {handoff_request['from_agent_id']}")
print(f" 到: {handoff_request['target_agent_id']} ({handoff_request['target_agent_name']})")
print(f" 原因: {handoff_request['reason']}")
print(f" 上下文: {handoff_request['context_summary']}")
# ==================== 与 LLM 集成示例 ====================
async def integrate_with_llm_example():
"""展示如何将动态工具集成到 LLM 调用中"""
from app.core.models import RedBearLLM
from app.core.models.base import RedBearModelConfig
# 1. 准备 Agent 信息
available_agents = {
"math-agent": {
"info": {
"name": "数学助手",
"role": "数学专家",
"capabilities": ["计算", "方程"]
}
}
}
# 2. 创建工具
tool_creator = DynamicHandoffToolCreator("general-agent", available_agents)
tools = tool_creator.get_tools_for_llm()
# 3. 构建 LLM 配置
model_config = RedBearModelConfig(
model_name="gpt-4",
provider="openai",
api_key="your-api-key",
extra_params={
"temperature": 0.7,
"tools": tools, # 注入动态创建的工具
"tool_choice": "auto" # 让 LLM 自动决定是否调用工具
}
)
# 4. 创建 LLM 实例
llm = RedBearLLM(model_config)
# 5. 构建消息(包含系统提示)
system_prompt = """你是一个智能助手。当用户的问题超出你的能力范围时,你可以使用工具切换到专业的 Agent。
可用的切换工具:
- transfer_to_math_agent: 当遇到数学问题时使用
请根据用户问题判断是否需要切换。"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": "帮我计算 3*6+15 的结果"}
]
# 6. 调用 LLM
response = await llm.ainvoke(messages)
# 7. 检查是否有工具调用
if hasattr(response, 'tool_calls') and response.tool_calls:
for tool_call in response.tool_calls:
print(f"\n✓ LLM 调用了工具: {tool_call.name}")
print(f" 参数: {tool_call.arguments}")
# 处理工具调用
handoff_request = tool_creator.handle_tool_call(
tool_call.name,
tool_call.arguments
)
if handoff_request:
print(f"\n✓ 执行 Handoff:")
print(f" 切换到: {handoff_request['target_agent_name']}")
print(f" 原因: {handoff_request['reason']}")
# 这里可以执行实际的 Agent 切换逻辑
# await execute_handoff(handoff_request)
else:
print(f"\n✓ LLM 直接回复: {response.content}")
# ==================== 完整的 Agent 执行流程 ====================
class AgentExecutorWithHandoffs:
"""支持 Handoffs 的 Agent 执行器"""
def __init__(self, agent_id: str, agent_config: Any, available_agents: Dict[str, Any]):
"""初始化执行器
Args:
agent_id: 当前 Agent ID
agent_config: Agent 配置
available_agents: 可用的其他 Agent
"""
self.agent_id = agent_id
self.agent_config = agent_config
self.available_agents = available_agents
# 创建工具创建器
self.tool_creator = DynamicHandoffToolCreator(agent_id, available_agents)
async def execute(
self,
message: str,
conversation_history: List[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""执行 Agent支持 handoff
Args:
message: 用户消息
conversation_history: 会话历史
Returns:
执行结果,可能包含 handoff_request
"""
from app.core.models import RedBearLLM
from app.core.models.base import RedBearModelConfig
# 1. 获取动态工具
tools = self.tool_creator.get_tools_for_llm()
# 2. 构建系统提示(包含工具说明)
system_prompt = self._build_system_prompt_with_tools()
# 3. 构建消息
messages = [{"role": "system", "content": system_prompt}]
# 添加历史消息
if conversation_history:
messages.extend(conversation_history)
# 添加当前消息
messages.append({"role": "user", "content": message})
# 4. 配置 LLM注入工具
model_config = RedBearModelConfig(
model_name=self.agent_config.model_name,
provider=self.agent_config.provider,
api_key=self.agent_config.api_key,
extra_params={
"temperature": 0.7,
"tools": tools, # 动态工具
"tool_choice": "auto"
}
)
llm = RedBearLLM(model_config)
# 5. 调用 LLM
response = await llm.ainvoke(messages)
# 6. 处理响应
result = {
"message": response.content if hasattr(response, 'content') else str(response),
"agent_id": self.agent_id
}
# 7. 检查工具调用
if hasattr(response, 'tool_calls') and response.tool_calls:
for tool_call in response.tool_calls:
# 检查是否是 handoff 工具
if tool_call.name in self.tool_creator.get_tool_names():
# 处理 handoff
handoff_request = self.tool_creator.handle_tool_call(
tool_call.name,
tool_call.arguments
)
if handoff_request:
result["handoff_request"] = handoff_request
result["is_final_answer"] = False
break
else:
# 处理其他业务工具
pass
return result
def _build_system_prompt_with_tools(self) -> str:
"""构建包含工具说明的系统提示"""
base_prompt = self.agent_config.system_prompt or "你是一个智能助手。"
# 添加工具说明
tool_names = self.tool_creator.get_tool_names()
if tool_names:
tools_desc = "\n\n你可以使用以下工具切换到专业的 Agent\n"
for tool in self.tool_creator.get_tools_for_llm():
tools_desc += f"- {tool['function']['name']}: {tool['function']['description']}\n"
tools_desc += "\n当用户的问题超出你的能力范围时,请使用相应的工具切换到专业 Agent。"
return base_prompt + tools_desc
return base_prompt
if __name__ == "__main__":
# 运行示例
print("\n🚀 动态 Handoff 工具创建示例\n")
example_usage()
print("\n\n" + "=" * 60)
print("完成!查看上面的输出了解工具创建流程。")
print("=" * 60)

View File

@@ -0,0 +1,314 @@
"""Multi-Agent Service 的 Handoffs 集成
将 Agent Handoffs 功能集成到现有的 Multi-Agent 系统中
"""
import uuid
import time
from typing import Dict, Any, Optional, AsyncGenerator
from sqlalchemy.orm import Session
from app.services.agent_handoff import get_handoff_manager
from app.services.collaborative_orchestrator import CollaborativeOrchestrator
from app.schemas.multi_agent_schema import MultiAgentRunRequest
from app.core.logging_config import get_business_logger
from app.core.exceptions import BusinessException
from app.core.error_codes import BizCode
logger = get_business_logger()
class MultiAgentHandoffsService:
"""Multi-Agent Handoffs 服务 - 扩展现有的 Multi-Agent Service"""
def __init__(self, db: Session, multi_agent_service):
"""初始化服务
Args:
db: 数据库会话
multi_agent_service: 现有的 MultiAgentService 实例
"""
self.db = db
self.multi_agent_service = multi_agent_service
self.handoff_manager = get_handoff_manager()
logger.info("Multi-Agent Handoffs 服务初始化完成")
async def run_with_handoffs(
self,
app_id: uuid.UUID,
request: MultiAgentRunRequest
) -> Dict[str, Any]:
"""运行支持 handoffs 的多 Agent 任务
Args:
app_id: 应用 ID
request: 运行请求
Returns:
执行结果
"""
start_time = time.time()
try:
# 1. 获取配置
config = self.multi_agent_service.get_config(app_id)
if not config:
raise BusinessException(
"多 Agent 配置不存在",
BizCode.RESOURCE_NOT_FOUND
)
# 2. 检查是否启用 handoffs
execution_config = config.execution_config or {}
enable_handoffs = execution_config.get("enable_handoffs", False)
if not enable_handoffs:
# 降级到普通模式
logger.info("Handoffs 未启用,使用普通模式")
return await self.multi_agent_service.run(app_id, request)
# 3. 创建协作编排器
orchestrator = CollaborativeOrchestrator(
db=self.db,
config=config,
handoff_manager=self.handoff_manager
)
# 4. 执行协作
result = await orchestrator.execute_with_handoffs(
message=request.message,
conversation_id=str(request.conversation_id) if request.conversation_id else None,
user_id=request.user_id,
variables=request.variables
)
# 5. 增强结果
result["mode"] = "handoffs"
result["elapsed_time"] = time.time() - start_time
logger.info(
"Handoffs 执行完成",
extra={
"app_id": str(app_id),
"handoff_count": result.get("handoff_count", 0),
"final_agent": result.get("final_agent_id"),
"elapsed_time": result["elapsed_time"]
}
)
return result
except Exception as e:
logger.error(f"Handoffs 执行失败: {str(e)}")
# 降级到普通模式
logger.info("降级到普通模式")
return await self.multi_agent_service.run(app_id, request)
async def run_stream_with_handoffs(
self,
app_id: uuid.UUID,
request: MultiAgentRunRequest
) -> AsyncGenerator[str, None]:
"""流式运行支持 handoffs 的多 Agent 任务
Args:
app_id: 应用 ID
request: 运行请求
Yields:
SSE 格式的事件流
"""
try:
# 1. 获取配置
config = self.multi_agent_service.get_config(app_id)
if not config:
yield f"data: {{\"event\": \"error\", \"error\": \"配置不存在\"}}\n\n"
return
# 2. 检查是否启用 handoffs
execution_config = config.execution_config or {}
enable_handoffs = execution_config.get("enable_handoffs", False)
if not enable_handoffs:
# 降级到普通流式模式
async for event in self.multi_agent_service.run_stream(app_id, request):
yield event
return
# 3. 创建协作编排器
orchestrator = CollaborativeOrchestrator(
db=self.db,
config=config,
handoff_manager=self.handoff_manager
)
# 4. 流式执行
async for event in orchestrator.execute_stream_with_handoffs(
message=request.message,
conversation_id=str(request.conversation_id) if request.conversation_id else None,
user_id=request.user_id,
variables=request.variables
):
yield event
except Exception as e:
logger.error(f"流式 Handoffs 执行失败: {str(e)}")
yield f"data: {{\"event\": \"error\", \"error\": \"{str(e)}\"}}\n\n"
def get_handoff_history(
self,
conversation_id: str
) -> Optional[Dict[str, Any]]:
"""获取会话的 handoff 历史
Args:
conversation_id: 会话 ID
Returns:
Handoff 历史信息
"""
state = self.handoff_manager.get_state(conversation_id)
if not state:
return None
return {
"conversation_id": state.conversation_id,
"current_agent_id": state.current_agent_id,
"handoff_count": state.get_handoff_count(),
"handoff_history": [
{
"from_agent": h.from_agent_id,
"to_agent": h.to_agent_id,
"reason": h.reason,
"timestamp": h.timestamp.isoformat(),
"user_message": h.user_message,
"context_summary": h.context_summary
}
for h in state.handoff_history
],
"created_at": state.created_at.isoformat(),
"updated_at": state.updated_at.isoformat()
}
def clear_handoff_state(self, conversation_id: str):
"""清除会话的 handoff 状态
Args:
conversation_id: 会话 ID
"""
self.handoff_manager.clear_state(conversation_id)
logger.info(f"清除 handoff 状态: {conversation_id}")
async def test_handoff_routing(
self,
app_id: uuid.UUID,
message: str
) -> Dict[str, Any]:
"""测试 handoff 路由决策(不实际执行)
Args:
app_id: 应用 ID
message: 测试消息
Returns:
路由决策结果
"""
# 1. 获取配置
config = self.multi_agent_service.get_config(app_id)
if not config:
raise BusinessException(
"多 Agent 配置不存在",
BizCode.RESOURCE_NOT_FOUND
)
# 2. 解析 sub agents
sub_agents = {}
for agent_data in config.sub_agents:
agent_id = agent_data.get("agent_id")
if agent_id:
sub_agents[str(agent_id)] = {
"info": agent_data
}
# 3. 测试路由
test_conversation_id = f"test-{uuid.uuid4()}"
# 选择初始 Agent
initial_agent_id = None
message_lower = message.lower()
for agent_id, agent_data in sub_agents.items():
agent_info = agent_data.get("info", {})
capabilities = agent_info.get("capabilities", [])
role = agent_info.get("role", "")
keywords = capabilities + ([role] if role else [])
for keyword in keywords:
if keyword.lower() in message_lower:
initial_agent_id = agent_id
break
if initial_agent_id:
break
if not initial_agent_id:
initial_agent_id = next(iter(sub_agents.keys()))
# 4. 生成 handoff 工具
handoff_tools = self.handoff_manager.generate_handoff_tools(
initial_agent_id,
sub_agents
)
# 5. 检查是否需要 handoff
handoff_suggestion = self.handoff_manager.should_handoff(
conversation_id=test_conversation_id,
current_agent_id=initial_agent_id,
message=message,
available_agents=sub_agents
)
return {
"message": message,
"initial_agent_id": initial_agent_id,
"initial_agent_name": sub_agents[initial_agent_id]["info"].get("name", ""),
"available_handoff_tools": [
{
"name": tool.name,
"target_agent_id": tool.target_agent_id,
"target_agent_name": tool.target_agent_name,
"description": tool.description
}
for tool in handoff_tools
],
"handoff_suggestion": handoff_suggestion,
"total_agents": len(sub_agents)
}
# 使用示例
"""
from app.services.multi_agent_service import MultiAgentService
from app.services.multi_agent_handoffs_integration import MultiAgentHandoffsService
# 创建服务
multi_agent_service = MultiAgentService(db)
handoffs_service = MultiAgentHandoffsService(db, multi_agent_service)
# 运行 handoffs
result = await handoffs_service.run_with_handoffs(
app_id=app_id,
request=MultiAgentRunRequest(
message="帮我解方程然后写诗",
conversation_id=uuid.uuid4(),
user_id="user-123"
)
)
# 查看 handoff 历史
history = handoffs_service.get_handoff_history(str(result["conversation_id"]))
print(f"Handoff 次数: {history['handoff_count']}")
for h in history['handoff_history']:
print(f"{h['from_agent']}{h['to_agent']}: {h['reason']}")
"""