首页 / 文章

你的编程智能体所需的四种记忆类型(但目前仅实现了一种)

代码图为智能体提供结构,但工程师们还需要理由、历史记录和操作流程。探讨四种记忆类型、置信度层级以及如何将它们结合使用。

发布者:REACTAPP.TOP阅读约 11 分钟2627 词

来源: Your Coding Agent Is Always the New Guy on Monday.

最近出现了一组工具,其目的是防止人工智能编码代理在代码仓库中随意搜索。这些工具使用 tree-sitter 解析代码,将符号和边关系存储在 SQLite 中并通过 MCP 提供服务,或者运行语言服务器为代理提供真正的 IDE 语义支持。据称其效果显著,且理念合理。

所有这些工具都在回答同一类问题:谁调用了这个函数?那是哪种类型?做出某项更改会破坏什么?

现在来看看一位经验丰富的工程师在处理陌生领域的代码更改时的情况。他找到相关调用函数大约需要三十秒,之后却要花一小时去解答这类工具根本无法处理的问题:为什么要这样设计?上次有人尝试这样做时发生了什么?这里的常规操作流程是什么?在运行测试之前需要重新生成代码吗?

四个问题,四种不同的记忆方式。如今几乎所有的智能体架构都会构建其中一种,并将其称为上下文。

区分它们的测试

在建立分类体系之前,首先要进行能够生成该体系的测试。对于希望智能体知晓的任何关系,都要先确认是否已有某种形式化的记录可供确定性工具读取。

如果有,就对其进行分析。这种记录可能是编译器、类型检查器、字节码读取器或分布式追踪数据。使用语言模型来提取解析器已经知晓的关系,不仅速度更慢、成本更高且精度更低,还会浪费你已经掌握的准确信息。

如果没有,那么这种关系仅存在于文字描述和人类判断中,此时模型提取便是获取该信息的唯一方法。

这并非代码与文档之间的区分,而是关乎是否存在某种语法结构。这种区分足够清晰,不同类型的知识需要不同的提取方式、不同的更新频率以及不同的置信度语义。如果将所有内容都通过同一个处理流程,至少会有其中一部分被错误处理。

1. 结构化记忆:变更的影响

这是智能体对代码本身的认知。这是每个人都会构建的记忆,常见的错误是从单一来源为它提供数据。

  • Tree-sitter 提供的是广度:它能列出节点与片段信息,支持40多种语言,具备错误容忍度以及针对每个文件的毫秒级处理速度。但它无法进行类型解析——只能说明第3行包含对名为 execute 的函数的调用,却无法指出具体是哪个 execute。名称匹配并非编译器级别的分析手段,而在多态、分层、依赖注入式的代码中,这种缺陷尤为明显。
  • LSP 通过 textDocument/definition、references 和 implementation 以及真正的类型检查器来确保正确性。它的启动速度较慢,需要完整的构建环境,且必须避免出现在查询路径上,因为未预热的语言服务器会严重增加延迟。建议将其作为后台增强工具来运行。
  • 字节码展示了仅在构建完成后才会出现的代码:Spring Data生成的查询、CGLIB代理、MapStruct映射器,以及注解处理器编写的任何代码。源代码中看不到这些内容,而target/classes目录中则包含了所有这些代码。
  • AOT编译输出或/actuator/beans能显示依赖注入的连接方式,换句话说,就是在不同配置下哪个具体类实现了哪个接口。仅通过阅读源代码是无法得知这一信息的,因为答案并不存在于源代码中。
  • 分布式追踪能够展示跨服务及异步操作之间的调用关系:从控制器到HTTP接口再到另一个服务,从生产者到主题消息再到消费者。每种语言的编译器都在模块边界处停止工作,而分布式系统也主要由这样的模块组成。如果你使用了OpenTelemetry,其实已经在收集服务间的调用关系图,只不过随后又将其丢弃了。
  • 最具决定性的设计原则是:每条边都被赋予一个层级:

    • resolved:类型检查器已确认,因此可以安全地采取行动。
    • observed:实际运行产生了该结果,可作为所涉及路径的真实依据。
    • inferred:仅通过名称匹配得出。很可能正确,但尚未经过验证。
    • ambiguous:存在多个候选选项。全部返回,不选择任何一个。
    • dynamic:在运行时计算得出且无法判定,因此记录为无法判定状态,不要丢弃。

    如果给智能体一个没有层级的图,它会将每条边都视为事实,然后基于猜测进行重构。如果告知某条边为inferred状态,它会在采取行动前进行验证。正是这一列的差异,决定了系统是会默默误导还是能优雅地应对问题。

    很少有实现会明确说明的一个后果是:在分层式的 Spring 或 Django 代码库中,影响分析只能得出下限,而无法得到完整的结果。说“17 个调用方,3 个未解决的接口调度,1 个生成的方法”是安全的;而只说“17 个调用方”则具有误导性。

    代码变更每小时都会发生,重新构建只需几秒钟。

    2. 知识记忆:代码背后的原因

    这种记忆存储着各种推理过程,它们存在于解析器无法读取的文档中:决策记录、事故事后分析、拉取请求讨论帖、工单、RFC、设计文档以及包含实际内容的提交信息。

    在配置文件中,重试次数可设置为 3。关于这一设定的解释分散在2023年的ADR文件、针对重复结算指令的事后分析报告,以及一篇描述交易对手服务等级协议的帖子中。这三份文档分别从不同角度说明了它们之间的关联——即因某起事件而制定的、用于限制系统集成的政策。

    GraphRAG是处理此问题的优秀工具,了解它的功能很有帮助。其工作流程包含六个步骤:

    1. 分块功能会以确定性的方式将文档拆分为文本单元。
    2. 提取是大型语言模型的任务:模型会读取每个文本块并输出其中的实体及关联信息。
    3. 描述汇总同样由大型语言模型完成,它会把以五十种不同方式描述的同一实体整合为一段统一的描述。
    4. 莱顿聚类无需模型。它通过模块度将图划分为嵌套的社区,这些社区内部的边数远多于向外连接的边数。
    5. 社区报告是针对每个社区的LLM总结,采用自下而上的方式编写,上层社区由下层社区的报告构建而成。
    6. 嵌入可生成向量,作为查询的切入点。

    有两个概念值得仔细研究。在此情境中,边并非指代类型的指针,而是包含模型生成的文字描述、权重以及来源引用,因此每条边都记录了模型对某句话的理解。同样,社区也不仅仅是任意的子图,因为随机选取的节点群可能完全没有边相连,从而形成无法总结内容的孤立点。莱顿大学选择那些诱导边较为密集的簇,这样才能生成连贯的报告。

    其优势在于全局搜索。系统不会只获取与查询最接近的片段,而是会分别向每个社区报告提出问题,再将这些部分答案整合起来。这样一来,整个语料库的属性都变得可检索:关于结算路径中反复出现的故障模式的问题,其答案并不存在于某一篇事后分析报告中。无论如何微调片段,普通的向量RAG都无法实现这一点。

    数据更新频率为每月一次,且重建成本很高:六个步骤中有三个需要调用大语言模型,因此索引成本会随着语料库规模、检索次数以及模型价格的增加而上升。不过由于语料库几乎不会发生变化,这种成本也是可以接受的。

    3. 情节记忆:之前尝试中发生的情况

    这个模块用于记录智能体在特定存储库中的操作历史。上次修改SettlementService时导致了三项分配测试失败。若在不重新生成映射器的情况下更改该接口,就会使构建过程出错。最近两篇跳过验证层的拉取请求均被拒绝。

    它还拥有一个两个语义索引都无法宣称的特性:其真实值是确定性的。事后分析中包含人类对模型的判断陈述。而“智能体修改了X,导致持续集成测试报错”则是由构建系统决定的判定结果。对于其所涉及的问题而言,这使得情景记忆成为该系统中最可靠的来源。

    有三个设计要点很容易出错:

    • 此处可跳过GraphRAG。各环节都遵循时间线并产生相应结果。按边密度分组并非正确结构,应当根据最新性、出现频率以及成败情况来设定权重。
    • 必须实现移除机制。语义索引可以没有该机制,但此系统却不可或缺。从两次架构重构中得到的经验不仅已经过时,还会引导智能体走向错误方向。
    • 错误会不断累积。假设智能体做出了错误推理并记录了结果,该错误就会变成被记住的事实,后续的所有会话都会继承这一错误。应将结果(如CI测试失败、PR被拒绝)以高置信度保存,而将智能体自身的结论以较低置信度保存,如此一来,结论会逐渐失效,但结果依然保留。

    用户流失是按会话计算的。

    4. 程序记忆:根本不是索引

    程序性记忆涉及做事的流程:构建命令、检查清单、迁移顺序以及风格规范。人们的第一反应往往是将其设为可检索内容,但这种冲动应当被抵制。

    任何始终相关的信息都不适合通过检索方式获取。这类信息应放在系统提示或AGENTS.md文件中,在每次会话时直接加载而无需检索。若将其置于检索机制之后,就会导致智能体不知道该去何处查找,而寄希望于智能体能自行检索并不可靠。

    仅在特定情况下才重要的信息,应当设计为带有触发条件的工具。例如,运行集成套件应该是智能体可以直接调用的功能,而非需要它主动去查找的文档。

    值得关注的是那种经过优化的工作流程。假设智能体发现在这个代码库中,必须在运行测试之前重新生成映射器,而且这种顺序始终有效。这就是从具体案例中总结出的流程。将其作为候选规则保存,并记录其成功次数;在证明其有效性后,将其提升为始终加载的指令;同时保留最初的案例,以便对这一提升过程进行审核。

    因此,程序记忆并非第四种索引。它是第三种索引使用后的剩余部分,从检索结果中提取出来并写入指令中。

    整合:统一的符号命名空间

    检索到的三条记忆在某个符号的身份上交汇,但至今仍无人解决这一难题。结构节点使用完全限定的名称。事件条目指向代理实际处理的符号,因此能够精确关联。知识索引中的实体是模型从文本中提取出的字符串,例如“SettlementService”、“the settlement service”、“the settlement reconciliation path”或“the counterparty SLA constraint”,将它们映射为真实符号则属于模糊匹配任务。

    假装情况并非如此正是各层级存在的目的所要防止的错误,因此这种关联有自己独立的层级结构:

    • exact:名称完全一致,且仅指向一个符号。
    • strong:标准化后的短语明确指向某一类别。
  • weak:指边界较为模糊的若干类别所构成的概念。系统会再次返回一组符号供智能体检查。
  • unresolved:指存在实际约束,但 nowhere都没有对应的符号。“交易方服务等级协议”就是一种真正的架构约束,而代码无法体现它。
  • 对于最后一类情况,应保留原样、直接返回,无需强行匹配。可读性强的未解决约束总比错误的符号绑定更好,而强行匹配反而会导致智能体错误地修改类别。

    智能体实际提出的问题

    查询界面可能如下所示:

    what_breaks(symbol)        → structural: resolved callers, tiered
    why_is_this(symbol)        → knowledge: decisions, incidents, constraints
    what_happened_before(sym)  → episodic: past attempts and their outcomes
    related_by_intent(concept) → resolve a phrase to a symbol set, tiered
    

    接着出现了一个三个人都无法单独回答的问题:“我打算更改结算时的重试策略。这会引发什么问题?为什么是三次?之前有人试过吗?”

    知识图谱将“重试策略”与层级为strong的SettlementService.retry关联起来,并返回相关解决方案及事后分析结果。结构化分析会提取该符号并显示其调用者,其中包括仅在追踪信息中可见的两个服务。事件驱动分析则会指出上季度类似变更曾导致分配测试失败。

    这就是通过三种类型的证据得出的一个答案,每种证据都由相应的生成机制产生,同时还会标注其可信度。

    其代价

    • 大多数文档集合的内容过于匮乏。仅有九条ADR以及标有“修复错误”的提交记录,根本无法为知识索引提供有效依据。在构建之前先审视现有的资源。
    • 实体解析永远无法做到完美。阶梯式方法只能帮助我们应对这一问题,却无法彻底解决它。弱层级很可能是最拥挤的,因此应做好核查准备,而非寄希望于完全信任。
    • 从简化版本开始。先使用tree-sitter加上LSP作为基础。当生成的代码开始出现错误答案时再引入字节码处理机制,当问题涉及多个服务时再添加追踪功能。只有当相关语料库值得被索引时,才添加知识索引。
  • 没有人能够掌握全部内容,这既可能是一种机遇,也可能是一种警示。这些技术分散在不同的团队中:专注于代码智能的人不阅读GraphRAG的相关论文,研究GraphRAG的人则不会考虑类型检查器的问题,而研究内存架构的人大多致力于聊天助手的开发,而非代码库。
  • 思考如何引导新人

    假设一名高级工程师在周一加入公司并获得了代码库访问权限,除此之外没有其他支持。他可以阅读每一行代码、追踪每一次函数调用并运行测试。但到了周五,他的工作效率仍然低于一年前入职的同事,而这并非能力问题。

    那名入职一年的同事拥有三样从未出现在代码库中的东西,而新人则没有。

    其一就是那些奇怪决策背后的原因。之所以要重试三次,是因为三月份发生过一次故障;而之所以要有那种协调解决方案,是因为某个合作方的API在周五时会出现异常。这些原因并没有在任何评论中提及,而是记载在ADR、事后分析报告以及相关讨论中。

    其二则是历史原因。有人在第二季度试图简化那个接口,结果分配测试因此出故障了一周,所以大家就不再去改动它了。

    其三则是团队的工作习惯。在运行测试之前会重新生成映射器。那些跳过验证的PR都会被拒绝。没人会去查阅相关文档,这只是团队的工作方式而已。

    那些只有代码结构认知的员工都是周一才入职的:他们对代码库的整体情况一无所知,不明白各项设置为何如此,也不记得之前的尝试,更不了解团队的本地习惯。每次会议开始都仿佛是新的周一。

    这就是存在的差距,且并非某款工具的错。代码图能够很好地回答自身的问题,如今的实现也很可靠。问题在于,一名优秀工程师所依赖的四个要素中有三个根本不在当前的技术栈中,因此只能由第四个要素承担所有工作。

    一句话版本

    简而言之:编译器负责处理结构,因为结构是确定性的;模型负责表达意图,因为意图属于非结构化的文字;构建系统则基于经验来决策;而流程则是从经验中提炼出来并预先加载的,不会被再次调取。

    这类工具中没有哪一个会在内存管理上出错。每个工具都只负责构建一部分内容便停止,它们之间也没有任何关联。更好的代码图并非解决方案,真正的解决方案是四种内存管理方式,每种都明确说明自身的可靠性,并映射到一个共享的符号命名空间中。

    GraphRAG的术语及处理流程阶段均遵循微软研究院的实现方案。成本会因语料库规模、采集设置及模型选择的不同而大幅波动,因此在投入使用前请先用自己的数据进行测试。

    十五分钟的检测

    在您的代码库中选择一个有五个及以上已知实现类的接口,比如PaymentProcessor或NotificationChannel,其中一个需通过@Autowired注入而非直接创建。查询代码依赖关系,统计调用该接口的节点数量。

    目前该领域的公开基准测试主要衡量token数量、工具调用次数及延迟时间,据所知尚无测试能验证返回的调用关系图是否正确。对于那些已定义接口且实现类明确的代码库,只需五分钟即可利用现有真实数据完成检测。