- Implement Markdown parsing for ghost text using Milkdown parser - Add support for both block and inline content in ghost text - Refactor prompt system with comprehensive rules and examples - Adjust LLM parameters: increase temperature to 0.7, add repeat_penalty - Add CSS styles for formatted ghost text (bold, italic, code, links) - Add documentation for Copilot prompt system and ghost text rendering
13 KiB
13 KiB
GitHub Copilot 提示词系统分析
概述
GitHub Copilot 的提示词系统是一个复杂的代码补全引擎,采用声明式组件架构来构建发送给 LLM 的提示词。本文档基于 completions-sample-code/ 目录的源代码分析。
核心架构
flowchart TB
subgraph Input
A[用户光标位置] --> B[CompletionState]
C[当前文档] --> B
D[相似文件] --> B
end
subgraph PromptFactory
B --> E[VirtualPrompt]
E --> F[组件树构建]
end
subgraph Components
F --> G[CompletionsContext]
G --> H[DocumentMarker]
G --> I[Traits]
G --> J[Diagnostics]
G --> K[CodeSnippets]
G --> L[SimilarFiles]
G --> M[RecentEdits]
F --> N[CurrentFile]
end
subgraph Rendering
H --> O[CompletionsPromptRenderer]
I --> O
J --> O
K --> O
L --> O
M --> O
N --> O
O --> P[Prompt对象]
end
subgraph Output
P --> Q[API请求]
Q --> R[LLM补全]
end
1. 提示词基础配置
1.1 Token 限制
// 最大补全长度
export const DEFAULT_MAX_COMPLETION_LENGTH = 500;
// 最大提示词长度 (模型上下文窗口 - 补全长度)
export const DEFAULT_MAX_PROMPT_LENGTH = 8192 - DEFAULT_MAX_COMPLETION_LENGTH;
// 默认代码片段数量
export const DEFAULT_NUM_SNIPPETS = 4;
// 后缀匹配阈值
export const DEFAULT_SUFFIX_MATCH_THRESHOLD = 10;
1.2 提示词分配比例
export const DEFAULT_PROMPT_ALLOCATION_PERCENT = {
prefix: 35, // 光标前代码
suffix: 15, // 光标后代码
stableContext: 35, // 稳定上下文
volatileContext: 15 // 动态上下文
};
2. 语言标记系统
2.1 支持的语言
来源: prompt/src/languageMarker.ts
支持 60+ 种编程语言,每种语言定义了:
lineComment: 单行注释标记 (start, end)markdownLanguageIds: Markdown 代码块语言标识符
示例:
python: {
lineComment: { start: '#', end: '' },
markdownLanguageIds: ['python', 'py', 'gyp'],
},
javascript: {
lineComment: { start: '//', end: '' },
markdownLanguageIds: ['javascript', 'js'],
},
2.2 语言标记生成
// 获取语言标记
export function getLanguageMarker(doc: DocumentInfo): string {
if (dontAddLanguageMarker.indexOf(languageId) === -1 && !hasLanguageMarker(doc)) {
if (languageId in shebangLines) {
return shebangLines[languageId]; // 如 #!/usr/bin/env python3
} else {
return `Language: ${languageId}`;
}
}
return '';
}
// 获取路径标记
export function getPathMarker(doc: DocumentInfo): string {
if (doc.relativePath) {
return `Path: ${doc.relativePath}`;
}
return '';
}
3. 组件系统架构
3.1 声明式组件
来源: prompt/src/components/components.ts
Copilot 使用类似 React 的 JSX 语法来声明提示词组件:
// 基础组件类型
export type PromptElementProps<P = object> = P & Readonly<PromptAttributes & { children?: PromptComponentChildren }>;
// 组件上下文,提供状态管理
export interface ComponentContext {
useState<S>(initialState: S): [S, Dispatch<StateUpdater<S>>];
useData<T>(typePredicate: TypePredicate<T>, consumer: DataConsumer<T>): void;
}
3.2 默认提示词组件结构
来源: lib/src/prompt/completionsPromptFactory/componentsCompletionsPromptFactory.tsx
function defaultCompletionsPrompt(accessor: ServicesAccessor) {
return (
<>
<CompletionsContext>
<DocumentMarker tdms={tdms} weight={0.7} />
<Traits weight={0.6} />
<Diagnostics tdms={tdms} weight={0.65} />
<CodeSnippets tdms={tdms} weight={0.9} />
<SimilarFiles tdms={tdms} instantiationService={instantiationService} weight={0.8} />
<RecentEdits tdms={tdms} recentEditsProvider={recentEditsProvider} weight={0.99} />
</CompletionsContext>
<CurrentFile weight={1} />
</>
);
}
3.3 组件权重说明
| 组件 | 权重 | 说明 |
|---|---|---|
| RecentEdits | 0.99 | 最近编辑内容,最高优先级 |
| CodeSnippets | 0.9 | 代码片段 |
| SimilarFiles | 0.8 | 相似文件内容 |
| DocumentMarker | 0.7 | 文档标记(语言/路径) |
| Diagnostics | 0.65 | 诊断信息(错误/警告) |
| Traits | 0.6 | 代码特征 |
| CurrentFile | 1.0 | 当前文件内容(必须包含) |
4. 当前文件组件
来源: lib/src/prompt/components/currentFile.tsx
4.1 光标前代码 (BeforeCursor)
export function BeforeCursor(props: {
document: CompletionRequestDocument | undefined;
position: Position | undefined;
maxCharacters: number;
}) {
let text = props.document.getText({ start: { line: 0, character: 0 }, end: props.position });
if (text.length > props.maxCharacters) {
text = text.slice(-props.maxCharacters); // 截取最后 maxCharacters 字符
}
return <Text>{text}</Text>;
}
4.2 光标后代码 (AfterCursor)
export function AfterCursor(props: {...}, context: ComponentContext) {
// 获取光标后所有文本
let suffix = props.document.getText({
start: props.position,
end: { line: Number.MAX_VALUE, character: Number.MAX_VALUE },
});
// 后缀缓存机制:使用编辑距离判断是否复用缓存
const dist = findEditDistanceScore(firstSuffixTokens.tokens, cachedSuffixTokens.tokens);
if (100 * dist < suffixMatchThreshold * tokens.length) {
suffixToUse = cachedSuffix; // 使用缓存的后缀
}
return <Text>{suffixToUse}</Text>;
}
5. 相似文件与代码片段
5.1 相似文件选择
来源: prompt/src/snippetInclusion/similarFiles.ts
export interface SimilarFilesOptions {
snippetLength: number; // 代码片段长度(行数)
threshold: number; // 相似度阈值
maxTopSnippets: number; // 最大返回片段数
maxCharPerFile: number; // 每文件最大字符数
maxNumberOfFiles: number; // 最大文件数
maxSnippetsPerFile: number; // 每文件最大片段数
}
// 默认配置
export const defaultSimilarFilesOptions: SimilarFilesOptions = {
snippetLength: 60,
threshold: 0.0,
maxTopSnippets: 4,
maxCharPerFile: 10000,
maxNumberOfFiles: 20,
maxSnippetsPerFile: 1,
};
5.2 Jaccard 相似度匹配
来源: prompt/src/snippetInclusion/selectRelevance.ts
// 使用 Jaccard 相似度计算代码片段相关性
abstract class WindowedMatcher {
protected abstract similarityScore(a: Set<string>, b: Set<string>): number;
// 分词器:将代码转换为 token 集合
class Tokenizer {
tokenize(a: string): Set<string> {
return new Set(splitIntoWords(a).filter(x => !this.stopsForLanguage.has(x)));
}
}
}
5.3 代码片段格式化
来源: prompt/src/snippetInclusion/snippets.ts
export function announceSnippet(snippet: SnippetToAnnounce) {
const headline = snippet.relativePath
? `Compare ${pluralizedSemantics} ${semantics} from ${snippet.relativePath}:`
: `Compare ${pluralizedSemantics} ${semantics}:`;
return { headline, snippet: snippet.snippet };
}
6. API 请求格式
6.1 请求结构
type CompletionRequest = {
prompt: string; // 前缀代码
suffix: string; // 后缀代码
stream: true; // 始终使用流式响应
max_tokens: number; // 最大生成 token 数
n: number; // 并行补全数量
temperature: number; // 温度参数
top_p: number; // nucleus 采样参数
stop: string[]; // 停止标记
logprobs?: number; // logprob 数量
extra: {
language: string; // 语言 ID
trim_by_indentation?: boolean;
force_indent?: number;
next_indent?: number;
prompt_tokens: number;
suffix_tokens: number;
context?: string[]; // 额外上下文
};
};
6.2 停止标记
const stopsForLanguage: { [key: string]: string[] } = {
markdown: ['\n\n\n'],
python: ['\ndef ', '\nclass ', '\nif ', '\n\n#'],
};
export function getStops(languageId?: string) {
return stopsForLanguage[languageId ?? ''] ?? ['\n\n\n', '\n```'];
}
6.3 温度参数
export function getTemperatureForSamples(numShots: number): number {
if (numShots <= 1) return 0.0;
else if (numShots < 10) return 0.2;
else if (numShots < 20) return 0.4;
else return 0.8;
}
7. Tokenization
来源: prompt/src/tokenization/tokenizer.ts
7.1 支持的 Tokenizer
export enum TokenizerName {
cl100k = 'cl100k_base', // GPT-3.5/GPT-4
o200k = 'o200k_base', // GPT-4o
mock = 'mock', // 测试用
}
7.2 Tokenizer 接口
export interface Tokenizer {
tokenLength(text: string): number;
tokenize(text: string): number[];
detokenize(tokens: number[]): string;
tokenizeStrings(text: string): string[];
takeLastTokens(text: string, n: number): { text: string; tokens: number[] };
takeFirstTokens(text: string, n: number): { text: string; tokens: number[] };
takeLastLinesTokens(text: string, n: number): string;
}
8. Tree-sitter 代码解析
8.1 支持的语言
export enum WASMLanguage {
Python = 'python',
JavaScript = 'javascript',
TypeScript = 'typescript',
TSX = 'tsx',
Go = 'go',
Ruby = 'ruby',
CSharp = 'c-sharp',
Java = 'java',
Php = 'php',
Cpp = 'cpp',
}
8.2 用途
- 判断代码块是否为空块开始 (
isEmptyBlockStart) - 判断代码块是否完成 (
isBlockBodyFinished) - 获取语法节点起始位置 (
getNodeStart)
9. 提示词构建流程
sequenceDiagram
participant User as 用户
participant VSCode as VS Code
participant GT as GhostText
participant PF as PromptFactory
participant Components as 组件系统
participant Tokenizer as Tokenizer
participant API as OpenAI API
User->>VSCode: 输入代码
VSCode->>GT: 请求补全
GT->>PF: extractPrompt
PF->>Components: 构建组件树
Components->>Components: DocumentMarker
Components->>Components: Traits
Components->>Components: Diagnostics
Components->>Components: CodeSnippets
Components->>Components: SimilarFiles
Components->>Components: RecentEdits
Components->>Components: CurrentFile
Components->>Tokenizer: 计算 token 数量
Tokenizer-->>Components: 返回 token 数
Components->>Components: Elision 省略处理
Components-->>PF: Prompt 对象
PF-->>GT: PromptResponse
GT->>API: 发送请求
API-->>GT: 流式返回补全
GT-->>VSCode: 显示 Ghost Text
VSCode-->>User: 展示建议
10. 关键设计模式
10.1 声明式组件
使用 JSX 语法声明提示词结构,支持:
- 组件组合
- 权重分配
- 状态管理
- 数据订阅
10.2 虚拟提示词树
在渲染前构建虚拟树结构,支持:
- 增量更新
- 高效 diff
- 条件渲染
10.3 Token 预算管理
- 每个组件有权重属性
- 根据 token 预算动态省略内容
- 优先保留高权重组件
10.4 后缀缓存
- 使用编辑距离判断后缀相似度
- 相似时复用缓存的后缀
- 减少 token 波动,提高缓存命中率
11. 实现参考
如果要在自己的项目中实现类似的提示词系统,需要关注以下核心模块:
- Tokenizer: 使用 tiktoken 进行准确的 token 计数
- 语言标记: 为不同语言生成适当的标记
- 上下文收集: 收集相似文件、最近编辑等上下文
- Token 预算: 动态分配 token 给不同组件
- FIM 格式: 使用 Fill-In-the-Middle 格式发送请求
总结
GitHub Copilot 的提示词系统是一个精心设计的工程系统,核心特点包括:
- 模块化组件架构: 使用声明式组件构建提示词
- 智能上下文选择: 通过 Jaccard 相似度选择相关代码片段
- Token 预算管理: 动态分配 token 给不同优先级的内容
- 多语言支持: 支持 60+ 种编程语言
- Tree-sitter 解析: 精确理解代码结构
- 流式响应: 实时返回补全结果