学习第17天:Agent Memory 与 MCP 协议

学习第17天:Agent Memory 与 MCP 协议

贯穿项目:Enterprise AI Agent Platform --- 本章为 Agent 赋予长期记忆,并实现 MCP 协议集成外部工具


1. 学习目标

完成本章学习后,你将能够:

  • 理解 Agent Memory 的三层架构:短期/长期/工作记忆
  • 实现对话摘要记忆向量记忆结构化记忆
  • 理解 MCP(Model Context Protocol) 协议的设计理念
  • MCP Python SDK 构建 MCP Server 和 Client
  • 将 MCP 工具集成到 LangChain/LangGraph Agent 中
  • 设计多 Agent 间的记忆共享机制
概念 含义 类比
Working Memory 当前对话上下文 CPU 寄存器
Short-term Memory 本次会话历史 RAM
Long-term Memory 跨会话持久化记忆 硬盘/数据库
MCP 模型上下文协议 USB-C 统一接口标准
MCP Server 提供工具/资源的服务 REST API Server
MCP Client 调用 MCP Server 的客户端 HTTP Client

2. 知识体系图

#mermaid-svg-mUT4prrmWLP2ScPn{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-mUT4prrmWLP2ScPn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-mUT4prrmWLP2ScPn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-mUT4prrmWLP2ScPn .error-icon{fill:#552222;}#mermaid-svg-mUT4prrmWLP2ScPn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-mUT4prrmWLP2ScPn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-mUT4prrmWLP2ScPn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-mUT4prrmWLP2ScPn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-mUT4prrmWLP2ScPn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-mUT4prrmWLP2ScPn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-mUT4prrmWLP2ScPn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-mUT4prrmWLP2ScPn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-mUT4prrmWLP2ScPn .marker.cross{stroke:#333333;}#mermaid-svg-mUT4prrmWLP2ScPn svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-mUT4prrmWLP2ScPn p{margin:0;}#mermaid-svg-mUT4prrmWLP2ScPn .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-mUT4prrmWLP2ScPn .cluster-label text{fill:#333;}#mermaid-svg-mUT4prrmWLP2ScPn .cluster-label span{color:#333;}#mermaid-svg-mUT4prrmWLP2ScPn .cluster-label span p{background-color:transparent;}#mermaid-svg-mUT4prrmWLP2ScPn .label text,#mermaid-svg-mUT4prrmWLP2ScPn span{fill:#333;color:#333;}#mermaid-svg-mUT4prrmWLP2ScPn .node rect,#mermaid-svg-mUT4prrmWLP2ScPn .node circle,#mermaid-svg-mUT4prrmWLP2ScPn .node ellipse,#mermaid-svg-mUT4prrmWLP2ScPn .node polygon,#mermaid-svg-mUT4prrmWLP2ScPn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-mUT4prrmWLP2ScPn .rough-node .label text,#mermaid-svg-mUT4prrmWLP2ScPn .node .label text,#mermaid-svg-mUT4prrmWLP2ScPn .image-shape .label,#mermaid-svg-mUT4prrmWLP2ScPn .icon-shape .label{text-anchor:middle;}#mermaid-svg-mUT4prrmWLP2ScPn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-mUT4prrmWLP2ScPn .rough-node .label,#mermaid-svg-mUT4prrmWLP2ScPn .node .label,#mermaid-svg-mUT4prrmWLP2ScPn .image-shape .label,#mermaid-svg-mUT4prrmWLP2ScPn .icon-shape .label{text-align:center;}#mermaid-svg-mUT4prrmWLP2ScPn .node.clickable{cursor:pointer;}#mermaid-svg-mUT4prrmWLP2ScPn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-mUT4prrmWLP2ScPn .arrowheadPath{fill:#333333;}#mermaid-svg-mUT4prrmWLP2ScPn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-mUT4prrmWLP2ScPn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-mUT4prrmWLP2ScPn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mUT4prrmWLP2ScPn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-mUT4prrmWLP2ScPn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mUT4prrmWLP2ScPn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-mUT4prrmWLP2ScPn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-mUT4prrmWLP2ScPn .cluster text{fill:#333;}#mermaid-svg-mUT4prrmWLP2ScPn .cluster span{color:#333;}#mermaid-svg-mUT4prrmWLP2ScPn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-mUT4prrmWLP2ScPn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-mUT4prrmWLP2ScPn rect.text{fill:none;stroke-width:0;}#mermaid-svg-mUT4prrmWLP2ScPn .icon-shape,#mermaid-svg-mUT4prrmWLP2ScPn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-mUT4prrmWLP2ScPn .icon-shape p,#mermaid-svg-mUT4prrmWLP2ScPn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-mUT4prrmWLP2ScPn .icon-shape .label rect,#mermaid-svg-mUT4prrmWLP2ScPn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-mUT4prrmWLP2ScPn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-mUT4prrmWLP2ScPn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-mUT4prrmWLP2ScPn :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 记忆管理策略
MCP 协议
Agent Memory 架构
三层记忆体系
Working: 当前窗口内消息
Short-term: 会话内全部历史
Long-term: 摘要+向量+结构化
MCP 统一接口
Resources: 数据资源暴露
Tools: 工具调用
Prompts: 预定义 Prompt
Sampling: LLM 采样
记忆操作
写入: 对话结束自动保存
检索: 向量相似度+时间权重
遗忘: TTL + 重要性过滤
共享: 多 Agent 记忆总线


3. 核心知识

3.1 Agent Memory 的三层模型

复制代码
┌──────────────────────────────────────────┐
│          Working Memory(工作记忆)        │
│  - 当前上下文窗口内的所有消息              │
│  - 最多 128K tokens(取决于模型)          │
│  - 每次 LLM 调用都会发送                  │
│  - 受 token 预算限制,需裁剪               │
├──────────────────────────────────────────┤
│         Short-term Memory(短期记忆)      │
│  - 当前会话的全部对话历史                  │
│  - 存储在 Redis/内存中                    │
│  - 会话结束后可选择性地保存为长期记忆       │
├──────────────────────────────────────────┤
│         Long-term Memory(长期记忆)       │
│  - 跨会话的持久化记忆                     │
│  - 三种形式:                              │
│    1. 摘要记忆:对话的压缩总结             │
│    2. 向量记忆:Embedding + 向量检索       │
│    3. 结构化记忆:用户偏好、知识图谱       │
└──────────────────────────────────────────┘

3.2 MCP 协议概览

MCP(Model Context Protocol)是 Anthropic 提出的开放标准,用于 LLM 与外部工具/数据源之间的通信。

复制代码
类比理解:
- MCP 之于 LLM 工具 = USB-C 之于外设
- 以前:每个 LLM 平台有自己的一套 Tool 定义格式
- MCP:统一标准,一次开发,所有支持 MCP 的 LLM 都能用

MCP 的四大能力

能力 说明 类比
Resources 暴露数据资源(文件、数据库等) GET /api/resource
Tools 暴露可执行的工具 POST /api/tool
Prompts 预定义的 Prompt 模板 模板库
Sampling Server 请求 Client 调用 LLM 反向调用

4. 详细讲解

4.1 短期记忆:对话摘要

python 复制代码
from langchain_core.messages import SystemMessage
from langchain_openai import ChatOpenAI

class ConversationSummarizer:
    """对话摘要器------将长对话压缩为摘要"""

    def __init__(self, llm: ChatOpenAI):
        self.llm = llm

    async def summarize(
        self, messages: list, max_summary_tokens: int = 500
    ) -> str:
        """对对话历史生成摘要"""
        # 将消息列表转为文本
        conversation_text = "\n".join([
            f"{m.role}: {m.content[:200]}" for m in messages
        ])

        prompt = f"""请用不超过 {max_summary_tokens} 字总结以下对话的关键信息:

{conversation_text}

总结应包括:
1. 用户的核心问题和需求
2. AI 的回答要点
3. 重要的决策和承诺
4. 待跟进的事项"""

        response = await self.llm.ainvoke(prompt)
        return response.content

4.2 长期记忆:向量记忆

python 复制代码
# app/memory/vector_memory.py
import chromadb
from datetime import datetime
from typing import List, Optional

class VectorMemory:
    """基于向量的长期记忆

    原理:将"记忆"Embedding 后存入向量库,查询时语义检索最相关的记忆
    """

    def __init__(self, collection_name: str = "agent_memory"):
        from chromadb.utils import embedding_functions
        self.client = chromadb.PersistentClient(path="./memory_db")

        self.ef = embedding_functions.OpenAIEmbeddingFunction(
            api_key=settings.OPENAI_API_KEY,
            model_name="text-embedding-3-small",
        )

        self.collection = self.client.get_or_create_collection(
            name=collection_name,
            embedding_function=self.ef,
        )

    async def save(
        self,
        content: str,
        memory_type: str = "conversation",
        user_id: str = "default",
        importance: float = 0.5,
        metadata: dict = None,
    ):
        """保存一条记忆"""
        import uuid

        meta = {
            "memory_type": memory_type,
            "user_id": user_id,
            "importance": importance,
            "timestamp": datetime.now().isoformat(),
            **(metadata or {}),
        }

        self.collection.add(
            documents=[content],
            metadatas=[meta],
            ids=[str(uuid.uuid4())],
        )

    async def search(
        self,
        query: str,
        n_results: int = 5,
        user_id: str = "default",
        min_importance: float = 0.0,
    ) -> List[dict]:
        """搜索相关记忆"""
        results = self.collection.query(
            query_texts=[query],
            n_results=n_results,
            where={
                "user_id": user_id,
            },
        )

        memories = []
        for doc, meta, distance in zip(
            results["documents"][0],
            results["metadatas"][0],
            results.get("distances", [[1.0]])[0],
        ):
            if meta.get("importance", 0) >= min_importance:
                memories.append({
                    "content": doc,
                    "metadata": meta,
                    "relevance": 1 - distance,  # 距离 → 相似度
                })

        return memories

    async def forget_old(self, days: int = 30):
        """遗忘过旧的记忆(按时间)"""
        # Chroma 不直接支持按时间过滤,需要维护一个元数据索引
        # 生产环境建议用 Milvus 的时间分区功能
        pass

4.3 结构化记忆:用户画像

python 复制代码
# app/memory/profile_memory.py
from dataclasses import dataclass, field
from typing import Optional
import json

@dataclass
class UserProfile:
    """用户画像------结构化长期记忆"""
    user_id: str
    name: Optional[str] = None
    preferences: dict = field(default_factory=dict)
    skills: list[str] = field(default_factory=list)
    known_facts: dict = field(default_factory=dict)  # key: fact_type, value: fact
    interaction_count: int = 0
    last_interaction: str = ""

class ProfileMemory:
    """用户画像记忆管理"""

    def __init__(self, redis_client):
        self.redis = redis_client

    async def get_profile(self, user_id: str) -> UserProfile:
        """获取用户画像"""
        key = f"profile:{user_id}"
        data = await self.redis.get(key)

        if data:
            return UserProfile(**json.loads(data))
        return UserProfile(user_id=user_id)

    async def update_profile(
        self, user_id: str, updates: dict
    ) -> UserProfile:
        """更新用户画像"""
        profile = await self.get_profile(user_id)

        if "name" in updates:
            profile.name = updates["name"]
        if "preferences" in updates:
            profile.preferences.update(updates["preferences"])
        if "fact" in updates:
            fact_type = updates["fact"]["type"]
            profile.known_facts[fact_type] = updates["fact"]["value"]

        profile.interaction_count += 1
        profile.last_interaction = datetime.now().isoformat()

        key = f"profile:{user_id}"
        await self.redis.set(key, json.dumps(profile.__dict__))
        return profile

    async def extract_and_save(
        self, user_id: str, conversation_text: str, llm
    ):
        """从对话中提取结构化信息并保存"""
        prompt = f"""从以下对话中提取用户信息,返回 JSON:

{conversation_text}

提取字段:
- name: 用户姓名
- preferences: 偏好(如喜欢的编程语言、技术栈)
- facts: 用户陈述的事实(如所在公司、职位)

JSON:"""

        response = await llm.ainvoke(prompt)
        extracted = json.loads(response.content)

        await self.update_profile(user_id, extracted)

4.4 MCP 协议实现

python 复制代码
# mcp_server.py --- MCP Server 端
import asyncio
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationCapabilities
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

# 创建 MCP Server
server = Server("agent-platform-mcp")

# 注册工具
@server.list_tools()
async def list_tools() -> list[Tool]:
    """返回可用工具列表"""
    return [
        Tool(
            name="search_knowledge_base",
            description="搜索企业知识库",
            inputSchema={
                "type": "object",
                "properties": {
                    "query": {"type": "string", "description": "搜索查询"},
                    "category": {
                        "type": "string",
                        "enum": ["tech", "product", "policy"],
                        "default": "all",
                    },
                },
                "required": ["query"],
            },
        ),
        Tool(
            name="get_user_info",
            description="获取用户信息",
            inputSchema={
                "type": "object",
                "properties": {
                    "user_id": {"type": "string"},
                },
                "required": ["user_id"],
            },
        ),
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    """执行工具调用"""
    if name == "search_knowledge_base":
        query = arguments["query"]
        category = arguments.get("category", "all")
        # 实际调用知识库搜索
        result = f"搜索'{query}'(类别:{category})的结果..."
        return [TextContent(type="text", text=result)]

    elif name == "get_user_info":
        user_id = arguments["user_id"]
        # 实际查询用户信息
        return [TextContent(type="text", text=f"用户{user_id}的信息...")]

    return [TextContent(type="text", text=f"未知工具: {name}")]

# 注册资源
@server.list_resources()
async def list_resources() -> list:
    """返回可用资源列表"""
    return [
        {
            "uri": "knowledge://company/policies",
            "name": "公司政策",
            "description": "公司内部政策文档",
            "mimeType": "text/markdown",
        },
    ]

@server.read_resource()
async def read_resource(uri: str) -> str:
    """读取资源内容"""
    if uri == "knowledge://company/policies":
        return "# 公司政策\n\n## 年假政策\n..."
    return f"未找到资源: {uri}"


async def main():
    """启动 MCP Server"""
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationCapabilities(
                sampling={},
                experimental={},
            ),
        )

if __name__ == "__main__":
    asyncio.run(main())
python 复制代码
# mcp_client.py --- MCP Client 端(集成到 Agent)
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from langchain_mcp_adapters.tools import load_mcp_tools

class MCPIntegration:
    """MCP 协议集成------将 MCP Server 的工具接入 Agent"""

    def __init__(self, server_command: str, server_args: list[str]):
        self.server_params = StdioServerParameters(
            command=server_command,
            args=server_args,
        )

    async def get_tools(self):
        """获取 MCP Server 提供的所有工具"""
        async with stdio_client(self.server_params) as (read, write):
            async with ClientSession(read, write) as session:
                await session.initialize()

                # 加载 MCP 工具为 LangChain 工具
                tools = await load_mcp_tools(session)
                return tools

    async def execute_tool(self, tool_name: str, **kwargs):
        """执行 MCP 工具"""
        async with stdio_client(self.server_params) as (read, write):
            async with ClientSession(read, write) as session:
                await session.initialize()
                result = await session.call_tool(tool_name, kwargs)
                return result

# 在 Agent 中使用 MCP 工具
async def integrate_mcp_to_agent(agent_service):
    """将 MCP 工具集成到 Agent"""
    mcp = MCPIntegration(
        server_command="python",
        server_args=["mcp_server.py"],
    )

    mcp_tools = await mcp.get_tools()

    # 注册到 Agent 的 Tool Registry
    for tool in mcp_tools:
        agent_service.register_tool(
            name=tool.name,
            description=tool.description,
            func=tool.arun if hasattr(tool, "arun") else tool.run,
        )

5. 代码示例

5.1 贯穿项目:统一记忆管理器

python 复制代码
# app/memory/manager.py
from app.memory.vector_memory import VectorMemory
from app.memory.profile_memory import ProfileMemory
import structlog

logger = structlog.get_logger(__name__)

class MemoryManager:
    """统一记忆管理器

    协调三层记忆的读写,为 Agent 提供"记得"的能力
    """

    def __init__(
        self,
        vector_memory: VectorMemory,
        profile_memory: ProfileMemory,
        short_term_store: dict = None,
    ):
        self.vector = vector_memory
        self.profile = profile_memory
        self.short_term: dict[str, list] = short_term_store or {}

    def get_conversation(self, session_id: str) -> list:
        """获取短期记忆(当前会话)"""
        return self.short_term.get(session_id, [])

    def add_message(self, session_id: str, message: dict):
        """添加消息到短期记忆"""
        if session_id not in self.short_term:
            self.short_term[session_id] = []
        self.short_term[session_id].append(message)

        # 超过 50 轮自动触发摘要
        if len(self.short_term[session_id]) > 100:
            self._trigger_summary(session_id)

    async def search_memory(
        self, query: str, user_id: str = "default"
    ) -> list[dict]:
        """搜索长期记忆"""
        return await self.vector.search(query, user_id=user_id)

    async def save_to_long_term(
        self, session_id: str, user_id: str,
        summarizer, llm,
    ):
        """将短期记忆存档为长期记忆"""
        messages = self.short_term.pop(session_id, [])
        if not messages:
            return

        # 生成摘要
        summary = await summarizer.summarize(messages)

        # 保存为向量记忆
        await self.vector.save(
            content=summary,
            memory_type="conversation_summary",
            user_id=user_id,
            importance=0.7,
            metadata={"session_id": session_id, "message_count": len(messages)},
        )

        # 提取并更新用户画像
        conversation_text = "\n".join([
            f"{m['role']}: {m['content']}" for m in messages
        ])
        await self.profile.extract_and_save(user_id, conversation_text, llm)

        logger.info(
            "memory_archived",
            session_id=session_id,
            messages=len(messages),
        )

    async def build_enriched_context(
        self, user_input: str, session_id: str, user_id: str
    ) -> str:
        """构建增强上下文------将记忆注入 Prompt"""
        context_parts = []

        # 1. 用户画像
        profile = await self.profile.get_profile(user_id)
        if profile.name:
            context_parts.append(f"用户姓名:{profile.name}")
        if profile.preferences:
            pref_str = ", ".join(f"{k}={v}" for k, v in profile.preferences.items())
            context_parts.append(f"用户偏好:{pref_str}")

        # 2. 相关长期记忆
        memories = await self.vector.search(user_input, user_id=user_id, n_results=3)
        if memories:
            context_parts.append("相关历史对话:")
            for m in memories[:3]:
                context_parts.append(f"- {m['content'][:200]}")

        return "\n".join(context_parts)

5.2 带记忆的 Agent Prompt

python 复制代码
MEMORY_AWARE_PROMPT = """你是 {agent_name},一个智能 AI Agent。

## 用户画像
{user_profile}

## 相关历史记忆
{relevant_memories}

## 当前对话历史
{conversation_history}

## 用户最新消息
{user_input}

请基于以上所有信息回应用户。如果历史记忆中有相关的上下文,请自然地引用它。
"""

6. 实战案例

案例1:个人助手 --- 完整的记忆系统

python 复制代码
# 构建一个"记得一切"的个人助手
class PersonalAssistant:
    def __init__(self, memory_manager, agent_service, llm):
        self.memory = memory_manager
        self.agent = agent_service
        self.llm = llm

    async def chat(self, user_id: str, session_id: str, message: str) -> str:
        # 1. 获取用户画像 + 相关记忆
        profile = await self.memory.profile.get_profile(user_id)
        memories = await self.memory.search_memory(message, user_id)

        # 2. 获取当前对话历史
        history = self.memory.get_conversation(session_id)

        # 3. 构建增强 Prompt
        enriched_prompt = MEMORY_AWARE_PROMPT.format(
            agent_name="小助手",
            user_profile=f"姓名:{profile.name or '未知'}\n偏好:{profile.preferences}",
            relevant_memories="\n".join([m["content"][:200] for m in memories]),
            conversation_history="\n".join([
                f"{m['role']}: {m['content'][:100]}" for m in history[-10:]
            ]),
            user_input=message,
        )

        # 4. 调用 Agent
        response = await self.agent.run(enriched_prompt)

        # 5. 更新记忆
        self.memory.add_message(session_id, {"role": "user", "content": message})
        self.memory.add_message(session_id, {"role": "assistant", "content": response})

        # 6. 提取并更新用户画像
        await self.memory.profile.extract_and_save(
            user_id,
            f"User: {message}\nAssistant: {response}",
            self.llm,
        )

        return response

案例2:MCP 多工具集成------数据库 + 天气 + 知识库

这个案例演示如何配置多个 MCP Server,让 Agent 同时具备数据库查询、天气查询和知识库搜索能力:

python 复制代码
"""
MCP 多 Server 配置示例。

安装:
  uv add mcp

启动方式:
  方式一:通过 IDE(如 WorkBuddy/Cursor)的 MCP 配置文件管理
  方式二:通过代码手动启动 MCP Server 进程

场景:Agent 需要同时访问以下三个 MCP Server:
  1. database-mcp:查询业务数据库
  2. weather-mcp:查询天气信息
  3. knowledge-mcp:搜索企业知识库
"""

import asyncio
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from contextlib import AsyncExitStack


class MultiMCPManager:
    """
    多 MCP Server 管理器。

    同时管理多个 MCP Server 的连接,获取所有可用工具列表,
    并支持工具调用路由------根据工具名自动路由到正确的 Server。
    """

    def __init__(self):
        self.sessions: dict[str, ClientSession] = {}
        self._exit_stack = AsyncExitStack()
        self._tools_cache: list[dict] = []

    async def connect_all(self):
        """连接所有 MCP Server"""

        servers = {
            "database": StdioServerParameters(
                command="uv",
                args=["run", "database-mcp-server"],
            ),
            "weather": StdioServerParameters(
                command="uv",
                args=["run", "weather-mcp-server"],
            ),
            "knowledge": StdioServerParameters(
                command="uv",
                args=["run", "knowledge-mcp-server"],
            ),
        }

        for name, params in servers.items():
            try:
                transport = await self._exit_stack.enter_async_context(
                    stdio_client(params)
                )
                read_stream, write_stream = transport
                session = await self._exit_stack.enter_async_context(
                    ClientSession(read_stream, write_stream)
                )
                await session.initialize()
                self.sessions[name] = session
                print(f"✅ MCP Server [{name}] 已连接")
            except Exception as e:
                print(f"❌ MCP Server [{name}] 连接失败: {e}")

    async def list_all_tools(self) -> list[dict]:
        """列出所有 MCP Server 提供的工具"""
        all_tools = []
        for name, session in self.sessions.items():
            try:
                result = await session.list_tools()
                for tool in result.tools:
                    # 给工具名加前缀以区分来源
                    all_tools.append({
                        "name": f"{name}__{tool.name}",
                        "description": tool.description,
                        "source": name,
                        "original_name": tool.name,
                    })
            except Exception as e:
                print(f"列出 {name} 工具失败: {e}")
        self._tools_cache = all_tools
        return all_tools

    async def call_tool(self, tool_name_with_prefix: str, arguments: dict):
        """根据带前缀的工具名路由到正确的 MCP Server"""
        source, original_name = tool_name_with_prefix.split("__", 1)
        session = self.sessions.get(source)
        if not session:
            raise ValueError(f"MCP Server [{source}] 未连接")

        return await session.call_tool(original_name, arguments)

    async def close_all(self):
        """关闭所有 MCP Server 连接"""
        await self._exit_stack.aclose()
        self.sessions.clear()
        print("👋 所有 MCP Server 已断开")


# ===== 在 Agent 中使用多 MCP =====
async def agent_with_multi_mcp(prompt: str):
    """Agent 使用多个 MCP Server 处理用户请求"""

    manager = MultiMCPManager()
    await manager.connect_all()

    # 获取所有可用工具
    tools = await manager.list_all_tools()
    print(f"可用工具数: {len(tools)}")
    for t in tools:
        print(f"  - {t['name']}: {t['description'][:50]}...")

    # 构建工具描述给 LLM
    tools_desc = "\n".join([
        f"- {t['name']}: {t['description']}" for t in tools
    ])

    # LLM 决定调用哪个工具
    llm_prompt = f"""你是一个智能 Agent,可以使用以下工具:

{tools_desc}

用户请求:{prompt}

分析需要使用哪些工具,以 JSON 格式返回工具调用列表:
[{{"tool": "database__query", "args": {{"sql": "SELECT ..."}}}}, ...]"""

    # 这里简化了 LLM 调用流程
    # 实际项目中用 LangChain/LangGraph 的 Tool Calling

    await manager.close_all()

案例3:多 Agent 共享记忆

当多个 Agent 协作时,需要共享记忆以实现上下文传递。以下是基于 Redis Pub/Sub 的跨 Agent 记忆总线:

python 复制代码
import json
import asyncio
from datetime import datetime
from typing import Optional


class SharedMemoryBus:
    """
    多 Agent 共享记忆总线。

    允许多个 Agent 实例之间共享短期记忆------当一个 Agent 学到新信息,
    其他 Agent 可以实时感知。

    使用 Redis Pub/Sub 作为通信通道(生产环境)或内存队列(开发环境)。
    """

    def __init__(self, use_redis: bool = False, redis_url: str = None):
        self._subscribers: dict[str, list[callable]] = {}
        self._memory_log: list[dict] = []  # 共享记忆日志
        self.use_redis = use_redis

    async def publish(self, event_type: str, agent_id: str, payload: dict):
        """
        发布事件到共享记忆总线。
        例如:一个 Agent 发现用户偏好后发布,其他 Agent 接收并更新。
        """
        event = {
            "type": event_type,
            "agent_id": agent_id,
            "timestamp": datetime.now().isoformat(),
            "payload": payload,
        }

        # 存入共享日志
        self._memory_log.append(event)

        # 通知所有订阅者
        subscribers = self._subscribers.get(event_type, [])
        for callback in subscribers:
            try:
                if asyncio.iscoroutinefunction(callback):
                    await callback(event)
                else:
                    callback(event)
            except Exception as e:
                print(f"[MemoryBus] 回调失败: {e}")

    def subscribe(self, event_type: str, callback: callable):
        """订阅特定类型的记忆事件"""
        if event_type not in self._subscribers:
            self._subscribers[event_type] = []
        self._subscribers[event_type].append(callback)

    async def query_shared_context(
        self, agent_id: str, context_key: str
    ) -> Optional[str]:
        """查询其他 Agent 发布的相关信息"""
        relevant = [
            e for e in self._memory_log
            if e["agent_id"] != agent_id  # 不查自己发布的
            and e["type"] == context_key
        ]
        if relevant:
            latest = relevant[-1]
            return json.dumps(latest["payload"], ensure_ascii=False)
        return None


# ===== 使用示例 =====
async def demo_shared_memory():
    bus = SharedMemoryBus()

    # Agent A:处理用户查询,发现用户偏好
    async def on_user_preference_learned(event):
        print(f"  [Agent B 收到] Agent {event['agent_id']} 学到:{event['payload']}")

    # Agent B 订阅偏好更新事件
    bus.subscribe("user_preference", on_user_preference_learned)

    # Agent A 发布新发现的用户偏好
    await bus.publish("user_preference", "agent-sales", {
        "user_id": "user_123",
        "preference": "喜欢简洁的回复,不要太多术语",
        "confidence": 0.9,
    })

    # Agent B 查询共享上下文
    context = await bus.query_shared_context("agent-sales", "user_preference")
    print(f"Agent B 查询到共享上下文: {context}")

    await asyncio.sleep(0.1)  # 等待异步回调


# 实际项目中的使用模式:
# 1. Customer Agent 发现用户偏好 → 发布到总线
# 2. Sales Agent 订阅 → 自动调整推销策略
# 3. Support Agent 订阅 → 知道用户喜欢简洁回答
# 4. 所有 Agent 共享同一份"用户理解"

Java 对照:SharedMemoryBus ≈ Spring Cloud Bus(事件总线) + Spring Event(应用事件),但更专注于 Agent 间的上下文共享而非系统级事件。


7. Java 对比

维度 Java 做法 Python 做法
短期记忆 ChatMemory (Spring AI) 内存 list / Redis
长期记忆 需自建 VectorMemory + Chroma
记忆检索 数据库查询 Embedding 语义搜索
用户画像 UserProfile Service ProfileMemory + Redis
MCP Spring AI MCP Boot Starter mcp Python SDK
工具协议 无统一标准 MCP 标准协议

8. 企业最佳实践

8.1 记忆系统设计原则

  1. 分层存储:短期 → 摘要 → 长期,逐层降级
  2. 重要性评分:不是所有对话都需要记住(LLM 评估重要性)
  3. 遗忘机制:TTL + 重要性衰减 + 存储上限
  4. 隐私优先:用户可以删除、导出记忆

8.2 MCP 服务设计

复制代码
单个 MCP Server = 一个领域的所有工具
例如:
- knowledge-mcp:知识库搜索、文档读取
- database-mcp:数据库查询
- filesystem-mcp:文件操作
- email-mcp:邮件发送

9. 常见错误与解决方案

错误 原因 解决
记忆膨胀 无限保存所有对话 实施 TTL + 重要性过滤
检索不准确 Embedding 质量不佳 使用更好的 Embedding 模型或 Hybrid Search
MCP 连接断开 stdio 进程退出 加入重连机制 + 健康检查
画像信息冲突 LLM 提取了矛盾的信息 带时间戳的版本化画像

10. 本章总结

本章完成了 Enterprise AI Agent Platform 的 记忆系统和 MCP 集成

  1. 三层记忆:Working/Short-term/Long-term 三层架构
  2. 记忆检索:向量语义搜索 + 用户画像
  3. MCP 协议:统一工具接口标准
  4. 记忆增强 Prompt:自动注入用户画像和历史记忆

贯穿项目进度:Agent 拥有了长期记忆。最后一章将实现 Multi-Agent 和企业级部署。


11. 面试题

  1. 解释 Agent Memory 的三层架构。为什么需要分层?
  2. MCP 协议解决了什么问题?为什么需要统一标准?
  3. 向量记忆和结构化记忆各自的适用场景是什么?
  4. 如何防止 Agent 记忆隐私泄露?

12. 练习

练习1:实现带记忆的客服 Agent(难度:★★★)

构建一个"记得用户上次问了什么"的客服 Agent。

练习2:构建 MCP Server(难度:★★★★)

用 MCP SDK 构建一个提供天气查询和新闻搜索的 MCP Server。

练习3:记忆管理 CLI(难度:★★★)

实现 memory search <query>memory forget <id>memory export 命令。


13. 作业

  1. 实现完整的 MemoryManager
  2. 构建 MCP Server(知识库搜索)
  3. 将记忆和 MCP 集成到 Agent 服务

14. 预习

最后一章:Multi-Agent 与企业级 AI Agent 平台 --- 多 Agent 协作、部署、监控和成本管理。


文档版本 :v1.0 | 创建日期 :2026-07-23 | 适用 Python 版本:3.12+

相关推荐
CodeBlog-star3 小时前
LLM安全实战:提示词注入与越狱攻击防御指南
python·agent·提示词攻击·越狱攻击
lifallen3 小时前
模型不是函数:claude-cookbooks/misc 十四篇的公共底层
人工智能·学习·ai·ai编程
小柯南敲键盘4 小时前
电商图片翻译工具推荐,批量处理主图视频字幕,免费试用
大数据·人工智能·python·音视频
比高创意品牌策划设计4 小时前
零售卖场设计软件用CAD还是SketchUp还是酷家乐
python
lucky_syq5 小时前
第5篇 · S1·下:前沿架构:MoE、Reasoning 模型、长上下文、多模态、SSM/Mamba 与模型谱系
人工智能·学习·架构
日拱一卒的小田5 小时前
ZYNQ学习笔记3-ZYNQ的IIC控制器1
笔记·单片机·学习
迷迭香yy6 小时前
行业板块轮动因子实战从板块资金到因子建模的本地化Python全流程
数据库·人工智能·python
W_326006 小时前
Python文件进阶:一维数据与 CSV 文件读写
开发语言·python
yaoxin5211237 小时前
497. Java 反射 - 使用反射读取注解
java·开发语言·python