Workshop

实战工坊:把 LLM Agent Memory 变成程序分析引擎

DOC
N°808
DATE
Sep 7, 2026
READ
5 min read
TAGS
LLM-agent, agent-memory, program-analysis, vulnerability-research, security, workshop

I accidentally turned LLM memory into program analysispwning.systems 上 Jordy Zomer 在 8 月底发布的一篇长文,最近在 Hacker News 上拿到 300+ 讨论热度。作者的本意是用 LLM agent 做漏洞研究,结果发现一个副作用:agent 在探索代码库时写入的 memory,随着时间推移沉淀成了结构化的程序知识。回头去查这些记忆时,它实际上承担了程序分析的工作——数据从哪里进来、流向哪些函数、哪些路径缺少校验。

这篇实战工坊不复述原文,而是把这个思路拆成可复现的工程步骤:记忆怎么设计、怎么抽取、怎么查询、边界在哪里。对于做大代码库安全审计或只是想快速理解陌生仓库的开发者,这个方法今天就能用。

现状:静态分析和 LLM 理解之间的缝隙

传统程序分析有两条成熟路线。静态分析基于语言语义,能做到调用图、数据流、污点传播,但受限于解析器覆盖、宏和动态分发,而且对注释、配置和运行时行为视而不见。LLM 直接读代码理解意图很强,但每次会话都是一次性劳动——这个会话里读懂的调用链,下个会话全部忘掉。

两边的缝隙正好是 memory 的位置。Agent memory 本来是用来记住用户偏好和项目约定的,但如果在探索代码时写入的是程序事实,它就变成了一个增量构建的代码知识库。探索即建模,这是这个思路的核心。

记忆模式设计

第一条经验是:不要存自然语言散文,要存结构化条目。散文记忆无法去重、无法检索、无法验证。一个可用的记忆条目至少要包含这些字段:

{
  "kind": "dataflow",
  "symbol": "parse_user_input",
  "location": "src/api/parser.ts:142",
  "observes": "request.body 未做类型校验直接展开",
  "flows_to": ["build_sql_query", "UserRepository.find"],
  "confidence": 0.85,
  "evidence": "trajectory-2026-09-02",
  "recorded_at": "2026-09-02T14:20:00Z"
}

类别建议从五种起步:

  1. symbol:函数、类、全局状态的职责一句话。
  2. dataflow:某个输入字段流向哪些函数,中间是否经过校验。
  3. boundary:外部输入进入系统的位置(HTTP 参数、文件、环境变量、消息队列)。
  4. dangerous_call:SQL 拼接、命令执行、反序列化、路径拼接等调用点。
  5. invariant:代码里隐含的不变式,例如「该字段始终由 middleware 先行过滤」。

字段越稳定,后面的合并和查询就越容易。confidenceevidence 是安全底线:没有证据指针的记忆无法审计,错误事实会长期污染知识库。

抽取流程

抽取不要单独跑任务,而是搭在 agent 的自然探索上。原则是「探索即建模」:agent 本来就要读文件、跑测试、追调用链,只需要在关键动作之后追加一次记忆写入。

一个可用的抽取 prompt 骨架如下:

你刚读完 {file}。请判断是否值得写入记忆:
1. 该文件是否定义了外部输入边界、危险调用或全局状态?
2. 是否观察到明确的字段级数据流(从输入到消费点)?
3. 是否存在依赖隐式校验的不变式?
对以上任一肯定回答,按既定 schema 追加一条记忆条目,
证据指向当前轨迹 ID。不要写入泛泛的架构感想。

注意最后一句。Memory 最常见的失败模式是积累「这个模块设计不太好」这类主观评论,它们很快会让知识库失去可信度。规则应该是:只记可指向代码位置的事实。

去重与合并

多次探索必然产生重复与冲突。合并策略可以很朴素:

  • 相同 symbol + kind 的条目,保留 confidence 最高的一条,其余计数归并。
  • 出现字段冲突(比如同一函数的职责描述完全不同),不自动覆盖,标记 conflict 留给下次探索确认。
  • 用代码变更做失效信号:文件变更后,把该文件相关的旧条目降权,下次 agent 路过时自然重验。

这一步不需要向量数据库,一个 JSON 文件加 jq 就能起步。规模上来之后再考虑向 SQLite 或向量库迁移。

查询:把污点分析变成提问

记忆库沉淀两三周后,查询阶段才真正有趣。传统污点分析要配置 source 和 sink,这里直接用自然语言提问:

问题 1:列出所有进入系统的外部输入,以及各自流向的消费函数。
问题 2:parse_user_input 观察到的字段会经过哪些校验?
       哪些路径没有校验?
问题 3:如果修改 build_sql_query 的签名,哪些记忆条目会失效?

回答这类问题时,agent 检索记忆条目、按 flows_to 沿边聚合、返回出处。这已经是近似污点分析和影响面评估了。pwning.systems 原文的场景正是如此:做漏洞研究时回头问 memory「这个输入有没有绕过校验的路径」,得到的是探索期间积累的真实观察,而不是从零开始的重新阅读。

对于安全审计,一个实用技巧是把 OWASP 式检查单转成记忆查询:外部输入边界有多少个、危险调用各自的输入是否可追溯到未校验来源、历史修复是否覆盖了同类模式。查询输出可以直接变成审计报告的初稿。

边界:它不是静态分析的替代品

必须诚实面对这个方法的三个硬限制。

第一,不完备。Memory 只知道 agent 看过的地方。没探索过的路径不存在于知识库里,查询返回「没发现」不等于「不存在」。把它当覆盖率工具用,会得到虚假的安全感。

第二,不保证正确。LLM 对代码的观察可能出错,尤其是间接调用和运行时行为。所以每条记忆都要有证据指针,高风险结论要回读源码确认。

第三,会过期。代码演进后记忆变陈旧。用文件变更做失效信号是必须的,不是可选优化。

正确的定位是:memory 是静态分析的放大器。静态分析给出完备的调用图,memory 补上意图和动态行为;memory 给出可疑路径,静态分析负责验证。两者交叉的部分才是高置信度结论。

落地路线

个人开发者今天就能跑最小版本:在项目里放一个 memory.json,把抽取 prompt 加进 coding agent 的系统提示,让它每次读完关键文件追加一条结构化记录。两周后对着陌生仓库提问,你会明显感觉到「这个仓库我好像已经熟悉了」。

安全团队可以做正式版本:统一 schema、轨迹 ID 与 agent 日志打通、每周跑一次记忆回归(重放已知漏洞,看记忆能否定位)、查询结果附带证据链输出到审计报告。

平台团队则应该关注接口:记忆写入是否原子、是否有版本和回滚、能否按项目隔离、敏感信息是否会被写进记忆(密钥、内网地址必须排除)。

结论

pwning.systems 这篇博客的价值在于点破了一个认知:agent memory 不只是上下文管理的配件。当写入的内容是程序事实而不是用户偏好时,探索过程本身就成了增量建模,多次会话的劳动第一次可以累积下来。

对开发者,我建议把它当成「带证据的代码笔记」来用,先在陌生代码库和新同事 onboarding 场景里验证价值,再逐步过渡到安全审计。对精确性要求高的场景,仍然让静态分析和测试兜底。记忆负责覆盖面和意图,工具负责正确性和完备性——这条分工线画清楚,这个方法才安全可用。

Frequently asked questions

LLM memory 程序分析和传统静态分析有什么区别?
静态分析基于语言语义做完备推理,结果可复现但覆盖率受解析器限制;memory 方案靠 agent 探索沉淀事实,覆盖动态行为和注释意图,但结果不保证完备,两者更适合互补而不是互相替代。
这个方法适合哪些安全场景?
最适合大代码库的初步审计、第三方依赖梳理、攻击面盘点和历史漏洞修复检查。精确的漏洞判定仍应交给静态分析器、fuzzer 和人工审计。
记忆条目应该存什么字段?
至少存符号名、文件位置、类别(函数、数据流、外部输入、危险调用)、关系(调用、写入、校验)、置信度和来源轨迹,再加时间戳方便过期和回归对比。
如何避免记忆里积累错误事实?
每条记忆保留证据指针、给低置信度条目降权、定期用代码变更触发重验、查询时返回出处让人工抽查,并在合并重复条目时做冲突检测。
个人开发者最小的落地版本是什么?
用一个 CLAUDE.md 或 memory 文件加固定抽取模板,让 coding agent 每次读完关键文件就追加一条结构化记录,两周后就能对陌生仓库做查询式分析。
// next.txt ›

Some outbound links in this post are affiliate links — see disclosure.