实战日志:用 Claude 交付生产代码的最佳实践
开发者分享用 Claude 完整开发生产项目的经验和教训。热议案例(354 points,99 条讨论)。
开发者分享用 Claude 完整开发生产项目的经验和教训。热议案例(354 points,99 条讨论)。
Shimmering Substance - Jackson Pollock
注:这篇文章配有 NotebookLM 播客(脚注 1 见下方),以及三个生成的音频录音。
你可以阅读我在准备这篇文章的草稿时与 ChatGPT 的对话。
相关 HN 帖子上的评论和讨论。
把这篇文章看作一份新软件构建方式的实战指南。读完之后,你不仅会理解 AI 辅助开发的方式是什么,还会理解为什么这些方式真正有效。
首先,我们将探讨如何真正实现 10 倍的生产力提升——不是通过魔法,而是通过有意识的实践来放大 AI 的优势,同时弥补它的不足。
接下来,我将带你了解我们在 Julep 使用的基础设施,用 Claude 的帮助每天交付生产代码。你将看到我们的 CLAUDE.md 模板、提交策略和护栏。
最重要的是,你将理解为什么编写自己的测试仍然是绝对神圣的,即使(特别是)在 AI 时代也是如此。这一个原则将拯救你于无数个午夜的调试时刻。
这是主要的洞察:好的开发实践不仅仅是锦上添花——它们是 AI 放大你的能力与陷入混乱之间的区别。研究证实了这一点。²使用严格实践的团队部署频率高 46 倍,从提交到部署的速度快 440 倍。当你将强大的 AI 助手加入其中时,这种效应会更加明显。
让我带你回到这一切开始的时候。³Andrej Karpathy ⁴发推文讨论"凭感觉编程"——这个想法是让 AI 写你的代码,而你只是凭感觉。开发者社区为此大笑了一番。这听起来像是开发者的终极幻想:放松,喝咖啡,让机器做工作。
凭感觉编程的诞生
随后 Anthropic 发布了 Sonnet 3.7 和 Claude Code,发生了意想不到的事情。这个笑话不再好笑了,因为它开始变成……可能的了?当然,我们信任的朋友 Cursor 已经出现一段时间了,但这个新界面终于感觉像真正的凭感觉编程。
在 Julep,我们构建 AI 工作流编排。我们的后端积累了多年的决策、模式和一些技术债。我们竭尽全力保持代码质量高,为自己准备了充分的文档。然而,仅是代码的庞大规模和为什么代码的不同部分以这种方式组织的历史背景,就需要一个优秀的工程师花费数周才能理解。
如果在使用 Claude 时没有适当的护栏,你基本上是在和一个过度热情的实习生玩打地鼠。
⁵Steve Yegge 在一篇标题略显戏剧化的文章《初级开发者的死亡》中巧妙地创造了术语 CHOP——面向聊天的编程。这是对使用 Claude 编码的完美、不废话的描述。
把传统编码想象成雕刻大理石。你从一块空白的石头开始,仔细地凿,一行一行,一个函数一个函数。每一刀都是深思熟虑的,每一个决定都是你的。这很令人满足但很缓慢。
凭感觉编程更像指挥一个管弦乐队。你不是在弹每一件乐器——你在指挥、塑造、引导。AI 提供了原始的音乐才能,但没有你的眼光,它只是噪音。
在凭感觉编程时,你可以采取三种不同的姿态,每一种都适合开发周期的不同阶段:
AI 作为初稿作者: 在这里,AI 生成初始实现,而你专注于架构和设计。这就像有一个可以以思想的速度打字但需要不断指导的初级开发者。非常适合样板代码、CRUD 操作和标准模式。
AI 作为初稿作者: 在这里,AI 生成初始实现,而你专注于架构和设计。这就像有一个可以以思想的速度打字但需要不断指导的初级开发者。非常适合样板代码、CRUD 操作和标准模式。
AI 作为结对编程者: 这是大多数开发的甜蜜点。你积极合作,来回碰撞想法。AI 建议方法,你改进它们。你勾勒轮廓,AI 填充细节。这就像与一个读过所有编程书籍但从未实际发布过代码的人进行结对编程。
AI 作为结对编程者: 这是大多数开发的甜蜜点。你积极合作,来回碰撞想法。AI 建议方法,你改进它们。你勾勒轮廓,AI 填充细节。这就像与一个读过所有编程书籍但从未实际发布过代码的人进行结对编程。
AI 作为验证者: 有时你写了代码,想要进行理智检查。AI 审查错误、建议改进、发现你可能遗漏的模式。把它看作一个极其博学的代码审查者,永远不会疲倦或脾气暴躁。
AI 作为验证者: 有时你写了代码,想要进行理智检查。AI 审查错误、建议改进、发现你可能遗漏的模式。把它看作一个极其博学的代码审查者,永远不会疲倦或脾气暴躁。
与其逐行编写,不如进行审查、改进、指导。但是——这一点怎么强调都不过分——你仍然是架构师。Claude 是你的实习生,有百科全书式的知识,但对你的具体系统、用户和业务逻辑一无所知。
经过数月的实验和几次生产事故后,我确定了三种不同的操作模式。每一种都有自己的节奏、自己的护栏和自己的用例。
何时使用: 周末黑客行为、个人脚本、概念验证以及那些"我想知道是否……"的时刻,这些时刻让编程变得有趣。
在游乐场模式下,你拥抱混乱。Claude 写 80-90% 的代码,而你只是提供足够的指导来保持事情正常进行。这既令人放松,也略显恐怖。专业提示:查看 claude-composer 来进入完全的 YOLO 模式。
这是游乐场模式的样子:你有一个想法,想要一个脚本来分析你的 Spotify 历史。你打开 Claude,用普通英文描述你想要什么,然后看着它生成一个完整的解决方案。没有 CLAUDE.md 文件,没有仔细的提示——只是原始的、未经过滤的 AI 编写的代码。
游乐场模式的美妙之处在于它的速度。你可以在几分钟内从想法变成工作原型。危险在于,这种牛仔编程风格对于任何重要的事情都是绝对不合适的。将其用于实验,永远不要用于生产。相信我,尽管有些了不起的人鼓吹相反的观点,良好的工程原则现在比以往任何时候都更重要。
何时使用: 代码行数不足约 5,000 行的项目、有真实用户的附带项目、演示(你不想破坏)或大型系统中定义良好的小服务。
这是凭感觉编程开始闪闪发光的地方。你需要结构,但不要太多,以至于放慢你的速度。这里的关键创新是 CLAUDE.md 文件——Claude 在被调用时自动将其纳入上下文的自定义文档。来自 Anthropic 的 Claude Code 最佳实践:
CLAUDE.md 是一个特殊文件,Claude 在开始对话时自动将其拉入上下文:
与其反复解释你的项目约定,不如文档化一次。这是来自最近一个附带项目的真实示例:
## 项目:Analytics 仪表板
这是一个用于可视化用户分析的 Next.js 仪表板:
### 架构决策
- 默认使用服务器组件,仅在必要时使用客户端组件
- 使用 tRPC 进行类型安全的 API 调用
- 使用 Prisma 进行数据库访问,使用显式 select 语句
- 使用 Tailwind 进行样式设计(没有自定义 CSS 文件)
### 代码风格
- 格式化:Prettier,100 字符行
- 导入:使用 simple-import-sort 排序
- 组件:Pascal 命名法,与测试共同定位
- 钩子:始终以 'use' 开头
### 要遵循的模式
- 数据获取发生在服务器组件中
- 客户端组件接收数据作为 props
- 对所有外部数据使用 Zod schemas
- 在每个数据显示组件周围有错误边界
### 不要做什么
- 不要为数据获取使用 useEffect
- 不要在没有明确批准的情况下创建全局状态
- 不要用 'any' 类型绕过 TypeScript
有了这个背景,Claude 变得非常有效。这就像每天向新员工解释你的项目和让他们读一次入职文档之间的区别。
但是结对编程模式需要不仅仅是文档。你需要用我称之为"锚点注释"——防止 Claude 游荡到荒野的面包屑来主动引导 AI:
// AIDEV-NOTE: This component uses virtual scrolling for performance
// See: https://tanstack.com/virtual/latest
// Don't convert to regular mapping—we handle 10k+ items
export function DataTable({ items }: DataTableProps) {
// Claude, when you edit this, maintain the virtual scrolling
...
}
这些注释有双重作用:既能指导 AI,也能为人类记录代码信息。这种文档能同时为双方带来长期收益。此类“锚点注释”与普通注释的关键区别在于:它们是专门为 Claude 编写和维护的,也应该由 Claude 自身使用。下面是我们项目中 CLAUDE.md 的一段真实内容:
## Anchor comments
Add specially formatted comments throughout the codebase, where appropriate, for yourself as inline knowledge that can be easily `grep`ped for.
### Guidelines:
- Use `AIDEV-NOTE:`, `AIDEV-TODO:`, or `AIDEV-QUESTION:` (all-caps prefix) for comments aimed at AI and developers.
- Keep them concise (≤ 120 chars).
- **Important:** Before scanning files, always first try to **locate existing anchors** `AIDEV-*` in relevant subdirectories.
- **Update relevant anchors** when modifying associated code.
- **Do not remove `AIDEV-NOTE`s** without explicit human instruction.
Example:
# AIDEV-NOTE: perf-hot-path; avoid extra allocations (see ADR-24)
async def render_feed(...):
...
适用场景:大型代码库、拥有真实用户的系统,以及任何出现缺陷便会造成金钱或声誉损失的项目。
Claude 可以生成海量代码,但要将这些代码集成到复杂系统中,就需要精心协调。
首先我要强调一个很大的限制:在这种规模下,凭感觉编程目前的扩展性并不好。我确实看到这些系统在处理大型代码库方面取得了显著进步,但要让它们真正发挥作用,仍需投入大量精力来帮助它们浏览、理解代码库,并在不迷失于迷宫的情况下安全地修改代码。一般来说,最好尽可能将代码库拆分成独立的服务和子模块。
一条普遍适用的原则是:无论是否采用凭感觉编程,良好的工程实践都适用于大型项目。例如,在生产环境规模下,边界至关重要。每一个集成点都需要明确的文档:
# AIDEV-NOTE: API Contract Boundary - v2.3.1
# ANY changes require version bump and migration plan
# See: docs/api-versioning.md
@router.get("/users/{user_id}/feed")
async def get_user_feed(user_id: UUID) -> FeedResponse:
# Claude: the response shape here is sacred
# Changes break real apps in production
...
如果没有这些边界,Claude 会很乐意“改进”你的 API,然后让生产环境中的每个客户端都崩溃。归根结底,大型项目绝对应该开始在局部采用凭感觉编程,并采用能够提升这种开发体验的方法,但目前还不要指望它能可靠地完成大型功能。(截至 2025 年 6 月 7 日 / AI 纪元)
关于这一点,我必须说得非常明确:CLAUDE.md 不是可有可无的文档。你每花一分钟更新它,之后就能节省一小时的清理时间。
可以把 CLAUDE.md 看作代码库的宪法。它确立了基本法则,规定代码应该如何编写、系统之间应该如何交互,以及应该遵循或避免哪些模式。愿意投资培养团队技能和能力的组织会获得更好的成果,而 CLAUDE.md 正是这种投资凝结而成的文档。
下面是我们生产环境中 CLAUDE.md 结构的精简版本,它经过数千次 AI 辅助提交的反复打磨:
# `CLAUDE.md` - Julep Backend Service
## The Golden Rule
When unsure about implementation details, ALWAYS ask the developer.
## Project Context
Julep enables developers to build stateful AI agents using declarative
workflows.
## Critical Architecture Decisions
### Why Temporal?
We use Temporal for workflow orchestration because:
1. Workflows can run for days/weeks with perfect reliability
2. Automatic recovery from any failure point
### Why PostgreSQL + pgvector?
1. ACID compliance for workflow state (can't lose user data)
2. Vector similarity search for agent memory
### Why TypeSpec?
Single source of truth for API definitions:
- OpenAPI specs
- TypeScript/Python clients
- Validation schemas
## Code Style and Patterns
### Anchor comments
Add specially formatted comments throughout the codebase, where appropriate, for yourself as inline knowledge that can be easily `grep`ped for.
### Guidelines:
- Use `AIDEV-NOTE:`, `AIDEV-TODO:`, or `AIDEV-QUESTION:` (all-caps prefix) for comments aimed at AI and developers.
- **Important:** Before scanning files, always first try to **grep for existing anchors** `AIDEV-*` in relevant subdirectories.
- **Update relevant anchors** when modifying associated code.
- **Do not remove `AIDEV-NOTE`s** without explicit human instruction.
- Make sure to add relevant anchor comments, whenever a file or piece of code is:
* too complex, or
* very important, or
* confusing, or
* could have a bug
## Domain Glossary (Claude, learn these!)
- **Agent**: AI entity with memory, tools, and defined behavior
- **Task**: Workflow definition composed of steps (NOT a Celery task)
- **Execution**: Running instance of a task
- **Tool**: Function an agent can call (browser, API, etc.)
- **Session**: Conversation context with memory
- **Entry**: Single interaction within a session
## What AI Must NEVER Do
1. **Never modify test files** - Tests encode human intent
2. **Never change API contracts** - Breaks real applications
3. **Never alter migration files** - Data loss risk
4. **Never commit secrets** - Use environment variables
5. **Never assume business logic** - Always ask
6. **Never remove AIDEV- comments** - They're there for a reason
Remember: We optimize for maintainability over cleverness.
When in doubt, choose the boring solution.
这份文档成为你和 Claude 之间共享的上下文。它就像一位高级开发者,在整个编程过程中不断贴着 Claude 的耳朵提供指导。
随着代码库不断增长,仅靠 CLAUDE.md 已经不够了。你还需要内联指导,也就是我所说的锚点注释。它们提供局部上下文,防止 AI 在局部做出糟糕的决策。
可以把代码库想象成一座城市,把锚点注释想象成路牌。没有路牌,即使聪明的访客也会迷路。下面是我们有效使用锚点注释的方式:
# AIDEV-NOTE: Critical performance path - this serves 100k req/sec
# DO NOT add database queries here
def get_user_feed(user_id: UUID, cached_data: FeedCache) -> List[FeedItem]:
# We need to avoid mutating the cached data
items = cached_data.items[:]
# AIDEV-TODO: Implement pagination (ticket: FEED-123)
# Need cursor-based pagination for infinite scroll
# AIDEV-QUESTION: Why do we filter private items here instead of in cache?
# AIDEV-ANSWER: Historical context: Privacy rules can change between cache updates
filtered = [item for item in items if user_has_access(user_id, item)]
return filtered
这些注释构成了一条叙事线索,不仅能帮助 AI 和人类理解代码做了什么,还能理解为什么要这样做。
AI 辅助开发最容易被低估的影响之一,是它对 Git 工作流的改变。如今,你生成代码的速度非常快,如果不加注意,很快就会污染 Git 历史。
这种方式实际上只适用于非常大型的代码库,因为它并不是一种特别直观的工具。不过,我建议使用 Git worktree 为 AI 实验创建隔离环境:
# Create an AI playground without polluting main
git worktree add ../ai-experiments/cool-feature -b ai/cool-feature
# Let Claude go wild in the isolated worktree
cd ../ai-experiments/cool-feature
# ... lots of experimental commits ...
# Cherry-pick the good stuff back to main
cd ../main-repo
git cherry-pick abc123 # Just the commits that worked
# Clean up when done
git worktree remove ../ai-experiments/cool-feature
专业提示:了解一下 worktree 的使用方式,再看看非常实用的 wt 工具。
这种方式能够兼顾两者的优势:Claude 可以自由实验,而主分支的历史仍能保持整洁且有意义。
对于提交消息,我们已经形成统一规范,会给 AI 辅助的提交添加标签:
feat: implement user feed caching [AI]
- Add Redis-based cache for user feeds
- Implement cache warming on user login
- Add metrics for cache hit rate
AI-assisted: core logic generated, tests human-written
这种透明度有助于代码审查——审查者知道需要格外留意 AI 生成的代码。
现在我们来到了 AI 辅助开发中最重要的原则。它是如此重要,以至于我要用多种方式重复它,直到它被刻进你的记忆里:
永远。不要。让。AI。写。测试。
测试不仅仅是验证其他代码是否工作的代码。测试是可执行的规范。它们编码了你真实的意图、你的边界情况、你对问题域的理解。高绩效者在速度和稳定性两方面都表现出色——这不是一个权衡问题。测试是你实现两者的方式。
让我用一个例子来说明为什么这很重要。假设我们要求 Claude 实现一个速率限制器:
class RateLimiter:
def __init__(self, max_requests: int, window_seconds: int):
self.max_requests = max_requests
self.window_seconds = window_seconds
self.requests = defaultdict(list)
def is_allowed(self, user_id: str) -> bool:
now = time.time()
user_requests = self.requests[user_id]
# Clean old requests
self.requests[user_id] = [
req_time for req_time in user_requests
if now - req_time < self.window_seconds
]
if len(self.requests[user_id]) < self.max_requests:
self.requests[user_id].append(now)
return True
return False
看起来合理,对吧?Claude 还贴心地生成了测试:
def test_rate_limiter():
limiter = RateLimiter(max_requests=3, window_seconds=60)
assert limiter.is_allowed("user1") == True
assert limiter.is_allowed("user1") == True
assert limiter.is_allowed("user1") == True
assert limiter.is_allowed("user1") == False # Limit reached
但这里是 Claude 的测试遗漏的东西——只有理解业务需求的人类才会测试的东西:Claude 的实现有一个内存泄漏。只访问过一次 API 且永不返回的用户会将其数据永久留在内存中。AI 生成的测试检查了快乐路径,但遗漏了这个关键的生产环境问题。
这是凭感觉编程的最高境界
这就是为什么人类要写测试。我们理解上下文、生产环境、重要的边界情况。在 Julep,我们的规则是绝对的:
## 测试纪律
| 内容 | AI 可以做 | AI 禁止做 |
|------|----------|----------|
| 实现 | 生成业务逻辑 | 触碰测试文件 |
| 测试规划 | 建议测试场景 | 编写测试代码 |
| 调试 | 分析测试失败 | 修改测试期望 |
如果 AI 工具触碰测试文件,PR 就会被拒绝。没有例外。
你的测试是你的规范。它们是你的安全网。它们是你修复过的每个 bug 和发现过的每个边界情况的编码智慧。要对它们严密把守。
AI 辅助开发中最反直觉的教训之一是,省吝上下文来节省 token 实际上花费更多。这就像试图通过只加半箱油来省钱——你最终只是要去加油站跑更多次。
Token 预算很重要。提供聚焦的提示、减少 diff 长度、通过事先总结意图来避免大文件膨胀。但"聚焦"不意味着"最少"——它意味着"相关且完整"。
让我展示给你节制提示的虚假经济:
节制提示尝试:
"为用户端点添加缓存"
Claude 的响应:实现了缓存……但是:
结果:3 轮额外修复,花费 4 倍的 token。
适当的上下文丰富提示:
将 Redis 缓存添加到 GET /users/{id} 端点。
上下文:
- 该端点每分钟服务 50k 请求
- 我们在负载均衡器后面运行 12 个 API 服务器
- 用户数据变化不频繁(每天几次)
- 我们已在 cache.redis.internal:6379 上有 Redis
- 使用我们的标准缓存键模式:"user:v1:{id}"
- 包含缓存命中/未命中指标(我们使用 Prometheus)
- 使用缓存旁路模式,1 小时 TTL
- 使用概率性提前过期处理缓存大雷群
参见我们的缓存指南:docs/patterns/caching.md
教训?预加载上下文以避免迭代循环。把 token 想象成投资好工具——前期成本会多次收回。
实际上,我建议所有项目应该定期让 Claude 查看代码库的变更,并向 CLAUDE.md 添加上下文。
这是另一个反直觉的做法:对不同的任务使用新的 Claude 会话。保持一个长期运行的对话很有诱惑力,但这会导致上下文污染。
可以这样想:你不会在切过生鸡肉后用同一块砧板来切蔬菜。同样,不要在讨论前端样式后对同一个 Claude 会话进行数据库迁移。上下文会以微妙的方式相互渗透。
我们的规则:一个任务,一个会话。任务完成后,重新开始。这样可以保持 Claude 的"思维模型"干净且聚焦。
让我带你走过一个在 Julep 进行的真实重构,它展示了生产规模的凭感觉编程。我们需要用跨 500+ 个端点的结构化错误层次替换我们的临时错误处理。
人类决策(为什么):
首先,我们必须决定错误分类法。这是纯粹的架构工作——Claude 不能做这些决定,因为它们涉及理解我们的业务、用户和运营需求:
# SPEC.md - 错误层次设计(人工编写)
## 错误理念
- 客户端错误 (4xx) 必须包含可操作的反馈
- 系统错误 (5xx) 必须包含用于调试的追踪 ID
- 所有错误都必须是 JSON 可序列化的
- 错误代码必须稳定(客户端依赖它们)
## 层次
BaseError
├── ClientError (4xx)
│ ├── ValidationError
│ │ ├── SchemaValidationError - 请求与 sc