Published on

用 Claude Code 和 Cursor 在3周内实现了一个真实生产环境可用的 RAG 项目

Authors
Table of Contents

最近一个月,我用 Cursor 和 Claude Code(下文有时称作 cc),一个人花了 3 周在公司产品上从零搭起了一个 RAG 服务接口,实现了意图识别、混合全文检索与向量搜索、重排以及动态拼装 prompt 等功能。尽管效果还有待提升,但是至少在产品上跑通了这个功能,作为最小可行性产品是没问题的,甚至这已经远超最小可行性产品了。

我熟悉 RAG 的整个流程和各个环节,去年做过类似的 demo ,但是这是我第一次使用 FastAPI 实现一个在生产环境可供后端服务并发调用的 API,响应时间平均在两三秒,如果没有 Cursor 和 Claude Code,我不可能一个人在 3 周内完成这个 4000 多行核心代码加上 5000 多行单元测试的工程。

在实现这个 RAG 接口的过程中(包括实现前后),有以下一些感想和心得想分享一下。

AI Coding

目前比较推荐的实践是在 Cursor IDE 中的终端打开并使用 Claude Code(或者 Cursor CLI),形成一个 GUI 外壳 + CLI 内核的混合模式。

Cursor 针对编程 UI/UX 的体验有较好的优化,对 Agent 的代码改动能比较直观的观察到,而 Claude Code 则不太方便观测;此外,Cursor 作为集成商,可以使用多个大模型 API,虽然基本还是用 Claude,不过 Gemini 我也挺常用的。

而最近上线的 gpt-5 编程能力比之前的 gpt-4 好了不少,可能是因为幻觉率大幅降低,上下文增加,以及模型会边调用工具边思考,而不是一次性调用完工具后在思考,这样的话中途有可能步骤错了都不知道,最后 gpt-5 的价格也很有竞争力。

cc 写出的代码质量感觉会更好。cc 能够更好地遵循现有代码风格和模块化结构,减少了代码重写次数,适合大型和复杂项目,更擅长处理涵盖前后端的复杂项目和集成任务。

如果你信任它,设定目标与需求,放开手让它在终端中自动运行的话,它能够在较长时间内自主运行下去,直接交付你结果,不需要人在 IDE 面前。Anthropic 团队他们很多人都是让 cc 在后台自己完成任务的,然后回来验收结果和单测。

与 Cursor 会事先对项目代码进行索引(包括语义嵌入)不同,cc 不对代码进行索引(index)或者嵌入(embedding),而是纯粹使用终端的 glob、gb 和 find 等命令进行代码检索,熟悉项目架构,像人类程序员在没有 AI 之前,刚开始接触一个陌生项目进行熟悉一样。

加上 Claude Code 的 subagent 模式,以及刚新上线的一百万上下文窗口,Claude Code 目前感觉除了贵,没太大的缺点。

API First

最好不要想着把 RAGFlow 这种更像是产品的开源项目直接用到你的业务,前期也不必非得用 LangChain 这种开源框架才能做 RAG(甚至 Deep Research),一开始调模型 API 就差不多了,在公司复杂的业务中自己实现一些功能并不意味着重复造轮子。

RAG 70% 的价值是来自搜索增强,而搜索最好结合传统的全文检索(关键词匹配)和向量搜索(语义匹配),所以我们需要一个搜索引擎,可以是 Milvus 这种专业的向量数据库,也可以是传统的 Elasticsearch,他们都可以执行上述的混合搜索,但他们各有所长,这里就不展开了。

有了可以搜索的框架之后,剩下的意图识别、嵌入向量模型和重排模型都可以先直接调 API 就好了,这些不需要 LangChain 等框架都可以实现。

一个现代化的 Python 项目推荐使用以下基础框架:

  • Python API 实现:FastAPI
  • 依赖管理:uv
  • 数据模型:Pydantic
  • 测试框架:pytest

后续需要对各个环节进行优化与降低成本时,则可以尝试在自己的机器上部署与微调自己的模型,而到时候我们会需要其他框架来达到我们的目的。

实际上,许多即开即用的开源框架,其核心都是对更通用、更基础的“积木”式组件的封装。因此,我们更应致力于熟悉并掌握这些底层“积木”,这才是灵活构建和定制特定业务场景下工具的关键。而是否采用框架,以及选择何种框架,则完全取决于对 RAG 的理解程度,以及开发者自身的工程品味和判断。

高内聚低耦合

善用 Cursor Rules。

我的 Cursor Rules 中有一条规则:

*必须 MUST*遵循高内聚低耦合的软件编程规范

内聚是指一个模块内部各个元素之间相互关联的紧密程度。高内聚低耦合(High Cohesion, Low Coupling)是软件工程中一个非常重要的设计原则,它旨在构建易于理解、维护、测试和扩展的系统。

高内聚意味着一个模块(例如一个类、一个函数或一个组件)内部的元素是紧密相关的,它们共同完成一个单一的、明确定义的功能。

如果一个模块只做一件事,并且把这件事做好,那么它就是高内聚的。一个高内聚的模块应该专注于一个特定的职责,而不是承担多个不相关的职责。

耦合是指不同模块之间相互依赖的程度。低耦合意味着模块之间的依赖关系尽可能地弱,一个模块的变化对其他模块的影响尽可能小。

如果一个模块的改变不会导致其他模块也需要改变,那么它们之间的耦合度就是低的。模块之间应该通过定义清晰的接口进行通信,而不是直接访问对方的内部实现细节。

该原则可以使得改变一个功能时,只需要修改对应的模块,不会影响其他不相关的功能;模块功能独立,可以单独进行单元测试;功能明确的模块更容易在其他地方被复用。

遵循这一原则可以帮助我们创建出更健壮、更灵活、更易于维护和扩展的软件系统,从而降低开发成本,提高软件质量。

单元测试驱动 (TDD)

单元测试很重要,而 AI 用于写单元测试的效率超高。我在这 3 周内,用 AI 写了超过 5000 行单测代码,比核心代码行数多了 25%。在开发过程中,我会尽量用单测覆盖每个模块的重要逻辑与接口,而 AI 很多时候都能帮我想到覆盖哪些边界条件(感觉我是在跟 AI 学习怎么写好单测)。

最近了解到 TDD 的开发模式,但还没尝试过。

单元测试驱动开发(Test-Driven Development,简称 TDD)是一种软件开发方法论,它颠覆了传统的“先写代码再测试”的顺序。在 TDD 中,开发者在编写任何功能代码之前,首先编写针对该功能的自动化测试用例。

TDD 的核心思想可以概括为三个简单的步骤,通常被称为“红-绿-重构”(Red-Green-Refactor)循环:

  • 编写测试(红,Red)
    • 开发者首先为一个小的功能点编写一个单元测试用例,描述代码应该实现的行为。
    • 此时,由于功能代码尚未实现,运行测试会失败(表现为“红”,即测试未通过)。
    • 测试用例应聚焦于单一功能,遵循“一个测试验证一个假设”的原则。
  • 编写代码(绿,Green)
    • 编写最简单的功能代码,使测试用例通过(表现为“绿”,即测试通过)。
    • 目标是快速通过测试,而不是追求完美的代码实现。
    • 代码可以是“硬编码”或简陋的,只要能通过测试即可。
  • 重构(重构,Refactor)
    • 在测试通过后,优化代码结构,消除冗余,提高可读性和性能,同时确保所有测试依然通过。
    • 重构的目标是改进代码质量,而不改变其功能行为。
    • 频繁运行测试,确保重构没有引入错误。

然后重复这个循环。这个循环会不断重复,每次只针对一小步功能进行开发,直到所有需求都被满足。

如果清晰地知道自己要什么,加上单元测试很完善的话,极端情况下看不懂 AI 实现功能的代码也能开发下去,实际上 AI Coding 就是这样左脚踩右脚螺旋攀登的,假如未来看不懂 AI 写的代码就算了,但我觉得单测必须得看懂。

Gemini 生成解决方案,Claude 执行代码生成

Claude 确实可以说是目前地表最强编码模型,但是他可能把很多 token 资源放在编码了,相对的思考的 token 就会减少。而 Gemini 2.5 pro 的推理能力是强过 Claude 的,而且一百万的上下文长度可以让 Gemini 看的更多更远,但是确实代码能力比不上 Claude。

Claude Code 最近也增加了一百万上下文了,可以说有这个更长的上下文加成,cc 将会强的可怕,但是超过 20w 的上下文收费也是贵的可怕,相比之下,Gemini 还是很便宜的。

那在 cursor 中,我们就可以先让 Gemini 看更多文件(非必要的就不要加进去上下文了)与日志 error,进行 bug 分析或者提出功能实现的解决方案,然后再让 Claude 执行编码。

不用 cursor 的话,也可以用免费的 Gemini CLI 将解决方案输出到一个文件,然后让 Claude Code 去读取并执行。

Plan First

Claude Code 可以用快捷键 Shift+Tab 切换 plan mode 模式,建议在让它动手干活前,先用 plan mode 聊透需求与解决方案,像前文提到的让 Gemini 先推理规划一样,想清楚再动手。

git worktree 实现真正的多 agent 并行开发

在传统的 Git 工作流中,当我们需要在同一项目中并行开发多个功能时,通常的做法是在单个工作目录下频繁切换分支。这种方式虽然可行,但存在上下文切换的开销,并且无法让多个开发任务在 IDE 层面同时进行。

git worktree 正是为解决这一痛点而设计的。

git worktree 的核心思想很简单:为一个 Git 仓库(Repository)创建多个工作树(Working Tree)

  • 共享的 .git 目录:所有的工作树共享同一个位于主工作区下的 .git 目录。这意味着所有的提交历史、分支、标签、暂存(stash)等元数据都是统一管理的。
  • 独立的物理目录:每个工作树都是一个独立的、位于文件系统不同路径下的文件夹。你在其中一个工作树中对文件进行的修改、编译或测试,都完全局限在该目录内,不会影响到其他工作树。

当创建一个新的 worktree 时,Git 会基于指定的分支或 commit,将项目文件检出到那个新目录中,使其成为一个功能齐全、可独立开发的“项目副本”。

一个高效且安全的 git worktree 工作流如下:

  1. 创建新的功能工作区

这是最关键的一步。假设我们要在 rag_service 项目中开发一个名为 query-rewrite 的新功能,可以使用以下命令:

# (在主工作区 /.../rag_service/ 目录下执行)
git fetch origin
git worktree add -b feature/query-rewrite ../rag_service-feature-query-rewrite origin/develop

这里的关键点:

  • -b feature/query-rewrite:这个参数至关重要。它会创建并检出一个名为 feature/query-rewrite 的新分支。这确保了新工作区在一个隔离的分支上进行开发,而不会“劫持”或干扰 develop 这样的主干分支。
  • ../rag_service-feature-query-rewrite:我们在项目父目录下创建一个清晰命名的文件夹,用于存放新工作区。
  • origin/develop:我们基于最新的远程 develop 分支来创建新功能分支,保证了起点是最新、最干净的。
  1. 真正的并发开发:多 IDE 工作区窗口

现在,你拥有了多个可以同时操作的独立开发环境:

  • /Users/.../rag_service/ (主工作区, 停留在 develop 分支)
  • /Users/.../rag_service-feature-query-rewrite/ (新工作区, 停留在 feature/query-rewrite 分支)

你可以打开两个独立的 Cursor IDE 窗口,一个加载主工作区,另一个加载新功能的工作区。每个窗口都拥有自己独立的上下文(打开的文件、终端会话、LSP 状态等),从而实现了真正的并行开发。

  1. 开发、提交与推送

在新功能的工作区(rag_service-feature-query-rewrite)内,所有 Git 操作(add, commit, push)都只针对 feature/query-rewrite 分支。

# 在新工作区的终端中
git commit -m "feat: implement query rewrite logic"
git push -u origin feature/query-rewrite
  1. 合并与清理

当功能开发完成并通过测试后,通过 Pull Request 或直接 Merge 的方式将 feature/query-rewrite 分支合并到 develop 分支。之后,可以安全地清理掉工作区和分支。

# (在主工作区执行)
# 1. 移除 worktree 物理目录
git worktree remove ../rag_service-feature-query-rewrite

# 2. 删除已经合并的本地分支
git branch -d feature/query-rewrite

git worktree 的模式完美契合了 AI Agent 的并发执行需求。

传统模式(串行)

在单个工作区内,如果你想让 Agent 先开发功能 A,再修复 Bug B,它必须:

  1. 在一个分支上开发 A。
  2. stashcommit A 的工作。
  3. 切换到另一个分支。
  4. 开始处理 B。

这个过程是线性的,Agent 在同一时间只能聚焦于一个任务,效率较低。

git worktree 模式(并行)

我们可以启动多个 Agent 实例(比如每个 IDE 的终端中都开启一个 Claude Code),并将它们的工作范围限定在不同的 worktree 中:

  • Agent 实例 1 (运行于 Cursor 窗口 1):

    • 工作目录: /.../rag_service-feature-A/
    • 任务: 专注开发功能 A,在此目录中执行代码编写、测试、调试等所有操作。
  • Agent 实例 2 (运行于 Cursor 窗口 2):

    • 工作目录: /.../rag_service-feature-B/
    • 任务: 并行地开发功能 B,其所有操作与 Agent 1 完全隔离。

核心优势

这种模式下,每个 Agent 都在一个干净、隔离的环境中运行,拥有独立的上下文和“操作空间”。它们可以同时进行编码,互不干扰,也无需进行耗时的上下文切换。

不过 Agent 并发执行编码时,最好开发的功能分支之间是没有交集的,也就是两个 worktree 开发的功能之间没有依赖关系,也最好不要修改同一个模块。

Claude Code subagents

Claude Code 中的自定义子代理是专门用于处理特定类型任务的 AI 助手,它们可以通过提供任务特定的配置、自定义系统提示、工具和独立的上下文窗口来提高问题解决的效率。

在我看来,cc 的 subagents 最关键的就是与主对话分离的独立上下文窗口,其次是配置该 subagent 可用的特定工具

现在 Agent 可以自主决策,决定调用什么工具,但会有一些问题需要解决:

  • 在一个任务涉及太多工具的话,会使 Agent 混乱,导致 Agent 性能下降;
  • 一个任务步骤很多,持续很久的话,上外文的显著增长会导致 Agent 推理性能下降。

如果我们能对复杂任务进行合理拆分,得到一个个简单的,能在较短步长内,使用较少工具完成的子任务的话,能对上述问题有所缓解。

所以可以采用多 Agent 并行处理子任务的方式,但需要解决 Agent 之间的通信问题,以及需要一个最强大脑中央 Agent 进行指挥与调度。中央 Agent 具有极强的推理与规划能力,子 Agent 其实就是一个个“工具人”,供中央 Agent 调度。

这样让每个子 Agent 的任务上下文都更加干净和专一,能有效利用有限的上下文,而中央 Agent 的上下文主要用于规划和调用合适的工具 Agent,并与其他子 Agent 进行交互,整合子 Agent 的结果,进行最终的任务交付。

因此,子 Agent 能使用一些工具,而对于中央 Agent 来说,子 Agent 就是它的工具,这样能把可使用的工具量和有限的上下文进行有层次的隔离与划分。

cc 的 subagent 功能主要就是解决上述问题,cc 推理能力更强但是更贵的 opus 作为中央调度与推理的主会话模型,而 sonnet 则主要作为执行编码的 subagent,更弱的 haiku 主要用来阅读一些文件或者日志。

example: 以开发一个 iOS APP 为例,我们可以在本项目中创建一个专精于 iOS UI/UX 设计的 Project subagent,配置在 .claude/agents/ 文件中,这个只对 iOS APP 开发这个项目生效。

而我们还可以分别创建一个全局的代码审查的 User subagent,一个专门写单元测试的 User subagent,配置在 ~/. Claude/agents/ 文件中,主要用于检查编写的代码是否符合自己软件工程的理念和品味,比如是否符合高内聚低耦合,以及编写规范的单元测试。这 2 个 subagent 不仅用在 iOS APP 这个项目,还在所有项目中都可用。

cc-subagent1.JPG

cc-subagent2.PNG

上图不同的色块就是不同的 subagent 在执行任务。

可预见的是,多个 Agent 并发带来多个彼此独立的上下文窗口,提高效率的同时, token 的消耗成倍地增加也就不足为奇了。

其他感想

  • 钱多事少离家近,除非公司是自己家的,不然很难实现;而做一项功能,要好,要快,要少资源,这也是绝对的不可能三角;

  • 系统设计中,架构选择太难做了,不断想出方案,不断推翻方案,不断陷入这个循环当中。

  • 无论是向量检索还是 RAG ,亦或是 Agentic ,即插即用的框架与模型可以在特定的业务上取得 50,60 分,这只是开始,几天实现与几小时实现不难,难的是做好做到 80,90 分甚至 100 分。你不能因为 50 分就觉得没戏,也不能因为谁都可以几小时几天实现一个 demo 就觉得很简单,往往魔鬼都藏在细节中;

  • 有个段子戏称:用 AI 做 demo 几天,上线花了半年。一个简单的例子,解析几个文档和解析上万个文档并不是一个量级,同时在真正的产品里需要考虑存储和并发,不能简单的以 demo 为准,demo 展示的是研究结果以及最小可行性产品验证,但是放到真正的产品上的时候需要各个层面进行 scale up;

  • demo 其实也很重要。我如果没有先研究做出一个 demo,要推动这个功能做到线上,哪怕只是在公司内部使用估计也很难;

更有趣的是,参与开发的一位 PM 和一位设计师直接用 AI 做出了一个原型。那位 PM 有工程背景,逻辑很强,但在 Duolingo 期间几乎没写过代码,这次他用 Cursor 和 AI 工具硬是做出了第一个版本。

正是这个原型说服了我们。要是只有个 Figma 设计稿,我们可能还会犹豫。但它真的做得很好,好到所有人都被打动,于是项目立刻推进。

  • 对于 AI 功能,快速获得反馈比完美重要。可以想象一个不完美的功能上线会造成什么后果,如果这个后果很严重,那我们就应该仔仔细细的测试,调整到尽可能完美才上线;如果这个功能很烂,但是后果不严重,而且可预期这个功能可以变得越来越好,那可以灰度上线获得反馈先;

  • 可观测性与自动化评估很重要,评估可以从几个样本人工评估开始,跟着整个功能一起迭代,慢慢增加样本量,同时思考怎么让其自动化,你需要选取几个对功能真正有意义的指标,然后持续迭代。


欢迎关注微信公众号👏 code
欢迎微信扫码加入我的付费知识星球👏 code