Lemmalog 是我最近在维护一堆遗留代码时写出来的一个本地工具。它的核心思路其实很窄:把 LLM 在各种聊天、日志、文档里生成的零散记忆,转化成可以被程序分析的结构化记录。所谓程序分析,不是说让 LLM 去读源码,而是让我能用检查调用关系和数据流的方式,去检查那些由语言模型输出的、容易漂移的旧结论。刚开始我只是想解决一个很现实的麻烦:同一个接口,我在两次会话里得到过完全相反的建议,但翻聊天记录根本翻不到当时的上下文。这个项目就是为这种场景写的。
如果你也是经常把大模型当结对程序员用的开发者,或者你维护的模块足够多,但自己的脑内记忆已经装不下那些历史决策、弃用接口和“为什么当初不这么做”的原因,那么 Lemmalog 这条路径值得参考。它不依赖企业级平台,不要求巨大的显存,也不需要把公司代码上传到第三方服务。它只是把自己的 LLM 输出当作一种待分析的数据,再给这些数据补上源码锚点、时间戳和一致性检查。下面按我实际的搭建顺序拆开讲。
1. 问题起点:LLM 帮你写代码,但它不知道自己上次说了什么
1.1 真正的痛点不是“没有上下文”,而是“记忆没有结构”
很多人觉得 LLM 写代码不靠谱,是因为上下文不够。于是大家疯狂加 prompt,把整段聊天历史塞进去,把 README 塞进去,把上次的报错也塞进去。我也这样试过。结果不是变的更聪明,而是更长、更贵、更容易自相矛盾。更关键的问题是:就算模型真的能从上下文里找出“上次说要改这个”,它找出来的结论也只是一个对话回复,二次使用价值很低。
我遇到的典型场景是这样的。
一个老模块里有个process_order()函数,代码注释很少,业务规则散落在三四处地方。前两个月我让 LLM 帮我分析过一次,它明确说“这个函数不应该直接发邮件,因为之前的方案被否掉了”。结果这个月我又开了一个新会话,换了模型,它给的建议变成“如果用户要求,直接在这里发邮件也可以”。如果只从单次对话看,每条建议都合理。但放到长期维护里,这就是认知断裂:我失去了“为什么当初否掉”的决策依据,后续所有依赖该决策的改动都会变得不稳。
聊天记录能搜到“发邮件”三个字,但搜不到“这个约束连接了哪个函数、哪个用户需求、哪个提交”。全文检索给不出关系,普通笔记软件也只是一个又一个孤立的页面。我需要的是把 LLM 说过的话,当成代码一样去追踪和分析。
1.2 为什么是“程序分析”,而不是“笔记软件”
笔记软件适合记事实,比如“这个接口已弃用,用order_totals.apply()替代”。这句话记下来以后,人去看还行。但如果我要在重构前知道“所有还在引用OrderService.update_total()的代码在哪里”,或者“有多少条记忆与当前代码状态冲突”,笔记软件就无能为力了。
程序分析的思路是:把记忆当作一个带类型、带引用的中间表示。它像编译器里的 IR 一样,有实体、有方向、有源位置,可以被静态扫描。我在 Lemmalog 里做的并不是真正分析程序源码,而是分析“LLM 关于程序的记忆程序”。这套 IR 会包含文件路径、函数符号、提交哈希、决策状态。然后我可以用类似检查代码的规则去检查记忆:引用是否还存在,约束是否已经过时,两个决策之间是否互相冲突。
这种方法让我第一次觉得,LLM 的输出不再是一段段漂浮的文字,而是一堆可以被 review、被测试、被版本化的数据。
1.3 项目要解决的四个基本任务
Lemmalog 把所有工作收敛成四条主线。
- 捕获:抓取历史日志、终端输出、提交信息、LLM 会话导出、自己写的临时备注。
- 归档:把非结构化文本转成统一的记忆单元,包括事件、决策、约束、API 经验。
- 关联:将每个记忆单元链接到具体的文件、函数、配置项和提交版本。
- 检验:运行规则分析器,检查记忆里的引用有没有失效,约束有没有被新代码打破,决策之间有没有矛盾。
这四条线走完,一个 LLM 记忆库才算真正进入了程序分析的流程。否则它只是另一个聊天记录存档。
2. 核心技术选择:如何让 LLM 从“上下文”变成“数据”
2.1 让 LLM 生成结构化记忆,而不是让它直接回答
Lemmalog 的第一个设计决定是:绝不要求 LLM“总结这段对话”,而是要求它按 schema 输出记忆条目。
比如我给它一段历史记录,它会拿到的指令是:
从下面的材料中提取记忆。每条记忆必须是: - kind: event / decision / constraint / api - source: 来自哪条消息或哪个文件 - entities: 涉及的文件路径和符号名 - claim: 一句话事实陈述 - status: proposed / verified / refuted 如果你无法确定某个字段,宁可输出空,也不要编造。把“总结”改成“按 schema 提取”,效果完全不一样。前者会丢细节,模型倾向于给一段流畅的概括,很多关键约束会被揉成一个抽象的句子。后者强迫模型逐条输出,每条还能单独追溯到来源。
有了这些结构化记忆之后,后续查询就不必再反复把长对话扔给 LLM,而是直接读本地数据。成本降低,结果也更容易被复验。
2.2 输入尽量带上位置,输出尽量能溯源
我踩过最大的坑是:让模型只读聊天文本,不给文件路径和函数名。结果它凭上下文猜了一个很接近但错误的符号,后续分析器把它当成真引用,到处报警。
后来 Lemmalog 的提示词里会明确把当前项目路径、文件路径、函数签名、提交哈希作为前缀加进去。模型在输出entities字段时,必须引用输入中存在的路径和符号,不能单凭记忆生成。这一步不需要什么高深技术,但能从根源减少“幽灵引用”。
输出侧也要溯源。每条记忆都带source.file、source.message_id或source.line。这样当分析器报出“该记忆引用的文件已不存在”时,我可以快速回到原始材料,确认是不是旧日志导致的误报。
2.3 一致性不能只靠模型,还得靠 schema 和重试
LLM 有一个很让人头疼的特点:你让它跑两次,两次结果不完全一样。如果 Lemmalog 把这种不稳定的输出直接入库,那分析器每天看到的都是不同世界。
我的处理方式分三层。
第一层是约束输出格式。所有记忆条目必须是合法 JSON,字段名固定,枚举值固定。解析失败时直接进入重试,不进入数据库。
第二层是重试策略。如果 JSON 解析失败或输出不完整,Lemmalog 会把同样的输入重新送给模型一次。第二次仍然失败,就标记成parse_error,写进错误日志,等待人工处理。批量跑任务时这个机制很重要:不能因为一条记录解析失败就把整个批次中断。
第三层是字段规范化。JSON 里的键名顺序、引号、布尔值都可能有细微差异。入库前我会做一遍字段排序和类型强转,保证重复导入同一份数据时,能够比较出“结构是否发生了真实变化”。
2.4 FP16、FP32、BF16 在抽取场景下怎么选
本地跑 LLM 时,大家经常会纠结该用 fp16、fp32 还是 bf16。Lemmalog 的场景不是写诗,而是批量抽取和归档。这时候最关心的不是生成文本多么华丽,而是同一输入在不同批次里能否得到一致的结果。
我测试下来,fp16 模型跑大多数抽取任务足够,生成的 JSON 结构基本稳定。但在上下文特别长、schema 特别复杂的时候,出现过漏掉个别约束的情况。换成 bf16 之后,同类任务的漏项明显减少,但我也没做过严格统计。真正起作用的其实是两层保险:一是把温度调到接近 0,二是在可能不稳定的条目上重试一次。
fp32 虽然更稳,但显存占用更高,单次推理更慢,对批量任务并不划算。我的结论是:不要在刚起步时就追求 fp32。先用 bf16 或 fp16 跑完整条链路,再对可疑条目做抽样比对。如果发现某类记忆经常漏,再针对性地把该条目的温度调低或换更高精度重跑。不要为了一个理论上的精度提升,把整台机器的吞吐压垮。
3. Lemmalog 的工作流:从原始日志到可查询记忆库
3.1 输入层:日志、文档、会话、随手笔记
Lemmalog 的输入没有做成复杂的数据库接入,就是一个本地目录,我把它叫inbox/。里面放几类材料:
logs/:终端输出、测试日志、部署报错。docs/:README、设计文档、临时需求文档。chats/:LLM 会话导出的 JSON,或者我从聊天窗口复制出来的文本。notes/:我自己写的备注,比如“xxx 那里有坑,下次注意”。
每种来源处理方式不同。日志按时间戳切片,文档按标题分块,会话按消息轮次切片。分块的目的是控制输入长度,避免把整个文件一次性塞进提示词,导致模型在中间丢失关键信息。
我一般会写一个很简单的 monitor 脚本,监听inbox/目录,有新文件进来,就丢到提取队列里。第一次跑通时,不要急着接入所有历史文件。先放两个小文件,验证提取结果,再逐步铺开。
3.2 记忆单元:Lemmalog 的数据最小环
一条记忆最终会变成类似这样的 JSON:
{ "id": "lem-0042", "kind": "api", "source": { "file": "logs/chat_20250414.json", "message_id": "m-28" }, "commit": "a3f9c1", "entities": ["src/order_service.py", "OrderService.update_total"], "claim": "OrderService.update_total() 在 v3.x 已被弃用,新代码应改用 order_totals.apply()。", "status": "proposed", "confidence": 0.82 }字段不多,但每个字段都有明确用途。
kind决定这条记忆参与哪类分析。约束类记忆用于检查当前代码是否违反约束,API 类记忆用于检查符号引用是否过期,决策类记忆用于追溯历史原因。entities是记忆和代码之间的桥梁。status表示这条记忆是否被后续代码或提交验证过。confidence只是参考值,不参与最终判断,我主要用来做排序。
这个结构越简单越好。如果一开始就设计十几种记忆类型,LLM 会频繁混淆,解析错误率会高很多。我最后只保留四种核心类型,其他类型都塞进claim的文本里,不参与分析逻辑。
3.3 关联层:把记忆链接到代码符号和提交
有了记忆单元之后,下一步是建立关联。Lemmalog 会把entities里的文件路径解析成绝对路径,并把函数名和类名映射到当前代码里真实的符号。
这一步非常重要,因为 LLM 输出的符号名并不总是和代码完全一致。可能它写的是OrderService.update_total(),而代码里的实际名称是OrderService.update_total,也可能是order_service.update_total。解析器需要做大小写、下划线、命名空间前缀的归一化。
如果符号对不上,我不急着判定为错误。可能这个符号只存在于某个历史分支,也可能已经被重构掉了。我会把它标记成unresolved,并且把这个状态写进分析报告。人工确认后再决定是修正记忆,还是继续保留为历史线索。
关联完成后,Lemmalog 会构建一个简单的关系图:文件 -> 符号 -> 记忆。后续问“某个函数被哪些记忆引用”时,直接查这张图,不需要再把原始文本翻出来重新读一遍。
3.4 分析器:不靠 LLM 判断,靠规则做一致性检查
很多人以为分析器也应该用 LLM。但我选择了相反方向:能写规则的部分全部用 Python 写死。
比如检查“孤儿引用”,直接解析所有记忆的entities,和当前代码文件列表比对。检查“过期符号”,从当前代码里提取符号集合,再去记忆库里扫描还引用旧符号的条目。检查“决策冲突”,把同属一个entities的decision条目拉出来,比较claim里的动词和否定词。
规则分析的好处是:结果稳定、可测试、也不消耗 GPU。每个规则可以单独写成函数,跑完生成一份 Markdown 报告。真正需要 LLM 介入的只有首次抽取、或者规则判定结果太模糊时的二次确认。不要所有环节都依赖同一个模型,否则一个问题没解决,还会引入新的不确定因素。
一个很简单的检查器:
for mem in memories: links = resolve_symbols(mem.entities) if not links: report("orphan-memory", mem.id, mem.source) elif mem.status == "proposed" and not version_match(mem.entities, current_commit): report("stale-reference", mem.id, mem.entities)这不复杂,但已经能抓出相当多实际问题。
4. 部署、参数和若干配置坑
4.1 运行条件和模型接口
Lemmalog 本身不假设你必须用某个特定模型。它对模型只要求两件事:支持 OpenAI 兼容的接口,输出 JSON 时不要擅自加额外格式。本地部署时,我一般用一个本地推理引擎加载开源模型,暴露一个本地端口,然后把模型的base_url填到 Lemmalog 配置里。如果你的机器没有独立显卡,可以用 CPU 版本跑小模型,但速度会慢,建议先把输入分块变小。
我的建议是:如果是学习验证,8GB 左右显存就够跑一个 7B 到 8B 的量化模型。如果要处理大量文档,16GB 以上加内存足够。低配机器不是不能跑,但要把分块长度、并发数都降下来,不要一上来就追求大窗口。
4.2 影响抽取效果的五个参数
这里列一张表,是我在 Lemmalog 里反复调过的核心参数。
| 参数 | 推荐初始值 | 说明 |
|---|---|---|
| temperature | 0 到 0.1 | 越低越稳定,适合归档类任务 |
| context_window | 4000 到 8000 字符 | 控制单次输入长度,太长容易丢开头 |
| max_tokens | 1500 以上 | 太短会导致 JSON 截断,解析失败 |
| retry | 1 到 2 次 | 解析失败时自动重试,但不建议无限重试 |
| batch_size | 1 开始,再逐步增加 | 并发太高会让单条质量下降,还容易触发限流 |
如果发现返回结果是合法 JSON 但内容总是漏字段,先检查 max_tokens 是否够用。如果发现同一条记忆隔几次跑结果不同,先检查 temperature 和输入顺序是否稳定。如果发现某个大文件处理到一半就丢了结尾,把 context_window 调小,或者把该文件拆成更多重叠块。
4.3 路径和输出目录,比模型更能坑人
这个部分值得单独拿出来讲。Lemmalog 卡住最多的不是模型能力,而是路径问题。
我在一个 Linux 服务器上用相对路径启动了后台任务,结果所有输出都写到了别的目录,第二天才发现。后来换到 Windows 测试,代码里用反斜杠拼接路径,导致一部分文件解析失败。还有一个更隐蔽的问题:把模型目录和输出目录放到外部磁盘,结果外部磁盘没有正常挂载,程序不报错,只会在运行时看起来像“模型没反应”。
另外,如果你的输出目录被.gitignore忽略,那记忆库可能只在本地可见。这对个人使用没问题,但如果你想把这个记忆库作为项目资产团队共享,就要提前决定好目录策略。我最后给 Lemmalog 加了一步启动自检:打印出模型路径、输入目录、输出目录的绝对路径,并且检查路径是否存在。宁可多打两行日志,也不要让错误在几个小时后才暴露。
4.4 LLM wiki、agent.md 和笔记工具带来的启发
我最早也没有直接写 Lemmalog,而是先试了市面上很多知识库方案。用过 Obsidian 搭配 LLM wiki 的思路,也参考过 Karpathy 提过的那种“给模型一份标准说明文档”的做法。agent.md 这个范式对我影响很大:在项目根目录放一个文件,告诉 LLM 项目如何构建、如何测试、有哪些约束。Lemmalog 借鉴了这一点,会生成一份.lemmalog/index.md,里面记录当前记忆库的目录结构、记忆类型、抽取规则。
这套做法让记忆库对人和模型都友好。人打开.lemmalog/index.md能快速知道当前有多少条记忆,分析器也可以直接读取这个索引做一致性检查。Obsidian 那一套强调“原子化笔记”和“模板”,Lemmalog 的对应物就是记忆单元和统一 schema。只是我不把结果放在可视化的笔记界面里,而是放在本地目录里,方便脚本继续加工。
AnythingLLM 和 ComfyUI 的使用经历也提醒了我一件事:很多工具不是能力不够,而是配置路径、模型路径和权限设置太容易出错。ComfyUI 里如果extra_model_paths.yaml写错了,模型不会加载,但程序启动时不一定报错。这给我的启示是:任何和外部路径打交道的工具,启动时都要主动打印关键配置,而不是让用户自己猜。
5. 用真实场景验证:重构一个遗留函数时,Lemmalog 做了什么
5.1 场景:一个没人敢改的 process_order
我拿 Lemmalog 做的第一个完整验证场景,是一个旧 Python 服务里的process_order()函数。这个函数有几百行,注释少,逻辑长,历史改动非常多。过去几个月的 bug 修复、需求变更、聊天记录里的讨论都散在各种地方。
我把这些材料全部丢进inbox/:
- 近半年的 git 提交记录
- 几次 LLM 会话导出
- 旧测试日志
- 我自己写的几条备注
Lemmalog 先做提取,再做符号关联,最后跑规则分析器。整个过程不复杂,但输出结果很有价值。
5.2 提取出来的三类关键记忆
第一类是决策记录。
kind: decision claim: 早期方案曾尝试在 process_order 里直接发送邮件,但用户要求改为异步通知,所以该路径被否决。 entities: ["src/order_service.py", "process_order", "NotificationService"]这条记忆直接解释了当前代码里为什么没有“发邮件”这个操作。如果未来某个新模型建议“直接在这里发邮件”,分析器可以立即把这条记忆找出来,提示用户这条建议与历史决策冲突。
第二类是约束记录。
kind: constraint claim: order 状态进入 finalized 后,不能再修改优惠金额。 entities: ["src/order_service.py", "order.status", "discount"]这个约束在代码里没有显式注释,但业务逻辑里隐含了这个条件。如果将来有新人接手,看到这个约束会少踩很多坑。
第三类是 API 过时记录。
kind: api claim: OrderService.update_total() 在 v3.x 被弃用,应该改用 order_totals.apply()。 entities: ["src/order_service.py", "OrderService.update_total"]分析器跑完之后发现,当前代码里OrderService.update_total()仍然存在引用。于是报告给出一个“过期引用”警告。这一条不需要 LLM 判断,规则本身就能发现。
5.3 验证指标:一致性、可复现性、可追溯性
这个场景真正验证的是三个指标。
一致性:我隔两天重新对同一份输入跑一遍,关键记忆的id没有变化。这靠的不是模型,而是把主键从“内容哈希”换成了“来源文件加来源位置”。内容会因分块方式变化而不同,来源位置是稳定的。
可复现性:我把温度调到 0,重跑三次,核心结论基本一致。抽样了十条记忆,人工比对后没有出现方向性反转。我不认为这是模型能力强的证明,而是 schema 约束和低温度共同作用的结果。
可追溯性:报告里每一条分析结果都带source指针。如果某个警告看起来不对,我可以直接回到原始聊天记录或日志,确认这条记忆是否真的来自那段材料。
这三条验证下来,Lemmalog 才真正算是一个可以放进开发流程的工具,而不是一个“看起来很有感觉”的玩具。
6. 边界、失败模式与演进方向
6.1 Lemmalog 不擅长什么
它不擅长实时问答。Lemmalog 是离线归档加离线分析工具,不是聊天机器人。你问它“这个函数现在能不能改”,它给出的回答不会那么自然,但会给你一堆证据。
它也不是 RAG 数据库。虽然它能按文件、符号、提交去查询记忆,但它不会做语义向量检索。如果需要“语义相近的过去讨论”,我会先用关键词过滤,再拿结果去问 LLM。
它更不能解决模型幻觉。Lemmalog 只能把幻觉出现的地方标注出来,或者用结构约束降低幻觉影响。如果一个模型在原始会话里编造了一个根本不存在的接口,Lemmalog 提取出的记忆依然会包含这个假接口。但有了status: proposed和后续分析器的代码比对,你会更快发现这个假接口在源代码中找不到对应符号。
6.2 容易忽略的失败模式
我实际使用中遇到过几个隐藏问题。
同名文件在不同目录下会被混淆。处理办法是符号归一化时带上相对路径前缀,而不是只记文件名。
日志时间戳与代码提交时间不一致,会导致排序错误。比如某条记忆写的是“当前代码已经修复”,但它的来源是三个月前的日志。处理办法只有一个:以提交哈希为准,不凭日志时间推断顺序。
多语言项目符号切分规则不同。Python 的类方法、C++ 的命名空间、JavaScript 的导出函数,字符串形式差异很大。我的做法是先按语言拆分析器,不强行用一套解析逻辑。
LLM 输出 JSON 时 key 顺序不稳定,导致 diff 对比看起来像变化很大。入库前对 JSON 做字段排序后再比较,问题就没了。
一次性导入大量历史数据,可能让模型接口超时。不要今天导入一个月的数据。先导入一周,确认输出目录干净、失败重试正常,再继续追加。
6.3 从单人脚本到团队共同使用的建议
如果只是自己一个人用,Lemmalog 不需要太多工程化。一个 Python 脚本加一个inbox/目录就够了。
但如果要让团队使用,就要补几件事。
第一是敏感信息过滤。日志和聊天记录里经常出现密钥、内网地址、客户信息。Lemmalog 在入库前应该扫一遍敏感字段,命中的文件直接标记为excluded,不进入提取流程。
第二是并发写保护。两个人同时跑分析器,可能写入同一个索引文件。我的处理是在写索引前先加锁,或者干脆让分析器只读,不修改记忆库,只生成报告。
第三是元数据记录。每条记忆最好记一下“谁在什么场景下导入的”。这样等记忆库越来越多,你至少能判断哪些条目是你自己处理的,哪些是同事批量导入的,哪些是从旧日志里自动生成的。
7. 后续演进:从记忆提取到记忆驱动的代码审查
7.1 让 Lemmalog 从记录过去变成监督未来
Lemmalog 当前版本更像一面后视镜,告诉你历史发生过什么。下一步我准备加入“监督未来”的能力。
具体来说,就是当分析器扫描到一条记忆说“不要使用某个接口”时,Lemmalog 会沿着entities找到对应文件,扫描是否出现了该接口的新引用。如果出现,就生成一条审查建议。当某个函数的签名发生变化时,所有关联到该函数的记忆都应该被自动标记为“需要复核”。这样记忆库不再是一个静态存档,而会随着代码演进不断更新状态。
这个方向我还在做,但已经验证过最关键的一点:只要entities关联做得足够准,规则分析完全能承担这个任务。不需要频繁调用 LLM,更不需要把整个工程都交给模型。
7.2 落地顺序和投入节奏
如果你想复刻 Lemmalog 的思路,我不建议一开始就搭全套系统。
先从一个最小的闭环开始:选择一份旧会话记录,让模型按一个很简单的 schema 提取记忆,存成 JSON。然后写一个 20 行的 Python 检查器,看看这些记忆指向的文件是否还存在。再把这套流程跑两周,确认它能真正帮你找回“以前明确否定过的方案”。
等验证了价值,再逐步加入更多分析规则、更多输入源、更完整的索引结构。Lemmalog 可以变得很轻,也可以做得很重,但重点从来不是工具本身,而是让 LLM 的输出从“漂亮的文字”变成“可审查的数据”。
我也必须诚实说一句:这种方案解决不了所有问题。它不能代替人读代码,也不能保证每条历史结论都正确。它唯一能保证的是,当你需要回头核查时,你能找到一个带来源、带版本、带状态的结构化记录。这个记录比聊天记录可靠,比记忆可靠,也比“回头再搜一下”可靠。
如果你也经常被 LLM 的“前后矛盾”困扰,可以先从一份对话、一个 schema、一个检查脚本开始。把 LLM 的记忆变成程序分析的对象,这条思路比你想的要简单,也比你想象的更值得做。