数据截至 (上游 commit 1a36a37b1a96)
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-map | tree-sitter 抽「文件里有哪些函数/类/枚举」 | lua/avante/repo_map.lua |
avante-templates | minijinja 渲染 .avanterules system prompt | lua/avante/path.lua(Prompt) |
avante-tokenizers | tiktoken / HF tokenizer 数 token | lua/avante/tokenizers.lua |
avante-html2md | 网页抓取后 HTML → Markdown | fetch 工具 / 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:86),返回该文件的符号清单字符串。
直觉: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 传进来的所有变量——ask、code_lang、selected_files、selected_code、recently_viewed_files、project_context、diagnostics、system_info、model_name、memory、todos、enable_fastapply、use_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-13、13-88)。
这就是为什么同一份代码能给很不一样的模型/模式生成贴合的 prompt:模板里用 Jinja 条件按
model_name、enable_fastapply等开关拼装。
4. 另两个 crate(简述)
- avante-tokenizers(
crates/avante-tokenizers/src/lib.rs,196 行):封装tiktoken-rs(OpenAI 系)和 HuggingFacetokenizers,给 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:545 的 acp_providers,默认就内置了一串:
| ACP provider | 启动命令 | 备注 |
|---|---|---|
gemini-cli | gemini --experimental-acp | 用 GEMINI_API_KEY |
claude-code | claude-agent-acp | 透传 ANTHROPIC_*、ACP_PERMISSION_MODE=bypassPermissions |
codex | codex-acp | 透传 OPENAI_API_KEY |
goose / opencode / kimi-cli | 各自 ... acp | (config.lua:566-588) |
走另一条代码路径: 当 Config.provider 命中某个 acp_provider,M._stream(llm.lua:1781-1782)一开头就转去 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:188——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:270)弹出确认,复用 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.rs | Definition、get_ts_language、stringify_definitions |
| repo map Lua 封装 | lua/avante/repo_map.lua | RepoMap.stringify_definitions |
| Jinja 模板渲染 | crates/avante-templates/src/lib.rs | render、TemplateContext |
| 模板 Lua 入口 | lua/avante/path.lua | Prompt.get_templates_dir、Prompt.initialize |
| agentic system prompt 模板 | lua/avante/templates/agentic.avanterules | extends base / block extra_prompt |
| 分词 | crates/avante-tokenizers/src/lib.rs、lua/avante/tokenizers.lua | — |
| ACP 分流入口 | lua/avante/llm.lua | M._stream_acp、M._continue_stream_acp |
| ACP JSON-RPC 客户端 | lua/avante/libs/acp_client.lua | ACPClient:new、session update 类型 |
| ACP 默认 provider 表 | lua/avante/config.lua | M.defaults.acp_providers |