Tabby tabby-agent 智能应用定位提示词解析:provide-smart-apply-line-range 与 Smart Apply 实现原理
【免费下载链接】tabbySelf-hosted AI coding assistant项目地址: https://gitcode.com/GitHub_Trending/tab/tabby
导读
provide-smart-apply-line-range.md是 tabby-agent(Tabby 的 LSP 智能体)中负责"代码落点定位"的核心提示词模板:当用户要求把一段新代码应用(apply)到当前文件时,模型需要先判断这段代码应该落在文件的哪个位置,并返回一个与待插入代码长度最接近的连续代码段行号区间。本文以该提示词为骨架,完整拆解其输入格式、输出协议与约束规则,并结合smartApply.ts、smartRange.ts、protocol.ts等源码,还原它在 Smart Apply 全流程中的真实调用链、解析逻辑与配置方式,帮助你理解如何定制这套提示词或在其基础上构建类似的"代码落点定位"能力。
一、提示词在 Smart Apply 流程中的定位
在 tabby-agent 中,"智能应用"(Smart Apply)是一条完整的 LSP 请求链路:客户端(如 IDE 插件)发起tabby/chat/smartApply请求,服务端经过范围定位 → 生成编辑 → 流式应用三个阶段完成对文档的修改。provide-smart-apply-line-range.md正是第一阶段"范围定位"的提示词模板。
从 protocol.ts 可以看到该请求的完整定义:
- 方法名:
tabby/chat/smartApply - 参数:
SmartApplyParams = { location: Location; text: string } - 返回:
boolean - 可能错误:
ChatFeatureNotAvailableError、ChatEditDocumentTooLongError、ChatEditMutexError
请求携带了目标文档的location和待应用的新代码文本text。服务端拿到后,在 smartApply.ts 的provideSmartApplyEdit中执行两步定位:
let applyRange = getSmartApplyRange(document, params.text); //if cannot find range, lets use backend LLMs if (!applyRange) { applyRange = await provideSmartApplyLineRange(document, params.text, ...); }即:先尝试本地模糊匹配(快速路径),匹配失败才调用 LLM(慢速兜底路径)。provide-smart-apply-line-range.md就是兜底路径所使用的提示词,因此它在整个流程中承担着"最后一道范围判定"的角色,直接决定后续编辑作用于文档的哪一段。
二、提示词的角色设定与任务目标
提示词开篇即明确模型身份:
You are an AI assistant specialized in determining the most appropriate location to insert new code into an existing file.
它要求模型扮演一个代码插入位置判定专家,输入是"现有文件内容 + 待插入代码",输出是"文件中一个与待插入代码长度最相似的连续代码段的行号区间"。
任务拆解为三个步骤:
- 分析现有代码结构与待插入的新代码;
- 找出与待插入代码长度最相似的一段连续现有代码;
- 仅返回该相似长度段的行号区间。
注意关键词"length"(长度):该提示词采用的定位策略并非语义相似度,而是长度(行数)相似度——这与后面源码中的模糊匹配策略在思路上是一致的(详见第五节)。这种策略的合理性在于:Smart Apply 常见的场景是"在相同结构处追加并列代码块",例如在if (method === "debug") {...}之后追加一个if (method === "add") {...},此时与目标代码结构最相似的既有代码段往往就是正确的插入锚点。
三、输入格式约定:带行号的逐行文档
提示词规定了输入侧的严格格式:
The file content is provided line by line, with each line in the format:
line number | code
即文档内容必须逐行呈现,每行前缀为该行的行号,用竖线|分隔代码本身。例如13 | target.trace(tagMessage(message), ...args);。
在源码实现中,这个格式由 smartApply.ts 的provideSmartApplyLineRange负责构造:
const documentText = document .getText() .split("\n") .map((line, idx) => `${idx + 1} | ${line}`) .join("\n");可以看到它用 1-based 行号(idx + 1)逐行拼装,与提示词要求的 "one-based (starting from 1)" 完全一致。
待插入的新代码则通过 XML 标签包裹传入:
The new code to be inserted is provided in
<APPLYCODE></APPLYCODE>XML tags.
对应的模板占位符为{{applyCode}},文档内容占位符为{{document}}。填充逻辑同样位于provideSmartApplyLineRange中:
content: promptTemplate.replace(/{{document}}|{{applyCode}}/g, (pattern: string) => { switch (pattern) { case "{{document}}": return documentText; case "{{applyCode}}": return applyCodeBlock; ... } }),四、输出协议:GENERATEDCODE 标签与闭区间行号
提示词对输出格式的要求极为严格,这是本模板最值得注意的工程化设计——结构化输出,禁止任何多余解释:
You must reply with ONLY the suggested range in the format
startLine-endLine, enclosed in<GENERATEDCODE></GENERATEDCODE>XML tags. Do not include any explanation, existing code, or the code to be inserted in your response.
格式要点:
- 输出唯一内容:
<GENERATEDCODE>startLine-endLine</GENERATEDCODE>; - 行号为 1-based(从 1 开始);
startLine与endLine均为闭区间(inclusive closed interval);- 区间必须覆盖一段与待插入代码长度相似的连续代码段。
提示词中给出的输出示例为:
<GENERATEDCODE>10-12</GENERATEDCODE>(对应在 10-12 行发现了一段与 3 行新代码长度相似的代码段。)
这一输出协议在源码端有精确的解析逻辑与之配套。smartApply.ts 中:
const regex = /<GENERATEDCODE>(.*?)<\/GENERATEDCODE>/s; const match = response.match(regex); if (match && match[1]) { response = match[1].trim(); } const range = response.split("-"); if (range.length !== 2) { return undefined; } const startLine = parseInt(range[0] ?? "0", 10) - 1; const endLine = parseInt(range[1] ?? "0", 10) - 1; return { range: { start: { line: startLine < 0 ? 0 : startLine, character: 0 }, end: { line: endLine < 0 ? 0 : endLine, character: Number.MAX_SAFE_INTEGER }, }, action: startLine == endLine ? "insert" : "replace", };解析逻辑的几个关键点:
- 用正则
/s标志匹配<GENERATEDCODE>标签内的任意内容并提取; - 按
-拆分为两段,若段数不为 2 则判定失败(返回undefined,整个 Smart Apply 静默放弃); - 1-based 行号转 0-based(
- 1),并做负数保护(< 0 ? 0); - 行为判定规则:
startLine == endLine时动作为insert(插入),否则为replace(替换)——即"单个行号区间"代表纯插入,"跨行区间"代表替换该段代码。这个判定与 smartRange.ts 中快速路径的判定逻辑保持了一致的设计哲学。
五、示例学习机制:EXAMPLE_DOCUMENT 与 EXAMPLE_APPLYCODE
提示词中段通过两个 XML 标签引入了少样本示例(few-shot examples):
<EXAMPLE_DOCUMENT></EXAMPLE_DOCUMENT>XML tags indicate the example code document.<EXAMPLE_APPLYCODE>XML tags indicate the example code to be applied.
示例内容为一个 TypeScript 的日志方法分发代码段:
EXAMPLE_DOCUMENT:包含 13-25 行带行号的方法分发代码(trace/debug/info分支);EXAMPLE_APPLYCODE:一个 4 行的新分支代码:
if (method === "add") { return (message: string, ...args: unknown[]) => { target.error(tagMessage(message), ...args); }; }随后提示词给出两条标注答案:
- If a 4-line segment similar to the apply code is found at lines 16-19, return:
<GENERATEDCODE>16-19</GENERATEDCODE>- If a 4-line segment similar to the apply code is found at lines 21-24, return:
<GENERATEDCODE>21-24</GENERATEDCODE>
这两条答案的价值在于:同一份新代码可能对应多个合法落点,只要返回的区间长度与目标代码匹配且落在现有代码的连续段上,都属于可接受答案。它向模型传递了"定位结果是候选而非唯一解"的松弛语义,有效避免模型在存在多个相似段落时陷入"选错即错"的过度纠结。
六、快速路径:基于 Levenshtein 距离的本地模糊匹配
理解了 LLM 兜底路径后,再看快速路径getSmartApplyRange(smartRange.ts),你会发现两者在设计上互为表里:
function fuzzyApplyRange(document: TextDocument, snippet: string): { range: Range; score: number } | null { const lines = document.getText().split("\n"); const snippetLines = snippet.split("\n"); let [minDistance, index] = [Number.MAX_SAFE_INTEGER, 0]; for (let i = 0; i <= lines.length - snippetLines.length; i++) { const window = lines.slice(i, i + snippetLines.length).join("\n"); const distance = levenshtein(window, snippet); if (minDistance >= distance) { minDistance = distance; index = i; } } ... }实现要点:
- 用滑动窗口遍历文档的每一行起点,取与待插入代码等长的窗口与目标代码计算Levenshtein 编辑距离;
- 距离最小者即"长度相同且内容最接近"的锚点段;
- 返回 0-based 闭区间(
startLine = index,endLine = index + snippetLines.length - 1); - 若
minDistance未更新(窗口根本不存在,即文档行数少于目标代码行数),返回null触发 LLM 兜底。
有趣的是,快速路径用"等长窗口 + 编辑距离"寻找内容相似段,而 LLM 提示词寻找长度相似段——两者都是从"锚定一段既有代码"的角度出发,只是相似度度量不同。这也解释了为什么provide-smart-apply-line-range特别强调 "similar in length":它要复刻快速路径的行为模式,只是放开了"内容相似"的约束,允许模型基于对代码结构的语义理解做出更灵活的落点判断。
七、两级定位后的编辑生成与流式应用
范围定位成功后,流程进入第二阶段:provideSmartApplyEditLLM(smartApply.ts)会使用另一个提示词模板 generate-smart-apply.md(占位符{{document}}/{{code}})生成实际编辑内容。
这一阶段的关键工程细节:
- 文档长度裁剪:若文档总长超过
config.chat.edit.documentMaxChars,会以选中区间为中心,对前后缀做对称截断(documentPrefix/documentSuffix各保留剩余额度的一半),确保送入 LLM 的上下文在预算内(smartApply.ts); - 编辑互斥:通过
mutexAbortController保证同一时刻只有一个 Smart Edit 在途,重复触发会抛出ChatEditMutexError(smartApply.ts); - 流式应用:生成结果经
readResponseStream流式回传,依据config.chat.edit.responseDocumentTag(默认["<GENERATEDCODE>", "</GENERATEDCODE>"])识别最终代码段(config/default.ts),并通过workspace/applyEdit写入编辑器; - 区间校正:调用编辑阶段时,将结束位置从
end.line修正为end.line + 1行首,以容纳整段代码的替换/插入(smartApply.ts)。
八、配置项与自定义方式
provide-smart-apply-line-range.md作为提示词模板,已内置在 tabby-agent 的默认配置中。相关配置链如下:
- 类型定义:type.d.ts 声明
chat.smartApplyLineRange.promptTemplate: string与chat.smartApply.promptTemplate: string; - 默认值:config/default.ts 将
chat.smartApplyLineRange.promptTemplate绑定为provideSmartApplyLineRangePrompt(即本文剖析的模板),chat.smartApply.promptTemplate绑定为generateSmartApplyPrompt; - 模板加载:prompts 目录下的
.md文件通过 index.d.ts 的模块声明(declare module "*.md")被 TypeScript 作为字符串模块导入。
因此,若想调整"落点定位"的行为,可以在配置文件中覆盖chat.smartApplyLineRange.promptTemplate,替换为自定义提示词。自定义时需要注意维持源码所依赖的接口契约,否则会破坏解析:
- 输出必须包含
<GENERATEDCODE>startLine-endLine</GENERATEDCODE>形式的唯一响应; - 行号必须 1-based、闭区间;
- 必须使用
{{document}}与{{applyCode}}占位符接收输入(见provideSmartApplyLineRange中的正则替换逻辑,smartApply.ts)。
九、失败模式与容错设计
整条链路对模型输出的异常有完善的兜底,理解这些边界有助于你在自研类似功能时规避同类坑点:
| 失败场景 | 处理位置 | 行为 |
|---|---|---|
| 快速路径找不到等长窗口 | smartRange.ts | 返回null,转入 LLM 兜底 |
LLM 响应中无<GENERATEDCODE>标签 | smartApply.ts | 正则匹配失败,match[1]为空,最终返回undefined |
响应格式非法(非a-b) | smartApply.ts | split("-")长度不为 2,返回undefined |
| 文档不存在 / LSP 连接断开 | smartApply.ts | 直接返回false,静默失败 |
| Chat 功能不可用(无 chat_model) | smartApply.ts | 抛出ChatFeatureNotAvailableError |
| 编辑期间并发冲突 | smartApply.ts | 抛出ChatEditMutexError |
| 编辑目标超长 | smartApply.ts | 抛出ChatEditDocumentTooLongError |
可见该提示词的设计始终围绕"机器可解析、失败可降级"展开:约束输出格式、提供少样本示例、允许候选多解、配合多层容错,这正是生产级 LLM 功能中提示词工程的关键范式。
结语
provide-smart-apply-line-range.md表面是一份只有 60 余行的提示词,实则完整定义了 tabby-agent Smart Apply 的落点定位协议:输入侧约定带行号的逐行文档与{{applyCode}}占位符,输出侧约定<GENERATEDCODE>闭区间格式与 insert/replace 语义,中间以长度相似段为定位目标并辅以少样本示例松弛候选约束。配合 smartRange.ts 的 Levenshtein 快速路径与 smartApply.ts 的解析、互斥、流式应用逻辑,构成了"本地优先、LLM 兜底、失败静默"的完整工程闭环。若你正在为代码编辑类 AI 功能设计"定位锚点"提示词,本文剖析的这套输入/输出协议与容错设计可直接复用。
【免费下载链接】tabbySelf-hosted AI coding assistant项目地址: https://gitcode.com/GitHub_Trending/tab/tabby
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考