Lemmalog:将LLM碎片化记忆转化为可追踪的程序分析数据

发布时间:2026/9/1 17:14:54
Lemmalog:将LLM碎片化记忆转化为可追踪的程序分析数据
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 里反复调过的核心参数。参数推荐初始值说明temperature0 到 0.1越低越稳定适合归档类任务context_window4000 到 8000 字符控制单次输入长度太长容易丢开头max_tokens1500 以上太短会导致 JSON 截断解析失败retry1 到 2 次解析失败时自动重试但不建议无限重试batch_size1 开始再逐步增加并发太高会让单条质量下降还容易触发限流如果发现返回结果是合法 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 的记忆变成程序分析的对象这条思路比你想的要简单也比你想象的更值得做。