Toadied's picture
2312
8b383ad verified
Raw
History Blame Contribute Delete
18.9 kB
"""
Hybrid ReAct Agent - 企业级架构实现
结合 ReAct 的思维链 (CoT) 与 Function Calling 的结构化执行
+ 四层自动记忆 + 长期记忆自动注入(真正个性化)
"""
import asyncio
import json
import logging
from typing import Optional, List, Dict, Any
from helloAgents.tools.builtin.memory_tool import MemoryTool
from ..core.agent import Agent
from ..core.llm import HelloAgentsLLM
from ..core.config import Config
from ..tools.registry import ToolRegistry
from redis_config import get_redis, QUEUE_MEMORY
redis = get_redis()
# 默认 Hybrid ReAct 提示词模板(企业级干净版)
DEFAULT_HYBRID_REACT_PROMPT = """你是一个具备深度推理和行动能力的企业级 AI 助手。
## 可用工具
{tools}
## 工作流程(严格遵守)
1. **思考**:在 content 中自然写出你的分析、推理、判断。
- 不需要任何前缀,不要写 Thought、Action、Finish。
2. **行动**:需要外部信息时,使用 tool_calls 调用工具。
- 不要在文本里模拟工具调用。
3. **结束**:信息足够回答用户时,
- 直接输出最终答案
- 不输出 tool_calls
- 不再写思考过程
## 重要规则
- 每次必须先思考再决定是否调用工具。
- 无 tool_calls 代表直接回答。
- 最终回答必须清晰、直接、干净。
## 用户的记忆
{memory}
## 当前任务
**Question:** {question}
## 执行历史
{history}
请根据以上信息进行推理并回应:"""
class ReActAgent(Agent):
"""
企业级 Hybrid ReAct Agent + 四层自动记忆 + 长期记忆自动注入
架构:记忆完全基座化,对 LLM 透明,业务工具无耦合
"""
def __init__(
self,
name: str,
llm: HelloAgentsLLM,
tool_registry: Optional[ToolRegistry] = None,
system_prompt: Optional[str] = None,
config: Optional[Config] = None,
max_steps: int = 5,
custom_prompt: Optional[str] = None
):
super().__init__(name, llm, system_prompt, config)
self.tool_registry = tool_registry if tool_registry else ToolRegistry()
self.max_steps = max_steps
self.prompt_template = custom_prompt or DEFAULT_HYBRID_REACT_PROMPT
self._tool_schemas = self._build_tool_schemas()
def _build_tool_schemas(self) -> List[Dict[str, Any]]:
"""构建 OpenAI 兼容的工具 Schema(memory 工具对 LLM 隐藏)"""
if not self.tool_registry:
return []
schemas = []
for tool in self.tool_registry.get_all_tools():
if tool.name == "memory":
continue
properties = {}
required = []
try:
params = tool.get_parameters()
for p in params:
p_type = (p.type or "string").lower()
if p_type not in ["string", "number", "integer", "boolean", "array", "object"]:
p_type = "string"
properties[p.name] = {
"type": p_type,
"description": p.description or ""
}
if getattr(p, "required", True):
required.append(p.name)
except Exception:
properties["input"] = {"type": "string", "description": "输入内容"}
required = ["input"]
schemas.append({
"type": "function",
"function": {
"name": tool.name,
"description": tool.description or "",
"parameters": {
"type": "object",
"properties": properties,
"required": required
}
}
})
# 兼容函数注册表中的裸函数
func_map = getattr(self.tool_registry, "_functions", {})
for name, info in func_map.items():
if not any(s["function"]["name"] == name for s in schemas):
schemas.append({
"type": "function",
"function": {
"name": name,
"description": info.get("description", ""),
"parameters": {
"type": "object",
"properties": {"input": {"type": "string", "description": "输入文本"}},
"required": ["input"]
}
}
})
return schemas
def add_tool(self, tool):
self.tool_registry.register_tool(tool)
self._tool_schemas = self._build_tool_schemas()
def _convert_params(self, tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
tool = self.tool_registry.get_tool(tool_name)
if not tool:
return args
try:
params = tool.get_parameters()
type_map = {p.name: p.type.lower() for p in params}
converted = {}
for k, v in args.items():
t = type_map.get(k, "string")
try:
if t in ["integer", "int"]:
converted[k] = int(v)
elif t in ["number", "float"]:
converted[k] = float(v)
elif t in ["boolean", "bool"]:
converted[k] = str(v).lower() in ["true", "1", "yes"]
else:
converted[k] = v
except (ValueError, TypeError):
converted[k] = v
return converted
except Exception:
return args
# =========================================================================
# 个性化核心:每轮自动加载长期记忆并注入 Prompt
# =========================================================================
async def _get_user_long_term_memory(self, input_text: str, **kwargs) -> str:
try:
memory_tool: MemoryTool = self.tool_registry.get_tool("memory")
if not memory_tool:
return "无用户记忆"
task = {
"tool_name": "memory",
"input_data": {
"action": "search",
"query": input_text,
"memory_types": ["working", "semantic", "episodic"],
"user_id": kwargs.get("user_id"),
"session_id": kwargs.get("session_id"),
"limit": 8
}
}
from ..tools.async_executor import run_parallel_tools
results = await run_parallel_tools(self.tool_registry, [task])
memory_result = results[0]["result"] if results else ""
return f"\n{memory_result}" if memory_result else "无用户记忆"
except Exception:
return "无用户记忆"
# =========================================================================
# 四层记忆自动保存
# =========================================================================
def _auto_save_all_memories(self, user_input: str, final_answer: str, system_prompt: str, **kwargs):
# 工作记忆
work_tasks = {
"action": "add",
"user_content": user_input,
"assistant_content": final_answer,
"memory_type": "working",
"user_id": kwargs.get("user_id"),
"session_id": kwargs.get("session_id"),
"importance": 0.3
}
redis.lpush(QUEUE_MEMORY, json.dumps(work_tasks))
# 语义记忆
semantic_tasks = {
"action": "add",
"content": user_input,
"memory_type": "semantic",
"user_id": kwargs.get("user_id"),
"session_id": None,
"importance": 0.8
}
redis.lpush(QUEUE_MEMORY, json.dumps(semantic_tasks))
# 事件记忆
episodic_tasks = {
"action": "add",
"role": "user",
"content": user_input,
"memory_type": "episodic",
"user_id": kwargs.get("user_id"),
"session_id": kwargs.get("session_id"),
"importance": 0.5
}
redis.lpush(QUEUE_MEMORY, json.dumps(episodic_tasks))
episodic_answer_tasks = {
"action": "add",
"role": "assistant",
"content": final_answer,
"memory_type": "episodic",
"user_id": kwargs.get("user_id"),
"session_id": kwargs.get("session_id"),
"importance": 0.5
}
redis.lpush(QUEUE_MEMORY, json.dumps(episodic_answer_tasks))
episodic_observation_tasks = {
"action": "add",
"role": "observation",
"content": system_prompt,
"memory_type": "episodic",
"user_id": kwargs.get("user_id"),
"session_id": kwargs.get("session_id"),
"importance": 0.5
}
redis.lpush(QUEUE_MEMORY, json.dumps(episodic_observation_tasks))
summary = self._extract_summary_by_llm(user_input, final_answer, **kwargs)
if summary:
summary_tasks = {
"action": "add",
"role": "summary",
"content": summary,
"memory_type": "episodic",
"user_id": kwargs.get("user_id"),
"session_id": kwargs.get("session_id"),
"importance": 0.5
}
redis.lpush(QUEUE_MEMORY, json.dumps(summary_tasks))
def _extract_summary_by_llm(self, user_input: str, final_answer: str, **kwargs) -> str:
"""
让 LLM 根据用户问题 + 助手回复 提取核心摘要,用于精简记忆存储
"""
try:
# 构造简洁的摘要提取提示词
prompt = f"""
请你对以下【用户问题】和【助手回复】进行精简摘要提取:
要求:
1. 只保留核心信息,不超过50字
2. 语言简洁、客观、通顺
3. 直接输出摘要,不要额外解释
用户问题:{user_input}
助手回复:{final_answer}
摘要:
"""
# 调用你的 LLM 接口(根据你项目的实际 LLM 调用方式修改)
# 这里是通用示例,你可以替换成项目真实调用方法
messages = [{"role": "user", "content": prompt}]
summary = self.llm.invoke(messages)
# 清理空白字符
summary = summary.strip()
return summary if summary else ""
except Exception as e:
print(f"LLM 提取摘要失败:{str(e)}")
return ""
# =========================================================================
# ReAct 主运行流程(企业级最终版)
# =========================================================================
async def run(self, input_text: str, **kwargs) -> str:
current_step = 0
final_answer = ""
react_trace = [] # 仅用于构建执行历史,不发给 LLM ✅
system_prompt = ""
print(f"\n🤖 {self.name} 开始处理:{input_text}")
memory = await self._get_user_long_term_memory(input_text, **kwargs)
tools_desc = self.tool_registry.get_tools_description()
while current_step < self.max_steps:
current_step += 1
print(f"\n--- 第 {current_step} 步 ---")
# 核心:所有思考轨迹 → 放入 ## 执行历史
history_str = self._build_react_history_str(react_trace)
system_prompt = self.prompt_template.format(
tools=tools_desc,
question=input_text,
history=history_str,
memory=memory
)
# LLM 永远只收到这一条消息 ✅ 干净稳定
llm_messages = [{"role": "user", "content": system_prompt}]
try:
client = self.llm._client
response = client.chat.completions.create(
model=self.llm.model,
messages=llm_messages,
tools=self._tool_schemas or None,
tool_choice="auto" if self._tool_schemas else None,
temperature=self.llm.temperature,
max_tokens=self.llm.max_tokens
)
assistant_message = response.choices[0].message
content = assistant_message.content or ""
tool_calls = assistant_message.tool_calls or []
except Exception as e:
print(f"❌ LLM 调用失败:{e}")
assistant_entry = {
"role": "assistant",
"content": f"LLM 调用失败:{str(e)}"
}
react_trace.append(assistant_entry)
history_str = self._build_react_history_str(react_trace)
system_prompt = self.prompt_template.format(
tools=tools_desc,
question=input_text,
history=history_str,
memory=memory
)
break
# 把思考加入轨迹(用于下一轮执行历史)
# 把思考 + 工具调用 一起加入轨迹(关键修复)
assistant_entry = {
"role": "assistant",
"content": content
}
# 把 tool_calls 也存进去!
if tool_calls:
assistant_entry["tool_calls"] = [
{
"id": tc.id,
"function": {
"name": tc.function.name,
"arguments": tc.function.arguments
}
} for tc in tool_calls
]
react_trace.append(assistant_entry)
# 无工具调用 = 结束
if not tool_calls:
final_answer = content
history_str = self._build_react_history_str(react_trace)
system_prompt = self.prompt_template.format(
tools=tools_desc,
question=input_text,
history=history_str,
memory=memory
)
break
# 并行执行工具
parallel_tasks = []
tool_map = {}
for idx, tc in enumerate(tool_calls):
func_name = tc.function.name
try:
args = json.loads(tc.function.arguments)
except json.JSONDecodeError:
args = {}
if func_name != "send_qq_email":
args["user_id"] = kwargs.get("user_id")
args["session_id"] = kwargs.get("session_id")
args = self._convert_params(func_name, args)
parallel_tasks.append({"tool_name": func_name, "input_data": args})
tool_map[idx] = tc
from ..tools.async_executor import run_parallel_tools
results = await run_parallel_tools(self.tool_registry, parallel_tasks, max_workers=4)
# 工具结果加入轨迹(同时携带参数)
for idx, res in enumerate(results):
tc = tool_map[idx]
try:
args = json.loads(tc.function.arguments)
except:
args = {}
react_trace.append({
"role": "tool",
"tool_call_id": tc.id,
"name": res["tool_name"],
"arguments": tc.function.arguments, # 👈 新增:把参数存在这里
"content": str(res["result"])
})
if not final_answer:
final_answer = "抱歉,无法在限定步数内完成任务。"
assistant_entry = {
"role": "assistant",
"content": final_answer
}
react_trace.append(assistant_entry)
history_str = self._build_react_history_str(react_trace)
system_prompt = self.prompt_template.format(
tools=tools_desc,
question=input_text,
history=history_str,
memory=memory
)
# 异步保存记忆
try:
self._auto_save_all_memories(input_text, final_answer, system_prompt, **kwargs)
except Exception:
pass
return final_answer
# =========================================================================
# 构建 ReAct 执行历史(企业级可审计)
# =========================================================================
def _build_react_history_str(self, react_trace: List[Dict[str, Any]]) -> str:
lines = []
step = 1
for msg in react_trace:
content = msg.get("content", "").strip()
if msg["role"] == "assistant":
# 思考过程
if content:
lines.append(f"Step {step} - Thought: {content}")
step += 1
# 如果有工具调用,在这里一起显示
tool_calls = msg.get("tool_calls", [])
for tc in tool_calls:
func = tc.get("function", {})
func_name = func.get("name", "unknown_tool")
arguments = func.get("arguments", "{}")
# 格式化显示:工具名 + 参数
lines.append(f"Step {step} - Action: 调用工具 [{func_name}],参数:{arguments}")
step += 1
elif msg["role"] == "tool":
# 工具返回结果
tool_name = msg.get("name", "unknown_tool")
arguments = msg.get("arguments", "{}")
if content:
lines.append(f"Step {step} - Observation: 工具 [{tool_name}] 参数:{arguments} 返回:{content}")
step += 1
return "\n".join(lines) if lines else "无历史记录"