用本地模型解读Java旧代码并生成重构差异
Exorcist结合Git历史和本地Ollama模型,在终端解释Java类、标注代码异味,并引用相关代码行。重构建议以统一差异格式输出,不直接修改文件,代码处理留在本机。
AI 深度解读
让老代码的作者「回来」解释设计、承认坏味道,再给出重构 diff,这是 Exorcist 的玩法。它从 Git 历史中找出 Java 类的主要贡献者,让本地模型扮演对方,在终端里讲代码。
但值得读的不是这个万圣节包装,而是作者如何约束一个会编故事的 7B 模型:先用 Git 和静态分析提供事实,再检查引用、替换范围和语法。实测也暴露了边界:行号可以全对,解释照样会错;文件能解析,重构照样可能无法编译。
让作者开口,靠的是 Git 证据
Exorcist 是一个基于 Java 21、Spring Boot、Spring Shell、Spring AI、JGit 和 JavaParser 的终端应用。默认模型为 qwen2.5-coder:7b,通过 Ollama 在本机运行。
工作流分三步:summon 扫描仓库,possess 选择一个类并解释,confess 列出代码问题;需要重构时再执行 exorcise。
作者拿 JUnit 4 做了演示:仓库从 2000 年 12 月起有 2,521 次提交,扫描历史不到五秒,找到 450 个 Java 类。随后选中的 junit.runner.BaseTestRunner 有 326 行、30 次提交和 10 个 catch 块。
这些数字描述的是作者的演示环境,原文没有提供硬件配置,也没有跨仓库性能对照。
「主要作者」的口径,是对该文件提交次数最多的人。扫描跳过 merge,追踪文件重命名;同一个人在多年间更换姓名和邮箱,则通过并查集归并身份。并查集可以理解成一套传递关系:两个身份分别与第三个身份对应,就能归到同一人。
JUnit 中的 dsaff <dsaff> 和 David Saff <saff@google.com>,正是通过 dsaff <david@saff.net> 连起来的。工具同时避开 noreply@github.com 这类共享占位邮箱,防止把陌生人合并。git blame 再补充哪些行最后由其他人修改。
这比直接让模型猜历史可靠,但「提交最多」仍不等于最了解设计的人。演示中,文件最后修改日期是 2016-10-15,模型扮演的作者却说自己最后修改于 2004-10-24。这可能是文件时间和个人贡献时间的区别,阅读时需要分清,不能把角色独白当成真实访谈。
行号和静态扫描,能把回答约束到哪一步
作者认为,角色口吻只需要一段系统提示词,真正困难的是阻止模型自信地乱说。
Exorcist 先给源码加行号,要求每条判断引用位置,再检查回答中的引用数量、行号是否越界,以及编号条目是否缺少引用。演示里的 [12 line citations, all inside the file] 是程序检查结果,干净时显示绿色,有问题时显示黄色。
第二层约束来自 JavaParser。模型开口前,静态扫描已经找出空 catch、宽泛 catch、printStackTrace()、System.out、深层嵌套、长方法、公开可变字段、Vector / Hashtable、@Deprecated 和 TODO,并附上准确行号。
模型的任务是结合证据解释问题,而非从零寻找一切。
在一次 confess 演示中,模型列出六项问题,包括长达 48 行的 getTest()、吞掉异常的 catch,以及直接使用 System.out.println()。作者称自己逐项核对,六项都真实存在,行号也正确。
其中一段代码足以说明为什么仍要看原文:
} catch (Exception IOException) {
return stack; // return the stack unfiltered
}
要看清第一行:捕获的类型是 Exception,IOException 只是变量名。名字像某种异常,不代表实际捕获的就是它。作者说这段代码从 2000 年起就存在于 JUnit 中。
然而,作者另一次初始运行曾在两个条目里虚构 IOException,还重复报告同一个问题。当时引用的行号也是对的。
引用检查证明的是回答可以定位,不能证明回答符合那几行代码的含义。
静态命中也需要语境。宽泛 catch 可能隐藏错误,也可能处于刻意设计的兼容边界;检测到它,距离判定「应该怎么改」还有一步。
重构 diff 由程序生成,仍过不了编译这一关
exorcise 没有要求小模型直接输出 patch。作者发现,小模型容易生成损坏的补丁,于是把任务拆开。
工具先选择一个静态扫描确认的问题,再扩大到能够完整修改它的最小代码范围,例如整个 try 语句、单条语句或整个方法。随后要求模型输出 REPLACE LINES a-b 和代码块。
Exorcist 负责拼接替换内容、修正缩进、检查文件是否仍能解析,最后用 JGit 计算 diff。它不会把修改写入磁盘。
这里的分工很实用:模型负责建议代码,行范围、文本拼接和差异计算交给确定性程序。长文件还会裁成相关行附近的窗口,并明确告诉模型哪些内容不可见,以适应小模型的上下文容量。
问题在于,语法合法远远不够。
作者第一次 exorcise 就生成了无法编译的修改:它删掉失败分支后的 return null,还捕获了 try 内代码根本不会抛出的异常。Exorcist 当时只检查解析,作者计划下一步结合项目 classpath 编译替换后的文件。
换成 gemma3:4b 后,情况更差。它把三行方法说成臃肿,试图把整个方法塞进一个 13 行的替换范围;这次工具发现文件无法解析,拦住了结果。它还攻击真实维护者的编程能力,促使作者增加第五条提示规则:评价代码,不评价人。
项目有 98 个测试,不需要模型或网络。作者使用 Claude Code 结对开发,写测试时找出四个真实缺陷,其中一个会导致模型被要求重写自己根本没见过的行。
这些测试能检查工具自身的约束逻辑,却不能替每一次生成结果担保。
我的判断
我认可这里的工程顺序:先收集可验证事实,再让模型解释,最后检查输出。对旧系统维护而言,带位置的错误通常比无法追溯的漂亮解释更容易发现和纠正。
但「让作者说话」适合作为交互包装,不适合作为可信度来源。模型没有访问作者当年的想法,Git 也只能记录修改,不能完整记录决策动机。角色越逼真,读者越需要区分史料和推测。
与常规静态分析相比,Exorcist 增加了自然语言解释和修改建议;与直接把整个类交给聊天模型相比,它增加了事实输入与程序校验。原文没有给出准确率、误报率或维护耗时的对照数据,因此「更有效」目前还是经验判断。qwen2.5-coder:7b 与 gemma3:4b 的表现差异,也不足以单独证明参数规模决定可靠性。
本地运行的价值更明确:按原文描述,仓库和模型都在同一台机器上,模型下载完成后可离线使用,不需要 API key 或账户,也没有按次提问费用。不过仍有硬件、内存、电力和等待成本。原文没给出这些数据,也没给出生成速度。
至于「开放模型才方便比较」的说法,我会保留意见。同样的提示也可以用于比较不同托管 API;本地方案更直接的优势,是源码处理位置和运行环境由使用者控制。
能拿来做什么
- 接手有完整 Git 历史的 Java 项目时,可以先用它梳理类的职责、贡献记录和异常处理位置,再回到引用行核对。前端或全栈开发者排查 Java 服务时,也能借此降低第一次阅读的门槛。
- 在日常 AI 编程工具里,可以复用「带行号的输入、明确的可见范围、可检查的输出」这套做法。如果处理 Node 或前端代码,需要另配相应解析器和规则,原文没有证明 Exorcist 支持这些语言。
- 把生成 diff 视为待审建议。用于实际重构前,除了语法检查,还应跑项目编译和相关测试,重点检查返回路径、异常类型与行为变化。
- 如果代码需要留在本机,可以评估 Ollama 的本地路线。更换模型时,用
EXORCIST_MODEL=gemma3:4b这样的配置在相同代码上比较,并记录错误类型,别只比较回答是否流畅。