做 Agent 最怕什么?聊两句就失忆,重启一下什么都不记得。我最近手头的项目就是这样踩出来的——基于AgentScope从零搭一个生产级记忆型AI Agent,不是那种“你问我答”的 Demo,而是真正能记住用户偏好、记住任务进度、在长对话里不跑偏的智能体。这篇文章我把项目从架构设计到落地细节完整拆一遍,重点聊记忆怎么设计、AgentScope 2.0 怎么用、并发怎么扛、上线有哪些坑。适合正在学 AI Agent 的开发者,也适合想从“能跑”推到“能生产”的团队。
1. 为什么是记忆型 Agent,以及 AgentScope 能帮到什么
1.1 没有记忆的 Agent 只能算“问答壳子”
很多人刚开始搭 Agent 的时候,都会觉得“接个大模型,写个 Prompt,再调个工具,就是 Agent 了”。实际用两轮就会发现不对劲:用户明明在上一句说“我习惯九点之后不要发通知”,下一轮 Agent 立刻忘得一干二净。这不是模型笨,而是你的 Agent 压根没有记忆机制。
传统的大模型调用是无状态的,每次请求都是独立对话,上下文全靠你手动把历史消息拼进去。但生产场景里,用户需要的是“一个长期陪伴的助手”,不是“每次都要重新自我介绍的路人”。记忆型 Agent 要解决的就是三件事:记住用户说了什么、筛选哪些信息值得长期保留、调用合适的信息来辅助当前决策。
我见过不少团队把“记忆”简单做成“把所有对话存数据库,每次全部拿出来塞进 Prompt”。这个方案在小规模没问题,一上量就完蛋:上下文越长,Token 成本越高,模型注意力越分散,关键信息反而被淹没。所以记忆不是存储问题,是检索和遗忘问题。
1.2 AgentScope 2.0 提供的生产线要素
我当时选 AgentScope,主要看中的是它不是一个“单 Agent 玩具框架”,而是把多 Agent 协作、消息通信、分布式执行这些生产级要素都考虑进去了。AgentScope 2.0 相比早期版本,明显强化了对复杂工作流的支持,它允许你把不同职责的 Agent 组合成一条流水线,Agent 之间通过结构化消息通信,而不是靠“一堆 Prompt 来回拼接”。
这个设计对记忆型 Agent 特别友好。为什么?因为记忆模块适合作为独立“中间件”存在,而不是硬塞在某个 Agent 的内部逻辑里。用 AgentScope 的消息驱动机制,我可以让“对话 Agent”收到用户消息时,自动触发“记忆查询 Agent”去检索相关历史,再把检索结果拼入上下文。这个流程在 AgentScope 里就是几个 Agent 节点之间的消息流动,清晰且好维护。
另外,AgentScope 自带分布式执行能力,这在后面扛并发时帮了大忙。它不是简单地把请求循环跑一遍,而是支持多个 Agent 实例并行运行、跨节点通信。对生产环境来说,这意味着你可以水平扩展,而不是单点瓶颈。
1.3 项目目标:从“聊得顺”到“记得住、用得上”
我给自己定的目标不是“做一个会聊天的机器人”,而是“做一个能独立接手任务、持续跟进、不会被历史信息搞晕的生产级助手”。
具体拆成四个目标:
- 多轮记忆:同一个用户的多轮对话,能自动提取关键事实并保存。
- 长期衰减:很久以前的信息会慢慢变淡,不能永远和刚发生的事一样重要。
- 主动检索:每次处理新请求时,自动找出最相关的历史记忆,而不是全量拼接。
- 可观测:能查到这个 Agent 当时想起了什么、忘了什么、为什么这样决策。
这四条落实下来,基本就是一套双网络记忆模型加衰减权重机制。下面我会把这块单独拿出来讲,因为这是整个项目的灵魂。
2. 记忆机制设计:score + 时间半衰期 + 双网络
2.1 记忆不是数据库堆字段
一开始我也走过弯路,觉得只要建一张memory表,字段有user_id、content、created_at,就万事大吉。结果上线测了三天,发现两个大问题:
第一,记忆没有优先级。用户三年前随口说的一句“我喜欢蓝色”和昨天刚说的“这个项目下周必须交付”,在检索时权重完全一样。模型很容易被旧信息干扰。
第二,记忆不会遗忘。有些临时性信息,比如“明天出差去上海”,本来过了就该失效,结果每次都被检索出来,反而干扰当前决策。后来我想明白一件事:人的记忆本来就有遗忘机制,AI Agent 如果想做到“像人一样”,就不能只做加法不做减法。
那答案就落到两个关键词上:score和时间半衰期。
2.2 记忆强度 score 怎么算
我们项目里的核心公式其实不复杂:
[ score = importance \times frequency \times decay^{(\Delta t / half_life)} ]
拆分解释:
- importance:这条记忆本身有多重要。可以是用户明确强调的信息(“记住,我晚上不接电话”),也可以是你用大模型对对话内容打的重要度标签(0~1)。
- frequency:这条记忆被提到的次数。被重复提及的信息,往往比只出现过一次的更可靠。比如用户连续三次强调“我不吃辣”,这个信息的置信度就该往上走。
- decay:衰减系数,一个小于 1 的常数,比如 0.5 或 0.9。
- half_life:半衰期,表示这条记忆从满权重衰减到一半需要多长时间。这个值取决于业务场景:临时日程可能只需要几小时,长期偏好可能按月算。
这个公式的妙处在于:一条重要但很久没被提到的记忆,不会永久霸占高位;一条被反复提及的新记忆,却能快速爬到高位。这很像人的记忆曲线。
举个例子:用户第一天说“我喜欢极简风”,第二天又说“我喜欢极简风”,第三天还说“买东西先考虑极简设计”。这条记忆的 importance=0.8,frequency=3,即使半衰期设为 30 天,它的 score 依然很高。反观用户某次随口说“最近想养猫”,之后再没提过,过了两个月 score 衰减到几乎可以忽略,检索时自然排到很后面,不会干扰决策。
2.3 双网络记忆模型:短期工作记忆与长期存储网络
热词里提到的“双网络记忆模型”,我在项目里的实现是分开两层,不是融合成一个模块:
第一层是短期工作记忆(Working Memory)。这层保存当前会话的上下文,包括最近几轮对话、当前任务状态、临时约束。它的特点是读取快、数据量小、跟随会话生命周期。在 AgentScope 里,这层直接挂在对话 Agent 内部,用一个大窗口队列实现,超时或者会话结束就自动清理。
第二层是长期记忆网络(Long-term Memory)。这一层负责把有价值的信息沉淀下来,做去重、更新、衰减、检索。它不是简单存字符串,而是以“记忆条目(Memory Item)”为单位存储。每个记忆条目除了包含内容本身,还携带用户 ID、来源会话、创建时间、最后访问时间、importance、frequency、当前 score 等元数据。
这两层网络不是孤立的。长期记忆经过检索后,会被拉取到短期工作记忆里,作为当前对话的“背景信息”;而短期工作记忆中那些被判定为重要的信息,会在会话结束或特定时机被写入长期记忆。这就是一个闭环。
2.4 写入、更新、提取的完整流程
我把整个记忆生命周期拆成了四条路径,每一条都在项目里落了代码:
写入路径:对话结束后,把当前会话的关键内容交给大模型做信息抽取,产出若干条结构化记忆候选。然后对每条候选判断:它是不是已经在长期记忆里存在?如果存在,走更新逻辑;如果不存在,作为新条目插入,初始 score 由 importance 和 frequency 共同决定。
更新路径:当新记忆和老记忆语义相似时,不是简单新增一条,而是合并。比如用户第一次说“我喜欢安静的环境”,第二次说“我在咖啡馆办公受不了噪音”,两条信息其实都在表达同一个偏好,就应该合并成一条“喜欢安静环境,受不了噪音”,同时 frequency+1,最后访问时间刷新。
衰减路径:定期跑一个批处理任务,对所有记忆条目重新计算 score。这个任务不需要太频繁,我一般每 10 分钟跑一次。衰减不是把 score 减到零就直接删,而是设一个阈值,低于阈值才进入“待清理”状态,再观察一段时间如果用户不再触发,才真正删除。
提取路径:当用户发起新消息时,先把当前短期记忆拉出来,再用用户 ID 和当前消息内容去长期记忆里检索 Top-K 条相关记忆。检索不一定只用向量相似度,还要结合刚才说的 score 做加权排序。我常用的做法是:先用 Embedding 召回 Top 50,再按“向量距离 × score”的综合分选出 Top 10,这样既保证语义相关,又保证重要记忆优先。
这套模型跑通之后,效果非常明显:同一个用户第二天回来,Agent 能一口叫出他的名字,记得他上次聊到一半的项目,还会主动问“上次那版方案的反馈你看了吗”。这种体验和没有记忆的 Agent 完全是两个物种。
3. 从零搭建:AgentScope 项目落地实操
3.1 项目骨架与依赖清单
我在项目里用的语言是 Python,框架层面选择 AgentScope 2.0,额外装上用于向量检索的库(比如 chromadb 或 qdrant),以及一个支持异步的 Web 框架暴露 API。依赖清单大致如下:
agentscope>=2.0 chromadb fastapi uvicorn pydantic openai / dashscope(或你需要的 LLM 客户端) sentence-transformers redis sqlalchemy不要把所有东西都堆在一个文件里。我建议按职责拆目录,后期找人维护和加功能都轻松:
agent_scope/ ├── agents/ # AgentScope Agent 定义 │ ├── chat_agent.py │ ├── memory_agent.py │ └── tool_agent.py ├── memory/ │ ├── long_term.py # 长期记忆库 │ ├── working.py # 短期工作记忆 │ ├── scoring.py # score 与半衰期计算 │ └── extractor.py # 大模型抽取记忆 ├── core/ # 配置、日志、数据库 ├── api/ # FastAPI 接口 └── main.py # 启动入口3.2 定义 Agent 与记忆单元
AgentScope 里的 Agent 可以理解成一个具备独立能力和消息处理逻辑的单元。项目里我定义了两个核心 Agent:ChatAgent负责和用户交互,MemoryAgent负责检索和更新记忆。下面是一个简化版的MemoryAgent代码结构,你可以直接参考:
from agentscope.agent import AgentBase from agentscope.message import Msg class MemoryAgent(AgentBase): def __init__(self, memory_store, llm_client): super().__init__(name="MemoryAgent") self.memory_store = memory_store self.llm_client = llm_client def __call__(self, message: Msg) -> Msg: # 从消息中拿到用户 ID 和当前内容 user_id = message.metadata.get("user_id") content = message.content # 第一阶段:检索相关记忆 recalled = self.memory_store.recall(user_id, content, top_k=10) # 第二阶段:把记忆拼入上下文返回 memory_text = self._format_memory(recalled) return Msg( name="MemoryAgent", content=memory_text, metadata={"recalled_count": len(recalled)} )这里的memory_store就是长期记忆库的封装。实际项目里,我会让ChatAgent在接收用户输入时,先向MemoryAgent发一条查询消息,把返回的记忆文本插入到系统 Prompt 里。这样模型看到的是“用户当前消息 + 相关历史记忆”,而不是一长串无差别对话。
3.3 LLM 调用与工具接入
有了记忆作为背景,下一步是让 Agent 能调用外部工具。生产级 Agent 不能只靠模型“想”,还得让它“做”,比如查天气、查数据库、发邮件。AgentScope 的工具注册机制很直接,用装饰器就能把一个普通函数变成 Agent 可调用的工具。
from agentscope.tool import Tool @Tool def get_order_status(order_id: str) -> str: # 这里模拟查询订单,实际可以接数据库或内部接口 return f"订单 {order_id} 当前状态:已发货" @Tool def create_reminder(content: str, user_id: str) -> str: # 写入长期记忆,供后续轮次使用 memory_store.add(user_id, content, importance=0.9) return f"已记住:{content}"这个设计的要点是把“记忆写入”也封装成一个工具。用户说“记住明天下午三点开会”,ChatAgent 识别意图后直接调用create_reminder,把这条信息写入长期记忆库。这比在对话结束后再统一抽取更及时,特别是对于“指令式”的记忆需求。
工具调用会有失败,别让 Agent 硬撑。我给每个工具调用都加了超时限制和错误返回,一旦工具报错,Agent 会收到一段错误描述,然后重新规划下一步,而不是直接把错误抛给用户。
3.4 多 Agent 协作扩展
项目到这里已经可以跑一个单 Agent 的记忆助手了。但生产环境往往需求更复杂,比如用户问“我的订单什么时候到”,这个问题可能同时涉及订单查询、物流查询、天气对配送影响判断三个能力。如果全塞进一个 Agent,逻辑会越来越臃肿。
AgentScope 2.0 的多 Agent 协作模型很适合解决这个问题。你可以把任务拆成专精的小 Agent,然后用一种“路由 + 执行”的模式组织起来:
- RouterAgent:接收用户问题,判断该交给哪个下游 Agent。
- OrderAgent:处理所有订单相关查询。
- MemoryAgent:提供历史记忆和用户偏好。
- ChatAgent:汇总各方结果,生成最终回复。
简化代码如下:
from agentscope.pipeline import SequentialPipeline from agentscope.message import Msg pipeline = SequentialPipeline( agents=[ memory_agent, router_agent, order_agent, chat_agent ] ) result = pipeline( Msg( name="User", content="我的订单为什么还没到?", metadata={"user_id": "u_123"} ) )这种方式的好处是:每个 Agent 只做自己擅长的事,MemoryAgent 不需要懂订单业务,OrderAgent 不需要管用户偏好,ChatAgent 只负责把信息组织成自然语言。代码复用性高,出问题也好排查——看哪一环坏了就修哪一环。
4. 生产级关键点:并发、持久化、可观测性
4.1 AgentScope 怎么扛并发
“AI Agent 怎么扛并发”这个热词我是深有感触的。很多套框架的单机 Demo 跑到生产环境,一上压力就崩,问题通常出在几个地方:消息并发处理、共享存储锁、记忆写入冲突。
AgentScope 本身支持多 Agent 并行执行,但你在部署时还需要做几件事:
第一,无状态与有状态分离。Agent 服务器保持无状态,用户会话状态全部放在 Redis 或数据库里。这样你可以水平起多个 Agent 实例,前面挂负载均衡,随便哪个实例接手请求都能从 Redis 恢复上下文。
第二,消息幂等。生产环境下请求可能重试,尤其是有外部回调的时候。我建议每条消息都带一个全局唯一的message_id,在消费端做去重。比如 Redis 里存已处理消息的 ID,重复收到就直接丢弃。
第三,记忆写入加分布式锁。如果有多个 Agent 实例同时处理同一个用户的会话,很容易出现“两个实例同时更新一条记忆,后写覆盖先写”的问题。我的方案是对记忆条目做版本号,更新时比较版本,如果不一致就合并重试。简单说就是乐观锁,而不是每次都锁整张表。
我在压测时的经验是:用好这些机制,单机 QPS 稳定在 50~100 不是问题,水平扩展 3 个节点以后就能轻松扛住生产流量。真正卡脖子的往往不是 Agent 框架,而是 LLM 接口的限流和数据库连接池。
4.2 记忆持久化选型:从 SQLite 到向量库
记忆数据不能一直放在内存里,必须落盘。选型我踩过一轮,直接给你对比结论:
| 存储方案 | 适合场景 | 优点 | 缺点 |
|---|---|---|---|
| SQLite | 单机开发、Demo | 零部署、简单 | 并发写入弱 |
| PostgreSQL | 中小规模生产 | 稳定、支持 JSON、事务强 | 向量检索能力一般 |
| ChromaDB | 向量检索为主 | 上手快、适合原型 | 生产运维生态一般 |
| Qdrant / Milvus | 大规模生产 | 分布式、高并发向量检索 | 部署和运维成本高 |
| Redis | 短期记忆、实时状态 | 极快、天然分布式 | 持久化能力有限 |
我的建议是“组合拳”:短期工作记忆放 Redis,长期记忆的元数据和结构化信息放 PostgreSQL,向量索引放 Qdrant。不要试图让一个数据库干所有事。
每个记忆条目在库里的结构大概这样:
memory_id : 字符串,唯一 ID user_id : 字符串,归属用户 content : 文本,记忆内容 embedding : 向量,用于语义检索 importance : 浮点数,初始重要度 frequency : 整数,被提到/命中的次数 score : 浮点数,当前记忆强度 last_accessed : 时间戳,最近一次被检索的时间 created_at : 时间戳,创建时间 version : 整数,乐观锁版本4.3 可观测性与调试技巧
生产级 Agent 最怕“黑盒”:用户反馈说 Agent 回答得不对,你却不知道它当时想起来什么。所以我在项目里加了非常详细的日志管道。
每次用户请求处理完,我都会输出一条结构化的 trace 日志,包含:
- 用户输入原文
- 检索到的记忆条目标题、score、最后访问时间
- 送入模型的最终 Prompt 片段
- 模型输出
- 工具调用记录和耗时
利用 AgentScope 的消息传递机制,每个节点间传递的消息都可以打上 tag,方便在日志系统里按trace_id串联。我用的是 JSON 格式日志,接 Elasticsearch 或者 Loki 都能直接查询。
还有一个小技巧:线上环境不要擅自关掉“记忆可解释”功能。我专门做了一个/memory/debug接口,支持传入用户 ID,直接返回该用户当前所有记忆条目及 score 排序。这样在用户投诉“你记错了”的时候,我能一眼看出是哪条记忆污染了上下文,而不是去模型输出里猜。
5. 踩坑清单:常见问题与排查思路
5.1 记忆没写进去 / 召回结果为空
这类问题太常见了。排查顺序从下往上走:
- 先确认抽取阶段的结果,是不是大模型抽取的字段本身就不对。
- 再看写入时 embedding 是否成功,如果 embedding 模型接口超时,记忆条目的向量字段会是空的,后面自然召回不到。
- 最后看召回阈值,我一开始 TopK 召回结果总是为空,后来发现是 score 阈值设太高。调低阈值或者去掉硬过滤,只靠排序选 TopK,效果立刻提升。
5.2 并发下记忆互相覆盖
这个问题在生产环境非常恶心。表象是:用户明明在端侧同时打开了两个会话,两个会话各有各的上下文,但后更新的记忆把先前的覆盖了。
排查时先看更新链路有没有版本控制。如果更新记忆时只用UPDATE memory SET content = ... WHERE id = ...,没有任何版本判断,那并发下一定丢数据。我改成先查 version,再执行带WHERE version = old_version的更新语句,返回受影响行数为 0 就说明有冲突,此时重新拉取最新条目做合并。这样虽然多了一次查询,但数据一致性明显提高。
5.3 记忆污染:老记忆把新对话带偏
记忆污染比“没有记忆”更可怕,因为 Agent 会一本正经地根据错误旧记忆给出错误回答。举例:用户三个月前说“我打算买 MacBook”,现在说“我已经买了联想”,如果你没有及时更新旧记忆,Agent 可能会反复推荐 MacBook 配件。
解决思路是:记忆更新不是增量,而是纠偏。当新信息和旧记忆冲突时,应该降低旧记忆的 score 并写入新记忆,而不是把新记忆存储成一条无关联的独立条目。我还会定期用大模型扫描每个用户的记忆库,主动找出冲突条目做合并或者标记过期。
5.4 调参建议:半衰期、阈值、TopK
这一块没有标准答案,但我可以给你一组我试过比较稳的初始值:
| 参数 | 初始值 | 调整方向 |
|---|---|---|
| half_life | 临时记忆 24h,长期偏好 30d | 越重要知识,半衰期越长 |
| decay 系数 | 0.5 | 想让遗忘更慢,调到 0.7~0.9 |
| importance 初始值 | 0.6 | 用户明确要求记住的设为 0.9 |
| frequency 加成 | 每次 +1 | 同一个事实被提 3 次可以直接加权重 |
| 召回 TopK | 10 | 上下文窗口大可放宽,但不建议超过 20 |
| 清理阈值 | score < 0.05 | 观察之后如无召回再删除 |
调参的时候不要看单个指标,要结合真实对话效果。我习惯每调一版参数后,用同一批用户对话跑回归测试,对比“记忆召回正确率”和“用户任务完成率”。只盯着“召回准确率”容易做出很激进、遗忘了重要记忆的系统;只盯着“任务完成率”又容易让老记忆长期占据上下文,导致模型变笨。两边都要看。
还有一些小经验:Embedding 模型别频繁切换,你会发现同一句话在不同模型里的向量相似度差异很大,这会导致召回结果上下波动;记忆条目别存太长,超过两句话的重要内容先让大模型做摘要,再入库,否则后续 Prompt 挤爆;定期人工抽检记忆库,这是最笨但最有效的办法,很多系统性问题靠日志是发现不了的。
我个人在实际操作里最大的体会是:记忆不是附加功能,而是 Agent 架构的核心。刚开始把它当“数据库字段”做,处处别扭;等把 score、衰减、双网络这套机制变成基础组件之后,后面加新能力都顺畅得多。你如果也在做 Agent,不妨从今天就试着给项目加上一条可以衰减、可以检索的记忆管道,体验完全不一样。