系列导航:第一篇:架构总览 | 第二篇:ReAct 推理链路 | 第三篇:A2A 协议与 MCP 工具调用


前言

前三篇文章分别从架构总览、ReAct 推理链路、A2A/MCP 协议层三个维度,拆解了 SmartVoyage 智途的"智能"部分。但在实际生产环境中,一个 AI 系统能不能用、好不好用,往往取决于那些"不那么性感"的工程细节:

  • 同一个问题问两次,要不要每次都调 LLM?(缓存
  • 用户上一轮说"我要二等座",下一轮 Agent 还记得吗?(记忆
  • 11 个容器怎么编排?API Key 怎么注入?SSE 流式响应怎么穿透 Nginx?(部署

这篇文章就是来解决这些问题的。我们会逐行阅读 cache_manager.py(454 行)、memory_manager.py(467 行)和 deploy/ 目录下的全部配置文件,把 SmartVoyage 的工程实践讲透。


一、双层缓存:Redis 精确匹配 + Milvus 语义匹配

1.1 为什么需要双层缓存?

LLM 调用有两个显著特点:(单次 2-10 秒)和(按 token 计费)。如果用户 A 问了"北京明天天气怎么样",Agent 花 5 秒、消耗 2000 token 得到了回答,5 分钟后用户 B 问了同样的问题——再调一次 LLM 就是纯粹的浪费。

最直接的方案是加缓存。但传统的 KV 缓存(如 Redis)只能做精确匹配——query 字符串必须一字不差才能命中。用户换一种说法("明天北京天气如何"vs"北京明天天气怎么样"),精确缓存就失效了。

SmartVoyage 的解法是双层缓存

层级 后端 匹配方式 速度 适用场景
L1 Redis MD5 哈希,精确匹配 O(1),极快 完全相同的 query
L2 Milvus 向量余弦相似度 ≥ 0.92 需要 Embedding + 向量检索 语义相似但措辞不同的 query

查询时先走 L1(快速路径),未命中再走 L2(慢速路径)。两层都没命中,才真正调用 LLM。

1.2 缓存架构全景

这个设计的精妙之处在于缓存提升(Cache Promotion):当 Milvus 语义缓存命中时,系统会自动把结果回写到 Redis。这样下次相同的 query 进来,就直接走 Redis 快速路径了,不需要再做向量检索。高频 query 会自然"沉淀"到 L1,低频 query 在 L2 兜底。

1.3 Redis 精确缓存:MD5 哈希 + 用户隔离

来看 CacheManager 的 Redis 层实现:

# core/cache_manager.py

class CacheManager:
    def __init__(self):
        # Redis 客户端初始化
        self._redis = redis.Redis(
            host=CONFIG.CACHE_REDIS_HOST,
            port=CONFIG.CACHE_REDIS_PORT,
            db=CONFIG.CACHE_REDIS_DB,
            password=CONFIG.CACHE_REDIS_PASSWORD,
            decode_responses=True,  # 自动解码为字符串
        )

    @staticmethod
    def _hash(text: str) -> str:
        """MD5 哈希取前 16 位——长度与碰撞概率的折中"""
        return hashlib.md5(text.encode()).hexdigest()[:16]

    @staticmethod
    def _k(user_id: str, text: str) -> str:
        """生成 Redis Key: voyage:exact:{user_id}:{md5_hash}"""
        return f"{CONFIG.CACHE_REDIS_KEY_PREFIX}:exact:{user_id}:{CacheManager._hash(text)}"

    def exact_get(self, user_id: str, query: str) -> str | None:
        """O(1) 精确匹配查询"""
        return self._redis.get(self._k(user_id, query))

    def exact_set(self, user_id: str, query: str, response: str):
        """SETEX 原子写入 + 设置 TTL"""
        self._redis.setex(
            self._k(user_id, query),
            CONFIG.CACHE_REDIS_TTL, response  # 默认 600 秒
        )

这里有几个工程细节值得注意:

Key 设计:voyage:exact:{user_id}:{md5_hash},通过嵌入 user_id 实现用户级隔离。用户 A 和用户 B 即使问了完全相同的问题,缓存也是独立的——这避免了用户看到别人缓存结果的安全隐患。

MD5 截取 16 位:完整 MD5 是 32 位十六进制字符串,取前 16 位在缓存场景下碰撞概率极低(约 16^16 ≈ 1.8×10^19 种组合),同时让 Key 更短,节省 Redis 内存。

SETEX 原子操作:用 setex 而不是先 set 再 expire,保证"设值"和"设过期"是一个原子操作,避免中间态(值写入了但过期没设上,导致缓存永不过期)。

1.4 Milvus 语义缓存:向量相似度 + TTL 过期

语义缓存是 SmartVoyage 缓存体系的亮点。来看核心代码:

# core/cache_manager.py

def semantic_get(self, user_id: str, query: str) -> str | None:
    """Milvus 语义缓存查询"""
    # 1. 将 query 转为向量(调用阿里云 text-embedding-v3)
    vec = self._milvus.generate_embedding(query)
    if vec is None:
        return None

    # 2. 计算 TTL 过期截止时间戳
    cutoff = int(time.time()) - CONFIG.CACHE_MILVUS_TTL
    # 3. 构造过滤表达式:用户隔离 + TTL 过期过滤
    expr = f'user_id == "{user_id}" && created_at > {cutoff}'

    # 4. 向量相似度搜索(top_k=1,只取最相似的 1 条)
    results = self._milvus.search(
        coll_name=CONFIG.CACHE_MILVUS_COLLECTION,
        vector=vec,
        top_k=1,
        output_fields=["query_text", "response_text", "created_at"],
        metric_type="IP",   # 内积(Inner Product)
        expr=expr,
    )

    # 5. 判断相似度是否达到阈值
    if results and results[0]:
        hit = results[0][0]
        if hit.distance >= CONFIG.CACHE_SIMILARITY_THRESHOLD:  # 默认 0.92
            return hit["entity"]["response_text"]
    return None

语义缓存的关键设计:

相似度阈值 0.92:这个值是经过实验调优的。太低(如 0.8)会把不相关的问题误判为"相同问题",返回错误的缓存结果;太高(如 0.98)则几乎退化为精确匹配,失去语义缓存的意义。0.92 在"北京明天天气"和"明天北京天气怎么样"这类同义改写场景下能稳定命中,同时不会把"北京天气"和"北京机票"混淆。

用户隔离用 expr 过滤:Redis 通过 Key 中嵌入 user_id 隔离,Milvus 则通过查询时的 expr 过滤条件实现。user_id == "xxx" && created_at > cutoff 这个表达式同时完成了用户隔离和 TTL 过期清理两件事。

TTL 过期清理:Milvus 不像 Redis 有原生的 Key 过期机制,所以 SmartVoyage 在 semantic_set 写入时会顺便清理该用户的过期记录:

def semantic_set(self, user_id: str, query: str, response: str):
    vec = self._milvus.generate_embedding(query)
    if vec is None:
        return
    # 写入前清理该用户的过期记录
    cutoff = int(time.time()) - CONFIG.CACHE_MILVUS_TTL
    try:
        self._milvus.client.delete(
            collection_name=CONFIG.CACHE_MILVUS_COLLECTION,
            filter=f'user_id == "{user_id}" && created_at <= {cutoff}'
        )
    except Exception as e:
        logger.warning("Milvus 过期缓存清理失败: %s", e)
    # 写入新记录...

1.5 缓存提升与双写策略

get() 和 set() 是对外暴露的组合入口:

def get(self, user_id: str, query: str) -> str | None:
    """双层查询:Redis → Milvus → None"""
    # L1: Redis 精确匹配
    result = self.exact_get(user_id, query)
    if result:
        return result
    # L2: Milvus 语义匹配
    result = self.semantic_get(user_id, query)
    if result:
        # 缓存提升:语义命中 → 回写 Redis
        self.exact_set(user_id, query, result)
        return result
    return None

def set(self, user_id: str, query: str, response: str):
    """双写:Redis + Milvus"""
    self.exact_set(user_id, query, response)
    self.semantic_set(user_id, query, response)

1.6 缓存在 API 层的集成

缓存并不在 VoyageChatCore(推理引擎)内部,而是在 API 层(main_http_api.py)作为外层包装集成:

# main_http_api.py

_cache = CacheManager()  # 全局单例

@app.post("/api/chat")
async def chat(request: ChatRequest):
    # 1. 先查缓存
    cached = _cache.get(request.user_id, request.message)
    if cached:
        return {"response": cached, "cached": True}

    # 2. 缓存未命中 → 调用推理引擎
    svc = ChatService(user_id=request.user_id)
    response = svc.chat(request.message)

    # 3. 写入缓存
    _cache.set(request.user_id, request.message, response)
    return {"response": response, "cached": False}

这种分层设计的好处是:推理引擎完全不需要知道缓存的存在。VoyageChatCore 只负责意图识别 → 任务规划 → ReAct 执行 → 结果汇总,缓存逻辑全部由 API 层处理。职责清晰,也方便单独测试。

在 SSE 流式响应场景下,缓存命中时会逐字符返回(每字符 50ms 延迟),模拟流式效果,保持前端体验一致。


二、四种记忆:让 Agent 真正"记住"用户

如果说缓存解决的是"相同问题不重复计算",那记忆解决的就是"Agent 能不能像人一样记住对话上下文"。SmartVoyage 实现了四种记忆类型,各有分工:

2.1 短期记忆:最近 10 条对话

短期记忆是最直接的"对话上下文"——让 Agent 知道用户前几句说了什么。

# core/memory_manager.py

class ConversationMemory:
    def __init__(self, user_id: str, messages_limit: int = 10):
        self.user_id = user_id
        self.short_term_messages = []  # 短期消息列表
        self.messages_limit = messages_limit  # 默认保留 10 条

    def add_message(self, role: str, content: str):
        """添加消息并持久化"""
        self.short_term_messages.append({
            "role": role,
            "content": content,
            "timestamp": datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%H:%M:%S')
        })
        # 先删除数据库中该用户的所有旧短期消息
        self._db.execute(
            'DELETE FROM short_term_messages WHERE user_id = %s',
            (self.user_id,)
        )
        # 全量重写最新的 messages_limit 条
        for i, msg in enumerate(self.short_term_messages[-self.messages_limit:]):
            self._db.execute(
                sql="INSERT INTO short_term_messages "
                    "(user_id, role, content, message_time, message_order) "
                    "VALUES (%s, %s, %s, %s, %s)",
                params=(self.user_id, msg["role"], msg["content"],
                        msg["timestamp"], i)
            )

这里用的是全量重写策略:每次添加新消息时,先 DELETE 该用户的所有旧记录,再把最新的 N 条重新 INSERT。为什么不增量?因为短期记忆最多 10 条,全量重写的开销可以忽略,但逻辑上更简单可靠——不用处理"哪条是新的、哪条是旧的"这种边界情况。

格式化后注入 LLM prompt 的效果:

User: 帮我查一下北京到上海的火车票 Assistant: 好的,请问您想查哪一天的? User: 明天

这段文本会被注入到意图识别 prompt 的 {conversation_history} 占位符中,让 LLM 知道用户说的"明天"是承接上一句的火车票查询。

2.2 用户偏好:upsert 语义的键值对

用户偏好记录从对话中提取的个性化信息,比如"我喜欢二等座""我偏好经济舱"。

def update_profile(self, profile_update: dict):
    """更新用户偏好——MySQL upsert"""
    self.user_profile.update(profile_update)
    for key, value in self.user_profile.items():
        self._db.execute(
            sql="INSERT INTO user_profiles "
                "(user_id, profile_key, profile_value) "
                "VALUES (%s, %s, %s) "
                "ON DUPLICATE KEY "
                "UPDATE profile_value = %s",
            params=(self.user_id, key, str(value), str(value))
        )

关键点是 INSERT ... ON DUPLICATE KEY UPDATE 语法——MySQL 的 upsert 实现。表上有 (user_id, profile_key) 的联合唯一约束,如果这个偏好项已存在就更新值,不存在就新增。这比"先查再决定 INSERT 还是 UPDATE"要高效得多,一条 SQL 搞定。

偏好文本会被注入到意图识别 prompt 的 {user_profile} 占位符:

座位喜好: 二等座,舱位类型: 经济舱

这样 LLM 在做意图识别和查询改写时,会自动带上用户的偏好。

2.3 任务上下文:仅存内存的临时状态

任务上下文跟踪用户当前正在处理的任务参数:

def update_task_context(self, task_update: dict):
    """更新任务上下文——仅内存,不持久化"""
    self.current_task.update(task_update)

注意:任务上下文不做数据库持久化。这是有意为之的设计——任务是会话级的临时状态("用户当前在查火车票"),服务重启后这个状态已经没有意义了。如果重启后还需要恢复任务上下文,说明业务流程本身需要重新设计。

任务上下文以 JSON 形式注入到意图识别 prompt 的 {task_context} 占位符,帮助 LLM 理解当前任务进展。

2.4 长期记忆:最近 20 轮提问历史

长期记忆保存用户的历史提问,让 Agent 能回溯用户之前的关注点:

def update_entities(self, intent_type: str, query: str):
    """添加一条长期记忆"""
    now = datetime.now(pytz.timezone('Asia/Shanghai')).strftime('%Y-%m-%d %H:%M:%S')
    self.entity_history.append({
        "type": intent_type,  # 意图分类,如 "train_booking"
        "query": query,
        "timestamp": now
    })
    # 裁剪:只保留最新 20 条
    self.entity_history = self.entity_history[-20:]
    # 持久化到数据库
    self._db.execute(
        sql="INSERT INTO query_history "
            "(user_id, intent_type, query_content, query_time) "
            "VALUES (%s, %s, %s, %s)",
        params=(self.user_id, intent_type,
                json.dumps({"query": query}, ensure_ascii=False), now)
    )

query_content 以 JSON 格式存储({"query": "帮我查明天北京到上海的火车"}),而不是直接存字符串。这是一个扩展性设计——未来如果需要增加更多字段(如意图置信度、执行结果摘要),只需要扩展 JSON 即可,不用改表结构。

2.5 记忆加载与序列化

服务重启时,通过 load_*_from_db 方法从 MySQL 恢复所有记忆:

def _load_memory_from_db(self):
    """从 MySQL 加载三类持久化记忆"""
    self.memory.load_profile_from_db(close_after=False)   # 用户偏好
    self.memory.load_entities_from_db(close_after=False)  # 长期记忆
    self.memory.load_messages_from_db(close_after=True)   # 短期记忆(最后一次关闭连接)

close_after 参数的设计是个小优化:前两次加载保持连接(close_after=False),最后一次加载时关闭(close_after=True),避免反复建连接。

此外,to_dict() 和 from_dict() 提供了序列化/反序列化能力,方便调试和跨模块传递记忆快照。

2.6 四种记忆的协作关系


三、Docker 部署:11 容器单端口编排

3.1 整体架构

SmartVoyage 的 Docker 部署方案用 11 个容器承载全部 7 个 Python 服务 + 3 个 Nginx 反向代理 + 1 个定时任务,对外仅暴露一个 8888 端口。

为什么要用三层 Nginx?因为内部服务(MCP、A2A)不需要对外暴露,通过 Nginx 代理后,所有服务间通信都在 Docker 内部网络完成,外部只能看到 8888 一个端口。这既是安全考虑(减少攻击面),也是架构清晰度的体现。

3.2 统一 Dockerfile:一个镜像服务七个角色

# deploy/Dockerfile

FROM python:3.10-slim
WORKDIR /app

# 系统依赖
RUN apt-get update && apt-get install -y --no-install-recommends \
    gcc \
    && rm -rf /var/lib/apt/lists/*

# 先复制依赖文件,利用 Docker 缓存层
COPY deploy/requirements.txt requirements.txt
RUN pip install --no-cache-dir -r requirements.txt

# 复制全部代码
COPY . .

# 设置 Python 模块搜索路径
ENV PYTHONPATH=/app

CMD ["python", "--version"]

这个 Dockerfile 有三个值得学习的设计:

一个镜像七个角色:所有 Python 服务共用同一个镜像,具体启动哪个服务由 docker-compose.yml 的 command 决定。比如 api-gateway 启动 uvicorn,a2a-weather 启动 waitress。这样做的好处是构建一次、处处运行,避免维护 7 个 Dockerfile。

分层缓存优化:先 COPY requirements.txt 再 pip install,最后 COPY . .。这样只要 requirements.txt 没变,pip install 这一步就会命中 Docker 缓存,不用重新安装 141 个依赖包,大幅加快构建速度。

PYTHONPATH 设置:ENV PYTHONPATH=/app 确保 from utils import CONFIG 这类导入在容器内正常工作,因为代码被复制到了 /app 根目录。

3.3 docker-compose.yml:11 容器编排

来看 docker-compose.yml 的核心结构(已精简注释):

# deploy/docker-compose.yml

services:
  # ===== API 网关 =====
  api-gateway:
    build:
      context: ..
      dockerfile: deploy/Dockerfile
    command: uvicorn main_http_api:app --host 0.0.0.0 --port 8888
    environment:
      - DOCKER_SERVER_HOST=0.0.0.0
      - DOCKER_SERVER_PORT=8888
      - DOCKER_DB_MYSQL_HOST=${DOCKER_DB_MYSQL_HOST:-192.168.88.101}
      - DOCKER_CACHE_REDIS_HOST=${DOCKER_CACHE_REDIS_HOST:-192.168.88.101}
      - DOCKER_MCP_WEATHER_URL=http://mcp-proxy:5001
      - DOCKER_A2A_WEATHER_URL=http://a2a-proxy:6001
      - DOCKER_ALI_API_KEY=${ALI_API_KEY}
      # ... 更多环境变量
    depends_on:
      - mcp-proxy
      - a2a-proxy

  # ===== MCP 工具服务(3个)=====
  mcp-weather:
    build: { context: .., dockerfile: deploy/Dockerfile }
    command: uvicorn mcp_server.mcp_weather:app --host 0.0.0.0 --port 5001

  mcp-ticket:
    command: uvicorn mcp_server.mcp_ticket:app --host 0.0.0.0 --port 5002

  mcp-trip:
    command: uvicorn mcp_server.mcp_trip:app --host 0.0.0.0 --port 5003

  # ===== A2A 智能体服务(3个)=====
  a2a-weather:
    command: waitress-serve --host=0.0.0.0 --port=6001 --threads=1 a2a_server.a2a_weather:app
    depends_on: [mcp-proxy]

  a2a-ticket:
    command: waitress-serve --host=0.0.0.0 --port=6002 --threads=1 a2a_server.a2a_ticket:app

  a2a-trip:
    command: waitress-serve --host=0.0.0.0 --port=6003 --threads=1 a2a_server.a2a_trip:app

  # ===== Nginx 反向代理(3个)=====
  api-proxy:
    image: nginx:alpine
    ports: ["8888:80"]     # 唯一对外暴露的端口
    volumes:
      - ./nginx/api-proxy.conf:/etc/nginx/nginx.conf:ro

  mcp-proxy:
    image: nginx:alpine
    volumes:
      - ./nginx/mcp-proxy.conf:/etc/nginx/nginx.conf:ro

  a2a-proxy:
    image: nginx:alpine
    volumes:
      - ./nginx/a2a-proxy.conf:/etc/nginx/nginx.conf:ro

  # ===== 定时任务 =====
  weather-collector:
    command: python -m mcp_tools.weather_tools
    restart: always

几个关键设计点:

uvicorn vs waitress:MCP 服务和 API 网关用 uvicorn(异步 ASGI 服务器),A2A 服务用 waitress(同步 WSGI 服务器)。这是因为 MCP 和 API 网关需要处理异步 IO(SSE 流式响应、asyncio 并发),而 A2A 服务内部使用 a2wsgi 将 ASGI 应用桥接为 WSGI,waitress 更稳定。

服务间通信用容器名:DOCKER_MCP_WEATHER_URL=http://mcp-proxy:5001 中的 mcp-proxy 是 Docker Compose 的服务名,Docker 内置 DNS 会自动解析为容器 IP。服务间不需要知道彼此的实际 IP 地址。

环境变量注入 API Key:${ALI_API_KEY} 从宿主机环境变量读取,不在代码或配置文件中硬编码。每次新建 SSH 会话都需要 export ALI_API_KEY=sk-xxx,否则变量会被替换为空字符串。

3.4 Nginx 反向代理:SSE 流式穿透

API 网关的 Nginx 配置需要特别处理 SSE(Server-Sent Events)流式响应:

# deploy/nginx/api-proxy.conf

worker_processes auto;

events {
    worker_connections 1024;
}

http {
    # Docker 内置 DNS,容器重建后自动解析到新 IP
    resolver 127.0.0.11 valid=10s;

    upstream api_gateway {
        server api-gateway:8888;
    }

    server {
        listen 80;

        location / {
            proxy_pass http://api_gateway;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;

            # SSE 流式响应关键配置
            proxy_buffering off;    # 关闭响应缓冲,数据实时推送
            proxy_cache off;        # 关闭缓存

            # 超时设置(AI 对话可能耗时较长)
            proxy_read_timeout 300s;
            proxy_send_timeout 300s;
            proxy_connect_timeout 10s;
        }
    }
}

proxy_buffering off 是 SSE 的关键——如果开启缓冲,Nginx 会把后端的流式数据攒到一定量再发给客户端,用户就会感觉"卡了一下然后一大段文字突然出现"。关掉缓冲后,每个 token 都能实时推送到前端。

resolver 127.0.0.11 valid=10s 使用 Docker 内置 DNS,并设置 10 秒缓存。这解决了容器重建后 IP 变化导致 Nginx 502 的问题——如果不设 valid=10s,Nginx 可能在启动时缓存了旧 IP,之后容器重建 IP 变了,Nginx 还在往旧 IP 发请求。

A2A 代理的超时设了 180 秒(proxy_read_timeout 180s),因为子 Agent 内部需要调 LLM + MCP 工具,耗时比 MCP 层更长。

3.5 配置驱动的动态地址:DockerConfig

在前三篇中提到过,SmartVoyage 用 DockerConfig 子类实现环境变量的动态覆盖。在 Docker 环境中,所有服务地址都通过 DOCKER_* 环境变量注入:

# utils/config.py

class DockerConfig(Config):
    """Docker 环境配置——环境变量覆盖 YAML 默认值"""

    def __init__(self):
        super().__init__()
        # 数据库地址
        self.DB_MYSQL_HOST = os.getenv("DOCKER_DB_MYSQL_HOST", self.DB_MYSQL_HOST)
        self.DB_MILVUS_HOST = os.getenv("DOCKER_DB_MILVUS_HOST", self.DB_MILVUS_HOST)
        # 缓存地址
        self.CACHE_REDIS_HOST = os.getenv("DOCKER_CACHE_REDIS_HOST", self.CACHE_REDIS_HOST)
        # MCP 服务地址
        self.MCP_WEATHER_URL = os.getenv("DOCKER_MCP_WEATHER_URL", self.MCP_WEATHER_URL)
        # A2A 服务地址
        self.A2A_WEATHER_HOST = os.getenv("DOCKER_A2A_WEATHER_HOST", "a2a-proxy")
        # API Key
        self.LLM_API_KEY = os.getenv("DOCKER_ALI_API_KEY", self.LLM_API_KEY)

这样同一份代码在本地开发时读 config.yaml 的默认值(127.0.0.1),在 Docker 中读环境变量(mcp-proxy、a2a-proxy 等容器名),不需要改任何代码。

3.6 部署命令速查

# 启动全部服务
cd /root/voyage/deploy && docker compose up -d --build

# 扩缩容:启动 3 个 MCP 天气实例
docker compose up -d --scale mcp-weather=3

# 重建后必须重启代理和网关(否则 Nginx 缓存旧 IP 导致 502)
docker restart deploy-api-proxy-1 deploy-mcp-proxy-1 deploy-a2a-proxy-1
docker restart deploy-api-gateway-1

# 查看日志
docker compose logs -f api-gateway

四、工程实践总结

4.1 缓存与记忆的关系

缓存和记忆在 SmartVoyage 中是两套独立系统,但目标互补:

维度 缓存(CacheManager) 记忆(ConversationMemory)
目的 减少 LLM 调用,降低成本 让 Agent 理解上下文
粒度 整个 query → response 单条消息、偏好、任务状态
生命周期 TTL 过期(600秒) 持久化,重启后恢复
用户隔离 Redis Key + Milvus expr MySQL WHERE user_id
存储后端 Redis + Milvus MySQL

4.2 系列回顾

至此,SmartVoyage 智途 A2A v7.8 的四篇技术解析全部完成:

  • 第一篇:六层七服务架构总览,从 API 网关到 mcp_tools 的全局视角
  • 第二篇:ReAct 推理链路,意图识别 → 任务规划 → 依赖分组并行执行 → 反思 → 汇总
  • 第三篇:A2A 协议与 MCP 工具调用,AgentCard 声明、任务状态机、LangChain Agent 桥接
  • 第四篇:工程实践,双层缓存(Redis + Milvus)、四种记忆(短期/偏好/任务/长期)、Docker 11 容器编排

整个项目的设计哲学可以总结为三个关键词:分层(每层只做一件事,通过接口串联)、配置驱动(新增 Agent 只需改 YAML + 部署)、用户隔离(缓存、记忆、任务状态全部按 user_id 隔离)。

希望这四篇文章能帮助你理解一个完整的多 Agent 系统是如何从代码变成可运行服务的。如果有任何问题,欢迎在评论区交流。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐