
git blame能告诉你谁、何时、改了什么。whyso 告诉你为什么改。
问题
想知道一行代码为什么长这个样子,我们能做的无非是看 git blame 和提交记录。
$ git blame internal/handler/page.go
a3f1b2c (parkjunwoo 2026-03-08) func CreatePage(c *gin.Context) {
谁改的、什么时候改的、改了什么——都有。为什么改——没有。
“在提交信息里写原因不就行了?“现实是这样的:
fix: update handler
refactor: clean up
wip
写好提交信息是一种自律。不管团队约定定得多严,凌晨三点修 bug 的时候,很少有人会想着"让我认真把这次变更的背景和理由记录在提交信息里”。
但在 AI 编程时代,情况不同了。变更的理由已经被记录下来了。
Claude Code 的会话数据
Claude Code 把所有对话以 JSONL 文件的形式保存在 ~/.claude/projects/ 下。每条记录包含用户消息、AI 回复、tool_use(Write、Edit、Bash)调用,以及将它们串联起来的 parentUuid 链。
用户:"开始实现 whyso validate 命令"
→ AI:Write internal/crosscheck/crosscheck.go
→ AI:Edit internal/crosscheck/crosscheck.go
→ AI:"初始化 crosscheck 包 — 验证 SSaC↔OpenAPI operationId 匹配"
文件为什么被创建、为什么被修改,都留在对话本身里。比提交信息更丰富,也不需要刻意去写。只要进行了对话,就会自动留下记录。
whyso 解析这些数据,提取每个文件的变更历史。
看见文件的一生
file: internal/crosscheck/crosscheck.go
created: 2026-03-08T14:23:01Z
history:
- timestamp: 2026-03-08T14:23:01Z
session: 09351222-d7be-41fe-994f-87c2d7067e5d
user_request: "开始实现 whyso validate 命令"
answer: "初始化 crosscheck 包 — 验证 SSaC↔OpenAPI operationId 匹配"
tool: Write
- timestamp: 2026-03-09T10:15:33Z
session: 4e9b4e5e-3a50-43f2-be6e-e5db228ecc3b
user_request: "加上验证 x-sort 列是否存在于 DDL 中"
answer: "新增 x-sort/x-filter 列 → DDL 交叉验证"
tool: Edit
- timestamp: 2026-03-10T09:41:22Z
session: b2e43b4f-cb21-4286-975d-1eb9de8a16c0
user_request: "也加上 Func spec 的交叉验证"
answer: "新增 @call ↔ Func spec 参数数量/类型交叉验证"
tool: Edit
为什么被创建、经历了哪些请求、如何演化——一个文件的完整一生一目了然。
为什么需要它
代码审查的空白
在审查 PR 时,总会遇到"这个改动为什么要做?“的时刻。提交信息里没有原因,就得去问作者。作者记不清了,就得重新读代码来推断。
在 AI 编程环境下,这个问题的答案就在会话数据里。whyso 把那个答案取出来。
入职培训
新加入团队的成员理解代码库最快的方式,就是知道"这段代码为什么变成现在这样”。git log 是变更的时间线,文档是当前状态的快照。whyso 的文件级历史展示了两者之间的那部分——意图的链条。
自我记录
就算是独立开发,也会遇到三个月前为什么这样写已经想不起来的时候。whyso 能还原过去的自己和 AI 之间的对话。提交信息里没写的上下文,就在那里。
与 git blame 的关系
whyso 不是要替代 git blame,而是补充它。
| git blame | whyso | |
|---|---|---|
| 谁 | O | — |
| 何时 | O | O |
| 改了什么 | O(行级别) | O(tool_use 级别) |
| 为什么改 | △(提交信息) | O(用户请求 + AI 说明) |
| 数据来源 | git 历史 | Claude Code 会话 |
如果说 git blame 是基于提交的"正式记录”,那 whyso 就是基于对话的"工作日志"。正式记录里没写进去的上下文,留在了工作日志里。
AI 读取自己记录的结构
whyso 最有趣的使用场景,是AI 自己读取,而不是人。
Claude Code 在会话结束后会忘掉一切。下一个会话打开同一个项目,昨天为什么那样写、考虑过哪些方案又放弃了、用户在什么背景下提出的请求——全都没了。只能靠读代码来推断。
想象一下在 CLAUDE.md 里加一行:
修改文件前务必先执行 `whyso history <file>`。如有历史记录,读完再修改。
这一行改变了什么:
知道哪些变更不能回退
打算修改某个文件,读着读着发现有段代码看起来很奇怪。光看当前状态会觉得"是不是写错了?“想改掉。但如果 whyso 历史里写着"用户明确要求这个结构,理由不是性能而是可读性”,就不会去动它。
不再重新提议已经放弃的方案
上一个会话里有过"试试 A 方案?"→“不,用 B"的对话,新会话的 AI 不知道这件事,于是又提议 A。对用户来说很烦。有了 whyso,就能知道哪个方向已经讨论过并被否定了。
接续连贯的工作上下文
说"帮我继续昨天的重构”,现在只能看 diff 来推断。有了 whyso 历史,就能直接读到昨天会话里的用户请求和 AI 的判断依据。不再是推断,而是基于事实的连续作业。
记录跨会话累积
有了这个结构,whyso 的历史会随着会话的积累而不断增长。这个会话里修改的原因,也会传递给下一个会话的 AI。会话间没有记忆的问题,以文件为单位得到了解决。不需要额外的记忆系统。
whyso 里记录的是 AI 做的事。但 AI 不记得那些事。自己创建的记录,对自己来说最有用的时刻。 这就是 whyso 与 AI 编程工具结合时独特的价值所在。
对人来说,whyso 是"自动留下好提交信息的工具"。对 AI 来说,whyso 是跨越会话的记忆。
使用方法
安装
go install github.com/park-jun-woo/whyso/cmd/whyso@latest
查询文件历史
# 单个文件
whyso history README.md
# 整个目录
whyso history internal/ --all
# 镜像文件结构输出
whyso history . --all --output .file-histories/
# JSON 格式
whyso history README.md --format json
查看会话列表
whyso sessions
工作原理
parentUuid 链回溯
JSONL 中的每条记录都通过 uuid 和 parentUuid 相互关联。从 AI 修改文件的 tool_use 开始,沿 parentUuid 链向上追溯,就能到达触发这次修改的用户最初请求。
user message (uuid: A) ← "加上验证 x-sort 列是否存在于 DDL 中"
→ assistant tool_use (parentUuid: A)
→ tool_result (parentUuid: B)
→ assistant Edit (parentUuid: C) ← 这次修改的原因 = A
中间夹着 tool_result 类型的消息,因此只要提取 type == "user" 且 content 为字符串的记录,就能找到真正的用户请求。
设计决策
只追踪 Write/Edit。 Claude Code 的 Write 和 Edit tool_use 明确包含文件路径和变更内容。Bash 命令的文件操作(rm、mv、cp)通过启发式方法检测,但 Write/Edit 是主要追踪对象。Claude Code 在修改文件时被设计为使用 Write/Edit,因此仅凭这两个工具就能捕获大部分变更。
包含子代理。 Claude Code 处理复杂任务时有时会创建子代理。子代理的会话保存在父会话目录下。whyso 也会解析这些子代理会话,构建完整的历史。
增量更新。 使用 --output 选项输出到目录时,已经解析过的会话会被跳过。即使会话增长到数百个,也不会每次都重新全量解析。
数据
用 whyso 追踪 whyso 项目自身的开发过程,结果如下:
| 项目 | 数值 |
|---|---|
| 会话数 | 17 |
| Write 调用 | 660 |
| Edit 调用 | 1,415 |
| Bash 文件操作 | 206 |
| 变更的唯一文件数 | 480 |
17 个会话中,提取出了 480 个文件的变更历史。每个文件都记录着"为什么被创建、为什么被修改"。
局限性
- 仅支持 Claude Code。 其他 AI 编程工具的会话数据格式不同。目前只支持 Claude Code 的 JSONL 格式。
- 只追踪文本文件。 Write/Edit tool_use 针对的是文本文件。图片、二进制文件的变更不会被追踪。
- 需要本地存在会话数据。
~/.claude/projects/下必须有会话文件。如果已删除或在其他机器上操作,那部分记录就不存在了。
总结
在与 AI 协作写代码的时代,变更的原因不再只能依赖提交信息。对话本身就是记录,可以从那份记录中自动提取每个文件的变更历史。
如果说 git blame 展示的是"谁、何时、改了什么",那 whyso 展示的就是"为什么改"。