跳到主要内容

模型没有记性,应用要有 — LangGraph 与记忆

这一章讲三件事: 「模型没有记忆」到底是什么意思、为什么一切聊天应用都要为此买单; LangGraph 用什么构造把记忆变成一行配置;以及历史越攒越长之后怎么修剪。 读完你会拿到全书的枢纽概念:状态(graph state)——后面 agent、架构阶梯、 人机协作全部站在这块地基上。

1. 先看现象:它转头就忘

同一次聊天里,你告诉它名字,下一句问「我叫什么」,它能答上来。像是记性很好?

其实每次都是同一场戏:模型收到提示词,吐出续写,然后一切清零。 原书开篇就把话说死:模型是无状态(stateless:每次生成都对上一轮零记忆)的1

所以「记忆」的真身是一个外包系统:每次调用前,由你的代码把过往对话拼回提示词。 这个动作每一轮都在发生,历史上过的每一句话都重新计费一遍—— 这不是实现的浪费,而是此类模型的物理设定。

2. 顶层全景

用户新输入 ─┐

┌───────────────┐ 读取 ┌──────────────┐
│ 中央状态(消息表)│◀──────▶│ checkpointer │ ← 自动存档,每步一次
└───────────────┘ 写入 └──────────────┘ thread_id 区分是谁的账本
│ 合并后的完整历史

[chatbot 节点: 调一次模型]
│ 新回复作为「状态更新」写回

结束等待下次唤醒

图说:整张图只有一种运转方式——读状态→干活→交回更新。记忆=「上次的存档还在」。

3. 核心原理

3.1 最土的办法,与它的天花板

不借助任何框架也能实现:把历届消息放在一个列表里, 模板里留一个占位槽(placeholder)整表填入,每轮往后追加2。书中实测有效: 先教翻译一句,再问「What did you just say?」,模型准确复述了 J'adore programmer.3

但拿到生产的秤上就露出四个缺口4:

缺口要命在哪
原子性失败时可能只记了问题没记回答,账本从此错位
持久化内存一断电全丢,得接数据库
取舍存哪些、用哪些,没有策略口径
可干预在两次调用之间,你想看一眼、改一笔都无从下手

这四条恰好是 LangGraph 的进场理由。

3.2 LangGraph 的三个字:多角色、多步、有状态

LangGraph 是 LangChain 家自己放出的库,定位一句话:串联调度「多角色、多步、有状态」的应用。 这三个词各管一层5:

  • 多角色(multi-actor):一个 LLM 提示擅长生成和规划,一个搜索工具擅长找实时事实, 强强联手需要协调层——定义谁(节点 node)、怎么接力(边 edge);
  • 多步(multi-step):接力过程按离散时间步展开,每一次交接触发下一步调度, 直到没人再交接为止;
  • 有状态(stateful):跨步协作必须有个共同账本,否则第二次调模型还会得到第一次的答案。 所有角色共同读写这一份中央状态,才能支撑快照、暂停恢复、人机审批(第 9 章兑现)。

构成一张图的要素于是只有三样:State(数据的形状与合并规则)、 Nodes(收状态、返回更新的普通函数)、Edges(固定走或条件走的连线)6

3.3 十五行代码的最小图

照书抄一遍骨架(Python 口径,JS 同构):

class State(TypedDict):
messages: Annotated[list, add_messages] # ← 关键在这一行的注解

def chatbot(state: State):
answer = model.invoke(state["messages"])
return {"messages": [answer]} # ← 返回的是「增量更新」

builder = StateGraph(State)
builder.add_node("chatbot", chatbot)
builder.add_edge(START, "chatbot")
builder.add_edge("chatbot", END)
graph = builder.compile()

Annotated[list, add_messages] 里的 add_messages 叫 reducer: 规定「该键的新值怎么并入旧值」。挂了它的字段做追加;不注解的字段默认整值覆盖—— 后者天生适合放「只关心最新值」的东西7。 而节点函数返回字典而不是改参数,是「声明我改了什么」而非「直接动手」, 这正是快照成为可能的原因8

graph.stream() 运行时你会看到这样一段一段的输出:

{ "chatbot": { "messages": [AIMessage("How can I help you?")] } }

顶层键是节点名,值是该节点刚产生的状态更新;每一个节点跑完,当前累计状态都会流出来一段9。 你之后看任何架构图,这行输出的键名就是「此刻跑到图中哪个框里了」的路标。

走查①:两轮调用之间的记忆从哪来

给编译期挂上一个 checkpointer(存档适配器——即插进不同数据库的统一转接头)后,同样的两步对话变了样10: 官方自带三种实现——内存版 MemorySaver(本书示例用)、SQLite 版、Postgres 版, 社区另有 Redis、MySQL 等;机制则共用抽象基类(补充(不在书里,依据我们的 frontier 书架): 各类 saver 都继承自同一 BaseCheckpointSaver 接口,自定义存储即子类化。 依据: shelf=ai-frontier-reference/langgraph@src:libs/checkpoint/langgraph/checkpoint/base/init.py:176 事实=源码中存在 class BaseCheckpointSaver(Generic[V]),适配器族由此派生)。

第一轮,带 thread 标识调用:

thread1 = {"configurable": {"thread_id": "1"}}
graph.invoke({"messages": [HumanMessage("hi, my name is Jack!")]}, thread1)
# → AIMessage("How can I help you, Jack?")

此时状态里存了两条消息。第二轮,同一个 thread 再问「what is my name?」, chatbot 节点这次收到的是三条消息:两条存档加一条新问题——于是答出「Your name is Jack」11

第 1 轮输入 [hi, my name is Jack!] → 回复 2 条入账
第 2 轮输入 [what is my name?] + 存档 2 条 → 模型看到 3 条 → 答对

图说:「记忆」的全部真相就是第 2 行的拼接动作,发生在进入模型之前。

thread_id 就是账本的户头号——首次使用自动开户,惯例用 UUID; 多个用户各持户头,永远不会串线。这是应用能服务多用户的里程碑一刻12

3.4 在两次调用之间动手术

因为有了显式状态,你获得了框架外的自由:get_state(thread) 随时查阅某户头的现状, update_state(thread, …) 直接注入一条消息,下次运行就会带上13

3.5 历史的减法:trim / filter / merge

用户聊上一下午,历史就要撑爆窗口。更糟的是书上补刀的一句: 过长的无关信息不只是贵,还会带偏模型、诱发幻觉14。 三把剪刀对应三种剪法:

trim_messages(按时长剪)。保最近 N 个 token,一组旋钮各有讲究15: strategy="last" 从尾部保留(通常正是你想要的;选 "first" 则反之); token 计数器直接传某个模型,用它自带的切分标准数; include_system=True 保证系统指令永不落选; allow_partial=False 表示超限就整条剔除而不剁半句; start_on="human" 是最精巧的一个——保证不会出现「AI 回复留着、配对的提问却被删了」的孤儿对。 书的实测:十条消息在 max_tokens=65 下剩七条,最早的两轮对话被裁掉16

filter_messages(按属性筛)。按类型(AI/human/system)、按名字、按 id 过滤17, 可以像零件一样串进管道:filter_ | model18

merge_message_runs(按粘连并)。有的模型(如 Anthropic 家的)不接受连续两条同角色消息。 合并工具把相邻同类消息并作一条——若内容本身是分段的(这种分段件就叫内容块(content block)),合并后保留列表结构;纯文本用换行衔接;同样可入链19

三者可以叠加使用,顺序视需求而定。

4. 作者的判断与证据

  • 「无状态」论断与四条生产挑战是本章的立论根基,均为工程共识式陈述而非实验数据14
  • Jack 两轮问答与 trim 结果都是书中实拍输出(含具体 token 数),属于作者给出的运行证据1116
  • 一处值得注意的书内瑕疵:章末小结声称讲了「trim, filter, and summarize」三种历史处理, 但正文从未介绍摘要式压缩20——这个许诺落空本身是条有用信息: 它说明长对话压缩在写作当时仍是被默认略过的话题(我们的判断,见下)。

判断(我们的,不是书里的): 摘要式压缩(summarize)恰是记忆系统从「能多用几轮」 走向「长期共存」的分水岭:修剪只留最近的,摘要才保久远的。 书小结里冒出的那个没人兑现的词,恰好暴露了这个空位。 如果错,会错在: 如果长上下文模型让「全量重发」变得足够便宜,摘要层就仍是过度设计; 但成本敏感或多轮跨月的场景里,这个空位几乎必然要补。

5. 边界与局限

  • 本章只覆盖「记住整段对话」这种短期记忆。跨天、跨月的长期记忆完全缺席—— MemorySaver 连进程重启都扛不住(它的「持久」只在内存里); 生产环境按书里的口径应换 SQLite/Postgres 版10
  • 「多角色协作」在本章只是定义级铺垫,真正的多角色协作要到第 8 章(反思与多 agent)落地。
  • reducer 只演示了追加与覆盖两种;数值聚合、交叉合并等自定义场景未涉猎, 书架补充(不在书里,依据我们的 frontier 书架):LangGraph 底层按 channel+reducer 建模, 支持任意合并的写法。依据: shelf=ai-frontier-reference/langgraph#02-channels-reducers.md 事实=StateGraph 编译产物即 channel 拓扑,reducer 即 channel 的写入合并规则。
  • 图的可视化(draw_mermaid_png)依赖外部渲染服务,离线不可用,书未提及。

6. 可带走的

  1. 模型无状态是一切对话应用的物理前提,所谓记忆=每轮重发历史,顺手记着这也是成本大头;
  2. 自制列表版记忆的四道坎(原子写/持久化/取舍权/可干预)恰好是评估任何记忆方案的清单;
  3. LangGraph 图三要素:State 定义形状、Node 干活、Edge 定路线,节点返回增量而非篡改现场;
  4. reducer 是状态键的「合并协议」:add_messages 追加,无注解覆盖;
  5. checkpointer 一行配置获得自动存档,thread_id 是区分多用户对话的唯一钥匙(用 UUID);
  6. stream 输出的键名告诉你「程序跑到哪个节点了」,调试架构图时的活地图;
  7. 历史必删:strategy last + 保 system + 整条删 + 成对保 Q&A,这四个旋钮的组合是安全剪法;
  8. merge 型工具照顾「不接受连续同类消息」的模型,Anthropic 家常见;
  9. 「summarize 被小结提及却无人兑现」——长期记忆压缩是个真实空白,选型时要自己补。

7. 原文地图

主题原书章原文位置
无状态定义与记忆补救Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:7(搜「stateless」)
记忆系统两个设计决定Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:17(搜「How state is stored」)
列表版记忆与 placeholderChapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:23(搜「list of messages」) · text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:43(搜「placeholder」)
J'adore programmer 复述实测Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:85(搜「J'adore」)
生产四挑战Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:91(搜「atomically」)
多角色类比与协调层Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:107(搜「team of specialists」) · text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:111(搜「Define the actors involved」)
多步离散时间模型Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:119(搜「discrete steps in time」)
有状态与中央状态红利Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:125(搜「single central state」) · text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:131(搜「human-in-the-loop controls」)
图三要素定义Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:111(搜「Nodes」)
State schema 与 add_messagesChapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:167(搜「Annotated」)
reducer 规则与自定义写法Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:197(搜「reducers」)
compile 与 START/ENDChapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:259(搜「add_edge」)
stream 输出形状解读Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:313(搜「same shape as the State」)
checkpointer 与三种官方实现Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:317(搜「storage adapter」) · text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:331(搜「MemorySaver」)
Jack 两轮走查Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:347(搜「my name is Jack」) · text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:353(搜「what is my name」) · text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:377(搜「essence of memory」)
thread 户头机制Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:373(搜「threads in LangGraph」)
get_state/update_stateChapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:383(搜「get_state」) · text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:401(搜「one more message」)
过长信息致幻觉警告Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:409(搜「distract the model」)
trim 六旋钮逐一解释Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:494(搜「strategy controls」) · text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:502(搜「start_on」)
trim 实测十变七Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:484(搜「good assistant」)
filter 按类型名字 idChapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:506(搜「filter_messages helper」) · text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:550(搜「example input」)
merge 兼容 Anthropic 场景Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:621(搜「consecutive messages of the same type」) · text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:686(搜「content blocks」)
小结(含未被兑现的 summarize)Chapter 4text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:704(搜「summarize」)

Footnotes

  1. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 7 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:7,搜「no memory of the prior prompt」)。「This historical information can then be included in the final prompt」承接在同段后半。 2

  2. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 21–29 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:25,搜「Updated by appending」)。

  3. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 83–87 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:85,搜「J'adore」)。

  4. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 89–99 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:89,搜「memory at scale」)。四条依次见第 91–97 段。 2

  5. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 103–133 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:103,搜「multiactor」)。

  6. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 133–145 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:111,搜「Edges」)。

  7. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 211 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:211,搜「overwritten by each update」)。

  8. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 215–217 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:215,搜「return a value that updates that state」)。

  9. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 309–313 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:313,搜「streams the full value of the state」)。

  10. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 315–339 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:317,搜「several adapters maintained by LangChain」)。「不为空白起步」机制描述在第 339 段。 2

  11. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 341–377 段。第一轮调用在第 345–350 段,第二轮在第 352–356 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:356,搜「Your name is Jack」),三轮消息解读在第 375–377 段(搜「three messages」)。 2

  12. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 373 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:373,搜「never mixed up」)。

  13. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 379–401 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:395,搜「update_state」)。

  14. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 409 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:409,搜「hallucination」)。

  15. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 492–502 段,六条注意事项(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:494,搜「prioritize the most recent messages」);start_on 成对保留逻辑在第 502 段(搜「corresponding HumanMessage」)。

  16. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 482–490 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:484,搜「good assistant」)。原图共十一条消息(含一条 system)。 2

  17. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 504–597 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:506,搜「by type, ID, or name」)。

  18. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 599–607 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:599,搜「imperatively or declaratively」)。

  19. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 619–700 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:621,搜「Anthropic chat models」);合并形态细则在第 686 段。

  20. 出处:「Chapter 4. Using LangGraph to Add Memory to Your Chatbot」第 704 段(text/07-ch04-chapter-4-using-langgraph-to-add-memory-to-your-.txt:704,搜「summarize」)。