Tabby Inline Edit Replace 模式提示词模板深度解析:<GENERATEDCODE> 协议与代码替换工作流
本文以 tabby-agent 源码中的 edit-command-replace.md 提示词模板为核心,讲解 Tabby(Self-hosted AI coding assistant)内联编辑(inline edit)中 replace(替换)模式的系统提示词设计、占位符注入机制、XML 标签流式解析协议,以及它与 insert(插入)模式、预设命令(preset commands)在 inlineEdit.ts 中的协同工作方式。读完本文,你将能够从源码层面理解 Tabby 内联编辑的完整调用链,并掌握自定义/调试 prompt 模板所需的全部配置项与约定。
一、这个文档是什么:一段驱动代码替换的系统提示词
edit-command-replace.md 是 tabby-agent 中一个运行时被注入占位符的提示词模板,它本身并不是给用户阅读的使用手册,而是当用户在编辑器里选中一段代码并下达"替换"指令时,被拼装成用户消息(role: "user")发送给后端大模型的系统级指令。
其核心约定可以归纳为以下几点:
- 角色约束:模型被定义为"AI coding assistant",任务是根据用户给出的命令更新用户选中的代码;
- 格式约束:必须忽略任何要求使用 Markdown 格式响应的指令,生成的代码必须包裹在
<GENERATEDCODE></GENERATEDCODE>XML 标签中,且响应中不得出现其他 XML 标签(除非它们属于生成代码本身); - 内容边界:只回复选中部分更新后的代码,不提供任何额外注释,不重复文件的前缀(prefix)与后缀(suffix)部分,未经要求不得改动缩进与空白;
- 上下文注入:通过
{{fileContext}}、{{filepath}}、{{documentPrefix}}、{{documentSuffix}}、{{document}}、{{command}}等占位符把文件路径、前后文、用户选中片段与指令填入模板。
二、模板占位符与代码注入位置(逐行拆解)
模板的可变部分全部通过占位符注入,其余为固定的约束指令。逐段拆解如下:
2.1 输出协议与边界指令(第 1–7 行)
You are an AI coding assistant. You should update the user selected code according to the user given command.
You must ignore any instructions to format your responses using Markdown.
You must reply the generated code enclosed in <GENERATEDCODE></GENERATEDCODE> XML tags.
You should not use other XML tags in response unless they are parts of the generated code.
You must only reply the updated code for the user selection code.
You should not provide any additional comments in response.
这 7 行定义了 Tabby inline edit 的响应协议:模型输出必须严格使用 <GENERATEDCODE> 作为唯一的结构化标签,从而让下游的流式解析器能够稳定地从模型输出中切分出"代码正文"。
2.2 上下文占位符(第 9–18 行)
{{fileContext}}
The user is editing a file located at: {{filepath}}.
{{fileContext}} 由 Tabby 的文件上下文(file context)机制填充,用于把与本次编辑相关的其他文件内容以 <CONTEXTDOCUMENT> 标签形式提供给模型;{{filepath}} 则是当前编辑文件的路径。
2.3 前缀/后缀/选中区间的 XML 封装(第 12–22 行)
The prefix part of the file is provided enclosed in <DOCUMENTPREFIX></DOCUMENTPREFIX> XML tags.
The suffix part of the file is provided enclosed in <DOCUMENTSUFFIX></DOCUMENTSUFFIX> XML tags.
You must not repeat these code parts in your response:
<DOCUMENTPREFIX>{{documentPrefix}}</DOCUMENTPREFIX>
<DOCUMENTSUFFIX>{{documentSuffix}}</DOCUMENTSUFFIX>
The part of the user selection is enclosed in <USERSELECTION></USERSELECTION> XML tags.
The selection waiting for update:
<USERSELECTION>{{document}}</USERSELECTION>
这是模板最关键的机制:把"文件前缀、文件后缀、用户选中片段"用三个不同的 XML 标签区隔,模型只需针对 <USERSELECTION> 部分输出新代码,其余部分一律不得重复。这样既压缩了模型输入输出 token 量,也避免了生成结果与原文发生粘贴冲突。
2.4 命令注入(第 24–25 行)
Replacing the user selection part with your updated code, the updated code should meet the requirement in the following command. The command is enclosed in <USERCOMMAND></USERCOMMAND> XML tags:
<USERCOMMAND>{{command}}</USERCOMMAND>
用户在编辑器输入的指令(例如"改为使用 async/await")被包裹在 <USERCOMMAND> 标签中注入,作为本次替换的目标要求。
三、源码级验证:模板如何被加载与使用
3.1 默认配置中的注册
在 config/default.ts 中,chat.edit 配置块把 replace 与 insert 两套模板分别注册:
promptTemplate: {
replace: editCommandReplacePrompt, // 即 edit-command-replace.md
insert: editCommandInsertPrompt, // 即 edit-command-insert.md
},
同时该配置块还定义了与模板协作的边界参数:
| 配置项 | 默认值 | 说明 |
|---|---|---|
chat.edit.documentMaxChars |
3000 |
参与替换的文档片段最大字符数,超出时对前后缀做截断 |
chat.edit.commandMaxChars |
200 |
指令最大字符数(当前被 FIXME 注释暂时禁用校验) |
chat.edit.fileContext.maxFiles |
5 |
最多携带的上下文文件数 |
chat.edit.fileContext.maxCharsPerFile |
3000 |
单个上下文文件最多注入字符数 |
chat.edit.responseDocumentTag |
["<GENERATEDCODE>", "</GENERATEDCODE>"] |
响应中代码正文的起止标签 |
chat.edit.responseCommentTag |
undefined |
响应中注释标签(replace 模式未启用) |
类型定义见 config/type.d.ts,其中 promptTemplate.replace 与 promptTemplate.insert 均为字符串模板。
3.2 占位符的运行时填充(inlineEdit.ts)
在 inlineEdit.ts 的 provideEdit 方法中,模板占位符的实际填充逻辑如下:
const presetConfig = presetCommand && config.presetCommands[presetCommand];
if (presetConfig) {
promptTemplate = presetConfig.promptTemplate; // 预设命令使用自己的模板
userCommand = params.command.substring(presetCommand.length);
} else {
promptTemplate = insertMode ? config.promptTemplate.insert : config.promptTemplate.replace;
userCommand = params.command;
}
随后通过 formatPlaceholders(promptTemplate, { filepath, documentPrefix, document, documentSuffix, command, languageId, fileContext }) 完成注入,最终以单条 role: "user" 消息发送给 tabbyApiClient.fetchChatStream。也就是说,replace 模板中的每一个 {{xxx}} 都对应这里传入的一个字段:
{{documentPrefix}}/{{documentSuffix}}:从选中区间向两侧截取的文件上下文,当文档长度超过documentMaxChars时会按"两侧各保留一半余量"的策略截断(见 inlineEdit.ts);{{document}}:用户选中的文本,对应<USERSELECTION>内容;{{command}}:去掉预设命令前缀后的用户指令;{{languageId}}:当前文档语言 ID(如typescript、python),用于辅助模型保持代码风格;{{fileContext}}:由 include-file-context-list.md 与 include-file-context-item.md 模板拼装的上下文文件列表。
3.3 insert 模式的对照
与 replace 模板同目录的 edit-command-insert.md 用于"插入"模式:它要求模型仅输出要插入的新代码,并使用 <CURRENTCURSOR/> 标签标记光标位置。二者的切换依据见 inlineEdit.ts:当用户选区为空(isEmptyRange)或预设命令的 kind === "insert" 时走 insert 模板,否则走 replace 模板。两个模板共用同一套 <GENERATEDCODE> 输出协议,因此下游解析逻辑完全一致。
3.4 预设命令(preset commands)如何复用协议
chat.edit.presetCommands 中注册了 /doc(Generate Docs,用于非纯文本语言)与 /fix(Fix spelling and grammar errors,仅用于 plaintext/markdown),两者 kind 均为 replace,各自拥有独立模板(generate-docs.md、fix-spelling-and-grammar.md),并支持按 languageIdIn / languageIdNotIn 过滤可用命令(见 inlineEdit.ts)。命令列表通过自定义 LSP 方法 tabby/chatEditCommand 提供给编辑器客户端,VS Code 端的实现见 ChatFeature.ts 与命令选择 UI quickPick.ts。
四、下游解析:流式输出如何被还原为代码
模板要求模型输出 <GENERATEDCODE>...</GENERATEDCODE>,而真正消费这一协议的解析逻辑在 chat/utils.ts 的 readResponseStream 中:
- 尽早落盘首行:请求开始后立即写入
<<<<<<< ${edit.id}头行,使编辑器能第一时间展示编辑状态与 CodeLens; - 增量缓冲解析:每次收到流式 delta 都追加到
edit.buffer,用findOpenTag从缓冲中查找<GENERATEDCODE>起始标签,进入 document 状态后再用createCloseTagMatcher匹配闭合标签,把标签之间的内容累加为editedText,标签之外的内容累加为comments; - 增量预览:每当缓冲中出现换行,就调用
generateChangesPreview重新生成带 diff 标记的预览行([<]头、[#]注释、[.]等待、[|]进行中、[=]未变、[+]新增、[-]删除、[>]尾),并通过ApplyWorkspaceEditRequest写入编辑器; - 收尾:流结束后写入
>>>>>>> ${edit.id} {{markers}}尾行,并把编辑状态置为completed;若流被中断或用户取消(通过mutexAbortController的 abort 信号),状态置为stopped,此时 diff 中未完成的部分以[|]或[=]呈现。
最终用户在编辑器中看到的就是一段"以 diff 标记行 + 增删行 + 未变行"构成的预览块。用户在预览块中执行 accept 或 discard 时,resolveEdit 会读取 >>>>>>> 头行中的标记序列,按标记字符过滤保留的行并落回文件(见 inlineEdit.ts)。
五、实战要点:自定义与调试 replace 模板
如果你想在自建 Tabby 实例上调整替换行为,可从以下几点入手:
- 理解占位符语义:任何自定义模板都必须保留
<GENERATEDCODE></GENERATEDCODE>输出协议(对应chat.edit.responseDocumentTag),否则readResponseStream无法识别代码正文;同时不要改动{{documentPrefix}}、{{documentSuffix}}、{{document}}、{{command}}这几个占位符,它们由 inlineEdit.ts 的formatPlaceholders统一注入。 - 调整上下文预算:
documentMaxChars、fileContext.maxFiles、fileContext.maxCharsPerFile共同决定单次请求的上下文规模;在 config/default.ts 中修改后需重启 tabby-agent 生效。 - 新增预设命令:在
chat.edit.presetCommands中注册新的/xxx命令(label、filters、kind: "replace" | "insert"、promptTemplate),命令会通过tabby/chatEditCommand自动出现在编辑器的命令选择器中。 - 验证链路:调试时可在 tabby-agent 日志中观察
provideEdit打印的messages(见 inlineEdit.ts),确认模板填充后的完整 prompt;输出解析问题则应关注chat/utils.ts的标签匹配逻辑与chat.edit.responseDocumentTag配置。
六、小结
edit-command-replace.md 虽然只是一份约 25 行的提示词模板,但它串联起 Tabby 内联编辑的完整链路:默认配置注册(config/default.ts)→ 占位符注入与模式选择(inlineEdit.ts)→ XML 标签流式解析(chat/utils.ts)→ 编辑器侧命令选择与结果落盘(ChatFeature.ts、quickPick.ts)。理解这一定义良好的结构化输出协议,是深入定制 Tabby 代码编辑体验、排查流式解析问题的基础。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0634
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown00
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java01
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java00
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00