SmartVoyage 智途 A2A v7.8 技术解析(四):工程实践——双层缓存、四种记忆与 Docker 部署
系列导航:第一篇:架构总览 | 第二篇: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 系统是如何从代码变成可运行服务的。如果有任何问题,欢迎在评论区交流。
更多推荐


所有评论(0)