Hindsight 如何教会我的 AI 代理记住真正有效的东西
我第一次测试它时,代理对一个已经很不耐烦的客户说"请查收邮件"——这已经是第三次了。那一刻我才意识到,问题从来不在提示词。
这是一篇构建日志,讲的是如何让一个"挽回收入"的 AI 代理拥有真正的记忆,而不是一个它只能靠猜的数据库。
"发送一封邮件提醒。"这就是我第一次测试时代理给出的答案——而那位客户已经连续无视了三封邮件提醒,只回应过 WhatsApp。我盯着这个输出看了整整一分钟,非常恼火,因为严格来说它并没有错,只是毫无用处。代理完全不知道自己在重复犯过的错误,因为它根本不知道曾经出过错。从那一刻起,整个项目就不再是关于提示词,而是关于记忆。
我当时正在为 PayEcho 构建记忆层。PayEcho 是一个为逾期发票给出处理建议的代理——用哪个渠道提醒、什么时候跟进,以及对于重复案例,客户的付款历史是否应该影响新的授信决策。它自己不批准或拒绝任何事,只负责带着推理过程给出建议,由人来拍板。团队其他人构建了催收引擎、仪表盘和动作触发器。我的职责是确保一条建议到达屏幕时,真正基于那位特定客户之前发生的事,而不是把模板化的猜测包装成 AI。
普通表格不是答案
我最初的想法是显而易见的那个,结果证明是错的。我把每个事件记录到一张普通表格里——客户 ID、渠道、响应时间、结果——然后在给出建议之前查询它,按客户过滤,把行数据倒进提示词。它几乎立刻就崩了,因为像{channel: "whatsapp", outcome: "paid", days: 3}这样的一行数据里没有任何故事。把几行这样的数据交给模型,它每次都不得不从头重建实际发生的事情,而且做得非常不稳定——有时它能抓到模式,有时它就直接默认输出"发送邮件",这正是整个故事的起点。
就在这时,我才真正认真研究了 Hindsight,而不是把它当作一个换了包装的数据库。它是一个专门为代理构建的记忆层,围绕两个操作组织——retain(保留)和 recall(召回)——而不是一张要你自己去查询的表格。这两个操作之间的区别,后来被证明就是整个故事的核心。
Retain:写一条笔记,而不是一行记录
Retain 是转变发生的地方。我不再写结构化的数据行,而是开始用一个人真正记笔记的方式去写记忆:
def log_recovery_outcome(customer_id, invoice_id, channel, days_to_respond, outcome): memory.retain( agent_id="payecho-recovery", user_id=customer_id, content=( f"Sent a payment reminder for invoice {invoice_id} via {channel}. " f"Customer responded after {days_to_respond} days. Outcome: {outcome}." ), metadata={ "channel": channel, "invoice_id": invoice_id, "outcome": outcome, }, )content字段是改变一切的部分——它读起来像催收分析师真的会随手记下的东西,而不是一行日志。Hindsight 把它绑定到user_id(客户)和agent_id(哪个代理写的),所以记忆不会在不同客户之间、也不会和系统里运行的其他代理之间串味。metadata 仍然对过滤和报表有用,但真正被召回并用于推理的是叙事性内容,而把那段措辞写对,比我一开始预想的更重要。
def get_recommendation(customer_id, invoice_amount): memories = memory.recall( agent_id="payecho-recovery", user_id=customer_id, query="past recovery attempts, channel responses, and payment outcomes", ) prompt = build_recommendation_prompt( customer_id=customer_id, invoice_amount=invoice_amount, past_experiences=memories, ) return llm.generate(prompt)这样一来,模型看到的是历史中一段简短、相关的切片,而不是一堵记录墙——而这正是输出从泛泛变成具体背后的真正原因。最有力的证明不是一个指标,而是看到完全相同的代码,因为存在的记忆不同而给出两种不同的答案。
一个没有历史的新客户仍然会得到"请为逾期发票发送邮件提醒"。同一个客户稍后再来——一旦 Hindsight 保留了那封被无视的邮件、一次 WhatsApp 回应,以及跟进三天后的一笔付款——得到的回答就完全是另一回事了。
两次调用之间,推荐逻辑没有任何改变——唯一的区别是代理能召回什么。这就是"代理记忆"作为一种设计模式的全部论点:一个基于留存经验行动的代理,与一个从无状态提示词推理的代理,做的是结构性不同的事情,而不只是同一件事的措辞更好版本。
预期输出——召回浮出客户的历史,而推荐直接建立在它之上,连同推理和语气一起。
两条历史,各自独立保存
真正让我措手不及的是一个我没有专门编码过的案例。一位客户在两张不同的发票上有两段完全不同的历史——一张在电话之后很快就付了款,另一张尽管发了 WhatsApp 提醒还是拖了好几周。我原本以为代理会把它们平均成一条泛泛的"这个客户是中等风险"建议。它没有。召回把两段经历都拉了回来,推荐也真的把它们分开处理了:它建议新发票用打电话的方式,同时把"WhatsApp 拖沓"的模式标记为延展授信前需要拉人进来会商的理由。
我没有写任何逻辑去分开这两条线索——它自然而然地产生于有两条不同的记忆可以推理,而不是一个混合后的分数。同样的思路可以更广泛地带入授信决策:当一个有逾期付款模式的客户申请新授信时,PayEcho 会把这段历史呈现给批准人,而不是让这个请求被当作第一次互动来评估。代理不拍板——它只是确保拍板的那个人不是在白纸上做决定。
它是如何拼合在一起的
Retain 和 recall 不是孤立的——它们和 PayEcho 做的其他所有事情处在同一个循环里:一次交互发生、被保留,下一次决策召回它,一条建议发出去,而结果又喂养下一次召回。
如果回到第一天,我会告诉自己什么
回顾这一切是如何成形的,有几件事特别突出。
- 更少但更完整的记忆,好过更多但更单薄的记忆。我一开始保留得太多、太早——第一版给每个微事件(已发送、已打开、已查看)都单独记了一条记忆,结果召回回来乱成一团,建议又变得含糊,因为模型是在拼凑十个碎片,而不是读一条清晰的笔记。把相关事件合并成"每次完成的催收尝试一条记忆",就解决了这个问题。
- 围绕决策去查询,而不是查关键词。早期我搜索的是字面术语,比如精确的渠道名,于是措辞稍有不同就被漏掉了。把查询写成围绕决策本身——"过去的催收尝试和结果"——检索效果明显变好。
- 空召回需要诚实的答案。不加处理时,代理在空召回时的行为是未定义的,有一次它甚至编了一段听起来很合理的"历史"(其实根本不存在)。现在空召回意味着代理会明确说出来,并回退到通用的首次接触策略。
- Metadata 是给机器的,content 是给模型的。全程最站得住的一条规则:把 metadata 当作过滤和报表用的数据,把保留的 content 当作叙事——真正被推理的字段,必须读起来像人写的东西,而不是人查的东西。
最后一件值得说的是:一条建议只有在读它的人能看到它的依据时才能赢得信任——把召回的历史放在建议旁边,而不是藏在输出后面,这一点和把召回本身做对同样重要。如果你在构建任何这样的东西——同一个实体(客户、用户、账户)应该根据它之前实际发生的事得到不同对待——那么 Hindsight 的 retain/recall 模型是一个比它看起来更小巧、更精准的工具。它无关乎存储更多。它关乎决定什么值得记住、把它写成模型能推理的形式,并且只把与眼前决策相关的部分拉回来。
如果你也在研究 AI 代理和编程,欢迎参考本站的 IT 教程文章,一起把技术做扎实。