Zep记忆服务部署实战:从本地Docker Compose到云端生产 1. Zep到底是什么为什么你需要单独部署一个记忆服务先说个我自己的经历。去年我在做一个客服助手项目最开始图省事直接用Redis存对话历史每次用户进来就拉最近20条消息拼进Prompt里。本地测试一切正常一上线就露馅了对话稍微一长Token开销成倍涨用户三天前问过的东西模型完全不记得——准确地说不是不记得是我根本没把钱花在让它记得这件事上。后来我认真调研了一圈发现行业内已经开始把记忆从业务代码里单独抽出来做成一个独立的基础设施。这就是Zep要做的事情。Zep是一个开源的长期记忆服务专门给AI Agent和对话应用用的。它的核心价值可以概括成三句话它会自动帮你在后台做对话历史的持久化存储不用你自己维护消息表它内置了Graphiti时序知识图谱能从对话里抽取实体和关系构建用户画像和事实记忆它通过记忆窗口和相关性搜索只把当前最有用的记忆注入到LLM上下文里大幅降低Token消耗我打个比方。普通的对话存储就像你往仓库里堆纸箱什么都有但想找东西得翻个底朝天。Zep的做法是先派了个图书管理员把每箱东西分类、贴标签、建立索引然后你说帮我找那份关于项目延期的沟通记录管理员直接走到对应货架把最贴切的几份文件递给你。在LLM的世界里递给你这个过程就是把它写进上下文窗口。这个项目的GitHub仓库目前有相当高的关注度而且它提供了Python和TypeScript两种SDK后端服务用Docker容器部署结构上分三个核心组件组件作用端口Zep Service主服务提供REST API和GraphQL API8000PostgreSQL存图数据、实体关系、Graphiti索引5432Vector DB存向量嵌入支持Qdrant或pgvector6333或复用PG所以如果你看到了Zep部署这个词不管你是做对话机器人、智能客服、Copilot应用还是做AI Agent的记忆增强这篇文章要讲的东西都对你有用。我会从本地开发环境一路讲到云端生产把那些文档里没写清楚但在实际部署中一定会踩的坑全部过一遍。2. 本地开发环境搭建一套Compose把三件套跑起来2.1 为什么推荐Docker Compose起步Zep最舒服的启动方式就是Docker Compose。官方仓库里带了完整的compose文件我没有做任何魔改就直接跑起来了。这里要说一下为什么不建议你直接在宿主机上装PostgreSQL和Qdrant再来跑ZepZep和PostgreSQL之间的版本耦合非常紧不同的Zep版本对PG的插件版本、Schema迁移都有要求消息队列和向量库的版本也需要匹配。用Docker Compose的好处是所有依赖都锁在一个编排文件里拉起来就是个完整环境删掉也不留垃圾。网上关于docker安装部署、dify本地部署的声音很多我的观点是Docker Compose不是可选项是默认项。尤其是Zep这种多组件协作的服务单容器部署反而会增加排障难度。2.2 具体步骤从零到一跑起来第一步确认环境。我建议Docker版本在20.10以上Docker Compose插件已启用。然后执行git clone https://github.com/getzep/zep.git cd zep仓库根目录下有个docker-compose.yaml文件。我用的这版内容大概是这样的version: 3.8 services: zep: image: ghcr.io/getzep/zep:0.23.0 ports: - 8000:8000 environment: - ZEP_STORE_POSTGRES_DSNpostgresql://postgres:postgresdb:5432/postgres - ZEP_GRAPHITE_VECTOR_DB_URLhttp://qdrant:6333 depends_on: - db - qdrant db: image: postgres:16-alpine environment: - POSTGRES_USERpostgres - POSTGRES_PASSWORDpostgres - POSTGRES_DBpostgres volumes: - pgdata:/var/lib/postgresql/data qdrant: image: qdrant/qdrant:v1.9.1 volumes: - qdrantdata:/qdrant/storage volumes: pgdata: qdrantdata:启动命令很简单docker compose up -d docker compose ps等服务状态变成healthy就可以请求健康检查接口了curl http://localhost:8000/health返回{status:ok}就说明主服务起来了。注意这时候PostgreSQL和Qdrant可能还在初始化最好再等个十几秒。2.3 首次启动必须确认的几个环境变量Zep的配置项很多但本地跑通只需要关注这几个ZEP_STORE_POSTGRES_DSN ZEP_GRAPHITE_VECTOR_DB_URL ZEP_AUTH_SECRET ZEP_OPENAI_API_KEY前两个是连接串第三个是JWT签名用的密钥第四个是Graphiti抽取实体时调用的LLM接口Key。这里有个非常容易忽略的点你本地哪怕只是跑demo也必须配置一个有效的OpenAI API Key否则Graphiti的实体抽取完全不会工作整个记忆图谱是空的你测来测去都会觉得Zep好像啥也没干。如果你不想用OpenAIZep也支持通过环境变量切换LLM提供方我后面会单独讲。2.4 初始化配置的验证方法配置完之后建议做一个最简单的冒烟测试用Python SDK创建一个用户和一个会话然后投递几条消息。from zep_cloud.client import Zep # 本地走的是开源版SDKfrom zep_python import ZepClient我这里用的是开源版from zep_python import ZepClient client ZepClient(base_urlhttp://localhost:8000, api_keyoptional) user client.user.add( user_idtest_user_001, emailtestexample.com, first_name张三, last_name测试 ) session client.memory.add_session( session_idsession_001, user_idtest_user_001 ) client.memory.add_memory( session_idsession_001, messages[ {role: user, content: 你好我叫王明我负责公司的采购业务。}, {role: assistant, content: 好的王明很高兴认识你。你是公司的采购负责人。} ] )然后隔十几秒再查一下这个会话的记忆摘要memory client.memory.get(session_idsession_001) print(memory.facts)如果能看到张三、采购这类实体被抽取出来说明你已经把本地环境跑通了而且Graphiti也确实在正常工作。这一整套流程是你后续所有开发的基础花20分钟把它跑通非常值得。3. 理解Zep的记忆模型Graphiti时序知识图谱是关键3.1 记忆不是存起来那么简单很多人第一次用Zep会带着老思路去套把Zep当成一个更高级的Redis存了就取。实际上Zep的工作机制是完全不同的它的核心引擎Graphiti(Falcon)对开发者的心智模型要求是记忆分两层——一层叫事实记忆从对话里抽出来的实体和关系另一层叫会话记忆原始的对话消息序列。举个例子。用户在第一次会话里说我们公司用的是电商ERP系统最近要替换成自研系统。第二次会话里又提到ERP迁移项目中周报需要同步给技术总监。如果你只做Redis式的存取第二次会话时你根本不知道该把哪条历史记录塞进Prompt。但Zep做的事情是先通过LLM把第一句话里的实体抽取成公司-使用-电商ERP系统、公司-计划替换-自研系统这样的三元组再通过相似度检索当第二次会话提到ERP迁移时把相关的实体关系图谱片段召回然后由LLM重写生成一段上下文摘要。这就是它省Token的底层逻辑不把原始历史全塞进去而是塞压缩后的关键事实。3.2 手动注入记忆的两种方式实际开发中你一定会遇到需要手动干预记忆的情况。Zep给了两条路径逐条追加消息适合流式对话client.memory.add_memory( session_idsession_001, messages[ {role: user, content: 我们决定先把ERP迁移项目延期两周。}, {role: assistant, content: 好的我记下来ERP迁移项目延期两周。} ] )直接添加事实文本适合在业务逻辑里直接沉淀结论client.memory.add_fact( session_idsession_001, factERP迁移项目当前状态已延期两周预计在下季度初重新启动。 )这两者的区别在于add_memory走的是Graphiti抽取链路会在后台调用LLM去理解实体关系add_fact则直接写入事实库不经过抽取。如果你明确知道某条信息很重要且不需要抽取用add_fact更快更省因为我实测下来add_fact的响应速度要比add_memory快两倍以上因为它省掉了LLM调用和实体解析的耗时。3.3 检索时用search还是getZep的Memory相关API里我用得最多的是search。它接受一个查询字符串和可选的元数据过滤器返回的是和查询语义最相关的记忆片段。比如results client.memory.search( session_idsession_001, queryERP迁移项目目前进展如何, limit5 ) for r in results: print(r.text)而get返回的是整个会话的完整记忆摘要包括事实列表、时序图谱的token计数等适合在会话初始化时一次性灌入上下文。我的建议是做Agent应用时别偷懒用get把所有记忆全塞进上下文那样效果虽然粗暴但Token成本会不可控地增长。正确的姿势是针对用户当前的问题做一次search只取和问题相关的记忆片段。3.4 系统提示词里的记忆注入范式再分享一个我们团队实践下来的完整范式。每次用户发起新对话我们在系统提示词里拼这么一段你是一个拥有长期记忆的AI助手。以下内容是你从和用户的过往对话中获取的可靠信息 memory {memory_text} /memory 请基于这些记忆回答用户的问题如果记忆中没有相关信息请直接说明你不知道。其中{memory_text}就是上面search返回的结果按相关度排序后拼接而成。这样设计的核心价值是给了模型一个明确的边界——记忆里有的就用没有的不要瞎编。我见过很多团队把记忆检索结果直接塞Prompt但没加任何说明模型反而容易把旧事实和新问题混在一起产生幻觉。4. 从本地到生产架构规划与配置项逐个拆解4.1 本地和云端生产环境的五个关键差异本地跑通只是第一步从docker compose up到上云端生产中间隔着一整套架构决策。我先用一张表把差异说清楚维度本地开发云端生产PostgreSQL默认配置单机高可用独立实例RDS或云上PGQdrantDocker容器独立集群或托管服务持久化保证认证关闭或固定KeyJWT签名按环境隔离密钥HTTPS不需要必须配合反向代理实施可观测性看日志需要Prometheus监控指标和集中日志数据备份不做必须做至少每日全量实时WAL这个差异表不是凭空写出来的是我把Zep从开发环境搬到测试环境时踩了一周坑后总结出来的。每一项后面都有具体的故事我挑重点讲。4.2 详细展开每个配置项在生产环境该设成什么ZEP_STORE_POSTGRES_DSN这个连接串直接决定了你的数据安全底线。本地可以无脑postgres:postgres生产绝对不行。需要做到独立账号、最小权限、SSL强制。ZEP_STORE_POSTGRES_DSNpostgresql://zep_user:强密码pg-host:5432/zepdb?sslmoderequire我见过有人直接把云数据库的管理员账号填进来这是巨大的安全隐患。建议在数据库里单独建一个用户只授予Zep所需要的库的读写权限其余一律不给。ZEP_AUTH_SECRETZep服务之间的内部通信和JWT签发都用这个密钥。本地可以用任意字符串生产必须用至少32字节的随机串。生成方式openssl rand -hex 32放到K8s的Secret或云厂商的密钥管理服务里不要直接写进Compose文件再推到Git仓库。ZEP_OPENAI_API_KEYZep的Graphiti实现依赖LLM来做实体抽取和关系构建。生产环境要特别注意这个Key对应的账号是否有足够的Rate Limit因为在高并发对话场景下LLM调用量会迅速攀升。我们线上曾遇到一个教训高峰期每秒有50多个会话进来Graphiti的抽取任务把OpenAI账号的TPM打到上限导致Zep整体响应变慢连带影响了主业务接口。解决办法是给Zep配置独立的API Key并在OpenAI侧设好Hard Limit避免它把别的服务的额度也吃光。4.3 生产环境还需要关注的消息队列与异步任务Zep里很多耗时操作比如对话总结、实体抽取、图更新是通过后台任务异步执行的。当数据量上来之后如果还让Zep主服务同步处理这些任务接口响应时间会不可控。生产环境一般需要给Zep配上消息队列。我最初部署时没配结果遇到一个场景一个会话里有上百条历史消息第一次触发记忆抽取时接口直接超时。后来参考官方文档里的架构引入了消息队列实际上Zep早期版本内置了简单的任务队列但高负载下建议外挂把Graphiti的图更新、记忆总结等任务全部异步化主接口响应时间立刻恢复到了毫秒级。这个消息队列相关的配置变量大概是ZEP_MESSAGE_QUEUE_TYPEredis ZEP_MESSAGE_QUEUE_DSNredis://redis-service:6379/0这个配置项的具体命名在不同版本里有差异但思路是一致的生产环境不要让Zep自己单机扛所有异步负载把消息队列独立出来既方便扩展也能在主服务重启时不丢任务。4.4 内存、CPU和连接池的预估方法部署Zep之前你得先回答一个问题我的业务规模需要多大的实例单个Zep服务实例的内存大头不是服务本身而是Graphiti做向量检索和实体处理时的临时内存。根据我们的压测数据每秒10次对话请求Qdrant和PG分别独立部署Zep服务分配2核4GB足够每秒50次以上建议Zep服务4核8GBQdrant独立节点如果你的场景是重度知识库问答每次对话都会触发大量历史检索内存预算要翻倍PostgreSQL的连接池也要提前调。Zep默认的连接数在某些云数据库上会直接打满。我建议在Zep环境的连接串里加上pool_size10这样的参数或者用PgBouncer做中间的连接池代理。5. 云端生产环境的完整部署过程5.1 生产编排用一套独立的Compose还是上K8s如果你的团队没有专职的K8s运维我建议别一上来就上Kubernetes。Zep这种有状态依赖PG、Qdrant的服务在K8s里要处理存储卷、网络策略、服务发现一堆事复杂度直接翻倍。用云服务器Docker Compose或者云厂商的容器服务足够覆盖大部分生产场景。下面这套Compose文件是我在云上跑了好几个月的生产配置你可以直接参考修改services: zep: image: ghcr.io/getzep/zep:0.23.0 restart: always environment: - ZEP_STORE_POSTGRES_DSN${POSTGRES_DSN} - ZEP_GRAPHITE_VECTOR_DB_URLhttp://qdrant:6333 - ZEP_AUTH_SECRET${AUTH_SECRET} - ZEP_OPENAI_API_KEY${OPENAI_API_KEY} - ZEP_LOG_LEVELINFO ports: - 127.0.0.1:8000:8000 depends_on: - qdrant healthcheck: test: [CMD, curl, -f, http://localhost:8000/health] interval: 30s timeout: 5s retries: 3 qdrant: image: qdrant/qdrant:v1.9.1 restart: always volumes: - qdrant_data:/qdrant/storage volumes: qdrant_data:注意PostgreSQL这里我没有放进Compose而是直接用云数据库RDS或云厂商的PG实例。这不是偷懒而是刻意为之云数据库自带自动备份、故障切换、监控告警这些能力自己用容器去复刻投入产出比极低。生产环境的数据存储组件能用托管就用托管。5.2 反向代理和HTTPS的配置要点Zep服务本身不推荐直接暴露公网端口。我的做法是在前面挂一层Nginx把本机的8000端口代理出去同时配上HTTPS证书。server { listen 443 ssl; server_name zep-api.example.com; ssl_certificate /etc/nginx/ssl/fullchain.pem; ssl_certificate_key /etc/nginx/ssl/privkey.pem; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-Proto $scheme; proxy_read_timeout 300s; } }这里有个容易被忽视的坑Zep的健康检查路径是/health你配置云厂商的负载均衡健康检查时一定要指向这个路径而不是/。我见过有团队配成了根路径负载均衡显示后端不健康排查了半天最后发现是健康检查路径的问题。5.3 数据备份与恢复Zep的数据分两块PostgreSQL里存图数据和会话元数据Qdrant里存向量。这两者的备份策略要分开设计。PG的备份直接用云数据库的自动备份功能每天全量每5分钟的增量备份保留7天。手动备份可以执行pg_dump postgresql://user:passpg-host:5432/zepdb -F c -f zep_backup.dumpQdrant没有云数据库那么省心需要自己做快照。Qdrant支持API级别的快照可以定期调用curl -X PUT http://qdrant:6333/collections/zep_collection/snapshots然后把快照文件搬到对象存储里。恢复的时候用快照文件重新导入。为什么向量库的备份也重要因为向量数据是你用户记忆的索引索引丢了那Graphiti构建的知识图谱就残缺不全用户历史记忆的召回能力会大打折扣。这里建议写个简单的定时任务每天晚上把Qdrant快照同步到OSS/S3。5.4 可观测性日志、指标与告警Zep启动后会在标准输出打日志生产环境需要把它收集到ELK或者Loki这类日志系统里。更关键的是Zep暴露了/metrics端点可以接Prometheus监控。如果你正在折腾prometheus监控部署Zep就是一个很好的实践对象。scrape_configs: - job_name: zep metrics_path: /metrics static_configs: - targets: [zep-service:8000]我在生产里重点盯这几个指标HTTP请求延迟P95如果持续超过500ms就要检查是不是LLM调用或数据库连接出现问题异步任务队列积压数量如果积压一直在涨说明消费者处理能力跟不上内存使用率Graphiti处理大图时内存会周期性上涨告警阈值可以按你的业务容忍度来设但至少预留一条Zep服务不可用的告警否则用户开始反馈对话变傻了你还不一定知道是记忆服务挂了。6. 升级、性能调优与若干踩坑实录6.1 版本升级的正确姿势Zep的迭代速度不算慢但升级不是一个docker pull就完事的。因为它内置了PostgreSQL Schema迁移和Qdrant索引重建升级前必须做两件事第一备份。全量备份PG数据和Qdrant快照这一步不能省。第二查看官方Release Notes里有没有破坏性变更。Zep在版本升级时Graphiti的图结构可能发生迁移如果直接从0.21跳到0.23可能会导致元数据不兼容。我建议的升级路径是先在一个测试环境跑新版本连同一个旧版数据备份验证记忆检索功能正常之后再切生产。这个流程虽然多花半小时但能避免线上用户记忆突然消失的灾难。6.2 连接池与并发配置的调优经验Zep服务内部对PostgreSQL和Qdrant都有连接池。默认配置在低并发下没问题但在生产高并发下需要手动调。我线上用的几个关键参数ZEP_STORE_POSTGRES_POOL_SIZE20 ZEP_STORE_POSTGRES_MAX_OVERFLOW10 ZEP_GRAPHITE_VECTOR_DB_POOL_SIZE10这些数字要根据你的实际连接数来定。如果连接池配得太小高峰期请求会排队配得太大数据库那边可能先撑不住。我一般以数据库侧允许的最大连接数的一半为上限来设置。6.3 对话自动摘要与记忆时效问题Zep的Graphiti设计里有一个重要的特性对话记忆会随时间衰减。默认设置下太久远且没有被反复提及的实体关系在检索召回时的权重会变低。这个设计本身合理因为人的记忆也是这样。但如果你做的业务是法律咨询、医疗顾问这种一字千金的场景建议把时间衰减参数调低或者手动把关键对话标记为important防止被冲刷掉。我在一个合同审核助手项目里就踩过这个坑用户月初提到的一个合同条款细节月底再问的时候Zep已经检索不到了因为那是唯一一次提及权重被时间衰减压得过低。后来在业务流程里凡是涉及合同编号、金额数字、截止日期这类信息我都同时通过add_fact做硬性写入保证关键信息不被时间衰减影响。6.4 三个必须分享的坑坑一容器时区导致的时间错位。默认容器时区是UTCZep存储的时间戳都是UTC。如果你的业务数据分析和日志系统用的是北京时间对账的时候会发现所有时间都差8小时。解决方案是在Compose里给Zep容器配置TZAsia/Shanghai但更推荐的是在数据展示层统一做时区转换因为底层存储统一用UTC才是正经做法。坑二Graphiti的LLM调用超时。默认情况下Zep调用OpenAI做实体抽取时单个请求的超时时间设置得比较保守。遇到上下文特别长的对话抽取出错或超时记忆就不会写入。这个可以在环境变量里调大超时时间但更合理的做法是控制输入对话长度超过一定长度先做截断再交给Graphiti。坑三Qdrant端口被占用。本地开发时如果你之前装过Qdrant或其他向量数据库6333端口可能冲突。Docker Compose的端口映射会静默失败表现是Zep服务能起但向量检索始终报错。排查时先netstat -tlnp | grep 6333看端口是否真的被监听了。6.5 生产环境实测后的最终建议最后离开之前我根据这大半年的线上使用经验给你一套可落地的开工检查清单PostgreSQL用云托管开启自动备份连接串带SSL独立账号跑Zep最低权限不要用超管密钥AUTH_SECRET、API Key全部放密钥管理服务Qdrant独立部署每天自动快照到对象存储反向代理配好HTTPS健康检查指向/healthPrometheus接/metrics至少盯P95延迟和队列积压关键业务信息用add_fact硬性写入不依赖抽取时效升级前备份测试环境先验证新版本按照这套流程走下来Zep的服务稳定性是可以保证的。我这边跑了好几个月除了有一次云数据库主动切换导致连接中断了十几秒基本没有因为Zep本身出过线上故障。它值得放进你AI应用的技术栈里。