前言

本文中总结了我自2023年以来,深度使用大语言模型辅助工作与编码的一些使用经验与技巧。

TL;DR

完善的文档 + 完善的单元测试 + 完善的注释 + 严格的代码审查 + 详细的需求描述,结合少量固定约束提示词,可以实现无负担或少负担的将AI融入编码工作中。


AI编程

迭代到目前,AI编程领域已经出现了Agent、MCP、Codebase RAG等多种增强工具。商业化工具从最初的 TabNine、Github Copilot 到现在的商业化 Cursor、Windsurf、Claude Code,开源 Roo-Code、Continue、Aider等工具。

其中每一个工具我都曾经重度在工作中使用过,基于我过往的使用经验,总结出一些使用技巧。

明确目标

首先需要 明确你的目标 ,以及思考这个目标大模型能否完成?

模型训练时的数据是有限的,所以首先需要考虑的问题是,你当前需要解决的这个问题,模型的训练数据中有没有可能存在相同或相似的数据?

例如你使用的技术栈是很主流的Java + spring boot,前端 TypeScript + Next.js,存在大量的训练数据,那AI生成效果可以达到满意。

但凡是会用到小众的库或小众语言,LLM就开始胡言乱语、伪造不存在的API、伪造不存在的语法,如果自身没有鉴别能力,很容易被误导。这种就是大模型的 幻觉问题

你的能力上限决定AI的能力上限,最佳场景是这个功能我自己会写,但是我懒得写,所以我指挥AI帮我完成工作。

Complete / Ask / Agent

AI辅助编程主要可以分为两大类

  1. 代码补全 (auto complete)
  2. 问答/结对编程 (chat / agent)

通常对话模型需要经过预训练、指令微调(Instruction Tuning)、强化学习等训练步骤,才能够正常理解对话形式的上下文。而代码补全通常是基于未经过指令微调的基础模型,主要用于文本的补全。

对我而言,日常并不依赖AI代码补全。因为补全需要一定实时性,对速度要求高,且基础模型通常不会太大(猜测不超过14B),无法包含足够的上下文信息,幻觉问题非常严重,补全错误率高,容易打断思路。

当然AI补全也不是完全无用,在特定场景下,例如需求明确的简单CRDD,流水线代码、文档注释、日志输出等,AI补全效果可以满足简单需求。

Agent 是对传统AI编程能力的一次大增强,利用Agent工作流可以使 LLM 将一个大任务拆分成多个小任务,逐步完成,而避免单次任务过于繁重导致结果不理想的问题,那 Agent 模式就一定好用吗?

我们都知道,Agent每次请求都需要带上过去的上下文信息,请求次数越多,Token数量成指数级放大。这会带来两个问题:

  1. TOKEN 数量大以后,大模型容易遗漏关键信息、注意力涣散,导致幻觉增加,代码错误率高。
  2. 多轮任务堆叠后,出现上下文稀释,状态同步困难、debug难度高。
  3. 钱包爆炸。

MCP (Multi-Component Pipeline)本质上是为LLM提供一套工具箱(toolbox), 使传统文本生成模型拥有了和“现实”的交互能力,实现例如访问网页、命令执行、连接数据库等功能。

在没有 Agent 和 MCP 之前,AI结对编程需要你手动将需要交互的文件加入到Chat对话中,交互后,LLM通常会返回diff结构(Ask Mode);或是先返回部分设计思路与代码片段,再由专用的diff模型生成diff(Architect mode)。整个交互逻辑需要依赖用户的能力,LLM更像是一个听话的“实习生”,根据用户提示解决问题,无法自动修复问题。

而现在,你可以无需手动添加,大模型可以利用MCP中提供的工具,通过分析需求,判断出自己需要读取哪些必要文件,然后自动读取。例如Agent的分步骤能力,可将任务拆分成多个子步骤,例如

Start -> 分析需求,提供设计思路 -> 列出需要完成的ToDo List 以及想要调用的MCP工具 -> 执行工具,读取文件 -> 分析文件,应用设计 -> 应用diff -> Next ToDo -> … -> Lint Check -> Write Unitest -> apply test -> bug fix -> … End

好处当然是能够充分利用模型能力,自动化大量需要手工完成的步骤,只需要简短的 Prompt,就可以将整个设计、开发、测试、修复完整的流程跑通(当然这是在理想情况)。例如通过数据库连接的MCP,大模型可以自己设计表结构,生成SQL语句创建表,还可以在编程过程中,实时的获取测试数据,补全表结构字段等功能。

缺点其实是跟模型能力有很大关系,大模型会出错,而且由于Agent链式调用,连续执行下来,模型很容易偏离最初的目标,又或者是一步错导致步步错,最后给出不可用的代码,还可能会因为一个简单的代码格式错误,导致将原本正确的代码改的面目全非。

我的选择

目前我主要使用 Aider + Emacs实现辅助编码,选择 Aider 恰恰是因为足够简单,我不需要 Agent, 也不需要 MCP,我可以充当人肉 Agent,分发任务,获取执行结果,并继续下一个任务。这样可以带来绝对的掌控能力,避免Agent模式下修改代码不可控的问题,Aidermacs 扩展 + emacs ediff,可以让我达到媲美 Cursur 的 diff 功能,目前足以完成编码需求了,比起工具,我觉得更重要的是打造自己的工作流。

Claude Code 不好用吗?非常好用,只是我认为这种基于Agent的AI开发工具目前比较适合 Vibe Coding。对现有代码库上使用时需要仔细审计AI修改的变更代码,并在偏离时直接打断,不要盲目信任AI,慎用auto-accept,每次需求前提交git commit方便回滚。

如何正确的使用AI

可以把AI根据根据使用场景分成两类,一类是用于从0开始到MVP的一站式服务工具,一类是辅助日常开发迭代的工具。

例如 Bolt、v0 以及“截图转代码”一类的 AI 工具属于前者,Cursor、Cline、Copilot 等等属于后者。

前者的使用逻辑通常会从一个大概的设计或概念开始,使用 AI 快速生成一个完成的基本代码框架,然后快速验证,快速迭代。

而后者通常用于AI代码补全、AI多行修改、基于Agent的重构、生成单元测试和文档,实行结对编程。

AI生成会导致的一些问题

  • 代码库快速膨胀,存在大量过度抽象、过度设计、模块拆分不符等问题,需要人为的进行重构。
  • AI生成的代码可能会漏掉代码边界的处理部分。
  • 生成代码存在类型错误、类或接口设计不合理。
  • 错误处理 / 异常处理部分很简陋。通常都是直接 catch 所有 Exception。

小白们喜欢吹嘘自己通过 Vibe Coding 实现了xxx项目,上线获取了多少多少的 Star

有经验的开发者使用AI来帮助完成已知步骤,我知道怎么写,但是我希望指导AI快速帮我写完。

而经验不足会导致试图用AI来学习怎么做。

资深开发者会:

  • 快速做出大脑中的原型
  • 让 AI 生成初步实现,然后进行重构
  • 探索已知问题的不同解决方案
  • 自动化各种常规编码任务

初级开发者:

  • 接受不正确或已过时的解决方案
  • 忽视关键的安全和性能考量
  • 调试 AI 生成的代码时感到困难
  • 构建出脆弱而自己并不真正理解的系统

根本原因是怎么看待“AI”这个工具

如果你自身的经验不足,你想修复一个Bug -> AI给出了一个看似合理的建议 -> 修改完成后又引发了其他问题 -> 你再让AI修复新问题 -> 导致更多问题。

更深层的麻烦在于:AI 能帮你干脏活,解决很多复杂问题,这恰恰使得非专业开发者更难真正学到开发的本质。当代码被批量生成出来,而你并不理解它背后的原理时:

  • 你不懂怎么调试代码
  • 你不懂一些基础知识,某个语法的含义
  • 你无法理解AI的架构设计,也无法进行修改
  • 造成代码难以维护

你只能不断让 AI 来修复问题,而不是掌握真正解决问题的能力。

经验不足的开发者应该如何使用AI?

  1. 根据一个想法或框架快速实现源型。
  2. 阅读代码,理解代码库的设计思路与工作原理。
  3. 使用 AI 进一步去学习基本的编程概念,补充基础。
  4. 将 AI 看成学习工具,而不是“自然语言代码生成器”

我的使用经验

  1. 如果已有设计,让AI根据设计生成代码;如果只有一个概念,则先与AI沟通,完善这个概念的细节。
  2. 人工审核代码是非常有必要的,人工进行代码拆分、重构或指挥AI完成重构。
  3. AI 生成的代码错误处理比较粗糙,需要人工完成或AI辅助实现比较完善的错误处理。
  4. 单元测试非常重要,使用真实的数据进行测试,而不是由AI生成数据 + AI生成测试。
  5. 使用文档记录关键架构,功能的执行流,开发过程中的关键决策等。
  6. 每个独立任务都创建一个新的会话,保证上下文精简、集中。
  7. 使用文档和开发日志辅助新会话快速理解整个工程。

先设计,再编码

软件工程中遵循先设计,再编码,设计粒度甚至要精确到某个类的某个函数是什么功能。

先设计后编码的弊端就是当项目开始以后的需求变更、设计实现生产无法实现等问题导致的问题,这是典型的自上而下设计思路导致的问题。

  • 传统:需求分析 -> 架构设计 -> 详细设计 -> 编码 -> 测试
  • AI时代:核心需求 -> AI快速原型 -> 功能验证 -> 迭代优化 -> 架构重构

所以编程步骤可以是先设计,由AI编码快速实现并试错,如有需求变更或功能无法实现时,快速修改并试错。

但这不代表自上而下的高级抽象开发或是自下而上的模块化开发不适合AI时代,而是需要做出结合从中间开始,依赖大模型的强大重构能力,向下使用模块之间的组装,结合AI快速完成功能并测试。向上结合AI对已稳定的代码进行封装重构,提高可维护性。

实际工作中还是会遇到大量项目是边研究边开发,项目初期只有一个模糊的目标,无法明确设计。这种情况我的做法是结合模块化函数组装 + 抽象重构。

  1. 将底层的功能模块拆分成独立的小模块,应用函数式思维,保证输入输出是纯函数,一个模块只做一件事,基于一个个小模块可以组装出一个功能,基于功能又可以组装成一个任务的处理过程。
  2. 当某一个功能已经趋于稳定以后,利用大模型重构能力,将抽象层添加到业务中。

不要过早抽象,不要过度抽象,见过很多Javaer上来就是 先写接口再写抽象类再写Base最后写Impl,两个函数就搞定的东西拉了一大坨,裤子都脱了一行有用的代码都没有。

项目开发的三个阶段

我将软件开发简化为三个大阶段,介绍项目周期中不同时间段内怎么合理利用LLM。

  • 设计阶段
  • 开发阶段
  • 维护阶段

设计阶段

我将开发目标大致划分为两大类:一类是成熟度较高,已有大量现成的代码可以参考和复用,例如XX管理平台;另一类则是市场上几乎没有现成的参考代码,需要从零开始,自行研发。

从0开始的项目,前期的设计思考最非常重要,你需要思考清楚最终想要达成的效果是什么,分别从目标开始往结果推导/从结果往向目标反推,尽量把每个细节都想清楚。

对于从零开始的项目,前期的设计阶段非常重要。所以你需要花足够的时间和精力,深入思考并明确最终想要达成的目标。你可以选择从目标出发,逐步推导出每个实现步骤,或者从预期结果反推所需的路径。

这个环节需要大量使用 LLM 辅助设计,如果是非常成熟,有大量参考的代码实现,可以比较信任LLM给出的代码结构框架。如果是从0开始的研发,需要先设计出最小的可测试POC,也就是将项目有难度的地方模块化,利用大模型的能力,快速生成可用代码,先测试,如果功能无法实现,马上修改设计,避免中后期项目架构完成后难以修改的问题。

请务必将与LLM讨论的结果输出为详细的文档,包括但不限于:系统设计文档、API接口文档、数据库表字段的设计文档、整体系统流程图、以及功能模块的最小可行原型(POC)。在开发过程中,也需要实时的进行文档的维护修改更新。由于LLM存在上下文窗口限制,无法读取整个项目,大模型需要依赖这些设计文档辅助理解代码。

注重单元测试,如果可以,在设计阶段就需要将可能的输入输出给定义好,完善的输入输出 + 详细的文档 + 适量参考代码,足以实现复杂需求。

开发阶段 和 维护阶段

开发阶段和维护阶段已有存量代码,存量代码由于代码量大、缺乏文档、频繁变动、缺乏注释等问题,首先需要做的是让大模型理解整个代码仓库,并在后续交互中持续更新包含项目设计、项目架构、模块调用关系等关键信息的开发文档。

当前AI Coding工具已经提供了很多方式试图让LLM理解代码库,例如RAG增强检索,tree-sitter语法树,Agent 调用模拟人去查找代码依赖关系等。结合文档可以快速理解代码,例如开发新功能,可以快速定位需要插入代码的位置;例如修复bug,可以通过文档快速定位可能出现的错误位置,而无需多次Agent调用去定位,节约时间成本。

Prompt 工程

现在2025年已过半,市面的主流大模型其实已经不需要详细的 prompt 就可以理解任务目标,LLM会自己补全任务细节。已经不需要像2023年一样使用大量提示词限制才能得出想要的结果。

AI交互语言只需要注意两点:学会提问 + 反向思考。

人与人的沟通中使用自然语言,自然语言语境中存在大量的背景省略和上下文省略,与AI对话时需要避免给出含糊不清的提示词,例如修复xx问题、实现xx功能这种提示词,最终导致代码质量差、功能实现与预期不符、核心功能缺失等问题。

我的经验是在写prompt之前思考,如果我自己需要实现这个功能,或是解决这个问题,我需要先研究哪些文件,需要哪些上下文信息,然后将这些文件路径和上下文限定信息一同发送给LLM。

举个例子,某个任务中我需要将 继承自 DeclarativeBase 的自定义数据库表结构转为纯SQL语句

使用RAW SQL重构 database/product_model.py 中由 SQLalchemy 驱动的数据库模型,转为纯 SQL语句实现,保存到独立的SQL文件中,目录是database/sql,使用的数据库是PostgreSQL.

对于动态字典类型,使用JSONB结构存储,在SQL文件中沿用代码中完善的字段注释。

其中 xx 字段需要接收由 Pydantic model_dump_json() 导出的JSON数据,使用TEXT类型存储。

项目选形

选择技术栈时,需要考虑AI对该语言或框架的掌握能力。

对于编程语言,除了 Python / JavaScript 这类存在大量基础库的动态语言,尽量选择静态、或有完善的 type hint语言,且拥有静态类型检查能力。

优先选择 Java、C#、TypeScript、Go,避免选择 Ruby、PHP、Rust、C++等。动态类型语言缺乏类型信息,AI编码时极容易出现类型错误,而Rust/C++语言本身过于复杂,容易出现低级问题。

那拥有完善类型推导的语言怎么样呢?例如 Haskell、OCaml;其实也不推荐,先不说训练数据中占比比较小,这类基于HM类型推导的编程语言,其代码中大量依赖推导而非显式类型定义,在开发环境中由于存在静态分析工具,所以不会出现问题,而AI编码时是不知道类型信息的,导致效果远远不如Python,毕竟Python现有代码库大量应用 type hint。

对于代码库的选择,建议优先学存量大,版本变动小的技术栈,例如 数据库选择裸SQL而不是ORM框架,大模型训练语料中的SQL相关数据一定是大于ORM框架的代码数据的。

模型能力排行

按照个人使用主观感受,我对当下(2025年6月)模型编程能力排行

  1. OpenAI: o3 / o4
  2. Anthropic: Claude 4 opus / Claude 4 sonnet
  3. Google: Gemini 2.5 pro

o3 / o4 强大,指令遵循能力强,不会过度发散,缺点是价格太贵

claude 4和当前最主流的编码模型,可看作其他代码生成模型的标杆,且为Fucntion Call做过强化训练,懂得合理调用工具。

Gemini 2.5 Pro 的模型能力实际上和claude 是在同一梯队的,但是Gemini的模型指令遵循能力比较差,工具调用能力差,且思维发散,日常使用中需要详细的Prompt进行限制。

总结

  • 重设计、重测试、轻编码。理想状态是AI完成80%的编码工作,人工只需要处理20%。
  • 优先使用显式类型定义,而非隐式类型推导,LLM需要通过显式定义去理解类型。
  • 显式定义数据结构,避免使用动态Dict、Map结构,避免在函数调用中传递动态结构的参数。
  • 提供约束文档,约束大模型行为。例如 Aider中的 CONVENTIONS.md,Claude Code 中的 CLAUDE.md
  • 提供项目开发文档帮助大模型理解庞大的代码库,梳理分层架构、理解调用关系,记录开发过程中的关键设计决策。
  • 详细每个函数的函数文档注释、变量在业务中的作用注释、调用注释等等,辅助大模型理解代码。
  • 完善单元测试,最好在设计阶段就开始编写,功能开发完成后需要适时修改,提高代码健硕性,避免AI重构导致逻辑代码缺失。
  • 快速实现POC测试可行性,利用LLM能力及时对POC代码进行重构,依赖单元测试保证重构后功能不变。