Zep记忆服务部署实战:从本地Docker Compose到云端生产
2026/9/18 9:33:30 网站建设 项目流程

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 API8000
PostgreSQL存图数据、实体关系、Graphiti索引5432
Vector DB存向量嵌入,支持Qdrant或pgvector6333或复用PG

所以如果你看到了"Zep部署"这个词,不管你是做对话机器人、智能客服、Copilot应用,还是做AI Agent的记忆增强,这篇文章要讲的东西都对你有用。我会从本地开发环境一路讲到云端生产,把那些文档里没写清楚但在实际部署中一定会踩的坑,全部过一遍。

2. 本地开发环境搭建:一套Compose把三件套跑起来

2.1 为什么推荐Docker Compose起步

Zep最舒服的启动方式就是Docker Compose。官方仓库里带了完整的compose文件,我没有做任何魔改就直接跑起来了。这里要说一下为什么不建议你直接在宿主机上装PostgreSQL和Qdrant再来跑Zep:Zep和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_DSN=postgresql://postgres:postgres@db:5432/postgres - ZEP_GRAPHITE_VECTOR_DB_URL=http://qdrant:6333 depends_on: - db - qdrant db: image: postgres:16-alpine environment: - POSTGRES_USER=postgres - POSTGRES_PASSWORD=postgres - POSTGRES_DB=postgres 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好像啥也没干"。

如果你不想用OpenAI,Zep也支持通过环境变量切换LLM提供方,我后面会单独讲。

2.4 初始化配置的验证方法

配置完之后,建议做一个最简单的冒烟测试:用Python SDK创建一个用户和一个会话,然后投递几条消息。

from zep_cloud.client import Zep # 本地走的是开源版SDK:from zep_python import ZepClient

我这里用的是开源版:

from zep_python import ZepClient client = ZepClient(base_url="http://localhost:8000", api_key="optional") user = client.user.add( user_id="test_user_001", email="test@example.com", first_name="张三", last_name="测试" ) session = client.memory.add_session( session_id="session_001", user_id="test_user_001" ) client.memory.add_memory( session_id="session_001", messages=[ {"role": "user", "content": "你好,我叫王明,我负责公司的采购业务。"}, {"role": "assistant", "content": "好的,王明,很高兴认识你。你是公司的采购负责人。"} ] )

然后隔十几秒再查一下这个会话的记忆摘要:

memory = client.memory.get(session_id="session_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_id="session_001", messages=[ {"role": "user", "content": "我们决定先把ERP迁移项目延期两周。"}, {"role": "assistant", "content": "好的,我记下来,ERP迁移项目延期两周。"} ] )

直接添加事实文本(适合在业务逻辑里直接沉淀结论):

client.memory.add_fact( session_id="session_001", fact="ERP迁移项目当前状态:已延期两周,预计在下季度初重新启动。" )

这两者的区别在于:add_memory走的是Graphiti抽取链路,会在后台调用LLM去理解实体关系;add_fact则直接写入事实库,不经过抽取。如果你明确知道某条信息很重要且不需要抽取,用add_fact更快更省,因为我实测下来add_fact的响应速度要比add_memory快两倍以上,因为它省掉了LLM调用和实体解析的耗时。

3.3 检索时用search还是get

Zep的Memory相关API里,我用得最多的是search。它接受一个查询字符串和可选的元数据过滤器,返回的是和查询语义最相关的记忆片段。比如:

results = client.memory.search( session_id="session_001", query="ERP迁移项目目前进展如何?", limit=5 ) 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或云上PG)
QdrantDocker容器独立集群或托管服务,持久化保证
认证关闭或固定KeyJWT签名,按环境隔离密钥
HTTPS不需要必须,配合反向代理实施
可观测性看日志需要Prometheus监控指标和集中日志
数据备份不做必须做,至少每日全量+实时WAL

这个差异表不是凭空写出来的,是我把Zep从开发环境搬到测试环境时,踩了一周坑后总结出来的。每一项后面都有具体的故事,我挑重点讲。

4.2 详细展开:每个配置项在生产环境该设成什么

ZEP_STORE_POSTGRES_DSN

这个连接串直接决定了你的数据安全底线。本地可以无脑postgres:postgres,生产绝对不行。需要做到:独立账号、最小权限、SSL强制。

ZEP_STORE_POSTGRES_DSN=postgresql://zep_user:强密码@pg-host:5432/zepdb?sslmode=require

我见过有人直接把云数据库的管理员账号填进来,这是巨大的安全隐患。建议在数据库里单独建一个用户,只授予Zep所需要的库的读写权限,其余一律不给。

ZEP_AUTH_SECRET

Zep服务之间的内部通信和JWT签发都用这个密钥。本地可以用任意字符串,生产必须用至少32字节的随机串。生成方式:

openssl rand -hex 32

放到K8s的Secret或云厂商的密钥管理服务里,不要直接写进Compose文件再推到Git仓库。

ZEP_OPENAI_API_KEY

Zep的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_TYPE=redis ZEP_MESSAGE_QUEUE_DSN=redis://redis-service:6379/0

这个配置项的具体命名在不同版本里有差异,但思路是一致的:生产环境不要让Zep自己单机扛所有异步负载,把消息队列独立出来,既方便扩展,也能在主服务重启时不丢任务。

4.4 内存、CPU和连接池的预估方法

部署Zep之前你得先回答一个问题:我的业务规模需要多大的实例?

单个Zep服务实例的内存大头不是服务本身,而是Graphiti做向量检索和实体处理时的临时内存。根据我们的压测数据:

  • 每秒10次对话请求,Qdrant和PG分别独立部署,Zep服务分配2核4GB足够
  • 每秒50次以上,建议Zep服务4核8GB,Qdrant独立节点
  • 如果你的场景是重度知识库问答,每次对话都会触发大量历史检索,内存预算要翻倍

PostgreSQL的连接池也要提前调。Zep默认的连接数在某些云数据库上会直接打满。我建议在Zep环境的连接串里加上pool_size=10这样的参数,或者用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_URL=http://qdrant:6333 - ZEP_AUTH_SECRET=${AUTH_SECRET} - ZEP_OPENAI_API_KEY=${OPENAI_API_KEY} - ZEP_LOG_LEVEL=INFO 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:pass@pg-host:5432/zepdb" -F c -f zep_backup.dump

Qdrant没有云数据库那么省心,需要自己做快照。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_SIZE=20 ZEP_STORE_POSTGRES_MAX_OVERFLOW=10 ZEP_GRAPHITE_VECTOR_DB_POOL_SIZE=10

这些数字要根据你的实际连接数来定。如果连接池配得太小,高峰期请求会排队;配得太大,数据库那边可能先撑不住。我一般以数据库侧允许的最大连接数的一半为上限来设置。

6.3 对话自动摘要与"记忆时效"问题

Zep的Graphiti设计里有一个重要的特性:对话记忆会随时间衰减。默认设置下,太久远且没有被反复提及的实体关系,在检索召回时的权重会变低。这个设计本身合理,因为人的记忆也是这样。但如果你做的业务是法律咨询、医疗顾问这种"一字千金"的场景,建议把时间衰减参数调低,或者手动把关键对话标记为important,防止被冲刷掉。

我在一个合同审核助手项目里就踩过这个坑:用户月初提到的一个合同条款细节,月底再问的时候Zep已经检索不到了,因为那是唯一一次提及,权重被时间衰减压得过低。后来在业务流程里,凡是涉及"合同编号、金额数字、截止日期"这类信息,我都同时通过add_fact做硬性写入,保证关键信息不被时间衰减影响。

6.4 三个必须分享的坑

坑一:容器时区导致的时间错位。默认容器时区是UTC,Zep存储的时间戳都是UTC。如果你的业务数据分析和日志系统用的是北京时间,对账的时候会发现所有时间都差8小时。解决方案是在Compose里给Zep容器配置TZ=Asia/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,健康检查指向/health
  • Prometheus接/metrics,至少盯P95延迟和队列积压
  • 关键业务信息用add_fact硬性写入,不依赖抽取时效
  • 升级前备份,测试环境先验证新版本

按照这套流程走下来,Zep的服务稳定性是可以保证的。我这边跑了好几个月,除了有一次云数据库主动切换导致连接中断了十几秒,基本没有因为Zep本身出过线上故障。它值得放进你AI应用的技术栈里。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询