我如何使用LLMs帮助我编写代码

78 阅读18分钟

原文

关于使用Large Language Models帮助编写代码的在线讨论中,总会有一些开发者表达他们的失望经历。他们经常问自己做错了什么——为什么有些人报告了如此好的结果,而他们自己的实验却不尽如人意?

使用LLMs来编写代码是困难且不直观的。需要付出大量努力才能弄清楚这种使用方式的尖锐和柔软边缘,而且几乎没有指导来帮助人们找出最佳应用方法。

如果有人告诉你使用LLMs编码很容易,他们(可能是无意中)在误导你。他们可能偶然发现了有效的模式,但这些模式并不是每个人都能自然掌握的。

两年多以来,我一直从LLMs中获得出色的代码编写结果。这是我尝试向你传递一些经验和直觉的努力。

设定合理的期望

忽略"AGI"的炒作——LLMs仍然是高级自动补全。它们所做的只是预测一系列标记——但事实证明,编写代码主要是将标记按正确顺序串联起来,所以只要你指引它们朝正确的方向发展,它们在这方面可能非常有用。

如果你假设这项技术会完美实现你的项目,而不需要你运用任何自己的技能,你很快就会失望。

相反,使用它们来增强你的能力。我目前最喜欢的心智模型是把它们视为一个过度自信的结对编程助手,它查找信息的速度快如闪电,能随时提供相关示例,并能不抱怨地执行繁琐任务。

过度自信很重要。它们绝对会犯错误——有时是微妙的,有时是巨大的。这些错误可能非常不像人类——如果一个人类合作者捏造了一个不存在的库或方法,你会立即失去对他们的信任。不要陷入拟人化LLMs的陷阱,认为那些会使人类失信的失败同样应该使机器失信。

在使用LLMs时,你经常会发现它们无法做到的事情。记下这些——它们是有用的教训!它们也是值得存储起来的有价值的未来例子——一个强大的新模型的标志是当它能为以前的模型无法处理的任务产生可用的结果。

考虑训练截止日期

任何模型的一个关键特性是其训练截止日期。这是他们被训练的数据停止收集的日期。对于OpenAI的模型,这通常是2023年10月。Anthropic、Gemini和其他提供商可能有更近期的日期。

这对代码来说极其重要,因为它影响模型熟悉哪些库。如果你使用的库在2023年10月后有重大变更,OpenAI模型就不会知道!

我从LLMs中获得了足够的价值,以至于现在选择库时会特意考虑这一点——我尝试坚持使用具有良好稳定性的库,这些库足够流行,以至于许多例子已经进入了训练数据。我喜欢应用无聊技术的原则——在项目的独特卖点上创新,对其他一切都坚持使用经过验证的解决方案。

LLMs仍然可以帮助你使用存在于其训练数据之外的库,但你需要投入更多工作——你需要在提示中提供关于这些库应如何使用的最新示例。

这就引出了使用LLMs时需要理解的最重要的事情:

上下文为王

从LLM获得良好结果的大部分技巧都归结为管理其上下文——作为当前对话一部分的文本。

这个上下文不仅仅是你提供给它的提示:成功的LLM交互通常以对话形式进行,上下文包括当前对话线程中来自你和LLM的每条消息。

当你开始新对话时,你将上下文重置为零。这很重要,因为通常修复已经不再有用的对话的方法是清除一切并重新开始。

一些LLM编码工具超越了仅仅的对话。例如,Claude Projects允许你用相当大量的文本预先填充上下文——包括最近能够直接从GitHub存储库导入代码的功能,我正在大量使用它。

像Cursor和VS Code Copilot这样的工具自动包含来自当前编辑器会话和文件布局的上下文,有时你可以使用像Cursor的@commands这样的机制来拉取额外的文件或文档。

我主要使用ChatGPT和Claude网页或应用程序界面的原因之一是,它使我更容易理解到底什么内容进入了上下文。对我来说,隐藏上下文的LLM工具效果较差。

你可以利用先前回复也是上下文一部分的事实。对于复杂的编码任务,尝试让LLM先编写一个更简单的版本,检查它是否有效,然后迭代构建更复杂的实现。

我经常通过倾倒现有代码来开始新聊天,以种子化上下文,然后与LLM合作以某种方式修改它。

我最喜欢的代码提示技术之一是放入几个与我想要构建的内容相关的完整示例,然后提示LLM使用它们作为新项目的灵感。我在描述我的JavaScript OCR应用程序时详细写了这一点,该应用程序结合了Tesseract.js和PDF.js——我过去使用过的两个库,我可以在提示中提供工作示例。

向它们询问选项

我的大多数项目都始于一些开放性问题:我试图做的事情是否可能?有哪些潜在的实现方式?这些选项中哪一个是最好的?

我将LLMs作为这个初始研究阶段的一部分。

我会使用这样的提示:"Rust中HTTP库的选项有哪些?包括使用示例"——或者"JavaScript中有哪些有用的拖放库?为我构建一个演示每个库的工件"(对Claude)。

训练截止日期在这里很重要,因为它意味着不会推荐较新的库。通常这没关系——我不想要最新的,我想要最稳定的和存在足够长时间已经解决了bug的库。

如果我要使用更新的东西,我会自己做研究,在LLM世界之外。

开始任何项目的最佳方式是使用原型证明该项目的关键需求可以满足。我经常发现,LLM可以在我坐下来使用笔记本电脑后的几分钟内让我得到那个可工作的原型——有时甚至在我用手机工作时也能做到。

明确告诉它们做什么

一旦完成初步研究,我会大幅改变模式。对于生产代码,我对LLM的使用更加独裁:我把它当作数字实习生,雇来根据我的详细指示为我输入代码。

这是最近的一个例子:

Write a Python function that uses asyncio httpx with this signature:

async def download_db(url, max_size_bytes=5 * 1025 * 1025): -> pathlib.Path
Given a URL, this downloads the database to a temp directory and returns a path to it. BUT it checks the content length header at the start of streaming back that data and, if it's more than the limit, raises an error. When the download finishes it uses sqlite3.connect(...) and then runs a PRAGMA quick_check to confirm the SQLite data is valid—raising an error if not. Finally, if the content length header lies to us— if it says 2MB but we download 3MB—we get an error raised as soon as we notice that problem.

我自己可以编写这个函数,但查找所有细节并让代码正确运行至少需要15分钟。Claude在15秒内就完成了。

我发现LLMs对我在这里使用的函数签名反应极好。我可以充当函数设计师,LLM负责按照我的规范构建函数体。

我经常会跟进说"现在使用pytest为我编写测试"。同样,我指定我选择的技术——我希望LLM节省我输入已经在我脑海中的代码的时间。

如果你的反应是"肯定输入代码比输入英文指令更快",我只能告诉你对我来说确实不是这样了。代码需要正确。英语有巨大的捷径空间、模糊性、拼写错误,以及像"使用那个流行的HTTP库"这样的说法(如果你一时想不起名字)。

优秀的编码LLMs善于填补空白。它们也比我勤奋得多——它们会记得捕获可能的异常,添加准确的文档字符串,并使用相关类型注释代码。

你必须测试它写的代码!

我上周详细写了这一点:你绝对不能外包给机器的一件事是测试代码是否真正有效。

作为软件开发人员,你的责任是交付工作系统。如果你没有看到它运行,它就不是一个工作系统。你需要投资加强那些手动QA习惯。

这可能不光彩,但无论是否涉及LLMs,它一直是交付好代码的关键部分。

记住这是一场对话

如果我不喜欢LLM写的东西,它们永远不会抱怨被告知重构!"将那个重复的代码分解成一个函数","使用字符串操作方法而不是正则表达式",甚至"写得更好!"——LLM首次生成的代码很少是最终实现,但它们可以为你重新输入几十次,而不会感到沮丧或厌烦。

偶尔我会从第一个提示中得到很好的结果——随着练习越来越频繁——但我预计至少需要一些后续操作。

我经常想知道这是否是人们缺失的关键技巧之一——糟糕的初始结果不是失败,而是推动模型朝向你真正想要的东西的起点。

使用能运行代码的工具

越来越多的LLM编码工具现在具有为你运行代码的能力。我对其中一些有点谨慎,因为错误的命令可能会造成真正的损害,所以我倾向于坚持那些在安全沙盒中运行代码的工具。我现在最喜欢的是:

ChatGPT Code Interpreter,ChatGPT可以直接在OpenAI管理的Kubernetes沙盒VM中编写和执行Python代码。这是完全安全的——它甚至不能进行出站网络连接,所以真正可能发生的是临时文件系统被弄乱然后重置。

Claude Artifacts,Claude可以为你构建一个完整的HTML+JavaScript+CSS网络应用程序,在Claude界面内显示。这个网络应用程序显示在一个非常锁定的iframe沙盒中,极大地限制了它能做什么,但防止了像意外泄露你的私人Claude数据这样的问题。

ChatGPT Canvas是一个较新的ChatGPT功能,具有与Claude Artifacts类似的能力。我还没有足够探索这一点。

如果你愿意冒更多险:

Cursor有一个"Agent"功能可以做到这一点,Windsurf和越来越多的其他编辑器也是如此。我还没有花足够时间与这些工具一起使用,无法提出建议。

Aider是这些模式的领先开源实现,也是dogfooding的一个很好的例子——Aider的最新版本有80%以上是由Aider自己编写的。

Claude Code是Anthropic在这个领域的新进入者。我将很快提供使用该工具的详细描述。

这种运行代码循环模式非常强大,以至于我选择核心LLM编码工具主要基于它们是否能安全运行和迭代我的代码。

Vibe-coding是学习的好方法

Andrej Karpathy在一个多月前创造了"vibe-coding"这个术语,它已经流行起来:

有一种新型的编码,我称之为"vibe coding",你完全屈服于氛围,拥抱指数级增长,忘记代码甚至存在。[...] 我要求最愚蠢的事情,比如"将侧边栏的填充减少一半",因为我太懒而不想找到它。我总是"全部接受",我不再阅读差异。当我收到错误消息时,我只是复制粘贴它们进去,通常这就修复了。

Andrej建议这对"一次性周末项目不太差"。这也是探索这些模型能力的绝佳方式——而且非常有趣。

学习LLMs的最佳方式是与它们玩耍。向它们抛出荒谬的想法并进行vibe-coding,直到它们几乎有点儿有效,这是一种真正有用的方式,可以加速你建立对什么有效、什么无效的直觉。

在Andrej给它命名之前,我就一直在进行vibe-coding!我的simonw/tools GitHub存储库有77个HTML+JavaScript应用和6个Python应用,每一个都是通过提示LLMs构建的。我从构建这个集合中学到了很多,我以每周几个新原型的速度添加。

你可以在tools.simonwillison.net上直接尝试我的大多数工具——这是该存储库的GitHub Pages发布版本。我在10月份在《我本周用Claude Artifacts构建的所有东西》中写了更详细的笔记。

如果你想查看用于每个工具的聊天记录,它几乎总是链接在该页面的提交历史中——或访问新的colophon页面,其中包含所有这些链接的索引。

使用Claude Code的详细示例

在写这篇文章时,我想到了tools.simonwillison.net/colophon页面的想法——我想要一个可以链接到的东西,比GitHub更明显地显示我每个工具的提交历史。

我决定利用这个机会来演示我的AI辅助编码过程。

这次我使用了Claude Code,因为我想让它能够直接针对我笔记本电脑上现有的tools存储库运行Python代码。

在会话结束时运行/cost命令显示:

> /cost 
  ⎿  Total cost: $0.61
     Total duration (API): 5m 31.2s
     Total duration (wall): 17m 18.7s

初始项目从开始到结束只花了我17分钟多一点,API调用花费了我61美分。

我使用了独裁过程,告诉模型我确切想要构建什么。这是我的提示序列(完整记录在这里)。

我首先请求一个初始脚本来收集新页面所需的数据:

Almost all of the HTML files in this directory were created using Claude prompts, and the details of those prompts are linked in the commit messages. Build a Python script that checks the commit history for each HTML file in turn and extracts any URLs from those commit messages into a list. It should then output a JSON file with this structure: {"pages": {"name-of-file.html": ["url"], {"name-of-file-2.html": ["url1", "url2"], ...—as you can see, some files may have more than one URL in their commit history. The script should be called gather_links.py and it should save a JSON file called gathered_links.json

我对第一个提示真的没有想太多——它更像是我在考虑初始问题时输入到机器人的意识流。

我检查了初始结果并发现了一些问题:

It looks like it just got the start of the URLs, it should be getting the whole URLs which might be to different websites—so just get anything that starts https:// and ends with whitespace or the end of the commit message

然后我改变了主意——我也想要那些完整的提交消息:

Update the script—I want to capture the full commit messages AND the URLs—the new format should be {"pages": {"aria-live-regions.html": {"commits": [{"hash": hash, "message": message, "date": iso formatted date], "urls": [list of URLs like before]

像这样提供例子是获得你想要的东西的一个很好的捷径。

请注意,我从未查看过它在gather_links.py中写的代码!这是纯粹的vibe-coding:我在观察它在做什么,但我完全将实现细节留给了LLM。

JSON看起来不错,所以我说:

This is working great. Write me a new script called build_colophon.py which looks through that gathered JSON file and builds and saves an HTML page. The page should be mobile friendly and should list every page—with a link to that page—and for each one display the commit messages neatly (convert newlines to br and linkify URLs but no other formatting)—plus the commit message dates and links to the commits themselves which are in https://github.com/simonw/tools

Claude知道GitHub URLs如何工作,所以告诉它链接到提交并提供存储库名称足以让它猜测github.com/simonw/tool…

我发现Claude在网页设计方面有很好的默认品味——我说"页面应该是移动友好的",就此打住。

Claude努力构建了一个不对的页面,所以我说:

it's not working right. ocr.html had a bunch of commits but in colophon.html there is only one link and heading for the first commit and the rest are shown within that same block—there should be separate HTML chunks with links and formatted dates for each of the other commits. Also the neatly formatted date should include the HH:MM as well as the date

它自己修复了bug,只剩下两个我决定要做的更改:

it's almost perfect, but each page should have the commits displayed in the opposite order—oldest first

然后:

One last change—the pages are currently listed alphabetically, lets instead list them with the most recently modified at the top

这就是整个项目!这是build_colophon.py,它生成的页面看起来很好:

Tools Colophon. This page documents the creation of the tools on tools.simonwillison.net, including links to the Claude conversations used to build them. social-media-cropper.html b4a2bc December 10, 2024 20:35 Social media cropper gist.github.com/simonw/12b8… a10954 February 28, 2025 16:02 Support 2:1 and 14:10 ratios gist.github.com/simonw/e239…

还有一项剩余任务:我需要将新的colophon部署为我网站的一部分,但我不想将新的colophon.html页面检入存储库本身。我想要一个自定义的GitHub Pages构建过程。

我开始了一个全新的Claude Code会话(清空一切)来看看Claude是否也能解决这个问题:

This repo is deployed using GitHub Pages. I want to modify the build slightly—before deploying to GitHub Pages it should run python gather_links.py which will generate a file called gathered_links.json—then it should run python build_colophon.py which will use that file to create colophon.htmlI want both gathered_links.json and colophon.html to be published to GitHub pages but I do not want them checked into the repo itself

与第一次不同,这次我非常仔细地观察它在做什么——我不知道如何以这种方式自定义GitHub Pages构建,我既想学习如何做到这一点,又想保持谨慎,因为它可能会产生幻觉并在任务中失败。

它告诉我它想创建这个文件并请求我的许可:

name: Deploy to GitHub Pages

on:
  # Run on push to main branch
  push:
    branches: [ main ]
  # Allow manual trigger
  workflow_dispatch:

# Sets permissions of the GITHUB_TOKEN
permissions:
  contents: read
  pages: write
  id-token: write

# Allow only one concurrent deployment
concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Need full history for git log in gather_links.py

      - name: Setup Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.12'

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip

      - name: Generate links and build colophon
        run: |
          python gather_links.py
          python build_colophon.py

      - name: Setup Pages
        uses: actions/configure-pages@v4

      - name: Build with Jekyll
        uses: actions/jekyll-build-pages@v1

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v3

  deploy:
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

我猜这是对的?我很欣赏那个关于"需要完整历史用于git log in gather_links.py"的注释——这是我很容易错过的事情。

然后它说它想将这些文件添加到.gitignore中——听起来是个好主意。

它吐出了到目前为止行动的总结:

I've created a GitHub Actions workflow for GitHub Pages that:

Creates a .github/workflows/pages.yml file that:
Runs on pushes to main branch
Checkouts the full git history (needed for gather_links.py)
Sets up Python 3.12
Runs both scripts in sequence to generate the files
Builds and deploys to GitHub Pages
Created .gitignore file to prevent the generated files from being committed
Now when you push to main, GitHub Actions will:

Run the gather_links.py script to create gathered_links.json
Run build_colophon.py to create colophon.html
Deploy both files (and all other files) to GitHub Pages

我检查了它花了多少钱:

> /cost 
  ⎿  Total cost: $0.1788
     Total duration (API): 44.6s
     Total duration (wall): 10m 18.6s

所以使用Claude API花了17美分和45秒。(我分心了,所以总时间是10分钟。)这是完整记录。

代码看起来不会不可逆地破坏任何东西,所以我把它推送到GitHub看看会发生什么。

...它起作用了!我的新colophon页面上线了。

有一个问题。我在它运行时观察GitHub Actions界面,发现有些不对劲:

GitHub Actions界面显示三个已完成的操作。Test for Custom pages workflow for colophon,2 Deploy for that same name和另一个名为pages-build-deployment的操作。

我预期那个"Test"作业,但为什么有两个单独的部署?

我有一种感觉,以前的默认Jekyll部署仍在运行,而新的部署同时运行——纯粹是时机的运气,新脚本后来完成并覆盖了原始结果。

是时候放弃LLMs并阅读一些文档了!

我找到了这个关于使用GitHub Pages自定义工作流的页面,但它没有告诉我我需要知道的内容。

凭着另一种直觉,我检查了我的存储库的GitHub Pages设置界面,发现了这个选项:

GitHub Pages UI - 显示你的网站在tools.simonwillison.net上线,7分钟前部署。然后在构建和部署下,源菜单显示GitHub Actions或从分支部署(已选中)的选项

我的存储库设置为"从分支部署",所以我将其切换为"GitHub Actions"。

我手动更新了README.md,在这个提交中添加了指向新Colophon页面的链接,这触发了另一个构建。

这次只运行了两个作业,最终结果是正确部署的网站:

现在只有两个正在进行的工作流,一个是Test,另一个是Deploy to GitHub Pages。

(我后来发现了另一个bug——一些链接在它们的href=中无意中包含了
标签,我通过另一个11美分的Claude Code会话修复了这个问题。)

更新:我通过添加AI生成的工具描述进一步改进了colophon。

准备好人类接管

我在这个例子中很幸运,因为它帮助说明了我的最后一点:预期需要人类接管。

LLMs无法替代人类的直觉和经验。我花了足够的时间与GitHub Actions一起工作,知道要寻找什么样的东西,在这种情况下,我自己介入并完成项目比继续尝试通过提示达到目标要快。

最大的优势是开发速度

我的新colophon页面从构思到完成、部署的功能不到半小时。

我确信,如果没有LLM的帮助,这会花费我明显更长的时间——以至于我可能根本不会费心去构建它。

这就是为什么我如此关心从LLMs获得的生产力提升:这不是为了更快地完成工作,而是能够交付那些我不能证明值得花时间的项目。

我在2023年3月写过这个:AI增强开发使我对项目更有雄心。两年后,这种效果没有任何减弱的迹象。

这也是加速学习新事物的好方法——今天是如何使用Actions自定义我的GitHub Pages构建,这是我将来肯定会再次使用的东西。

LLMs让我更快地执行想法的事实意味着我可以实现更多想法,这意味着我可以学习更多。

LLMs放大现有专业知识

其他人能用同样的方式完成这个项目吗?可能不能!我在这里的提示依赖于25多年的专业编码经验,包括我之前对GitHub Actions、GitHub Pages、GitHub本身和我使用的LLM工具的探索。

我也知道这会有效。我花了足够的时间使用这些工具,以至于我确信组装一个带有从Git历史中提取的信息的新HTML页面完全在好的LLM的能力范围内。

我的提示反映了这一点——这里没有特别新颖的东西,所以我指定了设计,在工作时测试结果,偶尔推动它修复bug。

如果我试图构建一个Linux内核驱动程序——一个我几乎一无所知的领域——我的过程将完全不同。

附加:回答关于代码库的问题

如果使用LLMs为你编写代码的想法仍然感觉令人深恶痛绝,还有另一个用例你可能会发现更有吸引力。

优秀的LLMs非常擅长回答关于代码的问题。

这也是风险很低的:最坏的情况是它们可能会弄错一些东西,这可能会让你花费更多时间来解决。与完全靠自己挖掘数千行代码相比,它仍然可能为你节省时间。

诀窍是将代码转储到长上下文模型中并开始提问。我目前最喜欢的是名字朗朗上口的gemini-2.0-pro-exp-02-05,这是Google的Gemini 2.0 Pro的预览版,目前可以通过他们的API免费使用。

就在前几天我使用了这个技巧。我正在尝试一个我以前没用过的工具,叫做monolith,一个用Rust编写的CLI工具,它下载网页及其所有依赖资产(CSS、图像等),并将它们捆绑到一个单一的存档文件中。

我很好奇它是如何工作的,所以我将它克隆到我的临时目录并运行这些命令:

cd /tmp
git clone https://github.com/Y2Z/monolith
cd monolith

files-to-prompt . -c | llm -m gemini-2.0-pro-exp-02-05 \
  -s 'architectural overview as markdown'

我在这里使用我自己的files-to-prompt工具(去年由Claude 3 Opus为我构建)来将存储库中所有文件的内容收集到单个流中。然后我将其通过管道传输到我的LLM工具,并告诉它(通过llm-gemini插件)用"architectural overview as markdown"的系统提示来提示Gemini 2.0 Pro。

这给了我一份详细的文档,描述了该工具的工作原理——哪些源文件做什么,以及至关重要的是,它使用了哪些Rust crates。我了解到它使用了reqwest、html5ever、markup5ever_rcdom和cssparser,而且它根本不评估JavaScript,这是一个重要的限制。

我每周使用这个技巧几次。这是开始深入研究新代码库的好方法——而且通常替代方案不是在此上花费更多时间,而是完全无法满足我的好奇心。