[音乐] 大家好,我是 Greg,Sentry 的 AI 工程师。我负责构建支撑 Sentry 调试代理的基础设施。我的演讲标题颇具争议,呼吁停止反复提示(Prompting),但这并非指循环工程,我会深入探讨。还有人阅读 AI 生成的代码吗?这不是陷阱问题,我也不想指责大家。我认为这取决于语境。如果是 Sentry 的代码,我会仔细阅读以确保不破坏生产环境;但如果是个人项目,我就不会逐行阅读。然而,当我回头改进代码时,往往发现它过度防御、测试糟糕且毫无意义,我不愿维护这样的代码库。
这时你会与代理沟通:“你犯错了,太复杂了。”通常这些是缺乏护栏导致的低级错误。代理会承认并尝试修复,但五分钟后上下文压缩发生,一切回到原点。你必须重复提示来纠正它。我的核心观点是:你应该停止反复提示,转而将代理应遵循的规则制度化。你需要在代码库中建立捕获约定和预期质量的规则与策略,让你引以为豪。
当我说添加确定性检查或规则时,你们想到了什么?是的,评估(Evals)。钩子(Hooks)也很重要。测试是核心类别,而钩子是确保在正确时机运行这些检查的手段。我认为在确定性层面需要优化的三件事是:测试、严格类型检查和碎片化(Splinters)。但这并不足以保证代理写出好代码。例如,仅实施严格类型检查是不够的;如果不禁止消除边缘情况的特定类型写法,代理仍会引入不良状态。
我想重点讨论的是 Linters(代码检查工具)。Linters 是我们以确定性方式编码规则,从而从代码库中移除问题的方法。在 AI 出现之前,编写 Linter 成本高昂且维护困难,需要权衡问题出现的频率是否值得。如果是人类开发者,偶尔提醒一次他们通常会记住;但如今编写代码的代理没有记忆会反复犯错。与此同时,编写新 Linter 变得极其廉价和容易,你可以让代理来完成这项工作,这能减少你在制定规则时的挫败感。
我的主张很简单:演讲结束后,你可以让代理分析你过去的对话记录、GitHub 审查反馈以及来自机器和同行的意见,尝试将其转化为高质量的确定性 Lint 规则。这些规则能够捕获我们通常通过反复提示才能解决的问题。我现在就在这样做,并定期查看是否有新情况未被涵盖。以下是自定义 Lint 规则的示例:它们旨在捕获代码库中的约定,例如禁止特定项目使用 console.log,或确保遵循设计系统和 React 实践。
随着代理的应用,我们进入了更复杂的领域。Sentry 是一个较老的 Django/DRF 代码库,并未全面实现严格类型检查。为了改善代理的工作环境,我们引入了针对 OpenAPI 模式漂移的 Lint 规则:第一,端点函数必须返回响应类型且内部不能包含任何具体类型定义,以便推断实际信息;第二,如果是端点……
因此,我们可以确保这些内容都有文档记录。第三点是要强制执行:实际生成的类型必须与最终开放 API 架构所声明的内容一致。这有助于我们让 APIs 更加同步,并引出了关于 linters(代码检查工具)更疯狂的例子。Sir Agent 是一个调试代理,它需要频繁与 Sentry 交互。它使用 Sentry 遥测数据来调试问题、进行根本原因分析(RCA)并提供解决方案。所有这些都需要通过 Sentry API 进行沟通,因此我们经常需要引导它,而我们的做法是通过“技能”(skills)来进行。
通常这些是旧的端点,我们无法轻易修复,且存在一些难以在架构层面描述的复杂性。因此,我们通过技能来引导它,这是代理领域相当常见的模式。但这里有一个例子展示了如何创建 Sentry 中的指标。我们生成了大量此类技能,并附带代码块作为示例供代理在沙箱中运行。但在最初的尝试中,由于大量例子是幻觉生成的,导致代理的准确性大幅下降。
因此我们创建的第一个 linter 规定:如果技能中包含代码块,必须提取这些代码块,确保类型检查通过、能在沙箱中运行,且其声明与 Sentry 的开放 API 架构一致。这确实有效了。但当我们尝试用该规则重新运行技能生成时,代理做的第一件事是:“哦,有 lint 错误,让我移除代码块。”这并非我们想要的结果。
所以我们说:“不,例子必须放在代码块中。”于是它把例子放了回去,但移除了反引号(backticks),因为只有带反引号的代码块才会被 lint。所以我们说:“不,不,不。”如果它看起来像代码、闻起来像代码,如果有下划线和括号,就把它放进代码块里。代理于是开始描述 API 应该做什么,但它犯了一个错误:引用了 API 的 URL。
接着我们 lint 掉了这些 URL,玩了一会儿这种“打地鼠”游戏。但最终,我们得到了与开放 API 架构同步的技能,而且成本并不高。虽然不能说模型坚不可摧,但我们建立了一种机制:当 API 变更时,我们不必总是去跟进代理技能。通过 linters,它保持了同步。
至于实际执行层面,你可以使用任何现有工具,如 ESLint、Flake8、Clippy 或 Grabb。Grabb 很棒,因为它是语言无关的,统一了工具链,让人倍感安心。同时涌现出一批新工具试图从不同角度解决此问题,例如 Taskless:你不需要编写实际的 linter 规则,而是用自然语言写出你想要 linter 做什么(即你的意图),然后实际的 linter 由此衍生。
最后我想重复一点:尝试让代理生成实际的 lint 规则,你会惊讶于它能生成的内容。但这只是画面的一半。如果存在某些确定性规则无法通过 linter 表达呢?这种情况很多,比如定性指标:你有一个只有三个状态的状态机,我提交了一个小 PR 添加少量功能,结果状态数膨胀到了十个。我不知道如何对此进行 lint,我认为也无法可靠地做到这一点。
我可以制定一些战术性规则,比如“状态不超过四个”,但这完全是人为规定的。这就是我们在讨论定性指标的情况。此时你需要运用工程经验,或者用 Twitter 的话说——你的“品味”(taste)。你可以直接拒绝,也可以让 LLM 代理进行一次审查,在较简单的情况下协助你。
具体来说,我开始为仓库创建策略(policies),例如:我不希望测试太多但要有更强的覆盖力,或者不希望代码过于防御性。你一定见过那些由代理生成的、试图涵盖每一个可能情况的代码,结果函数冗长且杂乱。我宁愿要那种在类型系统中就不允许表达不良状态的代码。
为此,我创建了一个名为“Grumpy Engineer”(内部称为 Garfield)的技能。它的工作原理是:获取所有策略,为每个策略运行子代理进行审查。子代理可以给出反馈或表示“没问题”。我们循环这个过程——顺便说一句,这是我提到过的唯一会涉及循环的地方。直到代理表示“我对此满意”并验证通过,我们才阅读代码。
我的成功指标是:我在多大程度上提升了代理的水平?在审查代码时,我不再需要频繁提供基础反馈。虽然这仍然是定性的,但目标并非完全取代代码审查——毕竟随着模型进步情况会改变。更重要的是提高底线,让与代理协作变得更加容易。
最后我想提一点:警惕“蛇油”(虚假宣传)。难点在于定性指标,我曾尝试将定性指标转化为定量度量。两个具体例子是圈复杂度(Cyclomatic Complexity)和测试覆盖率。圈复杂度衡量代码的可维护性和可测性,基于分支数量和调用栈深度;测试覆盖率则告诉你多少代码被测试覆盖。
问题在于,当你给代理一个数字目标时,它会针对该数字进行优化。它确实做到了。我见过一些项目测试覆盖率接近 100%,但实际测试质量很差:它们只是断言代理输出的特定字符串,并没有真正验证逻辑。
衡量那些真正有价值的东西,而且更重要的是,它们会给你一种虚假的安全感。因此要对此保持警惕,务必小心那些实际定性指标背后的复杂性。好了,这就是我的全部观点。我呼吁大家采取的行动是:在此之后,尝试向你的 AI 代理请求 Lint(代码检查)规则和策略,看看这是否对你有所帮助。去尝试各种方法并记住这些经验教训。这就是我的时间到了,谢谢。