LLM 驱动的 Go 到 Rust 项目迁移的挑战与实践

随着 LLM 不断发展、功能逐渐强大,我们发现可以基于 LLM 做很多具有挑战性的应用。比如,当我们公司内正在切换语言栈,那么基于 LLM 进行现有项目的迁移,无疑是一个高效的方式; 特别是从 Go 语言迁移到 Rust 语言,这一过程不仅涉及到语法和语义的转换,还需要处理语言生态、项目结构切换带来的的复杂问题。为了应对这些挑战,字节跳动基础架构服务框架团队使用 LLM,基于名为 ABCoder 的 AI 编程伙伴和一个称为 “半空” 的渐进式迁移工作流程,有效地简化和加速了语言迁移过程。本文由字节跳动研发工程师范广宇在 CloudWeGo 深圳站技术沙龙演讲实录整理而成。

图:字节跳动研发工程师范广宇

一、缘起:ABCoder:基于 Al 的编程伙伴

ABCoder:Al-Based Coder

首先,让我们详细了解一下 ABCoder,这是我们项目迁移的核心工具。ABCoder 是一个面向AI编程应用的开发工具,我们在 CloudWeGo 的三周年活动时介绍过这个工具。ABCoder 的主要功能是将整个编程项目抽象成一棵与具体编程语言无关的语法树,使得项目的结构和逻辑更加清晰。除此之外,ABCoder 还配备了一整套 AI 驱动的工作流程,包括执行索引操作和进行复杂的模型理解任务,这些都是为了更深层次地支持代码理解。

Golang Parser 介绍

接下来,我将通过一个实际示例来展示 ABCoder 的工作机制。假设我们有一个完整的 Hertz 项目,如右侧图所示的项目结构。以项目中的mainpackage 为例,我们可以识别出main函数。从这个main函数出发,以它的调用关系为边,我们可以构建一棵详细的调用关系树。

在这棵调用关系树中,每个节点代表一个函数、类型或者变量的定义。每个节点不仅记录了相应的源码信息,还包括了详细的调用关系。例如,我们可以看到一个特定的节点—— main 函数,它包含了丰富的信息,如在代码中的引用位置、函数名、源文件名、行号等。此外,此节点还详细记录了该函数内部所调用的其他函数和方法。

这种结构化的表示允许开发者快速理解代码的依赖和结构,极大地促进了代码维护和迁移工作的效率。通过这样的展示方式,ABCoder 不仅帮助开发者把握项目全局,也便于进行具体的代码审查和优化。

此外,ABCoder 采用深度优先遍历来理解项目中每个代码节点的含义,这种方法是从底部开始向上执行,从而逐层构建对整个程序的理解。以图示的 main 函数为例,假设它调用了 register 函数和 Server Default 函数,ABCoder 首先分析 register 函数的功能和作用,然后分析 Default 函数。在理解了这两个函数的基础上,ABCoder 能够对 main 函数的内容进行全面的语义填充。

这种方法使得我们能够清晰地看到,main 函数不仅是应用程序的入口点,它还负责创建一个 Hertz 实例,并进行路由注册与流量监听等关键操作。通过这种方式,ABCoder 不只是帮助我们理解单个函数,而是提供了对整个应用程序结构和行为的深入洞察。在此基础上,我们的团队已经围绕 ABCoder 展开了广泛的实践,包括项目的全面理解和研发流程支持等方面。这些实践不仅增强了我们对项目的掌握,也极大提高了研发效率。

ABCoder 开源

当前主流 AI 编程应用的往往采用单层语法解析+原始代码embedding方式实现,存在上下文信息密度低、无法依赖跳转问题,导致 API 调用幻觉、代码细节缺失、插入位置不当,且多为闭源 GUI 应用,难以修改定制,无法满足日益增长的离线复杂编程需求。为了解决上述问题,ABCoder 采用 深度语义解析+级联理解 方法,建立了一套 精确、可扩展、AI亲和的结构化代码上下文(UniAST)代码深度理解+语义化召回体系(CodeRAG),内部落地了仓库理解服务、代码翻译等应用,并沉淀出代码批处理开发框架。目前,统一抽象语法树 和 通用语义解析器 部分已经开源到 cloudwego/abcoder,欢迎使用、共建!

二、半空: 渐进式 Go2Rust 迁移工作流

快速掌握 Rust,轻松开启高效、安全的编程之旅

今天,我将主要介绍我们在语言迁移方面的创新应用——“半空”。这是一个专注于 Go 到 Rust 迁移的渐进式工作流程。此项目之所以被命名为“半空”,是因为它是我们更广泛愿景“空”的一部分,目前这个项目已经完成了一半的目标。稍后,我将详细介绍“空”项目的终极愿景。

  1. “半空”发起的背景

在介绍项目的背景之前,如果您熟悉 CloudWeGo,您可能知道我们不仅维护 Golang 的微服务框架,还维护了一套完整的 Rust 微服务框架生态。在公司内的一些 Rust 推广中,我们发现将某些 Golang 服务迁移到 Rust 后,在延迟和 CPU 使用效率方面都有显著改善。因此,我们考虑可以将公司内部的一部分 Golang 服务迁移到 Rust,以此来提升性能。

然而,Rust 语言的学习曲线比较陡峭,且公司内熟悉 Rust 的开发人员有限。鉴于这些情况,我们想到可以利用 ABCoder 的深度代码理解能力和大模型的支持,构建一个自动化的方案,将 Golang 项目迁移到 Rust,以实现性能优化。因此,我们启动了“半空”项目。

“半空”是一个渐进式的 Go 到 Rust 翻译工作流程,其中包含两个关键概念:一是“Go 到 Rust”,这是我们项目的主要目标;二是“渐进式”,即我们采用逐步迁移的方法,确保每个阶段正确性性和可控性。这种方法不仅可以逐步验证新系统的效能,还可以减轻开发团队的负担,使迁移过程更加平滑。

  1. 渐进式翻译

在此,我将详细解释渐进式翻译的概念。以一个典型的 Golang 项目为例,这类项目通常由多个 go package 构成。与一次性完成整个项目翻译并交付的方法不同,渐进式翻译采取一种分阶段的策略。具体来说,我们首先对项目中的所有 package 进行优先级排序,然后按顺序逐个翻译每个 package,并将每个翻译阶段的成果逐一交付给用户。

这种方式的核心优势在于,它允许用户在每个阶段结束时评估和验证翻译的质量。只有在获取用户的反馈并确认他们对翻译结果满意后,我们才会推进到下一个 package 的翻译。这样的渐进式处理不仅确保了翻译质量,还使的用户能够更好地控制项目迁移的进度和结果,从而增加了迁移项目的正确率。

1️⃣ 为什么要渐进式翻译?

整体交付方式的问题
  • 代码可读性差

我们选择不采用整体交付方式,背后有着充分的考虑。在项目初期,我们尝试了整体交付模式,但在与用户的沟通中意识到,实际业务代码的规模往往非常庞大。如果直接将整个项目的代码翻译成 Rust,这对用户而言,就像是在处理一个不透明的黑匣子。用户面对这些大量的翻译代码时往往感到无从下手,而且由于代码量庞大,代码的可读性也相对较差。

  • 面临海量报错,修复难度大意愿低

此外,尽管大模型通常能够准确地翻译业务逻辑,但偶尔也会出现编译错误,这可能导致整个项目无法正常启动。我们发现,用户对于自行解决这些编译问题的意愿通常较低,这进一步降低了他们参与迁移工作的积极性。

  • 翻译错误传播

还有一个重要的问题是,整体交付可能导致错误的传播。例如,如果某个关键节点的逻辑翻译出现错误,后续依赖于该节点的其他部分也可能出现问题。这样的错误链可能导致整个项目的翻译结果不可用或不正确。因此,为了提高项目的成功率和降低潜在的风险,我们最终决定采用渐进式的交付方式,从而使每个阶段的成果都能得到用户的充分验证和批准,确保整个项目的质量和可用性。

渐进式翻译的优势

鉴于前述问题,我们使用了渐进式翻译的策略,该策略具备以下显著优势:

首先,渐进式翻译采取逐轮、逐个包的翻译方式,每一轮交付的代码量都相对较少。这种方法确保用户能够集中精力对代码的逻辑和可编译性进行详细的校验,从而提高代码的质量和稳定性。

其次,渐进式翻译支持灵活的启动和停止。用户在完成某一轮翻译后,如果认为项目架构已基本形成,可以选择基于已翻译的部分继续手动翻译,或者暂停自动翻译服务。这种灵活性使项目迁移可以更加贴合实际需求和进度。

最后,我们在渐进式翻译过程中引入了人工交互机制。这一机制的主要目的是通过与用户的持续沟通和反馈,确保每一轮的翻译都尽可能准确无误。通过每轮的精确校准,我们能够逐步构建出完全正确的 Rust 代码,从而最终实现高质量的项目转换。

这些优势共同构成了渐进式翻译方案的核心价值,使其成为处理大规模和复杂代码项目的理想选择。

2️⃣ 人工交互在渐进式翻译中的角色

除了前述目的,人工交互在渐进式翻译中还扮演着其他两个关键角色:

首先,通过人工交互,用户可以逐步深入了解 Rust 代码。在这个过程中,用户从最初可能对 Rust 代码感到陌生,逐步通过搜索和学习相关知识,最终能够理解并掌握 Rust 的基本语法。我们希望通过这种逐步引导的方式,帮助用户快速适应新的编程环境,掌握必要的技能。

其次,人工交互的引入也旨在让用户深度参与到从 Go 到 Rust 的迁移过程中。这种深度参与不仅使用户对项目的整体流程和细节有更加深刻的理解,还有助于消除用户对于项目翻译过程的不透明感,避免让整个迁移过程显得像一个“黑盒”。这种透明和参与性的提高,有助于增强用户的信任和满意度。

通过这两方面的作用,人工交互在渐进式翻译项目中不仅提高了代码质量和项目的可接受度,还助力用户更好地适应技术变迁,提升了整个迁移过程的效果和效率。

3️⃣ 翻译目标

上面已经介绍了渐进式翻译的基本概念,现在我们将具体说明我们的翻译目标。整个翻译流程基于以下几个步骤完成:首先,使用 ABCoder 生成的 Golang 语法树(AST)作为输入,通过我们的“半空”翻译系统,这些 Golang 语法树被转化为对应的 Rust AST。接着,利用代码生成器(Generator),将这些 Rust AST 写入到相应的 Rust 源代码文件中。这样,就完成了整个项目从 Golang 到 Rust 的翻译。

  1. 渐进式翻译过程

渐进式翻译的过程可以被细分为四个主要部分,每部分处理流程扮演一个关键 Agent,确保翻译的准确性和高效性。通过这种方法,我们不仅确保了代码翻译的连贯性和一致性,还为用户提供了一个清晰、可跟踪的翻译流程。这样的系统设计使得整个翻译过程既透明又易于管理,极大地简化了从 Go 到 Rust 的迁移复杂度。

1️⃣ ABCoder 项目理解

首先,让我们深入了解项目理解阶段。在这一阶段,我们主要关注翻译过程图示的前三个节点。利用在第一节中介绍的 ABCoder 工具,我们执行项目理解和项目压缩任务。通过这种方法,我们能够有效地提取出 Golang 项目的AST。这一步是整个翻译过程的基础,因为它为之后的翻译规划和执行提供了必要的结构信息。

2️⃣ Package 翻译顺序规划

在翻译流程的第二部分,我们专注于规划 package 的翻译顺序。首先,根据从 Golang 语法树中提取的信息,我们分析所有 package 的调用和引用关系,从而构建出一个详细的 package 之间的调用关系图。

接下来,我们采用自顶向下的策略对这个关系图进行展开,优先翻译那些被依赖性较少的 package。

例如,对于给出的语法树,我们会按照如下顺序进行翻译:首先翻译main package,随后按照package 1package 2package 3package 4的顺序继续进行。这样的顺序安排帮助我们逻辑清晰地推进整个翻译过程。

在项目翻译的初期阶段,我们也深入探讨了翻译顺序的选择,具体是采用自顶向下还是自底向上的顺序?

首先考虑自顶向下的方法,它更符合我们编写代码的习惯,允许我们先建立项目的整体框架,随后在此框架的基础上逐步完善细节。此外,自顶向下的方法具有操作灵活性,支持随时开始和暂停翻译过程,便于人工直接接手代码编写。尽管如此,自顶向下的方法也存在一些劣势。例如,在主要的main package翻译完成之后,package 1package 2还没有被翻译,这会导致项目在某些阶段无法编译。为了解决这个问题,我们提出了一个策略:在翻译main package时,先 Mock package 1package 2所需的内容。这种前置的 Mock 允许整个项目在逻辑上保持连贯,直到我们开始具体翻译package 1package 2,才会真正填充它们的实际实现。这样的处理策略确保了翻译工作的连续性和项目的可编译性。

之后,分析下采用自底向上翻译顺序可能遇到的问题。在自底向上的翻译策略中,通常会首先翻译叶子节点,例如按照package 3package 4package 1package 2以及最后是main的顺序进行。这种方法的主要优势在于,它允许我们先翻译那些被依赖较少的组件或函数。这样,当翻译到依赖较多的函数或模块时,它们所需要的所有依赖函数已经被实现,可以直接使用。尽管自底向上的方法具备其优势,但它也存在一些不足。例如,使用这种方式可能会首先翻译一些通用或工具类(common 或 util)的函数。即便这些函数已翻译完毕并交付给用户,用户可能仍不清楚如何有效地使用它们,这不如自顶向下的方法直观便捷。

鉴于此,我们采纳了一种折中的策略,在正式翻译工作开始前,实施一个名为“cycle 0”的初步阶段。在这一阶段,我们采用自底向上的顺序对项目中的所有变量和类型定义进行翻译。这一步骤是必要的,因为在翻译具体的函数或方法之前,通常需要先确保它们所依赖的类型已经被正确实现。

在完成了 cycle 0 之后,在正式的翻译阶段,我们则采用自顶向下的顺序进行,这样可以最大限度地发挥这种方法的优势,同时确保项目的整体框架和架构清晰。

此外,在 cycle 0 中,除了进行类型定义和全局变量的翻译外,我们还进行了框架映射。目前,我们已经对 CloudWeGo 下的框架进行了默认适配。例如,如果您的微服务项目是基于 Hertz 或 Kitex,我们会自动将其映射为 Volo HTTP 和 Volo 框架,并完成相应的代码生成工作。这样,用户在后续的开发过程中可以直接调用这些框架,从而提高开发效率和项目的可维护性。

3️⃣ 翻译 WorkFlow

下面将介绍本次分享的另一个重点内容 —— 我们的翻译工作流程。此工作流程是项目的核心部分,半空团队为 Go 到 Rust 的转换专门设计了一套详细的工作流程。

首先,对 Golang 代码进行语义无关的抽取,即从代码中提取函数的业务逻辑和实现过程,而屏蔽掉具体的语言细节。

其次,我们接着进行知识召回,这一步骤利用已有的知识库来理解和解析代码结构。

之后,是前置 Mock,通过这一操作,我们提前 Mock 所有还没有翻译好的函数。

最终,完成最终 Rust 代码的翻译;此外我们还有个 double check 的工作,确保转换后的 Rust 代码既保留了原始逻辑的完整性,也符合 Rust 的语言习惯和性能优化标准。

下图展示了 CloudWeGo 下的easy_note项目的 API 层结构。在这个项目中,翻译的起始点是main package中的mainother函数。一旦这些核心函数完成翻译,我们随后会逐个处理Initregister等其他函数。现在,我将以main函数的翻译为例,详细介绍我们的翻译工作流程。这将帮助大家更好地理解整个翻译过程中的步骤和方法。

首个步骤是任务规划。在这一阶段,我们的输入数据是通过 ABCoder 压缩处理后的节点内容,例如针对main节点,输入包括main函数的完整源码、它所调用的函数和方法,以及main函数中所有的语义化描述步骤。这些信息经过抽取和整理之后,将输入到我们预先设计的任务规划 Agent 中。

下图展示了任务规划的具体处理流程:将 main 函数尽量抽象成为一个个语言无关的任务需求描述,这个描述详细定义了该需求的目标、实现该需求的过程,以及实现该需求用到了哪些依赖,指导我们在翻译rust代码的时候使用哪些依赖,避免llm出现幻觉。

完成任务规划后,我们得到了一个与语言无关的描述。这一描述虽然已脱离原始语言 Golang 的具体语法和结构约束,但在描述中仍可能包含对某些 golang 中间件的使用,例如 Hertz、OpenTelemetry 和 pprof 等。

直接将这些描述输入到模型中可能会导致模型产生幻觉,即根据其之前训练的数据或基于假设进行代码生成,这无法满足我们的精确翻译的需求。为了解决这一问题,我们开发了一个基于向量化和语义搜索的支持召回功能的 agent。该 Agent 能够根据给定的语义描述搜索与之相关的 Rust 实现知识,并确定这些知识在 Rust 中的具体应用方式。

在获取了所有必需的知识之后,我们还需要执行前置 Mock。正如之前提到的,由于我们采用自顶向下的翻译方法,在翻译 main 函数时,其依赖的内部函数调用还未具体实现,因此我们需要通过提前 Mock 来确保当前轮次的翻译是可编译的。

在 Mock 过程中,我们的首步是执行静态映射。以main函数中使用的init函数为例,我们通过静态映射的方法将其转换成 Rust 语言的对应格式,并将其映射至正确的 cmd::api 下的init函数。接着,我们将这些已翻译的节点和相关信息输入到前置 Mock API 中,以便对整个函数进行 Mock。在 Mock 实现中,函数体暂时以Todo形式表示,表示该函数尚未完全实现。此外,我们还会添加模拟标识符,这有助于在后续的实现阶段,根据函数的签名和Mock 结果来进行更精确的代码实现。这样的步骤确保我们在实际编写和完善代码前,能够预见和解决潜在的问题,提高开发效率和代码质量。

通过上述步骤,我们已经完成了翻译前的所有准备工作,并准备好了进行翻译该函数所需的全部上下文。具体来说,我们确定了目标 Rust 函数名为main,解构了目标任务,并详细记录了实现过程、函数调用的引用路径等关键信息。同时,我们也掌握了必要的 Rust 基础知识。

最终,我们利用一个最终实现 Agent 完成翻译工作,就可以生成相应的 Rust 代码。生成的代码如上图所示,我们可以模拟“人工交互的过程”对生成的 Rust 代码与原始的 Golang 代码进行对比,确保翻译的准确性和效果。这一过程不仅提高了代码的正确性和可维护性,也确保了功能的一致性。

大家可以看到,我们已经成功地将 Hertz 框架中的一些路由注册和使用模式映射到了 Volo 框架。此外,转换后的 Rust 代码还详细标注了每一步操作,包括初始化组件、注册路由等。特别需要注意的是,由于 Rust 中的某些依赖尚未完全实现,目前我们通过在代码中添加注释的方式来规避这些问题,这样做可以方便用户直接编译和使用这些代码。

在完成代码的最终实现之后,我们还引入了一个双重检查机制。这一功能主要用于对生成的 Rust 代码进行二次验证,确保代码严格遵守 Rust 的编码规范、并降低编译报错的数量。具体来说,我们与内部的算法团队密切合作,训练了一个专门用于 Rust 代码优化的加强小模型,这个模型专注于加深对 Rust 代码的理解。得益于其强大的代码理解能力,这个模型能够对生成的代码进行深入分析并提出优化建议,从而提高代码的质量和性能

在详细介绍了main节点的翻译流程之后,接下来我们需要构建相应的 Rust 语法树。最

首先,这棵语法树只包含一个空的根节点。随着main函数翻译的完成,我们将main函数的节点逐步插入到这棵 Rust 语法树中。当main包中的所有内容都翻译完成后,我们就能得到一棵完整的、包含所有节点的 Rust 语法树,如图所示。这棵语法树反映了代码结构的层级关系。

以上便是我们整个翻译工作流程的概述。在这里,我将对工作流程做一个简要的总结。我们的工作流程主要具有两个显著特点:

首先,翻译过程是对原始 Golang 逻辑的高度抽象,在保留原始业务逻辑的同时摆脱Golang的约束;使得翻译的 Rust 代码摆脱 Golang 的影子,达到“意译”的效果

其次,我们的知识库可以高度定制化领域特定知识。这一特性使得翻译后的 Rust 代码不仅遵循 Rust 的编程规范,还符合特定组件的最佳实践,进一步提升了代码的效率和可维护性。

4️⃣ 用户交互

接下来,我将介绍我们系统中的用户交互部分。在这一阶段,我们已经构建了一棵完整的 Rust 语法树。基于这棵语法树,我们将按照 Golang 代码一比一的组织结构将其转写为 Rust 源代码。完成这一过程后,我们将所有生成的 Rust 源码展示给用户,以便他们可以直接查看和进一步处理。

此阶段需要用户直接参与到翻译流程中来。我们期望用户能够检查生成的代码,以确认是否存在逻辑或编译错误。如果检查到错误,需要用户做一些必要的修改,确保项目逻辑正确、且可编译。用户完成修改后,便可以直接提交代码。对于用户所做的修改,我们利用 LSP(Language Server Protocol)开发了一个 Rust 版本的 ABCoder 解析器。通过这个解析器,我们可以对用户的修改和添加的内容进行重新解析。解析结果将即时反馈给用户,同时也为我们下轮的翻译提供了及时更新的数据。这样的机制确保了翻译质量的持续提升。当最后一轮的翻译工作完成后,我们最终将获得一个完整的 Rust 项目。

这样,我们就完成了一个 Golang 项目翻译到 Rust 的全部流程。

  1. “半空”项目的核心特点

在此,我将总结“半空”项目的几个核心特点:

  • 项目梳理:利用 ABCoder 的深度代码理解能力,能够帮助用户详尽地梳理项目。这包括对 Golang 源码的组织形式、代码结构和细节进行精细分析

  • Rust 新手友好:翻译过程对 Rust 新手极为友好。通过鼓励用户参与翻译,用户不仅能够学习和掌握 Rust 的基础语法,还能加速团队成员对 Rust 的掌握,从而节省了大量的培训和人力成本。

  • 渐进式翻译:我们希望用户能够积极参与到翻译过程中,使得翻译系统不仅仅是一个封闭的黑盒,而是一个对用户开放、可控的平台。

  • 翻译准确率高:我们建立了针对多个组件的知识映射库。此外,我们和算法团队深度合作,他们提供了强化代码理解的模型,进一步提高了 Rust 代码翻译的准确性。

这些特点共同构成了我们半空 go2Rust 的优势,使其能够在实际应用中提供显著的效益。

三、未来展望

这里我将对我们未来的工作进行一些展望。

首先,我们计划增强人机交互的功能。目前,翻译工作主要是由我们的团队完成的初步翻译,然后交由用户进行后续处理。为了提升交互性和便利性,我们正考虑开发一个 IDE 插件。将我们的翻译技术以插件的形式集成到用户的开发环境中,用户将能够更加直接和便捷地执行代码迁移和翻译任务。这不仅能提高工作效率,还能增强用户体验。

此外,我们希望构建一个能够实现多语言代码互转的翻译工作流程,暂名为“空”。目前,我们已完成了这一愿景的一半,因此项目被称为“半空”。如果我们能按照“半空”的架构完善所有语言的翻译流程,我们最终将能实现各种编程语言之间的无缝转换。

在实现多语言互转的基础上,我们还计划进一步发展 ABCoder 的 UniAST,将其转化为一种与具体编程语言无关的需求规范树。这将使我们能够根据抽象需求直接生成多种编程语言的代码,有效屏蔽不同语言之间的差异,从而大大简化跨语言的软件开发过程。

相关链接:

  • CloudWeGo: https://github.com/cloudwego

  • ABCoder:https://github.com/cloudwego/abcoder

  • 本次演讲视频回顾:https://www.bilibili.com/video/BV1b6Z7YQERT?spm_id_from=333.788.videopod.sections&vd_source=d8dd7b72c822f1b3cd619e69cfb25709

  • PPT 下载链接:https://github.com/cloudwego/community/tree/main/meetup/2025-03-22

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值