跳到主要内容

Rust 加速层 与 ACP 外接 agent

前几章讲的是 Lua 侧的循环与改代码。本章讲两块「外援」:① 用 Rust 做 Lua 不擅长的重活;② 用 ACP 协议把别人的 agent CLI 直接当后端。

1. 为什么要 Rust

Lua 跑得动业务逻辑,但有几件事它做起来慢或难:解析整个文件的语法树抽符号、渲染复杂模板、做 tiktoken 分词、把 HTML 转 Markdown。avante 把这四件事做成 Rust crate,通过 mlua 编译成 Neovim 能 require 的 Lua C 模块。

四个 crate(Cargo.toml:12-15[workspace.dependencies] 段;版本 version = "0.1.0" 在根 [workspace.package] 下,Cargo.toml:9,各 crate 用 version.workspace = true 解析到它):

crate干什么Lua 侧调用方
avante-repo-maptree-sitter 抽「文件里有哪些函数/类/枚举」lua/avante/repo_map.lua
avante-templatesminijinja 渲染 .avanterules system promptlua/avante/path.lua(Prompt)
avante-tokenizerstiktoken / HF tokenizer 数 tokenlua/avante/tokenizers.lua
avante-html2md网页抓取后 HTML → Markdownfetch 工具 / html2md.lua

下面挑两个最有代表性的细看。

2. avante-repo-map:给 AI 一张「项目地图」

它解决的小问题: 模型不可能把整个项目几百个文件都塞进上下文。但如果给它一张「每个文件里有哪些函数/类的签名」的地图,它就能判断该去 view 哪个文件。这张地图就是 repo map。

思路: 用 tree-sitter 解析源码,按语言专属的查询(query)把顶层定义抽出来,只留签名(函数名、参数、返回类型、可见性),丢掉函数体。

avante-repo-map(crates/avante-repo-map/src/lib.rs)的结构:

  • 定义了统一的 Definition 枚举:Func / Class / Module / Enum / Variable / Union(lib.rs:44-53),每个带名字、参数、类型、可见性修饰符等(lib.rs:9-42)。
  • get_ts_language(lib.rs:55)按语言名映射到对应 tree-sitter 语法——支持 rust/python/typescript/go/c/cpp/lua/ruby/java/php/zig/scala/swift/elixir/csharp 等十几种(lib.rs:56-73)。
  • 每种语言配一份 .scm 查询文件(include_str! 编进二进制,lib.rs:77-92),用来从语法树里「挑出」定义节点。

Lua 侧调用入口是 stringify_definitions(filetype, content)(在 read_file_toplevel_symbols 工具里用到,llm_tools/init.lua:90),返回该文件的符号清单字符串。

直觉:repo map 像「一本书的目录」——模型先读目录,再决定翻哪一页(view 哪个文件),而不是把整本书背下来。这是 aider 首创、被广泛借鉴的模式。

3. avante-templates:system prompt 是渲染出来的

01 章 说过 prompt 是 Jinja 模板渲染的,这里看实现。avante-templates(crates/avante-templates/src/lib.rs)基于 minijinja:

  • 维护一个全局 Environment(模板环境,lib.rs:7-9),启动时把 .avanterules 模板都加载进去。
  • 定义 TemplateContext 结构(lib.rs:33-49):渲染时 Lua 传进来的所有变量——askcode_langselected_filesselected_coderecently_viewed_filesproject_contextdiagnosticssystem_infomodel_namememorytodosenable_fastapplyuse_react_prompt
  • render(lib.rs:54)按模板名取出模板、注入 context、渲染成最终字符串。

模板本身是组合式的(lua/avante/templates/):agentic.avanterules extends "base.avanterules",再按条件 include 子片段——{% if 'gpt-4.1' in model_name %} 才包含 _gpt4-1-agentic.avanterules{% if not enable_fastapply %} 时才插入「write_to_file vs str_replace 怎么选」那一大段(templates/agentic.avanterules:5-1313-88)。

这就是为什么同一份代码能给很不一样的模型/模式生成贴合的 prompt:模板里用 Jinja 条件按 model_nameenable_fastapply 等开关拼装。

4. 另两个 crate(简述)

  • avante-tokenizers(crates/avante-tokenizers/src/lib.rs,196 行):封装 tiktoken-rs(OpenAI 系)和 HuggingFace tokenizers,给 Lua 提供 encode/count,用于 token 计数(决定何时触发历史压缩等)。Lua 侧 lua/avante/tokenizers.lua
  • avante-html2md:fetch 工具抓网页后,把 HTML 转成干净 Markdown 再喂给模型,避免一堆标签噪声占 token。

5. ACP:把别人的 agent 当后端

这是 avante 的另一条腿。ACP(Agent Client Protocol) 让 avante 不自己跑循环,而是启动一个外部 agent CLI(如 claude-code、gemini-cli、codex),通过 JSON-RPC over stdio 跟它对话——avante 只当前端 + diff 体验,大脑外包。

配置在 config.lua:551acp_providers,默认就内置了一串:

ACP provider启动命令备注
gemini-cligemini --experimental-acpGEMINI_API_KEY
claude-codeclaude-agent-acp透传 ANTHROPIC_*ACP_PERMISSION_MODE=bypassPermissions
codexcodex-acp透传 OPENAI_API_KEY
goose / opencode / kimi-cli各自 ... acp(config.lua:572-594)

走另一条代码路径:Config.provider 命中某个 acp_provider,M._stream(llm.lua:1769-1770)一开头就转去 M._stream_acp(opts)(llm.lua:917),完全绕开前几章讲的 HTTP + provider 解析那套。ACP 路径靠 lua/avante/libs/acp_client.lua 这个 JSON-RPC 客户端跟子进程通信。

ACP 的会话更新(session updates): 外部 agent 通过 ACP 推回各种增量事件,类型定义在 acp_client.lua:166——user_message_chunk / agent_message_chunk / agent_thought_chunk / tool_call / tool_call_update / plan / available_commands_update / current_mode_update 等。avante 把这些事件映射回自己的消息流和工具调用渲染,所以用 claude-code 当后端时,你照样在 Neovim 侧边栏里看到「思考 / 工具调用 / diff」。

权限回调: ACP agent 要改文件时会发权限请求,avante 通过 on_request_permission(acp_client.lua:248)弹出确认,复用 03 章 的确认 UI(经 ui/acp_confirm_adapter.lua 适配)。

avante(前端 + diff 体验)
│ JSON-RPC over stdio

外部 agent CLI 子进程(claude-code / gemini-cli / codex …)
│ session updates: 思考块 / 消息块 / tool_call / plan …

avante 把这些事件渲染回侧边栏 + 权限请求弹确认

战略意义:avante 不跟 claude-code 这类强 agent 正面竞争「谁的循环更聪明」,而是把它们吸纳进来当后端,自己专注做「Neovim 里最好的 AI 编辑体验」。README 的 Zen Mode 段落把这点说得很直白:Vim 的编辑能力 + ACP 借来的 agent 能力,二者都要。

6. 边界

  • Rust crate 是可选编译的:没 build 出 avante_templates 等模块时,相关功能会报「Make sure to build avante」(如 path.lua:293),Lua 侧有降级路径但能力受限。
  • ACP 后端能力取决于外部 CLI;avante 这边主要负责协议对接与 UI 映射,agent 的实际行为不由 avante 控制。
  • 本章对 _stream_acp / acp_client.lua 内部的逐条消息处理只覆盖到类型定义与分发层面;更细的状态机请直接读 acp_client.lua_continue_stream_acp

7. 代码地图

主题文件符号
tree-sitter 抽符号(repo map)crates/avante-repo-map/src/lib.rsDefinitionget_ts_languagestringify_definitions
repo map Lua 封装lua/avante/repo_map.luaRepoMap.stringify_definitions
Jinja 模板渲染crates/avante-templates/src/lib.rsrenderTemplateContext
模板 Lua 入口lua/avante/path.luaPrompt.get_templates_dirPrompt.initialize
agentic system prompt 模板lua/avante/templates/agentic.avanterulesextends base / block extra_prompt
分词crates/avante-tokenizers/src/lib.rslua/avante/tokenizers.lua
ACP 分流入口lua/avante/llm.luaM._stream_acpM._continue_stream_acp
ACP JSON-RPC 客户端lua/avante/libs/acp_client.luaACPClient:new、session update 类型
ACP 默认 provider 表lua/avante/config.luaM.defaults.acp_providers