Compare commits

...

3 Commits

18 changed files with 1591 additions and 0 deletions

1
client/__init__.py Normal file
View File

@ -0,0 +1 @@
"""__init__.py"""

158
client/agent_client.py Normal file
View File

@ -0,0 +1,158 @@
"""客户端:用户交互 & 会话管理"""
"""
client/agent_client.py
Agent 客户端协调 LLM 引擎MCP 服务器记忆模块驱动完整 Agent 执行流程
"""
from dataclasses import dataclass
from llm.llm_engine import LLMEngine
from mcp.mcp_protocol import MCPMethod, MCPRequest
from mcp.mcp_server import MCPServer
from memory.memory_store import MemoryStore
from utils.logger import get_logger
# ── 单轮执行结果 ───────────────────────────────────────────────
@dataclass
class AgentResponse:
"""一次完整 Agent 调用的结果"""
user_input: str
final_reply: str
tool_used: str | None = None
tool_output: str | None = None
success: bool = True
error: str | None = None
# ── Agent 客户端 ───────────────────────────────────────────────
class AgentClient:
"""
Agent 客户端实现完整的 ReAct 执行循环
执行流程 (5):
1. [CLIENT] 接收用户输入写入 Memory
2. [LLM] 分析意图决策是否调用工具
3. [MCP] 构造 JSON-RPC 请求发送给 MCP Server
4. [TOOL] MCP Server 执行工具返回结果
5. [LLM] 整合结果生成最终回复写入 Memory
使用示例:
client = AgentClient(llm=llm, mcp_server=mcp, memory=memory)
response = client.chat("帮我计算 100 * 200")
print(response.final_reply)
"""
def __init__(
self,
llm: LLMEngine,
mcp_server: MCPServer,
memory: MemoryStore,
):
self.llm = llm
self.mcp_server = mcp_server
self.memory = memory
self.logger = get_logger("CLIENT")
self.logger.info("💻 Agent Client 初始化完成")
# ── 主入口 ──────────────────────────────────────────────────
def chat(self, user_input: str) -> AgentResponse:
"""
处理一轮用户对话执行完整 Agent 流程
Args:
user_input: 用户输入的自然语言文本
Returns:
AgentResponse 实例
"""
self.logger.info(f"{'='*55}")
self.logger.info(f"📨 Step 1 [CLIENT] 收到用户输入: {user_input}")
# ── Step 1: 记录用户消息 ────────────────────────────────
self.memory.add_user_message(user_input)
context = self.memory.get_context_summary()
# ── Step 2: LLM 推理决策 ────────────────────────────────
self.logger.info("🧠 Step 2 [LLM] 开始推理,分析意图...")
tool_schemas = self.mcp_server.get_tool_schemas()
decision = self.llm.think_and_decide(user_input, tool_schemas, context)
# ── 分支:是否需要工具 ──────────────────────────────────
if not decision.need_tool:
return self._handle_direct_reply(user_input, context)
return self._handle_tool_call(user_input, decision, context)
# ── 私有流程方法 ────────────────────────────────────────────
def _handle_direct_reply(self, user_input: str, context: str) -> AgentResponse:
"""无需工具时直接生成回复"""
self.logger.info("💬 无需工具,直接生成回复")
reply = self.llm.generate_direct_reply(user_input, context)
self.memory.add_assistant_message(reply)
return AgentResponse(user_input=user_input, final_reply=reply)
def _handle_tool_call(
self,
user_input: str,
decision,
context: str,
) -> AgentResponse:
"""执行工具调用的完整流程Step 3 → 4 → 5"""
# ── Step 3: 构造 MCP 请求 ───────────────────────────────
mcp_request: MCPRequest = decision.to_mcp_request()
self.logger.info(
f"📡 Step 3 [MCP] 发送工具调用请求\n"
f" 方法: {mcp_request.method}\n"
f" 工具: {decision.tool_name}\n"
f" 参数: {decision.arguments}\n"
f" 请求体: {mcp_request.to_dict()}"
)
# ── Step 4: MCP Server 执行工具 ─────────────────────────
self.logger.info(f"🔧 Step 4 [TOOL] MCP Server 执行工具 [{decision.tool_name}]...")
mcp_response = self.mcp_server.handle_request(mcp_request)
if not mcp_response.success:
error_msg = f"工具调用失败: {mcp_response.error}"
self.logger.error(f"{error_msg}")
return AgentResponse(
user_input=user_input,
final_reply=f"抱歉,工具调用失败:{mcp_response.error.get('message')}",
tool_used=decision.tool_name,
success=False,
error=error_msg,
)
tool_output = mcp_response.content
self.logger.info(f"✅ 工具执行成功,输出: {tool_output[:80]}...")
self.memory.add_tool_result(decision.tool_name, tool_output)
# ── Step 5: LLM 整合结果,生成最终回复 ──────────────────
self.logger.info("✍️ Step 5 [LLM] 整合工具结果,生成最终回复...")
final_reply = self.llm.generate_final_reply(
user_input, decision.tool_name, tool_output, context
)
self.memory.add_assistant_message(final_reply)
self.logger.info(f"🎉 [CLIENT] 流程完成,回复已返回")
return AgentResponse(
user_input=user_input,
final_reply=final_reply,
tool_used=decision.tool_name,
tool_output=tool_output,
)
# ── 工具方法 ────────────────────────────────────────────────
def get_memory_stats(self) -> dict:
"""获取当前记忆统计"""
return self.memory.stats()
def clear_session(self) -> None:
"""清空当前会话"""
self.memory.clear_history()
self.logger.info("🗑 会话已清空")

1
llm/__init__.py Normal file
View File

@ -0,0 +1 @@
"""__init__.py"""

248
llm/llm_engine.py Normal file
View File

@ -0,0 +1,248 @@
"""LLM 引擎:意图理解 & 工具决策"""
"""
llm/llm_engine.py
LLM 引擎负责意图理解工具选择决策最终回复生成
生产环境可替换 _call_llm_api() 为真实 API 调用OpenAI / Anthropic
"""
import json
import re
from dataclasses import dataclass
from mcp.mcp_protocol import MCPRequest, MCPMethod, ToolSchema
from utils.logger import get_logger
from openai import OpenAI
# ── 工具调用决策结果 ───────────────────────────────────────────
@dataclass
class ToolDecision:
"""LLM 决策是否调用工具及调用参数"""
need_tool: bool
tool_name: str = ""
arguments: dict = None
reasoning: str = "" # 推理过程说明
def __post_init__(self):
self.arguments = self.arguments or {}
def to_mcp_request(self) -> MCPRequest | None:
"""将工具决策转换为 MCP 请求"""
if not self.need_tool:
return None
return MCPRequest(
method=MCPMethod.TOOLS_CALL,
params={"name": self.tool_name, "arguments": self.arguments},
)
class MonicaClient:
BASE_URL = "https://openapi.monica.im/v1"
def __init__(self, api_key):
self.client = OpenAI(base_url=self.BASE_URL,
api_key=api_key)
def create(self, model_name, tool_schemas, user_input) -> ToolDecision:
tools = [{
"name": s.name,
"description": s.description,
"parameters": s.parameters} for s in tool_schemas]
completion = self.client.chat.completions.create(
model=model_name,
functions=tools,
messages = [
{
"role": "user",
"content": [{
"type": "text",
"text": user_input
}]
}
]
)
response = json.loads(completion.choices[0].message.content)
return ToolDecision(need_tool=response['need_tool'],
tool_name=response['tool_name'],
arguments=response['arguments'],
reasoning=response['reasoning'])
# ── LLM 引擎 ──────────────────────────────────────────────────
class LLMEngine:
"""
LLM 推理引擎ReAct 模式
执行流程:
1. 接收用户输入 + 工具列表
2. 分析意图决策是否调用工具think
3. 若需要工具生成 MCPRequestact
4. 接收工具结果生成最终回复observe
生产环境替换:
_call_llm_api() 替换为真实 LLM API 调用即可
其余流程控制逻辑保持不变
"""
API_KEY = "sk-AUmOuFI731Ty5Nob38jY26d8lydfDT-QkE2giqb0sCuPCAE2JH6zjLM4lZLpvL5WMYPOocaMe2FwVDmqM_9KimmKACjR"
def __init__(self, model_name: str = "claude-sonnet-4-6"):
self.model_name = model_name
self.logger = get_logger("LLM")
self.logger.info(f"🧠 LLM 引擎初始化,模型: {model_name}")
self.client = MonicaClient(api_key=self.API_KEY)
# ── 核心推理流程 ────────────────────────────────────────────
def think_and_decide(
self,
user_input: str,
tool_schemas: list[ToolSchema],
context: str = "",
) -> ToolDecision:
"""
Step 1 & 2: 理解意图决策工具调用Think 阶段
Args:
user_input: 用户输入文本
tool_schemas: 可用工具的 Schema 列表
context: 对话历史上下文摘要
Returns:
ToolDecision 实例
"""
self.logger.info(f"💭 分析意图: {user_input[:50]}...")
# 构造 Prompt生产环境发送给真实 LLM
prompt = self._build_decision_prompt(user_input, tool_schemas, context)
self.logger.debug(f"📝 Prompt 已构造 ({len(prompt)} chars)")
# 调用 LLMDemo 中使用规则模拟)
# decision = self._call_llm_api(user_input, tool_schemas)
decision = self._call_llm_api(prompt, tool_schemas)
self.logger.info(
f"🎯 决策结果: {'调用工具 [' + decision.tool_name + ']' if decision.need_tool else '直接回复'}"
)
self.logger.debug(f"💡 推理: {decision.reasoning}")
return decision
def generate_final_reply(
self,
user_input: str,
tool_name: str,
tool_output: str,
context: str = "",
) -> str:
"""
Step 5: 整合工具结果生成最终自然语言回复Observe 阶段
Args:
user_input: 原始用户输入
tool_name: 被调用的工具名称
tool_output: 工具返回的原始输出
context: 对话历史上下文
Returns:
最终回复字符串
"""
self.logger.info("✍️ 整合工具结果,生成最终回复...")
# 生产环境:将 tool_output 注入 Prompt调用 LLM 生成回复
reply = self._synthesize_reply(user_input, tool_name, tool_output)
self.logger.info(f"💬 回复已生成 ({len(reply)} chars)")
return reply
def generate_direct_reply(self, user_input: str, context: str = "") -> str:
"""无需工具时直接生成回复"""
self.logger.info("💬 直接生成回复(无需工具)")
return f"[{self.model_name}] 您好!关于「{user_input}」,这是一个直接回复示例。\n(生产环境此处调用真实 LLM API"
# ── Prompt 构造 ─────────────────────────────────────────────
def _build_decision_prompt(
self,
user_input: str,
tool_schemas: list[ToolSchema],
context: str,
) -> str:
"""构造工具决策 PromptReAct 格式)"""
tools_desc = "\n".join(
f"- {s.name}: {s.description}" for s in tool_schemas
)
return (
f"你是一个智能助手,请分析用户输入并决定是否需要调用工具。\n\n"
f"## 可用工具\n{tools_desc}\n\n"
f"## 对话历史\n{context or '(无)'}\n\n"
f"## 用户输入\n{user_input}\n\n"
f"## 指令\n"
f"以 JSON 格式回复:\n"
f'{{"need_tool": true/false, "tool_name": "...", "arguments": {{...}}, "reasoning": "..."}}'
)
# ── 模拟 LLM APIDemo 用规则引擎替代)────────────────────
def _call_llm_api(self, user_input: str, tool_schemas: list[ToolSchema]) -> ToolDecision:
"""
模拟 LLM API 调用Demo 版本使用关键词规则
生产环境替换示例:
import anthropic
client = anthropic.Anthropic()
response = client.messages.create(
model=self.model_name,
tools=[s.to_dict() for s in tool_schemas],
messages=[{"role": "user", "content": user_input}]
)
# 解析 response.content 中的 tool_use block
"""
return self.client.create(self.model_name, user_input=user_input, tool_schemas=tool_schemas)
text = user_input.lower()
# 规则匹配:计算器
calc_pattern = re.search(r"[\d\s\+\-\*\/\(\)\^]+[=?]?", user_input)
if any(kw in text for kw in ["计算", "等于", "多少", "×", "÷"]) and calc_pattern:
expr = re.sub(r"[^0-9+\-*/().**]", "", user_input.replace("×","*").replace("÷","/"))
return ToolDecision(
need_tool=True, tool_name="calculator",
arguments={"expression": expr or "1+1"},
reasoning="用户请求数学计算,调用 calculator 工具",
)
# 规则匹配:搜索
if any(kw in text for kw in ["搜索", "查询", "天气", "新闻", "查一下", "search"]):
return ToolDecision(
need_tool=True, tool_name="web_search",
arguments={"query": user_input, "max_results": 3},
reasoning="用户需要实时信息,调用 web_search 工具",
)
# 规则匹配:文件读取
if any(kw in text for kw in ["文件", "读取", "file", "config", "json", "txt"]):
filename = re.search(r"[\w\-\.]+\.\w+", user_input)
return ToolDecision(
need_tool=True, tool_name="file_reader",
arguments={"path": filename.group() if filename else "config.json"},
reasoning="用户请求读取文件,调用 file_reader 工具",
)
# 规则匹配:代码执行
if any(kw in text for kw in ["执行", "运行", "代码", "python", "print", "code"]):
code_match = re.search(r'[`\'"](.+?)[`\'"]', user_input)
code = code_match.group(1) if code_match else 'print("Hello, Agent!")'
return ToolDecision(
need_tool=True, tool_name="code_executor",
arguments={"code": code, "timeout": 5},
reasoning="用户请求执行代码,调用 code_executor 工具",
)
# 默认:直接回复
return ToolDecision(
need_tool=False,
reasoning="问题可直接回答,无需工具",
)
def _synthesize_reply(self, user_input: str, tool_name: str, tool_output: str) -> str:
"""基于工具输出合成最终回复Demo 版本)"""
return (
f"✅ 已通过 [{tool_name}] 工具处理您的请求。\n\n"
f"**执行结果:**\n{tool_output}\n\n"
f"---\n*由 {self.model_name} 生成 · 工具: {tool_name}*"
)

210
logs/agent.log Normal file
View File

@ -0,0 +1,210 @@
[2026-02-28 13:16:51,269] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 13:16:51,270] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 13:16:51,271] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 13:16:51,271] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 13:16:51,271] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 13:16:51,271] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 13:16:51,272] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: claude-sonnet-4-6
[2026-02-28 13:16:51,273] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 13:16:51,273] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 13:16:51,273] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 13:59:08,843] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 13:59:08,844] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 13:59:08,845] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 13:59:08,845] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 13:59:08,845] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 13:59:08,845] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 13:59:08,846] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: claude-sonnet-4-6
[2026-02-28 13:59:08,847] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 13:59:08,848] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 13:59:08,848] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 13:59:40,111] [agent.CLIENT] INFO: =======================================================
[2026-02-28 13:59:41,400] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少
[2026-02-28 14:00:07,071] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少...
[2026-02-28 14:00:51,574] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图...
[2026-02-28 14:02:14,371] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少...
[2026-02-28 14:04:03,060] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars)
[2026-02-28 14:15:21,435] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 14:15:21,437] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 14:15:21,437] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 14:15:21,438] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 14:15:21,438] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 14:15:21,438] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 14:15:21,439] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: claude-sonnet-4-6
[2026-02-28 14:15:21,730] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 14:15:21,731] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 14:15:21,731] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 14:15:37,946] [agent.CLIENT] INFO: =======================================================
[2026-02-28 14:15:38,634] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1+1等于多少
[2026-02-28 14:15:39,212] [agent.MEMORY] DEBUG: 💬 [USER] 1+1等于多少...
[2026-02-28 14:15:40,563] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图...
[2026-02-28 14:15:47,714] [agent.LLM] INFO: 💭 分析意图: 1+1等于多少...
[2026-02-28 14:15:50,157] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars)
[2026-02-28 14:17:52,834] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 14:17:52,836] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 14:17:52,837] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 14:17:52,837] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 14:17:52,837] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 14:17:52,838] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 14:17:52,839] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: claude-sonnet-4-6
[2026-02-28 14:17:53,009] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 14:17:53,010] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 14:17:53,010] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 14:18:02,052] [agent.CLIENT] INFO: =======================================================
[2026-02-28 14:18:02,614] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少
[2026-02-28 14:18:03,083] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少...
[2026-02-28 14:18:04,093] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图...
[2026-02-28 14:18:07,998] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少...
[2026-02-28 14:18:09,082] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars)
[2026-02-28 14:19:28,781] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 14:19:28,781] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 14:19:28,782] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 14:19:28,782] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 14:19:28,783] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 14:19:28,783] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 14:19:28,783] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o
[2026-02-28 14:19:28,912] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 14:19:28,913] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 14:19:28,913] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 14:19:38,773] [agent.CLIENT] INFO: =======================================================
[2026-02-28 14:19:38,773] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少
[2026-02-28 14:19:38,774] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少...
[2026-02-28 14:19:38,774] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图...
[2026-02-28 14:19:38,774] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少...
[2026-02-28 14:19:38,774] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars)
[2026-02-28 14:24:25,989] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 14:24:25,990] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 14:24:25,991] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 14:24:25,991] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 14:24:25,991] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 14:24:25,991] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 14:24:25,992] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o
[2026-02-28 14:24:26,120] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 14:24:26,120] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 14:24:26,121] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 14:24:47,520] [agent.CLIENT] INFO: =======================================================
[2026-02-28 14:24:47,520] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少
[2026-02-28 14:24:47,521] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少...
[2026-02-28 14:24:47,521] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图...
[2026-02-28 14:24:47,521] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少...
[2026-02-28 14:24:47,521] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars)
[2026-02-28 14:26:41,628] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 14:26:41,629] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 14:26:41,630] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 14:26:41,630] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 14:26:41,631] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 14:26:41,631] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 14:26:41,632] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o
[2026-02-28 14:26:41,764] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 14:26:41,764] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 14:26:41,765] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 14:26:52,266] [agent.CLIENT] INFO: =======================================================
[2026-02-28 14:26:52,266] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少
[2026-02-28 14:26:52,267] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少...
[2026-02-28 14:26:52,267] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图...
[2026-02-28 14:26:52,267] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少...
[2026-02-28 14:26:52,268] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars)
[2026-02-28 14:29:31,672] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 14:29:31,673] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 14:29:31,674] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 14:29:31,674] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 14:29:31,674] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 14:29:31,675] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 14:29:31,675] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o
[2026-02-28 14:29:31,809] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 14:29:31,810] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 14:29:31,811] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 14:29:43,546] [agent.CLIENT] INFO: =======================================================
[2026-02-28 14:29:43,546] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少
[2026-02-28 14:29:43,546] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少...
[2026-02-28 14:29:43,547] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图...
[2026-02-28 14:29:43,547] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少...
[2026-02-28 14:29:43,547] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars)
[2026-02-28 14:32:01,347] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 14:32:01,348] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 14:32:01,349] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 14:32:01,349] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 14:32:01,349] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 14:32:01,350] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 14:32:01,351] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o
[2026-02-28 14:32:01,477] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 14:32:01,477] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 14:32:01,478] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 14:32:08,959] [agent.CLIENT] INFO: =======================================================
[2026-02-28 14:32:08,959] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少
[2026-02-28 14:32:08,959] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少...
[2026-02-28 14:32:08,960] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图...
[2026-02-28 14:32:08,960] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少...
[2026-02-28 14:32:08,960] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars)
[2026-02-28 14:34:04,981] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 14:34:04,982] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 14:34:04,983] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 14:34:04,984] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 14:34:04,984] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 14:34:04,985] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 14:34:04,987] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o
[2026-02-28 14:34:05,137] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 14:34:05,140] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 14:34:05,141] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 14:34:14,908] [agent.CLIENT] INFO: =======================================================
[2026-02-28 14:34:14,908] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少
[2026-02-28 14:34:14,908] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少...
[2026-02-28 14:34:14,908] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图...
[2026-02-28 14:34:14,909] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少...
[2026-02-28 14:34:14,909] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars)
[2026-02-28 14:35:39,204] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 14:35:39,205] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 14:35:39,205] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 14:35:39,205] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 14:35:39,206] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 14:35:39,206] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 14:35:39,207] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o
[2026-02-28 14:35:39,333] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 14:35:39,334] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 14:35:39,334] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 14:35:51,583] [agent.CLIENT] INFO: =======================================================
[2026-02-28 14:35:51,583] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少
[2026-02-28 14:35:51,584] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少...
[2026-02-28 14:35:51,584] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图...
[2026-02-28 14:35:51,584] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少...
[2026-02-28 14:35:51,585] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars)
[2026-02-28 14:43:51,069] [agent.SYSTEM] INFO: 🔧 开始组装 Agent 系统...
[2026-02-28 14:43:51,069] [agent.MCP] INFO: 🚀 MCP Server [DemoMCPServer] 启动
[2026-02-28 14:43:51,070] [agent.MCP] INFO: 📌 注册工具: [calculator] — 计算数学表达式,支持加减乘除、幂运算、括号等
[2026-02-28 14:43:51,070] [agent.MCP] INFO: 📌 注册工具: [web_search] — 在互联网上搜索信息,返回相关网页摘要
[2026-02-28 14:43:51,071] [agent.MCP] INFO: 📌 注册工具: [file_reader] — 读取本地文件内容,仅限 workspace/ 目录下的文件
[2026-02-28 14:43:51,071] [agent.MCP] INFO: 📌 注册工具: [code_executor] — 在沙箱环境中执行 Python 代码片段,返回标准输出
[2026-02-28 14:43:51,071] [agent.LLM] INFO: 🧠 LLM 引擎初始化,模型: gpt-4o
[2026-02-28 14:43:51,198] [agent.MEMORY] INFO: 💾 Memory 初始化,最大历史: 20 条
[2026-02-28 14:43:51,198] [agent.CLIENT] INFO: 💻 Agent Client 初始化完成
[2026-02-28 14:43:51,199] [agent.SYSTEM] INFO: ✅ Agent 组装完成,已注册工具: ['calculator', 'web_search', 'file_reader', 'code_executor']
[2026-02-28 14:44:04,009] [agent.CLIENT] INFO: =======================================================
[2026-02-28 14:44:04,009] [agent.CLIENT] INFO: 📨 Step 1 [CLIENT] 收到用户输入: 1加1等于多少
[2026-02-28 14:44:04,009] [agent.MEMORY] DEBUG: 💬 [USER] 1加1等于多少...
[2026-02-28 14:44:04,010] [agent.CLIENT] INFO: 🧠 Step 2 [LLM] 开始推理,分析意图...
[2026-02-28 14:44:04,010] [agent.LLM] INFO: 💭 分析意图: 1加1等于多少...
[2026-02-28 14:44:04,010] [agent.LLM] DEBUG: 📝 Prompt 已构造 (344 chars)
[2026-02-28 14:44:10,159] [agent.LLM] INFO: 🎯 决策结果: 调用工具 [calculator]
[2026-02-28 14:44:10,159] [agent.LLM] DEBUG: 💡 推理: 用户询问数学计算问题直接调用计算工具得到1+1的结果是最合适的。
[2026-02-28 14:44:21,419] [agent.CLIENT] INFO: 📡 Step 3 [MCP] 发送工具调用请求
方法: tools/call
工具: calculator
参数: {'expression': '1+1'}
请求体: {'jsonrpc': '2.0', 'id': 'e4d8732a', 'method': 'tools/call', 'params': {'name': 'calculator', 'arguments': {'expression': '1+1'}}}
[2026-02-28 14:44:36,340] [agent.CLIENT] INFO: 🔧 Step 4 [TOOL] MCP Server 执行工具 [calculator]...
[2026-02-28 14:44:40,181] [agent.MCP] INFO: 📨 收到请求 id=e4d8732a method=tools/call
[2026-02-28 14:46:24,701] [agent.TOOL] INFO: ▶ 执行工具 [calculator],参数: {'expression': '1+1'}
[2026-02-28 14:46:50,239] [agent.TOOL] INFO: ✅ 工具 [calculator] 执行成功
[2026-02-28 16:05:27,152] [agent.CLIENT] INFO: ✅ 工具执行成功,输出: 1+1 = 2...
[2026-02-28 16:05:46,521] [agent.MEMORY] DEBUG: 💬 [TOOL] 1+1 = 2...
[2026-02-28 16:05:49,469] [agent.CLIENT] INFO: ✍️ Step 5 [LLM] 整合工具结果,生成最终回复...
[2026-02-28 16:05:53,270] [agent.LLM] INFO: ✍️ 整合工具结果,生成最终回复...
[2026-02-28 16:05:53,271] [agent.LLM] INFO: 💬 回复已生成 (83 chars)
[2026-02-28 16:06:03,400] [agent.MEMORY] DEBUG: 💬 [ASSISTANT] ✅ 已通过 [calculator] 工具处理您的请求。
**执行结果:**
1+1 = 2
---
*由 gpt-...
[2026-02-28 16:06:05,167] [agent.CLIENT] INFO: 🎉 [CLIENT] 流程完成,回复已返回

160
main.py Normal file
View File

@ -0,0 +1,160 @@
"""程序入口"""
"""
main.py
智能体 Demo 程序入口
组装所有模块启动交互式对话循环
"""
import sys
# ── 导入各模块 ─────────────────────────────────────────────────
from client.agent_client import AgentClient
from llm.llm_engine import LLMEngine
from mcp.mcp_server import MCPServer
from memory.memory_store import MemoryStore
from tools.calculator import CalculatorTool
from tools.code_executor import CodeExecutorTool
from tools.file_reader import FileReaderTool
from tools.web_search import WebSearchTool
from utils.logger import get_logger
logger = get_logger("SYSTEM")
# ── 系统组装 ───────────────────────────────────────────────────
def build_agent() -> AgentClient:
"""
工厂函数组装并返回完整的 Agent 实例
组装顺序:
1. 初始化 MCP Server注册所有工具
2. 初始化 LLM 引擎
3. 初始化 Memory 模块
4. 组装 AgentClient
"""
logger.info("🔧 开始组装 Agent 系统...")
# 1. MCP Server注册所有工具
mcp_server = MCPServer(server_name="DemoMCPServer")
mcp_server.register_tools(
CalculatorTool,
WebSearchTool,
FileReaderTool,
CodeExecutorTool,
)
# 2. LLM 引擎
llm = LLMEngine(model_name="gpt-4o")
# 3. 记忆模块
memory = MemoryStore(max_history=20)
# 4. 组装客户端
client = AgentClient(llm=llm, mcp_server=mcp_server, memory=memory)
logger.info(f"✅ Agent 组装完成,已注册工具: {mcp_server.list_tools()}")
return client
# ── 演示场景 ───────────────────────────────────────────────────
def run_demo(client: AgentClient) -> None:
"""运行预设演示场景,展示各工具的完整调用链路"""
demo_cases = [
("🔢 数学计算", "计算 (100 + 200) × 3 等于多少"),
("🌐 网络搜索", "搜索 Python 最新版本的新特性"),
("📄 文件读取", "读取文件 config.json 的内容"),
("🐍 代码执行", '执行代码 `print("Hello, Agent!")`'),
]
logger.info("\n" + "" * 60)
logger.info("🎬 开始演示模式,共 4 个场景")
logger.info("" * 60)
for title, question in demo_cases:
logger.info(f"\n{'' * 55}")
logger.info(f"📌 场景: {title}")
logger.info(f"{'' * 55}")
response = client.chat(question)
print(f"\n{'' * 55}")
print(f"👤 用户: {response.user_input}")
if response.tool_used:
print(f"🔧 工具: {response.tool_used}")
print(f"📤 输出: {response.tool_output[:120]}...")
print(f"🤖 回复:\n{response.final_reply}")
print()
# 打印记忆统计
stats = client.get_memory_stats()
logger.info(f"\n📊 Memory 统计: {stats}")
# ── 交互式对话循环 ─────────────────────────────────────────────
def run_interactive(client: AgentClient) -> None:
"""启动交互式命令行对话"""
print("\n" + "" * 60)
print(" 🤖 Agent Demo — 交互模式")
print(" 输入 'quit' → 退出程序")
print(" 输入 'clear' → 清空会话历史")
print(" 输入 'stats' → 查看 Memory 统计")
print(" 输入 'tools' → 查看已注册工具列表")
print("" * 60 + "\n")
while True:
try:
user_input = input("👤 你: ").strip()
except (KeyboardInterrupt, EOFError):
print("\n👋 再见!")
break
if not user_input:
continue
# ── 内置命令 ──────────────────────────────────────────
match user_input.lower():
case "quit" | "exit":
print("👋 再见!")
break
case "clear":
client.clear_session()
print("✅ 会话已清空\n")
continue
case "stats":
print(f"📊 Memory 统计: {client.get_memory_stats()}\n")
continue
case "tools":
tools = client.mcp_server.list_tools()
print(f"🔧 已注册工具 ({len(tools)} 个): {', '.join(tools)}\n")
continue
# ── 执行 Agent 完整流程 ───────────────────────────────
response = client.chat(user_input)
print(f"\n{'' * 55}")
if response.tool_used:
print(f" 🔧 调用工具: {response.tool_used}")
print(f"🤖 Agent:\n{response.final_reply}")
print(f"{'' * 55}\n")
# ── 主函数 ─────────────────────────────────────────────────────
def main() -> None:
"""
主函数入口支持两种运行模式:
python main.py 交互模式默认
python main.py demo 演示模式自动执行预设场景
"""
client = build_agent()
mode = sys.argv[1] if len(sys.argv) > 1 else "interactive"
if mode == "demo":
run_demo(client)
else:
run_interactive(client)
if __name__ == "__main__":
main()

1
mcp/__init__.py Normal file
View File

@ -0,0 +1 @@
"""__init__.py"""

107
mcp/mcp_protocol.py Normal file
View File

@ -0,0 +1,107 @@
"""MCP 协议JSON-RPC 消息定义"""
"""
mcp/mcp_protocol.py
MCP (Model Context Protocol) 协议数据结构定义
基于 JSON-RPC 2.0 规范封装请求/响应消息体
"""
import uuid
from dataclasses import dataclass, field
from typing import Any
# ── MCP 方法常量 ───────────────────────────────────────────────
class MCPMethod:
TOOLS_LIST = "tools/list" # 列出所有可用工具
TOOLS_CALL = "tools/call" # 调用指定工具
RESOURCES_READ = "resources/read" # 读取资源
# ── 请求消息 ───────────────────────────────────────────────────
@dataclass
class MCPRequest:
"""
MCP 工具调用请求JSON-RPC 2.0 格式
示例:
{
"jsonrpc": "2.0",
"id": "abc-123",
"method": "tools/call",
"params": {
"name": "calculator",
"arguments": {"expression": "1+1"}
}
}
"""
method: str
params: dict[str, Any] = field(default_factory=dict)
jsonrpc: str = "2.0"
id: str = field(default_factory=lambda: str(uuid.uuid4())[:8])
def to_dict(self) -> dict:
return {
"jsonrpc": self.jsonrpc,
"id": self.id,
"method": self.method,
"params": self.params,
}
# ── 响应消息 ───────────────────────────────────────────────────
@dataclass
class MCPResponse:
"""
MCP 工具调用响应JSON-RPC 2.0 格式
成功示例:
{"jsonrpc": "2.0", "id": "abc-123", "result": {"content": [...]}}
失败示例:
{"jsonrpc": "2.0", "id": "abc-123", "error": {"code": -32601, "message": "..."}}
"""
id: str
result: dict[str, Any] | None = None
error: dict[str, Any] | None = None
jsonrpc: str = "2.0"
@property
def success(self) -> bool:
return self.error is None
@property
def content(self) -> str:
"""提取响应中的文本内容"""
if not self.success or not self.result:
return self.error.get("message", "Unknown error") if self.error else ""
items = self.result.get("content", [])
return "\n".join(item.get("text", "") for item in items if item.get("type") == "text")
def to_dict(self) -> dict:
base = {"jsonrpc": self.jsonrpc, "id": self.id}
if self.success:
base["result"] = self.result
else:
base["error"] = self.error
return base
# ── 工具描述 ───────────────────────────────────────────────────
@dataclass
class ToolSchema:
"""
工具的元数据描述用于 LLM 识别和选择工具
"""
name: str
description: str
parameters: dict[str, Any] # JSON Schema 格式的参数定义
def to_dict(self) -> dict:
return {
"name": self.name,
"description": self.description,
"inputSchema": {
"type": "object",
"properties": self.parameters,
},
}

143
mcp/mcp_server.py Normal file
View File

@ -0,0 +1,143 @@
"""MCP 服务器:工具注册 & 调度"""
"""
mcp/mcp_server.py
MCP Server工具注册中心与调度引擎
负责管理所有工具的生命周期处理 JSON-RPC 格式的工具调用请求
"""
import json
from typing import Type
from mcp.mcp_protocol import MCPMethod, MCPRequest, MCPResponse, ToolSchema
from tools.base_tool import BaseTool, ToolResult
from utils.logger import get_logger
class MCPServer:
"""
MCP 服务器核心类
职责:
1. 工具注册register_tool
2. 工具列表查询tools/list
3. 工具调用分发tools/call
4. JSON-RPC 协议封装/解析
使用示例:
server = MCPServer()
server.register_tool(CalculatorTool)
response = server.handle_request(request)
"""
def __init__(self, server_name: str = "AgentMCPServer"):
self.server_name = server_name
self.logger = get_logger("MCP")
self._registry: dict[str, BaseTool] = {} # 工具名 → 工具实例
self.logger.info(f"🚀 MCP Server [{server_name}] 启动")
# ── 工具注册 ────────────────────────────────────────────────
def register_tool(self, tool_class: Type[BaseTool]) -> None:
"""
注册一个工具类到服务器
Args:
tool_class: 继承自 BaseTool 的工具类传入类本身不是实例
"""
instance = tool_class()
if not instance.name:
raise ValueError(f"工具类 {tool_class.__name__} 未设置 name 属性")
self._registry[instance.name] = instance
self.logger.info(f"📌 注册工具: [{instance.name}] — {instance.description}")
def register_tools(self, *tool_classes: Type[BaseTool]) -> None:
"""批量注册多个工具类"""
for cls in tool_classes:
self.register_tool(cls)
# ── 请求处理入口 ────────────────────────────────────────────
def handle_request(self, request: MCPRequest) -> MCPResponse:
"""
处理 MCP 请求的统一入口根据 method 分发到对应处理器
Args:
request: MCPRequest 实例
Returns:
MCPResponse 实例
"""
self.logger.info(f"📨 收到请求 id={request.id} method={request.method}")
handlers = {
MCPMethod.TOOLS_LIST: self._handle_tools_list,
MCPMethod.TOOLS_CALL: self._handle_tools_call,
}
handler = handlers.get(request.method)
if handler is None:
return self._error_response(request.id, -32601, f"未知方法: {request.method}")
return handler(request)
# ── 私有处理器 ──────────────────────────────────────────────
def _handle_tools_list(self, request: MCPRequest) -> MCPResponse:
"""处理 tools/list 请求,返回所有已注册工具的 Schema"""
schemas = [tool.get_schema().to_dict() for tool in self._registry.values()]
self.logger.info(f"📋 返回工具列表,共 {len(schemas)} 个工具")
return MCPResponse(
id=request.id,
result={"tools": schemas},
)
def _handle_tools_call(self, request: MCPRequest) -> MCPResponse:
"""处理 tools/call 请求,调用指定工具并返回结果"""
tool_name = request.params.get("name")
arguments = request.params.get("arguments", {})
# 检查工具是否存在
tool = self._registry.get(tool_name)
if tool is None:
available = list(self._registry.keys())
return self._error_response(
request.id, -32602,
f"工具 [{tool_name}] 不存在,可用工具: {available}"
)
# 执行工具
result: ToolResult = tool.safe_execute(**arguments)
if result.success:
return MCPResponse(
id=request.id,
result={
"content": [{"type": "text", "text": result.output}],
"metadata": result.metadata,
},
)
else:
return self._error_response(request.id, -32000, result.output)
# ── 工具方法 ────────────────────────────────────────────────
def get_tool_schemas(self) -> list[ToolSchema]:
"""获取所有工具的 Schema 列表(供 LLM 引擎使用)"""
return [tool.get_schema() for tool in self._registry.values()]
def list_tools(self) -> list[str]:
"""返回所有已注册工具的名称列表"""
return list(self._registry.keys())
@staticmethod
def _error_response(req_id: str, code: int, message: str) -> MCPResponse:
"""构造标准 JSON-RPC 错误响应"""
return MCPResponse(
id=req_id,
error={"code": code, "message": message},
)
def __repr__(self) -> str:
return f"MCPServer(name={self.server_name!r}, tools={self.list_tools()})"

1
memory/__init__.py Normal file
View File

@ -0,0 +1 @@
"""__init__.py"""

128
memory/memory_store.py Normal file
View File

@ -0,0 +1,128 @@
"""记忆模块:对话历史管理"""
"""
memory/memory_store.py
Agent 记忆模块管理对话历史短期记忆与关键信息摘要长期记忆
"""
from collections import deque
from dataclasses import dataclass, field
from datetime import datetime
from typing import Literal
from utils.logger import get_logger
# ── 消息数据结构 ───────────────────────────────────────────────
@dataclass
class Message:
"""单条对话消息"""
role: Literal["user", "assistant", "tool"]
content: str
timestamp: str = field(default_factory=lambda: datetime.now().strftime("%H:%M:%S"))
metadata: dict = field(default_factory=dict)
def to_dict(self) -> dict:
return {
"role": self.role,
"content": self.content,
"timestamp": self.timestamp,
}
# ── 记忆存储 ───────────────────────────────────────────────────
class MemoryStore:
"""
对话记忆存储
短期记忆: 使用 deque 保存最近 N 轮对话自动滚动淘汰旧消息
长期记忆: 保存关键事实摘要生产环境可替换为向量数据库
使用示例:
memory = MemoryStore(max_history=10)
memory.add_user_message("你好")
memory.add_assistant_message("你好!有什么可以帮你?")
history = memory.get_history()
"""
def __init__(self, max_history: int = 20):
"""
Args:
max_history: 短期记忆保留的最大消息条数
"""
self.logger = get_logger("MEMORY")
self.max_history = max_history
self._history: deque[Message] = deque(maxlen=max_history)
self._facts: list[str] = [] # 长期记忆:关键事实
self.logger.info(f"💾 Memory 初始化,最大历史: {max_history}")
# ── 写入接口 ────────────────────────────────────────────────
def add_user_message(self, content: str) -> None:
"""记录用户消息"""
self._add(Message(role="user", content=content))
def add_assistant_message(self, content: str) -> None:
"""记录 Agent 回复"""
self._add(Message(role="assistant", content=content))
def add_tool_result(self, tool_name: str, result: str) -> None:
"""记录工具调用结果"""
self._add(Message(
role="tool",
content=result,
metadata={"tool": tool_name},
))
def add_fact(self, fact: str) -> None:
"""向长期记忆中添加关键事实"""
self._facts.append(fact)
self.logger.debug(f"📌 长期记忆新增: {fact}")
# ── 读取接口 ────────────────────────────────────────────────
def get_history(self, last_n: int | None = None) -> list[dict]:
"""
获取对话历史LLM 上下文格式
Args:
last_n: 仅返回最近 N None 表示全部
Returns:
消息字典列表格式: [{"role": ..., "content": ...}, ...]
"""
messages = list(self._history)
if last_n:
messages = messages[-last_n:]
return [m.to_dict() for m in messages]
def get_facts(self) -> list[str]:
"""获取所有长期记忆事实"""
return list(self._facts)
def get_context_summary(self) -> str:
"""生成上下文摘要字符串,供 LLM Prompt 使用"""
history = self.get_history(last_n=6)
lines = [f"[{m['role'].upper()}] {m['content'][:80]}" for m in history]
return "\n".join(lines) if lines else "(暂无对话历史)"
# ── 管理接口 ────────────────────────────────────────────────
def clear_history(self) -> None:
"""清空短期对话历史"""
self._history.clear()
self.logger.info("🗑 对话历史已清空")
def stats(self) -> dict:
"""返回记忆统计信息"""
return {
"history_count": len(self._history),
"facts_count": len(self._facts),
"max_history": self.max_history,
}
# ── 私有方法 ────────────────────────────────────────────────
def _add(self, message: Message) -> None:
self._history.append(message)
self.logger.debug(f"💬 [{message.role.upper()}] {message.content[:60]}...")

1
tools/__init__.py Normal file
View File

@ -0,0 +1 @@
"""__init__.py"""

82
tools/base_tool.py Normal file
View File

@ -0,0 +1,82 @@
"""
tools/base_tool.py
所有工具的抽象基类定义统一接口规范
"""
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
from mcp.mcp_protocol import ToolSchema
from utils.logger import get_logger
@dataclass
class ToolResult:
"""工具执行结果的统一封装"""
success: bool
output: str
metadata: dict[str, Any] = None
def __post_init__(self):
self.metadata = self.metadata or {}
class BaseTool(ABC):
"""
工具基类所有具体工具必须继承此类并实现 execute() 方法
子类示例:
class MyTool(BaseTool):
name = "my_tool"
description = "这是我的工具"
parameters = {"input": {"type": "string", "description": "输入内容"}}
def execute(self, **kwargs) -> ToolResult:
return ToolResult(success=True, output=f"处理结果: {kwargs['input']}")
"""
# 子类必须定义的类属性
name: str = ""
description: str = ""
parameters: dict[str, Any] = {}
def __init__(self):
self.logger = get_logger("TOOL")
@abstractmethod
def execute(self, **kwargs) -> ToolResult:
"""
执行工具逻辑子类必须实现
Args:
**kwargs: 工具参数 parameters 中定义的字段对应
Returns:
ToolResult 实例
"""
...
def get_schema(self) -> ToolSchema:
"""返回工具的 MCP Schema 描述"""
return ToolSchema(
name=self.name,
description=self.description,
parameters=self.parameters,
)
def safe_execute(self, **kwargs) -> ToolResult:
"""
带异常捕获的安全执行入口 MCP Server 调用
Returns:
ToolResult失败时 success=False 并携带错误信息
"""
self.logger.info(f"▶ 执行工具 [{self.name}],参数: {kwargs}")
try:
result = self.execute(**kwargs)
self.logger.info(f"✅ 工具 [{self.name}] 执行成功")
return result
except Exception as exc:
self.logger.error(f"❌ 工具 [{self.name}] 执行失败: {exc}")
return ToolResult(success=False, output=f"工具执行异常: {exc}")

64
tools/calculator.py Normal file
View File

@ -0,0 +1,64 @@
"""计算器工具"""
# ════════════════════════════════════════════════════════════════
# tools/calculator.py — 数学计算工具
# ════════════════════════════════════════════════════════════════
"""
tools/calculator.py
安全的数学表达式计算工具使用 ast 模块避免 eval 注入风险
"""
import ast
import operator
from tools.base_tool import BaseTool, ToolResult
class CalculatorTool(BaseTool):
name = "calculator"
description = "计算数学表达式,支持加减乘除、幂运算、括号等"
parameters = {
"expression": {
"type": "string",
"description": "数学表达式,例如 '(1+2)*3''2**10'",
}
}
# 允许的运算符白名单(防止注入)
_OPERATORS = {
ast.Add: operator.add,
ast.Sub: operator.sub,
ast.Mult: operator.mul,
ast.Div: operator.truediv,
ast.Pow: operator.pow,
ast.Mod: operator.mod,
ast.USub: operator.neg,
}
def execute(self, expression: str, **_) -> ToolResult:
try:
tree = ast.parse(expression, mode="eval")
result = self._eval_node(tree.body)
return ToolResult(
success=True,
output=f"{expression} = {result}",
metadata={"expression": expression, "result": result},
)
except (ValueError, TypeError, ZeroDivisionError) as exc:
return ToolResult(success=False, output=f"计算错误: {exc}")
def _eval_node(self, node: ast.AST) -> float:
"""递归解析 AST 节点"""
match node:
case ast.Constant(value=v) if isinstance(v, (int, float)):
return v
case ast.BinOp(left=left, op=op, right=right):
fn = self._OPERATORS.get(type(op))
if fn is None:
raise ValueError(f"不支持的运算符: {type(op).__name__}")
return fn(self._eval_node(left), self._eval_node(right))
case ast.UnaryOp(op=op, operand=operand):
fn = self._OPERATORS.get(type(op))
if fn is None:
raise ValueError(f"不支持的一元运算符: {type(op).__name__}")
return fn(self._eval_node(operand))
case _:
raise ValueError(f"不支持的表达式节点: {type(node).__name__}")

60
tools/code_executor.py Normal file
View File

@ -0,0 +1,60 @@
"""代码执行工具"""
# ════════════════════════════════════════════════════════════════
# tools/code_executor.py — 代码执行工具
# ════════════════════════════════════════════════════════════════
"""
tools/code_executor.py
沙箱代码执行工具在受限环境中运行 Python 代码片段
"""
import io
import contextlib
import time
from tools.base_tool import BaseTool, ToolResult
class CodeExecutorTool(BaseTool):
name = "code_executor"
description = "在沙箱环境中执行 Python 代码片段,返回标准输出"
parameters = {
"code": {
"type": "string",
"description": "要执行的 Python 代码",
},
"timeout": {
"type": "integer",
"description": "超时时间(秒),默认 5",
},
}
# 沙箱:仅允许安全的内置函数
_SAFE_BUILTINS = {
"print": print, "range": range, "len": len,
"int": int, "float": float, "str": str, "list": list,
"dict": dict, "tuple": tuple, "set": set, "bool": bool,
"abs": abs, "max": max, "min": min, "sum": sum,
"enumerate": enumerate, "zip": zip, "map": map,
"sorted": sorted, "reversed": reversed,
}
def execute(self, code: str, timeout: int = 5, **_) -> ToolResult:
stdout_buf = io.StringIO()
start_time = time.perf_counter()
try:
# 重定向 stdout捕获 print 输出
with contextlib.redirect_stdout(stdout_buf):
exec( # noqa: S102
compile(code, "<agent_sandbox>", "exec"),
{"__builtins__": self._SAFE_BUILTINS},
)
elapsed = (time.perf_counter() - start_time) * 1000
output = stdout_buf.getvalue() or "(无输出)"
return ToolResult(
success=True,
output=f"执行成功 ({elapsed:.1f}ms):\n{output}",
metadata={"elapsed_ms": elapsed},
)
except Exception as exc:
return ToolResult(success=False, output=f"执行错误: {type(exc).__name__}: {exc}")

65
tools/file_reader.py Normal file
View File

@ -0,0 +1,65 @@
"""文件读取工具"""
# ════════════════════════════════════════════════════════════════
# tools/file_reader.py — 文件读取工具
# ════════════════════════════════════════════════════════════════
"""
tools/file_reader.py
本地文件读取工具支持文本文件限制读取路径防止越权
"""
from pathlib import Path
from tools.base_tool import BaseTool, ToolResult
# 允许读取的根目录(沙箱限制)
_ALLOWED_ROOT = Path("./workspace")
class FileReaderTool(BaseTool):
name = "file_reader"
description = "读取本地文件内容,仅限 workspace/ 目录下的文件"
parameters = {
"path": {
"type": "string",
"description": "文件路径,相对于 workspace/ 目录",
},
"encoding": {
"type": "string",
"description": "文件编码,默认 utf-8",
},
}
def execute(self, path: str, encoding: str = "utf-8", **_) -> ToolResult:
_ALLOWED_ROOT.mkdir(exist_ok=True)
# 路径安全检查:防止目录穿越攻击
target = (_ALLOWED_ROOT / path).resolve()
if not str(target).startswith(str(_ALLOWED_ROOT.resolve())):
return ToolResult(success=False, output=f"❌ 拒绝访问: 路径超出允许范围")
if not target.exists():
# Demo 模式:自动创建示例文件
self._create_demo_file(target)
try:
content = target.read_text(encoding=encoding)
return ToolResult(
success=True,
output=f"文件 [{path}] 内容:\n{content}",
metadata={"path": str(target), "size": target.stat().st_size},
)
except OSError as exc:
return ToolResult(success=False, output=f"读取失败: {exc}")
@staticmethod
def _create_demo_file(path: Path) -> None:
"""自动创建演示用文件"""
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
'{\n "app": "AgentDemo",\n "version": "1.0.0",\n'
' "llm": "claude-sonnet-4-6",\n "tools": ["calculator", "web_search"]\n}\n',
encoding="utf-8",
)

65
tools/web_search.py Normal file
View File

@ -0,0 +1,65 @@
"""网络搜索工具"""
# ════════════════════════════════════════════════════════════════
# tools/web_search.py — 网络搜索工具(模拟)
# ════════════════════════════════════════════════════════════════
"""
tools/web_search.py
网络搜索工具Demo 中使用模拟数据生产环境可替换为真实 API
"""
import time
from tools.base_tool import BaseTool, ToolResult
# 模拟搜索结果数据库
_MOCK_RESULTS: dict[str, list[dict]] = {
"天气": [
{"title": "今日天气预报", "snippet": "晴转多云,气温 15°C ~ 24°C东南风 3 级"},
{"title": "未来 7 天天气", "snippet": "本周整体晴好,周末有小雨"},
],
"python": [
{"title": "Python 官方文档", "snippet": "Python 3.12 新特性:改进的错误提示、更快的启动速度"},
{"title": "Python 教程", "snippet": "从零开始学 Python包含 300+ 实战案例"},
],
}
_DEFAULT_RESULTS = [
{"title": "搜索结果 1", "snippet": "找到相关内容,请查看详情"},
{"title": "搜索结果 2", "snippet": "更多相关信息可通过链接访问"},
]
class WebSearchTool(BaseTool):
name = "web_search"
description = "在互联网上搜索信息,返回相关网页摘要"
parameters = {
"query": {
"type": "string",
"description": "搜索关键词或问题",
},
"max_results": {
"type": "integer",
"description": "返回结果数量,默认 3",
},
}
def execute(self, query: str, max_results: int = 3, **_) -> ToolResult:
time.sleep(0.1) # 模拟网络延迟
# 关键词匹配模拟结果
results = _DEFAULT_RESULTS
for keyword, data in _MOCK_RESULTS.items():
if keyword in query:
results = data
break
results = results[:max_results]
formatted = "\n".join(
f"[{i+1}] {r['title']}\n {r['snippet']}"
for i, r in enumerate(results)
)
return ToolResult(
success=True,
output=f"搜索「{query}」,共 {len(results)} 条结果:\n{formatted}",
metadata={"query": query, "count": len(results)},
)

96
utils/logger.py Normal file
View File

@ -0,0 +1,96 @@
"""日志工具"""
"""
utils/logger.py
统一日志模块支持彩色终端输出与文件记录
"""
import logging
import sys
from datetime import datetime
from pathlib import Path
# ── ANSI 颜色常量 ──────────────────────────────────────────────
class Color:
RESET = "\033[0m"
BOLD = "\033[1m"
CYAN = "\033[96m"
GREEN = "\033[92m"
YELLOW = "\033[93m"
RED = "\033[91m"
MAGENTA = "\033[95m"
BLUE = "\033[94m"
GREY = "\033[90m"
# ── 自定义彩色 Formatter ───────────────────────────────────────
class ColorFormatter(logging.Formatter):
LEVEL_COLORS = {
logging.DEBUG: Color.GREY,
logging.INFO: Color.CYAN,
logging.WARNING: Color.YELLOW,
logging.ERROR: Color.RED,
logging.CRITICAL: Color.MAGENTA,
}
COMPONENT_COLORS = {
"CLIENT": Color.BLUE,
"LLM": Color.GREEN,
"MCP": Color.YELLOW,
"TOOL": Color.MAGENTA,
"MEMORY": Color.CYAN,
"SYSTEM": Color.GREY,
}
def format(self, record: logging.LogRecord) -> str:
level_color = self.LEVEL_COLORS.get(record.levelno, Color.RESET)
time_str = datetime.now().strftime("%H:%M:%S.%f")[:-3]
# 从 logger 名称提取组件标签,例如 "agent.CLIENT"
component = record.name.split(".")[-1].upper()
comp_color = self.COMPONENT_COLORS.get(component, Color.RESET)
prefix = (
f"{Color.GREY}[{time_str}]{Color.RESET} "
f"{comp_color}{Color.BOLD}[{component:6s}]{Color.RESET} "
f"{level_color}{record.getMessage()}{Color.RESET}"
)
return prefix
# ── Logger 工厂函数 ────────────────────────────────────────────
def get_logger(component: str, level: int = logging.DEBUG) -> logging.Logger:
"""
获取指定组件的 Logger 实例
Args:
component: 组件名称 "CLIENT""LLM""MCP"
level: 日志级别默认 DEBUG
Returns:
配置好的 Logger 实例
"""
logger = logging.getLogger(f"agent.{component}")
logger.setLevel(level)
# 避免重复添加 Handler
if logger.handlers:
return logger
# 终端 Handler彩色
console_handler = logging.StreamHandler(sys.stdout)
console_handler.setFormatter(ColorFormatter())
logger.addHandler(console_handler)
# 文件 Handler纯文本
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
file_handler = logging.FileHandler(log_dir / "agent.log", encoding="utf-8")
file_handler.setFormatter(
logging.Formatter("[%(asctime)s] [%(name)s] %(levelname)s: %(message)s")
)
logger.addHandler(file_handler)
# 防止日志向上传播到 root logger
logger.propagate = False
return logger