“你有没有遇到过这样的场景:接手一个维护了三年的老项目,需求排下来,你需要评估改一个底层工具函数会影响多少上层业务。你打开 IDE,沿着调用链跳来跳去,跳了二十分钟,只能确定一部分直接调用方;再用全局搜索搜一遍,结果查出来一堆日志框架里的同名方法。你最后只能凭经验告诉产品经理:可能要改 5 个模块,但我不确定。”这不是你的能力问题,而是传统程序分析工具和“业务语义理解”之间一直存在一条巨大的鸿沟。
这两年,大语言模型(LLM)给这条鸿沟带来了一个新的可能。很多人第一反应是把整个代码库塞给 ChatGPT 或 Claude,让它直接帮忙分析。试过的人都知道,小项目上效果不错,一旦代码库超过几万行,模型就开始“失忆”:要么上下文不够用,要么它给出的结论经常张冠李戴。问题不在模型不够强,而在于我们让模型在一个没有结构化记忆的环境里做分析。
Lemmalog 的核心思路,是把代码库先转成一张清晰的“记忆地图”,让 LLM 先在记忆地图上做推理,而不是直接读原始代码。这个过程不是简单地把代码转换成 embedding 存进向量库,而是把程序分析需要的符号、调用关系、模块职责、业务语义,组织成 LLM 能读懂、能校验、能持续更新的结构化文档。本文会从设计动机、核心概念、架构、流程、代码实现、验证方式到落地建议,完整拆解 Lemmalog 的思路。
1. 这篇文章真正要解决的问题:为什么代码分析这么难
传统程序分析并不是没有工具。IDE 里有“查找所有引用”,命令行里有 grep、ctags、cscope,工程化一点还有 SonarQube、CodeQL、Soot 等静态分析框架。它们能解决“谁调用了这个函数”“这个变量在哪里被修改”这类精确问题,但面对下面几类需求时,往往力不从心:
- 变更影响评估:我改了某个底层函数,哪些业务模块会受影响?影响范围是什么?
- 业务语义溯源:这段看起来奇怪的代码,当初是为了满足什么业务规则才写出来的?注释没写,文档也没有。
- 跨语言/跨服务调用链:一个请求从 API 入口进来,经过网关、服务 A、消息队列、服务 B,最后落库。这中间的数据流和异常传播关系,静态分析工具很难一句话说清楚。
- 架构一致性审查:代码实际结构是不是符合团队规定的分层规范?有没有人绕过了 Service 层直接操作 Repository?
这些问题的共同点是:答案不在代码本身的语法结构里,而在代码与业务意图的对应关系中。传统静态分析擅长提取“程序事实”,比如函数定义、调用边、数据依赖,但它不懂“为什么”。人到是能理解业务,但面对几百万行代码,没有人能把所有调用链都记在脑子里。
LLM 的出现补上了“语义理解”这块拼图。它能读懂注释、变量名、模块命名背后的意图,能根据上下文推断一段代码是做什么的。但 LLM 也有它自己的问题:记不住大代码库的全貌。上下文窗口再大,也塞不下一个中大型项目的全部源码;即使塞下了,注意力机制也会让模型在大量无关代码中迷失,给出看似合理、实则经不起推敲的结论。
Lemmalog 要解决的就是这个问题:能不能设计一个系统,让传统程序分析工具负责“提取事实”,让 LLM 负责“基于事实做推理”,然后把模型产出的结论变成可复用的记忆,持续积累?
读完这篇文章,你可以得到三条具体收益:
- 理解如何把 LLM 的上下文管理能力改造成程序分析的“记忆层”。
- 拿到一套可以落地的流程:符号提取、模块地图生成、LLM 分析、程序化校验。
- 知道哪些场景适合用 LLM 做程序分析,哪些场景必须回到传统工具,避免被“幻觉”误导。
2. 核心概念:LLM 记忆与程序分析的交叉点
在深入 Lemmalog 之前,需要先统一几个概念。
2.1 什么是 LLM 的记忆
LLM 本身没有持久记忆。你每次调用它,它只根据你当前提供的上下文生成回复。所谓“让 LLM 记住”,本质上是在当前请求里提供足够多、足够有效的相关信息。常见的做法有几类:
- 上下文窗口直接塞入:把相关代码片段、文档全部拼进 prompt。适合小项目,不适合大仓库。
- 向量检索(RAG):把代码切块,做 embedding,用户提问时检索最相关的块再交给模型。适合“搜索式”问答,但检索召回质量直接决定分析质量,而且它更适合“定位知识”,不太适合“沿调用链推理”。
- 结构化外部记忆:先对原始材料做一次预处理,提取出符号、结构、摘要,组织成模型容易消费的文档或 JSON。模型看到的是经过提炼的“知识”,而不是原始噪声。
Karpathy 在 LLM wiki 方法里提到的思路,本质上属于第三类:与其让模型每次从头阅读原始资料,不如先维护一份高质量的外部文档,让模型在文档之上工作。Lemmalog 的记忆层设计,和这个思路高度一致,只不过我们记忆的对象从“通用知识”变成了“代码库的程序分析事实”。
2.2 程序分析需要什么样的记忆
程序分析工具输出的信息,大致可以分成两类:
第一类是程序事实。事实是确定的:某个类有哪些方法,某个函数在哪个文件第几行定义,某个函数调用了哪些其他函数,某个变量在哪些路径上被写入。这类信息适合用语法解析器(如 tree-sitter)或编译器前端提取,精确度高,几乎不会出错。
第二类是语义推断。语义是概率性的:这个模块的职责是什么,这段 catch 是不是在兜底某个外部依赖异常,这个设计模式是不是为了后续扩展插件。语义推断需要结合命名、注释、代码结构、甚至团队历史,传统工具做不好,但 LLM 恰好擅长。
Lemmalog 的基本判断是:程序事实和语义推断不应该混在一起。事实交给规则和解析器保证正确性,推断交给 LLM 给出候选,最后由人工或自动校验兜底。记忆层里装的是“事实 + 经过校验的推断”,而不是一堆未经处理的原始代码 embedding。
2.3 两者结合后会发生什么
当 LLM 拿到的不再是原始代码,而是一张“程序事实地图”时,它的推理质量会明显提升。原因很直接:模型不需要在几万行代码里去猜调用关系,它可以专注在“由工具保证正确的调用链”上,做它擅长的高层推理。
举个例子,传统静态分析告诉你:函数 A 调用了 B,B 调用了 C。但 A 调用 B 是正常业务路径,还是仅在异常时触发,工具不知道。如果记忆层里有一条语义标注:“B 只在 A 的异常处理块中被调用,作用是记录审计日志”,那么 LLM 在做变更影响分析时,就不会把 B 的错误理解为一条高频主链路。
记忆的作用,是把“代码长什么样”和“代码为什么这样写”拼在一起,让程序分析从“看语法”升级为“看语义”。
3. Lemmalog 的整体架构设计
Lemmalog 不是一个单一脚本,而是一个分层系统。整个架构可以分成三层:记忆层、分析层、应用层。
3.1 记忆层:代码事实库
记忆层是 Lemmalog 的地基,负责把代码库转成一系列结构化产物:
- 符号表:每个文件里定义了哪些函数、类、变量,行号范围是多少。
- 调用图:函数与函数之间的调用边,文件与文件之间的依赖边。
- 模块地图:按目录或模块组织,描述每个模块的职责、关键入口、暴露给外部的接口。
- 语义标注:人工或 LLM 撰写的模块说明,解释代码背后的业务规则、异常处理逻辑、设计意图。
记忆层推荐的存储形式是“主文件用 Markdown + JSON,辅助检索可以加向量索引”。第一版建议直接用 Markdown 文件配合 Git 管理,原因有三:
- 可读性强,团队里每个人都能 review。
- 可以 diff,代码变更后记忆的变更一目了然。
- 不需要额外基础设施,启动成本极低。
3.2 分析层:LLM 推理引擎
分析层接收三类输入:用户提出的分析任务、记忆层提供的相关事实、程序化约束(比如输出必须符合某个 JSON Schema)。分析层的主要工作,是把这些输入组织成结构化的 prompt,调用 LLM,得到候选结论。
分析层不直接读源码,这是一个原则。所有源码层面的事实,必须先经过记忆层提炼,再交给模型。这样做的最大好处是控制模型的输入规模,把注意力集中在与分析任务相关的信息和知识上。
3.3 应用层:面向开发者的交互入口
应用层是开发者实际接触的部分,形态可以是:
- CLI 工具:执行“影响分析”“模块扫描”“语义检索”等命令。
- CI 集成:提交 PR 时自动分析变更影响范围,把结果评论到 MR 中。
- 交互式问答:让开发者在终端或网页里,针对代码库发问,例如“支付超时重试逻辑在哪里实现”。
应用层本身不包含复杂的分析逻辑,它负责拆解用户请求、调度分析层、收集结果、并最终展示。
4. 环境准备与前置条件
在开始实现 Lemmalog 之前,需要准备以下环境。版本号请以实际安装为准,本文的重点是打通思路。
4.1 基础环境
- 操作系统:macOS / Linux / Windows(WSL 也可)。
- Python:3.10 或以上,用于编写分析脚本。
- Git:用于管理代码库和记忆文档。
- LLM API:一个支持 OpenAI 兼容接口的模型服务,或者本地部署的模型。建议选择输出 JSON 能力较强的模型,便于程序化解析。
- tree-sitter:多语言增量解析库,用于提取代码符号和调用关系。
4.2 安装依赖
以 Python 环境为例:
python -m venv .venv source .venv/bin/activate pip install tree-sitter openai注意,tree-sitter 对 Python 的版本要求可能随版本变化,如果安装失败,优先检查 Python 版本和 tree-sitter 版本是否兼容。如果要解析多种语言,需要提前编译对应语言的 grammar 文件,这部分官方文档有详细说明。
4.3 选择一个合适的试点项目
不建议一上来就对超大型微服务仓库做全量扫描。建议找一个自己熟悉的中小型项目,代码量在 1 万到 20 万行之间,有清晰的目录结构,最好是你正在维护的项目。这样你可以对照真实代码,验证 Lemmalog 产出的符号表和调用图是否正确。
从设计到落地,这条路最大的风险不是“LLM 不够聪明”,而是“记忆层本身是否准确”。如果符号表、调用图错了,LLM 再聪明也会在错误的地图上做出错误的分析。所以小项目、可控范围、人工抽查,是第一版最重要的原则。
5. 核心流程拆解:从代码库到“记忆”再到“分析”
这一节是全文的主干。Lemmalog 的运行流程可以拆成六个步骤。
5.1 扫描:提取符号表
第一步是用 tree-sitter 解析源码,提取每个文件里的函数、类、方法定义。这一步的目标是建立“文件名 → 符号列表 → 行号区间”的映射。
这一步为什么不能交给 LLM?因为 LLM 提取符号会漏、会错、会对不齐行号。行号一旦错了,后续所有分析都会错。符号表必须由确定性工具生成,零误差是唯一标准。
5.2 构建调用图
在符号表的基础上,继续用 tree-sitter 分析每个函数体内部,找出函数调用表达式,把“调用方函数名 → 被调用方函数名”的关系提取出来。同时,把同目录文件、同包文件的依赖关系一并记录。
调用图是记忆层里最有价值的资产。它比文本检索更精确:如果一个函数被 20 处调用,调用图能列出这 20 处的完整上下文。但要注意,tree-sitter 只能识别语法层面的直接调用,对于反射、动态派发、通过框架注册的回调,静态分析会漏掉。这一部分需要在后续语义标注时人工补充。
5.3 生成模块地图
模块地图是给 LLM 看的“知识提纲”,相当于一本书的目录。它按模块或目录组织,描述每一层有什么、入口在哪里、核心函数是哪些。
模块地图不需要覆盖每一个函数,它只需要让模型知道:“如果你想深入分析某个模块,应该去查哪一段记忆”。模块的粒度一般按源码目录划分,配合 README 和核心文件注释生成。
5.4 语义增强:补充代码背后的业务规则
符号表和调用图是“死”的,它们描述代码的结构,但不解释意图。语义增强是让记忆“活”起来的关键步骤。
具体做法有两个层面:
- 人工标注:对核心模块补充业务规则,例如“本模块订单状态流转遵循状态机,禁止跳过 TO_PAY 直接进入 COMPLETED”。
- LLM 辅助总结:让 LLM 读取某个模块的代码,输出职责摘要和关键逻辑说明。重点是,LLM 产出的是草稿,必须由熟悉该模块的工程师 review 后才允许写入记忆层。
很多团队会跳过这一步,直接进行第 5 步分析。结果就是 LLM 只能从代码字面推断语义,容易把异常路径当成主路径,把兼容性代码当成核心逻辑。语义标注的质量,决定了 Lemmalog 分析结果的天花板。
5.5 分析:用结构化 Prompt 驱动 LLM
当记忆层就绪,就可以执行具体的分析任务了。以“变更影响分析”为例,分析层会:
- 加载模块地图和调用图。
- 提取变更文件涉及的函数,以及这些函数在调用图中的上下游。
- 把上述信息连同变更 diff,一起组织进 prompt。
- 要求 LLM 输出结构化结论:受影响函数、风险等级、推理依据。
这里的关键是:prompt 里给的是“提炼后的信息”,而不是数百行原始代码。模型不用大海捞针,它可以立刻开始推理。
5.6 校验与回流:让记忆滚动起来
LLM 输出的结论不能直接信。Lemmalog 在分析层之后加了一个自动校验步骤:
- 检查 LLM 输出的函数名、类名是否真实存在于符号表。
- 检查它声称的调用关系是否匹配调用图。
- 检查文件路径是否存在。
校验通过的结果,可以写回记忆层,作为后续分析的知识沉淀。比如这一次影响分析发现了“A 模块在异常时会调用 B 模块的审计方法”,这条语义标注就可以保存下来,下次分析 A 模块时直接复用。
这一步,是把“一次性问答”升级为“可积累的程序分析资产”的关键。
6. 完整示例代码实现
下面用一个最小示例,演示 Lemmalog 从符号提取到影响分析的完整链路。示例项目结构如下:
lemmalog_demo/ ├── memory_builder/ │ ├── build_symbols.py │ └── build_map.py ├── analyzer/ │ ├── impact_analyzer.py │ └── validator.py └── sample_project/ ├── core/ │ ├── order.py │ └── payment.py └── api/ └── order_api.py6.1 用 tree-sitter 提取 Python 函数定义
文件路径:lemmalog_demo/memory_builder/build_symbols.py
# 示例:用 tree-sitter 提取 Python 文件中的函数定义 from pathlib import Path import tree_sitter from tree_sitter import Language, Parser # 根据你本地安装的 tree-sitter-python 进行加载 # 不同版本加载方式略有差异,建议参考官方文档 PY_LANGUAGE = Language("build/my-languages.so", "python") parser = Parser() parser.set_language(PY_LANGUAGE) def extract_functions(file_path: Path) -> list[dict]: source_code = file_path.read_text(encoding="utf-8") source_bytes = source_code.encode("utf-8") tree = parser.parse(source_bytes) root_node = tree.root_node functions = [] def walk(node): if node.type == "function_definition": name_node = node.child_by_field_name("name") if name_node: functions.append( { "name": name_node.text.decode("utf-8"), "start_line": node.start_point[0] + 1, "end_line": node.end_point[0] + 1, } ) for child in node.children: walk(child) walk(root_node) return functions if __name__ == "__main__": demo_file = Path("sample_project/core/order.py") for func in extract_functions(demo_file): print(func)这段代码做了三件事:解析 Python 源文件、遍历语法树查找function_definition节点、提取函数名和行号范围。输出结果是一个简单的 JSON 列表,后续可以写入符号表文件。
6.2 生成模块地图 Markdown
文件路径:lemmalog_demo/memory_builder/build_map.py
# 示例:生成模块地图 Markdown from pathlib import Path from memory_builder.build_symbols import extract_functions def generate_module_map(project_root: Path) -> str: lines = [ "# Project Map", "", "> 本文件由 Lemmalog 记忆构建器自动生成,建议纳入版本管理。", "", ] for py_file in sorted(project_root.rglob("*.py")): relative_path = py_file.relative_to(project_root) functions = extract_functions(py_file) if not functions: continue lines.append(f"## {relative_path}") lines.append("") for func in functions: lines.append( f"- `{func['name']}` at line {func['start_line']}-{func['end_line']}" ) lines.append("") return "\n".join(lines) if __name__ == "__main__": root = Path("sample_project") map_text = generate_module_map(root) output_path = Path("memory/project_map.md") output_path.parent.mkdir(exist_ok=True) output_path.write_text(map_text, encoding="utf-8") print(output_path)模块地图不是简单的文件列表,它面向的核心读者是“后续的 LLM 分析”。每一步生成的 Markdown 应该保持结构化、可 diff、可追溯。
6.3 调用 LLM 做变更影响分析
文件路径:lemmalog_demo/analyzer/impact_analyzer.py
# 示例:基于模块地图,让 LLM 做变更影响分析 import json from openai import OpenAI # openai>=1.0 的用法;如果你用的是 0.x 老版本,API 会不同 client = OpenAI( base_url="your-api-endpoint", # 替换为你的服务地址 api_key="your-api-key", # 生产环境请使用环境变量 ) def analyze_impact(module_map: str, changed_files: list[str]) -> dict: prompt = f""" 你是一个代码评审助手。下面是一个项目的模块地图: <module_map> {module_map[:12000]} # 控制输入长度,截取核心部分 </module_map> 本次变更涉及的文件如下: <changed_files> {json.dumps(changed_files, ensure_ascii=False)} </changed_files> 请分析变更的影响范围,并严格输出 JSON: {{ "affected_functions": ["函数名"], "risk_level": "high|medium|low", "reason": "影响分析依据" }} """ response = client.chat.completions.create( model="your-model-name", messages=[ {"role": "system", "content": "你是一个严谨的程序分析助手。"}, {"role": "user", "content": prompt}, ], response_format={"type": "json_object"}, ) content = response.choices[0].message.content return json.loads(content)这里有一个值得注意的设计:prompt 中先给模块地图,再给变更文件,最后要求结构化输出。顺序为什么重要?因为模型会优先理解前置信息,再结合任务做推理。如果把变更文件放在前面、模块地图放在后面,模型容易“带着任务找答案”,忽略全局结构,影响分析结果的整体性。
6.4 校验 LLM 输出结果
文件路径:lemmalog_demo/analyzer/validator.py
# 示例:校验 LLM 输出的函数名是否真实存在于符号表 def validate_affected_functions( affected_functions: list[str], symbol_table: set[str] ) -> dict: invalid_names = [ name for name in affected_functions if name not in symbol_table ] return { "valid": len(invalid_names) == 0, "invalid_names": invalid_names, "message": "校验通过" if not invalid_names else "以下名称未在符号表中找到,需要人工复核", } if __name__ == "__main__": # 实际使用时,symbol_table 由符号表构建阶段生成 fake_symbol_table = {"create_order", "pay_order", "cancel_order"} llm_output = ["create_order", "pay_order", "non_exist_function"] print(validate_affected_functions(llm_output, fake_symbol_table))校验逻辑不复杂,但它是整个系统可信赖的基础。只要 LLM 输出中出现符号表里不存在的函数名,就必须拦截下来交给人工,不能让错误结论流向后续流程。
7. 运行结果与效果验证
按照上面的代码,运行顺序如下:
cd lemmalog_demo python -m memory_builder.build_symbols python -m memory_builder.build_map python -m analyzer.impact_analyzer python -m analyzer.validator预期第一步会输出一行行函数信息:
{'name': 'create_order', 'start_line': 12, 'end_line': 30} {'name': 'pay_order', 'start_line': 32, 'end_line': 55}第二步会生成memory/project_map.md文件,内容大致如下:
## core/order.py - `create_order` at line 12-30 - `pay_order` at line 32-55 ## api/order_api.py - `create_order_api` at line 8-20如何判断成功?有一个非常直接的标准:符号表里的函数行号,在 IDE 里跳转后能与真实代码对齐。如果行号对不上,符号表就是废的,后面的分析都不值得信任。建议做一次抽样验证:随机挑 10 个函数,检查函数名和行号是否一致。
如果第三步调用 LLM 时报错,第一步应该检查网络或 API 服务配置;如果返回的不是合法 JSON,优先调整提示词中的输出格式要求,或更换对 JSON 支持更好的模型。第四步如果校验失败,不要直接修改 LLM 的 prompt 让它“再试一次”,而是先人工确认 LLM 输出的函数名是否真的存在——有时候是 LLM 幻觉,有时候是符号表本身漏掉了通过装饰器或动态方式注册的函数。
一次完整的效果验证,建议覆盖下面四个指标:
- 符号提取准确率:抽样比对函数名和行号,目标 100%。
- 调用图覆盖率:选择 5 个核心函数,人工确认调用关系是否完整。
- LLM 结果可复核性:影响分析结果中的每个函数,都能在调用图中找到路径。
- 结论可解释性:LLM 输出的推理依据,工程师能看懂且认可。
这四个指标不要求一次满分,但可以作为每一次迭代的验收标准。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| tree-sitter 解析失败 | 语言 grammar 未正确加载 | 检查 grammar 文件版本和加载路径 | 重新编译 grammar,或升级 tree-sitter |
| 符号表行号与 IDE 不一致 | 解析器版本与代码格式不兼容 | 抽查 10 个函数,比对行号 | 确认解析器使用的语言版本,必要时调整解析参数 |
| LLM 输出不是合法 JSON | 模型对输出格式约束不敏感 | 查看模型返回的原始内容 | 改用支持 JSON mode 的模型,或在 prompt 中给出更严格的 schema 示例 |
| 影响分析遗漏关键模块 | 语义标注不完整,或调用图存在动态调用漏洞 | 人工检查遗漏模块是否在调用图中有边 | 增加人工语义标注,或补充动态调用场景的规则 |
| 分析结果包含不存在的函数名 | LLM 幻觉 | 校验层如何拦截?查看校验日志 | 增加符号表校验,必要时对幻觉名称做二次确认 |
| 记忆文档过大 | 项目规模增长,Markdown 全量加载 | 检查模块地图文件大小 | 按模块拆分文件,分析时按需加载相关模块记忆 |
| 每次分析都重新构建全量记忆 | 没有增量更新机制 | 检查构建流程是否全量重跑 | 基于 Git diff 做增量更新,只重建变更模块 |
| 代码上传第三方 API 存在合规风险 | 数据脱敏不足或使用外部云模型 | 检查发送给 API 的 prompt 内容 | 优先使用私有化部署模型,脱敏敏感字段,或严格按合规流程审批 |
这其中的“LLM 幻觉”问题是最值得强调的。程序分析里不能接受“大概”“可能”的结论,所以校验层不是可选项,而是必需项。凡是没有符号表支撑的结论,一律标记为“待确认”,而不是直接采纳。
9. 最佳实践与工程边界
9.1 把记忆文档纳入版本管理
建议在项目仓库或独立文档仓库中,专门建立一个memory/目录,存放模块地图、符号表 JSON、语义标注 Markdown。每次代码变更后,有增量任务重建相关模块的记忆,并把变更记录提交到 Git。这样团队的每个人都能 diff 记忆的变化,及时发现记忆与代码脱节的情况。
9.2 建立“人工审核”的最终防线
LLM 生成的语义标注、影响分析结论,在写入记忆层之前,必须经过至少一名熟悉项目的工程师确认。实践中可以约定:自动生成的内容标为draft,review 通过后改为approved。分析层优先使用approved状态的记忆,draft状态只能作为参考。
9.3 先小后大,用真实项目迭代
第一版不要追求覆盖所有语言、所有动态特性。先选一种语言(比如 Python 或 Java),选一个模块,把符号提取、调用图、模块地图、LLM 分析这一条链路跑通。跑通后再扩展语言支持、增加动态调用处理、优化增量构建。这个路径看起来慢,实际是最快的——因为每一步都能被验证,不会在错误的基础上越走越偏。
9.4 区分“事实”和“推断”,写入不同的记忆层
记忆层里可以再分成两个子区:facts/和inferences/。facts/存放符号表、调用图、文件结构,由工具生成,保证正确;inferences/存放 LLM 和人工产出的语义标注,带置信度、审核状态、创建时间。分析时,先把两份记忆都加载给模型,但在 prompt 中明确标注哪些是确定性事实、哪些是推断,模型就能更合理地权衡信息权重。
9.5 数据安全与合规边界
如果代码库涉及未公开的商业逻辑、生产环境配置、用户数据相关代码,直接把完整代码或模块地图发送给外部 LLM API,存在合规风险。更稳妥的做法有三种:
- 优先使用私有化部署的开源模型,例如在本地或内网 GPU 服务器运行。
- 如果必须使用外部 API,需要先做敏感信息脱敏,并经过数据安全审批。
- 代码摘要、函数名、调用关系等元数据,可能比源码本身更敏感,在发送前也要评估。
在合规这一点上,技术方案做得再好,也无法替代制度和流程上的把关。
9.6 不要用 LLM 替代所有传统静态分析
明确 Lemmalog 的边界:它适合做“语义关联分析”,比如变更影响、模块职责梳理、代码为何这样写的解释性问答。但是在需要穷举可达路径、数据流精确分析、安全漏洞模式匹配的场景,传统静态分析工具(如 CodeQL、Soot、SpotBugs)仍然是不可替代的。
更合理的策略是混用:CodeQL 负责精确的污点分析,Lemmalog 负责解释“污染的根源在哪里、为什么这里会成为入口”。工具之间互补,而不是互相取代。
9.7 建立增量更新机制
代码仓库每天都在变,如果每次分析都重建全部记忆,不仅慢,还会产生大量噪声。增量更新的思路是:
- 监听 Git 提交记录或文件变更事件。
- 对变更文件重新提取符号,并更新模块地图对应段落。
- 只重建受影响模块的语义标注,推荐使用 LLM 对比旧标注和变更 diff,判断是否需要修改。
增量更新能显著降低使用成本,也让“记忆库”保持新鲜。没有增量机制的 Lemmalog,本质上还是实验玩具,很难进入日常开发流程。
10. 边界与风险:LLM 程序分析不等于自动驾驶
这一部分非常重要。很多团队看到“LLM 做程序分析”就兴奋,以为以后可以不用看代码了。这是一个危险的想法。
LLM 擅长的是模式识别和语言关联,它不是在“验证”程序性质,而是在“猜测”一段代码可能的行为。只有当它被约束在正确的、足够细粒度的事实之上,它的猜测才有价值。所以 Lemmalog 的定位是“辅助分析工具”,不是“自动决策系统”。
以下几点需要反复强调:
- 调用图是静态识别的,不是必然路径。函数 A 调用了 B,不代表 A 的所有调用路径都会执行到 B。
- LLM 的置信度不等同于正确率。模型可能用一种非常自信的语气,说出一个完全错误的结论。
- 人工审查是最终防线。任何影响线上系统的变更评估,都必须有人类工程师签字负责。
- 记忆库也会有偏差。代码更新了,但记忆没更新,LLM 就会基于过时信息做判断。
一个合格的落地方式是:让 Lemmalog 输出带证据链的分析报告,报告中每个关键结论都附上“对应的函数名、行号、调用图路径”,工程师可以顺着证据链快速核对。没有证据链的结论,默认不可信。
11. 总结与下一步实践
Lemmalog 这个项目的核心价值,不是发明了一种新的程序分析算法,而是重新组织了一整套工作流:用确定性工具保障程序事实的正确性,用 LLM 补足业务语义推理能力,用记忆层把一次性的分析沉淀成可复用的资产。
如果你也想在自己的项目里尝试这条路线,我的建议是从下面四步开始:
- 选一个自己熟悉的、规模不大的代码库。
- 用 tree-sitter 或其他解析器,生成准确的符号表和模块地图。
- 用今天给出的示例代码,跑通一次“变更影响分析”的 demo。
- 人工核验分析结果,记录失败案例,逐步增加语义标注和程序化校验规则,完善校验层。
这期间你会遇到很多看起来很麻烦的细节:动态调用怎么识别、语义标注谁来写、增量构建怎么设计、幻觉怎么拦截。但这些细节恰恰是 Lemmalog 的价值所在——不是模型本身多强,而是我们把“让 LLM 可靠地工作”这件事认真对待了。
程序分析的未来,大概率不是哪个工具单独胜出,而是静态分析、LLM、人工 review 三者各司其职。Lemmalog 只是这个方向的一个早期实践。如果你也在这个方向上有想法,不妨从一个最小的模块地图开始,把记忆层建起来,之后再逐步扩展。代码会变,业务会变,但一套能够随代码演进、持续积累语义的程序分析记忆系统,会成为整个研发团队真正的长期资产。