引言

如果你曾经让 ChatGPT 或 GitHub Copilot 写一个 Python 脚本或 JavaScript 函数,你会发现结果往往准得惊人。这些模型在数以亿计的热门语言代码行上进行过训练,因此非常精通标准的编程任务。

但是,当你涉足冷门领域时会发生什么?

当你要求大语言模型 (LLM) 为低资源语言——即特定领域语言 (DSL) ,如 Microsoft Power Query M、OfficeScript 或复杂的 Excel 公式——生成代码时,性能会显著下降。这些语言缺乏有效训练模型所需的海量开源代码库。

为了解决这个问题,开发者通常使用检索增强生成 (RAG) 。 他们在运行时为模型提供相关的文档或代码示例,以帮助模型理解任务。然而,标准的检索方法有一个缺陷: 它们通常将文档和示例视为分离的、独立的孤岛 。 你抓取前 5 个相关的示例前 5 个相关的文档页面,把它们扔进提示词里,然后祈祷能得到好结果。

在 Microsoft 研究人员最近发表的一篇题为 “RAR: Retrieval-augmented retrieval for code generation in low-resource languages” (RAR: 面向低资源语言代码生成的检索增强检索) 的论文中,提出了一种更聪明的方法。他们介绍了检索增强检索 (RAR) , 这是一种两步走的过程,模仿了人类开发者的实际工作方式: 利用示例来查找正确的文档,或者利用文档来查找最佳的示例。

在这篇文章中,我们将详细拆解 RAR 的工作原理,解释为什么独立检索在低资源语言上会失败,以及这种新方法如何显著提高代码生成的准确性。

低资源语言的挑战

低资源语言呈现出一个独特的悖论。它们对于业务逻辑 (如 Excel 公式或数据转换脚本) 往往至关重要,但在大语言模型的训练数据中却代表性不足。

为了填补这一空白,我们依赖两个主要的外部知识来源:

  1. 文档 (D) : 官方手册。它包含语法、函数定义 (例如 ExcelScript.Workbook.getActiveWorksheet) 和规则。
  2. 示例 (E) : 代码片段以及展示如何使用语法的自然语言描述 (utterances) 。

当前检索方法的问题

现有的方法通常使用“先检索后生成”的策略。它们可能会在示例库中搜索与用户请求相似的代码片段,或者在文档中搜索相关函数。

问题在于,这两种来源在检索过程中往往是脱节的。

  • 文档是抽象的: 知道 OrderByDescending 函数的存在,并不能告诉模型如何将其与其他函数组合来实现“高亮显示前 5 个项目”。
  • 示例容易导致过拟合: 如果模型看到一个看起来相似但解决的问题略有不同的示例,它可能会盲目地复制代码,而不是对其进行调整。

研究人员意识到,要生成好的代码,你需要同时拥有结构 (语法) 和上下文 (示例) ,而且关键在于,你需要确保它们彼此相关。

图 1: 展示了我们要如何从公开文档中提取语法 (蓝色标记) 和示例 (红色标记) 以构建各自的检索语料库。

如图 1 所示,研究人员通过抓取官方文档来构建数据集。他们提取特定的语法元素 (蓝色标记) 和代码示例 (红色标记) 。这确保了检索系统拥有一个干净、结构化的事实来源。

核心方法: 检索增强检索 (RAR)

这篇论文的核心创新在于从独立检索转向依赖 (两步) 检索。

在标准的 RAG 设置中,你可能会运行两个并行的搜索: 一个用于搜索示例,另一个用于搜索文档。在 RAR 中,检索是顺序进行的。第一个检索器( 驱动检索器 )的输出用于指导第二个检索器( 受控检索器 )。

这在“是什么” (用户的请求) 和“怎么做” (具体的语法和用法) 之间架起了一座桥梁。

架构

RAR 流程主要在两个方向上工作,如下图架构所示:

  1. 示例 \(\rightarrow\) 语法 (\(E \rightarrow D\)): 系统首先查找与用户请求相似的代码示例。然后,它分析这些示例中的代码,以找到其中使用的具体语法定义 (文档) 。
  2. 语法 \(\rightarrow\) 示例 (\(D \rightarrow E\)): 系统首先预测需要哪些函数或类。然后,它搜索使用了这些特定语法元素的示例。

图 3: RAR 概览。( G -> E ) 驱动检索器独立 (灰色) 选择语法元素并将其传递给受控检索器,后者利用它们来选择受正向 (绿色) 和负向 (红色) 影响的示例。( E -> G ) 驱动检索器独立选择示例并将其传递给受控检索器,后者利用它们来选择受正向 (绿色) 和负向 (红色) 影响的语法元素,以及独立的 (灰色) 语法元素。

让我们仔细看看上面的图 3。你可以看到流程:

  • 左侧 (\(D \rightarrow E\)): 驱动检索器选择语法 (\(D_n\))。受控检索器随后使用该语法来查找示例。注意那些绿色和红色的箭头吗?我们稍后会讲到——它们对于处理错误至关重要。
  • 右侧 (\(E \rightarrow D\)): 驱动检索器选择示例。受控检索器查看这些示例中的代码以提取相关的语法。

嵌入如何连接语言与代码

在进行检索之前,系统需要理解自然语言 (用户的提问) 和代码实体 (所需的函数) 之间的关系。标准的嵌入模型 (如 OpenAI 的 ada-002) 擅长处理通用文本,但它们可能不知道“highlight top 5 (高亮前 5) ”意味着需要 TopBottomConditionalFormat

为了解决这个问题,研究人员对嵌入模型进行了微调。他们训练模型来最小化用户话语 \(u\) 和语法元素 \(g\) 之间的距离,但前提是该语法元素实际上出现在了该话语对应的代码中。

描述用于微调嵌入的损失函数的方程,旨在将话语与语法联系起来。

这个方程确保了用户请求的向量表示在数学上接近解决该问题所需的特定函数,从而弥合了自然语言和编程语法之间的鸿沟。

将代码映射到语法

一个技术挑战是准确识别一段代码中存在哪些语法元素。仅仅对函数名进行字符串匹配是不够的 (例如,getFormat 可能同时存在于 ChartImage 类中) 。

研究人员使用抽象语法树 (AST) 来消除这些歧义。

图 2: 从一个 OfficeScript 程序样本中提取的示例代码实体 (1 到 5) 。提取的实体利用节点的抽象语法树映射到语法节点。图中的 (5) 被映射为 TopBottomConditionalFormat,尽管 Image 和 Chart 中也存在相同的属性。

在图 2 中,我们可以看到这种解析过程。看项目 ⑤ (getFormat) 。简单的搜索可能会将其与 Chart.getFormat 混淆。然而,通过追踪 AST,系统正确地识别出这个 getFormat 属于 ExcelScript.TopBottomConditionalFormat。这种精确性对于受控检索器提取正确的文档至关重要。

“负向影响”启发式策略

这是 RAR 最精妙的部分。如果驱动检索器犯了错怎么办?

如果第 1 步检索到了不相关的示例,而第 2 步完全依赖它们,整个过程就会失败。为了缓解这种情况,RAR 使用了负向影响

受控检索器根据两个标准选择项目:

  1. 正向影响: 与驱动检索器的输出相似的项目 (假设驱动器是正确的) 。
  2. 负向影响: 与驱动检索器的输出不相似的项目 (假设驱动器可能是错的) 。

通过特意包含多样性 (即那些像第一次检索结果的项目) ,RAR 在提示词中建立了一个“安全网”,如果主要检索路径偏离目标,LLM 仍有替代选项。

实验与结果

研究人员在三种低资源语言上测试了 RAR:

  1. OfficeScript: 基于 JavaScript 的 Excel 自动化脚本。
  2. Power Query M: 一种数据转换语言。
  3. Excel Formulas: 复杂的电子表格计算公式。

数据集直接构建自文档,凸显了任务的“低资源”属性。

表 1: 数据集摘要: n 表示数据集大小,| E | 表示示例数量,| D | 表示文档页数。我们从文档中提取 E 和 D,构成了我们方法的语料库。

指标: 执行匹配 vs. 草图匹配

评估不仅检查代码是否“看起来”正确。他们使用了两个严格的指标:

  • 草图匹配 (Sketch Match): 代码的结构 (忽略变量名/常量) 是否与解决方案匹配?
  • 执行匹配 (Execution Match): 当代码在表格上实际运行时,是否产生正确的结果? (这是黄金标准) 。

结果: 依赖检索 vs. 独立检索

那么,两步走的 RAR 流程真的比标准方法更好吗?表 4 中的结果是决定性的。

表 4: RAR 与独立检索技术的比较。Context (上下文) 表示提示词中是仅包含语法 \\(( D )\\)、仅包含示例 \\(( E )\\),还是两者都包含 \\(( D + E )\\)。带有 Ret 的方法是独立检索器,下标定义了其使用的语料库。数值表示匹配准确率 % (越高越好) 。RAR 在所有上下文场景下均优于对应的 Ret 方法。

从结果中得出的关键要点:

  1. RAR 全面获胜: 看一下 Office Scripts 列。标准的语法独立检索 (\(\operatorname{Ret}_D\)) 达到了 44.35% 的执行匹配率。RAR (\(\operatorname{RAR}_D\)) 跃升至 70.49% 。 仅仅通过改变找到文档的方式,就带来了巨大的提升。
  2. 组合上下文为王: 最佳性能来自底部几行 (\(\operatorname{RAR}_{E\to D}\) 和 \(\operatorname{RAR}_{D\to E}\)) ,即同时提供示例和语法。对于 OfficeScript,\(\operatorname{RAR}_{E\to D}\) 达到了 76.40% 的准确率。
  3. 语法助力示例: 在 Power Query M 中,使用语法来查找示例有显著帮助。语法充当了搜索过滤器,确保检索到的示例实际使用了模型可能需要的函数。

提示词长度重要吗?

RAG 研究中一个常见的反驳观点是: “你们表现更好仅仅是因为在上下文窗口里塞进了更多的 Token 吗?”

研究人员分析了性能随 Token 长度 (输入给 LLM 的文本量) 变化的情况。

图 4: 性能随提示词 Token 长度增加而变化的函数关系。左图显示草图匹配准确率,右图显示执行匹配准确率。即使在 Token 数量较大的情况下,RAR 的表现也优于其基准。我们发现较短的 Token 长度足以生成准确的代码。

图 4 揭示了两个有趣的趋势:

  1. 效率: 即使在较低的 Token 计数下,RAR (方块和菱形线条) 也始终优于基准 (圆形) 。你不需要巨大的提示词来获得收益;你只需要相关的提示词。
  2. 平台期: 性能在 3,000 个 Token 左右达到峰值。在此之后添加更多上下文往往会混淆模型而不是帮助它,这验证了精确检索优于海量检索的必要性。

第二步只是在复制第一步吗?

最后,人们可能会想: 受控检索器 (\(R_I\)) 真的在做有用的事情吗,还是完全依赖于驱动检索器 (\(R_D\))?

图 5: 当从驱动器检索到的上下文大小增加时对性能的影响。在每种设置下,基准和 RAR 具有相同的 R_D 输出。唯一带来性能差异的是 R_I 的输出。这表明 R_I 并不完全依赖于 R_D。它能够自我调整,随着上下文长度的增加保持性能高于基准。

图 5 将 RAR 与驱动检索器输出完全相同的基准进行了比较。由于 RAR (实线) 始终击败基准 (虚线) ,这证明了受控检索器正在积极地增加价值。它成功地过滤并选择了驱动器错过的补充信息。

结论与启示

RAR 论文为改进特定领域的代码生成提供了一个令人信服的蓝图。通过承认文档和示例是同一枚硬币的两面,研究人员证明了顺序检索远优于并行检索。

给学生和开发者的关键要点:

  1. 上下文关系很重要: 不要只检索“相关的东西”。要检索“能解释其他东西的东西”。
  2. 文档是强大的: 对于低资源语言,结构良好的语法 (文档) 可以像少样本 (few-shot) 示例一样有价值,前提是模型知道如何浏览它。
  3. 为失败做准备: “负向影响”技术是关于健壮系统设计的一堂好课。始终假设你的第一步检索可能是错的,并为模型提供多样化的替代方案。

随着我们迈向更专业的 AI 智能体——那些需要编写晦涩的数据库查询或控制专有软件的智能体——像 RAR 这样的技术对于将通用 LLM 转化为领域专家将至关重要。