半空:LLM 辅助的 Go2Rust 项目迁移

1,431 阅读19分钟

试想一下:将一个 Golang 项目(大象)改写为(装进) Rust(冰箱) 总共需要几步?

“Gopher in 冰箱” by DALLE3

背景

当 Rust 语言为我们展示出在「性能」、「安全」、「协作」等方面诱人的特性之后,却因为其陡峭的学习/上手曲线拒人千里之外。是否存在一种科技,能够帮助我们的同学在语言学习项目迁移上完美并行,最终真正将 Rust 项目迁移这个看似美好的荆棘之果转变为触手可得的「低垂果实」呢?

为了将美好的愿望转变为实际,我们结合 LLMs 做了一些尝试,利用 LLMs 在编程语言上体现出的「涌现」能力,设计了一套基于 LLMs 的应用开发基座(ABCoder),在这个基座之上进一步演进出了我们本篇的主角:「半空」。

ABCoder 是字节内部一个编程向 LLMs 应用开发基座,包含自研的 LLMs 原生解析器、工具(Tools)以及工作流(Workflows),对编程项目本身进行深度解析、理解和压缩,并将其制作为源码知识库(Source code as Knowledge),之后利用这类知识库实现对 LLMs 推理过程中所需上下文进行补齐,从而构建出高质量、低幻觉、稳定的编程类 LLMs 应用。有关 ABCoder 更多的介绍可以参考这里

半空

TL;DR 传送门

按照 ABCoder 的设想,让 LLMs 理解编程项目的入口就是结合对项目的解析、理解、压缩后的知识关联和构建,这对于一个轻量化的应用来说可能足够(ABCoder 当前已经能够实现将一个标准 Hertz 项目“转述”为一个 Volo-HTTP 项目),但对应到实际场景中的业务项目来说(增加大量业务属性且复杂度更高),要想真正让 LLMs 完整理解整个项目,并且在有需要的时候让 LLMs 完整的将整个项目“转述”为另外一个语言的项目时我们就需要对我们的解析、理解、压缩、应用流程进行更加细粒度的设计和优化了。

「半空」主要讨论的就是对于复杂项目的理解提升辅助 LLMs 渐进式多轮迭代构建出一个复杂项目的可行性。核心需要解决的是因为项目规模提升所带来的复杂度以及上下文规模提升和 ABCoder 所制作的对应知识库知识密度跟不上的矛盾。

内核简述

罗马不是一日建成的,参考软件工程标准的项目迭代方式,迭代一个越庞大的项目,引入的标准作业流程和所花费的迭代周期和人力就越多。ABCoder 要想深刻的解析并理解一个大型项目,一口永远吃不成一个胖子。

好消息是构建一个复杂项目的过程是有迹可循的的,ABCoder 需要做的其实就是逆着项目构建的路径,反向解析出项目构建过程中涉及到的不同粒度的知识库。

之后将这些知识库输入 LLMs 驱动的 Workflows,通过构建渐进式的多轮迭代流,将原来的项目以任意编程语言又输出出来,基于对知识库的持续构建,甚至实现为其他语言的项目:语言翻译

意译 or 直译?

相较于给 LLMs 一段代码,让他直接翻译为另外一个语言(直译),「半空」所做的类比下来更像是:帮助 LLMs 理解代码,之后经过抽象和设计结合我们希望它采纳的知识,重写出另外一个语言实现的版本(意译)。

理解和设计

按照 ABCoder 的通用处理流,一个任意庞大的项目我们几乎都可以通过解析、级联压缩的方式构建函数、方法、结构体、变量的语义化映射。但仅仅通过这些散落的信息 LLMs 是没有办法高效的建立一个对这个项目系统深刻的理解。因此我们在做 LLMs 辅助的项目文档梳理应用的时候,就已经开始下意识的做一些单元聚合工作了:通过将某个包(文件/模块)中的函数、方法、结构体、变量语义化含义进一步抽象,得到关于这个包(文件/模块)的语言和框架无关的高层次语义化抽象,按照这个思路,我们可以自底向上抽象,到最终项目维度。

举个直观的例子,对于 Hertz 的项目,任意一个 Hertz 项目在项目维度都能够抽象为形如:这个项目是一个基于 HTTP 应用框架的应用,它或许注册了/a/b/c 路由 (Route)的 GET 方法(Method),关联了某个对应的逻辑(Handler)

仔细分析这个抽象,尝试对其中蕴含的细节进行总结:

  1. 一个基于 Hertz 的 Golang 项目,在经过某个维度的抽象之后,丢掉了大量细节,留下了一些在当前维度的关键信息。在上述例子中,我们得到的抽象已经不关心这个项目具体采用的语言实现和具体涉及到的应用框架了,仅仅需要关注的是 HTTP 框架应用以及 HTTP 应用必备的信息:注册了某个路由,处理了某个业务逻辑。

  2. 通过这层抽象,我们可以将任意一个复杂项目映射出了一个最简单的迭代入口:启动一个 HTTP 应用框架,并注册处理某个 URL 的某个逻辑函数。

  3. 对整个复杂项目的理解过程被我们巧妙的转换为对一个项目自底向上的逐层抽象的过程,如果我们能将这个抽象过程做的足够清晰和准确,对于一个完成抽象的项目来说,我们反过来也得到了一个支持我们至顶向下层层细化的项目构建流。

  4. 理论上通过增加、减少、优化各层级抽象,我们就能不断提升对这个项目深度理解的效果。

多轮的抽象和迭代的本质是项目在不同维度上多语言实现和 ABCoder 抽象语义的不断对齐:

配合语言对应的知识库建设,按照标准抽象块(已归一化逻辑)进行知识检索,分层分模块持续迭代,填充核心逻辑,辅助业务完成项目构建。

实施和测试

当我们通过上述解析和抽象,得到了关于一个项目完整的理解知识,之后就可以至顶向下辅助 LLMs 逐层实现项目的渐进式迭代了。同样,接着上一小结里提到例子来说,我们在这层抽象上做的事情就是:

  1. 根据「HTTP 应用框架」匹配目标语言对应的知识,比如检索出 Volo-HTTP 库的知识(如果我们的目标是将这个应用实现为一个 Rust 项目),之后结合 Volo-HTTP 提供的框架初始化逻辑,拉起一个 Volo-HTTP 的项目
  2. 之后按照本层抽象剩下的描述信息,完成**「/a/b/c** 路由 **和对应处理函数」**的注册
  3. 由于本层抽象并不具备这个处理函数的详细描述信息,因此仅仅需要生成一个空实现的桩函数即可
  4. 之后我们所做的所有变成,二次确认完成了具体实现和对应语义化抽象的对齐

以上即是对一轮迭代核心流程的描述,完成本轮迭代之后即可开启下一层抽象的对齐。之后按照这个流程持续的迭代这个项目。

因为抽象本身会丢掉本层部分细节,而丢掉的这部分细节其实还是保留在抽象前的层级中的,对应迭代路径来说,上一层丢掉的细节一定会在下一层迭代中被补充回来。因此,通过多轮的迭代构建出来的项目,理论上也并不会丢失具体的实现细节。

每一层迭代后都会有一次人工介入时机 —— 即可以及时人工介入修改代码并反馈到后续的翻译轮次中,这也是「半空」的核心能力之一 —— 在这个切面上能够按需的扩展任意的软件测试解决方案,包括时下流行的:LLMs 辅助 UT 生成等技术。等到所有的修改和测试通过之后,即可开启下一层的迭代或者选择直接退出手动接管剩余的翻译工作。

交付内容

作为用户最为关心的部分,「半空」究竟在项目 Go2Rust 转换(存量 Golang 项目改写为 Rust)上帮助我们做到哪些事情呢?其实非常简单,好比将大象装进冰箱,「半空」辅助下的 Go2Rust 自动化迁移也是三个核心步骤:

  1. 打开冰箱门:基于 ABCoder 对存量 Go 项目完成系统解析,产出函数粒度的项目理解原料

  2. 把大象放进去:基于项目理解原料产出将该项目改写为 ****Rust 对应的项目设计文档

  3. 关上冰箱门:基于设计文档中指引的迭代顺序,全自动可控地,产出各层迭代代码

实际上,结合简介中的描述,聪明的小伙伴也许已经发现:「半空」作为一套通用框架,应用面其实并不仅仅局限在 Go2Rust 上,对于任意语言之间的相互转换逻辑上都是完全一致的,区别在于对语言特异性处理和特定语言的知识库构建。「半空」一期重点针对 Go2Rust 场景完成内场的适配和持续打磨,后续如果有对更多语言栈(Python2Go/Java2Go/...)的切换诉求也非常欢迎勾搭~

项目实战举例

一个使用「半空」做 Go2Rust 项目转换的示例

项目介绍

Easy_note 是 CloudWeGo 社区对外提供的一个基于 Hertz 和 KiteX 的实现复杂、功能覆盖度高的业务实战示例项目;其使用 Hertz 提供了若干 API 接口,并在接口实现中通过 KiteX client 发起对下游 KiteX Server RPC 接口的调用。

本次使用「半空」翻译的是其 API 模块,其主要功能列表如下:

  • 用户管理

    • 用户注册 (HTTP 接口 -> RPC 调用)
    • 用户登录 (HTTP 接口 -> RPC 调用)
  • 笔记管理

    • 创建笔记 (HTTP 接口 -> RPC 调用)

    • 查询笔记 (HTTP 接口 -> RPC 调用)

    • 更新笔记 (HTTP 接口 -> RPC 调用)

    • 删除笔记 (HTTP 接口 -> RPC 调用)

涉及到的 Hertz/KiteX 框架相关的核心能力如下:

  • 初始化 Hertz Server
  • 注册 Hertz 路由和 handler
  • 实现 Hertz 自定义中间件(JWT、服务发现)
  • 实现 Hertz 的 handler 逻辑
  • 使用 KiteX Client 调用下游接口

流程说明

从输入原始项目产出 ABCoder 理解知识原料开始,「半空」会结合函数粒度知识原料,自底向上完成整个项目的逐层抽象和理解,之后至顶向下完成重构设计的制定,同时确定项目渐进式构建顺序:从粗粒度 知识映射细粒度 知识映射到最后逐个 Package 的实现,最终完成 Golang 项目到 Rust 项目的渐进式构建(意译)。这个过程中项目构建进度完全由用户掌控,结合人工修改反馈辅助协同,推动项目完成 Go2Rust 迁移落地。

上图提到的 Golang AST / Rust AST 是 ABCoder 在分析仓库代码,将函数、方法、结构体、变量等定义以树形关联出来的数据结构体集合,是一个能够与项目一比一映射的 LLMs 原生 抽象语法树

设计阶段:Package 翻译顺序

根据 ABCoder 解析后的项目原料,「半空」自动化根据 Package 的依赖关系完成了使用 Rust 重构这个项目所需的设计文档的编写,自顶向下得到如下迭代顺序:

  1. "github.com/cloudwego/b…":项目的二进制入口和基础框架搭建
  2. "github.com/cloudwego/b…":HTTP 通用 handler 的实现
  3. "github.com/cloudwego/b…":HTTP 通用 router 的注册
  4. "github.com/cloudwego/b…":HTTP 业务 router 的注册
  5. "github.com/cloudwego/b…":HTTP 业务 handler 的实现
  6. "github.com/cloudwego/b…":请求下游的 RPC 封装
  7. "github.com/cloudwego/b…":通用/业务中间件具体实现

实施阶段:根据设计文档顺序逐步展开

  1. " easy_note/cmd/api "

对应 MR: github.com/cloudwego/b…

main package,主要实现了 HTTP server 的初始化、路由注册调用等能力

Golang 原始实现「半空」意译效果
main()main()
customizedRegister()customized_register()
常量定义[本轮不实现,只mock]常量定义[mock实现]
  • 结果评估

    • 目录:

      • 所有 main package 的内容,都生成到 Rust 项目的 /src/bin/main.rs下;后续支持细粒度的文件模块映射
    • 内容:

      • 翻译的函数内容逻辑,基本正确;会将函数的具体过程用顺序表示出来,便于进行修改
    • 错误:

      • Opentelemetry 相关的使用报错;原因:目前还没有注入相关知识;不影响正常逻辑,先注释掉
    • Mock:

      • Main package 会依赖其他包的内容,因此会将其他 package 下的内容进行 mock,确保可以正确编译,但是 mock 的内容不一定完全准确,会在后续迭代完成最终实现;具体 mock 内容可参考上面的示例
  • 修改记录

    • 对 main/init 中涉及 Opentelemetry 的代码注释掉
  • 优化方式

    • 通过补充内场 Opentelemetry 相关缺失知识可以进一步提升完备率和可编译度
  • 数据统计

    • 生成节点完备率=无需改造的节点/生成节点总数

      可编译度=1-修改的代码行数/生成的代码总行数

    • 生成节点完备率: 50%

    • 生成代码可编译度:73%

  1. " easy_note/cmd/api/hertz_handler "

对应 MR: github.com/cloudwego/b…

hertz_handler package 主要实现了一个 ping handler,用于处理 ping-pong 请求

Golang 原始实现Rust 意译效果
Ping() ping()
  • 结果评估

    • 目录:

      • 所有 Golang cmd/api/hertz_handler包的内容,都生成到 Rust 项目的 /src/cmd/api/hertz_handler/mod.rs
    • 内容:

      • 翻译的函数内容逻辑完全正确
    • 错误:

      • Cargo.toml 里没有加入 "serde_json" 依赖,导致报错
    • Mock:

      • 没有尝试参考 hertz 去 mock 状态码和 utils.H,而是自行利用 volo-http 框架的能力完成响应返回
  • 修改记录

    • 增加 "serde_json"
  • 优化方式

    • 在 cargo.toml 知识里增加通用、常用的依赖
  • 数据统计

    • 生成节点完备率=无需改造的节点/生成节点总数

      可编译度=1-修改的代码行数/生成的代码总行数

    • 生成节点完备率:100%

    • 生成代码可编译度:95%

  1. " easy_note/cmd/api/hertz_router "

对应 MR: github.com/cloudwego/b…

hertz_router 包主要实现 Hertz 路由的总体注册逻辑,调用 idl 生成的路由

Golang 原始实现「半空」意译效果
GeneratedRegister()generated_register()
Register()[本轮不实现,只mock]register()[mock]
  • 结果评估

    • 目录:

      • 所有 Golang cmd/api/hertz_``router包的内容,都生成到 Rust 项目的 /src/cmd/api/hertz_router/mod.rs
    • 内容:

      • 翻译的函数内容逻辑完全正确
    • 错误:

      • 没有正确地将下层依赖 pub 出来,而是直接使用了依赖路径
    • Mock:

      • IDL 生成的路由注册部分,将其 mock 出来
  • 修改记录

    • 将 "hertz_router/demoapi" mod pub 出来
  • 优化方式

    • 在生成代码后对新增内容做一次解析和关联
  • 数据统计

    • 生成节点完备率=无需改造的节点/生成节点总数

      可编译度=1-修改的代码行数/生成的代码总行数

    • 生成节点完备率: 100%

    • 生成代码可编译度:88%

  1. " easy_note/cmd/api/hertz_router/demoapi "

对应 MR: github.com/cloudwego/b…

hertz_router/demoapi package 主要实现了具体了路由注册(idl 映射)以及 Hertz 中间件的定义

Golang 原始实现「半空」意译效果
Register() register()[路由注册有问题,需要 check & 修改]
rootMw() root_mw()[包含了中间件里的 mock 实现]
mw 定义mw 定义
CreateUser[本轮不实现,只mock] create_user[mock]
  • 结果评估

    • 目录:

      • 所有 Golang cmd/api/hertz_``router/demoapi包的内容,都生成到 Rust 项目的 /src/cmd/api/hertz_router/demoapi/mod.rs
    • 内容:

      • register(): 路由注册的逻辑对应上了,但是实现不对;生成的路由没有和原始的路由一比一映射成功,是根据函数的描述自行生成的路由:需要用户手动将路由修改正确,参照生成的用法很快就可以实现

      • root_mw():

        • 能够以注释的形式描述出来 root_mw 里所需要做的内容,但是没有正确实现。因为 volo 里没有这样把多个中间件组成一个切片的操作:需要用户自行补充实现
        • 没能实现 recovery、RequestId、Gzip 的中间件逻辑;主要原因是无法推测出这些功能在 rust 里的实现方式:需要用户自行补充实现
      • 其余的中间件均正常

    • 错误:

      • register() 的路由注册逻辑不对,优化思路如下:

        • 这部分是 IDL 映射的内容,本身就被拆的比较细;后续会做框架之间的 IDL 映射
        • 增强函数的细节描述
    • Mock:

      • Mock 实现了所有的handler内容,这部分没什么问题
  • 修改记录

    • 对路由逻辑进行重新梳理和注册
    • 对 recovery/request_id/jwt 中间件的逻辑进行实现(ps. 示例还未实现,暂时注释掉)
    • 删除/添加一些依赖信息
  • 优化方式

    • 增强细节逻辑的总结&实现能力
  • 数据统计

    • 生成节点完备率=无需改造的节点/生成节点总数

      可编译度=1-修改的代码行数/生成的代码总行数

    • 生成节点完备率:62%

    • 生成代码可编译度:76%

  1. " easy_note/cmd/api/hertz_handler/demoapi "

对应 MR: github.com/cloudwego/b…

  • Hertz_handler/demoapi package 主要实现了具体的 HTTP 接口实现,下面使用 "create_note" 作为展示
Golang 原始实现「半空」意译效果
CreateNote() create_note()
SendResponse()send_response()
rpc CreateNote[本轮不实现,只mock] rpc create_note[mock]
ErrNo[本轮不实现,只 mock]ErrNo[mock]

handler 这轮翻译完,出现的代码报错较多,主要原因如下:

  1. 是代码量本身比较大,同样错误报错多次

  2. handler 里涉及了一些业务逻辑以及业务在 golang 里的特定的用法,LLMs 不能很好转换

以下都以"create_note" 接口为例,进行结果评估

  • 结果评估

    • 目录:

      • 所有 Golang cmd/api/hertz_``handler/demoapi包的内容,都生成到 Rust 项目的 /src/cmd/api/hertz_handlers/demoapi/mod.rs
    • 内容:

      • create_note(): 能把原 create_note 的逻辑按顺序进行实现,包括 获取参数、发起调用、返回响应等
      • send_response(): 基本能实现出原接口的含义,但是错误较多,图里展示的是手动修改过的
    • 错误:

      • create_note(): 逻辑是正确的,主要有以下错误

        • mock 的结构体,没有带 #[derive(Debug, Deserialize)]需要用户补
        • send_response() 的调用无法对齐,一直报错
        • 获取请求上下文的时候,可能会有误传参
      • send_response(): 整体逻辑是对的,但是不会用 volo-http 的写响应方式

    • Mock:

      • 直接 mock 的内容基本都正确不需要修改
      • 没有去对二级依赖进行mock,导致会有些编译错误;例如,当前接口依赖了 "rpc/create_note",其又依赖了 "NoteDate" 类型,这个没有进行实现
  • 修改记录

    • send_response 的逻辑重新实现
    • 修改 handler 的调用逻辑,以及一些 ctx 上下文传参的问题
    • 增加/删除一些依赖信息
  • 优化方式

    • 补充 volo-http 的请求/响应相关操作示例,指导 LLMs 生成更准确的 SDK 使用姿势
  • 数据统计

    • 生成节点完备率=无需改造的节点/生成节点总数

      可编译度=1-修改的代码行数/生成的代码总行数

    • 生成节点完备率:14%

    • 生成代码可编译度:88%

至此,我们就完成了 "github.com/cloudwego/biz-demo/easy_note/cmd/api" 这个 moudle 的全部翻译,用户在 check 完整个项目后,即可以编译 & 运行项目。

总结

整体意译效果说明

  • 函数翻译完备性

完备性说明:完全无需人工介入的函数统计为完备函数

package生成函数的个数完备函数的个数完备率
easy_note/cmd/api4250%
easy_note/cmd/api/hertz_handler11100%
easy_note/cmd/api/hertz_router11100%
easy_note/cmd/api/hertz_router/demoapi13862%
easy_note/cmd/api/hertz_handler/demoapi7114%
  • 代码可编译度

可编译说明:相对于整体生成代码行数,人工介入修改的代码行数占比,需要修改的代码越少,可编译度越高

package生成函数的行数人工修改的代码行数可编译度
easy_note/cmd/api1062873%
easy_note/cmd/api/hertz_handler19195%
easy_note/cmd/api/hertz_router9188%
easy_note/cmd/api/hertz_router/demoapi1733876%
easy_note/cmd/api/hertz_handler/demoapi2543088%

整体上,通过知识库的持续建设和关键知识的补齐,「半空」在完备性和可编译度上也会随之持续提升。

语言学习和项目迁移

在这个过程中,结合「半空」为我们生成的 Rust 项目设计文档,从整体项目的角度出发,逐步对每个包进行深入理解、翻译与确认。这一过程条理清晰、循序渐进地将一个 Golang 项目从零构建为一个 Rust 项目。同时,我们一同参与项目构建的每一个迭代,「半空」每一个迭代生成的代码完全遵循内场和业内 Rust 项目编写的最佳实践,这不仅帮助我们深刻理解整个项目,同时也为学习一门新语言提供了极大的支持。通过这种逐步渐进迁移的方式,我们能够不断深入学习并掌握 Rust 语言及项目本身,最终成功完成项目的转型。