晨光里的AI

固定版本 · 查看当前页面

深度解析Memory本质:Agent和Chatbot的差异

晨旭 · 内容日期 2025-11-28。此页保留当时的文本、性质和段落,不用于判断现在是否仍然有效。

版本 f6a33b3742d5f3d44c7b6672e45923cc5a11b25912137f902363b816a11131ed · 读取 JSON

p1

一直以来,我对 Agent 记忆这个概念都感觉像是雾里看花。

p2

最近在学习底层实现后我才发现:相比于看技术文档或二手拆解文章,直接看代码带来的理解冲击力是完全不同的。

p3

这篇文章,就是我从代码底层视角出发,对理解 Agent 记忆机制的一次复盘。(如果对多轮对话以及 Function Call 的基础实现已经很熟悉,可以直接跳到第二部分)

p4

本文会依次讲四件事:记忆的本质、Chatbot 的长短期记忆机制、Agent 记忆的核心差异、多 Agent 记忆设计思路。

p5

记忆的本质:拼接和重传

p6

多轮对话

p7

首先,为什么 ChatGPT 能接上话?

p8

因为在这个对话系统的后台,有一个中间人(应用程序代码),它把你之前说过的所有话,打包成一个长长的列表,每一次都完整地塞回给模型。

p9

这就像是你和一个失忆症患者聊天,每次你想问新问题,都必须把之前的聊天记录打印出来递给他,他看完这叠厚厚的纸,才能回答你的新问题。

p10

第一轮:用户问「北京天气怎么样?」 -> 系统发送 [User: 北京天气] -> 模型回答。 第二轮:用户问「那上海呢?」 -> 系统必须发送 [User: 北京天气, AI: ...(上一轮AI的回复), User: 那上海呢?]。 第 N 轮:系统发送 [User: ..., AI: ..., (无数轮)..., User: 新问题]。

p11

工具调用

p12

上面是 LLM 基于自身的知识对话的情况。当涉及到工具调用的时候,这时的工作流程是怎么样的呢?是模型和人一样去根据 URL 访问网页,获得结果然后回复吗?

p13

其实不是这样的。(在我深入学习代码之前,我就是这么觉得的——或者说,我都没有仔细去想过这中间的过程。)

p14

第一回合:LLM 的决策时刻

p15

(1)定义工具

p16

在代码里,需要定义一个 tools 列表(JSON Schema),告诉 LLM:「有一个叫 get_weather 的工具,需要 latitude 和 longitude 两个参数」。

p17

注意:这时候什么都没发生,只是定好了规则。

p18

(2)发起第一轮请求

p19

用户问:「What's the weather like in Paris today?」

p20

程序把这句话封装进 messages 列表发给 LLM。关键点:在 client.chat.completions.create 中,同时传入了 messages 和 tools。

p21

(3)LLM 的思考与停顿

p22

对应流程图中的「需要调用函数?」菱形判断框。

p23

LLM 收到请求后,发现自己不知道巴黎实时的天气(知识截止),但它发现手边有个 get_weather 的工具。这时 LLM 不会直接回答问题,而是返回一个特殊的指令。

p24

(4)第一轮输出(JSON Output)

p25

LLM 返回的内容不是自然语言,而是一段 JSON 数据,大概长这样:

p26

{ "name": "get_weather", "arguments": "{ 'latitude': 48.85, 'longitude': 2.35 }" }

p27

这一步非常关键:LLM 到此为止就收工了。它没有去查天气,它只是生成了一个条子,上面会写着要调用什么函数,以及调用这个函数所需的信息参数。

p28

中场休息:Python 程序的跑腿时刻

p29

这就到了程序执行环节。这里完全没有 LLM 的事,全是 Python 代码在干活:

p30

- 解析字符串:代码 json.loads(...) 把 LLM 给的字符串解析成参数。 - 执行函数:代码执行 get_weather(...)。这一步就是根据模型提供的信息去调外部 API。 - 拿到结果:程序拿到了 "14.2°C" 这个结果。

p31

第二回合:带上结果,重新排队

p32

这一步就是把历史记录全部重新塞给 LLM。为了让 LLM 能够根据查询结果回答用户,必须构造一个新的上下文。

p33

(1)追加 LLM 提供的条子

p34

messages.append(completion.choices[0].message)

p35

必须把第一回合 LLM 说的「我要调用工具……」这句话记入历史,否则 LLM 待会会因为不知道自己发起了调用而感到莫名其妙。

p36

(2)追加 Tool 的结果

p37

messages.append({ "role": "tool", "content": "14.2°C" })

p38

这里把 Python 跑出来的 14.2°C 塞进了历史记录。

p39

(3)发起第二轮请求

p40

再次调用 client.chat.completions.create。此时发给 LLM 的是三条信息:

p41

User: 「巴黎天气咋样?」 Assistant: 「(发出指令)调用 get_weather 参数是巴黎...」 Tool: 「14.2°C」

p42

LLM 看到这三句话,终于恍然大悟:「原来我查到了是 14.2 度。」

p43

(4)最终输出

p44

LLM 生成自然语言回复:「The current temperature in Paris is 14.2°C.」

p45

总结一下,Function Call 的本质就是:暂停 → 外部执行 → 拼接历史 → 重发。

p46

如果把这里的 get_weather 换成 save_memory(写入记忆)或 search_memory(读取记忆),原理是一模一样的:

p47

- Agent Memory 不是大模型脑子里的东西。 - 它是大模型在第一回合发出 search_memory 的指令(JSON)。 - Python 程序去数据库里捞出数据。 - Python 程序把捞出的记忆伪装成 tool 的消息,追加到对话历史里。 - 大模型在第二回合看到这条历史,才想起了这件事。

p48

在很多 Memory 设计里,search_memory 这一步就是一个小型的 RAG:用当前对话作为 query,在外部记忆库中做语义检索,再把结果拼回上下文。

p49

普通 Chatbot 的记忆机制

p50

所谓的拥有记忆,在代码层面,其实就是像上述那样一次次笨拙但有效的拼接和重传过程。记忆本质上就是一个特殊的检索源。要在工程上实现记忆需要解决四个问题:存什么、存哪儿、什么时候读、什么时候忘。

p51

下面拿一个 RAG 架构的企业问答助手为例。

p52

一、核心架构:双重检索机制

p53

在引入记忆模块后,RAG 的工作流就不再是单线条的了,而是变成了一个闭环系统。最核心的变化在于引入了双重检索。

p54

- 静态知识检索:去向量数据库里查企业文档、Wiki 等固定知识。 - 动态记忆检索:去记忆模块里查对话历史、用户偏好、中间推理结果等动态上下文。

p55

此时工作流变成了这样:

p56

用户提问 -> 双重检索(知识库 + 记忆库) -> 信息融合 -> 模型生成 -> 记忆更新(写入新一轮对话)

p57

这种架构让模型既有博学的一面(查资料),又有贴心的一面(查过往)。

p58

二、存储层:记忆放在哪里?

p59

这并不是一个简单的存数据库就能解决的,根据时效性,通常采用组合方案。

p60

(1)短期记忆:内存 / 键值存储(Redis)

p61

- 场景:就像人的工作记忆,只记最近几轮对话,随用随取。 - 技术:Redis 等内存数据库。 - 优点:速度极快,延迟低,适合高频读写。通常通过 Session ID 来存取。 - 缺点:容量有限,非持久化。一旦会话结束或超时,可能就清空了。而且只能按 Key 查,不能按语义查。

p62

(2)长期 / 语义记忆:向量数据库(Vector DB)

p63

- 场景:就像人的长期回忆,比如你问「我上次说的那个项目方案是什么?」,模型需要跨越几十轮对话去捞取信息。 - 技术:向量数据库,如 Pinecone、Weaviate。 - 优点:支持语义搜索。把记忆转化成向量,当用户说话时,系统计算语义相似度,去库里捞出语义相关的往事。 - 缺点:这种方式能实现长程回忆,但引入了额外的计算开销(Embedding + 搜索),且可能偶尔检索出语义相似但语境不相关的错误记忆。

p64

(3)用户画像:NoSQL 数据库

p65

- 场景:存用户画像、历史决策日志等结构化数据。 - 技术:MongoDB、DynamoDB。 - 作用:作为兜底的持久化存储,容量大且便宜,适合存那些不需要频繁语义检索的档案信息。比如构建用户画像来提炼用户的个性,让模型在任何时候都能调取背景偏好。

p66

三、管理层:如何避免脑容量爆炸?

p67

这是最需要关注的地方。如果把所有对话一股脑都塞给大模型,Token 费用会爆炸,模型也会因为上下文太长而变笨。所以需要记忆管理策略。

p68

(1)选择性遗忘

p69

人类的大脑会遗忘,AI 也要学。

p70

- 滑动窗口:最简单粗暴,只保留最近 N 轮,旧的直接扔掉。 - 艾宾浩斯遗忘曲线:模拟人类记忆规律,对很久没被提及(检索)的记忆降低权重,慢慢淡忘;对经常被提起的记忆进行加固。

p71

(2)层次化摘要

p72

别让模型读几十页的聊天记录,给它看摘要。

p73

- 操作:定期将详细的对话日志提炼成简短的要点。 - 效果:既保留了事情的梗概,又节省了 Token。新对话发生时,模型看的是「近期详细记录 + 远期摘要」。

p74

(3)用户画像持久化

p75

这是实现个性化的关键。

p76

- 操作:从对话中提取事实。比如用户说「我海鲜过敏」,系统将其转化为结构化数据 {Topic: Food, Preference: No Seafood} 存入专门的静态记忆块。 - 好处:无论过了多久,只要涉及吃饭场景,这个画像就会被调出来注入 Prompt,无需重复检索海量历史。

p77

单 Agent 的记忆机制:从说话到做事

p78

前面讲述的普通 Chatbot 记忆(向量库 + Redis),解决的核心问题是「如何像人一样聊天」。但 Agent 和 Chatbot 有两个关键差异,决定了它不能只靠对话历史:

p79

- 它需要多步推理。 - 它的一次任务可能跨越很多轮,涉及多次工具调用。

p80

所以,Agent 的记忆设计,必须在对话内容之外,额外增加两类核心记忆。

p81

1、行为记忆(工具调用日志)

p82

普通 Chatbot 记的是「你说了什么,我说了什么」,但 Agent 必须记住「我做过什么」。

p83

- 痛点:如果 Agent 不记得自己调用过 Google Search 且失败了,它可能会在一个死循环里不断重试。 - 解决:需要记录 (tool_name, input, output, success_flag)。 - 进阶:在任务结束后,将踩坑经验写入长期向量库。这样下次遇到类似任务,Agent 不仅记得用户说过什么,还能记得「上次怎么做才成功」。

p84

2、任务级临时记忆

p85

这是 Agent 的草稿纸。

p86

- 场景:比如写代码,Agent 需要拆解步骤:Step 1 建库,Step 2 写接口…… - 区别:这些中间思考过程对于任务执行至关重要,但对于用户来说是噪音。 - 策略:为每个 task_id 维护独立的短期记忆区。任务进行中完整保留,确保逻辑连贯;任务结束后只提炼关键结论存入长期记忆,把草稿纸直接扔掉,防止 Token 爆炸。

p87

基于上述差异,一个成熟的单 Agent 存储布局应该是以下的组合:

p88

- 短期记忆(Redis):存当前任务的「思考链、工具调用结果」。 - 长期记忆(Vector DB):存「用户事实、反思、最佳实践」。 - 持久化(NoSQL):存结构化的「用户配置、工具日志」。

p89

读写策略可以总结为:

p90

写入

p91

- 每轮对话:抽取新的事实,并实时更新用户画像。 - 任务结束:生成总结,丢弃过程草稿,沉淀经验。

p92

读取

p93

- 新任务启动:按「用户 ID + 任务类型」检索,加载背景偏好与历史同类任务摘要。 - 任务执行中:按「当前子任务语义」检索,动态查找历史错题集与最佳实践。

p94

多 Agent 记忆机制

p95

在搞懂了单 Agent 的拼接重传机制后,多 Agent 的记忆管理其实就很好理解了。

p96

多 Agent 的记忆,本质上是在前面那套「用户、任务、Agent」记忆体系上,再加一维:角色(agent_id)+ 作用域(scope)。

p97

可以拆成两个问题来想:

p98

- 每个 Agent 自己的记忆如何隔离? - 哪些东西要共享,怎么共享才不会乱?

p99

所以,多 Agent 记忆设计的核心,本质上就是在单 Agent 的基础上,加了一个权限管理的维度。

p100

1、记忆的作用域:给记忆打上标签

p101

在代码层面,要在多 Agent 场景下复用我们之前的记忆系统,其实只需要在数据库里多加几个字段(Tag)。可以把记忆分成这四层:

p102

- Global(全局知识):公司的图书馆。产品文档、API 手册,所有人随时都能查,基本不怎么变。 - User(用户画像):客户档案。「用户只用 Python」「用户是金融背景」,这些信息对于 Planner、Coder 都是通用的,谁接待用户谁就能看到。 - Task(任务白板):会议室的白板。这是多 Agent 协作的灵魂。当大家为了同一个 task_id 努力时,Planner 写下的计划、Researcher 查到的关键摘要,都要写在这个 scope 里。所有参与这个任务的 Agent 都能读到白板上的内容,这样 Coder 才知道 Planner 到底规划了什么。 - Agent(私有笔记):员工的私人笔记本。这是最容易被忽视的一点。Planner 可以在这里记下「最近这类的任务拆解容易出错」,Coder 可以记下「这个库的某个函数有问题」。关键点:Coder 的私人笔记,Planner 不需要看,也看不懂。隔离这些噪音,能极大节省 Token。

p103

2、怎么存?其实就是多加一列 ID

p104

不管是存 Redis 还是向量库,本质上就是在每一条记忆上打标签。

p105

以前单 Agent 可能长这样:

p106

{ "content": "...", "time": "..." }

p107

多 Agent 只需要扩展成:

p108

{ "content": "...", "user_id": "u_001", // 属于哪个用户 "task_id": "t_999", // 属于哪个任务(如果是跨任务记忆则为空) "agent_id": "coder_a", // 谁写的 "scope": "task" // 作用域:user / task / agent }

p109

3、一个协作的例子

p110

下面用一个简单形象的流程说明一下三个 Agent(Planner、Researcher、Coder)是怎么配合的。

p111

第一步:Planner 入场

p112

- 它向记忆模块发起查询:scope=user + scope=agent(planner)。 - 它看到了用户说「我要写 Python」,也看到了自己以前总结的「拆解报表任务的模版」。 - 它写下一份 Plan,标记为 scope = task。这意味着这份 Plan 被贴到了会议室白板上。

p113

第二步:Researcher 入场

p114

- 它不需要看 Planner 的私有经验,它只需要看白板(scope = task)。 - 它发现白板上写着:Step 1: 查一下某 API 文档。 - 于是它去执行,查完后,把结果摘要也贴回白板(scope = task)。

p115

第三步:Coder 入场

p116

- 轮到它干活时,它看一眼白板,上面已经有了 Plan 和 API 文档摘要。 - 它结合这些信息写出代码。如果报错了,它把这个坑记录在自己的 scope = agent 里,下次它再遇到类似问题就能避坑,但不会去打扰 Planner。

p117

总结一下:多 Agent 记忆机制并不是什么玄学的群体意识,它在工程上就是一个带权限控制的读写系统。

p118

- 隔离(依靠 agent_id)是为了减少噪音,让每个 Agent 专注自己的专业领域。 - 共享(依靠 task_id 和 user_id)是为了对齐目标,确保大家在一个频道上对话。

p119

最后

p120

理解了这些底层的拼接逻辑,我们在设计产品时才能更准确地预估 Token 成本、更合理地设计记忆策略。

p121

记忆不是越长越好,而是越准越好。

p122

希望我们也可以找准自己的定位,能够在曲折的路途中遇到任何问题都可以坚定自我地走下去。

该版本的判断

所谓「大模型有记忆」,在代码层面就是每一轮都把完整历史重新拼接、重新发送给模型。模型本身始终是失忆的,记忆只是一个挂在外面的特殊检索源。

fact · 2025-11-28 · 信心 high

Function Call 的本质是四步:模型暂停并输出一张调用条子,程序在外部执行,把结果伪装成 tool 消息追加进历史,再重发一次。把 get_weather 换成 search_memory,原理一模一样。

fact · 2025-11-28 · 信心 high

Agent 和 Chatbot 的关键差异在于,Chatbot 只需记「谁说了什么」,Agent 还必须记「我做过什么」——否则会在失败的工具调用上陷入死循环。

judgment · 2025-11-28

多 Agent 记忆不是什么群体意识,工程上就是给每条记忆打上 user / task / agent 作用域标签的读写权限系统。隔离为了降噪,共享为了对齐。

judgment · 2025-11-28

记忆不是越长越好,而是越准越好。

judgment · 2025-11-28 · 信心 high