I accidentally turned LLM memory into program analysis 是 pwning.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"
}
类别建议从五种起步:
symbol:函数、类、全局状态的职责一句话。dataflow:某个输入字段流向哪些函数,中间是否经过校验。boundary:外部输入进入系统的位置(HTTP 参数、文件、环境变量、消息队列)。dangerous_call:SQL 拼接、命令执行、反序列化、路径拼接等调用点。invariant:代码里隐含的不变式,例如「该字段始终由 middleware 先行过滤」。
字段越稳定,后面的合并和查询就越容易。confidence 和 evidence 是安全底线:没有证据指针的记忆无法审计,错误事实会长期污染知识库。
抽取流程
抽取不要单独跑任务,而是搭在 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 场景里验证价值,再逐步过渡到安全审计。对精确性要求高的场景,仍然让静态分析和测试兜底。记忆负责覆盖面和意图,工具负责正确性和完备性——这条分工线画清楚,这个方法才安全可用。