上一篇讲上下文,那是一次会话里的短期记忆,会话一结束就没了。可老用户回来排第三趟行程,Agent 不该还在问「你喜欢住民宿还是酒店」,这事他半年前就告诉过你。跨会话记住一个用户是谁,靠的是另一套东西,记忆系统。在 Claude Code 里它叫 CLAUDE.md,你的项目约定、代码风格写一次,之后每次它都记得。野行 Y 里对应的东西,是用户画像。
但记忆这东西,做起来特别容易在两个地方栽跟头,而且栽了比不做还糟。这篇就讲这两个坑,以及怎么绕开。
先说清楚记忆记什么。野行 Y 的用户画像记两类。一类是显式偏好,用户在账户里设过的:住宿档次、出行节奏、预算、体力、忌讳、兴趣,这些是他主动告诉系统的,最可靠。另一类是行为记忆,他做过、参与过的行程,这个不用问,从数据库里捞最近六条标题。于是一个老用户开新对话,Agent 一上来的 system 里就带着「固定偏好:住民宿、深度慢玩、预算适中;做过:川西小环线、洱海环湖」,它一张嘴就「记得」这个人,能说「你之前走过川西,这次给你换个方向」。
这里有个容易混的地方要厘清:记忆和上下文是两回事。上下文在模型的窗口里,活一次会话;记忆在数据库里,跨会话、长期存着。关键是,数据库里躺着的记忆,模型自己是看不见的,它不会凭空「感应」到。得在合适的时机,把它提炼成一小段,注入到这次会话的上下文窗口里,模型才读得到。记忆负责长期存,上下文负责当次用,两者是接力关系。
现在说第一个坑,也是最凶的一个:记岔了。
记忆最危险的地方,是记错了还理直气壮。模型对注入进来的东西是全盘相信的,你往它记忆里塞一句错的,它会拿这句错的去误导用户整趟行程,语气还特别笃定。所以这个项目有一条铁律,从第一天就立着:记忆里的每一条,都必须真实、可追溯,绝不能是模型自己编出来的。
看前面那段「做过的行程」,它是从行程表里真实查出来的标题,模型没插手,只是把真实数据摆出来。偏好也一样,来自用户在账户里真实设置,模型不参与猜测。这跟整个 Agent 的第一原则一致,系统提示词开头就写着「禁止凭空编城市、距离、价格」,这条对工具成立,对记忆同样成立。
为什么这么较真?因为一个会编记忆的 Agent,比一个没记忆的 Agent 更糟。没记忆,最多每次重新问一遍,烦一点;编记忆,是拿着假的「我记得你说过」去误导决策,用户还不容易察觉。所以记忆系统的可信度,要排在它的丰富度前面。
代码里有个小而关键的细节:新用户,没有任何数据的时候,画像函数返回一个空字符串,什么都不注入。宁可不注入,也不硬凑一段「这个用户可能……」的猜测。没有记忆,好过错的记忆。
第二个坑更微妙:就算记忆是真的,把它当成命令来执行,也会出问题。
一个用户去过川西,不代表他这次也想去高原;他偏好住民宿,也不代表这次带着老人还非民宿不可。记忆是背景,是帮你更懂他的一层底色,别把它当成这次任务的硬指标。所以注入用户画像的时候,我在提示语里专门给它松了绑,明写一句「这些是背景,帮你更懂他,别生搬,按当前这次的需求灵活参考」。
这一句松绑很要紧。它告诉模型:当前这次对话里用户新说的话,永远优先于历史记忆。记忆解决的是「默认值」问题,让你不用每次从零问起,但它不该盖过用户此刻的明确表达。把这个优先级搞反,Agent 会显得又固执又不听话,明明记性好,体验反而更差。
说到底,做专用 Agent 的记忆系统,我把它收成三条:只记这个领域里真正跨会话有价值的东西,别什么都往里塞;每一条都必须真实可追溯,从数据库来、从用户设置来,绝不让模型编,宁可空着;注入的时候讲清楚它是背景参考,当次意图永远优先。守住这三条,记忆会让 Agent 越用越懂你;破了任何一条,它会越记越坑。
到这儿,主循环里的独角戏就讲完了:一个模型、一套工具、一段上下文、一份记忆。但有些活,交给同一个模型在主循环里顺手做并不好。要么慢,该并行的串成了一条;要么不客观,让排方案的人自己检查自己的方案。下一篇讲子 Agent,什么时候该把活派出去、并行地做,或者换一个新的脑子来核对。