我阻止了那个靠“后见之明”运作的智能体编造我的过去

Dev.to AI 2026-11-07T15:27:52.928484

你问大语言模型(LLM)有没有「类似的过往投资」,它总能给你找出几个:名字、年份、核销金额,一应俱全。哪怕你压根没做过任何投资,它也照给不误。模型只是想帮上忙——在它看来,本该写着你的履历的地方空着,那就是个等着被填上的坑。

我在 Vectorize 的开源 Agent 记忆系统 Hindsight 之上做审计工具时,大部分时间都耗在这个坑上。给 Agent 加上记忆并不难,难的是让它只说自己真正记得的事。

我做了什么

这是一个 Streamlit 仪表盘,两种模式共用同一个 Hindsight 记忆库。

每次审计都先从我自己的过往投资或过往 bug 里做一次召回,然后把召回到的记录写进提示词,再以 JSON 模式调用 Groq(openai/gpt-oss-120b)。

我往记忆库里预置了 14 条记录:7 条过往投资,7 条已解决的 bug。这样,BP 是拿我真正经历过的事来评判,而不是拿模型训练数据里的印象来评判。

翻车一:模型会用编的填补空白

记忆库有内容时,审计按预期工作。可一旦库是空的,或者这份 BP 跟我见过的东西毫无相似之处,模型就会干它最擅长的事——编出一段听起来很像那么回事的历史。

我不想靠写更严格的提示词来解决这个问题。提示词终究只是「请求」。我要让代码来强制执行:

if not memories:
    audit["similar_idea_found"] = False
    audit["historical_loss"] = "None found"
    audit["similar_past_cases"] = []

只要召回结果是空的,模型写的任何关于「相似过往案例」的内容都会被覆盖。我的过去由 Hindsight 说了算,模型没资格当这个来源。

这个做法还有个意外的好处:仪表盘上显示「None found」,我就知道那是真的没找到,审计会自动退回到只看基本面来评这份 BP。

翻车二:我自己的审计变成了我的历史

这个问题我花了好久才发现。

每次审计都会把结论写回记忆库,这正是记忆存在的意义。但审计一份商业计划书,并不等于投了资。几天之后,一条写着「我评估过这份计划书,指出了它缺少营收模型」的记录,在后续召回时,看起来和「我投了这个项目,然后亏了钱」几乎没什么区别。在向量库里,这两条记录是近邻。除非你自己动手,否则没有任何机制标明哪条是观点、哪条是事实。所以我现在保存的每条审计记录,都在文本里直接标注:「这是一份被评估的计划书,并非真实投资」。系统提示词再要求模型,绝不能把这些记录当成盈亏来陈述。这两半缺一不可:标签让召回有东西可带,指令告诉模型该怎么处理它。

第三个坑:存进去 14 条,召回 99 条

我保留了 14 条记录,第一次召回却返回了 99 条结果。Hindsight 会把每条保留的记录拆成更小的事实,其中很多彼此重叠。直接塞进提示词,既超出了 Groq 的请求上限,内容还大量重复。你存进去的东西和取回来的东西,形状并不一样。

我不再把召回结果当作可以直接进提示词的材料,而是加了一小步清洗:

def prepare_memories(texts, limit=12, chars=700):
    seen, out = set(), []
    for t in texts:
        t = " ".join(str(t).split())
        if not t or t.lower() in seen:
            continue
        seen.add(t.lower())
        out.append(t[:chars])
        if len(out) >= limit:
            break
    return out

归一化空白字符、去掉重复、截断过长的内容、最多保留十二条。代码很无聊,但正是它决定了审计结果能塞进上下文窗口,还是直接报错。

还有一个集成上的小坑。Hindsight 客户端内部跑的是异步代码,而 Streamlit 会反复重跑你的脚本。缓存下来的客户端在第二次运行时抛出「Event loop is closed」;在脚本线程里新建客户端,又报「This event loop is already running」。解决办法是给每次 Hindsight 调用单独开一个短生命周期的线程,配上独立的事件循环。

实践中的变化

一份我原本可能一晃而过的计划书。

我贴进去一段点对点支付应用的路演材料:用户增长快、没有收入模式、单人创始人。用普通提示词,返回的是普通警告:“变现路径不清晰”“关键人风险”。把召回放进流程后,同样的警告挂到了我自己的记录上。审计翻出一家 2022 年的支付初创公司,增长很快,从未变现,最后关停,核销了五万卢比;它报出了结果、金额,还有我当时写下的教训。还有一次,路演材料什么都匹配不上,审计也如实说了:similar_idea_found 为 false,历史部分写着“未找到”,评分只基于基本面。这是我最在意的行为,因为正是它让那些正面匹配变得可信。

代码。一个用字符串拼接拼出来的登录查询,以前只会得到“请使用参数化查询”。现在它会匹配到我早先那条 SQL 注入记录,返回我已经信任的修复方案,还附带这类缺陷对应的历史损失。

在你动手做这个之前,我想告诉你

别让模型成为“过去”的来源。任何标着历史的东西都应该来自召回,而且当召回返回空时,代码要强制执行这一点。

给你写回去的东西打标签。审计、观察和事实在向量库里看起来一模一样,除非文本里写明了区别。

保留不等于检索。在召回结果进入提示词之前,先规划好如何去重和限制数量。

给记忆客户端独立的执行上下文。如果你的框架会重跑代码或接管事件循环,把异步客户端调用隔离出去。

别绑死在一个模型上。我起步时用的模型中途被 Groq 下架了。一份备用列表加上 JSON 模式检测,把那次事故变成了一行日志,而不是一次宕机。

限制

空召回保护只覆盖了“空”这一种情况。弱但非空的匹配仍可能被模型过度解读,这一点我用提示词指令来处理,而不是硬性检查。审计的质量也取决于我写的记录质量:我 2022 年的教训有多大用,取决于我当时写得多诚实。

我还是会做同样的选择。一个会说“我没有这方面的先例”的工具,比一个永远有答案的工具更有用。

如果让我先照搬一部分,那不会是仪表盘,而是那条规则:我的历史由回忆(recall)来界定,而不是由模型说了算。

基于 Hindsight 构建:https://github.com/vectorize-io/hindsight

仪表盘用 Streamlit 构建:https://github.com/sravanthbikkasani-ops/Streamlit.git

如果你问语言模型"类似的过往投资",它总会给你列出一些来:名字、年份、核销金额,一应俱全。哪怕你从来没做过任何投资,它也不在乎。模型的出发点是想帮忙——你本该有历史记录的地方空着,它看着就像一个该填上的坑。

我在给 Hindsight 构建审计工具时,大部分时间都花在了这个失败点上。Hindsight 是 Vectorize 出品的开源智能体记忆系统。给智能体加上记忆很容易,难的是确保它只声明它真正掌握的东西。

查看原文