1. 为什么 AI 写的代码总差那么点意思
用 Claude Code 写代码有一段时间之后,很多人会碰到同一个别扭的地方:功能实现了,逻辑也没错,但代码读起来就是"不像自己写的"。方法命名的习惯不一样,返回值的处理方式不同,注释的位置也不对。每次让 AI 改代码,都要再花时间把风格对齐,或者干脆忍着。
解决这个问题最直接的办法,是把编码规范写进CLAUDE.md,让 AI 每次都照着来。这个方向没错,但有个现实问题——规范是写不完的。能提前写出来的规范都是大原则,真正让代码"有个人味"的那些东西,是在一次次具体改动中体现出来的。比如你偏好把 null 判断前置而不是层层嵌套,比如你不喜欢在 Service 层里写 if/else 分支,比如你觉得方法名里的 do 前缀是多余的。这些东西事先想不到,只有在被触动的那一刻才知道。
所以真正的问题变成了:怎么把这些"被触动的瞬间"自动记录下来,让它们沉淀成规范,再反过来约束 AI 的输出?这篇就围绕 Claude Code 的 Hook 机制,给出一套可跟做的方案:用 Hook 在工具调用前后自动抓取改动,生成摘要归档,定期提炼进CLAUDE.md,形成一个"越写越像你"的反馈回路。适合已经在用 Claude Code、并且希望团队编码规范能持续沉淀的开发者。
2. 前置准备:Hook 机制与 TaoToken 接入
Claude Code 的 Hook 机制允许在特定事件点执行自定义脚本,这是整套方案的技术基础。常用的事件有四个:UserPromptSubmit(用户提交 prompt 时)、PreToolUse(工具调用前)、PostToolUse(工具调用后)、Stop(对话结束时)。脚本通过 stdin 接收 JSON 数据,包含session_id、tool_input、prompt等字段,正常退出码为 0。
在动手写脚本之前,先把模型调用通道准备好。Hook 里的摘要生成需要调用模型,我用的是 TaoToken 提供的接口,它兼容 Anthropic 的调用格式,配置起来比较省事。官网地址是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content= ,API 端点是 https://taotoken.net/api 。
拿到 API Key 的入口在这里:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。如果你还没配好环境,可以先在模型对话页面确认通道是否正常:https://taotoken.net/chat?utm_source=taotoken_aicg_blog_end&utm_content=model_chat&utm_campaign=rewrite 。长期跑编码任务、想让 Hook 稳定工作的,可以看下 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding_plan&utm_campaign=rewrite 。
环境变量建议这样设置,脚本里直接读:
export ANTHROPIC_BASE_URL="https://taotoken.net/api" export ANTHROPIC_API_KEY="你的_API_Key"注意:Hook 脚本里调用模型时,不要硬编码 Key,从环境变量读取,避免把密钥写进会被提交的文件。
3. 可复制配置:settings.json 与四个 Hook 脚本
整套系统由四个脚本组成,分别挂在四个 Hook 事件上,数据流是这样的:UserPromptSubmit检测关键词并初始化追踪轮次,PreToolUse在修改前拍快照,PostToolUse在修改后拍快照,Stop生成 diff、调模型摘要、写文件。
3.1 settings.json 配置骨架
Hook 配置写在~/.claude/settings.json的hooks字段下,支持 matcher 正则过滤触发的工具名:
{ "hooks": { "UserPromptSubmit": [ { "matcher": "", "hooks": [ { "type": "command", "command": "python3 ~/.claude/hooks/opt_track.py" } ] } ], "PreToolUse": [ { "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command", "command": "python3 ~/.claude/hooks/opt_capture_before.py" } ] } ], "PostToolUse": [ { "matcher": "Edit|Write|MultiEdit", "hooks": [ { "type": "command", "command": "python3 ~/.claude/hooks/opt_capture_after.py" } ] } ], "Stop": [ { "hooks": [ { "type": "command", "command": "python3 ~/.claude/hooks/opt_write_record.py" } ] } ] } }3.2 opt_track.py:关键词触发追踪
并不是每一轮对话都值得记录,只抓"有意识地让 AI 改代码"的时刻,用关键词做过滤:
import json, sys, os, time KEYWORDS = ['优化', '重构', '改进'] def main(): data = json.load(sys.stdin) session_id = data.get('session_id', 'default') prompt = data.get('prompt', '') session_dir = f'/tmp/claude-opt-{session_id}' os.makedirs(session_dir, exist_ok=True) index_file = os.path.join(session_dir, 'index') idx = int(open(index_file).read()) if os.path.exists(index_file) else 0 if any(kw in prompt for kw in KEYWORDS): idx += 1 open(index_file, 'w').write(str(idx)) os.makedirs(os.path.join(session_dir, 'changes', str(idx)), exist_ok=True) with open(os.path.join(session_dir, f'prompt-{idx}.txt'), 'w') as f: f.write(prompt) elif idx > 0: with open(os.path.join(session_dir, f'steps-{idx}.txt'), 'a') as f: f.write(prompt + '\n') sys.exit(0) main()命中关键词时创建新的追踪轮次,记录 prompt 和当前工作目录;没命中但当前有活跃追踪的,把这条 prompt 追加为"执行步骤",方便后续理解完整上下文。所有中间状态写在/tmp/claude-opt-{session_id}/里,按 session 隔离。
3.3 opt_capture_before.py:修改前快照
每次 AI 调用 Edit/Write/MultiEdit 前,把文件当前内容备份下来:
import json, sys, os, shutil def main(): data = json.load(sys.stdin) session_id = data.get('session_id', 'default') session_dir = f'/tmp/claude-opt-{session_id}' index_file = os.path.join(session_dir, 'index') if not os.path.exists(index_file): sys.exit(0) # 没有活跃追踪,直接放行 idx = open(index_file).read().strip() tool_input = data.get('tool_input', {}) file_path = tool_input.get('file_path') if not file_path or not os.path.exists(file_path): sys.exit(0) safe_name = file_path.replace('/', '_') before = os.path.join(session_dir, 'changes', idx, f'{safe_name}.before') if not os.path.exists(before): # 每个文件每轮只拍一次 shutil.copy2(file_path, before) sys.exit(0) main()有个细节值得强调:每个文件每轮只拍一次。如果 AI 对同一个文件做了多次修改,只保留最原始的版本,这样生成的 diff 才能反映完整的变化量。
3.4 opt_capture_after.py:修改后快照
每次修改完,把文件内容备份到.after文件,这里是每次都覆盖,保留最终状态:
import json, sys, os, shutil def main(): data = json.load(sys.stdin) session_id = data.get('session_id', 'default') session_dir = f'/tmp/claude-opt-{session_id}' index_file = os.path.join(session_dir, 'index') if not os.path.exists(index_file): sys.exit(0) idx = open(index_file).read().strip() tool_input = data.get('tool_input', {}) file_path = tool_input.get('file_path') if not file_path or not os.path.exists(file_path): sys.exit(0) safe_name = file_path.replace('/', '_') after = os.path.join(session_dir, 'changes', idx, f'{safe_name}.after') shutil.copy2(file_path, after) sys.exit(0) main()before 和 after 凑成一对,就能算出本轮的完整 diff。
3.5 opt_write_record.py:生成记录
对话结束时触发,做三件事:生成 diff、调模型生成摘要、写入 Markdown 文件。
import json, sys, os, difflib, datetime, urllib.request def gen_diff(before_path, after_path): before = open(before_path).readlines() after = open(after_path).readlines() return ''.join(difflib.unified_diff( before, after, fromfile='修改前', tofile='修改后', n=3)) def summarize(context): payload = json.dumps({ "model": "claude-haiku-4-5", "max_tokens": 300, "messages": [{"role": "user", "content": "以下是一次代码优化会话的内容,请用 2~4 句话总结:" "核心改动是什么,体现了哪些代码设计原则或最佳实践。\n\n" + context + "\n\n只输出总结内容,不要其他说明。"}] }).encode() req = urllib.request.Request( os.environ['ANTHROPIC_BASE_URL'] + '/v1/messages', data=payload, headers={ 'content-type': 'application/json', 'x-api-key': os.environ['ANTHROPIC_API_KEY'], 'anthropic-version': '2023-06-01' }) resp = json.loads(urllib.request.urlopen(req).read()) return resp['content'][0]['text'] def main(): data = json.load(sys.stdin) session_id = data.get('session_id', 'default') session_dir = f'/tmp/claude-opt-{session_id}' index_file = os.path.join(session_dir, 'index') if not os.path.exists(index_file): sys.exit(0) idx = open(index_file).read().strip() changes_dir = os.path.join(session_dir, 'changes', idx) if not os.path.isdir(changes_dir): sys.exit(0) now = datetime.datetime.now() out_dir = os.path.join('代码规范积累', now.strftime('%Y-%m')) os.makedirs(out_dir, exist_ok=True) out_file = os.path.join(out_dir, now.strftime('%Y%m%d-%H%M%S') + '.md') blocks = [] for name in os.listdir(changes_dir): if name.endswith('.before'): base = name[:-7] after = os.path.join(changes_dir, base + '.after') if os.path.exists(after): diff = gen_diff(os.path.join(changes_dir, name), after) blocks.append(f"### 文件:`{base}`\n\n```diff\n{diff}\n```") context = '\n\n'.join(blocks) summary = summarize(context) if context else '本轮无有效改动' with open(out_file, 'w') as f: f.write(f"# 代码优化记录 {now.strftime('%Y-%m-%d %H:%M:%S')}\n\n") f.write(f"## 摘要\n\n{summary}\n\n---\n\n") f.write(context) sys.exit(0) main()用 Haiku 而不是主模型,是为了省成本——摘要任务不需要很强的模型。归档目录按月份组织:
代码规范积累/ └── 2026-04/ ├── 20260401-143022.md ├── 20260415-090531.md └── 20260428-165847.md4. 验证请求:一次触发与结果确认
配置好之后,需要验证 Hook 是否真的生效。下面走一遍完整流程。
第一步,确认脚本可执行、路径正确:
chmod +x ~/.claude/hooks/*.py ls -la ~/.claude/hooks/第二步,在 Claude Code 里提交一个带关键词的 prompt,比如"帮我重构一下 UserService,把 null 判断统一提到方法入口"。这一步会触发UserPromptSubmit,opt_track.py创建追踪轮次。
第三步,让 AI 实际修改文件。此时PreToolUse和PostToolUse会分别拍下 before 和 after 快照。检查临时目录:
ls /tmp/claude-opt-*/changes/1/ # 期望看到类似: # src_main_java_UserService.java.before # src_main_java_UserService.java.after第四步,结束对话,触发Stop事件。检查归档目录:
ls 代码规范积累/2026-04/ cat 代码规范积累/2026-04/20260428-165847.md期望看到的内容结构大致是:
# 代码优化记录 2026-04-28 16:58:47 ## 摘要 本次优化将 Service 层中散落的 null 判断统一前置,避免了深层嵌套。 提取了重复的校验逻辑为私有方法,体现了单一职责原则。 --- ### 文件:`src_main_java_UserService.java` ```diff - if (user != null) { - if (user.getStatus() != null) { + Objects.requireNonNull(user, "user must not be null"); + if (user.getStatus() == null) {如果摘要和 diff 都正常出现,说明 Hook 生效了。接下来把提炼出的规则写进 `CLAUDE.md`,让后续会话读取。`CLAUDE.md` 放在项目根目录,写法上建议按"规则 + 反例"组织: ```markdown ## 编码规范 ### 空值处理 - 方法入口统一用 Objects.requireNonNull 做前置校验 - 禁止多层嵌套的 null 判断,优先提前返回 ### 命名 - 方法名不加 do 前缀 - Service 层方法名体现业务动作,不体现技术实现 ### 分层 - Service 层不写 if/else 分支,分支逻辑下沉到策略类这样下一次会话开始时,Claude Code 会自动读取CLAUDE.md,AI 的初稿就会向这些规则靠拢。整个循环跑起来之后,规范会越来越准确,需要手动对齐的次数自然减少。
5. 本篇常见错排查
5.1 Hook 没触发,临时目录是空的
先确认settings.json的 JSON 格式合法,可以用python3 -m json.tool ~/.claude/settings.json校验。再确认脚本路径是绝对路径或~展开正确,Hook 执行时的工作目录不一定是你以为的那个。另外检查脚本是否有可执行权限。
5.2 PreToolUse 脚本报错阻断了工具调用
PreToolUse的脚本若返回非 0,会阻断当次工具调用。所以opt_capture_before.py在找不到 session 追踪时直接sys.exit(0)而不报错。如果你自己改脚本,务必保证所有异常分支都exit(0),否则 AI 会改不动文件。
5.3 摘要生成失败或超时
多半是环境变量没读到。Hook 执行时的环境和你终端里的环境可能不同,建议在脚本里显式读取,或者把变量写进 shell 的启动文件。另外确认ANTHROPIC_BASE_URL指向的是 https://taotoken.net/api ,不要带多余路径。如果一直失败,可以先在模型对话页面单独测一下通道是否通。
5.4 diff 是空的
检查 before 和 after 文件是否成对存在。常见原因是PostToolUse的 matcher 没覆盖到实际使用的工具名,比如你用的是MultiEdit但 matcher 里漏了。matcher 是正则,Edit|Write|MultiEdit这种写法要确认没有拼写错误。
5.5 关键词过滤漏记录
用"优化/重构/改进"触发追踪,会漏掉一些有价值的改动。比如直接说"把这个方法改一下",就不会被记录。后续可以考虑换成"所有有文件修改的对话都记录",让整理阶段来做筛选,而不是记录阶段。这个取舍看你对记录量的容忍度。
6. 把规范沉淀变成被动发生的事
这套方案的本质是个反馈回路:用 AI 改代码,自动记录,人工提炼,写进规范,AI 再按规范写代码。规范的准确性会随着时间提升,因为它是从真实行为里归纳出来的,而不是提前猜测的。
我大概每两周做一次整理:翻看代码规范积累/下最近的文件,找出多次出现的同类改动,这意味着这是我真实的习惯,然后提炼成一条规则写进代码风格.md,再同步到工作项目的CLAUDE.md里作为 AI 的编码约束。
如果你也在用 Claude Code,可以把这几个脚本直接拿去用,按自己的关键词触发逻辑和归档路径改一改就行。接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite ,控制台在 https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_content=console&utm_campaign=rewrite ,需要管理密钥的去 API Keys 页面:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api_keys&utm_campaign=rewrite 。跑通之后你会发现,记录比想象中多,原来在无意识的情况下,已经在持续地用 AI 做小规模的代码调整了。