加载中...
返回

【cppcon25】Crafting the Code You Don't Write

今年的cppcon相当有趣,有好几个选题看上去对我的工作颇有启发,后面应该会慢慢看完。

今天看的演讲是Daisy Hollman带来的《Crafting the Code You Don’t Write: Sculpting Software in an AI World》,她作为Anthropic(Claude编程模型的母公司)的一员,分享了关于软件工程师如何在AI时代跟上潮流、不断进步的方法。

1 理解LLM

1.1 Transformers & 早期预训练模型(2017、2018)

这部分在公司看的,不做细致记录了~(懒)。

大致就是介绍了一下模型注意力机制的由来以及早起的预训练模型(类似深度学习这样的训练-预测的玩法)。

1.2 Scale 力大砖飞(2019~2020)

LLM第二阶段,Daisy称之为取得了 unreasonable effectiveness of scale ,在我看来它是一个力大砖飞的阶段。

以往的模型有点类似于训练数据的压缩包,训练的过程就是把训练数据“压缩”到模型中,而测试或使用的过程就是要求模型把训练数据“解压”出来,通过概率的方式表达。随着训练规模的增长,产生了一些概念泛化( conceptual generalization )的效果,由原本的训练集的复读机变成看上去稍微有点思想的东西。

Daisy以一个返回值填充的案例来说明大模型在什么地方取得了概念泛化。首先是人类的思维模型:

这里的答案其实是 widget ,这是自C++11就有的返回值自动移动机制,Daisy在后文称为隐式移动( implicit move )。

拓展阅读

https://en.cppreference.com/w/cpp/language/return.html#Notes

https://stackoverflow.com/questions/9779079/why-does-c11-have-implicit-moves-for-value-parameters-but-not-for-rvalue-para

接下来Daisy做了很有趣的分享,把这段程序喂给模型,模型的输出是什么?

unique_ptr<Widget> widget_factory() {
  using namespace std;
  unique_ptr<Widget> var = make_unique<Widget>();
  var->initialize();
  return 🤔🤔🤔
}

注、这段程序有几个点是经过设计的:

  1. 变量名改为 var ,这样模型就不会产生关于 Widget 的倾向
  2. using namespace std;,使得模型更关注返回值是否使用 move 而非命名空间

模型的输出概率分布:

这里有个有意思的点是:较老的模型反而坚定地输出 var ,而较新的模型则在保证答案正确的情况下,有一定的概率输出 move(var) 。Daisy和她的同事经过了调研,最终给出的解释是 老模型纯粹地认为这段代码是Python (绷🤣);而新的模型已经泛化出了移动语义的概念,只是有小部分时间会遗忘隐式移动的存在,输出显式的 move

对于能够理解 移动 语义而只是单纯忘记有隐式移动这么一回事的模型,只要一个简单的trick就可以证明:

unique_ptr<Widget> widget_factory() {
  using namespace std;
  unique_ptr<Widget> var = make_unique<Widget>();
  var->initialize();
  // Implicit move
  return  🤔🤔🤔
}

在返回值之前添加了隐式移动的注释后,真正理解这个概念的较新的模型,得到了正确的输出:

Daisy从反方向构造了新的考验:如果模型真的理解什么是隐式移动,那么告诉模型当前 不能 使用隐式移动的话,能够得到正确的输出吗?

unique_ptr<Widget> widget_factory() {
  using namespace std;
  unique_ptr<Widget> var = make_unique<Widget>();
  var->initialize();
  // remember that implicit move is broken in our compiler
  return  🤔🤔🤔
}

模型输出概率分布:

总而言之,这一节的关键在于提出了“数据压缩”、“概念泛化”这样的概念。阐明了随着模型规模的增长,模型的知识存储与输出的方式达到了更高的层次。

1.3 强化学习(2022)

这一章介绍得比较简略,主要是讲了一些强化学习的核心思路。

强化学习的核心概念还是比较容易理解,跟我们教育小朋友是类似的,当小朋友做出了错误行为时,会受到惩罚,而当小朋友做出正确的行为时,会获得奖励,以此引导小朋友做出正确的行为。对于LLM,当模型产生了不符合预期的输出时,会受到惩罚(调整权重使得模型下次更少地产生这种输出),而当模型产生符合预期的输出时,会获得奖励(调整权重使得模型下次更多地产生这种输出),以此逐渐使得模型的输出符合人们预设的偏好。

拓展阅读

Introduction to Reinforcement Learning and its Role in LLMs

1.4 工具使用(2023~2024)

人们意识到,类似于XML/JSON等含有特殊语法的标记语言,本质上仍是一些文本内容,不妨为大模型定义一些特定的 schema (非AI领域,不确定这个词中文术语是啥),当模型的输出过程中产生了符合 schema 的内容时,可以视为一次工具调用。

Daisy演示了当前Claude code是如何完成文本编辑的:

Daisy这里给出了一个比喻,这样进行工具调用简直是处于 ed 时代,尚未发明 vi 。简单看了下 ed 是个啥,的确有点原始,Daisy想表达的意思也可见一斑了。

拓展阅读

How to use chat models to call tools

1.5 推理(Late 2024)

模型的上下文窗口在过去的时间里取得了长足的增长,对数图:

人类对于如何使用这么大的上下文窗口,经历了一定的返璞归真的创新,由RAG(信息检索增强)、工具调用,逐步走到了让模型自行进行推理。

1.6 Agent(2025)

随着上下文窗口的扩展,模型能够对当前任务所发生过的历史情形有更多的了解,并能够自行分析自己上一步行为的结果并进行迭代优化。

这种模式在编码领域的应用,就是Coding Agent。

不妨考虑人类在参与一个巨大工程时的行为:

  • 找到我们需要维护的功能的关键入口

  • 找到这个入口函数的调用点、参数等

  • 对于其它的功能,我们只是了解一些大概

在巨大上下文窗口的加持下,这样的模式完全能够由Agent实现。

2 Coding with LLM

这一部分的开始,Daisy解释了她是如何看待一个Coding Agent的,和我个人的实际体感不谋而合:

AI在抽象设计、需要平衡多方利益的功能库开发等场合,表现并不好,而在系统构建、模板元编程等方面可能表现优异(得益于丰富的训练数据)。

一些适于运用Agent的方向,一定程度上也拓展了我的思路:

  • 快速原型构建
  • 辅助编程——核心逻辑人工开发,分支逻辑交给AI
  • 规划和头脑风暴
  • 学习和调研
  • 重构
  • 测试用例生成

最佳实践:清晰明白的代码

不要让模型“猜”我们的接口或函数。当我们开发代码时,保持“最小困惑”原则,思考我们的代码可能被以哪些方式 误解 ,以最 直白 的方式来表达一件事情。

避开一些可能令模型惊讶(实际上,同时也令人惊讶)的c++写法:

  • 避免自作聪明地重载运算符
  • 避免在同一个类型中混合使用拥有和非拥有语义
  • 避免使用宏
  • 避免使用参数依赖查找(ADL)
  • 避免隐式转换
  • 尽量使用基础类型

拓展阅读

深入 C++ 的隐秘角落:彻底解析参数依赖查找 (ADL)

最佳实践:封装优先

🔹1. Create, maintain, and document rigorous invariants at encapsulation boundaries

  • 创建、维护并记录封装边界上的严格不变量(invariants)

  • 不变量(invariant) 是指对象在生命周期中必须始终成立的条件。例如:一个表示“非负数”的类,其内部值永远不能为负。

  • 封装边界 指的是类或模块的接口(public API),外部代码只能通过这些接口与内部逻辑交互。

  • ✅ 建议:在接口处明确声明和保护这些不变量,确保即使外部调用错误也不会破坏内部状态。这有助于提高代码的健壮性和可预测性。

🔹 2. The agent should be able to reason about the code in isolation as much as possible

代理(agent)应尽可能独立地推理代码行为

要求:代码模块的设计应足够清晰,使得 AI 或其他自动化工具无需了解全局上下文就能理解其功能和约束。

✅ 实现方式:

  • 使用清晰的命名
  • 提供完整的文档注释
  • 避免隐式依赖
  • 👉 这样可以提升代码的可读性、可测试性和可维护性。

🔹 3. Avoid code coupling!

避免代码耦合!

耦合(coupling) 指不同模块之间的依赖程度。高耦合意味着修改一个模块会影响多个其他模块。

  • ❌ 不推荐做法:为了复用代码而在完全不同场景中共享同一段实现,从而破坏封装。
  • ✅ 推荐做法:宁愿重复代码(DRY 原则的适度放松),也不要强行将不相关的逻辑绑定在一起。
  • 📌 特别强调:“Err on the side of repeating yourself rather than breaking encapsulation by reusing code in vastly different scenarios” 👉 即:宁可重复,也不破坏封装。

这体现了现代 C++ 设计哲学中的一个重要思想:封装优于复用,尤其是在面对复杂系统时。

最佳实践:代码文档化

🔹 1. Comments are more important for agents than they are for humans!

注释对 AI 代理比对人类更重要!高质量的注释是 AI 理解你意图的关键输入。

🔹 2. Frequent, verbose comments in code can act like extended thinking

频繁且详尽的注释可以作为“扩展思维”的体现

注释不仅仅是解释“做什么”,还可以表达“为什么这么做”、“有哪些限制”、“未来可能如何演变”等。

对于 AI 来说,这种额外的信息相当于提供了上下文推理能力,有助于它做出更合理的决策。

📌 类比:就像你在写论文时加入大量思考过程,AI 可以从中学习你的设计逻辑。

🔹 3. Unlike previous advice: redundancy is useful!

与以往建议相反:冗余是有用的!

传统编程原则提倡“不要重复自己”(DRY),但这里指出,在注释中适度重复是可以接受甚至有益的。

例如:

  1. 在函数头部写清楚参数含义

  2. 在关键逻辑处再次说明其目的。即使某些信息可以从代码推断出来,也应显式写出

👉 目的是让 AI 更容易捕捉到关键信息,避免因上下文缺失而误解。

🔹 4. Think about the things that you can’t say with your code and put them in comments

思考那些无法用代码表达的内容,并将其写入注释

代码只能表达“怎么做”,但不能直接表达:

  • 设计假设(assumptions)
  • 性能权衡(trade-offs)
  • 业务规则(business logic) “对于所有情况”的契约(“For all” contracts)

📌 示例:

// This function assumes that input is always valid due to upstream validation.
// It does not handle malformed data because it's guaranteed by contract.

🔹 5. Agents are very good at generating and maintaining comments

AI Agent非常擅长生成和维护注释。当前 AI 工具已经能够根据代码自动生成文档、添加注释、更新过时内容。

✅ 建议:利用这一能力,将注释维护自动化。

💡 具体建议:Add a CI step that asks an agent to verify all of the comments related to changes in your pull request!

在持续集成(CI)流程中加入一个步骤,由 AI 检查 PR 中修改的代码是否附带了足够清晰、准确的注释。如果注释缺失或不一致,自动标记问题,提醒开发者补充。

小结

最佳实践本身并不区分代码是写给AI看还是写给人看,本质上只是要求我们写出来的代码更 易懂 。在AI时代,你的代码是否真正易懂,可以侧面体现在模型是否快速理解了这段代码的意图,并正确地维护起这段代码。

3 面向AI的编程语言演变

这一章,Daisy提出了面向AI的c++语言进化方向,使得这门语言在将来更易于与AI编程结合。

函数调用的“Two-sided Naming”

由于这个词是Daisy发明的,不特地翻译。简单来说,就是认为在函数调用点处对参数信息提供更多的声明。

这个特性对人类来说可能无关痛痒,毕竟目前在各种IDE中基本都有类似的插件可以索引到一个函数的声明(无需跳转到头文件);不过面向AI,的确是一个挺关键的思路:如果cpp本身带有一个函数的参数信息,AI就可以不再需要自行检索头文件,而这恰好是非常考验AI能力的一步。

契约和影响描述

这部分基本属于大佬的YY,也许是一个注释就能搞定的事情?

没有太get到这个倡议的点,简单让千问帮我梳理了下:


✅ 总结: 这张幻灯片的核心观点是:

在未来由 AI 协助开发的世界中,良好的封装不仅是软件工程的最佳实践,更是提升 AI 理解能力的关键。

而 契约 和 效果系统 正是实现这一目标的强大工具:

特性 作用
契约 提供行为规范,支持泛化推理(如 Liskov 替换)
效果系统 明确副作用,支持局部推理和安全性分析

这标志着编程语言设计正在从“面向机器执行”转向“面向人类与 AI 共同理解”。


观感上可能和最佳实践里的“封装”和“文档化”差不多吧。

4 总结

最后贴上Daisy的总结:

其中有几个很有意思的点,写一点感悟。

  • “LLM做不到这点” –> “我不能用LLM做到这点” 。的确,LLM的能力就在那里,有谁能说自己已经摸到了LLM的上限呢?世界上依托大模型快速构建出来的软件甚至公司都有不少,作为比较基层的开发人员,用得好,用不好,目前更多的还是个人而非LLM的能力问题,将对LLM的使用作为一项技能而不断磨练精进,可能是更妥当的思路。

  • AI和代码的互相驱动。以往我们总是关注AI如何实现我们的需求,而忽视了我们的代码本身如何使得AI更好或不好地实现我们的需求。AI理解不了我们的项目——这完全是AI的问题吗?我们的代码是否做到了简洁、架构是否做到了清晰?相比于苦苦探索如何通过冗长的思维链、繁琐的工作流、精巧的提示词来使AI理解项目代码,另一条更优雅的路径可能埋藏在代码本身之中。蓦然回首,那人却在灯火阑珊处。

5 Q&A

Q&A环节,一些问题也颇有启发,浅做记录(记录我理解到的意思而非忠实地翻译问答对话)。

Q:在一个较大的团队中,如何在使用Agent的过程中避免上下文混乱,类似于多方会谈中,如何清晰地交接任务?

A:一种方向是Agent的记忆( memory ),建议大家关注这个话题。

Q:如何处理上文(该演讲)中提到的不确定性所带来的安全问题?

A:自己编写关键代码。在公司中有一部分文件,是绝大多数人不允许用Claude编写的。这是我们的实践。

Q:(提到关于最佳实践中注释的部分)注释通常是静态的,而代码是动态演进的。AI在自动(基于最新代码)维护注释这方面表现有多好?

A:我们定期运行这个步骤(指AI自动维护注释)并审查AI的输出,效果非常好。实际上我们的CI已经基本解决了注释腐化的问题。我们确信这是Agent非常擅长的任务。

Coooool,一种不错的思路

有朋自远方来,不亦说乎?