ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

从内部工具到开源:Neovate Code 的 AI 编程助手架构与落地实践

从内部工具到开源:Neovate Code 的 AI 编程助手架构与落地实践 看到 Neovate Code 要从内部工具变成一个正式开源项目确实让我花了不少时间做心理建设。过去一年里我在维护这个智能编程助手的过程中收到了太多来自开发者的私信和 issue问得最多的就是能不能开放源码我想自己改一改。如今这个决定落地了我想把这套工具从设计思路到具体实现再到开源之后的规划都摊开来和各位聊聊。这篇文章适合这几类读者准备做 AI 编程工具但不知从何下手的人、正在纠结开源协议与商业化怎么共存的项目维护者还有单纯想找一个能离线用、能自己改的智能补全插件的普通开发者。我会尽量把技术决策背后的理由讲透也会把我在开源过程中踩过的坑一起写出来。1. 为什么一个又一个 AI 编程助手值得开源1.1 从内部工具到独立项目Neovate Code 的起点Neovate Code 最开始并不是一个对外宣称的产品而是我们团队内部为了处理大量遗留代码而写的一个命令行工具。当时团队要在一套维护了好几年的嵌入式 C 代码库里做模块拆分几千个文件来回跳IDE 自带的跳转和搜索根本顶不住我们就写了一个基于语法树索引的本地检索工具能快速定位函数定义、调用链和宏展开关系。这个工具在内部用了半年之后我们发现一个很有意思的需求开发者在看一段陌生代码时最花时间的往往不是找到某个函数而是理解这段代码为什么要这么写。于是我们开始尝试把代码检索和当前编辑上下文结合起来先用规则引擎提取代码结构再把结构信息发给大语言模型做解释和补全建议。这个过程慢慢就长成了 Neovate Code 的雏形。到了 2024 年中我们决定把它做成一个独立的编辑器插件支持 VS Code 和 JetBrains 系 IDE。之所以能做下来靠的是两件事一个是底层对代码解析的积累另一个是我们在后端做的模型请求缓存和异步调度让补全延迟压缩到了可接受的范围。1.2 闭源时期让我睡不着觉的三个问题闭源维护了将近一年我遇到的麻烦比想象中多得多。第一个问题是用户信任成本很高。AI 编程助手天生就要处理用户的一部分代码上下文闭源的情况下用户只能靠开发者说不会上传这句话来建立信任。我不止一次在用户群看到有人问这个工具到底把我的代码发到哪里去了哪怕我们在文档里写清楚了数据流还是会有人不放心。第二个问题是需求反馈的回路太长。用户想要某种语言的语法增强支持或者希望某个快捷键行为改成另一个 IDE 的风格这种需求堆积在 issue 里我一个个看过去却很难快速落地因为我不可能熟悉所有用户的使用习惯和插件生态。第三个问题也是最终推动我下决心的是私有化部署的需求根本接不住。不少企业用户对数据安全要求很高他们想要的是一个能完整跑在内网、代码完全不出域的版本。但在闭源模式下做私有化部署等于把整个核心代码交给对方这让我非常为难。1.3 开源协议的取舍为什么是 Apache 2.0 而不是 MIT 或 SSPL确定开源之后许可证的选择是第一个绕不开的决策。我在 Gitee 和 GitHub 上对比了很多项目的做法也查了不少关于许可证兼容性的讨论。最终选择 Apache 2.0我的理由很简单它允许商用、允许修改、允许闭源分发衍生作品这对企业用户来说负担最小。它包含明确的专利授权条款对于涉及 AI 模型调用的项目来说这个保护很重要。它和大多数主流开源许可证兼容后续如果要引入某些 LGPL 或 EPL 的组件不会产生授权冲突。我没有选 MIT是因为 Neovate Code 未来可能会和云服务做联动Apache 2.0 给了我们保留商标权和署名要求的空间。我也没有选 SSPL 或 AGPL因为那会直接劝退很多只打算在公司内部做工具链集成的开发者。这里多说一句开源不等于放弃商业化。Neovate Code 走的是开源核心 可选云服务的路线核心引擎完全开源但如果你需要模型网关、团队协作面板这类功能可以选择自己部署开源方案也可以使用我们提供的托管服务。两种方式的核心代码是一样的不存在功能阉割。2. Neovate Code 到底解决了什么问题2.1 定位差异它不是生成代码的玩具而是理解代码的第二大脑市面上大多数 AI 编程助手主打的是你说需求我生成代码这种交互在写独立函数、算法片段和脚本时非常好用但放到大型项目里效果会大打折扣。原因很简单大模型并没有真正读过你的整个代码库它只是在根据你当前打开的文件和剪贴板里的内容做推测。如果你正要对一个牵一发动全身的旧模块做改动它给出的建议往往会忽略掉上下文之间的约束关系。Neovate Code 的设计初衷就是先把代码库索引这件事做好再在这个基础上去做 AI 辅助。所以它的核心并不在于模型本身多强大而在于模型读取上下文之前我们已经帮它准备了什么。2.2 四个核心场景的取舍与优先级我在整理了社区反馈之后把 Neovate Code 的能力收敛到了四个场景依次是代码补全在光标处生成符合当前代码风格的续写内容。这个场景最常用也最容易做坏难点在于如何选择补全的触发时机。代码解释选中一段代码生成中文或者英文的解释。我们默认生成中文因为调研发现用户群里中文开发者的比例超过七成。重构建议对当前函数或类提出可操作的重构步骤而不是直接甩给你一段改完的代码。这需要先做数据流分析再让模型基于分析结果给出建议。生成单元测试根据函数签名和现有使用场景生成测试用例。这个功能目前只对 Python 和 Java 支持得比较好C/C 的还在打磨。这四个场景的顺序不是随意排的而是按照频率越高越靠前、误伤越大越谨慎的逻辑。代码解释和补全属于低频高风险不高重构建议和测试生成一旦错了会直接影响代码质量所以我们宁可保守也不激进。2.3 旁边加一个细节同名符号太多时补全会跑偏这里我想补一个具体的例子。有人反馈在写 Python 时补全出来的建议经常会和当前文件的局部变量风格不搭。排查之后才发现模型在批量补全时看到了同名函数的不同实现产生了混淆。我们的解决办法是在上下文工程层面加了一个符号去重的环节对当前文件里出现的所有函数名和变量名进行作用域分析只保留当前作用域可见的那一版定义。这个逻辑听起来容易真正实现时要处理闭包、嵌套作用域、import 别名花了一整个迭代的时间才稳定下来。3. 技术架构里的几个关键决定3.1 为什么不直接把整个代码库塞给模型很多人在做 AI 编程工具时第一反应是把整个项目目录打包丢给大模型让模型看到所有代码。这个想法在 demo 阶段没问题一旦放到真实项目里就立刻失效一个稍微有点规模的项目代码量随随便便就是几十万行即使模型上下文窗口支持这么大处理时间也等不起。Neovate Code 采用的做法是检索增强生成的变体在本地维护一个基于语法树的代码索引当用户触发补全或提问时先在索引里找出与当前编辑位置最相关的若干代码片段把这些片段拼成上下文再发给模型。这么做的好处是响应速度大幅提升而且因为输入的上下文更精准模型生成的建议质量也更高。代价是需要维护一个和 IDE 同步的索引文件保存时要增量更新这部分花了不少心思。3.2 模型路由本地小模型和云端大模型各管一段在模型选择这个问题上我一开始的想法是能用大模型一把梭的地方绝不手软后来被现实教育了一顿。大模型的参数量确实带来更好的生成质量但它的单次推理延迟会让补全这类高频操作变得不可用。最终 Neovate Code 采用了双轨模型路由本地模型负责补全采用量化后的轻量级模型CPU 上也能跑延迟控制在 200 毫秒左右。质量和云端大模型有差距但胜在快、稳定、不用联网。云端模型负责解释、重构和测试生成这些场景对延迟的敏感度相对低对生成质量要求高交给大模型做更合适。路由层的设计很关键。我们在配置里提供了一组阈值比如当本地模型的置信度低于某个值时自动升级到云端模型。这个阈值调参过程很有意思太低会导致云端请求频繁太高又会让一些明显错误建议直接输出给用户后来我们改成按代码块类型分别设阈值比如循环体和条件分支的置信度阈值就不同。3.3 异步任务调度从卡死到丝滑说一下异步编程这块。Neovate Code 的客户端有一个长时间运行的后台进程负责索引维护、模型请求、缓存管理等任务。如果这些任务全挤在 IDE 主线程里用户体验就是打字卡顿。我借鉴了消息队列的思路把任务按优先级分成几类索引更新是低优先级可以用空闲时间慢慢跑补全请求是高优先级必须立即处理模型请求是中等优先级但需要通过一个并发窗口控制并发数避免同时发出几十个请求把用户带宽打满。这里用到了 Python 的asyncio作为底层框架配合一个线程池处理 CPU 密集的语法分析任务。代码结构上分成了三层接口层接收 IDE 事件调度层决定任务优先级并分发给执行器执行器层调用具体的模型或解析器。这套设计让我们后来增加多文件重构这类重型功能时几乎不需要改动已有架构。3.4 插件协议LSP 之外的自定义扩展Neovate Code 对 IDE 的支持不是为每个 IDE 单独写一套插件而是实现了一个语言服务协议的服务端。标准 LSP 已经能覆盖跳转、补全、诊断这些功能但 AI 补全需要额外的控制信息比如补全的置信度、模型来源、是否流式输出。所以在标准 LSP 之外我们扩展了几条自定义消息用textDocument/neovateCompletion这类命名空间区分。这种做法付出的代价是不同 IDE 插件需要各自处理这些扩展消息但换来的是核心逻辑只需要维护一份整体收益明显更高。4. 和主流 AI 编程工具的差异化对标之后我才确定方向4.1 我试用完一圈之后的心态变化决定开源之前我把市面上能装的 AI 编程插件和软件基本都试了一遍包括几个热度比较高的产品。试完之后的感受很复杂一方面觉得这些工具做得确实不错UI 漂亮、交互流畅另一方面又觉得它们对代码理解这件事的重视程度远远不够。不少工具为了展示效果会把补全触发得很激进你在一个函数里敲了两个字母它就跳出一大段建议看着很酷但真正用过你会发现这些建议经常是从相似的公开代码库里缝合出来的换到你的项目里就和没穿外套一样不合身。Neovate Code 的思路反过来先花时间把你的项目读懂再决定要不要开口。这就好像一个刚来的同事与其一上来就抢着给你提建议不如先用三天把项目文档和代码结构过一遍。4.2 我把这些差异整理成了一张表对比维度Neovate Code 的做法多数同类工具的做法上下文来源语法树索引作用域分析最近编辑文件当前打开文件选中的代码补全触发策略按符号类型和编辑行为动态调整固定触发或过于频繁本地离线能力完整支持本地补全断网可用多数必须联网代码索引全项目增量索引支持跨文件检索通常不建索引或只做浅层索引企业私有化核心引擎开源可完整离线部署大多闭源私有化需商务谈判扩展开发基于 LSP 的自定义协议SDK 提供 Python 接口多数依赖官方市场分发插件这张表不是为了踩别人而是想说明一个事实在设计空间里不同工具的选择完全可以差异很大。Neovate Code 更偏向底层的代码理解基础设施而不是一个单纯的AI 对话窗口。4.3 为什么用户评价它话少但准确这个评价是我从用户群里看到后记下来的。很多工具为了增加存在感几乎每次编辑都会给出建议哪怕只是改了个变量名。Neovate Code 刻意保持了克制只有在代码结构允许且置信度较高的时候才弹补全。为了做到这个克制我在补全触发逻辑上做了很多微调。比如在 Python 里缩进变化和def关键字之后通常是补全的好时机而在一行注释中途就不触发在 C/C 里指针操作符附近触发补全时给模型提供的上下文要额外包含解引用相关的类型信息否则很容易给出类型不匹配的建议。有用户说这样存在感太低刚装上以为坏了但也有用户说用久了之后它给的每一条建议都值得认真看一下。后者正是我想追求的效果。5. 从零开始跑通 Neovate Code5.1 环境要求和基础安装Neovate Code 目前支持 VS Code 1.85 以上版本以及 JetBrains 系 2023.2 以上的 IDE。考虑到很多开发者是在 Windows 上做嵌入式开发Windows 的兼容性我们在发布前专门做了排查包括路径分隔符导致的索引异常问题都处理过。安装很简单直接在 IDE 的插件市场搜索 Neovate Code或者从项目 Release 页面下载对应的安装包。装完之后插件会自动在用户目录下初始化一个本地的代码索引目录默认位置是~/.neovate-code/。注意如果你用的是便携版 IDE 或者修改过配置目录的环境变量建议在设置里手动指定索引目录路径避免出现权限问题导致索引一直建立不成功。项目的前期配置都集中在一个neovate.json文件里。下面是一个典型的配置文件示例{ model: { local_model: auto, cloud_endpoint: , cloud_api_key: , confidence_threshold: 0.75 }, indexing: { include_dirs: [src, lib], exclude_dirs: [build, dist, node_modules], follow_symlinks: false }, completion: { enabled: true, max_tokens: 256, trigger_mode: balanced }, language_specific: { python: { enabled: true }, c_cpp: { enabled: true, include_macros: false } } }如果你不配置cloud_endpoint和cloud_api_key插件会完全工作在本地模式。这个模式下代码解释和重构建议功能会弱一些因为本地小模型实在干不了这个活但补全功能是可以正常用的。5.2 第一次补全用5 位水仙花数体验完整链路为了验证插件是否真的跑通了我建议你新建一个 Python 文件输入下面这行代码def find_armstrong_numbers():然后等一两秒Neovate Code 的补全建议应该会出现在光标处。如果补全建议里自动补出了判断位的逻辑说明本地模型和索引都在正常工作。水仙花数这类经典编程题其实很适合作为测试场景因为它的逻辑结构清晰、不依赖项目上下文模型生成的代码一般不会出大错。如果这一步就出现补全超时或者不弹建议优先检查两件事一是neovate.json里索引目录是否包含了当前项目路径二是 IDE 输出面板里有没有提示模型加载失败。我第一次在 Windows 上跑通这个场景时遇到过一个很奇怪的问题补全在终端窗口里能正常触发但在编辑器里就是没反应。后来排查发现是插件的事件监听没有正确注册到保存事件上导致索引一直停留在初始状态。这个问题在新版本中已经修复但如果你用的是旧版本遇到类似现象可以参考这个排查方向。5.3 断网环境下的离线部署技巧Neovate Code 在断网环境下的表现是我最满意的部分之一。只要模型文件已经在本地补全功能完全不需要网络。模型文件打好包之后大约有 200MB 左右可以拷贝到离线机器上手动指定路径加载。在配置里加这一段就能指定本地模型路径{ model: { local_model: D:/models/neovate-code/quantized.bin } }有一个小细节如果模型文件路径包含中文目录某些 Windows 环境下可能出现加载失败的问题。解决办法是把模型目录放在纯英文路径下或者升级到最新版新版本已经修复了文件路径编码的问题。6. 开源之后踩过的坑这几条是花钱买不来的教训6.1 许可证选型踩坑依赖树里混进了传染性许可证开源之后我做的第一件事是把整个项目的依赖树扫描了一遍确认有没有引入和 Apache 2.0 冲突的依赖。结果还真查出一个问题某个用于语法高亮的组件虽然主许可证是 MIT但它在条件编译分支里引用了少量 GPL 代码按严格意义来说这会传染到整个项目。这个问题让我出了一身冷汗。如果直接带着这个依赖开源短时间内可能没人发现但一旦项目做大被版权方盯上后果会很麻烦。最终我们换掉了那个组件自己在语法高亮这块补了一套实现处理能力和原来的差不多麻烦事却彻底解决了。这件事给所有准备开源的人一个提醒许可证合规不是看主依赖的声明而是要把传递依赖也一起查干净。推荐用 GitHub 的依赖审查功能或者协议扫描工具在每次发布前自动检查一遍。6.2 空白 README 会让项目看起来像个半成品说实话我在第一次开源预览版发布时犯过一个很低级的错误README 写得像给内部同事看的交接文档一句用法请参考 docs就把用户打发了。结果被好几个用户在 issue 里吐槽看不懂这东西是干什么的。后来我花了整整一天重写 README加上了完整的架构图说明、快速开始教程和 FAQ。还有一个体会是读 README 的人大多数是想快速判断这个项目能不能解决我的问题而不是想了解你写了多少代码。所以最前面放一段清晰的使用场景描述比放一堆代码徽章更管用。6.3 用户提的需求80% 都指向同一个底层能力开源之后我收到的前两百个 issue 里有将近一半是功能请求。我以为这些需求会五花八门结果整理之后发现80% 的需求最终都指向同一个底层能力更好的代码语义理解。比如有人想要自动发现相似代码块的功能本质上是要有跨文件的相似度检索能力有人想要根据一个功能描述自动定位到需要修改的代码位置本质上还是要先对代码库做深度分析。这个发现让我明白与其在应用层不断堆功能开关不如把底层语义索引的能力做扎实。所以在最近的版本里我们把大部分精力投在了索引准确率和增量更新速度上而不是急着加新按钮。7. 开发者如何参与到 Neovate Code 中来7.1 对新手友好的第一个 PR文档和翻译如果问我对想贡献的新手有什么建议我会毫不犹豫地说从文档和翻译开始。Neovate Code 的文档目前覆盖了中文和英文但英文的用户手册更新速度明显滞后而且很多专业术语的译法不一致。对于想练手的朋友维护文档翻译是一个绝佳的入门方式它不要求你懂 AST 解析也不需要你会跑模型只需要认真读代码、理解功能逻辑。还有一个很需要人手的方向是示例代码库的建设。我们在设计测试数据时需要覆盖 Python、Java、C/C、Go 等多种语言的典型项目需要大量高质量的示例代码来验证补全和解释功能的准确性。如果你正好有某个语言的工程经验按照仓库里的贡献指南补充示例项目对项目质量的提升帮助很大。7.2 核心代码结构地图从哪个目录开始看我做了一个简化的目录结构说明方便想深入源码的朋友快速定位src/server/核心语言服务负责解析、索引、模型路由。src/client/各类 IDE 的前端插件目前以 VS Code 插件为主。src/model/模型调用和缓存逻辑包括本地模型的加载和量化配置。tests/端到端测试用例包含针对不同语言场景的补全准确率测试。docs/用户文档和协议设计说明。如果你对补全效果不满意建议先看src/server里的触发策略模块和上下文构建模块。触发策略决定什么时候弹补全上下文构建决定模型看到哪些代码去生成建议这两块是补全体验的核心。7.3 贡献流程、代码规范和 Code Review 习惯项目采用的标准流程是 fork pull request 模式要求所有提交通过 CI 检查包括单元测试、Lint 检查和许可证扫描。我们规定了两条红线一是不允许引入未经审批的新依赖二是不允许破坏已有测试用例。Code Review 方面我个人的习惯是目标代码必须有对应的测试。如果提交的是 bug 修复必须附带一个回归测试如果提交的是新功能至少要有两个测试用例覆盖主路径和边界情况。这个要求在开始执行的时候会增加一些开发成本但长远来看它让代码库一直保持着可重构的弹性。7.4 社区治理如何避免开源项目变成一个人的玩具开源项目最怕的就是版本更新靠作者心情issue 回复靠缘分。我们目前建立了一个小型的社区维护组包括了几个活跃贡献者和用户代表重要功能的排期和取舍会在社区讨论里进行而不是我单方面拍板。我特别想强调的一点是开源项目的生命力不取决于初始代码写得多么完美而取决于是否有足够多的人愿意持续参与进来。所以在设计社区规则时我们尽量做到透明每个 PR 的合并理由都会公开说明每个功能请求都会标注当前状态和处理优先级。8. 接下来要做的事和一些真实感受按照目前的计划接下来的几个里程碑会围绕三个方向展开。第一个方向是把嵌入式 C/C 的支持做深。很多人用 Neovate Code 是在调试 STM32 这类单片机项目这些项目的代码常常涉及寄存器操作和宏定义通用编程助手在这种场景下经常会给出一些看似合理但完全不能编译的建议。我们计划在索引层增加对宏展开链的可视化支持并在上下文里把宏定义所在文件的路径一并提供给模型。第二个方向是企业私有化的完整方案。目前核心引擎已经可以离线运行但离开箱即用还有一段距离。我们准备在下一个版本提供一键打包的 Docker 镜像把索引服务、模型网关和文档服务都装进去让企业用户能够在内网环境里用一条命令启动整套服务。第三个方向是多语言支持的扩展。社区里已经有人在问 Rust 和 TypeScript 的深度支持了目前 Rust 可以做到基础的补全但跳转和重构建议还比较薄弱。这块需要依赖 tree-sitter 的语法规则完善工作量不小欢迎对编译原理感兴趣的朋友来共建。最后分享一个我个人的真实感受能把一个项目从自己手里的一堆代码变成一个社区共同维护的东西这个过程比写代码本身更有成就感。如果你对 Neovate Code 有什么想法或者在实际使用中遇到了问题欢迎到项目仓库里开 issue 或者参与讨论。代码已经在那边等着你了。
返回列表